[
  {
    "path": ".agents/skills/add-package/SKILL.md",
    "content": "---\nname: add-package\ndescription: Create or align a package in the Remix monorepo to match existing package conventions. Use when adding a brand new package under packages/, or when fixing an existing package's structure, test setup, TypeScript/build config, code style, and README layout to match the rest of Remix 3.\n---\n\n# Add Package\n\n## Overview\n\nUse this skill to scaffold and standardize packages so they look and behave like the existing `@remix-run/*` packages.\nFollow this exactly when creating package files, public exports, tests, and docs.\n\n## Workflow\n\n1. Create the package directory and baseline files.\n\n- Create `packages/<package-name>/`.\n- Add:\n  - `package.json`\n  - `tsconfig.json`\n  - `tsconfig.build.json`\n  - `CHANGELOG.md`\n  - `README.md`\n  - `LICENSE`\n  - `.changes/README.md`\n  - `src/`\n- For new packages, start `CHANGELOG.md` with `## Unreleased` as the first section to indicate changes are not released yet.\n\n2. Set up `package.json` using monorepo conventions.\n\n- Use:\n  - `name`: `@remix-run/<package-name>`\n  - `version` (for brand-new packages): `\"0.0.0\"`\n  - `type`: `\"module\"`\n  - `license`: `\"MIT\"`\n  - `repository.directory`: `packages/<package-name>`\n  - `homepage`: `https://github.com/remix-run/remix/tree/main/packages/<package-name>#readme`\n- Include `files`:\n  - `LICENSE`\n  - `README.md`\n  - `dist`\n  - `src`\n  - `!src/**/*.test.ts`\n- Add standard scripts:\n  - `build`: `tsgo -p tsconfig.build.json`\n  - `clean`: `git clean -fdX`\n  - `prepublishOnly`: `pnpm run build`\n  - `test`: `node --disable-warning=ExperimentalWarning --test`\n  - `typecheck`: `tsgo --noEmit`\n- Use baseline dev dependencies:\n  - `\"@types/node\": \"catalog:\"`\n  - `\"@typescript/native-preview\": \"catalog:\"`\n- Add `keywords` like existing packages (short, lowercase, feature-focused).\n\n3. Define exports with `src` entry files only.\n\n- In `exports`, map each public subpath to a dedicated file in `src`.\n- Always include `./package.json`.\n- Mirror each export in `publishConfig.exports` with `dist` output:\n  - `types`: `./dist/<entry>.d.ts`\n  - `default`: `./dist/<entry>.js`\n- Rule: every export must have a `src` file that re-exports from `src/lib`.\n  - Example: export `./foo` -> `src/foo.ts` -> `export { ... } from './lib/foo.ts'`\n\n4. Add TypeScript config files with shared defaults.\n\nUse this `tsconfig.json` pattern:\n\n```json\n{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  }\n}\n```\n\nUse this `tsconfig.build.json` pattern:\n\n```json\n{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\"]\n}\n```\n\n5. Implement source structure and test setup.\n\n- Structure source as:\n  - `src/<entry>.ts` for public entry points\n  - `src/lib/*.ts` for implementation\n  - `src/lib/*.test.ts` for tests (colocated with implementation)\n- Tests use Node's built-in test runner:\n  - `import * as assert from 'node:assert/strict'`\n  - `import { describe, it } from 'node:test'`\n- Keep tests IDE-friendly:\n  - Do not generate tests with loops/conditionals inside `describe()`.\n\n6. Follow monorepo code style rules while implementing.\n\n- Use `import type { ... }` and `export type { ... }` for types.\n- Include `.ts` extensions in relative imports.\n- Prefer `let` for locals; use `const` only at module scope.\n- Never use `var`.\n- Prefer function declarations/expressions for normal functions.\n- Use arrow functions for callbacks; use concise callbacks when returning a single expression.\n- Use object method shorthand (`method() {}`) instead of arrow properties.\n- Use native class fields and `#private` members.\n- Avoid Node-specific APIs when Web APIs are available.\n\n7. Write README in the same style and section order as existing packages.\n\n- Start with:\n  - `# <package-name>`\n  - One short paragraph describing purpose.\n- Typical section order:\n  - `## Features`\n  - `## Installation`\n  - `## Usage`\n  - Optional deep-dive sections (only if needed)\n  - `## Related Packages` (if applicable)\n  - `## License`\n- Installation instructions must always include installing the `remix` package.\n- If using the package requires a peer dependency, installation instructions must also include that peer dependency in the command.\n- Preferred installation pattern:\n\n```sh\nnpm i remix\n```\n\n- Example when a peer dependency is required:\n\n```sh\nnpm i remix <peer-dependency>\n```\n\n- Usage examples must always import from `remix` package exports, not from `@remix-run/<package-name>` directly.\n\n- License section format:\n  - `See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)`\n\n8. Do not manually update the generated `remix` package in PRs.\n\n- `packages/remix` is generated automatically in CI.\n- Do not manually edit `packages/remix/package.json` or `packages/remix/src/*` in new pull requests.\n- Do not add `packages/remix/.changes/*` change files in new pull requests.\n- If user asks for full surfacing, you can still update root `README.md` package list when applicable.\n\n9. Validate before finishing.\n\n- Run package checks:\n  - `pnpm --filter @remix-run/<package-name> run typecheck`\n  - `pnpm --filter @remix-run/<package-name> run test`\n  - `pnpm --filter @remix-run/<package-name> run build`\n- Run repo lint (required):\n  - `pnpm run lint`\n- Add or update a change file under `packages/<package-name>/.changes/` when requested by contribution workflow.\n- For a brand-new package, the initial change file should use a `minor.` filename (for example, `minor.initial-release.md`) so the first release bumps `0.0.0` to `0.1.0`.\n- Exception: do not add a change file under `packages/remix/.changes/`; `remix` package updates are CI-generated.\n\n## Templates\n\nUse this minimal `src/index.ts` style:\n\n```ts\nexport { createThing, type ThingOptions } from './lib/thing.ts'\n```\n\nUse this minimal test style:\n\n```ts\nimport * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createThing } from './thing.ts'\n\ndescribe('createThing', () => {\n  it('returns expected value', () => {\n    let result = createThing()\n    assert.equal(result, 'ok')\n  })\n})\n```\n"
  },
  {
    "path": ".agents/skills/add-package/agents/openai.yaml",
    "content": "interface:\n  display_name: 'Add Package'\n  short_description: 'Create new Remix monorepo packages consistently'\n  default_prompt: 'Use $add-package to scaffold and wire a new package in the Remix monorepo with the standard layout, scripts, tests, and README structure.'\n"
  },
  {
    "path": ".agents/skills/make-change-file/SKILL.md",
    "content": "---\nname: make-change-file\ndescription: Create or update Remix repo change files under `packages/*/.changes`. Use when a user asks for release notes, a change file, a missing changelog entry, a prerelease note, or an update to existing unpublished release notes.\n---\n\n# Make Change File\n\n## Overview\n\nWrite release notes that match this repository's `.changes` conventions. Use it for both new change files and edits to existing unpublished ones.\n\n## Workflow\n\n1. Read the package's `package.json`, `.changes/` directory, and any relevant PR diff or commit range before writing anything.\n2. Check whether an unpublished change file already exists for the same work. If it does, update it in place instead of creating a duplicate note.\n3. Choose the bump type from the package version and the user-facing impact.\n4. Write concise, user-facing release notes that describe shipped behavior, APIs, or exports.\n5. Run `pnpm changes:preview` to verify the rendered changelog output.\n6. Run `pnpm run lint` before finishing.\n\n## Naming\n\n- Use `packages/<package>/.changes/[major|minor|patch].short-description.md`.\n- Keep the slug short, specific, and stable.\n- Reuse existing deterministic names when the repo already has a pattern for that class of note.\n- For Remix export-only changes, update `packages/remix/.changes/minor.remix.update-exports.md` in place.\n- For brand-new package releases, prefer `minor.initial-release.md`.\n\n## Bump Rules\n\n- For `0.x` packages: use `minor` for new features and breaking changes, `patch` for bug fixes.\n- Do not use `major` for `0.x` packages unless explicitly instructed.\n- For `1.x+` packages: use standard semver.\n- Breaking changes are relative to `main`, not relative to earlier commits in the same PR.\n- In `0.x`, breaking change notes must start with `BREAKING CHANGE: `.\n- For `remix` prerelease mode, the bump type mostly controls changelog categorization while the prerelease counter advances.\n\n## Content Rules\n\n- Document user-visible behavior, public API changes, exports, migrations, or upgrade work.\n- Do not write release notes for internal refactors unless they surface as real API or behavior changes.\n- Prefer a small number of logically grouped notes over many tiny files.\n- If one package changes internally and another package re-exports the new surface, add notes for both when users can consume the change from both package entrypoints.\n- Do not manually hard-wrap prose in `.changes/*.md` files. Keep each paragraph or bullet on a single source line and let rendered changelogs wrap naturally.\n- Use flat bullets only when they add clarity. Short paragraphs are usually better.\n\n## Remix-Specific Rules\n\n- `packages/remix/src/*` re-export files are generated. Do not hand-edit them unless the task explicitly requires generated output.\n- When `packages/remix/package.json` gains or changes public exports, capture that in `minor.remix.update-exports.md` instead of inventing a one-off filename.\n- If the change exposes another package's new APIs through `remix/...`, describe the surfaced `remix/...` entrypoints, not just the underlying workspace package name.\n\n## Checklist\n\n- Did you inspect existing unpublished `.changes` files first?\n- Is the bump type correct for the package version?\n- Did you reuse any deterministic filename the repo already expects?\n- Does the note describe user-facing changes instead of implementation details?\n- Does each paragraph or bullet stay on one source line without manual hard wrapping?\n- Did `pnpm changes:preview` render the expected changelog entry?\n"
  },
  {
    "path": ".agents/skills/make-change-file/agents/openai.yaml",
    "content": "interface:\n  display_name: 'Make Change File'\n  short_description: 'Create or update Remix change files using repo conventions.'\n  default_prompt: 'Use $make-change-file to add or revise package change files for this Remix repo.'\n"
  },
  {
    "path": ".agents/skills/make-demo/SKILL.md",
    "content": "---\nname: make-demo\ndescription: Create or revise demos in the Remix repository. Use when adding a new demo under demos/, updating an existing demo, or reviewing demo code to ensure it showcases Remix packages, strong code hygiene, and production-quality patterns.\n---\n\n# Make Demo\n\n## Overview\n\nDemos in this repository are not throwaway prototypes. They are durable code artifacts that should teach people and other agents how to write Remix code well.\n\nA good demo should:\n\n- exercise Remix framework behavior in a realistic way\n- push the target APIs through meaningful edge cases and composition points\n- model clean structure, naming, and accessibility\n- be code that a reader could adapt into a real application\n\n## Workflow\n\n1. Read the target APIs and at least one or two existing demos before writing new code.\n2. Choose a focused scenario that exists to demonstrate Remix behavior, not a generic app shell.\n3. Build the demo under `demos/<name>/` using the same conventions as the existing demos.\n4. Treat the code as a reference artifact, not as temporary sample code.\n5. Validate the demo locally before finishing.\n\n## Rules\n\n- Use Remix library packages for the demo's framework behavior. Do not introduce unrelated routers, component frameworks, state managers, or middleware stacks that distract from the Remix patterns being demonstrated.\n- Keep any non-Remix dependency incidental to the runtime environment only. If a database driver, asset bundler, or type package is needed, it should support the demo rather than define its architecture.\n- Demos should push Remix to its limits in a focused way. Prefer realistic edge cases, composition, streaming, middleware, routing, navigation, forms, or request-handling scenarios over toy examples.\n- When demos use `remix/component`, prefer idiomatic Remix component patterns. Use normal JSX composition and built-in styling/mixin props such as `css={...}` or `mix={css(...)}`\n  and `mix={[...]}` instead of dropping down to manual DOM mutation or ad hoc class management.\n- Demo code must have good hygiene. Use clear names, small focused modules, explicit control flow, and accessible markup. Avoid hacks, dead code, unexplained shortcuts, or patterns that would be poor examples for users to copy.\n- Make the demo teach good patterns. Assume readers and future agents will study it as an example of how Remix code should be written in this repository.\n- All demo servers should use port `44100`.\n- Demo servers should handle `SIGINT` and `SIGTERM` cleanly by closing the server and exiting.\n\n## Typical Structure\n\nUse only the files the scenario needs, but prefer this shape:\n\n- `demos/<name>/package.json`\n- `demos/<name>/server.ts`\n- `demos/<name>/README.md`\n- `demos/<name>/app/`\n- `demos/<name>/public/` when serving built assets or other static files\n\n## README Expectations\n\n- Explain what the demo proves or teaches.\n- Document how to run it locally.\n- Point out the key Remix APIs or patterns being demonstrated.\n- Keep code examples and imports aligned with repo guidance: use `remix` package exports where available.\n\n## Validation\n\n- Run `pnpm -C demos/<name> typecheck` when the demo defines a typecheck script.\n- Run `pnpm -C demos/<name> test` when the demo defines tests.\n- Smoke-test the demo server locally when behavior depends on live requests or browser interaction.\n- Run `pnpm run lint` before finishing.\n"
  },
  {
    "path": ".agents/skills/make-demo/agents/openai.yaml",
    "content": "interface:\n  display_name: 'Make Demo'\n  short_description: 'Create high-quality Remix demos'\n  default_prompt: 'Use $make-demo to add or revise a demo under demos/ that showcases Remix packages with clean, artifact-quality code and strong repo conventions.'\n"
  },
  {
    "path": ".agents/skills/make-pr/SKILL.md",
    "content": "---\nname: make-pr\ndescription: Create GitHub pull requests with clear, reviewer-friendly descriptions. Use when asked to open or prepare a PR, especially when the PR needs strong context, related links, and feature usage examples. This skill enforces concise PR structure, avoids redundant sections like validation/testing, and creates the PR with gh CLI.\n---\n\n# Make PR\n\n## Overview\n\nUse this skill to draft and open a PR with consistent, high-signal writing.\nKeep headings sparse and focus on the problem/feature explanation, context links, and practical code examples.\nOptimize for the shortest path to a credible PR, not the fullest possible context-gathering pass.\n\n## Workflow\n\n1. Check the fast-path blockers first.\n\n- Check `git status --short --branch` and `git branch --show-current` before doing deeper prep.\n- If the repo is in a detached HEAD or worktree state and the user wants a PR opened, create a branch early.\n\n1. Gather only the context needed to write the PR.\n\n- Capture what changed, why it changed, and who it affects.\n- Find related issues/PRs and include links when relevant.\n- Prefer `git diff --stat` plus the relevant diff over broad repo archaeology when the change is small.\n- If the user supplies a report, issue, or related PR, treat that as the primary context source.\n\n1. Get the branch into a PR-ready state quickly.\n\n- If changes are still uncommitted and the user wants a PR, branch first, then commit.\n- Prefer a single clean commit unless the user asks for a different history shape.\n\n1. Check whether this PR also needs a change file.\n\n- Do not assume every PR needs one.\n- Before opening the PR, decide whether the change is user-facing enough to require release notes in `packages/*/.changes`.\n- If a change file is needed or likely needed, use the `make-change-file` skill instead of re-deriving that workflow here.\n\n1. Draft the PR body with minimal structure.\n\n- Start with 1-2 short introductory paragraphs.\n- After the intro, include clear bullets describing:\n  - the feature and/or issue addressed\n  - key behavior/API changes\n  - expected impact\n- If the change is extensive, expand to up to 3-4 paragraphs and include background context with related links.\n\n1. Add required usage examples for feature work.\n\n- If the PR introduces a new feature, include a comprehensive usage snippet.\n- If it replaces or improves an older approach, include before/after examples.\n\n1. Exclude redundant sections.\n\n- Do not include `Validation`, `Testing`, or other process sections that are already implicit in PR workflow.\n- Do not add boilerplate sections that do not help review.\n\n1. Create the PR.\n\n- Save the body to a temporary file and run:\n\n```bash\ngh pr create --base main --head <branch> --title \"<title>\" --body-file <file>\n```\n\n- If `gh pr create` fails, leave the branch pushed when possible and give the user a ready-to-open compare URL plus the prepared title/body details.\n\n## Body Template\n\nUse this as a base and fill with concrete repo-specific details:\n\n````md\n<One or two short intro paragraphs explaining the change and why it matters.>\n\n- <Feature/issue addressed>\n- <What changed in behavior or API>\n- <Why this is needed now>\n\n<Optional additional context paragraph(s), up to 3-4 total for large changes, including links to related PRs/issues.>\n\n```ts\n// New feature usage example\n```\n\n```ts\n// Before\n```\n\n```ts\n// After\n```\n````\n"
  },
  {
    "path": ".agents/skills/make-pr/agents/openai.yaml",
    "content": "interface:\n  display_name: 'Make PR'\n  short_description: 'Create high-quality GitHub pull requests'\n  default_prompt: 'Use $make-pr to draft and create a GitHub pull request with clear context and examples.'\n"
  },
  {
    "path": ".agents/skills/publish-placeholder-package/SKILL.md",
    "content": "---\nname: publish-placeholder-package\ndescription: Publish a placeholder npm package at version 0.0.0 so package names are reserved and npm OIDC permissions can be configured before CI publishing. Use when creating a brand-new package that is not ready for full release.\n---\n\n# Publish Placeholder Package\n\n## Overview\n\nUse this skill to publish a minimal placeholder package to npm at `0.0.0`.\nThis is used to reserve the package name and unblock npm-side OIDC configuration for CI publishing.\n\n## Workflow\n\n1. Confirm publish target.\n\n- Collect:\n  - npm package name (for example, `@remix-run/my-package`)\n  - package directory in repo (for example, `packages/my-package`)\n- Validate the package does not already exist at `0.0.0`:\n\n```sh\nnpm view <package-name>@0.0.0 version\n```\n\n- If it already exists, stop and report that no placeholder publish is needed.\n\n2. Build a temporary placeholder package outside the repo.\n\n- Always publish from a temp directory to avoid shipping real package files by mistake.\n- Create the temp directory and write a minimal `package.json`:\n\n```sh\ntmp_dir=\"$(mktemp -d)\"\ncd \"$tmp_dir\"\n\ncat > package.json <<'JSON'\n{\n  \"name\": \"<package-name>\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Placeholder package for Remix CI/OIDC setup\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"<repo-package-dir>\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\nJSON\n```\n\n- Add a short README:\n\n```sh\ncat > README.md <<'MD'\n# Placeholder Package\n\nThis package is a placeholder published at `0.0.0` to reserve the npm name and configure CI publish permissions.\nMD\n```\n\n3. Ensure npm auth is valid (expect re-auth/OTP).\n\n- Check session:\n\n```sh\nnpm whoami\n```\n\n- If not authenticated, run:\n\n```sh\nnpm login\n```\n\n- Expect npm to require a fresh login and/or one-time password. If prompted for OTP, request it from the user and continue.\n\n4. Publish the placeholder.\n\n- Publish with public access:\n\n```sh\nnpm publish --access public\n```\n\n- If the account enforces 2FA for writes, publish with OTP:\n\n```sh\nnpm publish --access public --otp <code>\n```\n\n5. Verify and report.\n\n- Verify the published version:\n\n```sh\nnpm view <package-name>@0.0.0 version\n```\n\n- Report:\n  - package name\n  - published version (`0.0.0`)\n  - confirmation that npm package exists for OIDC permission setup\n\n6. Clean up temp files.\n\n```sh\nrm -rf \"$tmp_dir\"\n```\n\n## Notes\n\n- Keep placeholder publish minimal. Do not publish full source code for this step.\n- This is a one-time bootstrap step. Normal releases should continue through CI.\n"
  },
  {
    "path": ".agents/skills/publish-placeholder-package/agents/openai.yaml",
    "content": "interface:\n  display_name: 'Publish Placeholder Package'\n  short_description: 'Publish npm placeholder package at 0.0.0'\n  default_prompt: 'Use $publish-placeholder-package to publish a minimal npm placeholder package at 0.0.0 so we can configure npm OIDC permissions for CI publishing.'\n"
  },
  {
    "path": ".agents/skills/supersede-pr/SKILL.md",
    "content": "---\nname: supersede-pr\ndescription: Safely replace one GitHub pull request with another. Use when a user says a PR supersedes/replaces an older PR, asks to auto-close a superseded PR, or needs guaranteed closure behavior after merge. This skill explicitly closes the superseded PR with gh CLI and verifies final PR states instead of relying on closing keywords.\n---\n\n# Supersede PR\n\n## Overview\n\nUse this skill to handle PR supersession end-to-end.\nDo not rely on `Closes #<number>` to close another PR. GitHub closing keywords close issues, not pull requests.\n\n## Workflow\n\n1. Identify PR numbers and target repo.\n\n- Capture `old_pr` (the superseded PR) and `new_pr` (the replacement PR).\n- Resolve the repo with `gh repo view --json nameWithOwner -q .nameWithOwner` when not provided.\n\n1. Create or update the replacement PR first.\n\n- Open/push the replacement branch.\n- Open the new PR.\n- Include a traceable link in the PR body such as `Supersedes #<old_pr>`.\n\n1. Close the superseded PR explicitly.\n\n- Run:\n\n```bash\nscripts/close_superseded_pr.ts <old_pr> <new_pr>\n```\n\n- This adds a comment (`Superseded by #<new_pr>.`) and closes the old PR.\n\n1. Verify states.\n\n- Confirm the superseded PR is closed:\n\n```bash\ngh pr view <old_pr> --json state,url\n```\n\n- Confirm the replacement PR status/checks:\n\n```bash\ngh pr checks <new_pr>\n```\n\n## Rules\n\n1. Do not use `Closes #<old_pr>` when `<old_pr>` is a pull request.\n\n- Use `Closes/Fixes` only for issues.\n- Use `Supersedes #<old_pr>` or `Refs #<old_pr>` for PR-to-PR linkage.\n\n1. Prefer explicit closure over implied automation.\n\n- Always run the close command when the user asks to supersede a PR.\n- Treat closure as incomplete until `gh pr view <old_pr>` returns `CLOSED`.\n\n## Script\n\nUse the bundled script for deterministic closure:\n\n- `scripts/close_superseded_pr.ts`\n"
  },
  {
    "path": ".agents/skills/supersede-pr/agents/openai.yaml",
    "content": "interface:\n  display_name: 'Supersede PR'\n  short_description: 'Close superseded pull requests safely'\n  default_prompt: 'Use $supersede-pr to replace a PR and close the superseded PR safely.'\n"
  },
  {
    "path": ".agents/skills/supersede-pr/scripts/close_superseded_pr.ts",
    "content": "#!/usr/bin/env node\nimport { spawnSync } from 'node:child_process'\nimport * as process from 'node:process'\n\ntype ParsedArgs = {\n  dryRun: boolean\n  newPr: string\n  oldPr: string\n  repo: string | null\n}\n\nfunction main(): void {\n  let parsed = parseArgs(process.argv.slice(2))\n  ensureNumericPrNumber(parsed.oldPr, 'old_pr')\n  ensureNumericPrNumber(parsed.newPr, 'new_pr')\n\n  if (parsed.oldPr === parsed.newPr) {\n    fail('old_pr and new_pr must be different.')\n  }\n\n  let repo =\n    parsed.repo ?? ghCapture(['repo', 'view', '--json', 'nameWithOwner', '-q', '.nameWithOwner'])\n  let oldState = ghCapture([\n    'pr',\n    'view',\n    parsed.oldPr,\n    '--repo',\n    repo,\n    '--json',\n    'state',\n    '-q',\n    '.state',\n  ])\n  let newState = ghCapture([\n    'pr',\n    'view',\n    parsed.newPr,\n    '--repo',\n    repo,\n    '--json',\n    'state',\n    '-q',\n    '.state',\n  ])\n\n  if (newState !== 'OPEN' && newState !== 'MERGED') {\n    fail(`Replacement PR #${parsed.newPr} is in state '${newState}'. Expected OPEN or MERGED.`)\n  }\n\n  if (oldState !== 'OPEN') {\n    process.stdout.write(`Superseded PR #${parsed.oldPr} is already ${oldState}. Nothing to do.\\n`)\n    return\n  }\n\n  let comment = `Superseded by #${parsed.newPr}.`\n\n  process.stdout.write(`Repo: ${repo}\\n`)\n  process.stdout.write(`Closing PR #${parsed.oldPr} with comment: ${comment}\\n`)\n\n  if (parsed.dryRun) {\n    process.stdout.write(\n      `[dry-run] gh pr close \"${parsed.oldPr}\" --repo \"${repo}\" --comment \"${comment}\"\\n`,\n    )\n    return\n  }\n\n  ghInherit(['pr', 'close', parsed.oldPr, '--repo', repo, '--comment', comment])\n\n  let finalState = ghCapture([\n    'pr',\n    'view',\n    parsed.oldPr,\n    '--repo',\n    repo,\n    '--json',\n    'state',\n    '-q',\n    '.state',\n  ])\n  if (finalState !== 'CLOSED') {\n    fail(`Failed to close PR #${parsed.oldPr}. Final state: ${finalState}`)\n  }\n\n  process.stdout.write(`Closed PR #${parsed.oldPr} successfully.\\n`)\n}\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n  if (argv.includes('-h') || argv.includes('--help')) {\n    printUsage()\n    process.exit(0)\n  }\n\n  if (argv.length < 2) {\n    printUsage()\n    process.exit(1)\n  }\n\n  let oldPr = argv[0]\n  let newPr = argv[1]\n  let repo: string | null = null\n  let dryRun = false\n  let index = 2\n\n  while (index < argv.length) {\n    let arg = argv[index]\n    if (arg === '--repo') {\n      let next = argv[index + 1]\n      if (!next) {\n        fail('--repo requires a value like owner/repo')\n      }\n      repo = next\n      index += 2\n      continue\n    }\n    if (arg === '--dry-run') {\n      dryRun = true\n      index++\n      continue\n    }\n    fail(`Unknown argument: ${arg}`)\n  }\n\n  return { dryRun, newPr, oldPr, repo }\n}\n\nfunction printUsage(): void {\n  process.stdout.write(`Usage:\n  close_superseded_pr.ts <old_pr> <new_pr> [--repo <owner/repo>] [--dry-run]\n\nExamples:\n  close_superseded_pr.ts 11085 11087\n  close_superseded_pr.ts 11085 11087 --repo remix-run/remix\n  close_superseded_pr.ts 11085 11087 --dry-run\n`)\n}\n\nfunction ensureNumericPrNumber(value: string, label: string): void {\n  if (!/^[0-9]+$/.test(value)) {\n    fail(`${label} must be a numeric pull request number.`)\n  }\n}\n\nfunction ghCapture(args: string[]): string {\n  let result = spawnSync('gh', args, { encoding: 'utf8' })\n  if (result.status !== 0) {\n    let stderr = (result.stderr ?? '').trim()\n    let details = stderr ? `\\n${stderr}` : ''\n    fail(`gh ${args.join(' ')} failed.${details}`)\n  }\n  return (result.stdout ?? '').trim()\n}\n\nfunction ghInherit(args: string[]): void {\n  let result = spawnSync('gh', args, { stdio: 'inherit' })\n  if (result.status !== 0) {\n    fail(`gh ${args.join(' ')} failed with exit code ${result.status ?? 'unknown'}.`)\n  }\n}\n\nfunction fail(message: string): never {\n  process.stderr.write(`${message}\\n`)\n  process.exit(1)\n}\n\nmain()\n"
  },
  {
    "path": ".agents/skills/supersede-pr/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowJs\": false,\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"noEmit\": true,\n    \"strict\": true,\n    \"target\": \"ESNext\",\n    \"verbatimModuleSyntax\": true\n  },\n  \"include\": [\"scripts/**/*.ts\"]\n}\n"
  },
  {
    "path": ".agents/skills/update-pr/SKILL.md",
    "content": "---\nname: update-pr\ndescription: Update an existing GitHub pull request title and description so they accurately describe the pull request as it exists now. Use when the user asks to update, rewrite, refresh, fix, or tighten a PR title/body, or when the PR scope has changed and the metadata needs to be brought back in sync.\n---\n\n# Update PR\n\n## Overview\n\nRewrite pull request metadata as if drafting it from scratch for the current diff. Treat the title and body as a current reviewer-facing summary of the PR, not as commentary about prior versions of the PR.\n\n## Workflow\n\n1. Read the current PR title/body and the current branch diff before drafting.\n2. Identify the PR's current scope, APIs, behavior changes, and reviewer-relevant context.\n3. Rewrite the body from scratch so it describes the PR as it exists now.\n4. Review the title at the same time and update it whenever the body is updated.\n5. Apply the update with `gh pr edit`.\n\n## Rules\n\n- Never write the description as an update to itself. Do not say things like \"this expands the original PR\", \"this PR now also\", or similar process narration unless the user explicitly wants history called out.\n- Always evaluate the title when updating a PR. If the scope or emphasis changed, rewrite the title too.\n- Write in terms of the present PR contents, using concise reviewer-facing language.\n- Keep the structure minimal: one short introductory paragraph plus flat bullets is usually enough.\n- Include usage examples when the PR introduces or materially changes a feature API.\n- Preserve still-relevant issue links or context, but drop stale framing.\n\n## Applying The Update\n\n- Draft the new title and body in a temporary file.\n- Use `gh pr edit <number> --title \"<title>\" --body-file <file>`.\n- Re-read the PR after editing to confirm the final title/body match the intended framing.\n"
  },
  {
    "path": ".agents/skills/update-pr/agents/openai.yaml",
    "content": "interface:\n  display_name: 'Update PR'\n  short_description: 'Refresh an existing pull request title and body.'\n  default_prompt: 'Use $update-pr to rewrite this pull request title and description so they accurately describe the PR as it exists now.'\n"
  },
  {
    "path": ".agents/skills/write-api-docs/SKILL.md",
    "content": "---\nname: write-api-docs\ndescription: Write or audit public API docs for Remix packages. Use when adding or tightening JSDoc on exported functions, classes, interfaces, type aliases, or option objects.\n---\n\n# Write API Docs\n\n## Overview\n\nUse this skill when documenting public APIs in Remix packages.\n\nThe goal is to document the API users can actually import, not every helper in `src/lib`.\nWork from the package exports outward, add concise JSDoc to the public declarations, and make sure the result passes the repo's ESLint JSDoc rules.\n\n## Workflow\n\n1. Identify the package's public exports.\n2. Find the `src` entry files that back those exports.\n3. Trace those entry files to the declarations they re-export from `src/lib`.\n4. Add or tighten JSDoc on the public declarations only.\n5. Run package typecheck if appropriate and always run `pnpm run lint`.\n\n## How To Identify Public API\n\nThe source of truth is the package's `package.json`.\n\n- Start with `package.json` `exports`.\n- Each public export should map to a file directly under `src/`.\n- Those `src/*.ts` entry files define the public surface by re-exporting symbols from `src/lib`.\n- A declaration in `src/lib` is public only if it is re-exported by one of those public `src/*.ts` entry files.\n\nRules:\n\n- Do not assume everything in `src/lib` is public.\n- Do not document private helpers just because they are exported within `src/lib`.\n- If a declaration is not reachable from a package export, it is internal unless the user explicitly asks otherwise.\n\n## What To Document\n\nFor public API, add JSDoc to:\n\n- exported functions\n- exported classes\n- exported interfaces\n- exported type aliases\n- exported public constants when they are part of the API shape\n\nFor public interfaces:\n\n- add a JSDoc block on the interface itself\n- add a property-level JSDoc block for every property on the interface, even when the name seems obvious\n\nFor public object-shaped type aliases:\n\n- prefer an `interface` when you are introducing a new public object shape\n- if an existing public type alias cannot reasonably become an interface, document the object shape as thoroughly as the syntax allows\n\nFor overloads:\n\n- document the public overload signatures or the exported declaration in a way that makes the callable surface clear to users\n\n## JSDoc Style For This Repo\n\nKeep comments short, factual, and user-facing.\n\n- Describe what the API does, not how the implementation works internally.\n- Prefer one concise summary sentence, then short `@param` / `@returns` docs as needed.\n- Do not put TypeScript types in JSDoc tags. ESLint forbids JSDoc type syntax here because the source of truth is the TypeScript signature.\n- Keep parameter names in JSDoc exactly aligned with the function signature.\n- Use `@returns` for non-void functions and include a real description.\n- For `@param`, include descriptions and do not add a hyphen before the description.\n- Specify `@param` default values in parenthesis at the end of the comment, do not use `@default` tags\n- Include an `@example` code block when it helps to show a use-case or pattern. Skip `@example` for simple getters, trivial constructors, or APIs whose usage is self-evident.\n- Use `{@link API}` to link to related Remix APIs when it adds value. Don't link every related API — use discretion to avoid noise.\n- Use backticks for all other unlinked code references — identifiers, HTTP methods, special values.\n\nGood:\n\n```ts\n/**\n * Creates an {@link AuthProvider} for direct credentials-based authentication.\n *\n * @param options Parsing and verification hooks for submitted credentials.\n * @returns A provider that can be passed to `login()`.\n */\nexport function createCredentialsAuthProvider(...) {}\n```\n\nAvoid:\n\n```ts\n/**\n * @param {CredentialsOptions} options - options\n * @returns {CredentialsProvider}\n */\n```\n\n## ESLint Expectations\n\nThe relevant rules live in [`eslint.config.js`](../../eslint.config.js).\n\nFor `packages/**/*.{ts,tsx}` (excluding tests), ESLint enforces JSDoc on callable declarations such as:\n\n- function declarations\n- function expressions\n- arrow functions\n- class declarations\n- public methods\n\nImportant enforced details:\n\n- `jsdoc/require-param`\n- `jsdoc/require-param-name`\n- `jsdoc/require-param-description`\n- `jsdoc/require-returns`\n- `jsdoc/require-returns-description`\n- `jsdoc/no-types`\n- `jsdoc/check-param-names`\n- `jsdoc/check-types`\n- `jsdoc/check-alignment`\n\nPractical implication:\n\n- if a public function takes parameters, document all of them\n- if a public function returns a value, document the return value\n- do not use JSDoc type annotations\n- keep the block formatted cleanly enough to satisfy alignment checks\n\n## Review Checklist\n\n- Did you start from `package.json` exports instead of guessing from `src/lib`?\n- Are all documented declarations actually reachable from a public `src/*.ts` entry file?\n- Do all public functions and methods have JSDoc with `@param` and `@returns` where required?\n- Do public interfaces and type aliases have a concise doc block explaining what they represent?\n- Does every property on every public interface have its own property-level JSDoc block?\n- Did you avoid documenting internal helpers that are not exported publicly?\n- Did `pnpm run lint` pass?\n"
  },
  {
    "path": ".agents/skills/write-readme/SKILL.md",
    "content": "---\nname: write-readme\ndescription: Write or rewrite package README files in the style used by the Remix repository. Use when drafting a new package README, revising an existing README, or reviewing README structure, examples, installation instructions, and section ordering for Remix packages.\n---\n\n# Write Readme\n\n## Overview\n\nDraft README files as concise package documentation for real users, not as marketing copy or API dumps. Mirror the structure used across this repository, keep examples production-oriented, and avoid awkward manual line breaks in prose.\n\n## Workflow\n\n1. Read the package API and at least one or two sibling package READMEs before drafting.\n2. Document the package as it exists today, not the package you wish existed.\n3. Start with a realistic production usage example as soon as the installation section is done.\n4. Cover each major feature with a concrete example.\n5. Finish with internal ecosystem links, external related work, and license info.\n\n## Structure\n\nUse this section order unless there is a strong package-specific reason not to:\n\n1. `# short package-name` (i.e. `fetch-router` instead of `@remix-run/fetch-router`)\n2. Intro: one or two sentences explaining what the package does and why it exists\n3. `## Features`: a flat bullet list of the main highlights\n4. `## Installation`\n5. `## Usage`: a production-like example that shows the package in context\n6. One section per major feature, each with focused examples\n7. `## Related Packages`\n8. `## Related Work`\n9. `## License`\n\n## Rules\n\n- Installation should always start with:\n\n```sh\nnpm i remix\n```\n\n- If the package requires a third-party dependency or peer, include it explicitly in the installation section after `remix`.\n- Usage examples should import from `remix/...`, not `@remix-run/...`.\n- The first example should look like real application code, not the smallest possible snippet.\n- Feature sections should show how to use the package's major capabilities in practice, with one example per capability when useful.\n- Keep prose compact. Do not hard-wrap paragraphs at awkward places in the middle of a sentence just to force a line length.\n- Prefer flat bullets and short paragraphs over long explanatory blocks.\n- `Related Packages` should point to relevant Remix packages in the monorepo.\n- `Related Work` should point to external libraries, specs, standards, or prior art that help readers place the package.\n- `License` should use the standard repo wording and link.\n\n## Checklist\n\n- Does the intro explain the package in one or two sentences?\n- Does the features list surface the package's main value quickly?\n- Does the installation section use `npm i remix`?\n- Does the main usage example show a realistic production scenario?\n- Does each major feature have an example?\n- Does the README end with `Related Packages`, `Related Work`, and `License`?\n- Does the prose read naturally without awkward manual line breaks?\n"
  },
  {
    "path": ".agents/skills/write-readme/agents/openai.yaml",
    "content": "interface:\n  display_name: 'Write Readme'\n  short_description: 'Write Remix package READMEs in the repo style.'\n  default_prompt: 'Use $write-readme to draft or rewrite this package README in the style used by the Remix repository.'\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: Build\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches-ignore:\n      - release-v2\n      - v2\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest]\n\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Build packages\n        run: pnpm build\n"
  },
  {
    "path": ".github/workflows/check.yaml",
    "content": "name: Type check, lint, validate change files\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches-ignore:\n      - release-v2\n      - v2\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Lint\n        run: pnpm lint\n\n  typecheck:\n    name: Typecheck\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Typecheck\n        run: pnpm typecheck\n\n  change-files:\n    name: Validate change files\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n\n      - name: Check change files\n        run: node ./scripts/changes-validate.ts\n"
  },
  {
    "path": ".github/workflows/data-table-integration.yaml",
    "content": "name: Data Table Integration Tests\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - '.github/workflows/data-table-integration.yaml'\n      - 'packages/data-table/**'\n      - 'packages/data-table-postgres/**'\n      - 'packages/data-table-mysql/**'\n      - 'packages/data-table-sqlite/**'\n      - 'pnpm-workspace.yaml'\n      - 'package.json'\n  pull_request:\n    paths:\n      - '.github/workflows/data-table-integration.yaml'\n      - 'packages/data-table/**'\n      - 'packages/data-table-postgres/**'\n      - 'packages/data-table-mysql/**'\n      - 'packages/data-table-sqlite/**'\n      - 'pnpm-workspace.yaml'\n      - 'package.json'\n\njobs:\n  unit:\n    name: Unit and Build\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run data-table package checks\n        run: |\n          pnpm --filter @remix-run/data-table run typecheck\n          pnpm --filter @remix-run/data-table run test\n          pnpm --filter @remix-run/data-table run test:coverage\n          pnpm --filter @remix-run/data-table run build\n          pnpm --filter @remix-run/data-table-postgres run typecheck\n          pnpm --filter @remix-run/data-table-postgres run test\n          pnpm --filter @remix-run/data-table-postgres run test:coverage\n          pnpm --filter @remix-run/data-table-postgres run build\n          pnpm --filter @remix-run/data-table-mysql run typecheck\n          pnpm --filter @remix-run/data-table-mysql run test\n          pnpm --filter @remix-run/data-table-mysql run test:coverage\n          pnpm --filter @remix-run/data-table-mysql run build\n          pnpm --filter @remix-run/data-table-sqlite run typecheck\n          pnpm --filter @remix-run/data-table-sqlite run test\n          pnpm --filter @remix-run/data-table-sqlite run test:coverage\n          pnpm --filter @remix-run/data-table-sqlite run build\n\n  postgres-integration:\n    name: Postgres Integration\n    runs-on: ubuntu-latest\n\n    services:\n      postgres:\n        image: postgres:16\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DB: remix\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd=\"pg_isready -U postgres\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=5\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run postgres integration tests\n        env:\n          DATA_TABLE_INTEGRATION: '1'\n          DATA_TABLE_POSTGRES_URL: postgres://postgres:postgres@127.0.0.1:5432/remix\n        run: node --disable-warning=ExperimentalWarning --test './packages/data-table-postgres/src/lib/adapter.integration.test.ts'\n\n  mysql-integration:\n    name: MySQL Integration\n    runs-on: ubuntu-latest\n\n    services:\n      mysql:\n        image: mysql:8\n        env:\n          MYSQL_ROOT_PASSWORD: root\n          MYSQL_DATABASE: remix\n        ports:\n          - 3306:3306\n        options: >-\n          --health-cmd=\"mysqladmin ping -h 127.0.0.1 -uroot -proot\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=10\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run mysql integration tests\n        env:\n          DATA_TABLE_INTEGRATION: '1'\n          DATA_TABLE_MYSQL_URL: mysql://root:root@127.0.0.1:3306/remix\n        run: node --disable-warning=ExperimentalWarning --test './packages/data-table-mysql/src/lib/adapter.integration.test.ts'\n\n  sqlite-integration:\n    name: SQLite Integration\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Build sqlite native module\n        run: pnpm rebuild better-sqlite3\n\n      - name: Run sqlite adapter tests\n        env:\n          DATA_TABLE_INTEGRATION: '1'\n        run: node --disable-warning=ExperimentalWarning --test './packages/data-table-sqlite/src/lib/adapter.integration.test.ts'\n"
  },
  {
    "path": ".github/workflows/file-storage-integration.yaml",
    "content": "name: File Storage Integration Tests\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - '.github/workflows/file-storage-integration.yaml'\n      - 'packages/file-storage/**'\n      - 'packages/file-storage-s3/**'\n      - 'pnpm-workspace.yaml'\n      - 'package.json'\n  pull_request:\n    paths:\n      - '.github/workflows/file-storage-integration.yaml'\n      - 'packages/file-storage/**'\n      - 'packages/file-storage-s3/**'\n      - 'pnpm-workspace.yaml'\n      - 'package.json'\n\njobs:\n  unit:\n    name: Unit and Build\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run file-storage-s3 package checks\n        run: |\n          pnpm --filter @remix-run/file-storage-s3 run typecheck\n          pnpm --filter @remix-run/file-storage-s3 run test\n          pnpm --filter @remix-run/file-storage-s3 run build\n\n  s3-integration:\n    name: S3 Integration\n    runs-on: ubuntu-latest\n\n    services:\n      localstack:\n        image: localstack/localstack:4.4.0\n        env:\n          SERVICES: s3\n          AWS_ACCESS_KEY_ID: test\n          AWS_SECRET_ACCESS_KEY: test\n          DEFAULT_REGION: us-east-1\n        ports:\n          - 4566:4566\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Wait for LocalStack\n        run: |\n          for i in {1..60}; do\n            if curl -fsS http://127.0.0.1:4566/_localstack/health > /dev/null; then\n              exit 0\n            fi\n            sleep 1\n          done\n          echo \"LocalStack did not become ready in time\"\n          exit 1\n\n      - name: Run S3 integration tests\n        env:\n          FILE_STORAGE_S3_INTEGRATION: '1'\n          FILE_STORAGE_S3_ENDPOINT: http://127.0.0.1:4566\n          FILE_STORAGE_S3_BUCKET: remix-file-storage-integration\n          FILE_STORAGE_S3_REGION: us-east-1\n          FILE_STORAGE_S3_ACCESS_KEY_ID: test\n          FILE_STORAGE_S3_SECRET_ACCESS_KEY: test\n          FILE_STORAGE_S3_FORCE_PATH_STYLE: '1'\n        run: node --disable-warning=ExperimentalWarning --test './packages/file-storage-s3/src/lib/s3.integration.test.ts'\n"
  },
  {
    "path": ".github/workflows/format.yml",
    "content": "name: Format\n\non:\n  push:\n    branches:\n      - main\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  format:\n    if: \"${{ github.repository == 'remix-run/remix' && !startsWith(github.event.head_commit.message, 'chore: format') }}\"\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          token: ${{ secrets.FORMAT_PAT }}\n          fetch-depth: 0\n\n      - name: Detect formatting-relevant changes\n        id: changes\n        run: |\n          base=\"${{ github.event.before }}\"\n\n          if [ \"$base\" = \"0000000000000000000000000000000000000000\" ]; then\n            files=\"$(git ls-files)\"\n          else\n            files=\"$(git diff --name-only \"$base\" \"$GITHUB_SHA\")\"\n          fi\n\n          if [ -z \"$files\" ]; then\n            echo \"No changed files detected\"\n            echo \"should_run=false\" >> \"$GITHUB_OUTPUT\"\n            exit 0\n          fi\n\n          while IFS= read -r file; do\n            case \"$file\" in\n              .prettierignore|.prettierrc|.prettierrc.json|.prettierrc.yml|.prettierrc.yaml|.prettierrc.js|.prettierrc.cjs|.prettierrc.mjs|*.js|*.jsx|*.cjs|*.mjs|*.ts|*.tsx|*.cts|*.mts|*.json|*.jsonc|*.md|*.mdx|*.yaml|*.yml|*.css|*.scss|*.html)\n                echo \"Formatting-relevant change: $file\"\n                echo \"should_run=true\" >> \"$GITHUB_OUTPUT\"\n                exit 0\n                ;;\n            esac\n          done <<EOF\n          $files\n          EOF\n\n          echo \"No formatting-relevant files changed\"\n          echo \"should_run=false\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Install pnpm\n        if: steps.changes.outputs.should_run == 'true'\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        if: steps.changes.outputs.should_run == 'true'\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        if: steps.changes.outputs.should_run == 'true'\n        run: pnpm install --frozen-lockfile\n\n      - name: Format\n        if: steps.changes.outputs.should_run == 'true'\n        run: pnpm format\n\n      - name: Commit\n        if: steps.changes.outputs.should_run == 'true'\n        run: |\n          git config --local user.email \"hello@remix.run\"\n          git config --local user.name \"Remix Run Bot\"\n\n          git add .\n          git restore .github/workflows # PAT doesn't have permission to push workflow changes\n          if [ -z \"$(git status --porcelain)\" ]; then\n            echo \"No formatting changes\"\n            exit 0\n          fi\n          git commit -m \"chore: format\"\n          git push\n          echo \"Pushed formatting changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)\"\n"
  },
  {
    "path": ".github/workflows/generate-remix.yaml",
    "content": "# Update the `remix` package by auto-generating from the `@remix-run/*` packages in the repo\n# runs on any pushes/PRs to `main` that touch any `package.json` files since that would\n# potentially alter the sub-exports that need to be reflected in the `remix` package.\n\n# Note: Does not currently run on PRs from forked repos.\n\nname: Update Remix package\n\non:\n  push:\n    branches:\n      - 'main'\n    paths:\n      - 'packages/**/package.json'\n  pull_request:\n    branches-ignore:\n      - release-v2\n      - v2\n    paths:\n      - 'packages/**/package.json'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  generate-remix:\n    if: |\n      github.repository == 'remix-run/remix' &&\n      (\n        github.event_name == 'push' ||\n        github.event.pull_request.head.repo.full_name == github.repository\n      )\n    runs-on: ubuntu-latest\n    steps:\n      # Normal checkout of the trigger branch on pushes\n      - name: Checkout\n        if: github.event_name == 'push'\n        uses: actions/checkout@v4\n        with:\n          # Use a PAT because using the default `GITHUB_TOKEN`/`github.token` will not\n          # trigger workflow runs, so use a PAT to ensure new commits will run CI checks.\n          # See: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow\n          token: ${{ secrets.GH_REMIX_PAT }}\n\n      # Checkout the PR branch when running on PRs\n      - name: Checkout\n        if: github.event_name == 'pull_request'\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.ref }}\n          # Use a PAT because using the default `GITHUB_TOKEN`/`github.token` will not\n          # trigger workflow runs, so use a PAT to ensure new commits will run CI checks.\n          # See: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow\n          token: ${{ secrets.GH_REMIX_PAT }}\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Generate remix package\n        run: pnpm run generate-remix\n\n      - name: Commit and push changes\n        id: commit\n        run: |\n          if [ -z \"$(git status --porcelain)\" ]; then\n            echo \"💿 no updates to the remix package needed\"\n            exit 0\n          fi\n\n          # Check for unintentional changes\n          OUTSIDE_CHANGES=$(git status --porcelain | awk '{print $2}' | grep -v \"^packages/remix/\" || true)\n          if [ -n \"$OUTSIDE_CHANGES\" ]; then\n            echo \"Refusing to commit changes outside of packages/remix/:\"\n            echo \"$OUTSIDE_CHANGES\"\n            exit 1\n          fi\n\n          # Re-install to ensure any new remix peerDependencies are reflected\n          pnpm install --no-frozen-lockfile\n\n          git config --local user.email \"hello@remix.run\"\n          git config --local user.name \"Remix Run Bot\"\n          git add .\n          git commit -a -m \"build: update remix package\"\n          git push\n          echo \"💿 pushed updates: https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)\"\n          echo \"new_sha=$(git rev-parse --short HEAD)\" >> $GITHUB_OUTPUT\n\n      - name: Comment on PR\n        if: github.event_name == 'pull_request' && steps.commit.outputs.new_sha != ''\n        env:\n          # `GH_TOKEN` is required to use the `gh` CLI to add comments to PRs.\n          GH_TOKEN: ${{ secrets.GH_REMIX_PAT }}\n        run: gh pr comment ${{ github.event.pull_request.number }} --body \"Changes in this PR resulted in updates to the auto-generated \\`remix\\` package in ${{ steps.commit.outputs.new_sha }}. Please review those changes prior to merging.\"\n"
  },
  {
    "path": ".github/workflows/preview.yml",
    "content": "# Create \"installable\" preview branches\n#\n# Commits to `main` push builds to a `preview/main` branch:\n#   pnpm install \"remix-run/remix#preview/main&path:packages/remix\"\n#\n# Pull Requests create `preview/pr-{number}` branches:\n#   pnpm install \"remix-run/remix#preview/pr-12345&path:packages/remix\"\n#\n# Can also be dispatched manually with base/installable branches to provide\n# `experimental` branches from PRs or otherwise.\n\nname: Preview Build\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n    inputs:\n      baseBranch:\n        description: Base Branch\n        required: true\n      installableBranch:\n        description: Installable Branch\n        required: true\n  pull_request:\n    types: [opened, synchronize, reopened, closed]\n\nconcurrency:\n  # Include `event_name` here because when a pull_request is merged (closed), the\n  # `github.ref` goes back to `ref/heads/main` which will conflict with the run on\n  # `main` from the merged PR\n  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  preview:\n    # Don't run on PRs from forked repos\n    if: github.repository == 'remix-run/remix' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout (push)\n        if: github.event_name == 'push'\n        uses: actions/checkout@v4\n\n      - name: Checkout (pull_request)\n        if: github.event_name == 'pull_request'\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n\n      - name: Checkout (workflow_dispatch)\n        if: github.event_name == 'workflow_dispatch'\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ inputs.baseBranch }}\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Setup git\n        run: |\n          git config --local user.email \"hello@remix.run\"\n          git config --local user.name \"Remix Run Bot\"\n\n      # Build and force push over the preview/main branch\n      - name: Build/push branch (push)\n        if: github.event_name == 'push'\n        run: |\n          pnpm run setup-installable-branch preview/main\n          git push --force --set-upstream origin preview/main\n          echo \"💿 pushed installable branch: https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)\"\n\n      # Build and force push over the PR preview/pr-{number} branch + comment on the PR\n      - name: Build/push branch (pull_request)\n        if: github.event_name == 'pull_request' && github.event.pull_request.state == 'open'\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        run: |\n          pnpm run setup-installable-branch preview/pr-${{ github.event.pull_request.number }}\n          git push --force --set-upstream origin preview/pr-${{ github.event.pull_request.number }}\n          echo \"pushed installable branch: https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)\"\n          pnpm run pr-preview comment ${{ github.event.pull_request.number }} preview/pr-${{ github.event.pull_request.number }}\n\n      # Build and normal push for experimental releases to avoid unintended force\n      # pushes over remote branches in case of a branch name collision\n      - name: Build/push branch (workflow_dispatch)\n        if: github.event_name == 'workflow_dispatch'\n        run: |\n          pnpm run setup-installable-branch ${{ inputs.installableBranch }}\n          git push --set-upstream origin ${{ inputs.installableBranch }}\n          echo \"💿 pushed installable branch: https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)\"\n\n      # Cleanup PR preview/pr-{number} branches when the PR is closed\n      - name: Cleanup preview branch\n        if: github.event_name == 'pull_request' && github.event.pull_request.state == 'closed'\n        env:\n          GITHUB_TOKEN: ${{ github.token }}\n        run: |\n          pnpm run pr-preview cleanup ${{ github.event.pull_request.number }} preview/pr-${{ github.event.pull_request.number }}\n"
  },
  {
    "path": ".github/workflows/publish.yaml",
    "content": "name: Publish\n\non:\n  push:\n    branches:\n      - main\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\njobs:\n  check:\n    name: Check release readiness\n    runs-on: ubuntu-latest\n    outputs:\n      has_change_files: ${{ steps.check.outputs.has_change_files }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Check for change files\n        id: check\n        run: |\n          # Look for change files in any package's .changes directory (excluding README.md)\n          change_files=$(find packages/*/.changes -name \"*.md\" ! -name \"README.md\" 2>/dev/null)\n          if [ -n \"$change_files\" ]; then\n            echo \"Change files found (blocking publish):\"\n            echo \"$change_files\"\n            echo \"has_change_files=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"No change files found, proceeding to publish.\"\n            echo \"has_change_files=false\" >> $GITHUB_OUTPUT\n          fi\n\n  publish:\n    name: Publish any unreleased packages\n    needs: check\n    if: needs.check.outputs.has_change_files == 'false'\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write # Required for creating tags and GitHub releases\n      id-token: write # OIDC ID token is used for authentication with npm/jsr\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Get Playwright Version\n        id: playwright-version\n        run: echo \"version=$(pnpm --filter @remix-run/component exec playwright --version | cut -d ' ' -f2)\" >> $GITHUB_OUTPUT\n\n      - name: Cache Playwright Browsers\n        uses: actions/cache@v4\n        id: cache-browsers\n        with:\n          path: ~/.cache/ms-playwright\n          key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}\n\n      - name: Install Playwright Browsers\n        if: steps.cache-browsers.outputs.cache-hit != 'true'\n        run: pnpm --filter @remix-run/component exec playwright install --with-deps\n\n      - name: Run tests\n        run: pnpm test\n\n      - name: Build packages\n        run: pnpm build\n\n      - name: Publish to npm and create releases\n        run: node ./scripts/publish.ts\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Output tag\n        run: echo \"tag=$(git tag --points-at HEAD | grep -e '^remix@3')\" >> $GITHUB_OUTPUT\n\n  docs:\n    name: Update API Docs\n    needs: publish\n    runs-on: ubuntu-latest\n    steps:\n      - name: Trigger remix-api-docs\n        uses: actions/github-script@v8\n        with:\n          github-token: ${{ secrets.REMIX_API_DOCS_PAT }}\n          script: |\n            await github.rest.repos.createDispatchEvent({\n              owner: 'remix-run',\n              repo: 'remix-api-docs',\n              event_type: 'update-docs',\n              client_payload: { tag: '${{ needs.publish.outputs.tag }}' }\n            });\n"
  },
  {
    "path": ".github/workflows/release-pr.yaml",
    "content": "name: Update \"Release\" PR\n\non:\n  push:\n    branches:\n      - main\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\njobs:\n  update:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write\n      pull-requests: write\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          token: ${{ secrets.GH_REMIX_PAT }}\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Update PR\n        run: node scripts/release-pr.ts\n        env:\n          GITHUB_TOKEN: ${{ secrets.GH_REMIX_PAT }}\n"
  },
  {
    "path": ".github/workflows/session-integration.yaml",
    "content": "name: Session Integration Tests\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - '.github/workflows/session-integration.yaml'\n      - 'packages/session/**'\n      - 'packages/session-storage-memcache/**'\n      - 'packages/session-storage-redis/**'\n      - 'pnpm-workspace.yaml'\n      - 'package.json'\n  pull_request:\n    paths:\n      - '.github/workflows/session-integration.yaml'\n      - 'packages/session/**'\n      - 'packages/session-storage-memcache/**'\n      - 'packages/session-storage-redis/**'\n      - 'pnpm-workspace.yaml'\n      - 'package.json'\n\njobs:\n  memcache-integration:\n    name: Memcache Integration\n    runs-on: ubuntu-latest\n\n    services:\n      memcached:\n        image: memcached:1.6\n        ports:\n          - 11211:11211\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run memcache integration tests\n        env:\n          SESSION_MEMCACHE_INTEGRATION: '1'\n          SESSION_MEMCACHE_SERVER: 127.0.0.1:11211\n        run: node --disable-warning=ExperimentalWarning --test './packages/session-storage-memcache/src/lib/memcache-storage.integration.test.ts'\n\n  redis-integration:\n    name: Redis Integration\n    runs-on: ubuntu-latest\n\n    services:\n      redis:\n        image: redis:7\n        ports:\n          - 6379:6379\n        options: >-\n          --health-cmd=\"redis-cli ping || exit 1\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=5\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Run redis integration tests\n        env:\n          SESSION_REDIS_INTEGRATION: '1'\n          SESSION_REDIS_URL: redis://127.0.0.1:6379\n        run: node --disable-warning=ExperimentalWarning --test './packages/session-storage-redis/src/lib/redis-storage.integration.test.ts'\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: Test\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches-ignore:\n      - release-v2\n      - v2\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  test-ubuntu:\n    name: test (ubuntu-latest)\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Get Playwright Version\n        id: playwright-version\n        shell: bash\n        run: echo \"version=$(pnpm --filter @remix-run/component exec playwright --version | cut -d ' ' -f2)\" >> $GITHUB_OUTPUT\n\n      - name: Cache Playwright Browsers\n        uses: actions/cache@v4\n        id: cache-browsers\n        with:\n          path: |\n            ~/.cache/ms-playwright\n          key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}\n\n      - name: Install Playwright Browsers\n        if: steps.cache-browsers.outputs.cache-hit != 'true'\n        run: pnpm --filter @remix-run/component exec playwright install --with-deps\n\n      - name: Run tests\n        run: pnpm test\n\n  test-windows-pr:\n    name: test (windows-latest, changed packages)\n    if: github.event_name == 'pull_request'\n    runs-on: windows-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Get Playwright Version\n        id: playwright-version\n        shell: bash\n        run: echo \"version=$(pnpm --filter @remix-run/component exec playwright --version | cut -d ' ' -f2)\" >> $GITHUB_OUTPUT\n\n      - name: Cache Playwright Browsers\n        uses: actions/cache@v4\n        id: cache-browsers\n        with:\n          path: ~/AppData/Local/ms-playwright\n          key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}\n\n      - name: Install Playwright Browsers\n        if: steps.cache-browsers.outputs.cache-hit != 'true'\n        run: pnpm --filter @remix-run/component exec playwright install --with-deps\n\n      - name: Run changed package tests\n        run: node ./scripts/detect-changed-packages.ts origin/${{ github.base_ref }}\n\n  test-windows-main:\n    name: test (windows-latest)\n    if: github.event_name == 'push'\n    runs-on: windows-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: 'package.json'\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Get Playwright Version\n        id: playwright-version\n        shell: bash\n        run: echo \"version=$(pnpm --filter @remix-run/component exec playwright --version | cut -d ' ' -f2)\" >> $GITHUB_OUTPUT\n\n      - name: Cache Playwright Browsers\n        uses: actions/cache@v4\n        id: cache-browsers\n        with:\n          path: ~/AppData/Local/ms-playwright\n          key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}\n\n      - name: Install Playwright Browsers\n        if: steps.cache-browsers.outputs.cache-hit != 'true'\n        run: pnpm --filter @remix-run/component exec playwright install --with-deps\n\n      - name: Run tests\n        run: pnpm test\n"
  },
  {
    "path": ".gitignore",
    "content": "dist/\nnode_modules/\npnpm-publish-summary.json\nreference/\n.tmp/\n/demos/tmp/\n*.db\n*.db-*\n*.sqlite\n*.sqlite-*\n*.sqlite3\n*.sqlite3-*\n\n.env\n\n/reference\n"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules/\ndist/\ntmp/\nreference/\n**/*.bundled.*\n**/public/assets/\n**/test/fixtures/\n**/worker-configuration.d.ts\npnpm-lock.yaml\n"
  },
  {
    "path": ".prettierrc",
    "content": "printWidth: 100\nsemi: false\nsingleQuote: true\nuseTabs: false\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"deno.enablePaths\": [\"./packages/multipart-parser/examples/deno\"],\n  \"nodejs-testing.extensions\": [\n    {\n      \"extensions\": [\"ts\", \"tsx\"],\n      \"parameters\": [\"--import\", \"tsx\"]\n    }\n  ],\n  \"typescript.tsdk\": \"./node_modules/typescript/lib\"\n}\n"
  },
  {
    "path": ".vscode/task.json",
    "content": "{\n  \"version\": \"0.1.0\",\n  \"command\": \"./node_modules/.bin/tsc\",\n  \"args\": [\"-v\"],\n  \"echoCommand\": true\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Remix 3 Development Guide\n\n## Commands\n\n- **Build**: `pnpm run build` (all packages) or `pnpm --filter @remix-run/<package> run build` (single package)\n- **Test**: `pnpm test` (all packages) or `pnpm --filter @remix-run/<package> run test` (single package)\n- **Single test file**: `node --test './packages/<package>/src/**/<filename>.test.ts'`\n- **Typecheck**: `pnpm run typecheck` (all packages) or `pnpm --filter @remix-run/<package> run typecheck`\n- **Lint**: `pnpm run lint` (check) or `pnpm run lint:fix` (auto-fix)\n- **Before finishing work**: Run `pnpm run lint` and resolve any lint errors before reporting completion.\n- **Format**: `pnpm run format` (auto-fix) or `pnpm run format:check` (check only)\n- **Clean**: `pnpm run clean` (git clean -fdX)\n\n## Architecture\n\n- **Monorepo**: pnpm workspace with packages in `packages/` directory\n- **Key packages**: headers, fetch-proxy, fetch-router, file-storage, form-data-parser, lazy-file, multipart-parser, node-fetch-server, route-pattern, tar-parser\n- **Package exports**: All `exports` in `package.json` have a dedicated file in `src` that defines the public API by re-exporting from within `src/lib`\n- **Lib module boundaries**: Files in `src/lib` are implementation files. Do not add barrel-style re-exports or thin pass-through wrapper APIs between `src/lib` files. Re-exporting belongs only in top-level `src` barrel files that map to package exports.\n- **Cross-package boundaries**: Avoid re-exporting APIs/types from other packages. Consumers should import from the owning package directly. Reuse shared concepts from sibling packages internally instead of creating bespoke duplicate implementations.\n- **Documentation imports/install**: In package READMEs, documentation, and pull request code examples, installation instructions should always include `npm i remix`, usage examples should import from `remix` package exports (not `@remix-run/*`), and any required peer dependency should be included in the installation command.\n- **Philosophy**: Web standards-first, runtime-agnostic (Node.js, Bun, Deno, Cloudflare Workers). Use Web Streams API, Uint8Array, Web Crypto API, Blob/File instead of Node.js APIs\n- **Tests run from source** (no build required), using Node.js test runner\n\n## Code Style\n\n- **Imports**: Always use `import type { X }` for types (separate from value imports); use `export type { X }` for type exports; include `.ts` extensions\n- **One-off scripts**: Write one-off scripts in this repo as TypeScript and make them executable natively with modern Node.js (for example, executable `.ts` files)\n- **Node runtime assumption**: Assume a modern Node.js runtime that supports running TypeScript files natively; prefer `node path/to/script.ts` in examples and instructions.\n- **Variables**: Prefer `let` for locals, `const` only at module scope; never use `var`\n- **Functions**: Use regular function declarations/expressions by default. For callback-based APIs (array methods, Promise callbacks, test callbacks, transaction callbacks, etc.), prefer arrow functions over `function` expressions. When an arrow callback only returns a single expression, use a concise body (`value => expression`) instead of braces/`return`\n- **Object methods**: When defining functions in object literals, use shorthand method syntax (`{ method() {} }`) instead of arrow functions (`{ method: () => {} }`)\n- **Classes**: Use native fields (omit `public`), `#private` for private members (no TypeScript accessibility modifiers)\n- **Formatting**: Prettier (printWidth: 100, no semicolons, single quotes, spaces not tabs)\n- **TypeScript**: Strict mode, ESNext target, ES2022 modules, bundler resolution, verbatimModuleSyntax\n- **Generics**: Use descriptive lowercase names for type parameters (e.g., `source`, `method`, `pattern`) instead of single uppercase letters like `T`, `P`, or `K`\n- **Comments**: Only add non-JSDoc comments when the code is doing something surprising or non-obvious\n\n## Test Structure\n\n- **No loops or conditionals in test suites**: Do not use `for` loops or conditional statements (`if`, `switch`, etc.) to generate test cases within `describe()` blocks. This breaks the Node.js test runner's ability to run individual tests via IDE features (like clicking test icons in the sidebar).\n\n## Demos\n\n- All demo servers should use port **44100** for consistency across the monorepo\n- **Accessible navigation**: Always use proper `<a>` elements for navigation links. Never use JavaScript `onclick` handlers on non-interactive elements like `<tr>`, `<div>`, or `<span>` for navigation. Links should be keyboard accessible and work with screen readers.\n- **Clean shutdown**: Demo servers should handle `SIGINT` and `SIGTERM` signals to exit cleanly when Ctrl+C is pressed. Close the server and call `process.exit(0)`.\n\n## Documentation\n\n- API documentation is handled by scripts in the docs/ directory\n- We use `typedoc` to process the source code, and then generate markdown files from the typedoc output\n- Markdown API documentation files be generated via `pnpm run docs` in the docs/ directory\n\n## Changes and Releases\n\n- **Automated releases**: When changes are pushed to `main`, the [release-pr workflow](/.github/workflows/release-pr.yaml) automatically opens/updates a \"Release\" PR. The [publish workflow](/.github/workflows/publish.yaml) runs on every push to `main` and publishes when no change files are present (i.e., after merging the Release PR).\n- **Manual releases**: `pnpm changes:version` updates package.json, CHANGELOG.md, and creates a git commit. Push to `main` and the publish workflow will handle the rest (including tags and GitHub releases).\n- **How publishing works**: The publish workflow checks for change files. If none exist, it runs `pnpm publish --recursive --report-summary`, reads the summary JSON to see what was published, then creates git tags and GitHub releases for each published package.\n- **Test change/release code with preview scripts**: When modifying any change/release code, run `pnpm changes:preview` to test locally. For the release PR script, run `node ./scripts/release-pr.ts --preview`. For the publish script, run `node ./scripts/publish.ts --dry-run` to see what commands would be executed without actually publishing.\n\n## Skills\n\nA skill is a reusable local instruction set stored in a `SKILL.md` file.\n\n### Available skills\n\n- **add-package**: Create or align a package in the Remix monorepo to match existing package conventions. Use when adding a brand new package under packages/, or when fixing an existing package's structure, test setup, TypeScript/build config, code style, and README layout to match the rest of Remix 3. (file: `./.agents/skills/add-package/SKILL.md`)\n- **make-change-file**: Create or update package change files using Remix repo conventions, deterministic naming, and release-note style. (file: `./.agents/skills/make-change-file/SKILL.md`)\n- **make-demo**: Create or revise demos in the Remix repository so they stay focused on Remix packages, strong code hygiene, and production-quality patterns. (file: `./.agents/skills/make-demo/SKILL.md`)\n- **make-pr**: Create GitHub pull requests with clear context, issue/feature bullets, and required usage examples for new or changed APIs. (file: `./.agents/skills/make-pr/SKILL.md`)\n- **publish-placeholder-package**: Publish a minimal npm package at `0.0.0` to reserve the name and enable npm OIDC setup before CI-based publishing. (file: `./.agents/skills/publish-placeholder-package/SKILL.md`)\n- **supersede-pr**: Replace one GitHub PR with another and explicitly close the superseded PR (instead of relying on `Closes #...` keywords). (file: `./.agents/skills/supersede-pr/SKILL.md`)\n- **update-pr**: Rewrite GitHub PR titles and descriptions from scratch so they match the PR as it exists now, and always review the title when updating the body. (file: `./.agents/skills/update-pr/SKILL.md`)\n- **write-api-docs**: Write or audit public API docs for Remix packages. Use when adding or tightening JSDoc on exported functions, classes, interfaces, type aliases, or option objects. (file: `./.agents/skills/write-api-docs/SKILL.md`)\n- **write-readme**: Write or rewrite Remix package READMEs using this repo's structure, installation conventions, production-style examples, and section ordering. (file: `./.agents/skills/write-readme/SKILL.md`)\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Welcome to Remix! We're excited to have you contribute.\n\nThis guide will help you get started.\n\n## Setting Up Your Environment\n\nWe develop Remix using [pnpm](https://pnpm.io) on Node 24.3+.\n\nIf you're using [VS Code](https://code.visualstudio.com/), we recommend installing the [`node:test runner` extension](https://marketplace.visualstudio.com/items?itemName=connor4312.nodejs-testing) for a smooth testing experience.\n\nOnce that's set up, run `pnpm install` to get all the project dependencies.\n\n## Testing\n\nAll tests run directly from source. This makes it easy to use breakpoint debugging when running tests. This also means you should not need to run a build before running the tests.\n\n```sh\n# Run all tests\n$ pnpm test\n# Run the tests for a specific package\n$ pnpm --filter @remix-run/headers run test\n```\n\n## Building\n\nAll packages are built using a combination of tsc and esbuild.\n\n```sh\n# Build all packages\n$ pnpm run build\n# Build a specific package\n$ pnpm --filter @remix-run/headers run build\n```\n\nAll packages are published with TypeScript types along with both ESM and CJS module formats.\n\n## Making Changes\n\nPackages live in the [`packages` directory](https://github.com/remix-run/remix/tree/v3/packages). At a minimum, each package includes:\n\n- `.changes/`: Directory containing change files for the next release\n- `CHANGELOG.md`: A log of what's changed\n- `package.json`: Package metadata and dependencies\n- `README.md`: Information about the package\n- `src/`: The package's source code\n\nWhen you make changes to a package, please make sure you add a few relevant tests and run the whole test suite to make sure everything still works. Then, [add a change file](#adding-a-change-file) describing your changes and [make a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). We will take a look at it as soon as we can.\n\n### Adding a Change File\n\nWhen making changes to a package, create a markdown file in the package's `.changes/` directory following this naming convention:\n\n```\n[major|minor|patch].short-description.md\n```\n\n- `major` - Breaking changes for v1.x+ packages\n- `minor` - Breaking changes for v0.x packages, new features\n- `patch` - Bug fixes\n\n#### Examples\n\n- `major.change-something.md` - Breaking change for v1.x+ packages\n- `minor.change-something.md` - Breaking change for v0.x packages\n- `minor.add-something.md` - New feature\n- `patch.fix-something.md` - Bug fix\n\n#### Content Format\n\nWrite your change as a bullet point (without the leading `-` or `*`). This content will be added to the CHANGELOG during release.\n\n```markdown\nAdd support for X feature\n\nThis is an optional longer explanation that will be indented\nunder the main bullet point in the CHANGELOG.\n```\n\nFor breaking changes in v0.x packages, any change files that begin with `BREAKING CHANGE: ` will be hoisted to the top of the release notes:\n\n```markdown\nBREAKING CHANGE: Renamed `foo` option to `bar`\n\nMigration: Update your config to use `bar` instead of `foo`.\n```\n\n#### Validation\n\nChange files are automatically validated in CI. You can also validate them locally:\n\n```sh\npnpm changes:validate\n```\n\n## Releases\n\nReleases are automated via the [release-pr workflow](/.github/workflows/release-pr.yaml) and [publish workflow](/.github/workflows/publish.yaml).\n\n1. **You push changes to `main`** with change files in `packages/*/.changes/`\n\n2. **A \"Release\" PR is automatically opened** (or updated if one exists)\n\n   The PR contains:\n\n   - Updated `package.json` versions\n   - Updated `CHANGELOG.md` files\n   - Deleted change files\n\n   This PR should not be edited manually. If you need to make changes, modify the change files and/or scripts in `main` to trigger an update to the PR.\n\n3. **When you merge the PR**, the publish workflow runs (it runs on every push to `main` and checks for change files). Since the change files have been deleted, it publishes all unpublished packages to npm, then creates git tags and GitHub releases based on what was actually published.\n\n### Manual Versioning\n\nThe \"Release\" PR simply automates the `pnpm changes:version` command. If needed, you can run this command manually. This will update the `package.json` versions, `CHANGELOG.md` files, and delete the change files. It will then commit the result.\n\n```sh\npnpm changes:version\n```\n\nYou can skip committing the changes by using the `--no-commit` flag. This will leave the changes in a staged state for you to review and commit manually. The command will also output the commit message that would have been used.\n\n```sh\npnpm changes:version --no-commit\n```\n\nTags and GitHub releases are created automatically by the publish workflow after successful npm publish.\n\n### Prerelease Mode for `remix`\n\nThe `remix` package supports prerelease mode via an optional `.changes/config.json` file:\n\n```json\n{\n  \"prereleaseChannel\": \"alpha\"\n}\n```\n\nThe `prereleaseChannel` field determines the version suffix (e.g. `alpha`, `beta`, `rc`), while prereleases are always published to npm with the `next` tag. This is only supported for `remix` because it's the only package that needs to publish prereleases alongside an existing stable version on npm. All other packages in this monorepo are new and publish directly as `latest`.\n\n#### Bumping `remix` prerelease versions\n\nWhile in prerelease mode, add change files as normal. The prerelease counter increments (e.g. `3.0.0-alpha.1` → `3.0.0-alpha.2`). Changelog entries still get proper \"Major Changes\" / \"Minor Changes\" / \"Patch Changes\" sections, but the bump type is otherwise ignored—only the prerelease counter is bumped.\n\n#### Transitioning between `remix` prerelease channels\n\nTo transition between channels (e.g. `alpha` → `beta`):\n\n1. Update `prereleaseChannel` in `.changes/config.json` to the new channel\n2. Add a change file describing the transition\n\nVersion resets to the new channel (e.g. `3.0.0-alpha.7` → `3.0.0-beta.0`). The bump type is for changelog categorization only—by convention, use `patch`.\n\n#### Graduating `remix` to stable\n\nTo release the stable version:\n\n1. Remove `prereleaseChannel` from `.changes/config.json` (or delete the file)\n2. Add a change file describing the stable release\n\nThe prerelease suffix is stripped (e.g. `3.0.0-rc.7` → `3.0.0`). The bump type is for changelog categorization only—by convention, use `major` for a major release announcement.\n\n## Preview builds\n\nWe maintain installable builds of `main` in a `preview/main` branch as a way for folks to test out the latest `main` branch without needing to publish releases to npm and clutter up the npm registry and version history UI.\n\nThis is managed via the [`preview` workflow](/.github/workflows/preview.yaml) which uses the [`setup-installable-branch.ts`](./scripts/setup-installable-branch.ts) script to build and commit the build and required `package.json` changes to the `preview/main` branch on every new commit to `main`.\n\nThe `preview/main` branch build can be [installed directly](https://pnpm.io/package-sources#install-from-a-git-repository-combining-different-parameters) with `pnpm` (version 9+):\n\n```sh\npnpm install \"remix-run/remix#preview/main&path:packages/remix\"\n\n# Or, just install a single package\npnpm install \"remix-run/remix#preview/main&path:packages/fetch-router\"\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Welcome to Remix 3!\n\nThis is the source repository for Remix 3. It is under active development.\n\nWe published [a blog post](https://remix.run/blog/wake-up-remix) earlier this year with some of our thoughts around Remix 3. It explains our philosophy for web development and why we think the time is right for something new. When working on Remix 3, we follow these principles:\n\n1. **Model-First Development**. AI fundamentally shifts the human-computer interaction model for both user experience and developer workflows. Optimize the source code, documentation, tooling, and abstractions for LLMs. Additionally, develop abstractions for applications to use models in the product itself, not just as a tool to develop it.\n2. **Build on Web APIs**. Sharing abstractions across the stack greatly reduces the amount of context switching, both for humans and machines. Build on the foundation of Web APIs and JavaScript because it is the only full stack ecosystem.\n3. **Religiously Runtime**. Designing for bundlers/compilers/typegen (and any pre-runtime static analysis) leads to poor API design that eventually pollutes the entire system. All packages must be designed with no expectation of static analysis and all tests must run without bundling. Because browsers are involved, `--import` loaders for simple transformations like TypeScript and JSX are permissible.\n4. **Avoid Dependencies**. Dependencies lock you into somebody else's roadmap. Choose them wisely, wrap them completely, and expect to replace most of them with our own package eventually. The goal is zero.\n5. **Demand Composition**. Abstractions should be single-purpose and replaceable. A composable abstraction is easy to add and remove from an existing program. Every package must be useful and documented independent of any other context. New features should first be attempted as a new package. If impossible, attempt to break up the existing package to make it more composable. However, tightly coupled modules that almost always change together in both directions should be moved to the same package.\n6. **Distribute Cohesively**. Extremely composable ecosystems are difficult to learn and use. Remix will be distributed as a single `remix` package for both distribution and documentation.\n\n## Goals\n\nAlthough we recommend the `remix` package for ease of use, all packages that make up Remix should be usable standalone as well. This forces us to consider package boundaries and helps us define public interfaces that are portable and interoperable.\n\nEach package in Remix:\n\n- Has a [single responsibility](https://en.wikipedia.org/wiki/Single-responsibility_principle)\n- Prioritizes web standards to ensure maximum interoperability and portability across JavaScript runtimes\n- Augments standards unobtrusively where they are missing or incomplete, minimizing incompatibility risks\n\nThis means Remix code is **portable by default**. Remix packages work seamlessly across [Node.js](https://nodejs.org/), [Bun](https://bun.sh/), [Deno](https://deno.com/), [Cloudflare Workers](https://workers.cloudflare.com/), and other environments.\n\nWe leverage server-side web APIs when they are available:\n\n- [The Web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) instead of `node:stream`\n- [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) instead of Node.js `Buffer`s\n- [The Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) instead of `node:crypto`\n- [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) instead of some bespoke runtime-specific API\n\nThe benefit is code that's not just reusable, but **future-proof**.\n\n## Packages\n\nWe currently publish the following packages:\n\n- [async-context-middleware](packages/async-context-middleware): Middleware for storing request context in AsyncLocalStorage\n- [component](packages/component): UI components for Remix\n- [compression-middleware](packages/compression-middleware): Middleware for compressing HTTP responses\n- [cop-middleware](packages/cop-middleware): Middleware for tokenless cross-origin protection in Fetch API servers\n- [cors-middleware](packages/cors-middleware): Middleware for handling CORS in Fetch API servers\n- [csrf-middleware](packages/csrf-middleware): Middleware for CSRF protection in Fetch API servers\n- [cookie](packages/cookie): A toolkit for working with cookies in JavaScript\n- [data-schema](packages/data-schema): Tiny, standards-aligned schema validation\n- [data-table](packages/data-table): A typed, relational query toolkit for Remix\n- [data-table-mysql](packages/data-table-mysql): MySQL adapter for remix/data-table\n- [data-table-postgres](packages/data-table-postgres): PostgreSQL adapter for remix/data-table\n- [data-table-sqlite](packages/data-table-sqlite): SQLite adapter for remix/data-table\n- [fetch-proxy](packages/fetch-proxy): An HTTP proxy for the web Fetch API\n- [fetch-router](packages/fetch-router): A minimal, composable router for the web Fetch API\n- [file-storage](packages/file-storage): Key/value storage for JavaScript File objects\n- [file-storage-s3](packages/file-storage-s3): S3 backend for remix/file-storage\n- [form-data-middleware](packages/form-data-middleware): Middleware for parsing FormData from request bodies\n- [form-data-parser](packages/form-data-parser): A request.formData() wrapper with streaming file upload handling\n- [fs](packages/fs): Filesystem utilities using the Web File API\n- [headers](packages/headers): A toolkit for working with HTTP headers in JavaScript\n- [html-template](packages/html-template): HTML template tag with auto-escaping for JavaScript\n- [lazy-file](packages/lazy-file): Lazy, streaming files for JavaScript\n- [logger-middleware](packages/logger-middleware): Middleware for logging HTTP requests and responses\n- [method-override-middleware](packages/method-override-middleware): Middleware for overriding HTTP request methods from form data\n- [mime](packages/mime): Utilities for working with MIME types\n- [multipart-parser](packages/multipart-parser): A fast, efficient parser for multipart streams in any JavaScript environment\n- [node-fetch-server](packages/node-fetch-server): Build servers for Node.js using the web fetch API\n- [remix](packages/remix): Remix Web Framework\n- [response](packages/response): Response helpers for the web Fetch API\n- [route-pattern](packages/route-pattern): Match and generate URLs with strong typing\n- [session](packages/session): Session management for JavaScript\n- [session-middleware](packages/session-middleware): Middleware for managing sessions with cookie-based storage\n- [session-storage-memcache](packages/session-storage-memcache): Memcache session storage for remix/session\n- [session-storage-redis](packages/session-storage-redis): Redis session storage for remix/session\n- [static-middleware](packages/static-middleware): Middleware for serving static files from the filesystem\n- [tar-parser](packages/tar-parser): A fast, efficient parser for tar streams in any JavaScript environment\n\n## Installation\n\nTo try the current Remix alpha, install the `next` dist-tag:\n\n```sh\nnpm install remix@next\n```\n\nIf you want to play around with the bleeding edge, we also build the latest `main` branch into a `preview/main` branch which can be [installed directly](https://pnpm.io/package-sources#install-from-a-git-repository-combining-different-parameters) with `pnpm` (version 9+):\n\n```sh\npnpm install \"remix-run/remix#preview/main&path:packages/remix\"\n\n# Or, just install a single package\npnpm install \"remix-run/remix#preview/main&path:packages/fetch-router\"\n```\n\n## Contributing\n\nWe welcome contributions! If you'd like to contribute, please feel free to open an issue or submit a pull request. See [CONTRIBUTING](https://github.com/remix-run/remix/blob/main/CONTRIBUTING.md) for more information.\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "cspell.yml",
    "content": "version: '0.2'\nlanguage: en\nwords:\n  - accentunder\n  - actiontype\n  - activedescendant\n  - aftertoggle\n  - arcrole\n  - Arcrole\n  - autocorrect\n  - backlink\n  - bbox\n  - biblioentry\n  - biblioref\n  - Booleanish\n  - braillelabel\n  - brailleroledescription\n  - closedby\n  - closerequest\n  - colcount\n  - colindex\n  - colindextext\n  - columnalign\n  - columnlines\n  - columnspacing\n  - columnspan\n  - commandfor\n  - contenteditable\n  - contentinfo\n  - controlslist\n  - denomalign\n  - describedby\n  - desync\n  - disableremoteplayback\n  - displaystyle\n  - DOMCSS\n  - dropeffect\n  - elementtiming\n  - enterkeyhint\n  - exportparts\n  - fetchpriority\n  - fixpoint\n  - flowto\n  - focusin\n  - focusout\n  - fontstyle\n  - fontweight\n  - formaction\n  - formenctype\n  - formmethod\n  - formnovalidate\n  - formtarget\n  - FOUC\n  - framespacing\n  - glossref\n  - haspopup\n  - healthcheck\n  - horiz\n  - hsba\n  - inlist\n  - inputmode\n  - itemprop\n  - itemref\n  - itemscope\n  - itemtype\n  - jsxs\n  - keybind\n  - keyshortcuts\n  - keyup\n  - labelledby\n  - largeop\n  - linethickness\n  - lquote\n  - lspace\n  - maction\n  - mathbackground\n  - mathcolor\n  - mathsize\n  - mathvariant\n  - maxlength\n  - maxsize\n  - menclose\n  - menuitemcheckbox\n  - menuitemradio\n  - merror\n  - mfenced\n  - mfrac\n  - Microdata\n  - minsize\n  - Mmulti\n  - mmultiscripts\n  - movablelimits\n  - mpadded\n  - mpath\n  - mphantom\n  - mprescripts\n  - mroot\n  - mrow\n  - mspace\n  - msqrt\n  - mstyle\n  - msub\n  - msubsup\n  - msup\n  - mtable\n  - mtext\n  - multiselectable\n  - munder\n  - munderover\n  - noteref\n  - novalidate\n  - numalign\n  - outerclick\n  - pagebreak\n  - pagelist\n  - panose\n  - playsinline\n  - pointerdown\n  - pointerenter\n  - pointerleave\n  - pointermove\n  - pointerup\n  - popovertarget\n  - popovertargetaction\n  - posinset\n  - pullquote\n  - referrerpolicy\n  - renderable\n  - roledescription\n  - roletype\n  - rowalign\n  - rowcount\n  - rowindex\n  - rowindextext\n  - rowlines\n  - rowspacing\n  - rowspan\n  - rquote\n  - rspace\n  - scriptlevel\n  - scriptminsize\n  - scriptsizemultiplier\n  - sectionhead\n  - setsize\n  - Signalish\n  - spinbutton\n  - statusline\n  - stemh\n  - stemv\n  - subscriptshift\n  - Subsup\n  - superscriptshift\n  - SVGM\n  - treegrid\n  - treeitems\n  - unitless\n  - unkeyed\n  - valuenow\n  - valuetext\n  - vdom\n  - visibilitychange\n  - vnode\n  - VNODE\n  - vnodes\n  - voffset\n  - wmode\n  - xlink\nignorePaths:\n  - node_modules/**\n  - dist/**\n  - build/**\n  - pnpm-lock.yaml\n"
  },
  {
    "path": "decisions/001-route-pattern-vs-url-pattern.md",
    "content": "# RoutePattern vs. URLPattern\n\nThe web has a built-in URL matcher called [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern). Why don't we use it instead of creating our own thing with [`RoutePattern`](../packages/route-pattern)?\n\n## Main Differences\n\nThere are a number of major differences between `RoutePattern` and `URLPattern`. Here are the main ones:\n\n- **Generating URLs**: `RoutePattern` comes with an \"href builder\" for building URLs from route patterns. This is the logical bookend to \"matching\" or parsing a URL.\n\n- **Easier pathname-only Matching**: `RoutePattern` allows matching only a URL pathname without resorting to the object syntax, or beginning with a leading `/`\n\n```tsx\nlet pattern = new RoutePattern('products/:id') // matches <protocol>://<host>/products/:id\n// vs. URLPattern, requires object syntax and leading slash\nlet pattern = new URLPattern({ pathname: '/products/:id' })\n```\n\n- **Non-exhaustive Search Matching**: `RoutePattern` does not treat the pattern's search string as exhaustive. This allows matching URLs that contain additional query parameters, which is important for allowing traffic that comes from sources where you don't have full control over the search string.\n\n```tsx\nlet pattern = new RoutePattern('?q=remix')\npattern.match('https://remix.run/?q=remix') // match\npattern.match('https://remix.run/?q=remix&utm_source') // also match!\n\nlet pattern = new URLPattern({ search: '?q=remix' })\npattern.exec('https://remix.run/?q=remix') // match\npattern.exec('https://remix.run/?q=remix&utm_source') // null :(\n```\n\n- **More Intuitive Optionals**: `RoutePattern` expresses optionals using parentheses, similar to Rails. These read like English instead of using `?` to indicate optional groups as in regular expressions. It also makes the start and end positions of an optional group immediately obvious.\n\n```tsx\n// An \"optional group\" using URLPattern\nlet pattern = new URLPattern('/books/:id?', 'https://example.com')\npattern.test('https://example.com/books/123') // true\npattern.test('https://example.com/books') // true\n// This behavior is unintuitive. Is the \":id\" optional? Or the \"/:id\"?\n// There's no way to know when you only have a single group modifier character (the `?`)\npattern.test('https://example.com/books/') // false\n\n// An optional using RoutePattern\nlet pattern = new RoutePattern('/books(/:id)')\npattern.test('https://example.com/books/123') // true\npattern.test('https://example.com/books') // true\n// This result is more intuitive because the () surround the optional\n// portion of the pattern, indicating both start and end characters\npattern.test('https://example.com/books/') // false\n```\n\n- **Uniform Param Access**: `RoutePattern` does not support \"unnamed groups\" that must be accessed by index in the match result. Instead, all variables (groups) must have names and are accessed by that name at `match.params[name]`.\n\n- **No RegExp Syntax**: `RoutePattern` does not allow regex syntax. This means route patterns are statically analyzable without parsing RegExp grammar, which makes it easier to provide type safety. Also, the whole point of `RoutePattern` is to provide a syntax that is sufficient for matching URLs without resorting to some other syntax.\n"
  },
  {
    "path": "decisions/002-branching-and-releasing.md",
    "content": "# Branching and Releasing in Remix 3\n\nBeginning in Remix 3 the `remix` package is an umbrella package for everything in Remix. This includes a number of \"sub-packages\" that are all published under the `@remix-run/*` scope. The `remix` package re-exports everything from all sub-packages as a transparent pass-thru. This means users only have to install `remix` to get everything we publish, and they can import everything from some `remix/*` export.\n\nWe anticipate the majority of Remix development will happen directly on the `main` branch. `main` should always be publishable. We will publish minor/patch changes from `main` often for both `remix` and any sub-package that needs it.\n\n## Breaking Changes\n\nWhen it comes to breaking changes (that require a major version bump), we have 2 goals:\n\n- Be slow and deliberate about cutting major `remix` releases so we don't stress people out by releasing majors too often\n- Release breaking changes in sub-packages as soon as they are ready so people can play with them\n\nMajor `remix` releases will happen on a predetermined schedule so that users may plan upgrades into their development lifecycle.\n\nBreaking changes will accumulate on a `future` branch. The `future` branch is a preview of what the next major version of Remix will look like. If someone wants to play with the latest stuff, they can build directly from `future`. We don't make any guarantees about the [stability][^stability] of `future`, which is why users must build from source.\n\nWe will publish new majors of sub-packages as soon as they are ready from the `future` branch. When it's time to cut the next major `remix` release, we will merge `future` into `main`. Of course, this means that `main` should be merged into `future` periodically to make this easier.\n\n[^stability]: By \"stable\" we mean \"won't break between releases\". Both `main` and `future` should always pass all tests and be usable, but on `main` we have versions and stability guarantees between them. On `future`, we don't.\n"
  },
  {
    "path": "demos/bookstore/.gitignore",
    "content": "public/assets/\ntmp/\ndata/*.sqlite\n"
  },
  {
    "path": "demos/bookstore/README.md",
    "content": "# Bookstore Demo\n\nA full-featured e-commerce bookstore demonstrating the most powerful patterns and features of Remix. This demo showcases authentication, shopping cart, admin CRUD operations, file uploads, progressive enhancement, and much more.\n\n## Running the Demo\n\n```bash\ncd demos/bookstore\npnpm install\npnpm start\n```\n\nThen visit http://localhost:44100\n\n### Demo Accounts\n\n- **Admin**: admin@bookstore.com / admin123\n- **Customer**: customer@example.com / password123\n\n## Database and Migrations\n\n- The SQLite file is stored at `data/bookstore.sqlite`\n- Migration files live in `data/migrations`\n- On startup, the app loads migrations from `data/migrations` and runs pending migrations before seeding demo data\n\n## Code Highlights\n\n- [`app/routes.ts`](app/routes.ts) shows declarative route definitions using `route()`, `form()`, and `resources()` helpers. All route URLs are generated with full type safety, so `routes.admin.books.edit.href({ bookId: '123' })` ensures you never have broken links.\n- [`app/router.ts`](app/router.ts) demonstrates how to compose middleware for cross-cutting concerns: static file serving, form data parsing, method override, sessions, and async context. Each middleware is independent and reusable.\n- [`data/migrations/20260228090000_create_bookstore_schema.ts`](data/migrations/20260228090000_create_bookstore_schema.ts) defines the schema using `remix/data-table/migrations`.\n- [`app/middleware/database.ts`](app/middleware/database.ts) stores the bookstore database on request context with `context.set(Database, db)`, and request handlers read it back with `get(Database)` just like they do for `Session` and `FormData`.\n- [`app/middleware/auth.ts`](app/middleware/auth.ts) provides two patterns:\n  - **`loadAuth()`** - Optionally loads the current user without requiring authentication\n  - **`requireAuth()`** - Redirects to login with a `returnTo` parameter for post-login redirect\n- [`app/middleware/admin.ts`](app/middleware/admin.ts) shows role-based authorization that returns 403 for non-admin users.\n- [`app/utils/context.ts`](app/utils/context.ts) demonstrates sharing data across the request lifecycle without prop drilling. Any code can call `getCurrentUser()` to access the authenticated user set by middleware earlier in the chain.\n- [`app/utils/session.ts`](app/utils/session.ts) configures signed cookies and filesystem-based session storage.\n- [`app/utils/uploads.ts`](app/utils/uploads.ts) handles file uploads with `@remix-run/form-data-middleware`. The upload handler stores files and returns public URLs. [`app/uploads.tsx`](app/uploads.tsx) serves uploaded files with appropriate caching headers.\n- HTML forms only support GET and POST. [`app/components/restful-form.tsx`](app/components/restful-form.tsx) adds a hidden `_method` field for PUT and DELETE, which the `methodOverride()` middleware translates back to the original method.\n- [`app/assets/cart-button.tsx`](app/assets/cart-button.tsx) shows a button that works without JavaScript (full form submission) but upgrades to fetch-based updates when JS is available. Notice how `hydrated()` wraps a component that maintains local state (`updating`) and calls `this.update()` to re-render.\n- [`app/assets/image-carousel.tsx`](app/assets/image-carousel.tsx) demonstrates a similar pattern for an interactive image carousel.\n- [`app/books.tsx`](app/books.tsx) uses `<Frame>` to render book cards that can be loaded independently. The frame URLs point to [`app/fragments.tsx`](app/fragments.tsx), and [`app/utils/frame.tsx`](app/utils/frame.tsx) shows how frames are resolved server-side during initial render.\n- [`app/admin.books.tsx`](app/admin.books.tsx) demonstrates complete CRUD operations with file uploads, using the RESTful routes generated by `resources('books')`.\n"
  },
  {
    "path": "demos/bookstore/app/account.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.ts'\nimport { loginAsCustomer, requestWithSession, assertContains } from '../test/helpers.ts'\n\ndescribe('account handlers', () => {\n  it('GET /account redirects to login when not authenticated', async () => {\n    let response = await router.fetch('https://remix.run/account')\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/login?returnTo=%2Faccount')\n  })\n\n  it('GET /account returns account page when authenticated', async () => {\n    let sessionId = await loginAsCustomer(router)\n\n    // Now access account page with session\n    let request = requestWithSession('https://remix.run/account', sessionId)\n    let response = await router.fetch(request)\n\n    assert.equal(response.status, 200)\n    let html = await response.text()\n    assertContains(html, 'My Account')\n    assertContains(html, 'Account Information')\n    assertContains(html, 'John Doe')\n  })\n\n  it('GET /account/orders/:orderId shows order for authenticated user', async () => {\n    let sessionId = await loginAsCustomer(router)\n\n    // Access existing order\n    let request = requestWithSession('https://remix.run/account/orders/1001', sessionId)\n    let response = await router.fetch(request)\n\n    assert.equal(response.status, 200)\n    let html = await response.text()\n    assertContains(html, 'Order #1001')\n    assertContains(html, 'Ash &amp; Smoke')\n  })\n\n  it('GET /account/orders shows item counts from normalized order items', async () => {\n    let sessionId = await loginAsCustomer(router)\n    let request = requestWithSession('https://remix.run/account/orders', sessionId)\n    let response = await router.fetch(request)\n\n    assert.equal(response.status, 200)\n    let html = await response.text()\n    assertContains(html, '2 item(s)')\n    assertContains(html, '1 item(s)')\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/account.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\nimport * as s from 'remix/data-schema'\nimport * as f from 'remix/data-schema/form-data'\nimport { Database } from 'remix/data-table'\nimport { redirect } from 'remix/response/redirect'\n\nimport { routes } from './routes.ts'\nimport { Layout } from './layout.tsx'\nimport { requireAuth } from './middleware/auth.ts'\nimport { orders, orderItemsWithBook, users } from './data/schema.ts'\nimport { getCurrentUser } from './utils/context.ts'\nimport { parseId } from './utils/ids.ts'\nimport { render } from './utils/render.ts'\nimport { RestfulForm } from './components/restful-form.tsx'\n\nconst textField = f.field(s.defaulted(s.string(), ''))\nconst accountSettingsSchema = f.object({\n  name: textField,\n  email: textField,\n  password: textField,\n})\n\nexport default {\n  middleware: [requireAuth()],\n  actions: {\n    index() {\n      let user = getCurrentUser()\n\n      return render(\n        <Layout>\n          <h1>My Account</h1>\n\n          <div class=\"card\">\n            <h2>Account Information</h2>\n            <p>\n              <strong>Name:</strong> {user.name}\n            </p>\n            <p>\n              <strong>Email:</strong> {user.email}\n            </p>\n            <p>\n              <strong>Role:</strong> {user.role}\n            </p>\n            <p>\n              <strong>Member Since:</strong> {new Date(user.created_at).toLocaleDateString()}\n            </p>\n\n            <p mix={[css({ marginTop: '1.5rem' })]}>\n              <a href={routes.account.settings.index.href()} class=\"btn\">\n                Edit Settings\n              </a>\n            </p>\n          </div>\n\n          <div class=\"card\" mix={[css({ marginTop: '1.5rem' })]}>\n            <h2>Quick Links</h2>\n            <p>\n              <a href={routes.account.orders.index.href()} class=\"btn btn-secondary\">\n                View Orders\n              </a>\n              <a\n                href={routes.books.index.href()}\n                class=\"btn btn-secondary\"\n                mix={[css({ marginLeft: '0.5rem' })]}\n              >\n                Browse Books\n              </a>\n            </p>\n          </div>\n        </Layout>,\n      )\n    },\n\n    settings: {\n      actions: {\n        index() {\n          let user = getCurrentUser()\n\n          return render(\n            <Layout>\n              <h1>Account Settings</h1>\n\n              <div class=\"card\">\n                <RestfulForm method=\"PUT\" action={routes.account.settings.update.href()}>\n                  <div class=\"form-group\">\n                    <label for=\"name\">Name</label>\n                    <input type=\"text\" id=\"name\" name=\"name\" value={user.name} required />\n                  </div>\n\n                  <div class=\"form-group\">\n                    <label for=\"email\">Email</label>\n                    <input type=\"email\" id=\"email\" name=\"email\" value={user.email} required />\n                  </div>\n\n                  <div class=\"form-group\">\n                    <label for=\"password\">New Password (leave blank to keep current)</label>\n                    <input\n                      type=\"password\"\n                      id=\"password\"\n                      name=\"password\"\n                      autoComplete=\"new-password\"\n                    />\n                  </div>\n\n                  <button type=\"submit\" class=\"btn\">\n                    Update Settings\n                  </button>\n                  <a\n                    href={routes.account.index.href()}\n                    class=\"btn btn-secondary\"\n                    mix={[css({ marginLeft: '0.5rem' })]}\n                  >\n                    Cancel\n                  </a>\n                </RestfulForm>\n              </div>\n            </Layout>,\n          )\n        },\n\n        async update({ get }) {\n          let db = get(Database)\n          let formData = get(FormData)\n          let user = getCurrentUser()\n\n          let { email, name, password } = s.parse(accountSettingsSchema, formData)\n\n          let updateData: any = { name, email }\n          if (password) {\n            updateData.password = password\n          }\n\n          await db.update(users, user.id, updateData)\n\n          return redirect(routes.account.index.href())\n        },\n      },\n    },\n\n    orders: {\n      actions: {\n        async index({ get }) {\n          let db = get(Database)\n          let user = getCurrentUser()\n          let userOrders = await db.findMany(orders, {\n            where: { user_id: user.id },\n            orderBy: ['created_at', 'asc'],\n            with: { items: orderItemsWithBook },\n          })\n\n          return render(\n            <Layout>\n              <h1>My Orders</h1>\n\n              <div class=\"card\">\n                {userOrders.length > 0 ? (\n                  <table>\n                    <thead>\n                      <tr>\n                        <th>Order ID</th>\n                        <th>Date</th>\n                        <th>Items</th>\n                        <th>Total</th>\n                        <th>Status</th>\n                        <th>Actions</th>\n                      </tr>\n                    </thead>\n                    <tbody>\n                      {userOrders.map((order) => (\n                        <tr>\n                          <td>#{order.id}</td>\n                          <td>{new Date(order.created_at).toLocaleDateString()}</td>\n                          <td>{order.items.length} item(s)</td>\n                          <td>${order.total.toFixed(2)}</td>\n                          <td>\n                            <span class=\"badge badge-info\">{order.status}</span>\n                          </td>\n                          <td>\n                            <a\n                              href={routes.account.orders.show.href({ orderId: order.id })}\n                              class=\"btn btn-secondary\"\n                              mix={[css({ fontSize: '0.875rem', padding: '0.25rem 0.5rem' })]}\n                            >\n                              View\n                            </a>\n                          </td>\n                        </tr>\n                      ))}\n                    </tbody>\n                  </table>\n                ) : (\n                  <p>You have no orders yet.</p>\n                )}\n              </div>\n\n              <p mix={[css({ marginTop: '1.5rem' })]}>\n                <a href={routes.account.index.href()} class=\"btn btn-secondary\">\n                  Back to Account\n                </a>\n              </p>\n            </Layout>,\n          )\n        },\n\n        async show({ get, params }) {\n          let db = get(Database)\n          let user = getCurrentUser()\n          let orderId = parseId(params.orderId)\n          let order =\n            orderId === undefined\n              ? undefined\n              : await db.find(orders, orderId, {\n                  with: { items: orderItemsWithBook },\n                })\n\n          if (!order || order.user_id !== user.id) {\n            return render(\n              <Layout>\n                <div class=\"card\">\n                  <h1>Order Not Found</h1>\n                  <p>\n                    <a href={routes.account.orders.index.href()} class=\"btn\">\n                      Back to Orders\n                    </a>\n                  </p>\n                </div>\n              </Layout>,\n              { status: 404 },\n            )\n          }\n\n          let shippingAddress = JSON.parse(order.shipping_address_json) as {\n            street: string\n            city: string\n            state: string\n            zip: string\n          }\n\n          return render(\n            <Layout>\n              <h1>Order #{order.id}</h1>\n\n              <div class=\"card\">\n                <p>\n                  <strong>Order Date:</strong> {new Date(order.created_at).toLocaleDateString()}\n                </p>\n                <p>\n                  <strong>Status:</strong> <span class=\"badge badge-info\">{order.status}</span>\n                </p>\n\n                <h2 mix={[css({ marginTop: '2rem' })]}>Items</h2>\n                <table mix={[css({ marginTop: '1rem' })]}>\n                  <thead>\n                    <tr>\n                      <th>Book</th>\n                      <th>Quantity</th>\n                      <th>Price</th>\n                      <th>Subtotal</th>\n                    </tr>\n                  </thead>\n                  <tbody>\n                    {order.items.map((item) => (\n                      <tr>\n                        <td>{item.title}</td>\n                        <td>{item.quantity}</td>\n                        <td>${item.unit_price.toFixed(2)}</td>\n                        <td>${(item.unit_price * item.quantity).toFixed(2)}</td>\n                      </tr>\n                    ))}\n                  </tbody>\n                  <tfoot>\n                    <tr>\n                      <td colSpan={3} mix={[css({ textAlign: 'right', fontWeight: 'bold' })]}>\n                        Total:\n                      </td>\n                      <td mix={[css({ fontWeight: 'bold' })]}>${order.total.toFixed(2)}</td>\n                    </tr>\n                  </tfoot>\n                </table>\n\n                <h2 mix={[css({ marginTop: '2rem' })]}>Shipping Address</h2>\n                <p>{shippingAddress.street}</p>\n                <p>\n                  {shippingAddress.city}, {shippingAddress.state} {shippingAddress.zip}\n                </p>\n              </div>\n\n              <p mix={[css({ marginTop: '1.5rem' })]}>\n                <a href={routes.account.orders.index.href()} class=\"btn btn-secondary\">\n                  Back to Orders\n                </a>\n              </p>\n            </Layout>,\n          )\n        },\n      },\n    },\n  },\n} satisfies Controller<typeof routes.account>\n"
  },
  {
    "path": "demos/bookstore/app/admin.books.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.ts'\nimport { loginAsAdmin, requestWithSession } from '../test/helpers.ts'\n\ndescribe('admin books handlers', () => {\n  it('POST /admin/books creates new book when admin', async () => {\n    let sessionId = await loginAsAdmin(router)\n\n    // Create new book\n    let createRequest = requestWithSession('https://remix.run/admin/books', sessionId, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: new URLSearchParams({\n        slug: 'test-book',\n        title: 'Test Book',\n        author: 'Test Author',\n        description: 'Test description',\n        price: '29.99',\n        genre: 'test',\n        isbn: '978-0000000000',\n        publishedYear: '2024',\n        inStock: 'true',\n      }),\n    })\n    let response = await router.fetch(createRequest)\n\n    assert.equal(response.status, 302)\n    assert.ok(response.headers.get('Location')?.includes('/admin/books'))\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/admin.books.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\nimport * as s from 'remix/data-schema'\nimport * as f from 'remix/data-schema/form-data'\nimport * as coerce from 'remix/data-schema/coerce'\nimport { Database } from 'remix/data-table'\nimport { redirect } from 'remix/response/redirect'\n\nimport { routes } from './routes.ts'\nimport { books } from './data/schema.ts'\nimport { Layout } from './layout.tsx'\nimport { parseId } from './utils/ids.ts'\nimport { render } from './utils/render.ts'\nimport { RestfulForm } from './components/restful-form.tsx'\n\nconst textField = f.field(s.defaulted(s.string(), ''))\nconst optionalTextField = f.field(s.optional(s.string()))\nconst priceField = f.field(s.defaulted(s.string(), '0'))\nconst publishedYearField = f.field(s.defaulted(s.string(), '2024'), {\n  name: 'publishedYear',\n})\nconst inStockField = f.field(s.defaulted(coerce.boolean(), false), {\n  name: 'inStock',\n})\nconst bookSchema = f.object({\n  slug: textField,\n  title: textField,\n  author: textField,\n  description: textField,\n  price: priceField,\n  genre: textField,\n  cover: optionalTextField,\n  isbn: textField,\n  publishedYear: publishedYearField,\n  inStock: inStockField,\n})\n\nexport default {\n  actions: {\n    async index({ get }) {\n      let db = get(Database)\n      let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })\n\n      return render(\n        <Layout>\n          <h1>Manage Books</h1>\n\n          <p mix={[css({ marginBottom: '1rem' })]}>\n            <a href={routes.admin.books.new.href()} class=\"btn\">\n              Add New Book\n            </a>\n            <a\n              href={routes.admin.index.href()}\n              class=\"btn btn-secondary\"\n              mix={[css({ marginLeft: '0.5rem' })]}\n            >\n              Back to Dashboard\n            </a>\n          </p>\n\n          <div class=\"card\">\n            <table>\n              <thead>\n                <tr>\n                  <th>Title</th>\n                  <th>Author</th>\n                  <th>Genre</th>\n                  <th>Price</th>\n                  <th>Stock</th>\n                  <th>Actions</th>\n                </tr>\n              </thead>\n              <tbody>\n                {allBooks.map((book) => (\n                  <tr>\n                    <td>{book.title}</td>\n                    <td>{book.author}</td>\n                    <td>{book.genre}</td>\n                    <td>${book.price.toFixed(2)}</td>\n                    <td>\n                      <span class={`badge ${book.in_stock ? 'badge-success' : 'badge-warning'}`}>\n                        {book.in_stock ? 'Yes' : 'No'}\n                      </span>\n                    </td>\n                    <td class=\"actions\">\n                      <a\n                        href={routes.admin.books.edit.href({ bookId: book.id })}\n                        class=\"btn btn-secondary\"\n                        mix={[css({ fontSize: '0.875rem', padding: '0.25rem 0.5rem' })]}\n                      >\n                        Edit\n                      </a>\n                      <RestfulForm\n                        method=\"DELETE\"\n                        action={routes.admin.books.destroy.href({ bookId: book.id })}\n                        mix={[css({ display: 'inline' })]}\n                      >\n                        <button\n                          type=\"submit\"\n                          class=\"btn btn-danger\"\n                          mix={[css({ fontSize: '0.875rem', padding: '0.25rem 0.5rem' })]}\n                        >\n                          Delete\n                        </button>\n                      </RestfulForm>\n                    </td>\n                  </tr>\n                ))}\n              </tbody>\n            </table>\n          </div>\n        </Layout>,\n      )\n    },\n\n    async show({ get, params }) {\n      let db = get(Database)\n      let bookId = parseId(params.bookId)\n      let book = bookId === undefined ? undefined : await db.find(books, bookId)\n\n      if (!book) {\n        return render(\n          <Layout>\n            <div class=\"card\">\n              <h1>Book Not Found</h1>\n            </div>\n          </Layout>,\n          { status: 404 },\n        )\n      }\n\n      return render(\n        <Layout>\n          <h1>Book Details</h1>\n\n          <div class=\"card\">\n            <p>\n              <strong>Title:</strong> {book.title}\n            </p>\n            <p>\n              <strong>Author:</strong> {book.author}\n            </p>\n            <p>\n              <strong>Slug:</strong> {book.slug}\n            </p>\n            <p>\n              <strong>Description:</strong> {book.description}\n            </p>\n            <p>\n              <strong>Price:</strong> ${book.price.toFixed(2)}\n            </p>\n            <p>\n              <strong>Genre:</strong> {book.genre}\n            </p>\n            <p>\n              <strong>ISBN:</strong> {book.isbn}\n            </p>\n            <p>\n              <strong>Published:</strong> {book.published_year}\n            </p>\n            <p>\n              <strong>In Stock:</strong>{' '}\n              <span class={`badge ${book.in_stock ? 'badge-success' : 'badge-warning'}`}>\n                {book.in_stock ? 'Yes' : 'No'}\n              </span>\n            </p>\n\n            <div mix={[css({ marginTop: '2rem' })]}>\n              <a href={routes.admin.books.edit.href({ bookId: book.id })} class=\"btn\">\n                Edit\n              </a>\n              <a\n                href={routes.admin.books.index.href()}\n                class=\"btn btn-secondary\"\n                mix={[css({ marginLeft: '0.5rem' })]}\n              >\n                Back to List\n              </a>\n            </div>\n          </div>\n        </Layout>,\n      )\n    },\n\n    new() {\n      return render(\n        <Layout>\n          <h1>Add New Book</h1>\n\n          <div class=\"card\">\n            <form\n              method=\"POST\"\n              action={routes.admin.books.create.href()}\n              encType=\"multipart/form-data\"\n            >\n              <div class=\"form-group\">\n                <label for=\"title\">Title</label>\n                <input type=\"text\" id=\"title\" name=\"title\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"author\">Author</label>\n                <input type=\"text\" id=\"author\" name=\"author\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"slug\">Slug (URL-friendly name)</label>\n                <input type=\"text\" id=\"slug\" name=\"slug\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"description\">Description</label>\n                <textarea id=\"description\" name=\"description\" required></textarea>\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"price\">Price</label>\n                <input type=\"number\" id=\"price\" name=\"price\" step=\"0.01\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"genre\">Genre</label>\n                <input type=\"text\" id=\"genre\" name=\"genre\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"isbn\">ISBN</label>\n                <input type=\"text\" id=\"isbn\" name=\"isbn\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"publishedYear\">Published Year</label>\n                <input type=\"number\" id=\"publishedYear\" name=\"publishedYear\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"inStock\">In Stock</label>\n                <select id=\"inStock\" name=\"inStock\">\n                  <option value=\"true\">Yes</option>\n                  <option value=\"false\">No</option>\n                </select>\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"cover\">Book Cover Image</label>\n                <input type=\"file\" id=\"cover\" name=\"cover\" accept=\"image/*\" />\n                <small mix={[css({ color: '#666' })]}>\n                  Optional. Upload a cover image for this book.\n                </small>\n              </div>\n\n              <button type=\"submit\" class=\"btn\">\n                Create Book\n              </button>\n              <a\n                href={routes.admin.books.index.href()}\n                class=\"btn btn-secondary\"\n                mix={[css({ marginLeft: '0.5rem' })]}\n              >\n                Cancel\n              </a>\n            </form>\n          </div>\n        </Layout>,\n      )\n    },\n\n    async create({ get }) {\n      let db = get(Database)\n      let formData = get(FormData)\n      let { author, cover, description, genre, inStock, isbn, price, publishedYear, slug, title } =\n        s.parse(bookSchema, formData)\n\n      await db.create(books, {\n        slug,\n        title,\n        author,\n        description,\n        price: parseFloat(price),\n        genre,\n        cover_url: cover ?? '/images/placeholder.jpg',\n        image_urls: JSON.stringify([]),\n        isbn,\n        published_year: parseInt(publishedYear, 10),\n        in_stock: inStock,\n      })\n\n      return redirect(routes.admin.books.index.href())\n    },\n\n    async edit({ get, params }) {\n      let db = get(Database)\n      let bookId = parseId(params.bookId)\n      let book = bookId === undefined ? undefined : await db.find(books, bookId)\n\n      if (!book) {\n        return render(\n          <Layout>\n            <div class=\"card\">\n              <h1>Book Not Found</h1>\n            </div>\n          </Layout>,\n          { status: 404 },\n        )\n      }\n\n      return render(\n        <Layout>\n          <h1>Edit Book</h1>\n\n          <div class=\"card\">\n            <RestfulForm\n              method=\"PUT\"\n              action={routes.admin.books.update.href({ bookId: book.id })}\n              encType=\"multipart/form-data\"\n            >\n              <div class=\"form-group\">\n                <label for=\"title\">Title</label>\n                <input type=\"text\" id=\"title\" name=\"title\" value={book.title} required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"author\">Author</label>\n                <input type=\"text\" id=\"author\" name=\"author\" value={book.author} required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"slug\">Slug (URL-friendly name)</label>\n                <input type=\"text\" id=\"slug\" name=\"slug\" value={book.slug} required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"description\">Description</label>\n                <textarea id=\"description\" name=\"description\" required>\n                  {book.description}\n                </textarea>\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"price\">Price</label>\n                <input\n                  type=\"number\"\n                  id=\"price\"\n                  name=\"price\"\n                  step=\"0.01\"\n                  value={book.price}\n                  required\n                />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"genre\">Genre</label>\n                <input type=\"text\" id=\"genre\" name=\"genre\" value={book.genre} required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"isbn\">ISBN</label>\n                <input type=\"text\" id=\"isbn\" name=\"isbn\" value={book.isbn} required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"publishedYear\">Published Year</label>\n                <input\n                  type=\"number\"\n                  id=\"publishedYear\"\n                  name=\"publishedYear\"\n                  value={book.published_year}\n                  required\n                />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"inStock\">In Stock</label>\n                <select id=\"inStock\" name=\"inStock\">\n                  <option value=\"true\" selected={book.in_stock}>\n                    Yes\n                  </option>\n                  <option value=\"false\" selected={!book.in_stock}>\n                    No\n                  </option>\n                </select>\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"cover\">Book Cover Image</label>\n                {book.cover_url !== '/images/placeholder.jpg' && (\n                  <div mix={[css({ marginBottom: '0.5rem' })]}>\n                    <img\n                      src={book.cover_url}\n                      alt={book.title}\n                      mix={[css({ maxWidth: '200px', height: 'auto', borderRadius: '4px' })]}\n                    />\n                    <p mix={[css({ fontSize: '0.875rem', color: '#666' })]}>Current cover image</p>\n                  </div>\n                )}\n                <input type=\"file\" id=\"cover\" name=\"cover\" accept=\"image/*\" />\n                <small mix={[css({ color: '#666' })]}>\n                  Optional. Upload a new cover image to replace the current one.\n                </small>\n              </div>\n\n              <button type=\"submit\" class=\"btn\">\n                Update Book\n              </button>\n              <a\n                href={routes.admin.books.index.href()}\n                class=\"btn btn-secondary\"\n                mix={[css({ marginLeft: '0.5rem' })]}\n              >\n                Cancel\n              </a>\n            </RestfulForm>\n          </div>\n        </Layout>,\n      )\n    },\n\n    async update({ get, params }) {\n      let db = get(Database)\n      let formData = get(FormData)\n      let bookId = parseId(params.bookId)\n      let book = bookId === undefined ? undefined : await db.find(books, bookId)\n      if (!book) {\n        return new Response('Book not found', { status: 404 })\n      }\n\n      let { author, cover, description, genre, inStock, isbn, price, publishedYear, slug, title } =\n        s.parse(bookSchema, formData)\n\n      // The uploadHandler automatically saves the file and returns the URL path\n      // If no file was uploaded, keep the existing cover_url\n      let cover_url = cover || book.cover_url\n\n      await db.update(books, book.id, {\n        slug,\n        title,\n        author,\n        description,\n        price: parseFloat(price),\n        genre,\n        cover_url,\n        isbn,\n        published_year: parseInt(publishedYear, 10),\n        in_stock: inStock,\n      })\n\n      return redirect(routes.admin.books.index.href())\n    },\n\n    async destroy({ get, params }) {\n      let db = get(Database)\n      let bookId = parseId(params.bookId)\n      let book = bookId === undefined ? undefined : await db.find(books, bookId)\n      if (book) {\n        await db.delete(books, book.id)\n      }\n\n      return redirect(routes.admin.books.index.href())\n    },\n  },\n} satisfies Controller<typeof routes.admin.books>\n"
  },
  {
    "path": "demos/bookstore/app/admin.orders.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\nimport { Database } from 'remix/data-table'\n\nimport { routes } from './routes.ts'\nimport { orders, orderItemsWithBook } from './data/schema.ts'\nimport { Layout } from './layout.tsx'\nimport { parseId } from './utils/ids.ts'\nimport { render } from './utils/render.ts'\n\nexport default {\n  actions: {\n    async index({ get }) {\n      let db = get(Database)\n      let allOrders = await db.findMany(orders, {\n        orderBy: ['created_at', 'asc'],\n        with: { items: orderItemsWithBook },\n      })\n\n      return render(\n        <Layout>\n          <h1>Manage Orders</h1>\n\n          <p mix={[css({ marginBottom: '1rem' })]}>\n            <a href={routes.admin.index.href()} class=\"btn btn-secondary\">\n              Back to Dashboard\n            </a>\n          </p>\n\n          <div class=\"card\">\n            <table>\n              <thead>\n                <tr>\n                  <th>Order ID</th>\n                  <th>Date</th>\n                  <th>Items</th>\n                  <th>Total</th>\n                  <th>Status</th>\n                  <th>Actions</th>\n                </tr>\n              </thead>\n              <tbody>\n                {allOrders.map((order) => (\n                  <tr>\n                    <td>#{order.id}</td>\n                    <td>{new Date(order.created_at).toLocaleDateString()}</td>\n                    <td>{order.items.length} item(s)</td>\n                    <td>${order.total.toFixed(2)}</td>\n                    <td>\n                      <span class=\"badge badge-info\">{order.status}</span>\n                    </td>\n                    <td>\n                      <a\n                        href={routes.admin.orders.show.href({ orderId: order.id })}\n                        class=\"btn btn-secondary\"\n                        mix={[css({ fontSize: '0.875rem', padding: '0.25rem 0.5rem' })]}\n                      >\n                        View\n                      </a>\n                    </td>\n                  </tr>\n                ))}\n              </tbody>\n            </table>\n          </div>\n        </Layout>,\n      )\n    },\n\n    async show({ get, params }) {\n      let db = get(Database)\n      let orderId = parseId(params.orderId)\n      let order =\n        orderId === undefined\n          ? undefined\n          : await db.find(orders, orderId, {\n              with: { items: orderItemsWithBook },\n            })\n\n      if (!order) {\n        return render(\n          <Layout>\n            <div class=\"card\">\n              <h1>Order Not Found</h1>\n            </div>\n          </Layout>,\n          { status: 404 },\n        )\n      }\n\n      let shippingAddress = JSON.parse(order.shipping_address_json) as {\n        street: string\n        city: string\n        state: string\n        zip: string\n      }\n\n      return render(\n        <Layout>\n          <h1>Order #{order.id}</h1>\n\n          <div class=\"card\">\n            <p>\n              <strong>Order Date:</strong> {new Date(order.created_at).toLocaleDateString()}\n            </p>\n            <p>\n              <strong>User ID:</strong> {order.user_id}\n            </p>\n            <p>\n              <strong>Status:</strong> <span class=\"badge badge-info\">{order.status}</span>\n            </p>\n\n            <h2 mix={[css({ marginTop: '2rem' })]}>Items</h2>\n            <table mix={[css({ marginTop: '1rem' })]}>\n              <thead>\n                <tr>\n                  <th>Book</th>\n                  <th>Quantity</th>\n                  <th>Price</th>\n                  <th>Subtotal</th>\n                </tr>\n              </thead>\n              <tbody>\n                {order.items.map((item) => (\n                  <tr>\n                    <td>{item.title}</td>\n                    <td>{item.quantity}</td>\n                    <td>${item.unit_price.toFixed(2)}</td>\n                    <td>${(item.unit_price * item.quantity).toFixed(2)}</td>\n                  </tr>\n                ))}\n              </tbody>\n              <tfoot>\n                <tr>\n                  <td colSpan={3} mix={[css({ textAlign: 'right', fontWeight: 'bold' })]}>\n                    Total:\n                  </td>\n                  <td mix={[css({ fontWeight: 'bold' })]}>${order.total.toFixed(2)}</td>\n                </tr>\n              </tfoot>\n            </table>\n\n            <h2 mix={[css({ marginTop: '2rem' })]}>Shipping Address</h2>\n            <p>{shippingAddress.street}</p>\n            <p>\n              {shippingAddress.city}, {shippingAddress.state} {shippingAddress.zip}\n            </p>\n          </div>\n\n          <p mix={[css({ marginTop: '1.5rem' })]}>\n            <a href={routes.admin.orders.index.href()} class=\"btn btn-secondary\">\n              Back to Orders\n            </a>\n          </p>\n        </Layout>,\n      )\n    },\n  },\n} satisfies Controller<typeof routes.admin.orders>\n"
  },
  {
    "path": "demos/bookstore/app/admin.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.ts'\nimport { loginAsCustomer, requestWithSession } from '../test/helpers.ts'\n\ndescribe('admin handlers', () => {\n  it('GET /admin redirects when not authenticated', async () => {\n    let response = await router.fetch('https://remix.run/admin')\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/login?returnTo=%2Fadmin')\n  })\n\n  it('GET /admin returns 403 for non-admin users', async () => {\n    let sessionId = await loginAsCustomer(router)\n\n    // Try to access admin\n    let request = requestWithSession('https://remix.run/admin', sessionId)\n    let response = await router.fetch(request)\n\n    assert.equal(response.status, 403)\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/admin.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\n\nimport { routes } from './routes.ts'\nimport { Layout } from './layout.tsx'\nimport { requireAuth } from './middleware/auth.ts'\nimport { requireAdmin } from './middleware/admin.ts'\nimport { render } from './utils/render.ts'\n\nimport adminBooksController from './admin.books.tsx'\nimport adminOrdersController from './admin.orders.tsx'\nimport adminUsersController from './admin.users.tsx'\n\nexport default {\n  middleware: [requireAuth(), requireAdmin()],\n  actions: {\n    index() {\n      return render(\n        <Layout>\n          <h1>Admin Dashboard</h1>\n\n          <div\n            mix={[\n              css({\n                display: 'grid',\n                gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',\n                gap: '1.5rem',\n              }),\n            ]}\n          >\n            <div class=\"card\">\n              <h2>Manage Books</h2>\n              <p>Add, edit, or remove books from the catalog.</p>\n              <a\n                href={routes.admin.books.index.href()}\n                class=\"btn\"\n                mix={[css({ marginTop: '1rem' })]}\n              >\n                View Books\n              </a>\n            </div>\n\n            <div class=\"card\">\n              <h2>Manage Users</h2>\n              <p>View and manage user accounts.</p>\n              <a\n                href={routes.admin.users.index.href()}\n                class=\"btn\"\n                mix={[css({ marginTop: '1rem' })]}\n              >\n                View Users\n              </a>\n            </div>\n\n            <div class=\"card\">\n              <h2>View Orders</h2>\n              <p>Monitor and manage customer orders.</p>\n              <a\n                href={routes.admin.orders.index.href()}\n                class=\"btn\"\n                mix={[css({ marginTop: '1rem' })]}\n              >\n                View Orders\n              </a>\n            </div>\n          </div>\n        </Layout>,\n      )\n    },\n\n    books: adminBooksController,\n    users: adminUsersController,\n    orders: adminOrdersController,\n  },\n} satisfies Controller<typeof routes.admin>\n"
  },
  {
    "path": "demos/bookstore/app/admin.users.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\nimport * as s from 'remix/data-schema'\nimport * as f from 'remix/data-schema/form-data'\nimport { Database } from 'remix/data-table'\nimport { redirect } from 'remix/response/redirect'\n\nimport { routes } from './routes.ts'\nimport { users } from './data/schema.ts'\nimport { Layout } from './layout.tsx'\nimport { render } from './utils/render.ts'\nimport { getCurrentUser } from './utils/context.ts'\nimport { parseId } from './utils/ids.ts'\nimport { RestfulForm } from './components/restful-form.tsx'\n\nconst textField = f.field(s.defaulted(s.string(), ''))\nconst roleField = f.field(\n  s.defaulted(s.union([s.literal('customer'), s.literal('admin')]), 'customer'),\n)\nconst userSchema = f.object({\n  name: textField,\n  email: textField,\n  role: roleField,\n})\n\nexport default {\n  actions: {\n    async index({ get }) {\n      let db = get(Database)\n      let user = getCurrentUser()\n      let allUsers = await db.findMany(users, { orderBy: ['id', 'asc'] })\n\n      return render(\n        <Layout>\n          <h1>Manage Users</h1>\n\n          <p mix={[css({ marginBottom: '1rem' })]}>\n            <a href={routes.admin.index.href()} class=\"btn btn-secondary\">\n              Back to Dashboard\n            </a>\n          </p>\n\n          <div class=\"card\">\n            <table>\n              <thead>\n                <tr>\n                  <th>Name</th>\n                  <th>Email</th>\n                  <th>Role</th>\n                  <th>Created</th>\n                  <th>Actions</th>\n                </tr>\n              </thead>\n              <tbody>\n                {allUsers.map((u) => (\n                  <tr>\n                    <td>{u.name}</td>\n                    <td>{u.email}</td>\n                    <td>\n                      <span class={`badge ${u.role === 'admin' ? 'badge-info' : 'badge-success'}`}>\n                        {u.role}\n                      </span>\n                    </td>\n                    <td>{new Date(u.created_at).toLocaleDateString()}</td>\n                    <td class=\"actions\">\n                      <a\n                        href={routes.admin.users.edit.href({ userId: u.id })}\n                        class=\"btn btn-secondary\"\n                        mix={[css({ fontSize: '0.875rem', padding: '0.25rem 0.5rem' })]}\n                      >\n                        Edit\n                      </a>\n                      {u.id !== user.id ? (\n                        <RestfulForm\n                          method=\"DELETE\"\n                          action={routes.admin.users.destroy.href({ userId: u.id })}\n                          mix={[css({ display: 'inline' })]}\n                        >\n                          <button\n                            type=\"submit\"\n                            class=\"btn btn-danger\"\n                            mix={[css({ fontSize: '0.875rem', padding: '0.25rem 0.5rem' })]}\n                          >\n                            Delete\n                          </button>\n                        </RestfulForm>\n                      ) : null}\n                    </td>\n                  </tr>\n                ))}\n              </tbody>\n            </table>\n          </div>\n        </Layout>,\n      )\n    },\n\n    async show({ get, params }) {\n      let db = get(Database)\n      let userId = parseId(params.userId)\n      let targetUser = userId === undefined ? undefined : await db.find(users, userId)\n\n      if (!targetUser) {\n        return render(\n          <Layout>\n            <div class=\"card\">\n              <h1>User Not Found</h1>\n            </div>\n          </Layout>,\n          { status: 404 },\n        )\n      }\n\n      return render(\n        <Layout>\n          <h1>User Details</h1>\n\n          <div class=\"card\">\n            <p>\n              <strong>Name:</strong> {targetUser.name}\n            </p>\n            <p>\n              <strong>Email:</strong> {targetUser.email}\n            </p>\n            <p>\n              <strong>Role:</strong>{' '}\n              <span class={`badge ${targetUser.role === 'admin' ? 'badge-info' : 'badge-success'}`}>\n                {targetUser.role}\n              </span>\n            </p>\n            <p>\n              <strong>Created:</strong> {new Date(targetUser.created_at).toLocaleDateString()}\n            </p>\n\n            <div mix={[css({ marginTop: '2rem' })]}>\n              <a href={routes.admin.users.edit.href({ userId: targetUser.id })} class=\"btn\">\n                Edit\n              </a>\n              <a\n                href={routes.admin.users.index.href()}\n                class=\"btn btn-secondary\"\n                mix={[css({ marginLeft: '0.5rem' })]}\n              >\n                Back to List\n              </a>\n            </div>\n          </div>\n        </Layout>,\n      )\n    },\n\n    async edit({ get, params }) {\n      let db = get(Database)\n      let userId = parseId(params.userId)\n      let targetUser = userId === undefined ? undefined : await db.find(users, userId)\n\n      if (!targetUser) {\n        return render(\n          <Layout>\n            <div class=\"card\">\n              <h1>User Not Found</h1>\n            </div>\n          </Layout>,\n          { status: 404 },\n        )\n      }\n\n      return render(\n        <Layout>\n          <h1>Edit User</h1>\n\n          <div class=\"card\">\n            <RestfulForm\n              method=\"PUT\"\n              action={routes.admin.users.update.href({ userId: targetUser.id })}\n            >\n              <div class=\"form-group\">\n                <label for=\"name\">Name</label>\n                <input type=\"text\" id=\"name\" name=\"name\" value={targetUser.name} required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"email\">Email</label>\n                <input type=\"email\" id=\"email\" name=\"email\" value={targetUser.email} required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"role\">Role</label>\n                <select id=\"role\" name=\"role\">\n                  <option value=\"customer\" selected={targetUser.role === 'customer'}>\n                    Customer\n                  </option>\n                  <option value=\"admin\" selected={targetUser.role === 'admin'}>\n                    Admin\n                  </option>\n                </select>\n              </div>\n\n              <button type=\"submit\" class=\"btn\">\n                Update User\n              </button>\n              <a\n                href={routes.admin.users.index.href()}\n                class=\"btn btn-secondary\"\n                mix={[css({ marginLeft: '0.5rem' })]}\n              >\n                Cancel\n              </a>\n            </RestfulForm>\n          </div>\n        </Layout>,\n      )\n    },\n\n    async update({ get, params }) {\n      let db = get(Database)\n      let formData = get(FormData)\n      let userId = parseId(params.userId)\n      let targetUser = userId === undefined ? undefined : await db.find(users, userId)\n      let { email, name, role } = s.parse(userSchema, formData)\n\n      if (targetUser) {\n        await db.update(users, targetUser.id, {\n          name,\n          email,\n          role,\n        })\n      }\n\n      return redirect(routes.admin.users.index.href())\n    },\n\n    async destroy({ get, params }) {\n      let db = get(Database)\n      let userId = parseId(params.userId)\n      let targetUser = userId === undefined ? undefined : await db.find(users, userId)\n      if (targetUser) {\n        await db.delete(users, targetUser.id)\n      }\n\n      return redirect(routes.admin.users.index.href())\n    },\n  },\n} satisfies Controller<typeof routes.admin.users>\n"
  },
  {
    "path": "demos/bookstore/app/assets/cart-button.tsx",
    "content": "import { type Handle, clientEntry, on } from 'remix/component'\n\nimport { routes } from '../routes.ts'\n\nlet moduleUrl = routes.assets.href({ path: 'cart-button.js#CartButton' })\n\nexport const CartButton = clientEntry(moduleUrl, (handle: Handle) => {\n  let pending = false\n\n  return ({ inCart, id, slug }: { inCart: boolean; id: string | number; slug: string }) => (\n    <button\n      type=\"button\"\n      mix={[\n        on('click', async (_event, signal) => {\n          pending = true\n          handle.update()\n\n          let formData = new FormData()\n          formData.set('bookId', String(id))\n          formData.set('slug', slug)\n\n          await fetch(routes.api.cartToggle.href(), {\n            method: 'POST',\n            body: formData,\n            signal,\n          })\n\n          await handle.frame.reload()\n          await new Promise((resolve) => setTimeout(resolve, 500))\n          if (signal.aborted) return\n          pending = false\n          handle.update()\n        }),\n      ]}\n      class=\"btn\"\n    >\n      {pending ? 'Saving...' : inCart ? 'Remove from Cart' : 'Add to Cart'}\n    </button>\n  )\n})\n"
  },
  {
    "path": "demos/bookstore/app/assets/cart-items.tsx",
    "content": "import { css, type Handle, clientEntry, on } from 'remix/component'\n\nimport { routes } from '../routes.ts'\n\nlet moduleUrl = routes.assets.href({ path: 'cart-items.js#CartItems' })\n\ntype CartItem = {\n  bookId: number\n  slug: string\n  title: string\n  price: number\n  quantity: number\n}\n\ntype CartItemsProps = {\n  items: CartItem[]\n  total: number\n  canCheckout: boolean\n}\n\ntype PendingAction = {\n  type: 'update' | 'remove'\n  bookId: number\n} | null\n\nexport let CartItems = clientEntry(moduleUrl, (handle: Handle) => {\n  let pendingAction: PendingAction = null\n\n  let submit = async (form: HTMLFormElement, signal: AbortSignal, nextAction: PendingAction) => {\n    if (pendingAction) return\n\n    pendingAction = nextAction\n    handle.update()\n\n    try {\n      let formData = new FormData(form)\n      formData.set('redirect', 'none')\n\n      await fetch(form.action, {\n        method: 'POST',\n        body: formData,\n        signal,\n      })\n\n      if (signal.aborted) return\n\n      await handle.frame.reload()\n    } finally {\n      pendingAction = null\n      handle.update()\n    }\n  }\n\n  return ({ items, total, canCheckout }: CartItemsProps) => {\n    let isPending = pendingAction !== null\n    let totalLabel = isPending ? '---' : `$${total.toFixed(2)}`\n\n    return (\n      <>\n        {isPending ? (\n          <p mix={[css({ marginBottom: '1rem', fontSize: '0.9rem', color: '#666' })]}>\n            Updating your cart...\n          </p>\n        ) : null}\n\n        <table>\n          <thead>\n            <tr>\n              <th>Book</th>\n              <th>Price</th>\n              <th>Quantity</th>\n              <th>Subtotal</th>\n              <th>Actions</th>\n            </tr>\n          </thead>\n          <tbody>\n            {items.map((item) => {\n              let isUpdating =\n                pendingAction?.type === 'update' && pendingAction.bookId === item.bookId\n              let isRemoving =\n                pendingAction?.type === 'remove' && pendingAction.bookId === item.bookId\n\n              return (\n                <tr key={item.bookId}>\n                  <td>\n                    <a href={routes.books.show.href({ slug: item.slug })}>{item.title}</a>\n                  </td>\n\n                  <td>${item.price.toFixed(2)}</td>\n\n                  <td>\n                    <form\n                      method=\"POST\"\n                      action={routes.cart.api.update.href()}\n                      mix={[\n                        on('submit', async (event, signal) => {\n                          event.preventDefault()\n                          await submit(event.currentTarget, signal, {\n                            type: 'update',\n                            bookId: item.bookId,\n                          })\n                        }),\n                        css({ display: 'inline-flex', gap: '0.5rem', alignItems: 'center' }),\n                      ]}\n                    >\n                      <input type=\"hidden\" name=\"_method\" value=\"PUT\" />\n                      <input type=\"hidden\" name=\"bookId\" value={item.bookId} />\n\n                      <input\n                        type=\"number\"\n                        name=\"quantity\"\n                        defaultValue={item.quantity}\n                        min=\"1\"\n                        disabled={isPending}\n                        mix={[css({ width: '70px' })]}\n                      />\n\n                      <button\n                        type=\"submit\"\n                        disabled={isPending}\n                        class=\"btn btn-secondary\"\n                        mix={[\n                          css({\n                            fontSize: '0.875rem',\n                            padding: '0.25rem 0.5rem',\n                            minWidth: '6.25rem',\n                            textAlign: 'center',\n                          }),\n                        ]}\n                      >\n                        {isUpdating ? 'Saving...' : 'Update'}\n                      </button>\n                    </form>\n                  </td>\n\n                  <td>${(item.price * item.quantity).toFixed(2)}</td>\n\n                  <td>\n                    <form\n                      method=\"POST\"\n                      action={routes.cart.api.remove.href()}\n                      mix={[\n                        on('submit', async (event, signal) => {\n                          event.preventDefault()\n                          await submit(event.currentTarget, signal, {\n                            type: 'remove',\n                            bookId: item.bookId,\n                          })\n                        }),\n                        css({ display: 'inline' }),\n                      ]}\n                    >\n                      <input type=\"hidden\" name=\"_method\" value=\"DELETE\" />\n                      <input type=\"hidden\" name=\"bookId\" value={item.bookId} />\n\n                      <button\n                        type=\"submit\"\n                        disabled={isPending}\n                        class=\"btn btn-danger\"\n                        mix={[\n                          css({\n                            fontSize: '0.875rem',\n                            padding: '0.25rem 0.5rem',\n                            minWidth: '7rem',\n                            textAlign: 'center',\n                          }),\n                        ]}\n                      >\n                        {isRemoving ? 'Removing...' : 'Remove'}\n                      </button>\n                    </form>\n                  </td>\n                </tr>\n              )\n            })}\n          </tbody>\n        </table>\n\n        <div mix={[css({ marginTop: '2rem', display: 'flex', alignItems: 'center', gap: '1rem' })]}>\n          <p\n            mix={[css({ margin: 0, fontSize: '1.25rem', fontWeight: 'bold', marginRight: 'auto' })]}\n          >\n            Total: {totalLabel}\n          </p>\n\n          <a href={routes.books.index.href()} class=\"btn btn-secondary\">\n            Continue Shopping\n          </a>\n\n          {canCheckout ? (\n            <a href={routes.checkout.index.href()} class=\"btn\">\n              Proceed to Checkout\n            </a>\n          ) : (\n            <a href={routes.auth.login.index.href()} class=\"btn\">\n              Login to Checkout\n            </a>\n          )}\n        </div>\n      </>\n    )\n  }\n})\n"
  },
  {
    "path": "demos/bookstore/app/assets/entry.tsx",
    "content": "import { run } from 'remix/component'\n\nlet app = run({\n  async loadModule(moduleUrl: string, exportName: string) {\n    let mod = await import(moduleUrl)\n    let Component = (mod as any)[exportName]\n    if (!Component) {\n      throw new Error(`Unknown component: ${moduleUrl}#${exportName}`)\n    }\n    return Component\n  },\n  async resolveFrame(src, signal) {\n    let response = await fetch(src, { headers: { accept: 'text/html' }, signal })\n    if (!response.ok) {\n      return `<pre>Frame error: ${response.status} ${response.statusText}</pre>`\n    }\n    // let text = await response.text()\n    // console.log(text)\n    // return text\n    if (response.body) return response.body\n    return response.text()\n  },\n})\n\napp.ready().catch((error: unknown) => {\n  console.error('Frame adoption failed:', error)\n})\n"
  },
  {
    "path": "demos/bookstore/app/assets/image-carousel.tsx",
    "content": "import { css, type Handle, clientEntry, on } from 'remix/component'\n\nimport { routes } from '../routes.ts'\n\nexport const ImageCarousel = clientEntry(\n  routes.assets.href({ path: 'image-carousel.js#ImageCarousel' }),\n  function ImageCarousel(handle: Handle, setup?: { startIndex?: number }) {\n    let index = setup?.startIndex ?? 0\n\n    let goPrev = (total: number) => {\n      if (index <= 0) return\n      index = index - 1\n      handle.update()\n    }\n\n    let goNext = (total: number) => {\n      if (index >= total - 1) return\n      index = index + 1\n      handle.update()\n    }\n\n    return ({ images }: { images: string[] }) => {\n      let total = images.length\n      if (total === 0) return null\n      if (index > total - 1) index = total - 1\n      if (index < 0) index = 0\n\n      return (\n        <div\n          mix={[\n            css({\n              position: 'relative',\n              width: '100%',\n              height: '100%',\n              overflow: 'hidden',\n              backgroundColor: '#f5f5f5',\n            }),\n          ]}\n        >\n          <div\n            mix={[\n              css({\n                display: 'flex',\n                height: '100%',\n                width: '100%',\n                transition: 'transform 350ms cubic-bezier(0.22, 1, 0.36, 1)',\n                willChange: 'transform',\n              }),\n            ]}\n            style={{\n              transform: `translateX(-${index * 100}%)`,\n            }}\n          >\n            {images.map((src, i) => (\n              <div\n                key={src + i}\n                mix={[\n                  css({\n                    minWidth: '100%',\n                    height: '100%',\n                    position: 'relative',\n                  }),\n                ]}\n              >\n                <img\n                  src={src}\n                  alt={`Image ${i + 1} of ${total}`}\n                  mix={[\n                    css({\n                      width: '100%',\n                      height: '100%',\n                      objectFit: 'cover',\n                      display: 'block',\n                      userSelect: 'none',\n                      pointerEvents: 'none',\n                    }),\n                  ]}\n                  draggable={false}\n                />\n              </div>\n            ))}\n          </div>\n\n          <button\n            aria-label=\"Previous image\"\n            disabled={index === 0}\n            mix={[\n              on('click', () => goPrev(total)),\n              css({\n                position: 'absolute',\n                top: '50%',\n                left: '8px',\n                transform: 'translateY(-50%)',\n                width: '40px',\n                height: '40px',\n                display: 'flex',\n                alignItems: 'center',\n                justifyContent: 'center',\n                background: 'transparent',\n                color: 'white',\n                border: 'none',\n                borderRadius: '999px',\n                cursor: 'pointer',\n                outline: 'none',\n                transition: 'background-color 150ms ease, opacity 150ms ease',\n              }),\n            ]}\n            style={{\n              opacity: index === 0 ? 0.4 : 0.9,\n            }}\n          >\n            <span mix={[css({ fontSize: '22px', lineHeight: '1' })]}>{'‹'}</span>\n          </button>\n\n          <button\n            aria-label=\"Next image\"\n            disabled={index === total - 1}\n            mix={[\n              on('click', () => goNext(total)),\n              css({\n                position: 'absolute',\n                top: '50%',\n                right: '8px',\n                transform: 'translateY(-50%)',\n                width: '40px',\n                height: '40px',\n                display: 'flex',\n                alignItems: 'center',\n                justifyContent: 'center',\n                background: 'transparent',\n                color: 'white',\n                border: 'none',\n                borderRadius: '999px',\n                cursor: 'pointer',\n                outline: 'none',\n                transition: 'background-color 150ms ease, opacity 150ms ease',\n              }),\n            ]}\n            style={{\n              opacity: index === total - 1 ? 0.4 : 0.9,\n            }}\n          >\n            <span mix={[css({ fontSize: '22px', lineHeight: '1' })]}>{'›'}</span>\n          </button>\n        </div>\n      )\n    }\n  },\n)\n"
  },
  {
    "path": "demos/bookstore/app/auth.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.ts'\nimport { getSessionCookie, assertContains } from '../test/helpers.ts'\n\ndescribe('auth handlers', () => {\n  it('POST /login with valid credentials sets session cookie and redirects', async () => {\n    let response = await router.fetch('https://remix.run/login', {\n      method: 'POST',\n      body: new URLSearchParams({\n        email: 'admin@bookstore.com',\n        password: 'admin123',\n      }),\n      redirect: 'manual',\n    })\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/account')\n\n    let sessionId = getSessionCookie(response)\n    assert.ok(sessionId, 'Expected session cookie to be set')\n  })\n\n  it('POST /login with invalid credentials redirects back to login with error', async () => {\n    let response = await router.fetch('https://remix.run/login', {\n      method: 'POST',\n      body: new URLSearchParams({\n        email: 'wrong@example.com',\n        password: 'wrongpassword',\n      }),\n      redirect: 'manual',\n    })\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/login')\n\n    // Follow redirect to see the error message\n    let sessionCookie = getSessionCookie(response)\n    let followUpResponse = await router.fetch('https://remix.run/login', {\n      headers: {\n        Cookie: `session=${sessionCookie}`,\n      },\n    })\n\n    let html = await followUpResponse.text()\n    assertContains(html, 'Invalid email or password')\n  })\n\n  it('POST /login does not treat wildcard characters as email matches', async () => {\n    let response = await router.fetch('https://remix.run/login', {\n      method: 'POST',\n      body: new URLSearchParams({\n        email: '%@bookstore.com',\n        password: 'admin123',\n      }),\n      redirect: 'manual',\n    })\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/login')\n  })\n\n  it('flash error message is cleared after being displayed once', async () => {\n    // POST invalid credentials to trigger flash message\n    let response = await router.fetch('https://remix.run/login', {\n      method: 'POST',\n      body: new URLSearchParams({\n        email: 'wrong@example.com',\n        password: 'wrongpassword',\n      }),\n      redirect: 'manual',\n    })\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/login')\n\n    // Follow redirect to see the error message (first request)\n    let sessionCookie = getSessionCookie(response)\n    let firstFollowUp = await router.fetch('https://remix.run/login', {\n      headers: {\n        Cookie: `session=${sessionCookie}`,\n      },\n    })\n\n    let firstHtml = await firstFollowUp.text()\n    assertContains(firstHtml, 'Invalid email or password')\n\n    // Get updated session cookie (session should be updated to clear flash)\n    let updatedSessionCookie = getSessionCookie(firstFollowUp) || sessionCookie\n\n    // Refresh the page (second request) - error should NOT be shown\n    let secondFollowUp = await router.fetch('https://remix.run/login', {\n      headers: {\n        Cookie: `session=${updatedSessionCookie}`,\n      },\n    })\n\n    let secondHtml = await secondFollowUp.text()\n    assert.ok(\n      !secondHtml.includes('Invalid email or password'),\n      'Expected flash error to be cleared after first display',\n    )\n  })\n\n  it('POST /register creates new user and sets session', async () => {\n    let uniqueEmail = `newuser-${Date.now()}@example.com`\n\n    let response = await router.fetch('https://remix.run/register', {\n      method: 'POST',\n      body: new URLSearchParams({\n        name: 'New User',\n        email: uniqueEmail,\n        password: 'password123',\n      }),\n      redirect: 'manual',\n    })\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/account')\n\n    let sessionId = getSessionCookie(response)\n    assert.ok(sessionId, 'Expected session cookie to be set')\n  })\n\n  it('accessing protected route redirects to login with returnTo parameter', async () => {\n    let response = await router.fetch('https://remix.run/checkout', {\n      redirect: 'manual',\n    })\n\n    assert.equal(response.status, 302)\n    let location = response.headers.get('Location')\n    assert.ok(location, 'Expected Location header')\n    assert.ok(location.startsWith('/login?returnTo='), 'Expected redirect to login with returnTo')\n    assert.ok(\n      location.includes(encodeURIComponent('/checkout')),\n      'Expected returnTo to contain /checkout',\n    )\n  })\n\n  it('successful login with returnTo redirects to original destination', async () => {\n    let response = await router.fetch(\n      'https://remix.run/login?returnTo=' + encodeURIComponent('/checkout'),\n      {\n        method: 'POST',\n        body: new URLSearchParams({\n          email: 'customer@example.com',\n          password: 'password123',\n        }),\n        redirect: 'manual',\n      },\n    )\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/checkout')\n\n    let sessionId = getSessionCookie(response)\n    assert.ok(sessionId, 'Expected session cookie to be set')\n  })\n\n  it('failed login with returnTo preserves returnTo parameter', async () => {\n    let response = await router.fetch(\n      'https://remix.run/login?returnTo=' + encodeURIComponent('/checkout'),\n      {\n        method: 'POST',\n        body: new URLSearchParams({\n          email: 'wrong@example.com',\n          password: 'wrongpassword',\n        }),\n        redirect: 'manual',\n      },\n    )\n\n    assert.equal(response.status, 302)\n    let location = response.headers.get('Location')\n    assert.ok(location, 'Expected Location header')\n    assert.ok(\n      location.includes('returnTo=' + encodeURIComponent('/checkout')),\n      'Expected returnTo to be preserved in redirect',\n    )\n\n    // Follow redirect to verify error message is shown\n    let sessionCookie = getSessionCookie(response)\n    let followUpResponse = await router.fetch('https://remix.run' + location, {\n      headers: {\n        Cookie: `session=${sessionCookie}`,\n      },\n    })\n\n    let html = await followUpResponse.text()\n    assertContains(html, 'Invalid email or password')\n    assertContains(html, 'returnTo=' + encodeURIComponent('/checkout'))\n  })\n\n  it('POST /reset-password with mismatched passwords redirects back with error', async () => {\n    // First, request a password reset to get a token\n    let forgotPasswordResponse = await router.fetch('https://remix.run/forgot-password', {\n      method: 'POST',\n      body: new URLSearchParams({\n        email: 'customer@example.com',\n      }),\n    })\n\n    let html = await forgotPasswordResponse.text()\n    // Extract token from the reset link in the demo response\n    let tokenMatch = html.match(/\\/reset-password\\/([^\"]+)/)\n    assert.ok(tokenMatch, 'Expected to find reset token in response')\n    let token = tokenMatch[1]\n\n    // Try to reset password with mismatched passwords\n    let response = await router.fetch(`https://remix.run/reset-password/${token}`, {\n      method: 'POST',\n      body: new URLSearchParams({\n        password: 'newpassword123',\n        confirmPassword: 'differentpassword',\n      }),\n      redirect: 'manual',\n    })\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), `/reset-password/${token}`)\n\n    // Follow redirect to see the error message\n    let sessionCookie = getSessionCookie(response)\n    let followUpResponse = await router.fetch(`https://remix.run/reset-password/${token}`, {\n      headers: {\n        Cookie: `session=${sessionCookie}`,\n      },\n    })\n\n    let errorHtml = await followUpResponse.text()\n    assertContains(errorHtml, 'Passwords do not match')\n  })\n\n  it('POST /reset-password with invalid token redirects back with error', async () => {\n    let invalidToken = 'invalid-token-12345'\n\n    let response = await router.fetch(`https://remix.run/reset-password/${invalidToken}`, {\n      method: 'POST',\n      body: new URLSearchParams({\n        password: 'newpassword123',\n        confirmPassword: 'newpassword123',\n      }),\n      redirect: 'manual',\n    })\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), `/reset-password/${invalidToken}`)\n\n    // Follow redirect to see the error message\n    let sessionCookie = getSessionCookie(response)\n    let followUpResponse = await router.fetch(`https://remix.run/reset-password/${invalidToken}`, {\n      headers: {\n        Cookie: `session=${sessionCookie}`,\n      },\n    })\n\n    let errorHtml = await followUpResponse.text()\n    assertContains(errorHtml, 'Invalid or expired reset token')\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/auth.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\nimport * as s from 'remix/data-schema'\nimport * as f from 'remix/data-schema/form-data'\nimport { Database } from 'remix/data-table'\nimport { redirect } from 'remix/response/redirect'\n\nimport { routes } from './routes.ts'\nimport { passwordResetTokens, users } from './data/schema.ts'\nimport { Document } from './layout.tsx'\nimport { loadAuth } from './middleware/auth.ts'\nimport { render } from './utils/render.ts'\nimport { Session } from './utils/session.ts'\n\nconst textField = f.field(s.defaulted(s.string(), ''))\nconst loginSchema = f.object({\n  email: textField,\n  password: textField,\n})\nconst registrationSchema = f.object({\n  name: textField,\n  email: textField,\n  password: textField,\n})\nconst forgotPasswordSchema = f.object({\n  email: textField,\n})\nconst resetPasswordSchema = f.object({\n  password: textField,\n  confirmPassword: textField,\n})\n\nexport default {\n  middleware: [loadAuth()],\n  actions: {\n    login: {\n      actions: {\n        index({ get, url }) {\n          let session = get(Session)\n          let error = session.get('error')\n          let formAction = routes.auth.login.action.href(undefined, {\n            returnTo: url.searchParams.get('returnTo') ?? undefined,\n          })\n\n          return render(\n            <Document>\n              <div class=\"card\" mix={[css({ maxWidth: '500px', margin: '2rem auto' })]}>\n                <h1>Login</h1>\n\n                {typeof error === 'string' ? (\n                  <div class=\"alert alert-error\" mix={[css({ marginBottom: '1.5rem' })]}>\n                    {error}\n                  </div>\n                ) : null}\n\n                <form method=\"POST\" action={formAction}>\n                  <div class=\"form-group\">\n                    <label for=\"email\">Email</label>\n                    <input type=\"email\" id=\"email\" name=\"email\" required autoComplete=\"email\" />\n                  </div>\n\n                  <div class=\"form-group\">\n                    <label for=\"password\">Password</label>\n                    <input\n                      type=\"password\"\n                      id=\"password\"\n                      name=\"password\"\n                      required\n                      autoComplete=\"current-password\"\n                    />\n                  </div>\n\n                  <button type=\"submit\" class=\"btn\">\n                    Login\n                  </button>\n                </form>\n\n                <p mix={[css({ marginTop: '1.5rem' })]}>\n                  Don't have an account?{' '}\n                  <a href={routes.auth.register.index.href()}>Register here</a>\n                </p>\n                <p>\n                  <a href={routes.auth.forgotPassword.index.href()}>Forgot password?</a>\n                </p>\n\n                <div\n                  mix={[\n                    css({\n                      marginTop: '2rem',\n                      padding: '1rem',\n                      background: '#f8f9fa',\n                      borderRadius: '4px',\n                    }),\n                  ]}\n                >\n                  <p mix={[css({ fontSize: '0.9rem' })]}>\n                    <strong>Demo Accounts:</strong>\n                  </p>\n                  <p mix={[css({ fontSize: '0.9rem' })]}>Admin: admin@bookstore.com / admin123</p>\n                  <p mix={[css({ fontSize: '0.9rem' })]}>\n                    Customer: customer@example.com / password123\n                  </p>\n                </div>\n              </div>\n            </Document>,\n          )\n        },\n\n        async action({ get, url }) {\n          let db = get(Database)\n          let session = get(Session)\n          let formData = get(FormData)\n          let { email, password } = s.parse(loginSchema, formData)\n          let returnTo = url.searchParams.get('returnTo') ?? undefined\n          let normalizedEmail = normalizeEmail(email)\n\n          let user = await db.findOne(users, { where: { email: normalizedEmail } })\n          if (!user || user.password !== password) {\n            session.flash('error', 'Invalid email or password. Please try again.')\n            return redirect(routes.auth.login.index.href(undefined, { returnTo }))\n          }\n\n          session.regenerateId(true)\n          session.set('userId', user.id)\n\n          return redirect(returnTo ?? routes.account.index.href())\n        },\n      },\n    },\n\n    register: {\n      actions: {\n        index() {\n          return render(\n            <Document>\n              <div class=\"card\" mix={[css({ maxWidth: '500px', margin: '2rem auto' })]}>\n                <h1>Register</h1>\n                <form method=\"POST\" action={routes.auth.register.action.href()}>\n                  <div class=\"form-group\">\n                    <label for=\"name\">Name</label>\n                    <input type=\"text\" id=\"name\" name=\"name\" required autoComplete=\"name\" />\n                  </div>\n\n                  <div class=\"form-group\">\n                    <label for=\"email\">Email</label>\n                    <input type=\"email\" id=\"email\" name=\"email\" required autoComplete=\"email\" />\n                  </div>\n\n                  <div class=\"form-group\">\n                    <label for=\"password\">Password</label>\n                    <input\n                      type=\"password\"\n                      id=\"password\"\n                      name=\"password\"\n                      required\n                      autoComplete=\"new-password\"\n                    />\n                  </div>\n\n                  <button type=\"submit\" class=\"btn\">\n                    Register\n                  </button>\n                </form>\n\n                <p mix={[css({ marginTop: '1.5rem' })]}>\n                  Already have an account? <a href={routes.auth.login.index.href()}>Login here</a>\n                </p>\n              </div>\n            </Document>,\n          )\n        },\n\n        async action({ get }) {\n          let db = get(Database)\n          let session = get(Session)\n          let formData = get(FormData)\n          let { email, name, password } = s.parse(registrationSchema, formData)\n          let normalizedEmail = normalizeEmail(email)\n\n          // Check if user already exists\n          if (await db.findOne(users, { where: { email: normalizedEmail } })) {\n            return render(\n              <Document>\n                <div class=\"card\" mix={[css({ maxWidth: '500px', margin: '2rem auto' })]}>\n                  <div class=\"alert alert-error\">An account with this email already exists.</div>\n                  <p>\n                    <a href={routes.auth.register.index.href()} class=\"btn\">\n                      Back to Register\n                    </a>\n                    <a\n                      href={routes.auth.login.index.href()}\n                      class=\"btn btn-secondary\"\n                      mix={[css({ marginLeft: '0.5rem' })]}\n                    >\n                      Login\n                    </a>\n                  </p>\n                </div>\n              </Document>,\n              { status: 400 },\n            )\n          }\n\n          let user = await db.create(\n            users,\n            {\n              email: normalizedEmail,\n              password,\n              name,\n            },\n            { returnRow: true },\n          )\n\n          session.set('userId', user.id)\n\n          return redirect(routes.account.index.href())\n        },\n      },\n    },\n\n    logout({ get }) {\n      let session = get(Session)\n      session.destroy()\n      return redirect(routes.home.href())\n    },\n\n    forgotPassword: {\n      actions: {\n        index() {\n          return render(\n            <Document>\n              <div class=\"card\" mix={[css({ maxWidth: '500px', margin: '2rem auto' })]}>\n                <h1>Forgot Password</h1>\n                <p>Enter your email address and we'll send you a link to reset your password.</p>\n\n                <form method=\"POST\" action={routes.auth.forgotPassword.action.href()}>\n                  <div class=\"form-group\">\n                    <label for=\"email\">Email</label>\n                    <input type=\"email\" id=\"email\" name=\"email\" required autoComplete=\"email\" />\n                  </div>\n\n                  <button type=\"submit\" class=\"btn\">\n                    Send Reset Link\n                  </button>\n                </form>\n\n                <p mix={[css({ marginTop: '1.5rem' })]}>\n                  <a href={routes.auth.login.index.href()}>Back to Login</a>\n                </p>\n              </div>\n            </Document>,\n          )\n        },\n\n        async action({ get }) {\n          let db = get(Database)\n          let formData = get(FormData)\n          let { email } = s.parse(forgotPasswordSchema, formData)\n          let normalizedEmail = normalizeEmail(email)\n          let user = await db.findOne(users, { where: { email: normalizedEmail } })\n          let token = undefined as string | undefined\n\n          if (user) {\n            token = Math.random().toString(36).substring(2, 15)\n            await db.create(passwordResetTokens, {\n              token,\n              user_id: user.id,\n              expires_at: Date.now() + 3600000,\n            })\n          }\n\n          return render(\n            <Document>\n              <div class=\"card\" mix={[css({ maxWidth: '500px', margin: '2rem auto' })]}>\n                <div class=\"alert alert-success\">Password reset link sent! Check your email.</div>\n\n                {token ? (\n                  <div\n                    mix={[\n                      css({\n                        marginTop: '1rem',\n                        padding: '1rem',\n                        background: '#f8f9fa',\n                        borderRadius: '4px',\n                      }),\n                    ]}\n                  >\n                    <p mix={[css({ fontSize: '0.9rem' })]}>\n                      <strong>Demo Mode:</strong> Click the link below to reset your password\n                    </p>\n                    <p mix={[css({ marginTop: '0.5rem' })]}>\n                      <a\n                        href={routes.auth.resetPassword.index.href({ token })}\n                        class=\"btn btn-secondary\"\n                      >\n                        Reset Password\n                      </a>\n                    </p>\n                  </div>\n                ) : null}\n\n                <p mix={[css({ marginTop: '1.5rem' })]}>\n                  <a href={routes.auth.login.index.href()} class=\"btn\">\n                    Back to Login\n                  </a>\n                </p>\n              </div>\n            </Document>,\n          )\n        },\n      },\n    },\n\n    resetPassword: {\n      actions: {\n        index({ params, get }) {\n          let session = get(Session)\n          let token = params.token\n          let error = session.get('error')\n\n          return render(\n            <Document>\n              <div class=\"card\" mix={[css({ maxWidth: '500px', margin: '2rem auto' })]}>\n                <h1>Reset Password</h1>\n                <p>Enter your new password below.</p>\n\n                {typeof error === 'string' ? (\n                  <div class=\"alert alert-error\" mix={[css({ marginBottom: '1.5rem' })]}>\n                    {error}\n                  </div>\n                ) : null}\n\n                <form method=\"POST\" action={routes.auth.resetPassword.action.href({ token })}>\n                  <div class=\"form-group\">\n                    <label for=\"password\">New Password</label>\n                    <input\n                      type=\"password\"\n                      id=\"password\"\n                      name=\"password\"\n                      required\n                      autoComplete=\"new-password\"\n                    />\n                  </div>\n\n                  <div class=\"form-group\">\n                    <label for=\"confirmPassword\">Confirm Password</label>\n                    <input\n                      type=\"password\"\n                      id=\"confirmPassword\"\n                      name=\"confirmPassword\"\n                      required\n                      autoComplete=\"new-password\"\n                    />\n                  </div>\n\n                  <button type=\"submit\" class=\"btn\">\n                    Reset Password\n                  </button>\n                </form>\n              </div>\n            </Document>,\n          )\n        },\n\n        async action({ get, params }) {\n          let db = get(Database)\n          let session = get(Session)\n          let formData = get(FormData)\n          let { confirmPassword, password } = s.parse(resetPasswordSchema, formData)\n          let token = params.token\n\n          if (!token) {\n            session.flash('error', 'Invalid or expired reset token.')\n            return redirect(routes.auth.forgotPassword.index.href())\n          }\n\n          if (password !== confirmPassword) {\n            session.flash('error', 'Passwords do not match.')\n            return redirect(routes.auth.resetPassword.index.href({ token }))\n          }\n\n          let tokenData = await db.find(passwordResetTokens, { token })\n\n          if (!tokenData || tokenData.expires_at < Date.now()) {\n            session.flash('error', 'Invalid or expired reset token.')\n            return redirect(routes.auth.resetPassword.index.href({ token }))\n          }\n\n          let user = await db.find(users, tokenData.user_id)\n          if (!user) {\n            session.flash('error', 'Invalid or expired reset token.')\n            return redirect(routes.auth.resetPassword.index.href({ token }))\n          }\n\n          await db.update(users, user.id, { password })\n          await db.delete(passwordResetTokens, { token })\n\n          return render(\n            <Document>\n              <div class=\"card\" mix={[css({ maxWidth: '500px', margin: '2rem auto' })]}>\n                <div class=\"alert alert-success\">\n                  Password reset successfully! You can now login with your new password.\n                </div>\n                <p>\n                  <a href={routes.auth.login.index.href()} class=\"btn\">\n                    Login\n                  </a>\n                </p>\n              </div>\n            </Document>,\n          )\n        },\n      },\n    },\n  },\n} satisfies Controller<typeof routes.auth>\n\nfunction normalizeEmail(email: string): string {\n  return email.trim().toLowerCase()\n}\n"
  },
  {
    "path": "demos/bookstore/app/books.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.ts'\nimport { assertContains } from '../test/helpers.ts'\n\ndescribe('books handlers', () => {\n  it('GET /books returns list of books', async () => {\n    let response = await router.fetch('https://remix.run/books')\n\n    assert.equal(response.status, 200)\n    let html = await response.text()\n    assertContains(html, 'Browse Books')\n    assertContains(html, 'Ash &amp; Smoke')\n    assertContains(html, 'Heavy Metal Guitar Riffs')\n    assertContains(html, 'Three Ways to Change Your Life')\n  })\n\n  it('GET /books/:slug returns book details', async () => {\n    let response = await router.fetch('https://remix.run/books/bbq')\n\n    assert.equal(response.status, 200)\n    let html = await response.text()\n    assertContains(html, 'Ash &amp; Smoke')\n    assertContains(html, 'Rusty Char-Broil')\n    assertContains(html, 'Add to Cart')\n  })\n\n  it('GET /books/:slug returns 404 for non-existent book', async () => {\n    let response = await router.fetch('https://remix.run/books/does-not-exist')\n\n    assert.equal(response.status, 404)\n    let html = await response.text()\n    assertContains(html, 'Book Not Found')\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/books.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { Frame, css } from 'remix/component'\n\nimport { routes } from './routes.ts'\nimport { Database, ilike } from 'remix/data-table'\n\nimport { books } from './data/schema.ts'\nimport { BookCard } from './components/book-card.tsx'\nimport { Layout } from './layout.tsx'\nimport { loadAuth } from './middleware/auth.ts'\nimport { render } from './utils/render.ts'\nimport { getCurrentCart } from './utils/context.ts'\nimport { ImageCarousel } from './assets/image-carousel.tsx'\n\nexport default {\n  middleware: [loadAuth()],\n  actions: {\n    async index({ get }) {\n      let db = get(Database)\n      let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })\n      let genres = await db.query(books).select('genre').distinct().orderBy('genre', 'asc').all()\n      let cart = getCurrentCart()\n\n      return render(\n        <Layout>\n          <h1>Browse Books</h1>\n\n          <div class=\"card\" mix={[css({ marginBottom: '2rem' })]}>\n            <form\n              action={routes.search.href()}\n              method=\"GET\"\n              mix={[css({ display: 'flex', gap: '0.5rem' })]}\n            >\n              <input\n                type=\"search\"\n                name=\"q\"\n                placeholder=\"Search books by title, author, or description...\"\n                mix={[css({ flex: 1, padding: '0.5rem' })]}\n              />\n              <button type=\"submit\" class=\"btn\">\n                Search\n              </button>\n            </form>\n          </div>\n\n          <div class=\"card\" mix={[css({ marginBottom: '2rem' })]}>\n            <h3>Browse by Genre</h3>\n            <div\n              mix={[css({ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginTop: '1rem' })]}\n            >\n              {genres.map((genreRow) => (\n                <a\n                  href={routes.books.genre.href({ genre: genreRow.genre })}\n                  class=\"btn btn-secondary\"\n                >\n                  {genreRow.genre}\n                </a>\n              ))}\n            </div>\n          </div>\n\n          <div class=\"grid\">\n            {allBooks.map((book) => {\n              let inCart = cart.items.some((item) => item.slug === book.slug)\n              return <BookCard book={book} inCart={inCart} />\n            })}\n          </div>\n        </Layout>,\n      )\n    },\n\n    async genre({ get, params }) {\n      let db = get(Database)\n      let genre = params.genre\n      let matchingBooks = await db.findMany(books, {\n        where: ilike('genre', genre),\n        orderBy: ['id', 'asc'],\n      })\n\n      if (matchingBooks.length === 0) {\n        return render(\n          <Layout>\n            <div class=\"card\">\n              <h1>Genre Not Found</h1>\n              <p>No books found in the \"{genre}\" genre.</p>\n              <p mix={[css({ marginTop: '1rem' })]}>\n                <a href={routes.books.index.href()} class=\"btn\">\n                  Browse All Books\n                </a>\n              </p>\n            </div>\n          </Layout>,\n          { status: 404 },\n        )\n      }\n\n      let cart = getCurrentCart()\n\n      return render(\n        <Layout>\n          <h1>{genre.charAt(0).toUpperCase() + genre.slice(1)} Books</h1>\n          <p mix={[css({ margin: '1rem 0' })]}>\n            <a href={routes.books.index.href()} class=\"btn btn-secondary\">\n              View All Books\n            </a>\n          </p>\n\n          <div class=\"grid\" mix={[css({ marginTop: '2rem' })]}>\n            {matchingBooks.map((book) => {\n              let inCart = cart.items.some((item) => item.slug === book.slug)\n              return <BookCard book={book} inCart={inCart} />\n            })}\n          </div>\n        </Layout>,\n      )\n    },\n\n    async show({ get, params }) {\n      let db = get(Database)\n      let book = await db.findOne(books, { where: { slug: params.slug } })\n\n      if (!book) {\n        return render(\n          <Layout>\n            <div class=\"card\">\n              <h1>Book Not Found</h1>\n            </div>\n          </Layout>,\n          { status: 404 },\n        )\n      }\n\n      let imageUrls = JSON.parse(book.image_urls) as string[]\n\n      return render(\n        <Layout>\n          <div mix={[css({ display: 'grid', gridTemplateColumns: '300px 1fr', gap: '2rem' })]}>\n            <div\n              mix={[\n                css({\n                  height: '400px',\n                  borderRadius: '8px',\n                  boxShadow: '0 4px 8px rgba(0,0,0,0.1)',\n                  overflow: 'hidden',\n                }),\n              ]}\n            >\n              <ImageCarousel images={imageUrls} />\n            </div>\n\n            <div class=\"card\">\n              <h1>{book.title}</h1>\n              <p class=\"author\" mix={[css({ fontSize: '1.2rem', margin: '0.5rem 0' })]}>\n                by {book.author}\n              </p>\n\n              <p mix={[css({ margin: '1rem 0' })]}>\n                <span class=\"badge badge-info\">{book.genre}</span>\n                <span\n                  class={`badge ${book.in_stock ? 'badge-success' : 'badge-warning'}`}\n                  mix={[css({ marginLeft: '0.5rem' })]}\n                >\n                  {book.in_stock ? 'In Stock' : 'Out of Stock'}\n                </span>\n              </p>\n\n              <p class=\"price\" mix={[css({ fontSize: '2rem', margin: '1rem 0' })]}>\n                ${book.price.toFixed(2)}\n              </p>\n\n              <p mix={[css({ margin: '1.5rem 0', lineHeight: 1.8 })]}>{book.description}</p>\n\n              <div\n                mix={[\n                  css({\n                    margin: '1.5rem 0',\n                    padding: '1rem',\n                    background: '#f8f9fa',\n                    borderRadius: '4px',\n                  }),\n                ]}\n              >\n                <p>\n                  <strong>ISBN:</strong> {book.isbn}\n                </p>\n                <p>\n                  <strong>Published:</strong> {book.published_year}\n                </p>\n              </div>\n\n              {book.in_stock ? (\n                <div mix={[css({ marginTop: '2rem' })]}>\n                  <Frame src={routes.fragments.cartButton.href({ bookId: book.id })} />\n                </div>\n              ) : (\n                <p mix={[css({ color: '#e74c3c', fontWeight: 500 })]}>\n                  This book is currently out of stock.\n                </p>\n              )}\n\n              <p mix={[css({ marginTop: '1.5rem' })]}>\n                <a href={routes.books.index.href()} class=\"btn btn-secondary\">\n                  Back to Books\n                </a>\n              </p>\n            </div>\n          </div>\n        </Layout>,\n        { headers: { 'Cache-Control': 'no-store' } },\n      )\n    },\n  },\n} satisfies Controller<typeof routes.books>\n"
  },
  {
    "path": "demos/bookstore/app/cart.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.ts'\nimport { getSessionCookie, requestWithSession, assertContains } from '../test/helpers.ts'\n\ndescribe('cart handlers', () => {\n  it('POST /cart/api/add adds book to cart', async () => {\n    let response = await router.fetch('https://remix.run/cart/api/add', {\n      method: 'POST',\n      body: new URLSearchParams({\n        bookId: '1',\n        slug: 'bbq',\n      }),\n      redirect: 'manual',\n    })\n\n    assert.equal(response.status, 302)\n    assert.ok(response.headers.get('Location')?.includes('/cart'))\n  })\n\n  it('GET /cart shows cart items', async () => {\n    // First, add item to cart to get a session\n    let addResponse = await router.fetch('https://remix.run/cart/api/add', {\n      method: 'POST',\n      body: new URLSearchParams({\n        bookId: '2',\n        slug: 'heavy-metal',\n      }),\n      redirect: 'manual',\n    })\n\n    let sessionId = getSessionCookie(addResponse)\n    assert.ok(sessionId)\n\n    // Now view cart with session\n    let request = requestWithSession('https://remix.run/cart', sessionId)\n    let response = await router.fetch(request)\n\n    assert.equal(response.status, 200)\n    let html = await response.text()\n    assertContains(html, 'Shopping Cart')\n    assertContains(html, 'Heavy Metal Guitar Riffs')\n  })\n\n  it('cart persists state across requests with same session', async () => {\n    // Add first item\n    let addResponse1 = await router.fetch('https://remix.run/cart/api/add', {\n      method: 'POST',\n      body: new URLSearchParams({\n        bookId: '1',\n        slug: 'bbq',\n      }),\n      redirect: 'manual',\n    })\n\n    let sessionId = getSessionCookie(addResponse1)\n    assert.ok(sessionId)\n\n    // Add second item with same session\n    let addRequest2 = requestWithSession('https://remix.run/cart/api/add', sessionId, {\n      method: 'POST',\n      body: new URLSearchParams({\n        bookId: '3',\n        slug: 'three-ways',\n      }),\n    })\n    await router.fetch(addRequest2)\n\n    // View cart - should have both items\n    let cartRequest = requestWithSession('https://remix.run/cart', sessionId)\n    let cartResponse = await router.fetch(cartRequest)\n\n    let html = await cartResponse.text()\n    assertContains(html, 'Ash & Smoke')\n    assertContains(html, 'Three Ways to Change Your Life')\n  })\n\n  it('GET /fragments/cart-items renders table fragment for cart items', async () => {\n    let addResponse = await router.fetch('https://remix.run/cart/api/add', {\n      method: 'POST',\n      body: new URLSearchParams({\n        bookId: '2',\n        slug: 'heavy-metal',\n      }),\n      redirect: 'manual',\n    })\n\n    let sessionId = getSessionCookie(addResponse)\n    assert.ok(sessionId)\n\n    let request = requestWithSession('https://remix.run/fragments/cart-items', sessionId)\n    let response = await router.fetch(request)\n    let html = await response.text()\n\n    assert.equal(response.status, 200)\n    assertContains(html, '<table>')\n    assertContains(html, '<th>Book</th>')\n    assertContains(html, 'Heavy Metal Guitar Riffs')\n    assertContains(html, 'Update')\n    assertContains(html, 'Remove')\n    assertContains(html, 'Total:')\n  })\n\n  it('GET /fragments/cart-items renders totals and actions', async () => {\n    let addResponse = await router.fetch('https://remix.run/cart/api/add', {\n      method: 'POST',\n      body: new URLSearchParams({\n        bookId: '1',\n        slug: 'bbq',\n      }),\n      redirect: 'manual',\n    })\n\n    let sessionId = getSessionCookie(addResponse)\n    assert.ok(sessionId)\n\n    let request = requestWithSession('https://remix.run/fragments/cart-items', sessionId)\n    let response = await router.fetch(request)\n    let html = await response.text()\n\n    assert.equal(response.status, 200)\n    assertContains(html, 'Total:')\n    assertContains(html, '$16.99')\n    assertContains(html, 'Continue Shopping')\n    assertContains(html, 'Login to Checkout')\n  })\n\n  it('GET /fragments/cart-items renders empty state when cart is empty', async () => {\n    let response = await router.fetch('https://remix.run/fragments/cart-items')\n    let html = await response.text()\n\n    assert.equal(response.status, 200)\n    assertContains(html, 'Your cart is empty.')\n    assertContains(html, 'Browse Books')\n  })\n\n  it('PUT /cart/api/update returns 204 when redirect is none', async () => {\n    let addResponse = await router.fetch('https://remix.run/cart/api/add', {\n      method: 'POST',\n      body: new URLSearchParams({\n        bookId: '1',\n        slug: 'bbq',\n      }),\n      redirect: 'manual',\n    })\n\n    let sessionId = getSessionCookie(addResponse)\n    assert.ok(sessionId)\n\n    let request = requestWithSession('https://remix.run/cart/api/update', sessionId, {\n      method: 'PUT',\n      body: new URLSearchParams({\n        bookId: '1',\n        quantity: '2',\n        redirect: 'none',\n      }),\n    })\n    let response = await router.fetch(request)\n\n    assert.equal(response.status, 204)\n  })\n\n  it('DELETE /cart/api/remove returns 204 when redirect is none', async () => {\n    let addResponse = await router.fetch('https://remix.run/cart/api/add', {\n      method: 'POST',\n      body: new URLSearchParams({\n        bookId: '1',\n        slug: 'bbq',\n      }),\n      redirect: 'manual',\n    })\n\n    let sessionId = getSessionCookie(addResponse)\n    assert.ok(sessionId)\n\n    let request = requestWithSession('https://remix.run/cart/api/remove', sessionId, {\n      method: 'DELETE',\n      body: new URLSearchParams({\n        bookId: '1',\n        redirect: 'none',\n      }),\n    })\n    let response = await router.fetch(request)\n\n    assert.equal(response.status, 204)\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/cart.tsx",
    "content": "import type { Controller, RequestContext } from 'remix/fetch-router'\nimport { Frame } from 'remix/component'\nimport { Database } from 'remix/data-table'\nimport * as s from 'remix/data-schema'\nimport * as f from 'remix/data-schema/form-data'\nimport { redirect } from 'remix/response/redirect'\n\nimport { routes } from './routes.ts'\n\nimport { books } from './data/schema.ts'\nimport { addToCart, removeFromCart, updateCartItem } from './data/cart.ts'\nimport { Layout } from './layout.tsx'\nimport { loadAuth } from './middleware/auth.ts'\nimport { getCurrentCart } from './utils/context.ts'\nimport { parseId } from './utils/ids.ts'\nimport { render } from './utils/render.ts'\nimport { Session } from './utils/session.ts'\n\nconst bookIdField = f.field(s.optional(s.string()))\nconst quantityField = f.field(s.defaulted(s.string(), '1'))\nconst redirectField = f.field(s.optional(s.string()))\nconst bookIdSchema = f.object({\n  bookId: bookIdField,\n})\nconst cartActionSchema = f.object({\n  bookId: bookIdField,\n  redirect: redirectField,\n})\nconst cartUpdateSchema = f.object({\n  bookId: bookIdField,\n  quantity: quantityField,\n  redirect: redirectField,\n})\n\nexport default {\n  middleware: [loadAuth()],\n  actions: {\n    index() {\n      return render(\n        <Layout>\n          <h1>Shopping Cart</h1>\n\n          <div class=\"card\">\n            <Frame name=\"cart\" src={routes.fragments.cartItems.href()} />\n          </div>\n        </Layout>,\n      )\n    },\n\n    api: {\n      actions: {\n        async add({ get }) {\n          let db = get(Database)\n          let session = get(Session)\n          let formData = get(FormData)\n          let { bookId, redirect: redirectTo } = s.parse(cartActionSchema, formData)\n          if (process.env.NODE_ENV !== 'test') {\n            // Simulate network latency\n            await new Promise((resolve) => setTimeout(resolve, 1000))\n          }\n\n          let parsedBookId = parseId(bookId)\n          let book = parsedBookId === undefined ? undefined : await db.find(books, parsedBookId)\n          if (!book) {\n            return new Response('Book not found', { status: 404 })\n          }\n\n          session.set(\n            'cart',\n            addToCart(getCurrentCart(), book.id, book.slug, book.title, book.price, 1),\n          )\n\n          if (redirectTo === 'none') {\n            return new Response(null, { status: 204 })\n          }\n\n          return redirect(routes.cart.index.href())\n        },\n\n        async update({ get }) {\n          let db = get(Database)\n          let session = get(Session)\n          let formData = get(FormData)\n          let { bookId, quantity, redirect: redirectTo } = s.parse(cartUpdateSchema, formData)\n          await new Promise((resolve) => setTimeout(resolve, 1000))\n\n          let parsedBookId = parseId(bookId)\n          let book = parsedBookId === undefined ? undefined : await db.find(books, parsedBookId)\n          if (!book) {\n            return new Response('Book not found', { status: 404 })\n          }\n\n          let nextQuantity = parseInt(quantity, 10)\n\n          session.set('cart', updateCartItem(getCurrentCart(), book.id, nextQuantity))\n\n          if (redirectTo === 'none') {\n            return new Response(null, { status: 204 })\n          }\n\n          return redirect(routes.cart.index.href())\n        },\n\n        async remove({ get }) {\n          let db = get(Database)\n          let session = get(Session)\n          let formData = get(FormData)\n          let { bookId, redirect: redirectTo } = s.parse(cartActionSchema, formData)\n          if (process.env.NODE_ENV !== 'test') {\n            // Simulate network latency\n            await new Promise((resolve) => setTimeout(resolve, 1000))\n          }\n\n          let parsedBookId = parseId(bookId)\n          let book = parsedBookId === undefined ? undefined : await db.find(books, parsedBookId)\n          if (!book) {\n            return new Response('Book not found', { status: 404 })\n          }\n\n          session.set('cart', removeFromCart(getCurrentCart(), book.id))\n\n          if (redirectTo === 'none') {\n            return new Response(null, { status: 204 })\n          }\n\n          return redirect(routes.cart.index.href())\n        },\n      },\n    },\n  },\n} satisfies Controller<typeof routes.cart>\n\nexport async function toggleCart({ get }: RequestContext) {\n  let db = get(Database)\n  let session = get(Session)\n  let formData = get(FormData)\n  let { bookId } = s.parse(bookIdSchema, formData)\n  let parsedBookId = parseId(bookId)\n  let book = parsedBookId === undefined ? undefined : await db.find(books, parsedBookId)\n  if (!book) {\n    return new Response('Book not found', { status: 404 })\n  }\n\n  let cart = getCurrentCart()\n  let inCart = cart.items.some((item) => item.bookId === book.id)\n\n  let next = inCart\n    ? removeFromCart(cart, book.id)\n    : addToCart(cart, book.id, book.slug, book.title, book.price, 1)\n\n  session.set('cart', next)\n\n  return new Response(null, { status: 204 })\n}\n"
  },
  {
    "path": "demos/bookstore/app/checkout.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.ts'\nimport {\n  assertContains,\n  getSessionCookie,\n  loginAsCustomer,\n  requestWithSession,\n} from '../test/helpers.ts'\n\ndescribe('checkout handlers', () => {\n  it('GET /checkout redirects when not authenticated', async () => {\n    let response = await router.fetch('https://remix.run/checkout')\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/login?returnTo=%2Fcheckout')\n  })\n\n  it('POST /checkout creates order when authenticated with items in cart', async () => {\n    let sessionCookie = await loginAsCustomer(router)\n\n    // Add item to cart\n    let addRequest = requestWithSession('https://remix.run/cart/api/add', sessionCookie, {\n      method: 'POST',\n      body: new URLSearchParams({\n        bookId: '1',\n        slug: 'bbq',\n      }),\n    })\n    let addResponse = await router.fetch(addRequest)\n\n    // Get updated session cookie after cart modification\n    sessionCookie = getSessionCookie(addResponse) ?? sessionCookie\n\n    // Submit checkout\n    let checkoutRequest = requestWithSession('https://remix.run/checkout', sessionCookie, {\n      method: 'POST',\n      body: new URLSearchParams({\n        street: '123 Test St',\n        city: 'Test City',\n        state: 'TS',\n        zip: '12345',\n      }),\n    })\n    let checkoutResponse = await router.fetch(checkoutRequest)\n    let confirmationUrl = checkoutResponse.headers.get('Location')\n    sessionCookie = getSessionCookie(checkoutResponse) ?? sessionCookie\n\n    assert.equal(checkoutResponse.status, 302)\n    assert.ok(confirmationUrl?.includes('/checkout/'))\n    assert.ok(confirmationUrl?.includes('/confirmation'))\n\n    let orderId = confirmationUrl?.match(/\\/checkout\\/([^/]+)\\/confirmation/)?.[1]\n    assert.ok(orderId)\n\n    let orderDetailsRequest = requestWithSession(\n      `https://remix.run/account/orders/${orderId}`,\n      sessionCookie,\n    )\n    let orderDetailsResponse = await router.fetch(orderDetailsRequest)\n\n    assert.equal(orderDetailsResponse.status, 200)\n    let orderDetailsHtml = await orderDetailsResponse.text()\n    assertContains(orderDetailsHtml, 'Ash &amp; Smoke')\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/checkout.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\nimport * as s from 'remix/data-schema'\nimport * as f from 'remix/data-schema/form-data'\nimport { Database } from 'remix/data-table'\nimport { redirect } from 'remix/response/redirect'\n\nimport { routes } from './routes.ts'\nimport { requireAuth } from './middleware/auth.ts'\nimport { clearCart, getCartTotal } from './data/cart.ts'\nimport { itemsByOrder, orders, orderItemsWithBook } from './data/schema.ts'\nimport { Layout } from './layout.tsx'\nimport { render } from './utils/render.ts'\nimport { getCurrentUser, getCurrentCart } from './utils/context.ts'\nimport { parseId } from './utils/ids.ts'\nimport { Session } from './utils/session.ts'\n\nconst textField = f.field(s.defaulted(s.string(), ''))\nconst shippingAddressSchema = f.object({\n  street: textField,\n  city: textField,\n  state: textField,\n  zip: textField,\n})\n\nexport default {\n  middleware: [requireAuth()],\n  actions: {\n    index() {\n      let cart = getCurrentCart()\n      let total = getCartTotal(cart)\n\n      if (cart.items.length === 0) {\n        return render(\n          <Layout>\n            <div class=\"card\">\n              <h1>Checkout</h1>\n              <p>Your cart is empty. Add some books before checking out.</p>\n              <p mix={[css({ marginTop: '1rem' })]}>\n                <a href={routes.books.index.href()} class=\"btn\">\n                  Browse Books\n                </a>\n              </p>\n            </div>\n          </Layout>,\n        )\n      }\n\n      return render(\n        <Layout>\n          <h1>Checkout</h1>\n\n          <div class=\"card\">\n            <h2>Order Summary</h2>\n            <table mix={[css({ marginTop: '1rem' })]}>\n              <thead>\n                <tr>\n                  <th>Book</th>\n                  <th>Quantity</th>\n                  <th>Price</th>\n                  <th>Subtotal</th>\n                </tr>\n              </thead>\n              <tbody>\n                {cart.items.map((item) => (\n                  <tr>\n                    <td>{item.title}</td>\n                    <td>{item.quantity}</td>\n                    <td>${item.price.toFixed(2)}</td>\n                    <td>${(item.price * item.quantity).toFixed(2)}</td>\n                  </tr>\n                ))}\n              </tbody>\n              <tfoot>\n                <tr>\n                  <td colSpan={3} mix={[css({ textAlign: 'right', fontWeight: 'bold' })]}>\n                    Total:\n                  </td>\n                  <td mix={[css({ fontWeight: 'bold' })]}>${total.toFixed(2)}</td>\n                </tr>\n              </tfoot>\n            </table>\n          </div>\n\n          <div class=\"card\" mix={[css({ marginTop: '1.5rem' })]}>\n            <h2>Shipping Information</h2>\n            <form method=\"POST\" action={routes.checkout.action.href()}>\n              <div class=\"form-group\">\n                <label for=\"street\">Street Address</label>\n                <input type=\"text\" id=\"street\" name=\"street\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"city\">City</label>\n                <input type=\"text\" id=\"city\" name=\"city\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"state\">State</label>\n                <input type=\"text\" id=\"state\" name=\"state\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"zip\">ZIP Code</label>\n                <input type=\"text\" id=\"zip\" name=\"zip\" required />\n              </div>\n\n              <button type=\"submit\" class=\"btn\">\n                Place Order\n              </button>\n              <a\n                href={routes.cart.index.href()}\n                class=\"btn btn-secondary\"\n                mix={[css({ marginLeft: '0.5rem' })]}\n              >\n                Back to Cart\n              </a>\n            </form>\n          </div>\n        </Layout>,\n      )\n    },\n\n    async action({ get }) {\n      let db = get(Database)\n      let session = get(Session)\n      let formData = get(FormData)\n      let user = getCurrentUser()\n      let cart = getCurrentCart()\n\n      if (cart.items.length === 0) {\n        return redirect(routes.cart.index.href())\n      }\n\n      let shippingAddress = s.parse(shippingAddressSchema, formData)\n\n      let total = cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)\n\n      let order = await db.transaction(async (tx) => {\n        let createdOrder = await tx.create(\n          orders,\n          {\n            user_id: user.id,\n            total,\n            shipping_address_json: JSON.stringify(shippingAddress),\n          },\n          { returnRow: true },\n        )\n\n        await tx.createMany(\n          itemsByOrder.targetTable,\n          cart.items.map((item) => ({\n            order_id: createdOrder.id,\n            book_id: item.bookId,\n            title: item.title,\n            unit_price: item.price,\n            quantity: item.quantity,\n          })),\n        )\n\n        let created = await tx.find(orders, createdOrder.id, {\n          with: { items: orderItemsWithBook },\n        })\n\n        if (!created) {\n          throw new Error('Failed to load created order')\n        }\n\n        return created\n      })\n\n      session.set('cart', clearCart(cart))\n\n      return redirect(routes.checkout.confirmation.href({ orderId: order.id }))\n    },\n\n    async confirmation({ get, params }) {\n      let db = get(Database)\n      let user = getCurrentUser()\n      let orderId = parseId(params.orderId)\n      let order =\n        orderId === undefined\n          ? undefined\n          : await db.find(orders, orderId, {\n              with: { items: orderItemsWithBook },\n            })\n\n      if (!order || order.user_id !== user.id) {\n        return render(\n          <Layout>\n            <div class=\"card\">\n              <h1>Order Not Found</h1>\n              <p>\n                <a href={routes.account.orders.index.href()} class=\"btn\">\n                  View My Orders\n                </a>\n              </p>\n            </div>\n          </Layout>,\n          { status: 404 },\n        )\n      }\n\n      return render(\n        <Layout>\n          <div class=\"alert alert-success\">\n            <h1 mix={[css({ marginBottom: '0.5rem' })]}>Order Confirmed!</h1>\n            <p>Thank you for your purchase. Your order has been placed successfully.</p>\n          </div>\n\n          <div class=\"card\">\n            <h2>Order #{order.id}</h2>\n            <p>\n              <strong>Order Date:</strong> {new Date(order.created_at).toLocaleDateString()}\n            </p>\n            <p>\n              <strong>Total:</strong> ${order.total.toFixed(2)}\n            </p>\n            <p>\n              <strong>Status:</strong> <span class=\"badge badge-info\">{order.status}</span>\n            </p>\n\n            <p mix={[css({ marginTop: '2rem' })]}>\n              We'll send you a confirmation email shortly. You can track your order status in your\n              account.\n            </p>\n\n            <div mix={[css({ marginTop: '2rem' })]}>\n              <a href={routes.account.orders.show.href({ orderId: order.id })} class=\"btn\">\n                View Order Details\n              </a>\n              <a\n                href={routes.books.index.href()}\n                class=\"btn btn-secondary\"\n                mix={[css({ marginLeft: '0.5rem' })]}\n              >\n                Continue Shopping\n              </a>\n            </div>\n          </div>\n        </Layout>,\n      )\n    },\n  },\n} satisfies Controller<typeof routes.checkout>\n"
  },
  {
    "path": "demos/bookstore/app/components/book-card.tsx",
    "content": "import { routes } from '../routes.ts'\nimport type { Book } from '../data/schema.ts'\nimport { Frame, css } from 'remix/component'\n\nexport interface BookCardProps {\n  book: Book\n  inCart: boolean\n}\n\nexport function BookCard() {\n  return ({ book }: BookCardProps) => (\n    <div class=\"book-card\">\n      <img src={book.cover_url} alt={book.title} />\n      <div class=\"book-card-body\">\n        <h3>{book.title}</h3>\n        <p class=\"author\">by {book.author}</p>\n        <p class=\"price\">${book.price.toFixed(2)}</p>\n        <div mix={[css({ display: 'flex', gap: '0.5rem', alignItems: 'center' })]}>\n          <a href={routes.books.show.href({ slug: book.slug })} class=\"btn\">\n            View Details\n          </a>\n\n          <Frame src={routes.fragments.cartButton.href({ bookId: book.id })} />\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "demos/bookstore/app/components/restful-form.tsx",
    "content": "import type { Props, RemixNode } from 'remix/component'\n\nexport interface RestfulFormProps extends Props<'form'> {\n  /**\n   * The name of the hidden <input> field that contains the method override value.\n   * Default is `_method`.\n   */\n  methodOverrideField?: string\n}\n\n/**\n * A wrapper around the `<form>` element that supports RESTful API methods like `PUT` and `DELETE`.\n *\n * When the method is not `GET` or `POST`, a hidden <input> field is added to the form with a\n * \"method override\" value that instructs the server to use the specified method when routing\n * the request.\n */\nexport function RestfulForm() {\n  return ({ method = 'GET', methodOverrideField = '_method', ...props }: RestfulFormProps) => {\n    let upperMethod = method.toUpperCase()\n\n    if (upperMethod === 'GET') {\n      return <form method=\"GET\" {...props} />\n    }\n\n    return (\n      <form method=\"POST\" {...props}>\n        {upperMethod !== 'POST' && (\n          <input type=\"hidden\" name={methodOverrideField} value={upperMethod} />\n        )}\n        {props.children}\n      </form>\n    )\n  }\n}\n"
  },
  {
    "path": "demos/bookstore/app/data/cart.ts",
    "content": "export interface CartItem {\n  bookId: number\n  slug: string\n  title: string\n  price: number\n  quantity: number\n}\n\nexport interface Cart {\n  items: CartItem[]\n}\n\nexport function isCart(value: unknown): value is Cart {\n  return (\n    typeof value === 'object' && value !== null && 'items' in value && Array.isArray(value.items)\n  )\n}\n\nexport function getCart(value: unknown): Cart {\n  return isCart(value) ? value : { items: [] }\n}\n\nexport function addToCart(\n  cart: Cart,\n  bookId: number,\n  slug: string,\n  title: string,\n  price: number,\n  quantity: number = 1,\n): Cart {\n  let existingItem = cart.items.find((item) => item.bookId === bookId)\n  if (existingItem) {\n    existingItem.quantity += quantity\n  } else {\n    cart.items.push({ bookId, slug, title, price, quantity })\n  }\n\n  return cart\n}\n\nexport function updateCartItem(cart: Cart, bookId: number, quantity: number): Cart | undefined {\n  let item = cart.items.find((item) => item.bookId === bookId)\n\n  if (!item) return undefined\n\n  if (quantity <= 0) {\n    cart.items = cart.items.filter((item) => item.bookId !== bookId)\n  } else {\n    item.quantity = quantity\n  }\n\n  return cart\n}\n\nexport function removeFromCart(cart: Cart, bookId: number): Cart {\n  cart.items = cart.items.filter((item) => item.bookId !== bookId)\n  return cart\n}\n\nexport function clearCart(cart: Cart): Cart {\n  return { items: [] }\n}\n\nexport function getCartTotal(cart: Cart): number {\n  return cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)\n}\n"
  },
  {
    "path": "demos/bookstore/app/data/schema.ts",
    "content": "import { belongsTo, column as c, table, hasMany } from 'remix/data-table'\nimport type { TableRow, TableRowWith } from 'remix/data-table'\n\nexport const books = table({\n  name: 'books',\n  columns: {\n    id: c.integer(),\n    slug: c.text(),\n    title: c.text(),\n    author: c.text(),\n    description: c.text(),\n    price: c.decimal(10, 2),\n    genre: c.text(),\n    image_urls: c.text(),\n    cover_url: c.text(),\n    isbn: c.text(),\n    published_year: c.integer(),\n    in_stock: c.boolean(),\n  },\n  beforeWrite({ value }) {\n    let next = { ...value }\n\n    if (typeof next.slug === 'string') {\n      next.slug = normalizeSlug(next.slug)\n    }\n\n    if (typeof next.title === 'string') {\n      next.title = normalizeText(next.title)\n    }\n\n    if (typeof next.author === 'string') {\n      next.author = normalizeText(next.author)\n    }\n\n    if (typeof next.description === 'string') {\n      next.description = normalizeText(next.description)\n    }\n\n    if (typeof next.genre === 'string') {\n      next.genre = normalizeText(next.genre)\n    }\n\n    if (typeof next.isbn === 'string') {\n      next.isbn = normalizeText(next.isbn)\n    }\n\n    if (typeof next.cover_url === 'string' && next.cover_url.trim() === '') {\n      next.cover_url = '/images/placeholder.jpg'\n    }\n\n    return { value: next }\n  },\n  validate({ operation, value }) {\n    let issues: Array<{ message: string; path?: Array<string | number> }> = []\n    let slug = typeof value.slug === 'string' ? normalizeSlug(value.slug) : undefined\n    let title = typeof value.title === 'string' ? normalizeText(value.title) : undefined\n\n    if (operation === 'create' && !slug) {\n      issues.push({ message: 'Book slug is required.', path: ['slug'] })\n    }\n\n    if (slug !== undefined && slug.length === 0) {\n      issues.push({ message: 'Book slug is required.', path: ['slug'] })\n    }\n\n    if (operation === 'create' && !title) {\n      issues.push({ message: 'Book title is required.', path: ['title'] })\n    }\n\n    if (title !== undefined && title.length === 0) {\n      issues.push({ message: 'Book title is required.', path: ['title'] })\n    }\n\n    if (typeof value.price === 'number' && (!Number.isFinite(value.price) || value.price < 0)) {\n      issues.push({ message: 'Price must be a non-negative number.', path: ['price'] })\n    }\n\n    if (\n      typeof value.published_year === 'number' &&\n      (!Number.isInteger(value.published_year) || value.published_year < 0)\n    ) {\n      issues.push({\n        message: 'Published year must be a valid positive integer.',\n        path: ['published_year'],\n      })\n    }\n\n    return issues.length > 0 ? { issues } : { value }\n  },\n  afterRead({ value }) {\n    if (typeof value.cover_url !== 'string' || value.cover_url.trim() !== '') {\n      return { value }\n    }\n\n    return {\n      value: {\n        ...value,\n        cover_url: '/images/placeholder.jpg',\n      },\n    }\n  },\n})\n\nexport const users = table({\n  name: 'users',\n  columns: {\n    id: c.integer(),\n    email: c.text(),\n    password: c.text(),\n    name: c.text(),\n    role: c.enum(['customer', 'admin']),\n    created_at: c.integer(),\n  },\n  beforeWrite({ operation, value }) {\n    let next = { ...value }\n\n    if (typeof next.name === 'string') {\n      next.name = normalizeText(next.name)\n    }\n\n    if (typeof next.email === 'string') {\n      next.email = normalizeEmail(next.email)\n    }\n\n    if (typeof next.password === 'string') {\n      next.password = next.password.trim()\n    }\n\n    if (operation === 'create' && next.role === undefined) {\n      next.role = 'customer'\n    }\n\n    if (operation === 'create' && next.created_at === undefined) {\n      next.created_at = Date.now()\n    }\n\n    return { value: next }\n  },\n  validate({ operation, value }) {\n    let issues: Array<{ message: string; path?: Array<string | number> }> = []\n    let email = typeof value.email === 'string' ? normalizeEmail(value.email) : undefined\n    let name = typeof value.name === 'string' ? normalizeText(value.name) : undefined\n\n    if (operation === 'create' && !name) {\n      issues.push({ message: 'Name is required.', path: ['name'] })\n    }\n\n    if (name !== undefined && name.length === 0) {\n      issues.push({ message: 'Name is required.', path: ['name'] })\n    }\n\n    if (operation === 'create' && !email) {\n      issues.push({ message: 'Email is required.', path: ['email'] })\n    }\n\n    if (email !== undefined && !isValidEmail(email)) {\n      issues.push({ message: 'Email address is invalid.', path: ['email'] })\n    }\n\n    if (\n      (operation === 'create' && typeof value.password !== 'string') ||\n      (typeof value.password === 'string' && value.password.length < 8)\n    ) {\n      issues.push({\n        message: 'Password must be at least 8 characters long.',\n        path: ['password'],\n      })\n    }\n\n    return issues.length > 0 ? { issues } : { value }\n  },\n  afterRead({ value }) {\n    if (typeof value.email !== 'string' || typeof value.name !== 'string') {\n      return { value }\n    }\n\n    let email = normalizeEmail(value.email)\n    let name = normalizeText(value.name)\n\n    if (email === value.email && name === value.name) {\n      return { value }\n    }\n\n    return {\n      value: {\n        ...value,\n        email,\n        name,\n      },\n    }\n  },\n})\n\nexport const orders = table({\n  name: 'orders',\n  columns: {\n    id: c.integer(),\n    user_id: c.integer(),\n    total: c.decimal(10, 2),\n    status: c.enum(['pending', 'processing', 'shipped', 'delivered']),\n    shipping_address_json: c.text(),\n    created_at: c.integer(),\n  },\n  beforeWrite({ operation, value }) {\n    let next = { ...value }\n\n    if (operation === 'create' && next.status === undefined) {\n      next.status = 'pending'\n    }\n\n    if (operation === 'create' && next.created_at === undefined) {\n      next.created_at = Date.now()\n    }\n\n    return { value: next }\n  },\n  validate({ value }) {\n    let issues: Array<{ message: string; path?: Array<string | number> }> = []\n\n    if (typeof value.total === 'number' && (!Number.isFinite(value.total) || value.total < 0)) {\n      issues.push({ message: 'Order total must be a non-negative number.', path: ['total'] })\n    }\n\n    if (\n      typeof value.shipping_address_json === 'string' &&\n      !isJsonObject(value.shipping_address_json)\n    ) {\n      issues.push({\n        message: 'Shipping address must be a valid JSON object string.',\n        path: ['shipping_address_json'],\n      })\n    }\n\n    return issues.length > 0 ? { issues } : { value }\n  },\n})\n\nexport const orderItems = table({\n  name: 'order_items',\n  primaryKey: ['order_id', 'book_id'],\n  columns: {\n    order_id: c.integer(),\n    book_id: c.integer(),\n    title: c.text(),\n    unit_price: c.decimal(10, 2),\n    quantity: c.integer(),\n  },\n  beforeWrite({ value }) {\n    let next = { ...value }\n\n    if (typeof next.title === 'string') {\n      next.title = normalizeText(next.title)\n    }\n\n    return { value: next }\n  },\n  validate({ value }) {\n    let issues: Array<{ message: string; path?: Array<string | number> }> = []\n\n    if (\n      typeof value.quantity === 'number' &&\n      (!Number.isInteger(value.quantity) || value.quantity < 1)\n    ) {\n      issues.push({ message: 'Quantity must be an integer greater than 0.', path: ['quantity'] })\n    }\n\n    if (\n      typeof value.unit_price === 'number' &&\n      (!Number.isFinite(value.unit_price) || value.unit_price < 0)\n    ) {\n      issues.push({\n        message: 'Unit price must be a non-negative number.',\n        path: ['unit_price'],\n      })\n    }\n\n    return issues.length > 0 ? { issues } : { value }\n  },\n})\n\nexport const itemsByOrder = hasMany(orders, orderItems)\nexport const bookForOrderItem = belongsTo(orderItems, books)\nexport const orderItemsWithBook = itemsByOrder\n  .orderBy('book_id', 'asc')\n  .with({ book: bookForOrderItem })\n\nexport const passwordResetTokens = table({\n  name: 'password_reset_tokens',\n  primaryKey: ['token'],\n  columns: {\n    token: c.text(),\n    user_id: c.integer(),\n    expires_at: c.integer(),\n  },\n})\n\nexport type Book = TableRow<typeof books>\nexport type User = TableRow<typeof users>\nexport type Order = TableRowWith<typeof orders, { items: OrderItem[] }>\nexport type OrderItem = TableRowWith<\n  typeof itemsByOrder.targetTable,\n  { book: TableRow<typeof bookForOrderItem.targetTable> | null }\n>\n\nfunction normalizeEmail(email: string): string {\n  return email.trim().toLowerCase()\n}\n\nfunction normalizeText(value: string): string {\n  return value.trim()\n}\n\nfunction normalizeSlug(value: string): string {\n  return value\n    .trim()\n    .toLowerCase()\n    .replace(/[^a-z0-9\\s-]/g, '')\n    .replace(/\\s+/g, '-')\n    .replace(/-+/g, '-')\n}\n\nfunction isValidEmail(email: string): boolean {\n  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)\n}\n\nfunction isJsonObject(value: string): boolean {\n  try {\n    let parsed = JSON.parse(value)\n    return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)\n  } catch {\n    return false\n  }\n}\n"
  },
  {
    "path": "demos/bookstore/app/data/setup.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\nimport { fileURLToPath } from 'node:url'\nimport { sql } from 'remix/data-table'\nimport { loadMigrations } from 'remix/data-table/migrations/node'\n\nimport { db, initializeBookstoreDatabase } from './setup.ts'\n\nfunction getRows(result: { rows?: Record<string, unknown>[] }): Record<string, unknown>[] {\n  return result.rows ?? []\n}\n\nfunction readRowString(row: Record<string, unknown>, key: string): string {\n  let value = row[key]\n\n  if (typeof value !== 'string') {\n    throw new Error('Expected string row value for key \"' + key + '\"')\n  }\n\n  return value\n}\n\nfunction readRowCount(row: Record<string, unknown>, key: string): number {\n  let value = row[key]\n\n  if (typeof value === 'number') {\n    return value\n  }\n\n  if (typeof value === 'bigint') {\n    return Number(value)\n  }\n\n  if (typeof value === 'string') {\n    return Number(value)\n  }\n\n  throw new Error('Expected numeric row value for key \"' + key + '\"')\n}\n\ndescribe('bookstore database setup', () => {\n  it('applies migrations and materializes expected schema artifacts', async () => {\n    await initializeBookstoreDatabase()\n\n    let migrationsPath = fileURLToPath(new URL('../../data/migrations/', import.meta.url))\n    let migrations = await loadMigrations(migrationsPath)\n\n    let journalResult = await db.exec(\n      sql`select id, name from data_table_migrations order by id asc`,\n    )\n    let journalRows = getRows(journalResult)\n    let journalIds = journalRows.map((row) => readRowString(row, 'id'))\n    let migrationIds = migrations.map((migration) => migration.id)\n\n    assert.equal(journalRows.length, migrations.length)\n    assert.deepEqual(journalIds, migrationIds)\n\n    assert.equal(await db.adapter.hasTable({ name: 'books' }), true)\n    assert.equal(await db.adapter.hasTable({ name: 'users' }), true)\n    assert.equal(await db.adapter.hasTable({ name: 'orders' }), true)\n    assert.equal(await db.adapter.hasTable({ name: 'order_items' }), true)\n    assert.equal(await db.adapter.hasTable({ name: 'password_reset_tokens' }), true)\n\n    assert.equal(await db.adapter.hasColumn({ name: 'books' }, 'slug'), true)\n    assert.equal(await db.adapter.hasColumn({ name: 'users' }, 'email'), true)\n    assert.equal(await db.adapter.hasColumn({ name: 'orders' }, 'user_id'), true)\n\n    let ordersIndex = await db.exec(\n      sql`select name from sqlite_master where type = 'index' and name = 'orders_user_id_idx'`,\n    )\n    let orderItemsOrderIndex = await db.exec(\n      sql`select name from sqlite_master where type = 'index' and name = 'order_items_order_id_idx'`,\n    )\n\n    assert.equal(getRows(ordersIndex).length, 1)\n    assert.equal(getRows(orderItemsOrderIndex).length, 1)\n  })\n\n  it('does not duplicate migration journal entries when initialized more than once', async () => {\n    await initializeBookstoreDatabase()\n\n    let before = await db.exec(sql`select count(*) as count from data_table_migrations`)\n    let beforeRows = getRows(before)\n    assert.ok(beforeRows.length > 0)\n    let beforeCount = readRowCount(beforeRows[0], 'count')\n\n    await initializeBookstoreDatabase()\n\n    let after = await db.exec(sql`select count(*) as count from data_table_migrations`)\n    let afterRows = getRows(after)\n    assert.ok(afterRows.length > 0)\n    let afterCount = readRowCount(afterRows[0], 'count')\n\n    assert.equal(afterCount, beforeCount)\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/data/setup.ts",
    "content": "import * as fs from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport BetterSqlite3 from 'better-sqlite3'\nimport { createDatabase } from 'remix/data-table'\nimport { createMigrationRunner } from 'remix/data-table/migrations'\nimport { loadMigrations } from 'remix/data-table/migrations/node'\nimport { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'\n\nimport { books, orderItems, orders, users } from './schema.ts'\n\nlet dataDirectoryUrl = new URL('../../data/', import.meta.url)\nlet migrationsDirectoryPath = fileURLToPath(new URL('migrations/', dataDirectoryUrl))\nlet databaseFilePath = getDatabaseFilePath()\n\nfs.mkdirSync(fileURLToPath(dataDirectoryUrl), { recursive: true })\n\nif (process.env.NODE_ENV === 'test' && fs.existsSync(databaseFilePath)) {\n  fs.unlinkSync(databaseFilePath)\n}\n\nlet sqlite = new BetterSqlite3(databaseFilePath)\nsqlite.pragma('foreign_keys = ON')\nlet adapter = createSqliteDatabaseAdapter(sqlite)\n\nexport let db = createDatabase(adapter)\n\nlet initializePromise: Promise<void> | null = null\n\nexport async function initializeBookstoreDatabase(): Promise<void> {\n  if (!initializePromise) {\n    initializePromise = initialize()\n  }\n\n  await initializePromise\n}\n\nasync function initialize(): Promise<void> {\n  let migrations = await loadMigrations(migrationsDirectoryPath)\n  let migrationRunner = createMigrationRunner(adapter, migrations)\n  await migrationRunner.up()\n\n  let booksCount = await db.count(books)\n  if (booksCount === 0) {\n    await db.createMany(books, [\n      {\n        id: 1,\n        slug: 'bbq',\n        title: 'Ash & Smoke',\n        author: 'Rusty Char-Broil',\n        description: 'The perfect gift for the BBQ enthusiast in your life!',\n        price: 16.99,\n        genre: 'cookbook',\n        image_urls: JSON.stringify(['/images/bbq-1.png', '/images/bbq-2.png', '/images/bbq-3.png']),\n        cover_url: '/images/bbq-1.png',\n        isbn: '978-0525559474',\n        published_year: 2020,\n        in_stock: true,\n      },\n      {\n        id: 2,\n        slug: 'heavy-metal',\n        title: 'Heavy Metal Guitar Riffs',\n        author: 'Axe Master Krush',\n        description: 'The ultimate guide to heavy metal guitar riffs!',\n        price: 27.0,\n        genre: 'music',\n        image_urls: JSON.stringify([\n          '/images/heavy-metal-1.png',\n          '/images/heavy-metal-2.png',\n          '/images/heavy-metal-3.png',\n        ]),\n        cover_url: '/images/heavy-metal-1.png',\n        isbn: '978-0735211292',\n        published_year: 2018,\n        in_stock: true,\n      },\n      {\n        id: 3,\n        slug: 'three-ways',\n        title: 'Three Ways to Change Your Life',\n        author: 'Wisdom Sage',\n        description: 'Life-changing strategies for modern living and personal growth.',\n        price: 28.99,\n        genre: 'self-help',\n        image_urls: JSON.stringify([\n          '/images/three-ways-1.png',\n          '/images/three-ways-2.png',\n          '/images/three-ways-3.png',\n        ]),\n        cover_url: '/images/three-ways-1.png',\n        isbn: '978-0061120084',\n        published_year: 2021,\n        in_stock: false,\n      },\n    ])\n  }\n\n  let usersCount = await db.count(users)\n  if (usersCount === 0) {\n    await db.createMany(users, [\n      {\n        id: 1,\n        email: 'admin@bookstore.com',\n        password: 'admin123',\n        name: 'Admin User',\n        role: 'admin',\n        created_at: new Date('2024-01-15').getTime(),\n      },\n      {\n        id: 2,\n        email: 'customer@example.com',\n        password: 'password123',\n        name: 'John Doe',\n        role: 'customer',\n        created_at: new Date('2024-03-01').getTime(),\n      },\n    ])\n  }\n\n  let ordersCount = await db.count(orders)\n  if (ordersCount === 0) {\n    await db.createMany(orders, [\n      {\n        id: 1001,\n        user_id: 2,\n        total: 45.98,\n        status: 'delivered',\n        shipping_address_json: JSON.stringify({\n          street: '123 Main St',\n          city: 'Boston',\n          state: 'MA',\n          zip: '02101',\n        }),\n        created_at: new Date('2024-09-15').getTime(),\n      },\n      {\n        id: 1002,\n        user_id: 2,\n        total: 54.0,\n        status: 'shipped',\n        shipping_address_json: JSON.stringify({\n          street: '123 Main St',\n          city: 'Boston',\n          state: 'MA',\n          zip: '02101',\n        }),\n        created_at: new Date('2024-10-01').getTime(),\n      },\n    ])\n  }\n\n  let orderItemsCount = await db.count(orderItems)\n  if (orderItemsCount === 0) {\n    await db.createMany(orderItems, [\n      {\n        order_id: 1001,\n        book_id: 1,\n        title: 'Ash & Smoke',\n        unit_price: 16.99,\n        quantity: 1,\n      },\n      {\n        order_id: 1001,\n        book_id: 3,\n        title: 'Three Ways to Change Your Life',\n        unit_price: 28.99,\n        quantity: 1,\n      },\n      {\n        order_id: 1002,\n        book_id: 2,\n        title: 'Heavy Metal Guitar Riffs',\n        unit_price: 27.0,\n        quantity: 2,\n      },\n    ])\n  }\n}\n\nfunction getDatabaseFilePath(): string {\n  let fileName =\n    process.env.NODE_ENV === 'test'\n      ? `bookstore.test.${process.pid}.${Date.now()}.sqlite`\n      : 'bookstore.sqlite'\n\n  return fileURLToPath(new URL(fileName, dataDirectoryUrl))\n}\n"
  },
  {
    "path": "demos/bookstore/app/fragments.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\nimport { Database } from 'remix/data-table'\nimport type { routes } from './routes.ts'\nimport { CartButton } from './assets/cart-button.tsx'\nimport { CartItems } from './assets/cart-items.tsx'\nimport { getCartTotal } from './data/cart.ts'\nimport { books } from './data/schema.ts'\nimport { loadAuth } from './middleware/auth.ts'\nimport { getCurrentCart, getCurrentUserSafely } from './utils/context.ts'\nimport { parseId } from './utils/ids.ts'\nimport { renderFragment } from './utils/render.ts'\nimport { routes as appRoutes } from './routes.ts'\n\nexport default {\n  middleware: [loadAuth()],\n  actions: {\n    async cartButton({ get, params }) {\n      let db = get(Database)\n      let bookId = parseId(params.bookId)\n      let book = bookId === undefined ? undefined : await db.find(books, bookId)\n\n      if (!book) {\n        return renderFragment(<p>Book not found</p>, { status: 404 })\n      }\n\n      let cart = getCurrentCart()\n      let inCart = cart.items.some((item) => item.bookId === book.id)\n\n      return renderFragment(<CartButton inCart={inCart} id={book.id} slug={book.slug} />)\n    },\n\n    cartItems() {\n      let cart = getCurrentCart()\n      let total = getCartTotal(cart)\n      let user = getCurrentUserSafely()\n\n      if (cart.items.length === 0) {\n        return renderFragment(\n          <div mix={[css({ marginTop: '2rem' })]}>\n            <p>Your cart is empty.</p>\n            <p mix={[css({ marginTop: '1rem' })]}>\n              <a href={appRoutes.books.index.href()} class=\"btn\">\n                Browse Books\n              </a>\n            </p>\n          </div>,\n        )\n      }\n\n      return renderFragment(<CartItems items={cart.items} total={total} canCheckout={!!user} />)\n    },\n  },\n} satisfies Controller<typeof routes.fragments>\n"
  },
  {
    "path": "demos/bookstore/app/layout.tsx",
    "content": "import type { RemixNode } from 'remix/component'\n\nimport { routes } from './routes.ts'\nimport { getCurrentUserSafely } from './utils/context.ts'\n\nexport function Document() {\n  return ({ title = 'Bookstore', children }: { title?: string; children?: RemixNode }) => (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"UTF-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        <title>{title}</title>\n        <script type=\"module\" async src={routes.assets.href({ path: 'entry.js' })} />\n        <link rel=\"stylesheet\" href=\"/app.css\" />\n      </head>\n      <body>{children}</body>\n    </html>\n  )\n}\n\nexport function Layout() {\n  return ({ children }: { children?: RemixNode }) => {\n    let user = getCurrentUserSafely()\n\n    return (\n      <Document>\n        <header>\n          <div class=\"container\">\n            <h1>\n              <a href={routes.home.href()}>📚 Bookstore</a>\n            </h1>\n            <nav>\n              <a href={routes.home.href()}>Home</a>\n              <a href={routes.books.index.href()}>Books</a>\n              <a href={routes.about.href()}>About</a>\n              <a href={routes.contact.index.href()}>Contact</a>\n              <a href={routes.cart.index.href()}>Cart</a>\n              {user ? (\n                <>\n                  <a href={routes.account.index.href()}>Account</a>\n                  {user.role === 'admin' ? <a href={routes.admin.index.href()}>Admin</a> : null}\n                  <form\n                    method=\"POST\"\n                    action={routes.auth.logout.href()}\n                    style={{ display: 'inline' }}\n                  >\n                    <button type=\"submit\" class=\"btn btn-secondary\" style=\"margin-left: 1rem;\">\n                      Logout\n                    </button>\n                  </form>\n                </>\n              ) : (\n                <>\n                  <a href={routes.auth.login.index.href()}>Login</a>\n                  <a href={routes.auth.register.index.href()}>Register</a>\n                </>\n              )}\n            </nav>\n          </div>\n        </header>\n        <main>\n          <div class=\"container\">{children}</div>\n        </main>\n        <footer>\n          <div class=\"container\">\n            <p>&copy; {new Date().getFullYear()} Bookstore Demo. Built with Remix.</p>\n          </div>\n        </footer>\n      </Document>\n    )\n  }\n}\n"
  },
  {
    "path": "demos/bookstore/app/marketing.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.ts'\nimport { assertContains } from '../test/helpers.ts'\n\ndescribe('marketing handlers', () => {\n  it('GET / returns home page', async () => {\n    let response = await router.fetch('https://remix.run/')\n\n    assert.equal(response.status, 200)\n    let html = await response.text()\n    assertContains(html, 'Welcome to the Bookstore')\n    assertContains(html, 'Browse Books')\n  })\n\n  it('POST /contact returns success message', async () => {\n    let response = await router.fetch('https://remix.run/contact', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: new URLSearchParams({\n        name: 'Test User',\n        email: 'test@example.com',\n        message: 'Test message',\n      }).toString(),\n    })\n\n    assert.equal(response.status, 200)\n    let html = await response.text()\n    assertContains(html, 'Thank you for your message')\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/marketing.tsx",
    "content": "import type { BuildAction, Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\n\nimport { routes } from './routes.ts'\nimport { BookCard } from './components/book-card.tsx'\nimport { Layout } from './layout.tsx'\nimport { Database, ilike, inList, or } from 'remix/data-table'\nimport { books } from './data/schema.ts'\nimport { render } from './utils/render.ts'\nimport { getCurrentCart } from './utils/context.ts'\n\nexport let home: BuildAction<'GET', typeof routes.home> = {\n  async action({ get }) {\n    let db = get(Database)\n    let cart = getCurrentCart()\n    let featuredSlugs = ['bbq', 'heavy-metal', 'three-ways']\n    let featuredBookRows = await db.findMany(books, {\n      where: inList('slug', featuredSlugs),\n    })\n    let featuredBooksBySlug = new Map(featuredBookRows.map((book) => [book.slug, book]))\n    let featuredBooks = featuredSlugs.flatMap((slug) => {\n      let book = featuredBooksBySlug.get(slug)\n      return book ? [book] : []\n    })\n\n    return render(\n      <Layout>\n        <div class=\"card\">\n          <h1>Welcome to the Bookstore</h1>\n          <p mix={[css({ margin: '1rem 0' })]}>\n            Discover your next favorite book from our curated collection of fiction, non-fiction,\n            and more.\n          </p>\n          <p>\n            <a href={routes.books.index.href()} class=\"btn\">\n              Browse Books\n            </a>\n          </p>\n        </div>\n\n        <h2 mix={[css({ margin: '2rem 0 1rem' })]}>Featured Books</h2>\n        <div class=\"grid\">\n          {featuredBooks.map((book) => {\n            let inCart = cart.items.some((item) => item.slug === book.slug)\n            return <BookCard book={book} inCart={inCart} />\n          })}\n        </div>\n      </Layout>,\n      { headers: { 'Cache-Control': 'no-store' } },\n    )\n  },\n}\n\nexport let about: BuildAction<'GET', typeof routes.about> = {\n  action() {\n    return render(\n      <Layout>\n        <div class=\"card\">\n          <h1>About Our Bookstore</h1>\n          <p mix={[css({ margin: '1rem 0' })]}>\n            Welcome to our online bookstore, a demo application built to showcase the capabilities\n            of\n            <strong>fetch-router</strong> - a powerful, type-safe routing library for web\n            applications.\n          </p>\n\n          <h2 mix={[css({ margin: '1.5rem 0 0.5rem' })]}>What This Demo Shows</h2>\n          <ul mix={[css({ marginLeft: '2rem', lineHeight: 2 })]}>\n            <li>\n              <strong>Resource Routes:</strong> Full RESTful CRUD operations\n            </li>\n            <li>\n              <strong>Nested Routes:</strong> Deep route hierarchies with type safety\n            </li>\n            <li>\n              <strong>Custom Parameters:</strong> Flexible parameter naming (slug, orderId, etc.)\n            </li>\n            <li>\n              <strong>HTTP Methods:</strong> GET, POST, PUT, DELETE properly used\n            </li>\n            <li>\n              <strong>Middleware:</strong> Authentication and authorization\n            </li>\n            <li>\n              <strong>Type Safety:</strong> End-to-end type checking for routes and handlers\n            </li>\n          </ul>\n\n          <h2 mix={[css({ margin: '1.5rem 0 0.5rem' })]}>Try It Out</h2>\n          <p mix={[css({ margin: '1rem 0' })]}>\n            Explore the site to see all these features in action. You can browse books, create an\n            account, add items to your cart, and even access the admin panel (login as\n            admin@bookstore.com / admin123).\n          </p>\n\n          <p mix={[css({ marginTop: '2rem' })]}>\n            <a href={routes.books.index.href()} class=\"btn\">\n              Explore Books\n            </a>\n            <a\n              href={routes.auth.register.index.href()}\n              class=\"btn btn-secondary\"\n              mix={[css({ marginLeft: '1rem' })]}\n            >\n              Create Account\n            </a>\n          </p>\n        </div>\n      </Layout>,\n    )\n  },\n}\n\nexport let contact: Controller<typeof routes.contact> = {\n  actions: {\n    index() {\n      return render(\n        <Layout>\n          <div class=\"card\">\n            <h1>Contact Us</h1>\n            <p mix={[css({ margin: '1rem 0' })]}>\n              Have a question or feedback? We'd love to hear from you!\n            </p>\n\n            <form method=\"POST\" action={routes.contact.action.href()}>\n              <div class=\"form-group\">\n                <label for=\"name\">Name</label>\n                <input type=\"text\" id=\"name\" name=\"name\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"email\">Email</label>\n                <input type=\"email\" id=\"email\" name=\"email\" required />\n              </div>\n\n              <div class=\"form-group\">\n                <label for=\"message\">Message</label>\n                <textarea id=\"message\" name=\"message\" required></textarea>\n              </div>\n\n              <button type=\"submit\" class=\"btn\">\n                Send Message\n              </button>\n            </form>\n          </div>\n        </Layout>,\n      )\n    },\n\n    async action() {\n      return render(\n        <Layout>\n          <div class=\"alert alert-success\">\n            Thank you for your message! We'll get back to you soon.\n          </div>\n          <div class=\"card\">\n            <p>\n              <a href={routes.home.href()} class=\"btn\">\n                Return Home\n              </a>\n            </p>\n          </div>\n        </Layout>,\n      )\n    },\n  },\n}\n\nexport let search: BuildAction<'GET', typeof routes.search> = {\n  async action({ get, url }) {\n    let db = get(Database)\n    let query = url.searchParams.get('q') ?? ''\n    let matchingBooks = query\n      ? await db.findMany(books, {\n          where: or(\n            ilike('title', `%${query.toLowerCase()}%`),\n            ilike('author', `%${query.toLowerCase()}%`),\n            ilike('description', `%${query.toLowerCase()}%`),\n          ),\n          orderBy: ['id', 'asc'],\n        })\n      : []\n    let cart = getCurrentCart()\n\n    return render(\n      <Layout>\n        <h1>Search Results</h1>\n\n        <div class=\"card\" mix={[css({ marginBottom: '2rem' })]}>\n          <form\n            action={routes.search.href()}\n            method=\"GET\"\n            mix={[css({ display: 'flex', gap: '0.5rem' })]}\n          >\n            <input\n              type=\"search\"\n              name=\"q\"\n              placeholder=\"Search books...\"\n              value={query}\n              mix={[css({ flex: 1, padding: '0.5rem' })]}\n            />\n            <button type=\"submit\" class=\"btn\">\n              Search\n            </button>\n          </form>\n        </div>\n\n        {query ? (\n          <p mix={[css({ marginBottom: '1rem' })]}>\n            Found {matchingBooks.length} result(s) for \"{query}\"\n          </p>\n        ) : null}\n\n        <div class=\"grid\">\n          {matchingBooks.length > 0 ? (\n            matchingBooks.map((book) => {\n              let inCart = cart.items.some((item) => item.slug === book.slug)\n              return <BookCard book={book} inCart={inCart} />\n            })\n          ) : (\n            <p>No books found matching your search.</p>\n          )}\n        </div>\n      </Layout>,\n    )\n  },\n}\n"
  },
  {
    "path": "demos/bookstore/app/middleware/admin.ts",
    "content": "import type { Middleware } from 'remix/fetch-router'\n\nimport { getCurrentUser } from '../utils/context.ts'\n\n/**\n * Middleware that requires a user to have admin role.\n * Returns 403 Forbidden if user is not an admin.\n * Must be used after requireAuth middleware.\n */\nexport function requireAdmin(): Middleware {\n  return () => {\n    let user = getCurrentUser()\n\n    if (user.role !== 'admin') {\n      return new Response('Forbidden', { status: 403 })\n    }\n  }\n}\n"
  },
  {
    "path": "demos/bookstore/app/middleware/auth.ts",
    "content": "import type { Middleware } from 'remix/fetch-router'\nimport type { Route } from 'remix/fetch-router/routes'\nimport { Database } from 'remix/data-table'\nimport { redirect } from 'remix/response/redirect'\n\nimport { routes } from '../routes.ts'\nimport { users } from '../data/schema.ts'\nimport { setCurrentUser } from '../utils/context.ts'\nimport { parseId } from '../utils/ids.ts'\nimport { Session } from '../utils/session.ts'\n\n/**\n * Middleware that optionally loads the current user if authenticated.\n * Does not redirect if not authenticated.\n * Attaches user (if any) to request context.\n */\nexport function loadAuth(): Middleware {\n  return async ({ get }) => {\n    let db = get(Database)\n    let session = get(Session)\n    let userId = parseId(session.get('userId'))\n\n    if (userId !== undefined) {\n      let user = await db.find(users, userId)\n      if (user) {\n        setCurrentUser(user)\n      }\n    }\n  }\n}\n\nexport interface RequireAuthOptions {\n  /**\n   * Where to redirect if the user is not authenticated.\n   * Defaults to the login page.\n   */\n  redirectTo?: Route\n}\n\n/**\n * Middleware that requires a user to be authenticated.\n * Redirects to login if not authenticated.\n * Attaches user to request context.\n */\nexport function requireAuth(options?: RequireAuthOptions): Middleware {\n  let redirectRoute = options?.redirectTo ?? routes.auth.login.index\n\n  return async ({ get, url }) => {\n    let db = get(Database)\n    let session = get(Session)\n    let userId = parseId(session.get('userId'))\n    let user = userId === undefined ? undefined : await db.find(users, userId)\n\n    if (!user) {\n      // Capture the current URL to redirect back to after login\n      return redirect(redirectRoute.href(undefined, { returnTo: url.pathname + url.search }), 302)\n    }\n\n    setCurrentUser(user)\n  }\n}\n"
  },
  {
    "path": "demos/bookstore/app/middleware/database.ts",
    "content": "import type { Middleware } from 'remix/fetch-router'\nimport { Database } from 'remix/data-table'\n\nimport { db } from '../data/setup.ts'\n\nexport function loadDatabase(): Middleware {\n  return async (context, next) => {\n    context.set(Database, db)\n    return next()\n  }\n}\n"
  },
  {
    "path": "demos/bookstore/app/router.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.ts'\n\ndescribe('router', () => {\n  it('responds to basic GET request', async () => {\n    let response = await router.fetch('https://remix.run/')\n\n    assert.equal(response.status, 200)\n    assert.equal(response.headers.get('Content-Type'), 'text/html; charset=UTF-8')\n\n    let html = await response.text()\n    assert.ok(html.includes('Welcome to the Bookstore'))\n  })\n\n  it('returns 404 for unknown routes', async () => {\n    let response = await router.fetch('https://remix.run/does-not-exist')\n\n    assert.equal(response.status, 404)\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/router.ts",
    "content": "import { createRouter } from 'remix/fetch-router'\nimport { asyncContext } from 'remix/async-context-middleware'\nimport { compression } from 'remix/compression-middleware'\nimport { formData } from 'remix/form-data-middleware'\nimport { logger } from 'remix/logger-middleware'\nimport { methodOverride } from 'remix/method-override-middleware'\nimport { session } from 'remix/session-middleware'\nimport { staticFiles } from 'remix/static-middleware'\n\nimport { routes } from './routes.ts'\nimport { initializeBookstoreDatabase } from './data/setup.ts'\nimport { sessionCookie, sessionStorage } from './utils/session.ts'\nimport { uploadHandler } from './utils/uploads.ts'\n\nimport adminController from './admin.tsx'\nimport accountController from './account.tsx'\nimport authController from './auth.tsx'\nimport booksController from './books.tsx'\nimport cartController from './cart.tsx'\nimport { toggleCart } from './cart.tsx'\nimport checkoutController from './checkout.tsx'\nimport * as marketingController from './marketing.tsx'\nimport { uploadsAction } from './uploads.tsx'\nimport fragmentsController from './fragments.tsx'\nimport { loadDatabase } from './middleware/database.ts'\n\nlet middleware = []\n\nif (process.env.NODE_ENV === 'development') {\n  middleware.push(logger())\n}\n\nmiddleware.push(compression())\nmiddleware.push(\n  staticFiles('./public', {\n    cacheControl: 'no-store, must-revalidate',\n    etag: false,\n    lastModified: false,\n  }),\n)\nmiddleware.push(formData({ uploadHandler }))\nmiddleware.push(methodOverride())\nmiddleware.push(session(sessionCookie, sessionStorage))\nmiddleware.push(asyncContext())\nmiddleware.push(loadDatabase())\n\nawait initializeBookstoreDatabase()\n\nexport let router = createRouter({ middleware })\n\nrouter.get(routes.uploads, uploadsAction)\nrouter.map(routes.fragments, fragmentsController)\nrouter.post(routes.api.cartToggle, toggleCart)\n\nrouter.map(routes.home, marketingController.home)\nrouter.map(routes.about, marketingController.about)\nrouter.map(routes.contact, marketingController.contact)\nrouter.map(routes.search, marketingController.search)\n\nrouter.map(routes.books, booksController)\nrouter.map(routes.auth, authController)\nrouter.map(routes.cart, cartController)\nrouter.map(routes.account, accountController)\nrouter.map(routes.checkout, checkoutController)\nrouter.map(routes.admin, adminController)\n"
  },
  {
    "path": "demos/bookstore/app/routes.ts",
    "content": "import { del, get, post, put, route, form, resources } from 'remix/fetch-router/routes'\n\nexport let routes = route({\n  assets: '/assets/*path',\n  uploads: '/uploads/*key',\n  fragments: route('fragments', {\n    cartButton: get('/cart-button/:bookId'),\n    cartItems: get('/cart-items'),\n  }),\n  api: route('api', {\n    cartToggle: post('/cart/toggle'),\n  }),\n\n  // Simple static routes\n  home: '/',\n  about: '/about',\n  contact: form('contact'),\n  search: '/search',\n\n  // Public book routes\n  books: {\n    index: '/books',\n    genre: '/books/genre/:genre',\n    show: '/books/:slug',\n  },\n\n  // Auth routes\n  auth: {\n    login: form('login'),\n    register: form('register'),\n    logout: post('logout'),\n    forgotPassword: form('forgot-password'),\n    resetPassword: form('reset-password/:token'),\n  },\n\n  // Account section (protected, nested routes)\n  account: route('account', {\n    index: '/',\n    settings: form('settings', {\n      formMethod: 'PUT',\n      names: {\n        action: 'update',\n      },\n    }),\n\n    // Orders as nested resources with custom param\n    orders: resources('orders', {\n      only: ['index', 'show'], // Read-only, no create/edit/delete\n      param: 'orderId',\n    }),\n  }),\n\n  // Cart and shopping\n  cart: route('cart', {\n    index: get('/'),\n\n    // API-style endpoints under /cart/api\n    api: {\n      add: post('/api/add'),\n      update: put('/api/update'),\n      remove: del('/api/remove'),\n    },\n  }),\n\n  // Checkout flow\n  checkout: route('checkout', {\n    index: get('/'),\n    action: post('/'),\n    confirmation: get('/:orderId/confirmation'),\n  }),\n\n  // Admin section (protected, showcases full CRUD on multiple resources)\n  admin: route('admin', {\n    index: get('/'),\n\n    // Full CRUD on books\n    books: resources('books', { param: 'bookId' }),\n\n    // Partial CRUD on users (no create, users self-register)\n    users: resources('users', {\n      only: ['index', 'show', 'edit', 'update', 'destroy'],\n      param: 'userId',\n    }),\n\n    // Orders view-only\n    orders: resources('orders', {\n      only: ['index', 'show'],\n      param: 'orderId',\n    }),\n  }),\n})\n"
  },
  {
    "path": "demos/bookstore/app/uploads.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { loginAsAdmin, requestWithSession } from '../test/helpers.ts'\nimport { router } from './router.ts'\nimport { books } from './data/schema.ts'\nimport { db } from './data/setup.ts'\nimport { uploadsStorage as uploads } from './utils/uploads.ts'\n\ndescribe('uploads handler', () => {\n  it('serves uploaded files from storage', async () => {\n    let sessionId = await loginAsAdmin(router)\n\n    // Get initial book count\n    let initialBookCount = await db.count(books)\n\n    // Create a multipart form with a file upload\n    let boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'\n    let fileContent = 'fake image data'\n    let formBody = [\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"title\"',\n      '',\n      'Book with Cover',\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"author\"',\n      '',\n      'Test Author',\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"slug\"',\n      '',\n      'book-with-cover',\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"description\"',\n      '',\n      'A book with a cover image',\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"price\"',\n      '',\n      '19.99',\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"genre\"',\n      '',\n      'test',\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"isbn\"',\n      '',\n      '978-1234567890',\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"publishedYear\"',\n      '',\n      '2024',\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"inStock\"',\n      '',\n      'true',\n      `------${boundary}`,\n      'Content-Disposition: form-data; name=\"cover\"; filename=\"test-cover.jpg\"',\n      'Content-Type: image/jpeg',\n      '',\n      fileContent,\n      `------${boundary}--`,\n    ].join('\\r\\n')\n\n    // Create book with file upload\n    let createRequest = requestWithSession('https://remix.run/admin/books', sessionId, {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=----${boundary}`,\n      },\n      body: formBody,\n    })\n\n    let createResponse = await router.fetch(createRequest)\n    assert.equal(createResponse.status, 302)\n    assert.ok(createResponse.headers.get('Location')?.includes('/admin/books'))\n\n    // Get the newly created book from the database\n    let currentBookCount = await db.count(books)\n    assert.equal(currentBookCount, initialBookCount + 1)\n\n    let newBook = await db.findOne(books, { where: { slug: 'book-with-cover' } })\n    assert.ok(newBook)\n    assert.equal(newBook.slug, 'book-with-cover')\n    assert.ok(newBook.cover_url.startsWith('/uploads/'))\n\n    // Now fetch the uploaded file from the /uploads route using the book's cover_url\n    let fileResponse = await router.fetch(`https://remix.run${newBook.cover_url}`)\n\n    assert.equal(fileResponse.status, 200)\n    assert.equal(fileResponse.headers.get('Content-Type'), 'image/jpeg')\n    assert.equal(fileResponse.headers.get('Cache-Control'), 'public, max-age=31536000')\n    assert.equal(await fileResponse.text(), fileContent)\n  })\n\n  it('returns 404 for non-existent files', async () => {\n    let response = await router.fetch('https://remix.run/uploads/nonexistent/file.jpg')\n\n    assert.equal(response.status, 404)\n    assert.equal(await response.text(), 'File not found')\n  })\n\n  it('serves files with correct content type', async () => {\n    // Store different file types\n    let pngFile = new File(['png data'], 'test.png', { type: 'image/png' })\n    let pdfFile = new File(['pdf data'], 'test.pdf', { type: 'application/pdf' })\n\n    await uploads.set('images/test.png', pngFile)\n    await uploads.set('docs/test.pdf', pdfFile)\n\n    // Verify PNG\n    let pngResponse = await router.fetch('https://remix.run/uploads/images/test.png')\n    assert.equal(pngResponse.status, 200)\n    assert.equal(pngResponse.headers.get('Content-Type'), 'image/png')\n\n    // Verify PDF\n    let pdfResponse = await router.fetch('https://remix.run/uploads/docs/test.pdf')\n    assert.equal(pdfResponse.status, 200)\n    assert.equal(pdfResponse.headers.get('Content-Type'), 'application/pdf')\n  })\n})\n"
  },
  {
    "path": "demos/bookstore/app/uploads.tsx",
    "content": "import type { BuildAction } from 'remix/fetch-router'\nimport { createFileResponse as sendFile } from 'remix/response/file'\n\nimport type { routes } from './routes.ts'\nimport { uploadsStorage } from './utils/uploads.ts'\n\nexport let uploadsAction: BuildAction<'GET', typeof routes.uploads> = async ({\n  request,\n  params,\n}) => {\n  let file = await uploadsStorage.get(params.key)\n\n  if (!file) {\n    return new Response('File not found', { status: 404 })\n  }\n\n  return sendFile(file, request, {\n    cacheControl: 'public, max-age=31536000',\n  })\n}\n"
  },
  {
    "path": "demos/bookstore/app/utils/context.ts",
    "content": "import { createContextKey } from 'remix/fetch-router'\nimport { getContext } from 'remix/async-context-middleware'\n\nimport { getCart } from '../data/cart.ts'\nimport type { Cart } from '../data/cart.ts'\nimport type { User } from '../data/schema.ts'\nimport { Session } from './session.ts'\n\n// Context key for attaching user data to request context\nconst CurrentUser = createContextKey<User>()\n\n/**\n * Get the current authenticated user from request context.\n */\nexport function getCurrentUser(): User {\n  return getContext().get(CurrentUser)\n}\n\n/**\n * Get the current authenticated user from request context, or null if not authenticated.\n * Safe to use when running behind loadAuth middleware (not requireAuth).\n */\nexport function getCurrentUserSafely(): User | null {\n  try {\n    return getCurrentUser()\n  } catch {\n    return null\n  }\n}\n\n/**\n * Set the current authenticated user in request context.\n */\nexport function setCurrentUser(user: User): void {\n  getContext().set(CurrentUser, user)\n}\n\n/**\n * Get the current cart from the session.\n */\nexport function getCurrentCart(): Cart {\n  return getCart(getContext().get(Session).get('cart'))\n}\n"
  },
  {
    "path": "demos/bookstore/app/utils/ids.ts",
    "content": "export function parseId(value: unknown): number | undefined {\n  if (typeof value === 'number') {\n    return Number.isSafeInteger(value) ? value : undefined\n  }\n\n  if (typeof value !== 'string') {\n    return undefined\n  }\n\n  let parsed = Number(value)\n  return Number.isSafeInteger(parsed) ? parsed : undefined\n}\n"
  },
  {
    "path": "demos/bookstore/app/utils/render.ts",
    "content": "import type { RemixNode } from 'remix/component'\nimport { renderToStream } from 'remix/component/server'\nimport { getContext } from 'remix/async-context-middleware'\nimport type { Router } from 'remix/fetch-router'\n\nexport function render(node: RemixNode, init?: ResponseInit) {\n  let context = getContext()\n  let request = context.request\n  let router = context.router\n\n  let stream = renderToStream(node, {\n    resolveFrame: (src) => resolveFrame(router, request, src),\n    onError(error) {\n      console.error(error)\n    },\n  })\n\n  let headers = new Headers(init?.headers)\n  if (!headers.has('Content-Type')) {\n    headers.set('Content-Type', 'text/html; charset=UTF-8')\n  }\n\n  return new Response(stream, { ...init, headers })\n}\n\nasync function resolveFrame(router: Router, request: Request, src: string) {\n  let url = new URL(src, request.url)\n\n  let headers = new Headers()\n  headers.set('accept', 'text/html')\n  headers.set('accept-encoding', 'identity')\n\n  let cookie = request.headers.get('cookie')\n  if (cookie) headers.set('cookie', cookie)\n\n  let res = await router.fetch(\n    new Request(url, {\n      method: 'GET',\n      headers,\n      signal: request.signal,\n    }),\n  )\n\n  if (!res.ok) {\n    return `<pre>Frame error: ${res.status} ${res.statusText}</pre>`\n  }\n\n  if (res.body) return res.body\n  return res.text()\n}\n\nexport function renderFragment(node: RemixNode, init?: ResponseInit) {\n  let headers = new Headers(init?.headers)\n  if (!headers.has('Cache-Control')) {\n    headers.set('Cache-Control', 'no-store')\n  }\n\n  return render(node, { ...init, headers })\n}\n"
  },
  {
    "path": "demos/bookstore/app/utils/session.ts",
    "content": "import * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { createCookie } from 'remix/cookie'\nimport { Session } from 'remix/session'\nimport { createFsSessionStorage } from 'remix/session/fs-storage'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\n\n/**\n * Session cookie configuration for the bookstore demo.\n * Uses secure defaults with a 30-day expiration.\n */\nexport let sessionCookie = createCookie('session', {\n  secrets: ['s3cr3t-k3y-for-d3mo'],\n  httpOnly: true,\n  sameSite: 'Lax',\n  maxAge: 2592000, // 30 days\n  path: '/',\n})\n\n/**\n * Filesystem-based session storage.\n * Sessions are stored in the app's tmp/sessions directory.\n */\nexport let sessionStorage = createFsSessionStorage(\n  path.resolve(__dirname, '..', '..', 'tmp', 'sessions'),\n)\n\nexport { Session }\n"
  },
  {
    "path": "demos/bookstore/app/utils/uploads.ts",
    "content": "import { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { FileUpload } from 'remix/form-data-parser'\nimport { createFsFileStorage } from 'remix/file-storage/fs'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nexport const uploadsStorage = createFsFileStorage(resolve(__dirname, '..', '..', 'tmp', 'uploads'))\n\n/**\n * Upload handler for file uploads. Stores files in local storage and returns\n * a public URL path that can be used to access the file.\n */\nexport async function uploadHandler(file: FileUpload): Promise<string> {\n  // Generate unique key for this file\n  let ext = file.name.split('.').pop() || 'jpg'\n  let key = `${file.fieldName}/${Date.now()}-${Math.random().toString(36).substring(7)}.${ext}`\n\n  // Put the file in storage\n  await uploadsStorage.set(key, file)\n\n  // Return public URL path\n  return `/uploads/${key}`\n}\n"
  },
  {
    "path": "demos/bookstore/data/migrations/20260228090000_create_bookstore_schema.ts",
    "content": "import { column as c, createMigration } from 'remix/data-table/migrations'\nimport { table } from 'remix/data-table'\n\nexport default createMigration({\n  async up({ schema }) {\n    let books = table({\n      name: 'books',\n      columns: {\n        id: c.integer().primaryKey().autoIncrement(),\n        slug: c.text().notNull().unique(),\n        title: c.text().notNull(),\n        author: c.text().notNull(),\n        description: c.text().notNull(),\n        price: c.decimal(10, 2).notNull(),\n        genre: c.text().notNull(),\n        image_urls: c.text().notNull(),\n        cover_url: c.text().notNull(),\n        isbn: c.text().notNull(),\n        published_year: c.integer().notNull(),\n        in_stock: c.boolean().notNull(),\n      },\n    })\n    await schema.createTable(books)\n\n    let users = table({\n      name: 'users',\n      columns: {\n        id: c.integer().primaryKey().autoIncrement(),\n        email: c.text().notNull().unique(),\n        password: c.text().notNull(),\n        name: c.text().notNull(),\n        role: c.text().notNull(),\n        created_at: c.integer().notNull(),\n      },\n    })\n    await schema.createTable(users)\n\n    let orders = table({\n      name: 'orders',\n      columns: {\n        id: c.integer().primaryKey().autoIncrement(),\n        user_id: c\n          .integer()\n          .notNull()\n          .references('users', 'id', 'orders_user_id_fk')\n          .onDelete('restrict'),\n        total: c.decimal(10, 2).notNull(),\n        status: c.text().notNull(),\n        shipping_address_json: c.text().notNull(),\n        created_at: c.integer().notNull(),\n      },\n    })\n    await schema.createTable(orders)\n    await schema.createIndex('orders', 'user_id', { name: 'orders_user_id_idx' })\n\n    let orderItems = table({\n      name: 'order_items',\n      primaryKey: ['order_id', 'book_id'],\n      columns: {\n        order_id: c\n          .integer()\n          .notNull()\n          .references('orders', 'id', 'order_items_order_id_fk')\n          .onDelete('cascade'),\n        book_id: c\n          .integer()\n          .notNull()\n          .references('books', 'id', 'order_items_book_id_fk')\n          .onDelete('restrict'),\n        title: c.text().notNull(),\n        unit_price: c.decimal(10, 2).notNull(),\n        quantity: c.integer().notNull(),\n      },\n    })\n    await schema.createTable(orderItems)\n    await schema.createIndex('order_items', 'order_id', { name: 'order_items_order_id_idx' })\n    await schema.createIndex('order_items', 'book_id', { name: 'order_items_book_id_idx' })\n\n    let passwordResetTokens = table({\n      name: 'password_reset_tokens',\n      primaryKey: ['token'],\n      columns: {\n        token: c.text().primaryKey(),\n        user_id: c\n          .integer()\n          .notNull()\n          .references('users', 'id', 'password_reset_tokens_user_id_fk')\n          .onDelete('cascade'),\n        expires_at: c.integer().notNull(),\n      },\n    })\n    await schema.createTable(passwordResetTokens)\n    await schema.createIndex('password_reset_tokens', 'user_id', {\n      name: 'password_reset_tokens_user_id_idx',\n    })\n  },\n  async down({ schema }) {\n    await schema.dropTable('password_reset_tokens', { ifExists: true })\n    await schema.dropTable('order_items', { ifExists: true })\n    await schema.dropTable('orders', { ifExists: true })\n    await schema.dropTable('users', { ifExists: true })\n    await schema.dropTable('books', { ifExists: true })\n  },\n})\n"
  },
  {
    "path": "demos/bookstore/package.json",
    "content": "{\n  \"name\": \"bookstore-demo\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"better-sqlite3\": \"^12.6.2\",\n    \"remix\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/better-sqlite3\": \"^7.6.13\",\n    \"@types/dom-navigation\": \"^1.0.7\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"esbuild\": \"^0.25.10\",\n    \"tsx\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"dev\": \"pnpm run --filter \\\".\\\" --parallel \\\"/^dev:/\\\"\",\n    \"dev:server\": \"NODE_ENV=development tsx watch server.ts\",\n    \"dev:browser\": \"esbuild app/assets/*.tsx --outbase=app/assets --outdir=public/assets --bundle --minify --splitting --format=esm --entry-names='[dir]/[name]' --chunk-names='chunks/[name]-[hash]' --sourcemap --watch\",\n    \"start\": \"tsx server.ts\",\n    \"test\": \"NODE_ENV=test tsx --test\",\n    \"typecheck\": \"tsc --noEmit\"\n  }\n}\n"
  },
  {
    "path": "demos/bookstore/public/app.css",
    "content": "@layer app, rmx;\n\n/* CSS Reset */\n@layer app {\n  *,\n  *::before,\n  *::after {\n    margin: 0;\n    padding: 0;\n    box-sizing: border-box;\n  }\n  button,\n  input,\n  select,\n  textarea {\n    font: inherit;\n    color: inherit;\n    line-height: inherit;\n  }\n  button {\n    background: none;\n    border: none;\n    cursor: pointer;\n  }\n  img {\n    display: block;\n    max-width: 100%;\n  }\n  a {\n    color: inherit;\n  }\n\n  /* Base Styles */\n  body {\n    font-family:\n      system-ui,\n      -apple-system,\n      sans-serif;\n    line-height: 1.6;\n    color: #333;\n    background: #f5f5f5;\n  }\n  .container {\n    max-width: 1200px;\n    margin: 0 auto;\n    padding: 0 20px;\n  }\n  header {\n    background: #2c3e50;\n    color: white;\n    padding: 1rem 0;\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  }\n  header .container {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n  }\n  header h1 {\n    font-size: 1.5rem;\n  }\n  header h1 a {\n    color: white;\n    text-decoration: none;\n  }\n  nav a {\n    color: white;\n    text-decoration: none;\n    margin-left: 1.5rem;\n    padding: 0.5rem;\n    border-radius: 4px;\n    transition: background 0.2s;\n  }\n  nav a:hover {\n    background: rgba(255, 255, 255, 0.1);\n  }\n  main {\n    padding: 2rem 0;\n    min-height: calc(100vh - 200px);\n  }\n  footer {\n    background: #34495e;\n    color: white;\n    padding: 2rem 0;\n    margin-top: 4rem;\n  }\n  .card {\n    background: white;\n    border-radius: 8px;\n    padding: 1.5rem;\n    margin-bottom: 1.5rem;\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  }\n  .btn {\n    display: inline-block;\n    padding: 0.5rem 1rem;\n    background: #3498db;\n    color: white;\n    text-decoration: none;\n    border-radius: 4px;\n    border: none;\n    cursor: pointer;\n    font-size: 1rem;\n    transition: background 0.2s;\n  }\n  .btn:hover {\n    background: #2980b9;\n  }\n  .btn-secondary {\n    background: #95a5a6;\n  }\n  .btn-secondary:hover {\n    background: #7f8c8d;\n  }\n  .btn-danger {\n    background: #e74c3c;\n  }\n  .btn-danger:hover {\n    background: #c0392b;\n  }\n  .form-group {\n    margin-bottom: 1rem;\n  }\n  .form-group label {\n    display: block;\n    margin-bottom: 0.25rem;\n    font-weight: 500;\n  }\n  .form-group input,\n  .form-group textarea,\n  .form-group select {\n    width: 100%;\n    padding: 0.5rem;\n    border: 1px solid #ddd;\n    border-radius: 4px;\n    font-size: 1rem;\n  }\n  .form-group textarea {\n    min-height: 100px;\n    resize: vertical;\n  }\n  .alert {\n    padding: 1rem;\n    border-radius: 4px;\n    margin-bottom: 1rem;\n  }\n  .alert-success {\n    background: #d4edda;\n    border: 1px solid #c3e6cb;\n    color: #155724;\n  }\n  .alert-error {\n    background: #f8d7da;\n    border: 1px solid #f5c6cb;\n    color: #721c24;\n  }\n  .grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n    gap: 1.5rem;\n  }\n  .book-card {\n    background: white;\n    border-radius: 8px;\n    overflow: hidden;\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n    transition: transform 0.2s;\n  }\n  .book-card:hover {\n    transform: translateY(-4px);\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);\n  }\n  .book-card img {\n    width: 100%;\n    height: 300px;\n    object-fit: cover;\n    background: #ecf0f1;\n  }\n  .book-card-body {\n    padding: 1rem;\n  }\n  .book-card h3 {\n    font-size: 1.1rem;\n    margin-bottom: 0.5rem;\n  }\n  .book-card .author {\n    color: #7f8c8d;\n    font-size: 0.9rem;\n    margin-bottom: 0.5rem;\n  }\n  .book-card .price {\n    font-size: 1.25rem;\n    font-weight: bold;\n    color: #27ae60;\n    margin: 0.5rem 0;\n  }\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    background: white;\n  }\n  th,\n  td {\n    padding: 0.75rem;\n    text-align: left;\n    border-bottom: 1px solid #ddd;\n  }\n  th {\n    background: #f8f9fa;\n    font-weight: 600;\n  }\n  .actions {\n    display: flex;\n    gap: 0.5rem;\n  }\n  .actions form {\n    display: inline;\n  }\n  .badge {\n    display: inline-block;\n    padding: 0.25rem 0.5rem;\n    border-radius: 4px;\n    font-size: 0.875rem;\n    font-weight: 500;\n  }\n  .badge-success {\n    background: #d4edda;\n    color: #155724;\n  }\n  .badge-warning {\n    background: #fff3cd;\n    color: #856404;\n  }\n  .badge-info {\n    background: #d1ecf1;\n    color: #0c5460;\n  }\n}\n"
  },
  {
    "path": "demos/bookstore/server.ts",
    "content": "import * as http from 'node:http'\nimport { createRequestListener } from 'remix/node-fetch-server'\n\nimport { router } from './app/router.ts'\n\nlet server = http.createServer(\n  createRequestListener(async (request) => {\n    try {\n      return await router.fetch(request)\n    } catch (error) {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  }),\n)\n\nlet port = process.env.PORT ? parseInt(process.env.PORT, 10) : 44100\n\nserver.listen(port, () => {\n  console.log(`Bookstore is running on http://localhost:${port}`)\n  console.log('')\n  console.log('Demo accounts:')\n  console.log('  Admin:    admin@bookstore.com / admin123')\n  console.log('  Customer: customer@example.com / password123')\n  console.log('')\n})\n\nlet shuttingDown = false\n\nfunction shutdown() {\n  if (shuttingDown) return\n  shuttingDown = true\n  server.close(() => {\n    process.exit(0)\n  })\n}\n\nprocess.on('SIGINT', shutdown)\nprocess.on('SIGTERM', shutdown)\n"
  },
  {
    "path": "demos/bookstore/test/helpers.ts",
    "content": "import { SetCookie, Cookie } from 'remix/headers'\n\n/**\n * Extract a specific cookie value from Set-Cookie headers\n */\nexport function getCookie(response: Response, name: string): string | null {\n  let setCookieHeaders = response.headers.getSetCookie()\n\n  for (let header of setCookieHeaders) {\n    let setCookie = new SetCookie(header)\n    if (setCookie.name === name) {\n      return setCookie.value ?? null\n    }\n  }\n\n  return null\n}\n\n/**\n * Extract session cookie from Set-Cookie headers\n */\nexport function getSessionCookie(response: Response): string | null {\n  return getCookie(response, 'session')\n}\n\n/**\n * Create a request with a session cookie\n */\nexport function requestWithSession(\n  url: string,\n  sessionCookie: string,\n  init?: RequestInit,\n): Request {\n  let cookie = new Cookie({ session: sessionCookie })\n\n  return new Request(url, {\n    ...init,\n    headers: {\n      ...init?.headers,\n      Cookie: cookie.toString(),\n    },\n  })\n}\n\n/**\n * Assert that HTML contains a substring\n */\nexport function assertContains(html: string, text: string): void {\n  if (!html.includes(text)) {\n    throw new Error(`Expected HTML to contain \"${text}\"`)\n  }\n}\n\n/**\n * Assert that HTML does not contain a substring\n */\nexport function assertNotContains(html: string, text: string): void {\n  if (html.includes(text)) {\n    throw new Error(`Expected HTML to not contain \"${text}\"`)\n  }\n}\n\n/**\n * Login and return the session cookie\n */\nexport async function login(router: any, email: string, password: string): Promise<string> {\n  let loginResponse = await router.fetch('https://remix.run/login', {\n    method: 'POST',\n    body: new URLSearchParams({ email, password }),\n    redirect: 'manual',\n  })\n\n  let sessionId = getSessionCookie(loginResponse)\n  if (!sessionId) {\n    throw new Error('Failed to get session cookie from login response')\n  }\n\n  return sessionId\n}\n\n/**\n * Login as admin and return the session cookie\n */\nexport function loginAsAdmin(router: any): Promise<string> {\n  return login(router, 'admin@bookstore.com', 'admin123')\n}\n\n/**\n * Login as customer and return the session cookie\n */\nexport function loginAsCustomer(router: any): Promise<string> {\n  return login(router, 'customer@example.com', 'password123')\n}\n"
  },
  {
    "path": "demos/bookstore/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\", \"DOM.AsyncIterable\"],\n    \"types\": [\"node\", \"dom-navigation\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"remix/component\",\n    \"paths\": {\n      \"*\": [\"./*\"],\n      \"remix/component/jsx-runtime\": [\"../../packages/remix/src/component/jsx-runtime.ts\"],\n      \"remix/component/jsx-dev-runtime\": [\"../../packages/remix/src/component/jsx-dev-runtime.ts\"]\n    }\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "demos/frame-navigation/.gitignore",
    "content": "public/assets/\n"
  },
  {
    "path": "demos/frame-navigation/app/assets/dashboard-stat-grid.tsx",
    "content": "import { clientEntry, css, link, type Handle } from 'remix/component'\n\ntype StatCard = {\n  label: string\n  value: string\n  href: string\n}\n\ntype DashboardStatGridProps = {\n  cards: StatCard[]\n}\n\nexport let DashboardStatGrid = clientEntry(\n  '/assets/dashboard-stat-grid.js#DashboardStatGrid',\n  function DashboardStatGrid(_handle: Handle) {\n    return ({ cards }: DashboardStatGridProps) => (\n      <div mix={statsGridStyle}>\n        {cards.map((card) => (\n          <article mix={[statCardStyle, link(card.href)]}>\n            <p mix={statLabelStyle}>{card.label}</p>\n            <p mix={statValueStyle}>{card.value}</p>\n          </article>\n        ))}\n      </div>\n    )\n  },\n)\n\nlet statsGridStyle = css({\n  marginTop: '1.25rem',\n  display: 'grid',\n  gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',\n  gap: '0.75rem',\n})\n\nlet statCardStyle = css({\n  border: '1px solid #e2e8f0',\n  borderRadius: '12px',\n  padding: '1rem',\n  backgroundColor: '#ffffff',\n  cursor: 'pointer',\n  transition: 'border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease',\n  '&:hover': {\n    borderColor: '#94a3b8',\n    transform: 'translateY(-1px)',\n  },\n  '&:focus-visible': {\n    outline: '2px solid #2563eb',\n    outlineOffset: '2px',\n    borderColor: '#2563eb',\n  },\n})\n\nlet statLabelStyle = css({\n  margin: 0,\n  fontSize: '0.875rem',\n  color: '#64748b',\n})\n\nlet statValueStyle = css({\n  margin: '0.3rem 0 0',\n  fontSize: '1.5rem',\n  fontWeight: 700,\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/assets/entry.tsx",
    "content": "import type { FrameContent, RemixNode } from 'remix/component'\nimport { animateEntrance, createRoot, css, on, run, spring } from 'remix/component'\n\nimport { routes } from '../../config/routes.ts'\n\nlet app = run({\n  async loadModule(moduleUrl, exportName) {\n    let mod = await import(moduleUrl)\n    let exp = (mod as any)[exportName]\n    if (typeof exp !== 'function') {\n      throw new Error(`Export \"${exportName}\" from \"${moduleUrl}\" is not a function`)\n    }\n    return exp\n  },\n  async resolveFrame(src, signal, target) {\n    return resolveFrameResponse(new URL(src, window.location.href), signal, target)\n  },\n})\n\nasync function resolveFrameResponse(\n  url: URL,\n  signal?: AbortSignal,\n  target?: string,\n): Promise<FrameContent> {\n  let headers = new Headers()\n  headers.set('accept', 'text/html')\n  headers.set('x-remix-frame', 'true')\n\n  if (target) {\n    headers.set('x-remix-target', target)\n  }\n\n  let res = await fetch(url, { headers, signal })\n\n  if (res.status === 401) {\n    window.location.assign(routes.auth.login.index.href())\n    return new Promise(() => {})\n  }\n\n  if (!res.ok) {\n    return (\n      <ErrorCard\n        eyebrow=\"Unexpected Error\"\n        title=\"Reload required\"\n        message=\"An unexpected error occurred. Please reload the page to try again.\"\n        action={\n          <a rmx-document href={window.location.href} mix={actionLinkCss}>\n            Reload\n          </a>\n        }\n      />\n    )\n  }\n\n  if (res.body) return res.body\n  return await res.text()\n}\n\napp.addEventListener('error', async (event) => {\n  app.dispose()\n  await fadeOutBody()\n\n  let message = 'message' in event.error ? event.error.message : 'Unknown error'\n  createRoot(document.body).render(\n    <div mix={pageCss}>\n      <ErrorCard\n        eyebrow=\"Unexpected Error\"\n        title=\"Something went wrong\"\n        message={message}\n        animated\n        action={\n          <button\n            mix={[\n              reloadButtonCss,\n              on('click', () => {\n                window.location.reload()\n              }),\n            ]}\n          >\n            Reload the page\n          </button>\n        }\n      />\n    </div>,\n  )\n})\n\nasync function fadeOutBody() {\n  let animation = document.body.animate(\n    [\n      { opacity: 1, transform: 'translateY(0) scale(1)' },\n      { opacity: 0, transform: 'translateY(10px) scale(0.985)' },\n    ],\n    { ...spring('snappy') },\n  )\n\n  await animation.finished\n  document.body.innerHTML = ''\n}\n\ntype ErrorCardProps = {\n  eyebrow: string\n  title: string\n  message: string\n  action?: RemixNode\n  animated?: boolean\n}\n\nfunction ErrorCard() {\n  return ({ eyebrow, title, message, action, animated }: ErrorCardProps) => (\n    <div mix={animated ? [cardCss, animateGentlyIn] : cardCss}>\n      <p mix={eyebrowCss}>{eyebrow}</p>\n      <h1 mix={titleCss}>{title}</h1>\n      <p mix={messageCss}>{message}</p>\n      {action}\n    </div>\n  )\n}\n\nlet pageCss = css({\n  minHeight: '100vh',\n  display: 'grid',\n  placeItems: 'center',\n  padding: '32px',\n  background: '#f8fafc',\n  color: '#0f172a',\n  fontFamily: 'ui-sans-serif, system-ui, sans-serif',\n})\n\nlet cardCss = css({\n  width: '100%',\n  maxWidth: '560px',\n  padding: '40px 36px',\n  background: '#ffffff',\n  border: '1px solid #e2e8f0',\n  borderRadius: '20px',\n  boxShadow: '0 20px 45px rgba(15, 23, 42, 0.08)',\n})\n\nlet animateGentlyIn = animateEntrance({\n  opacity: 0,\n  transform: 'translateY(-14px) scale(0.97)',\n  ...spring('smooth'),\n})\n\nlet eyebrowCss = css({\n  margin: '0 0 12px',\n  fontSize: '12px',\n  fontWeight: '600',\n  letterSpacing: '0.08em',\n  textTransform: 'uppercase',\n  color: '#64748b',\n})\n\nlet titleCss = css({\n  margin: '0 0 12px',\n  fontSize: '32px',\n  lineHeight: '1.1',\n  fontWeight: '700',\n})\n\nlet messageCss = css({\n  margin: '0',\n  fontSize: '16px',\n  lineHeight: '1.6',\n  color: '#475569',\n})\n\nlet reloadButtonCss = css({\n  marginTop: '24px',\n  padding: '12px 18px',\n  border: 'none',\n  borderRadius: '999px',\n  background: '#0f172a',\n  color: '#ffffff',\n  fontSize: '14px',\n  fontWeight: '600',\n  lineHeight: '1',\n  cursor: 'pointer',\n  '&:hover': {\n    background: '#1e293b',\n  },\n})\n\nlet actionLinkCss = css({\n  display: 'inline-flex',\n  marginTop: '24px',\n  padding: '12px 18px',\n  borderRadius: '999px',\n  background: '#0f172a',\n  color: '#ffffff',\n  fontSize: '14px',\n  fontWeight: '600',\n  lineHeight: '1',\n  textDecoration: 'none',\n  '&:hover': {\n    background: '#1e293b',\n  },\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/assets/fake.tsx",
    "content": "// don't know why we need this, but esbuild won't build without, just waiting\n// for the JS asset handler\nconsole.log('lol')\n"
  },
  {
    "path": "demos/frame-navigation/app/auth/controller.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport { css } from 'remix/component'\nimport { redirect } from 'remix/response/redirect'\n\nimport { routes } from '../../config/routes.ts'\nimport { render } from '../../config/render.tsx'\nimport { authCookie, isAuthenticated } from './session.ts'\n\nexport default {\n  actions: {\n    login: {\n      actions: {\n        async index() {\n          if (await isAuthenticated()) {\n            return redirect(routes.main.index.href())\n          }\n\n          return render(\n            <html lang=\"en\">\n              <head>\n                <meta charSet=\"utf-8\" />\n                <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n                <title>Sign in | LMS</title>\n                <script async type=\"module\" src=\"/assets/entry.js\" />\n              </head>\n              <body mix={loginBodyStyle}>\n                <main mix={loginShellStyle}>\n                  <section mix={cardStyle}>\n                    <p mix={eyebrowStyle}>Demo auth</p>\n                    <h2 mix={titleStyle}>Fake login page</h2>\n                    <p mix={bodyStyle}>\n                      This route renders outside the app shell so it is obvious the protected frame\n                      navigation fell back to a top-level login page.\n                    </p>\n\n                    <form method=\"POST\" action={routes.auth.login.action.href()} mix={formStyle}>\n                      <button type=\"submit\" mix={primaryButtonStyle}>\n                        Set auth cookie\n                      </button>\n                    </form>\n                  </section>\n                </main>\n              </body>\n            </html>,\n          )\n        },\n        async action() {\n          return redirect(routes.main.index.href(), {\n            headers: {\n              'Set-Cookie': await authCookie.serialize('1'),\n            },\n          })\n        },\n      },\n    },\n    async logout() {\n      return redirect(routes.auth.login.index.href(), {\n        headers: {\n          'Set-Cookie': await authCookie.serialize('', { maxAge: 0 }),\n        },\n      })\n    },\n  },\n} satisfies Controller<typeof routes.auth>\n\nlet cardStyle = css({\n  maxWidth: '38rem',\n  border: '1px solid #e2e8f0',\n  borderRadius: '18px',\n  backgroundColor: '#ffffff',\n  padding: '1.5rem',\n  boxShadow: '0 12px 30px rgba(15, 23, 42, 0.06)',\n})\n\nlet loginBodyStyle = css({\n  margin: 0,\n  minHeight: '100vh',\n  fontFamily:\n    'Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji',\n  color: '#0f172a',\n  background: 'radial-gradient(circle at top, rgba(99, 102, 241, 0.16), transparent 28%), #f8fafc',\n})\n\nlet loginShellStyle = css({\n  minHeight: '100vh',\n  display: 'grid',\n  placeItems: 'center',\n  padding: '2rem',\n})\n\nlet eyebrowStyle = css({\n  margin: 0,\n  fontSize: '0.8rem',\n  fontWeight: 700,\n  letterSpacing: '0.08em',\n  textTransform: 'uppercase',\n  color: '#6366f1',\n})\n\nlet titleStyle = css({\n  marginTop: '0.6rem',\n  marginBottom: '0.75rem',\n  fontSize: '1.7rem',\n  color: '#0f172a',\n})\n\nlet bodyStyle = css({\n  margin: 0,\n  color: '#475569',\n  lineHeight: 1.7,\n})\n\nlet formStyle = css({\n  marginTop: '1.5rem',\n})\n\nlet primaryButtonStyle = css({\n  border: 'none',\n  borderRadius: '999px',\n  padding: '0.8rem 1.1rem',\n  fontSize: '0.95rem',\n  fontWeight: 600,\n  cursor: 'pointer',\n  backgroundColor: '#0f172a',\n  color: '#ffffff',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/auth/session.ts",
    "content": "import { createCookie } from 'remix/cookie'\nimport { getContext } from 'remix/async-context-middleware'\n\nexport let authCookie = createCookie('frame-navigation-auth', {\n  httpOnly: true,\n  sameSite: 'Lax',\n  path: '/',\n})\n\nexport async function hasAuthCookie(cookieHeader: string | null) {\n  let cookie = await authCookie.parse(cookieHeader)\n  return cookie === '1'\n}\n\nexport async function isAuthenticated() {\n  return hasAuthCookie(getContext().request.headers.get('cookie'))\n}\n"
  },
  {
    "path": "demos/frame-navigation/app/lib/Layout.tsx",
    "content": "import type { RemixNode } from 'remix/component'\nimport { css } from 'remix/component'\n\nimport { routes } from '../../config/routes.ts'\nimport { NavLink } from './NavLink.tsx'\n\ntype MainNavItem = 'dashboard' | 'courses' | 'calendar' | 'account' | 'settings'\n\ntype LayoutProps = {\n  title: string\n  activeNav?: MainNavItem\n  children?: RemixNode\n}\n\nlet navItems = [\n  { id: 'dashboard', label: 'Dashboard', route: routes.main.index },\n  { id: 'courses', label: 'Courses', route: routes.main.courses },\n  { id: 'calendar', label: 'Calendar', route: routes.main.calendar },\n  { id: 'account', label: 'Account', route: routes.main.account },\n  { id: 'settings', label: 'Settings', route: routes.settings.index },\n]\n\nexport function Layout() {\n  return ({ title, activeNav, children }: LayoutProps) => (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <title>{title} | LMS</title>\n        <script async type=\"module\" src=\"/assets/entry.js\" />\n      </head>\n      <body mix={bodyStyle}>\n        <div mix={appShellStyle}>\n          <aside mix={sidebarStyle}>\n            <a href={routes.main.index.href()} mix={brandLinkStyle}>\n              Atlas LMS\n            </a>\n            <p mix={sidebarSubtitleStyle}>Student workspace</p>\n            <nav mix={navStyle}>\n              {navItems.map((item) => (\n                <NavLink route={item.route} active={activeNav === item.id}>\n                  {item.label}\n                </NavLink>\n              ))}\n            </nav>\n            <form method=\"POST\" action={routes.auth.logout.href()} mix={logoutFormStyle}>\n              <button type=\"submit\" mix={logoutButtonStyle}>\n                Logout\n              </button>\n            </form>\n          </aside>\n\n          <main mix={mainStyle}>\n            <header mix={mainHeaderStyle}>\n              <h1 mix={mainTitleStyle}>{title}</h1>\n            </header>\n            {children}\n          </main>\n        </div>\n      </body>\n    </html>\n  )\n}\n\nlet bodyStyle = css({\n  margin: 0,\n  minHeight: '100vh',\n  fontFamily:\n    'Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji',\n  color: '#0f172a',\n  backgroundColor: '#f8fafc',\n})\n\nlet appShellStyle = css({\n  height: '100vh',\n  display: 'grid',\n  gridTemplateColumns: '280px minmax(0, 1fr)',\n})\n\nlet sidebarStyle = css({\n  position: 'sticky',\n  top: 0,\n  alignSelf: 'start',\n  height: '100vh',\n  boxSizing: 'border-box',\n  borderRight: '1px solid #e2e8f0',\n  backgroundColor: '#ffffff',\n  padding: '1.25rem 1rem',\n  display: 'flex',\n  flexDirection: 'column',\n  gap: '0.75rem',\n})\n\nlet brandLinkStyle = css({\n  fontWeight: 700,\n  letterSpacing: '-0.01em',\n  color: '#0f172a',\n  textDecoration: 'none',\n  fontSize: '1.1rem',\n})\n\nlet sidebarSubtitleStyle = css({\n  margin: 0,\n  color: '#64748b',\n  fontSize: '0.9rem',\n})\n\nlet navStyle = css({\n  display: 'grid',\n  gap: '0.35rem',\n  marginTop: '0.5rem',\n  '& a': {\n    textDecoration: 'none',\n    borderRadius: '10px',\n    padding: '0.55rem 0.75rem',\n    fontSize: '0.95rem',\n    color: '#334155',\n    backgroundColor: 'transparent',\n    fontWeight: 500,\n  },\n  '& a[aria-current=\"page\"]': {\n    color: '#0f172a',\n    backgroundColor: '#e2e8f0',\n    fontWeight: 600,\n  },\n})\n\nlet logoutFormStyle = css({\n  marginTop: 'auto',\n  paddingTop: '1rem',\n})\n\nlet logoutButtonStyle = css({\n  width: '100%',\n  border: 'none',\n  borderRadius: '10px',\n  padding: '0.65rem 0.75rem',\n  fontSize: '0.9rem',\n  fontWeight: 600,\n  cursor: 'pointer',\n  backgroundColor: '#e2e8f0',\n  color: '#0f172a',\n})\n\nlet mainStyle = css({\n  padding: '1.5rem 2rem 3rem',\n})\n\nlet mainHeaderStyle = css({\n  marginBottom: '1.25rem',\n  paddingBottom: '0.75rem',\n  borderBottom: '1px solid #e2e8f0',\n})\n\nlet mainTitleStyle = css({\n  margin: 0,\n  fontSize: '1.25rem',\n  color: '#0f172a',\n})\n\nexport type { MainNavItem }\n"
  },
  {
    "path": "demos/frame-navigation/app/lib/NavLink.tsx",
    "content": "import type { RemixNode } from 'remix/component'\nimport type { Route } from 'remix/fetch-router/routes'\n\ntype NavLinkProps = {\n  route: Route<any, string>\n  active?: boolean\n  target?: string\n  frameSrc?: string\n  children?: RemixNode\n}\n\nexport function NavLink() {\n  return ({ route, active, target: frameTarget, frameSrc, children }: NavLinkProps) => {\n    let href = route.href()\n\n    return (\n      <a\n        href={href}\n        aria-current={active ? 'page' : undefined}\n        rmx-target={frameTarget}\n        rmx-src={frameSrc}\n      >\n        {children}\n      </a>\n    )\n  }\n}\n"
  },
  {
    "path": "demos/frame-navigation/app/main/account.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function MainAccountPage() {\n  return () => (\n    <section>\n      <h1 mix={titleStyle}>Account</h1>\n      <p mix={descriptionStyle}>\n        Manage your profile, enrollment details, and contact preferences.\n      </p>\n\n      <dl mix={detailsGridStyle}>\n        <dt mix={termStyle}>Name</dt>\n        <dd mix={definitionStyle}>Riley Student</dd>\n        <dt mix={termStyle}>Program</dt>\n        <dd mix={definitionStyle}>Human Computer Interaction</dd>\n        <dt mix={termStyle}>Expected graduation</dt>\n        <dd mix={definitionStyle}>May 2027</dd>\n      </dl>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  marginTop: 0,\n  fontSize: '1.8rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.5rem',\n  color: '#475569',\n  lineHeight: 1.7,\n  maxWidth: '65ch',\n})\n\nlet detailsGridStyle = css({\n  marginTop: '1.25rem',\n  border: '1px solid #e2e8f0',\n  borderRadius: '14px',\n  padding: '1rem',\n  display: 'grid',\n  gridTemplateColumns: '150px 1fr',\n  rowGap: '0.6rem',\n})\n\nlet termStyle = css({\n  color: '#64748b',\n})\n\nlet definitionStyle = css({\n  margin: 0,\n  color: '#0f172a',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/main/calendar.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function MainCalendarPage() {\n  return () => (\n    <section>\n      <h1 mix={titleStyle}>Calendar</h1>\n      <p mix={descriptionStyle}>Track class sessions, assignment due dates, and office hours.</p>\n\n      <div mix={cardStyle}>\n        <p mix={cardTitleStyle}>Today</p>\n        <p mix={eventFirstStyle}>11:00 AM - UX Research Workshop</p>\n        <p mix={eventNextStyle}>3:30 PM - Intro to Accessibility Quiz</p>\n      </div>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  marginTop: 0,\n  fontSize: '1.8rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.5rem',\n  color: '#475569',\n  lineHeight: 1.7,\n  maxWidth: '65ch',\n})\n\nlet cardStyle = css({\n  marginTop: '1.25rem',\n  border: '1px solid #e2e8f0',\n  borderRadius: '14px',\n  padding: '1rem',\n  backgroundColor: '#ffffff',\n})\n\nlet cardTitleStyle = css({\n  margin: 0,\n  fontWeight: 600,\n  color: '#0f172a',\n})\n\nlet eventFirstStyle = css({\n  margin: '0.5rem 0 0',\n  color: '#334155',\n})\n\nlet eventNextStyle = css({\n  margin: '0.35rem 0 0',\n  color: '#334155',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/main/controller.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport type { RemixNode } from 'remix/component'\n\nimport type { routes } from '../../config/routes.ts'\nimport { render } from '../../config/render.tsx'\nimport { type MainNavItem, Layout } from '../lib/Layout.tsx'\nimport { MainAccountPage } from './account.tsx'\nimport { MainCalendarPage } from './calendar.tsx'\nimport { MainCoursesPage } from './courses.tsx'\nimport { MainIndexPage } from './index.tsx'\n\nfunction renderMainPage(title: string, activeNav: MainNavItem, content: RemixNode) {\n  return render(\n    <Layout title={title} activeNav={activeNav}>\n      {content}\n    </Layout>,\n  )\n}\n\nlet mainController: Controller<typeof routes.main> = {\n  actions: {\n    index() {\n      return renderMainPage('Dashboard', 'dashboard', <MainIndexPage />)\n    },\n    courses() {\n      return renderMainPage('Courses', 'courses', <MainCoursesPage />)\n    },\n    calendar() {\n      return renderMainPage('Calendar', 'calendar', <MainCalendarPage />)\n    },\n    account() {\n      return renderMainPage('Account', 'account', <MainAccountPage />)\n    },\n  },\n}\n\nexport default mainController\n"
  },
  {
    "path": "demos/frame-navigation/app/main/courses.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function MainCoursesPage() {\n  return () => (\n    <section>\n      <h1 mix={titleStyle}>Courses</h1>\n      <p mix={descriptionStyle}>Continue where you left off in each course track.</p>\n\n      <ul mix={courseListStyle}>\n        <li mix={courseItemStyle}>Introduction to Product Design</li>\n        <li mix={courseItemStyle}>Applied Statistics for Engineers</li>\n        <li mix={courseItemStyle}>Web Accessibility Foundations</li>\n      </ul>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  marginTop: 0,\n  fontSize: '1.8rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.5rem',\n  color: '#475569',\n  lineHeight: 1.7,\n  maxWidth: '65ch',\n})\n\nlet courseListStyle = css({\n  listStyle: 'none',\n  margin: '1.25rem 0 0',\n  padding: 0,\n  display: 'grid',\n  gap: '0.75rem',\n})\n\nlet courseItemStyle = css({\n  border: '1px solid #e2e8f0',\n  borderRadius: '12px',\n  padding: '0.9rem 1rem',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/main/index.tsx",
    "content": "import { css } from 'remix/component'\n\nimport { DashboardStatGrid } from '../assets/dashboard-stat-grid.tsx'\nimport { routes } from '../../config/routes.ts'\n\nlet statCards = [\n  {\n    label: 'In progress',\n    value: '4 courses',\n    href: routes.main.courses.href(),\n  },\n  {\n    label: 'Due this week',\n    value: '7 tasks',\n    href: routes.main.calendar.href(),\n  },\n  {\n    label: 'Average grade',\n    value: '92%',\n    href: routes.main.account.href(),\n  },\n]\n\nexport function MainIndexPage() {\n  return () => (\n    <section>\n      <h1 mix={titleStyle}>Learning dashboard</h1>\n      <p mix={descriptionStyle}>\n        Keep up with coursework, deadlines, and instructor updates from one central place.\n      </p>\n\n      <DashboardStatGrid cards={statCards} />\n\n      <p mix={hintStyle}>Try tabbing to a card, pressing Enter, or Cmd/Ctrl-clicking one.</p>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  marginTop: 0,\n  fontSize: '1.8rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.5rem',\n  color: '#475569',\n  lineHeight: 1.7,\n  maxWidth: '65ch',\n})\n\nlet hintStyle = css({\n  marginTop: '0.85rem',\n  color: '#64748b',\n  fontSize: '0.9rem',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/settings/controller.tsx",
    "content": "import type { Controller } from 'remix/fetch-router'\nimport type { RemixNode } from 'remix/component'\nimport { Frame } from 'remix/component'\nimport { getContext } from 'remix/async-context-middleware'\n\nimport { frames, type routes } from '../../config/routes.ts'\nimport { render } from '../../config/render.tsx'\nimport { Layout } from '../lib/Layout.tsx'\nimport { type SettingsNavItem, SettingsLayout } from './layout.tsx'\n\nimport { Grading } from './grading.tsx'\nimport { Index } from './index.tsx'\nimport { Integrations } from './integrations.tsx'\nimport { Notifications } from './notifications.tsx'\nimport { Privacy } from './privacy.tsx'\nimport { Profile } from './profile.tsx'\n\nexport default {\n  actions: {\n    index() {\n      return renderSettingsPage('overview', <Index />)\n    },\n    profile() {\n      return renderSettingsPage('profile', <Profile />)\n    },\n    notifications() {\n      return renderSettingsPage('notifications', <Notifications />)\n    },\n    privacy() {\n      return renderSettingsPage('privacy', <Privacy />, { status: 500 })\n    },\n    grading() {\n      return renderSettingsPage('grading', <Grading />)\n    },\n    integrations() {\n      return renderSettingsPage('integrations', <Integrations />)\n    },\n  },\n} satisfies Controller<typeof routes.settings>\n\ntype SettingsPageProps = {\n  activeItem: SettingsNavItem\n  children?: RemixNode\n}\n\nfunction renderSettingsPage(activeItem: SettingsNavItem, content: RemixNode, init?: ResponseInit) {\n  return render(\n    <SettingsShellOrFragment activeItem={activeItem}>{content}</SettingsShellOrFragment>,\n    init,\n  )\n}\n\nfunction SettingsShellOrFragment() {\n  return ({ activeItem, children }: SettingsPageProps) => {\n    if (isFrameRequest()) {\n      return <SettingsLayout activeItem={activeItem}>{children}</SettingsLayout>\n    }\n\n    return (\n      <Layout title=\"Settings\" activeNav=\"settings\">\n        <Frame name={frames.settings} src={getContext().request.url} />\n      </Layout>\n    )\n  }\n}\n\nfunction isFrameRequest() {\n  return getContext().request.headers.get('x-remix-target') === frames.settings\n}\n"
  },
  {
    "path": "demos/frame-navigation/app/settings/grading.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function Grading() {\n  return () => (\n    <section>\n      <h2 mix={titleStyle}>Grading</h2>\n      <p mix={descriptionStyle}>\n        Set grading display preferences and default rubric visibility for your courses.\n      </p>\n      <div mix={cardStyle}>\n        <p mix={settingStyle}>\n          <span>Default grade format</span>\n          <strong>Percentage + Letter</strong>\n        </p>\n        <p mix={settingStyle}>\n          <span>Show running course average</span>\n          <strong>Enabled</strong>\n        </p>\n        <p mix={settingStyle}>\n          <span>Rubric criteria expanded by default</span>\n          <strong>Enabled</strong>\n        </p>\n      </div>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  margin: 0,\n  fontSize: '1.5rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.6rem',\n  color: '#475569',\n  lineHeight: 1.7,\n})\n\nlet cardStyle = css({\n  marginTop: '1rem',\n  border: '1px solid #e2e8f0',\n  borderRadius: '14px',\n  backgroundColor: '#ffffff',\n  padding: '0.85rem 1rem',\n  display: 'grid',\n  gap: '0.7rem',\n})\n\nlet settingStyle = css({\n  margin: 0,\n  display: 'flex',\n  justifyContent: 'space-between',\n  alignItems: 'center',\n  color: '#0f172a',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/settings/index.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function Index() {\n  return () => (\n    <section>\n      <h2 mix={titleStyle}>Settings overview</h2>\n      <p mix={descriptionStyle}>\n        Configure your LMS experience, from profile details to grading visibility and connected\n        learning tools.\n      </p>\n      <div mix={cardStyle}>\n        <p mix={cardTitleStyle}>Recommended setup</p>\n        <p mix={cardBodyStyle}>\n          Complete profile, enable deadline reminders, and review privacy controls at least once\n          this semester.\n        </p>\n      </div>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  margin: 0,\n  fontSize: '1.5rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.6rem',\n  color: '#475569',\n  lineHeight: 1.7,\n  maxWidth: '70ch',\n})\n\nlet cardStyle = css({\n  marginTop: '1rem',\n  border: '1px solid #e2e8f0',\n  borderRadius: '14px',\n  backgroundColor: '#ffffff',\n  padding: '1rem',\n})\n\nlet cardTitleStyle = css({\n  margin: 0,\n  fontWeight: 600,\n  color: '#0f172a',\n})\n\nlet cardBodyStyle = css({\n  margin: '0.5rem 0 0',\n  color: '#64748b',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/settings/integrations.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function Integrations() {\n  return () => (\n    <section>\n      <h2 mix={titleStyle}>Integrations</h2>\n      <p mix={descriptionStyle}>\n        Manage connected tools used for video conferencing, cloud storage, and assignment syncing.\n      </p>\n      <ul mix={integrationsStyle}>\n        <li mix={integrationRowStyle}>\n          <span>Zoom</span>\n          <strong>Connected</strong>\n        </li>\n        <li mix={integrationRowStyle}>\n          <span>Google Drive</span>\n          <strong>Connected</strong>\n        </li>\n        <li mix={integrationRowStyle}>\n          <span>Notion</span>\n          <strong>Not Connected</strong>\n        </li>\n      </ul>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  margin: 0,\n  fontSize: '1.5rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.6rem',\n  color: '#475569',\n  lineHeight: 1.7,\n})\n\nlet integrationsStyle = css({\n  listStyle: 'none',\n  margin: '1rem 0 0',\n  padding: 0,\n  display: 'grid',\n  gap: '0.55rem',\n})\n\nlet integrationRowStyle = css({\n  border: '1px solid #e2e8f0',\n  borderRadius: '12px',\n  backgroundColor: '#ffffff',\n  padding: '0.85rem 1rem',\n  display: 'flex',\n  justifyContent: 'space-between',\n  alignItems: 'center',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/settings/layout.tsx",
    "content": "import type { RemixNode } from 'remix/component'\nimport { css } from 'remix/component'\n\nimport { frames, routes } from '../../config/routes.ts'\nimport { NavLink } from '../lib/NavLink.tsx'\n\ntype SettingsNavItem =\n  | 'overview'\n  | 'profile'\n  | 'notifications'\n  | 'privacy'\n  | 'grading'\n  | 'integrations'\n\ntype SettingsLayoutProps = {\n  activeItem?: SettingsNavItem\n  children?: RemixNode\n}\n\nlet settingsItems = [\n  {\n    id: 'overview',\n    route: routes.settings.index,\n    label: 'Overview',\n  },\n  {\n    id: 'profile',\n    route: routes.settings.profile,\n    label: 'Profile',\n  },\n  {\n    id: 'notifications',\n    route: routes.settings.notifications,\n    label: 'Notifications',\n  },\n  {\n    id: 'privacy',\n    route: routes.settings.privacy,\n    label: 'Privacy',\n  },\n  {\n    id: 'grading',\n    route: routes.settings.grading,\n    label: 'Grading',\n  },\n  {\n    id: 'integrations',\n    route: routes.settings.integrations,\n    label: 'Integrations',\n  },\n]\n\nexport function SettingsLayout() {\n  return ({ activeItem, children }: SettingsLayoutProps) => (\n    <section mix={contentShellStyle}>\n      <aside mix={secondarySidebarStyle}>\n        <p mix={secondarySidebarTitleStyle}>Settings</p>\n        <nav mix={secondaryNavStyle}>\n          {settingsItems.map((item) => (\n            <NavLink route={item.route} target={frames.settings} active={activeItem === item.id}>\n              {item.label}\n            </NavLink>\n          ))}\n        </nav>\n      </aside>\n      <section mix={secondaryContentStyle}>{children}</section>\n    </section>\n  )\n}\n\nlet contentShellStyle = css({\n  display: 'grid',\n  gridTemplateColumns: '240px minmax(0, 1fr)',\n  gap: '1.25rem',\n  alignItems: 'start',\n})\n\nlet secondarySidebarStyle = css({\n  position: 'sticky',\n  top: '1.5rem',\n  border: '1px solid #e2e8f0',\n  borderRadius: '14px',\n  backgroundColor: '#ffffff',\n  padding: '0.9rem',\n})\n\nlet secondarySidebarTitleStyle = css({\n  margin: 0,\n  color: '#64748b',\n  fontSize: '0.85rem',\n  textTransform: 'uppercase',\n  letterSpacing: '0.04em',\n  fontWeight: 600,\n})\n\nlet secondaryNavStyle = css({\n  marginTop: '0.7rem',\n  display: 'grid',\n  gap: '0.35rem',\n  '& a': {\n    textDecoration: 'none',\n    borderRadius: '10px',\n    padding: '0.55rem 0.65rem',\n    fontSize: '0.9rem',\n    color: '#334155',\n    backgroundColor: 'transparent',\n    fontWeight: 500,\n  },\n  '& a[aria-current=\"page\"]': {\n    color: '#0f172a',\n    backgroundColor: '#e2e8f0',\n    fontWeight: 600,\n  },\n})\n\nlet secondaryContentStyle = css({\n  minWidth: 0,\n})\n\nexport type { SettingsNavItem }\n"
  },
  {
    "path": "demos/frame-navigation/app/settings/notifications.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function Notifications() {\n  return () => (\n    <section>\n      <h2 mix={titleStyle}>Notifications</h2>\n      <p mix={descriptionStyle}>\n        Choose how and when you are notified about assignment deadlines, grade releases, and course\n        announcements.\n      </p>\n      <ul mix={listStyle}>\n        <li mix={rowStyle}>\n          <span>Assignment due reminders</span>\n          <strong>Enabled</strong>\n        </li>\n        <li mix={rowStyle}>\n          <span>Weekly progress summary</span>\n          <strong>Enabled</strong>\n        </li>\n        <li mix={rowStyle}>\n          <span>Push notifications</span>\n          <strong>Muted after 8PM</strong>\n        </li>\n      </ul>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  margin: 0,\n  fontSize: '1.5rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.6rem',\n  color: '#475569',\n  lineHeight: 1.7,\n  maxWidth: '70ch',\n})\n\nlet listStyle = css({\n  listStyle: 'none',\n  margin: '1rem 0 0',\n  padding: 0,\n  display: 'grid',\n  gap: '0.55rem',\n})\n\nlet rowStyle = css({\n  border: '1px solid #e2e8f0',\n  borderRadius: '12px',\n  backgroundColor: '#ffffff',\n  padding: '0.85rem 1rem',\n  display: 'flex',\n  justifyContent: 'space-between',\n  alignItems: 'center',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/settings/privacy.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function Privacy() {\n  return () => (\n    <section>\n      <h2 mix={titleStyle}>Privacy</h2>\n      <p mix={descriptionStyle}>\n        Control what activity and profile details are visible to classmates and collaborators.\n      </p>\n      <div mix={cardStyle}>\n        <p mix={rowStyle}>\n          <span>Show course progress to classmates</span>\n          <strong>Off</strong>\n        </p>\n        <p mix={rowStyle}>\n          <span>Allow direct messages from peers</span>\n          <strong>On</strong>\n        </p>\n        <p mix={rowStyle}>\n          <span>Display email in group projects</span>\n          <strong>Off</strong>\n        </p>\n      </div>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  margin: 0,\n  fontSize: '1.5rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.6rem',\n  color: '#475569',\n  lineHeight: 1.7,\n})\n\nlet cardStyle = css({\n  marginTop: '1rem',\n  border: '1px solid #e2e8f0',\n  borderRadius: '14px',\n  backgroundColor: '#ffffff',\n  padding: '0.85rem 1rem',\n  display: 'grid',\n  gap: '0.7rem',\n})\n\nlet rowStyle = css({\n  margin: 0,\n  display: 'flex',\n  justifyContent: 'space-between',\n  alignItems: 'center',\n  color: '#0f172a',\n})\n"
  },
  {
    "path": "demos/frame-navigation/app/settings/profile.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function Profile() {\n  return () => (\n    <section>\n      <h2 mix={titleStyle}>Profile</h2>\n      <p mix={descriptionStyle}>Update personal details shown to instructors and classmates.</p>\n      <dl mix={detailsStyle}>\n        <dt mix={termStyle}>Display name</dt>\n        <dd mix={valueStyle}>Riley Student</dd>\n        <dt mix={termStyle}>Timezone</dt>\n        <dd mix={valueStyle}>America/Los_Angeles</dd>\n      </dl>\n    </section>\n  )\n}\n\nlet titleStyle = css({\n  margin: 0,\n  fontSize: '1.5rem',\n  color: '#0f172a',\n})\n\nlet descriptionStyle = css({\n  marginTop: '0.6rem',\n  color: '#475569',\n})\n\nlet detailsStyle = css({\n  marginTop: '1rem',\n  border: '1px solid #e2e8f0',\n  borderRadius: '14px',\n  backgroundColor: '#ffffff',\n  padding: '1rem',\n  display: 'grid',\n  gridTemplateColumns: '180px 1fr',\n  rowGap: '0.6rem',\n})\n\nlet termStyle = css({\n  color: '#64748b',\n})\n\nlet valueStyle = css({\n  margin: 0,\n  color: '#0f172a',\n})\n"
  },
  {
    "path": "demos/frame-navigation/config/render.tsx",
    "content": "import type { RemixNode } from 'remix/component'\nimport { renderToStream, type ResolveFrameContext } from 'remix/component/server'\nimport { getContext } from 'remix/async-context-middleware'\nimport type { Router } from 'remix/fetch-router'\n\nexport function render(node: RemixNode, init?: ResponseInit) {\n  let context = getContext()\n  let request = context.request\n  let router = context.router\n\n  let stream = renderToStream(node, {\n    frameSrc: request.url,\n    resolveFrame: (src, target, context) => resolveFrame(router, request, src, target, context),\n    onError(error) {\n      console.error(error)\n    },\n  })\n\n  let headers = new Headers(init?.headers)\n  if (!headers.has('Content-Type')) {\n    headers.set('Content-Type', 'text/html; charset=UTF-8')\n  }\n\n  return new Response(stream, { ...init, headers })\n}\n\nasync function resolveFrame(\n  router: Router,\n  request: Request,\n  src: string,\n  target?: string,\n  context?: ResolveFrameContext,\n) {\n  let frameSrc = context?.currentFrameSrc ?? request.url\n  let url = new URL(src, frameSrc)\n\n  let headers = new Headers()\n  headers.set('accept', 'text/html')\n  headers.set('accept-encoding', 'identity')\n  headers.set('x-remix-frame', 'true')\n  if (target) {\n    headers.set('x-remix-target', target)\n  }\n\n  let cookie = request.headers.get('cookie')\n  if (cookie) headers.set('cookie', cookie)\n\n  let res = await followFrameRedirects(router, request, url, headers)\n  if (res.body) return res.body\n\n  if (res.ok) return res.text()\n  return `<pre>Frame error: ${res.status} ${res.statusText}</pre>`\n}\n\nasync function followFrameRedirects(router: Router, request: Request, url: URL, headers: Headers) {\n  let currentUrl = url\n  let redirectsRemaining = 10\n\n  while (true) {\n    let res = await router.fetch(\n      new Request(currentUrl, {\n        method: 'GET',\n        headers,\n        signal: request.signal,\n      }),\n    )\n\n    let location = res.headers.get('location')\n    if (!location || res.status < 300 || res.status >= 400) {\n      return res\n    }\n\n    if (redirectsRemaining-- <= 0) {\n      throw new Error('Too many frame redirects')\n    }\n\n    currentUrl = new URL(location, currentUrl)\n  }\n}\n"
  },
  {
    "path": "demos/frame-navigation/config/router.tsx",
    "content": "import { createRouter } from 'remix/fetch-router'\nimport { asyncContext } from 'remix/async-context-middleware'\nimport { logger } from 'remix/logger-middleware'\nimport { redirect } from 'remix/response/redirect'\nimport { staticFiles } from 'remix/static-middleware'\nimport type { Middleware } from 'remix/fetch-router'\n\nimport authController from '../app/auth/controller.tsx'\nimport { hasAuthCookie } from '../app/auth/session.ts'\nimport mainController from '../app/main/controller.tsx'\nimport settingsController from '../app/settings/controller.tsx'\nimport { routes } from './routes.ts'\n\nlet middleware = []\n\nlet requireAuth: Middleware = async ({ request, url }, next) => {\n  let loginPath = routes.auth.login.index.href()\n  if (url.pathname === loginPath) {\n    return next()\n  }\n\n  if (await hasAuthCookie(request.headers.get('cookie'))) {\n    return next()\n  }\n\n  let isFrameRequest = request.headers.get('x-remix-frame') === 'true'\n  if (isFrameRequest) {\n    return new Response(\n      '<div><h1>Not authorized</h1><p>Refresh the page to sign in again.</p></div>',\n      {\n        status: 401,\n        headers: {\n          'Content-Type': 'text/html; charset=UTF-8',\n        },\n      },\n    )\n  }\n\n  return redirect(loginPath)\n}\n\nif (process.env.NODE_ENV === 'development') {\n  middleware.push(logger())\n}\n\nmiddleware.push(\n  staticFiles('./public', {\n    cacheControl: 'no-store',\n    etag: false,\n    lastModified: false,\n    index: false,\n  }),\n)\nmiddleware.push(asyncContext())\nmiddleware.push(requireAuth)\n\nexport let router = createRouter({ middleware })\n\nrouter.map(routes.main, mainController)\nrouter.map(routes.auth, authController)\nrouter.map(routes.settings, settingsController)\n"
  },
  {
    "path": "demos/frame-navigation/config/routes.ts",
    "content": "import { form, get, post, route } from 'remix/fetch-router/routes'\n\nexport let frames = {\n  settings: 'settings',\n} as const\n\nexport let routes = {\n  main: route('/', {\n    index: get('/'),\n    courses: get('courses'),\n    calendar: get('calendar'),\n    account: get('account'),\n  }),\n  auth: route('auth', {\n    login: form('login'),\n    logout: post('logout'),\n  }),\n  settings: route('settings', {\n    index: get('/'),\n    profile: get('profile'),\n    notifications: get('notifications'),\n    privacy: get('privacy'),\n    grading: get('grading'),\n    integrations: get('integrations'),\n  }),\n}\n"
  },
  {
    "path": "demos/frame-navigation/config/server.ts",
    "content": "import * as http from 'node:http'\nimport { createRequestListener } from 'remix/node-fetch-server'\n\nimport { router } from './router.tsx'\n\nlet server = http.createServer(\n  createRequestListener(async (request: Request) => {\n    try {\n      return await router.fetch(request)\n    } catch (error) {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  }),\n)\n\nlet port = process.env.PORT ? parseInt(process.env.PORT, 10) : 44100\n\nserver.listen(port, () => {\n  console.log(`Frame navigation demo is running on http://localhost:${port}`)\n})\n\nlet shuttingDown = false\n\nfunction shutdown() {\n  if (shuttingDown) return\n  shuttingDown = true\n  server.close(() => {\n    process.exit(0)\n  })\n}\n\nprocess.on('SIGINT', shutdown)\nprocess.on('SIGTERM', shutdown)\n"
  },
  {
    "path": "demos/frame-navigation/package.json",
    "content": "{\n  \"name\": \"frame-navigation-demo\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"remix\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/dom-navigation\": \"^1.0.7\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"esbuild\": \"^0.25.10\",\n    \"tsx\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"dev\": \"pnpm run --filter \\\".\\\" --parallel \\\"/^dev:/\\\"\",\n    \"dev:server\": \"NODE_ENV=development tsx watch config/server.ts\",\n    \"dev:browser\": \"esbuild app/assets/*.tsx --outbase=app/assets --outdir=public/assets --bundle --format=esm --entry-names='[dir]/[name]' --chunk-names='chunks/[name]-[hash]' --sourcemap --watch\",\n    \"start\": \"tsx config/server.ts\",\n    \"typecheck\": \"tsc --noEmit\"\n  }\n}\n"
  },
  {
    "path": "demos/frame-navigation/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"remix/component\",\n    \"paths\": {\n      \"*\": [\"./*\"],\n      \"remix/component/jsx-runtime\": [\"../../packages/remix/src/component/jsx-runtime.ts\"],\n      \"remix/component/jsx-dev-runtime\": [\"../../packages/remix/src/component/jsx-dev-runtime.ts\"]\n    }\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "demos/frames/.gitignore",
    "content": "public/assets/\n\n"
  },
  {
    "path": "demos/frames/app/assets/client-frame-example.tsx",
    "content": "import { clientEntry, Frame, css, on, type Handle } from 'remix/component'\n\nexport let ClientFrameExample = clientEntry(\n  '/assets/client-frame-example.js#ClientFrameExample',\n  function ClientFrameExample(handle: Handle) {\n    let mounted = false\n\n    return () => (\n      <section\n        mix={[\n          css({\n            marginTop: 16,\n            border: '1px solid rgba(255,255,255,0.12)',\n            borderRadius: 10,\n            padding: 12,\n            background: 'rgba(255,255,255,0.03)',\n          }),\n        ]}\n      >\n        <div\n          mix={[\n            css({\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'space-between',\n              gap: 12,\n            }),\n          ]}\n        >\n          <div>\n            <div mix={[css({ fontSize: 13, color: '#b9c6ff' })]}>Client-rendered Frame</div>\n            <div mix={[css({ fontSize: 12, color: '#9aa8e8' })]}>\n              Added after hydration via a client entry component.\n            </div>\n          </div>\n          <button\n            type=\"button\"\n            mix={[\n              css({\n                padding: '6px 10px',\n                borderRadius: 10,\n                border: '1px solid rgba(255,255,255,0.18)',\n                background: 'rgba(255,255,255,0.06)',\n                color: '#e9eefc',\n                cursor: 'pointer',\n                '&:hover': { background: 'rgba(255,255,255,0.10)' },\n              }),\n              on('click', () => {\n                mounted = !mounted\n                handle.update()\n              }),\n            ]}\n          >\n            {mounted ? 'Remove Frame' : 'Mount Frame'}\n          </button>\n        </div>\n\n        {mounted ? (\n          <div mix={[css({ marginTop: 10 })]}>\n            <Frame\n              src=\"/frames/client-frame-example\"\n              fallback={<div mix={[css({ color: '#9aa8e8' })]}>Loading client frame content…</div>}\n            />\n          </div>\n        ) : null}\n      </section>\n    )\n  },\n)\n"
  },
  {
    "path": "demos/frames/app/assets/client-mounted-page-example.tsx",
    "content": "import { clientEntry, Frame, css, on, type Handle } from 'remix/component'\nimport { routes } from '../routes.ts'\n\nexport let ClientMountedPageExample = clientEntry(\n  '/assets/client-mounted-page-example.js#ClientMountedPageExample',\n  (handle: Handle) => {\n    let showFrame = false\n\n    return () => (\n      <section\n        mix={[\n          css({\n            marginTop: 16,\n            border: '1px solid rgba(255,255,255,0.12)',\n            borderRadius: 10,\n            padding: 12,\n            background: 'rgba(255,255,255,0.03)',\n          }),\n        ]}\n      >\n        <div\n          mix={[\n            css({\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'space-between',\n              gap: 12,\n            }),\n          ]}\n        >\n          <div>\n            <div mix={[css({ fontSize: 13, color: '#b9c6ff' })]}>Client-mounted frame test</div>\n            <div mix={[css({ fontSize: 12, color: '#9aa8e8' })]}>\n              Mount a frame whose server content includes a nested non-blocking frame.\n            </div>\n          </div>\n          <button\n            type=\"button\"\n            mix={[\n              css({\n                padding: '6px 10px',\n                borderRadius: 10,\n                border: '1px solid rgba(255,255,255,0.18)',\n                background: 'rgba(255,255,255,0.06)',\n                color: '#e9eefc',\n                cursor: 'pointer',\n                '&:hover': { background: 'rgba(255,255,255,0.10)' },\n              }),\n              on('click', () => {\n                showFrame = !showFrame\n                handle.update()\n              }),\n            ]}\n          >\n            {showFrame ? 'Remove Frame' : 'Mount Frame'}\n          </button>\n        </div>\n\n        {showFrame ? (\n          <div mix={[css({ marginTop: 10 })]}>\n            <Frame\n              src={routes.frames.clientMountedOuter.href()}\n              fallback={<div mix={[css({ color: '#9aa8e8' })]}>Loading outer mounted frame…</div>}\n            />\n          </div>\n        ) : null}\n      </section>\n    )\n  },\n)\n"
  },
  {
    "path": "demos/frames/app/assets/counter.tsx",
    "content": "import { clientEntry, css, on, type Handle } from 'remix/component'\n\nexport let Counter = clientEntry(\n  '/assets/counter.js#Counter',\n  function Counter(handle: Handle, setup: number) {\n    let count = setup\n\n    return (props: { label: string }) => (\n      <div mix={[css({ display: 'flex', gap: 12, alignItems: 'center' })]}>\n        <strong mix={[css({ width: 72 })]}>{props.label}</strong>\n        <button\n          type=\"button\"\n          mix={[\n            css({\n              padding: '6px 10px',\n              borderRadius: 10,\n              border: '1px solid rgba(255,255,255,0.18)',\n              background: 'rgba(255,255,255,0.06)',\n              color: '#e9eefc',\n              cursor: 'pointer',\n              '&:hover': { background: 'rgba(255,255,255,0.10)' },\n            }),\n            on('click', () => {\n              count--\n              handle.update()\n            }),\n          ]}\n        >\n          −\n        </button>\n        <span\n          mix={[css({ minWidth: 48, textAlign: 'center', fontVariantNumeric: 'tabular-nums' })]}\n        >\n          {count}\n        </span>\n        <button\n          type=\"button\"\n          mix={[\n            css({\n              padding: '6px 10px',\n              borderRadius: 10,\n              border: '1px solid rgba(255,255,255,0.18)',\n              background: 'rgba(255,255,255,0.06)',\n              color: '#e9eefc',\n              cursor: 'pointer',\n              '&:hover': { background: 'rgba(255,255,255,0.10)' },\n            }),\n            on('click', () => {\n              count++\n              handle.update()\n            }),\n          ]}\n        >\n          +\n        </button>\n      </div>\n    )\n  },\n)\n"
  },
  {
    "path": "demos/frames/app/assets/entry.tsx",
    "content": "import { run } from 'remix/component'\n\nlet app = run({\n  async loadModule(moduleUrl, exportName) {\n    let mod = await import(moduleUrl)\n    let exp = (mod as any)[exportName]\n    if (typeof exp !== 'function') {\n      throw new Error(`Export \"${exportName}\" from \"${moduleUrl}\" is not a function`)\n    }\n    return exp\n  },\n  async resolveFrame(src, signal) {\n    let res = await fetch(src, { headers: { accept: 'text/html' }, signal })\n    if (!res.ok) {\n      return `<pre>Frame error: ${res.status} ${res.statusText}</pre>`\n    }\n    if (res.body) return res.body\n    return await res.text()\n  },\n})\n\napp.ready().catch((error: unknown) => console.error(error))\n"
  },
  {
    "path": "demos/frames/app/assets/reload-scope.tsx",
    "content": "import { clientEntry, css, on, type Handle } from 'remix/component'\n\nexport let ReloadScope = clientEntry(\n  '/assets/reload-scope.js#ReloadScope',\n  function ReloadScope(handle: Handle) {\n    let framePending = false\n    let topPending = false\n\n    return () => (\n      <div mix={[css({ display: 'flex', gap: 8, flexWrap: 'wrap' })]}>\n        <button\n          type=\"button\"\n          mix={[\n            css({\n              padding: '6px 10px',\n              borderRadius: 10,\n              border: '1px solid rgba(255,255,255,0.18)',\n              background: framePending ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.06)',\n              color: '#e9eefc',\n              cursor: framePending ? 'default' : 'pointer',\n              '&:hover': { background: 'var(--frame-bg)' },\n            }),\n            on('click', async () => {\n              if (framePending || topPending) return\n              framePending = true\n              handle.update()\n              let signal = await handle.frame.reload()\n              if (signal.aborted) return\n              framePending = false\n              handle.update()\n            }),\n          ]}\n          style={{\n            '--frame-bg': framePending ? undefined : 'rgba(255,255,255,0.10)',\n          }}\n        >\n          {framePending ? 'Reloading frame…' : 'Reload this frame'}\n        </button>\n        <button\n          type=\"button\"\n          mix={[\n            css({\n              padding: '6px 10px',\n              borderRadius: 10,\n              border: '1px solid rgba(255,255,255,0.18)',\n              background: topPending ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.06)',\n              color: '#e9eefc',\n              cursor: topPending ? 'default' : 'pointer',\n              '&:hover': { background: 'var(--top-bg)' },\n            }),\n            on('click', async () => {\n              if (topPending || framePending) return\n              topPending = true\n              handle.update()\n              let signal = await handle.frames.top.reload()\n              if (signal.aborted) return\n              topPending = false\n              handle.update()\n            }),\n          ]}\n          style={{\n            '--top-bg': topPending ? undefined : 'rgba(255,255,255,0.10)',\n          }}\n        >\n          {topPending ? 'Reloading page…' : 'Reload top frame'}\n        </button>\n      </div>\n    )\n  },\n)\n"
  },
  {
    "path": "demos/frames/app/assets/reload-time.tsx",
    "content": "import { clientEntry, css, on, type Handle } from 'remix/component'\n\nexport let ReloadTime = clientEntry(\n  '/assets/reload-time.js#ReloadTime',\n  function ReloadTime(handle: Handle) {\n    let pending = false\n\n    return () => (\n      <button\n        type=\"button\"\n        mix={[\n          css({\n            padding: '6px 10px',\n            borderRadius: 10,\n            border: '1px solid rgba(255,255,255,0.18)',\n            background: pending ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.06)',\n            color: '#e9eefc',\n            cursor: pending ? 'default' : 'pointer',\n            '&:hover': { background: 'var(--bg)' },\n          }),\n          on('click', async () => {\n            pending = true\n            handle.update()\n            let reloadSignal = await handle.frame.reload()\n            if (reloadSignal.aborted) return\n            pending = false\n            handle.update()\n          }),\n        ]}\n        style={{\n          '--bg': pending ? undefined : 'rgba(255,255,255,0.1)',\n        }}\n      >\n        {pending ? 'Refreshing…' : 'Refresh'}\n      </button>\n    )\n  },\n)\n"
  },
  {
    "path": "demos/frames/app/assets/state-search-page.tsx",
    "content": "import { clientEntry, Frame, css, on, ref, type Handle } from 'remix/component'\nimport { routes } from '../routes.ts'\n\nlet moduleUrl = '/assets/state-search-page.js#StateSearchPage'\nexport let StateSearchPage = clientEntry(moduleUrl, (handle: Handle, setup?: string) => {\n  let query = setup || ''\n  let input: HTMLInputElement\n\n  return () => (\n    <section>\n      <form\n        mix={[\n          on('submit', async (event) => {\n            event.preventDefault()\n            query = input.value.trim()\n            await handle.update()\n            input.select()\n          }),\n          css({ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }),\n        ]}\n      >\n        <label mix={[css({ display: 'flex', flexDirection: 'column', gap: 4, flex: '1 1 320px' })]}>\n          <span mix={[css({ fontSize: 13, color: '#b9c6ff' })]}>Search states</span>\n          <input\n            placeholder=\"Try: carolina, dakota, new\"\n            mix={[\n              ref((node) => (input = node)),\n              css({\n                minWidth: 260,\n                padding: '8px 10px',\n                borderRadius: 10,\n                border: '1px solid rgba(255,255,255,0.18)',\n                background: 'rgba(255,255,255,0.04)',\n                color: '#e9eefc',\n              }),\n            ]}\n          />\n        </label>\n        <button\n          type=\"submit\"\n          mix={[\n            css({\n              padding: '8px 12px',\n              borderRadius: 10,\n              border: '1px solid rgba(255,255,255,0.18)',\n              background: 'rgba(255,255,255,0.06)',\n              color: '#e9eefc',\n              cursor: 'pointer',\n              marginTop: 20,\n              '&:hover': { background: 'rgba(255,255,255,0.1)' },\n            }),\n          ]}\n        >\n          Search\n        </button>\n      </form>\n\n      {query.trim() ? (\n        <div\n          mix={[\n            css({\n              border: '1px solid rgba(255,255,255,0.12)',\n              borderRadius: 12,\n              padding: 12,\n              background: 'rgba(255,255,255,0.03)',\n            }),\n          ]}\n        >\n          <Frame\n            src={routes.frames.stateSearchResults.href(undefined, { query })}\n            fallback={<div mix={[css({ color: '#9aa8e8' })]}>Searching states…</div>}\n          />\n        </div>\n      ) : (\n        <p mix={[css({ margin: 0, color: '#9aa8e8' })]}>\n          Enter a state name to run the frame search.\n        </p>\n      )}\n    </section>\n  )\n})\n"
  },
  {
    "path": "demos/frames/app/router.tsx",
    "content": "import { createRouter } from 'remix/fetch-router'\nimport { logger } from 'remix/logger-middleware'\nimport { staticFiles } from 'remix/static-middleware'\nimport { renderToStream } from 'remix/component/server'\nimport { Frame } from 'remix/component'\n\nimport { routes } from './routes.ts'\nimport { Counter } from './assets/counter.tsx'\nimport { ReloadTime } from './assets/reload-time.tsx'\nimport { ReloadScope } from './assets/reload-scope.tsx'\nimport { ClientFrameExample } from './assets/client-frame-example.tsx'\nimport { ClientMountedPageExample } from './assets/client-mounted-page-example.tsx'\nimport { StateSearchPage } from './assets/state-search-page.tsx'\nimport { searchUnitedStates } from './us-states.ts'\n\nlet middleware = []\n\nif (process.env.NODE_ENV === 'development') {\n  middleware.push(logger())\n}\n\nmiddleware.push(\n  staticFiles('./public', {\n    cacheControl: 'no-store',\n    etag: false,\n    lastModified: false,\n    index: false,\n  }),\n)\n\nexport let router = createRouter({ middleware })\n\nfunction App() {\n  return () => (\n    <html>\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <title>Frames + fetch-router demo</title>\n        <script async type=\"module\" src=\"/assets/entry.js\" />\n      </head>\n      <body\n        style={{\n          fontFamily:\n            'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji',\n          margin: 0,\n          padding: 24,\n          background: '#0b1020',\n          color: '#e9eefc',\n        }}\n      >\n        <div style={{ maxWidth: 980, margin: '0 auto' }}>\n          <h1 style={{ margin: 0, letterSpacing: '-0.02em' }}>Full-stack Frames</h1>\n          <p style={{ marginTop: 8, color: '#b9c6ff' }}>\n            Server routes are handled by <code>remix/fetch-router</code>; UI is streamed with{' '}\n            <code>remix/component</code> Frames and client entries.\n          </p>\n          <p style={{ marginTop: 0, marginBottom: 16 }}>\n            <a href=\"/time\" style={{ color: '#b9c6ff', textDecoration: 'underline' }}>\n              Server time demo\n            </a>\n            {' · '}\n            <a href=\"/reload-scope\" style={{ color: '#b9c6ff', textDecoration: 'underline' }}>\n              Frame vs top reload demo\n            </a>\n            {' · '}\n            <a href=\"/state-search\" style={{ color: '#b9c6ff', textDecoration: 'underline' }}>\n              Dynamic src search demo\n            </a>\n            {' · '}\n            <a href=\"/client-mounted\" style={{ color: '#b9c6ff', textDecoration: 'underline' }}>\n              Client-mounted nested frame demo\n            </a>\n          </p>\n\n          <div\n            style={{\n              display: 'grid',\n              gridTemplateColumns: '320px 1fr',\n              gap: 16,\n              alignItems: 'start',\n              marginTop: 24,\n            }}\n          >\n            <aside\n              style={{\n                border: '1px solid rgba(255,255,255,0.12)',\n                borderRadius: 12,\n                padding: 16,\n                background: 'rgba(255,255,255,0.04)',\n              }}\n            >\n              <h2 style={{ marginTop: 0, fontSize: 16 }}>Sidebar (Frame)</h2>\n              <Frame\n                src=\"/frames/sidebar\"\n                fallback={<div style={{ color: '#9aa8e8' }}>Loading sidebar…</div>}\n              />\n            </aside>\n\n            <main\n              style={{\n                border: '1px solid rgba(255,255,255,0.12)',\n                borderRadius: 12,\n                padding: 16,\n                background: 'rgba(255,255,255,0.04)',\n              }}\n            >\n              <h2 style={{ marginTop: 0, fontSize: 16 }}>Main</h2>\n              <p style={{ color: '#b9c6ff', marginTop: 0 }}>The counter below is a client entry.</p>\n              <Counter setup={0} label=\"Clicks\" />\n              <ClientFrameExample />\n\n              <div style={{ height: 16 }} />\n\n              <h3 style={{ margin: 0, fontSize: 14, color: '#cfd8ff' }}>Activity (Frame)</h3>\n              <Frame\n                src=\"/frames/activity\"\n                fallback={<div style={{ color: '#9aa8e8' }}>Loading activity…</div>}\n              />\n            </main>\n          </div>\n        </div>\n      </body>\n    </html>\n  )\n}\n\nrouter.get(routes.home, async (context: any) => {\n  let stream = renderToStream(<App />, {\n    resolveFrame: (src: string) => resolveFrameViaRouter(context.request, src),\n    onError(error) {\n      console.error(error)\n    },\n  })\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.clientMounted, async (context: any) => {\n  function ClientMountedPage() {\n    return () => (\n      <html>\n        <head>\n          <meta charSet=\"utf-8\" />\n          <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n          <title>Client-mounted nested frame</title>\n          <script async type=\"module\" src=\"/assets/entry.js\" />\n        </head>\n        <body\n          style={{\n            fontFamily:\n              'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji',\n            margin: 0,\n            padding: 24,\n            background: '#0b1020',\n            color: '#e9eefc',\n          }}\n        >\n          <div style={{ maxWidth: 760, margin: '0 auto' }}>\n            <a href=\"/\" style={{ color: '#b9c6ff', textDecoration: 'underline' }}>\n              ← Back\n            </a>\n            <h1 style={{ marginTop: 16, marginBottom: 8, letterSpacing: '-0.02em' }}>\n              Client-mounted nested non-blocking frame\n            </h1>\n            <p style={{ marginTop: 0, color: '#b9c6ff' }}>\n              Mount the outer frame, then watch the nested frame fallback render before its server\n              content streams in.\n            </p>\n            <ClientMountedPageExample />\n          </div>\n        </body>\n      </html>\n    )\n  }\n\n  let stream = renderToStream(<ClientMountedPage />, {\n    resolveFrame: (src: string) => resolveFrameViaRouter(context.request, src),\n    onError(error) {\n      console.error(error)\n    },\n  })\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.time, async (context: any) => {\n  function TimePage() {\n    return () => (\n      <html>\n        <head>\n          <meta charSet=\"utf-8\" />\n          <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n          <title>Server time</title>\n          <script async type=\"module\" src=\"/assets/entry.js\" />\n        </head>\n        <body\n          style={{\n            fontFamily:\n              'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji',\n            margin: 0,\n            padding: 24,\n            background: '#0b1020',\n            color: '#e9eefc',\n          }}\n        >\n          <div style={{ maxWidth: 720, margin: '0 auto' }}>\n            <a href=\"/\" style={{ color: '#b9c6ff', textDecoration: 'underline' }}>\n              ← Back\n            </a>\n\n            <h1 style={{ marginTop: 16, marginBottom: 8, letterSpacing: '-0.02em' }}>\n              Server time (Frame reload)\n            </h1>\n            <p style={{ marginTop: 0, color: '#b9c6ff' }}>\n              The frame below renders the current server time. Click “Refresh” to call{' '}\n              <code>frame.reload()</code> from a client entry inside the frame.\n            </p>\n\n            <div\n              style={{\n                border: '1px solid rgba(255,255,255,0.12)',\n                borderRadius: 12,\n                padding: 16,\n                background: 'rgba(255,255,255,0.04)',\n              }}\n            >\n              <Frame\n                src={routes.frames.time.href()}\n                fallback={<div style={{ color: '#9aa8e8' }}>Loading server time…</div>}\n              />\n            </div>\n          </div>\n        </body>\n      </html>\n    )\n  }\n\n  let stream = renderToStream(<TimePage />, {\n    resolveFrame: (src: string) => resolveFrameViaRouter(context.request, src),\n    onError(error) {\n      console.error(error)\n    },\n  })\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.reloadScope, async (context: any) => {\n  function ReloadScopePage() {\n    let pageNow = new Date()\n\n    return () => (\n      <html>\n        <head>\n          <meta charSet=\"utf-8\" />\n          <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n          <title>Frame vs top reload</title>\n          <script async type=\"module\" src=\"/assets/entry.js\" />\n        </head>\n        <body\n          style={{\n            fontFamily:\n              'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji',\n            margin: 0,\n            padding: 24,\n            background: '#0b1020',\n            color: '#e9eefc',\n          }}\n        >\n          <div style={{ maxWidth: 760, margin: '0 auto' }}>\n            <a href=\"/\" style={{ color: '#b9c6ff', textDecoration: 'underline' }}>\n              ← Back\n            </a>\n            <h1 style={{ marginTop: 16, marginBottom: 8, letterSpacing: '-0.02em' }}>\n              Frame reload vs top reload\n            </h1>\n            <p style={{ marginTop: 0, color: '#b9c6ff' }}>\n              Reload only this frame, or reload the entire runtime tree from inside the same client\n              entry.\n            </p>\n            <div style={{ marginBottom: 10 }}>\n              <div style={{ fontSize: 13, color: '#b9c6ff' }}>Page server time</div>\n              <div style={{ fontSize: 20, fontVariantNumeric: 'tabular-nums' }}>\n                {pageNow.toLocaleTimeString()}\n              </div>\n            </div>\n            <div\n              style={{\n                border: '1px solid rgba(255,255,255,0.12)',\n                borderRadius: 12,\n                padding: 16,\n                background: 'rgba(255,255,255,0.04)',\n              }}\n            >\n              <Frame\n                src={routes.frames.reloadScope.href()}\n                fallback={<div style={{ color: '#9aa8e8' }}>Loading reload controls…</div>}\n              />\n            </div>\n          </div>\n        </body>\n      </html>\n    )\n  }\n\n  let stream = renderToStream(<ReloadScopePage />, {\n    resolveFrame: (src: string) => resolveFrameViaRouter(context.request, src),\n    onError(error) {\n      console.error(error)\n    },\n  })\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.stateSearch, async (context: any) => {\n  let url = new URL(context.request.url)\n  let initialQuery = url.searchParams.get('query') ?? ''\n\n  function StateSearchRoutePage() {\n    return () => (\n      <html>\n        <head>\n          <meta charSet=\"utf-8\" />\n          <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n          <title>Dynamic Frame src search</title>\n          <script async type=\"module\" src=\"/assets/entry.js\" />\n        </head>\n        <body\n          style={{\n            fontFamily:\n              'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji',\n            margin: 0,\n            padding: 24,\n            background: '#0b1020',\n            color: '#e9eefc',\n          }}\n        >\n          <div style={{ maxWidth: 760, margin: '0 auto' }}>\n            <a href=\"/\" style={{ color: '#b9c6ff', textDecoration: 'underline' }}>\n              ← Back\n            </a>\n            <h1 style={{ marginTop: 16, marginBottom: 8, letterSpacing: '-0.02em' }}>\n              Dynamic <code>{'<Frame src>'}</code> state search\n            </h1>\n            <p style={{ marginTop: 0, color: '#b9c6ff' }}>\n              Submit the form to update the frame <code>src</code> query params and fetch matching\n              U.S. states.\n            </p>\n            <StateSearchPage setup={initialQuery} />\n          </div>\n        </body>\n      </html>\n    )\n  }\n\n  let stream = renderToStream(<StateSearchRoutePage />, {\n    resolveFrame: (src: string) => resolveFrameViaRouter(context.request, src),\n    onError(error) {\n      console.error(error)\n    },\n  })\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.sidebar, async () => {\n  await new Promise((resolve) => setTimeout(resolve, 400))\n\n  let stream = renderToStream(\n    <div>\n      <p style={{ marginTop: 0, color: '#b9c6ff' }}>\n        This content is rendered by <code>/frames/sidebar</code>.\n      </p>\n      <ul style={{ margin: 0, paddingLeft: '18px', color: '#e9eefc' }}>\n        <li>Streams in after initial HTML</li>\n        <li>Can contain client entries</li>\n        <li>Can nest frames</li>\n      </ul>\n    </div>,\n    { onError: console.error },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.activity, async (context: any) => {\n  await new Promise((resolve) => setTimeout(resolve, 2000))\n\n  let stream = renderToStream(\n    <div>\n      <p style={{ marginTop: 0, color: '#b9c6ff' }}>\n        Rendered by <code>/frames/activity</code> at <time>{new Date().toLocaleTimeString()}</time>.\n      </p>\n      <Frame\n        src={routes.frames.activityDetail.href()}\n        fallback={<div style={{ color: '#9aa8e8' }}>Loading detail…</div>}\n      />\n    </div>,\n    {\n      resolveFrame: (src: string) => resolveFrameViaRouter(context.request, src),\n      onError: console.error,\n    },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.activityDetail, async (context: any) => {\n  await new Promise((resolve) => setTimeout(resolve, 600))\n\n  let stream = renderToStream(\n    <div>\n      <p style={{ marginTop: 0, marginBottom: 8, color: '#9aa8e8' }}>\n        Nested frame with a hydrated counter:\n      </p>\n      <div style={{ marginTop: 12 }}>\n        <Frame\n          src={routes.frames.time.href()}\n          fallback={<div style={{ color: '#9aa8e8' }}>Loading server time…</div>}\n        />\n      </div>\n    </div>,\n    {\n      resolveFrame: (src: string) => resolveFrameViaRouter(context.request, src),\n      onError: console.error,\n    },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.clientFrameExample, async (context) => {\n  await new Promise((resolve) => setTimeout(resolve, 500))\n\n  let now = new Date()\n  let stream = renderToStream(\n    <div\n      style={{\n        border: '1px solid rgba(255,255,255,0.10)',\n        borderRadius: 10,\n        padding: 10,\n        background: 'rgba(255,255,255,0.02)',\n      }}\n    >\n      <div style={{ fontSize: 12, color: '#b9c6ff' }}>\n        Server fragment from /frames/client-frame-example\n      </div>\n      <div style={{ fontSize: 16, fontVariantNumeric: 'tabular-nums', marginTop: 2 }}>\n        {now.toLocaleTimeString()}\n      </div>\n      <div style={{ marginTop: 8 }}>\n        <Counter setup={5} label=\"Inside mounted frame\" />\n      </div>\n      <div style={{ marginTop: 10 }}>\n        <div style={{ fontSize: 12, color: '#9aa8e8', marginBottom: 6 }}>Nested frame:</div>\n        <Frame\n          src={routes.frames.clientFrameExampleNested.href()}\n          fallback={<div style={{ color: '#9aa8e8' }}>Loading nested frame…</div>}\n        />\n      </div>\n    </div>,\n    {\n      resolveFrame: (src: string) => resolveFrameViaRouter(context.request, src),\n      onError: console.error,\n    },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.clientFrameExampleNested, async () => {\n  await new Promise((resolve) => setTimeout(resolve, 350))\n\n  let stream = renderToStream(\n    <div\n      style={{\n        border: '1px solid rgba(255,255,255,0.10)',\n        borderRadius: 8,\n        padding: 8,\n        background: 'rgba(255,255,255,0.02)',\n      }}\n    >\n      <div style={{ fontSize: 12, color: '#b9c6ff' }}>Nested server fragment</div>\n      <div style={{ marginTop: 6 }}>\n        <Counter setup={1} label=\"Nested frame counter\" />\n      </div>\n    </div>,\n    { onError: console.error },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.clientMountedOuter, async (context: any) => {\n  await new Promise((resolve) => setTimeout(resolve, 350))\n\n  let stream = renderToStream(\n    <div\n      style={{\n        border: '1px solid rgba(255,255,255,0.10)',\n        borderRadius: 10,\n        padding: 10,\n        background: 'rgba(255,255,255,0.02)',\n      }}\n    >\n      <div style={{ fontSize: 12, color: '#b9c6ff' }}>\n        Outer server fragment from /frames/client-mounted-outer\n      </div>\n      <div style={{ marginTop: 8 }}>\n        <Counter setup={2} label=\"Outer frame counter\" />\n      </div>\n      <div style={{ marginTop: 10 }}>\n        <div style={{ fontSize: 12, color: '#9aa8e8', marginBottom: 6 }}>\n          Nested non-blocking frame:\n        </div>\n        <Frame\n          src={routes.frames.clientMountedNested.href()}\n          fallback={<div style={{ color: '#9aa8e8' }}>Loading nested non-blocking frame…</div>}\n        />\n      </div>\n    </div>,\n    {\n      resolveFrame: (src: string) => resolveFrameViaRouter(context.request, src),\n      onError: console.error,\n    },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.clientMountedNested, async () => {\n  await new Promise((resolve) => setTimeout(resolve, 2200))\n\n  let stream = renderToStream(\n    <div\n      style={{\n        border: '1px solid rgba(255,255,255,0.10)',\n        borderRadius: 8,\n        padding: 8,\n        background: 'rgba(255,255,255,0.02)',\n      }}\n    >\n      <div style={{ fontSize: 12, color: '#b9c6ff' }}>Nested server fragment</div>\n      <div style={{ fontSize: 16, marginTop: 2, fontVariantNumeric: 'tabular-nums' }}>\n        {new Date().toLocaleTimeString()}\n      </div>\n      <div style={{ marginTop: 6 }}>\n        <Counter setup={3} label=\"Nested frame counter\" />\n      </div>\n    </div>,\n    { onError: console.error },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.time, async () => {\n  // Artificial delay so the frame fallback/pending UI is visible.\n  await new Promise((resolve) => setTimeout(resolve, 1200))\n\n  let now = new Date()\n  let stream = renderToStream(\n    <div>\n      <div\n        style={{\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'space-between',\n          gap: 12,\n        }}\n      >\n        <div>\n          <div style={{ fontSize: 13, color: '#b9c6ff' }}>Server time</div>\n          <div style={{ fontSize: 18, fontVariantNumeric: 'tabular-nums' }}>\n            {now.toLocaleTimeString()}\n          </div>\n        </div>\n        <Counter setup={0} label=\"In a frame\" />\n        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>\n          <ReloadTime />\n        </div>\n      </div>\n    </div>,\n    { onError: console.error },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.reloadScope, async () => {\n  await new Promise((resolve) => setTimeout(resolve, 700))\n\n  let now = new Date()\n  let stream = renderToStream(\n    <div>\n      <div style={{ fontSize: 13, color: '#b9c6ff' }}>Frame server time</div>\n      <div style={{ fontSize: 18, fontVariantNumeric: 'tabular-nums', marginBottom: 10 }}>\n        {now.toLocaleTimeString()}\n      </div>\n      <ReloadScope />\n    </div>,\n    { onError: console.error },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nrouter.get(routes.frames.stateSearchResults, async (context: any) => {\n  await new Promise((resolve) => setTimeout(resolve, 300))\n\n  let url = new URL(context.request.url)\n  let query = (url.searchParams.get('query') ?? '').trim()\n  let matches = searchUnitedStates(query)\n\n  let stream = renderToStream(\n    <div>\n      <p style={{ marginTop: 0, marginBottom: 10, color: '#b9c6ff' }}>\n        {query\n          ? `Results for \"${query}\" (${matches.length})`\n          : `Showing all states (${matches.length})`}\n      </p>\n      {matches.length > 0 ? (\n        <ul style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 4 }}>\n          {matches.map((state) => (\n            <li key={state}>{state}</li>\n          ))}\n        </ul>\n      ) : (\n        <p style={{ margin: 0, color: '#9aa8e8' }}>No states matched that query.</p>\n      )}\n    </div>,\n    { onError: console.error },\n  )\n\n  return new Response(stream, {\n    headers: {\n      'Content-Type': 'text/html; charset=utf-8',\n      'Cache-Control': 'no-store',\n    },\n  })\n})\n\nasync function resolveFrameViaRouter(request: Request, src: string) {\n  let url = new URL(src, request.url)\n\n  // IMPORTANT: this is a server-internal fetch to get *HTML*, so do not forward\n  // Accept-Encoding — otherwise compression middleware could return compressed bytes.\n  let headers = new Headers(request.headers)\n  headers.delete('accept-encoding')\n  headers.set('accept', 'text/html')\n\n  let res = await router.fetch(\n    new Request(url, {\n      method: 'GET',\n      headers,\n      signal: request.signal,\n    }),\n  )\n\n  if (!res.ok) {\n    return `<pre>Frame error: ${res.status} ${res.statusText}</pre>`\n  }\n\n  if (res.body) {\n    return res.body\n  }\n\n  return await res.text()\n}\n"
  },
  {
    "path": "demos/frames/app/routes.ts",
    "content": "import { get, route } from 'remix/fetch-router/routes'\n\nexport let routes = route({\n  home: get('/'),\n  time: get('/time'),\n  reloadScope: get('/reload-scope'),\n  stateSearch: get('/state-search'),\n  clientMounted: get('/client-mounted'),\n  frames: route('frames', {\n    clientFrameExample: get('/client-frame-example'),\n    clientFrameExampleNested: get('/client-frame-example/nested'),\n    clientMountedOuter: get('/client-mounted-outer'),\n    clientMountedNested: get('/client-mounted-nested'),\n    sidebar: get('/sidebar'),\n    activity: get('/activity'),\n    activityDetail: get('/activity/detail'),\n    time: get('/time'),\n    reloadScope: get('/reload-scope'),\n    stateSearchResults: get('/state-search-results'),\n  }),\n})\n"
  },
  {
    "path": "demos/frames/app/us-states.ts",
    "content": "export let unitedStates = [\n  'Alabama',\n  'Alaska',\n  'Arizona',\n  'Arkansas',\n  'California',\n  'Colorado',\n  'Connecticut',\n  'Delaware',\n  'Florida',\n  'Georgia',\n  'Hawaii',\n  'Idaho',\n  'Illinois',\n  'Indiana',\n  'Iowa',\n  'Kansas',\n  'Kentucky',\n  'Louisiana',\n  'Maine',\n  'Maryland',\n  'Massachusetts',\n  'Michigan',\n  'Minnesota',\n  'Mississippi',\n  'Missouri',\n  'Montana',\n  'Nebraska',\n  'Nevada',\n  'New Hampshire',\n  'New Jersey',\n  'New Mexico',\n  'New York',\n  'North Carolina',\n  'North Dakota',\n  'Ohio',\n  'Oklahoma',\n  'Oregon',\n  'Pennsylvania',\n  'Rhode Island',\n  'South Carolina',\n  'South Dakota',\n  'Tennessee',\n  'Texas',\n  'Utah',\n  'Vermont',\n  'Virginia',\n  'Washington',\n  'West Virginia',\n  'Wisconsin',\n  'Wyoming',\n]\n\nexport function searchUnitedStates(query: string): string[] {\n  let normalized = query.trim().toLowerCase()\n  if (!normalized) return unitedStates\n  return unitedStates.filter((state) => state.toLowerCase().includes(normalized))\n}\n"
  },
  {
    "path": "demos/frames/package.json",
    "content": "{\n  \"name\": \"frames-demo\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"remix\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/dom-navigation\": \"^1.0.7\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"esbuild\": \"^0.25.10\",\n    \"tsx\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"dev\": \"pnpm run --filter \\\".\\\" --parallel \\\"/^dev:/\\\"\",\n    \"dev:server\": \"NODE_ENV=development tsx watch server.ts\",\n    \"dev:browser\": \"esbuild app/assets/*.tsx --outbase=app/assets --outdir=public/assets --bundle --minify --splitting --format=esm --entry-names='[dir]/[name]' --chunk-names='chunks/[name]-[hash]' --sourcemap --watch\",\n    \"start\": \"tsx server.ts\",\n    \"typecheck\": \"tsc --noEmit\"\n  }\n}\n"
  },
  {
    "path": "demos/frames/server.ts",
    "content": "import * as http from 'node:http'\nimport { createRequestListener } from 'remix/node-fetch-server'\n\nimport { router } from './app/router.tsx'\n\nlet server = http.createServer(\n  createRequestListener(async (request: Request) => {\n    try {\n      return await router.fetch(request)\n    } catch (error) {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  }),\n)\n\nlet port = process.env.PORT ? parseInt(process.env.PORT, 10) : 44100\n\nserver.listen(port, () => {\n  console.log(`Frames demo is running on http://localhost:${port}`)\n})\n\nlet shuttingDown = false\n\nfunction shutdown() {\n  if (shuttingDown) return\n  shuttingDown = true\n  server.close(() => {\n    process.exit(0)\n  })\n}\n\nprocess.on('SIGINT', shutdown)\nprocess.on('SIGTERM', shutdown)\n"
  },
  {
    "path": "demos/frames/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\", \"DOM.AsyncIterable\"],\n    \"types\": [\"node\", \"dom-navigation\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"remix/component\",\n    \"paths\": {\n      \"*\": [\"./*\"],\n      \"remix/component/jsx-runtime\": [\"../../packages/remix/src/component/jsx-runtime.ts\"],\n      \"remix/component/jsx-dev-runtime\": [\"../../packages/remix/src/component/jsx-dev-runtime.ts\"]\n    }\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "demos/sse/.gitignore",
    "content": "public/assets/\n"
  },
  {
    "path": "demos/sse/app/assets/entry.tsx",
    "content": "import { run } from 'remix/component'\n\nlet app = run({\n  async loadModule(moduleUrl: string, name: string) {\n    let mod = await import(moduleUrl)\n    if (!mod) {\n      throw new Error(`Unknown module: ${moduleUrl}`)\n    }\n\n    let Component = mod[name]\n    if (!Component) {\n      throw new Error(`Unknown component: ${moduleUrl}#${name}`)\n    }\n\n    return Component\n  },\n})\n\napp.ready().catch((error: unknown) => {\n  console.error('Hydration failed:', error)\n})\n"
  },
  {
    "path": "demos/sse/app/assets/message-stream.tsx",
    "content": "import { addEventListeners, clientEntry, css, type Handle } from 'remix/component'\n\nimport { routes } from '../routes.ts'\n\nexport const MessageStream = clientEntry(\n  routes.assets.href({ path: 'message-stream.js#MessageStream' }),\n  function MessageStream(handle: Handle, setup: { limit: number | null }) {\n    let { limit } = setup\n    let messages: Array<{ count: number; message: string }> = []\n    let connected = false\n\n    handle.queueTask(() => {\n      let eventSource = new EventSource(routes.messages.href(null, limit ? { limit } : {}))\n\n      addEventListeners(eventSource, handle.signal, {\n        open: () => {\n          connected = true\n          handle.update()\n        },\n        message: (event) => {\n          let data = JSON.parse(event.data)\n          messages.push(data)\n          handle.update()\n        },\n        error: () => {\n          connected = false\n          handle.update()\n          eventSource.close()\n        },\n      })\n\n      handle.signal.addEventListener('abort', () => {\n        eventSource.close()\n      })\n    })\n\n    return () => (\n      <>\n        <div\n          mix={[\n            css({\n              background: 'white',\n              padding: '1.5rem',\n              borderRadius: '8px',\n              boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',\n              marginBottom: '1.5rem',\n            }),\n          ]}\n        >\n          <div\n            mix={[\n              css({\n                display: 'flex',\n                alignItems: 'center',\n                gap: '0.5rem',\n                padding: '0.75rem 1rem',\n                background: connected ? '#e3f2fd' : '#ffebee',\n                borderLeft: connected ? '4px solid #2196f3' : '4px solid #f44336',\n                borderRadius: '4px',\n                color: connected ? '#1976d2' : '#c62828',\n                fontWeight: 500,\n              }),\n            ]}\n          >\n            <span\n              mix={[\n                css({\n                  width: '10px',\n                  height: '10px',\n                  borderRadius: '50%',\n                  background: connected ? '#2196f3' : '#f44336',\n                  animation: connected ? 'pulse 2s ease-in-out infinite' : 'none',\n                }),\n              ]}\n            />\n            <span>{connected ? 'Connected' : 'Disconnected'}</span>\n          </div>\n        </div>\n\n        <div\n          mix={[\n            css({\n              background: 'white',\n              padding: '1.5rem',\n              borderRadius: '8px',\n              boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',\n              minHeight: '200px',\n            }),\n          ]}\n        >\n          <h2 mix={[css({ color: '#333', marginBottom: '1rem', fontSize: '1.25rem' })]}>\n            Messages\n          </h2>\n          <ul mix={[css({ listStyle: 'none' })]}>\n            {messages.length === 0 ? (\n              <li mix={[css({ textAlign: 'center', color: '#999', padding: '3rem 1rem' })]}>\n                Waiting for messages...\n              </li>\n            ) : (\n              messages.map((message) => (\n                <li\n                  mix={[\n                    css({\n                      padding: '0.75rem 1rem',\n                      background: '#f8f9fa',\n                      borderLeft: '3px solid #4caf50',\n                      borderRadius: '4px',\n                      marginBottom: '0.5rem',\n                      animation: 'slideIn 0.3s ease-out',\n                    }),\n                  ]}\n                  key={message.count}\n                >\n                  <span mix={[css({ fontWeight: 600, color: '#4caf50' })]}>#{message.count}</span>{' '}\n                  <span mix={[css({ color: '#666' })]}>{message.message}</span>\n                </li>\n              ))\n            )}\n          </ul>\n        </div>\n      </>\n    )\n  },\n)\n"
  },
  {
    "path": "demos/sse/app/layout.tsx",
    "content": "import { css, type RemixNode } from 'remix/component'\n\nimport { routes } from './routes.ts'\n\nconst rawCss = String.raw\n\nexport function Layout() {\n  return ({ children }: { children?: RemixNode }) => (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"UTF-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        <title>Server-Sent Events Demo</title>\n        <script type=\"module\" async src={routes.assets.href({ path: 'entry.js' })} />\n        <style>\n          {rawCss`\n            @layer reset {\n              * {\n                box-sizing: border-box;\n                margin: 0;\n                padding: 0;\n              }\n            }\n\n            @keyframes pulse {\n              0%,\n              100% {\n                opacity: 1;\n              }\n              50% {\n                opacity: 0.5;\n              }\n            }\n\n            @keyframes slideIn {\n              from {\n                opacity: 0;\n                transform: translateX(-20px);\n              }\n              to {\n                opacity: 1;\n                transform: translateX(0);\n              }\n            }\n          `}\n        </style>\n      </head>\n      <body\n        mix={[\n          css({\n            fontFamily: 'system-ui, -apple-system, sans-serif',\n            lineHeight: 1.5,\n            padding: '2rem',\n            maxWidth: '800px',\n            margin: '0 auto',\n            background: '#f5f5f5',\n          }),\n        ]}\n      >\n        {children}\n      </body>\n    </html>\n  )\n}\n"
  },
  {
    "path": "demos/sse/app/router.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { router } from './router.tsx'\n\ndescribe('router', () => {\n  describe('home page', () => {\n    it('returns 200', async () => {\n      let response = await router.fetch(new Request('http://localhost/'))\n\n      assert.equal(response.status, 200)\n    })\n\n    it('returns HTML content', async () => {\n      let response = await router.fetch(new Request('http://localhost/'))\n      let text = await response.text()\n\n      assert.ok(response.headers.get('Content-Type')?.startsWith('text/html'))\n      assert.ok(text.includes('<html'))\n    })\n\n    it('includes expected content', async () => {\n      let response = await router.fetch(new Request('http://localhost/'))\n      let text = await response.text()\n\n      assert.ok(text.includes('Server-Sent Events'))\n      assert.ok(text.includes('Compression'))\n    })\n\n    it('shows limit info when limit param is provided', async () => {\n      let response = await router.fetch(new Request('http://localhost/?limit=5'))\n      let text = await response.text()\n\n      assert.ok(text.includes('5'))\n      assert.ok(text.includes('message'))\n    })\n  })\n\n  describe('messages endpoint', () => {\n    it('returns SSE content type', async () => {\n      let controller = new AbortController()\n      let response = await router.fetch(\n        new Request('http://localhost/messages', { signal: controller.signal }),\n      )\n\n      assert.equal(response.headers.get('Content-Type'), 'text/event-stream')\n      assert.equal(response.headers.get('Cache-Control'), 'no-cache')\n\n      controller.abort()\n    })\n\n    it('streams messages with limit', async () => {\n      let response = await router.fetch(new Request('http://localhost/messages?limit=2'))\n\n      assert.ok(response.body)\n\n      let text = await response.text()\n      let events = text.split('\\n\\n').filter((e) => e.trim())\n\n      // Each message has 2 lines: event and data\n      assert.equal(events.length, 2)\n      assert.ok(text.includes('event: message'))\n      assert.ok(text.includes('\"count\":1'))\n      assert.ok(text.includes('\"count\":2'))\n    })\n  })\n\n  describe('POST requests', () => {\n    it('returns 404 for non-GET requests to home', async () => {\n      let response = await router.fetch(new Request('http://localhost/', { method: 'POST' }))\n\n      assert.equal(response.status, 404)\n    })\n\n    it('returns 404 for non-GET requests to messages', async () => {\n      let response = await router.fetch(\n        new Request('http://localhost/messages', { method: 'POST' }),\n      )\n\n      assert.equal(response.status, 404)\n    })\n  })\n})\n"
  },
  {
    "path": "demos/sse/app/router.tsx",
    "content": "import { createRouter } from 'remix/fetch-router'\nimport { compression } from 'remix/compression-middleware'\nimport { logger } from 'remix/logger-middleware'\nimport { staticFiles } from 'remix/static-middleware'\nimport { css } from 'remix/component'\nimport * as s from 'remix/data-schema'\nimport * as f from 'remix/data-schema/form-data'\nimport * as coerce from 'remix/data-schema/coerce'\n\nimport { routes } from './routes.ts'\nimport { MessageStream } from './assets/message-stream.tsx'\nimport { Layout } from './layout.tsx'\nimport { render } from './utils/render.ts'\n\nlet middleware = []\n\nif (process.env.NODE_ENV === 'development') {\n  middleware.push(logger())\n}\n\nmiddleware.push(compression())\nmiddleware.push(\n  staticFiles('./public', {\n    cacheControl: 'no-store',\n    etag: false,\n    lastModified: false,\n  }),\n)\n\nexport let router = createRouter({ middleware })\n\nconst messageLimitSchema = f.object({\n  limit: f.field(s.optional(coerce.number())),\n})\n\n// The assets route is handled by the static files middleware above\nlet { assets, ...pageRoutes } = routes\n\nrouter.map(pageRoutes, {\n  actions: {\n    home(context) {\n      let limit = getMessageLimit(context.url)\n\n      return render(\n        <Layout>\n          <h1 mix={[css({ color: '#333', marginBottom: '0.5rem' })]}>Server-Sent Events Demo</h1>\n          <p mix={[css({ color: '#666', marginBottom: '2rem' })]}>\n            Real-time updates with compression middleware\n          </p>\n\n          <div\n            mix={[\n              css({\n                background: 'white',\n                padding: '1.5rem',\n                borderRadius: '8px',\n                boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',\n                marginBottom: '1.5rem',\n              }),\n            ]}\n          >\n            <label\n              mix={[\n                css({\n                  display: 'block',\n                  fontWeight: 600,\n                  marginBottom: '0.5rem',\n                  color: '#333',\n                }),\n              ]}\n            >\n              Compression:\n            </label>\n            <div\n              mix={[\n                css({\n                  padding: '0.5rem',\n                  background: '#f8f9fa',\n                  borderRadius: '4px',\n                  color: '#666',\n                }),\n              ]}\n            >\n              Encoding is negotiated automatically via{' '}\n              <code\n                mix={[\n                  css({\n                    background: '#f5f5f5',\n                    padding: '0.2rem 0.4rem',\n                    borderRadius: '3px',\n                    fontFamily: \"'Courier New', monospace\",\n                    fontSize: '0.9em',\n                  }),\n                ]}\n              >\n                Accept-Encoding\n              </code>{' '}\n              header.\n              <br />\n              Open DevTools Network tab to see{' '}\n              <code\n                mix={[\n                  css({\n                    background: '#f5f5f5',\n                    padding: '0.2rem 0.4rem',\n                    borderRadius: '3px',\n                    fontFamily: \"'Courier New', monospace\",\n                    fontSize: '0.9em',\n                  }),\n                ]}\n              >\n                Content-Encoding\n              </code>{' '}\n              response header.\n            </div>\n          </div>\n\n          <div\n            mix={[\n              css({\n                background: 'white',\n                padding: '1.5rem',\n                borderRadius: '8px',\n                boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',\n                marginBottom: '1.5rem',\n              }),\n            ]}\n          >\n            <label\n              mix={[\n                css({\n                  display: 'block',\n                  fontWeight: 600,\n                  marginBottom: '0.5rem',\n                  color: '#333',\n                }),\n              ]}\n            >\n              Message Limit:\n            </label>\n            <div\n              mix={[\n                css({\n                  padding: '0.5rem',\n                  background: '#f8f9fa',\n                  borderRadius: '4px',\n                  color: '#666',\n                }),\n              ]}\n            >\n              {limit ? (\n                <>\n                  Stream will close after <strong>{limit}</strong> message{limit === 1 ? '' : 's'}.\n                </>\n              ) : (\n                <>\n                  No limit set.{' '}\n                  <a\n                    href=\"?limit=10\"\n                    mix={[css({ color: '#007bff', textDecoration: 'underline' })]}\n                  >\n                    Add{' '}\n                    <code\n                      mix={[\n                        css({\n                          background: '#f5f5f5',\n                          padding: '0.2rem 0.4rem',\n                          borderRadius: '3px',\n                          fontFamily: \"'Courier New', monospace\",\n                          fontSize: '0.9em',\n                        }),\n                      ]}\n                    >\n                      ?limit=10\n                    </code>{' '}\n                    to the URL\n                  </a>{' '}\n                  to limit messages.\n                </>\n              )}\n            </div>\n          </div>\n\n          <MessageStream setup={{ limit }} />\n        </Layout>,\n      )\n    },\n\n    messages(context) {\n      let limit = getMessageLimit(context.url)\n\n      let stream = new ReadableStream({\n        start(controller) {\n          let messageCount = 0\n\n          let interval = setInterval(() => {\n            try {\n              messageCount++\n\n              let timestamp = new Date().toLocaleTimeString()\n              let text = `Message #${messageCount} at ${timestamp}`\n\n              // Send SSE formatted message\n              controller.enqueue(new TextEncoder().encode(`event: message\\n`))\n              controller.enqueue(\n                new TextEncoder().encode(\n                  `data: ${JSON.stringify({ count: messageCount, message: text })}\\n\\n`,\n                ),\n              )\n\n              if (limit && messageCount >= limit) {\n                clearInterval(interval)\n                controller.close()\n              }\n            } catch (error) {\n              console.error('Error enqueuing message:', error)\n              clearInterval(interval)\n            }\n          }, 1000)\n\n          context.request.signal.addEventListener('abort', () => {\n            clearInterval(interval)\n            try {\n              controller.close()\n            } catch (error) {\n              // Stream may already be closed\n            }\n          })\n        },\n      })\n\n      return new Response(stream, {\n        headers: {\n          'Content-Type': 'text/event-stream',\n          'Cache-Control': 'no-cache',\n          Connection: 'keep-alive',\n        },\n      })\n    },\n  },\n})\n\nfunction getMessageLimit(url: URL): number | null {\n  let result = s.parseSafe(messageLimitSchema, url.searchParams)\n\n  if (!result.success || !result.value.limit) {\n    return null\n  }\n\n  return result.value.limit\n}\n"
  },
  {
    "path": "demos/sse/app/routes.ts",
    "content": "import { get, route } from 'remix/fetch-router/routes'\n\nexport let routes = route({\n  assets: '/assets/*path',\n  home: get('/'),\n  messages: get('/messages'),\n})\n"
  },
  {
    "path": "demos/sse/app/utils/render.ts",
    "content": "import type { RemixNode } from 'remix/component'\nimport { renderToStream } from 'remix/component/server'\nimport { createHtmlResponse } from 'remix/response/html'\n\nexport function render(node: RemixNode, init?: ResponseInit) {\n  return createHtmlResponse(renderToStream(node), init)\n}\n"
  },
  {
    "path": "demos/sse/package.json",
    "content": "{\n  \"name\": \"sse-demo\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@remix-run/component\": \"workspace:*\",\n    \"@remix-run/events\": \"jam\",\n    \"remix\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/dom-navigation\": \"^1.0.7\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"esbuild\": \"^0.25.10\",\n    \"tsx\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"dev\": \"pnpm run --filter \\\".\\\" --parallel \\\"/^dev:/\\\"\",\n    \"dev:server\": \"NODE_ENV=development tsx watch server.ts\",\n    \"dev:browser\": \"esbuild app/assets/*.tsx --outbase=app/assets --outdir=public/assets --bundle --minify --splitting --format=esm --entry-names='[dir]/[name]' --chunk-names='chunks/[name]-[hash]' --sourcemap --watch\",\n    \"start\": \"tsx server.ts\",\n    \"test\": \"tsx --test\",\n    \"typecheck\": \"tsc --noEmit\"\n  }\n}\n"
  },
  {
    "path": "demos/sse/server.ts",
    "content": "import * as http from 'node:http'\nimport { createRequestListener } from 'remix/node-fetch-server'\n\nimport { router } from './app/router.tsx'\n\nlet server = http.createServer(\n  createRequestListener(async (request) => {\n    try {\n      return await router.fetch(request)\n    } catch (error) {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  }),\n)\n\nlet port = process.env.PORT ? parseInt(process.env.PORT, 10) : 44100\n\nserver.listen(port, () => {\n  console.log(`Server-Sent Events demo is running on http://localhost:${port}`)\n})\n"
  },
  {
    "path": "demos/sse/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\", \"DOM.AsyncIterable\"],\n    \"types\": [\"node\", \"dom-navigation\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"remix/component\",\n    \"paths\": {\n      \"*\": [\"./*\"],\n      \"remix/component/jsx-runtime\": [\"../../packages/remix/src/component/jsx-runtime.ts\"],\n      \"remix/component/jsx-dev-runtime\": [\"../../packages/remix/src/component/jsx-dev-runtime.ts\"]\n    }\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "demos/unpkg/README.md",
    "content": "# UNPKG Demo\n\nA lo-fi clone of [unpkg.com](https://unpkg.com) that lets you browse the contents of any npm package. This demo showcases tarball parsing, filesystem caching, HTML templating, and clean URL routing with catch-all patterns.\n\n## Running the Demo\n\n```bash\ncd demos/unpkg\npnpm install\npnpm start\n```\n\nThen visit http://localhost:44100\n\nSemver ranges are supported (via the [semver](https://www.npmjs.com/package/semver) package). URLs without a version, or with partial/range versions, automatically redirect to the fully resolved version.\n\n## Code Highlights\n\n- [`app/routes.ts`](app/routes.ts) uses a single `/*path` route to handle all package URLs. This one pattern handles package names, versions, scoped packages, and file paths.\n- [`app/lib/npm.ts`](app/lib/npm.ts) fetches package tarballs from npm, decompresses them with `node:zlib`, and parses them using `@remix-run/tar-parser`. The `parsePackagePath()` function handles the tricky parsing of URLs like `/@remix-run/cookie@1.0.0/src/index.ts`.\n- Also in [`app/lib/npm.ts`](app/lib/npm.ts), `resolveVersion()` handles dist-tags like `latest`, exact versions, partial versions (e.g., `18` resolves to `18.3.1`), and semver ranges (e.g., `^18.2` or `~1.0.0`).\n- [`app/lib/cache.ts`](app/lib/cache.ts) caches decompressed tarballs to the temp directory using `@remix-run/file-storage`. This avoids re-downloading packages on repeated requests.\n- [`app/lib/render.ts`](app/lib/render.ts) uses the `html` template tag from `@remix-run/html-template` for safe HTML generation with automatic XSS escaping.\n"
  },
  {
    "path": "demos/unpkg/app/breadcrumb.ts",
    "content": "import { html } from './utils/render.ts'\n\nexport function renderBreadcrumb(packageName: string, version: string, dirPath: string) {\n  let parts: Array<{ name: string; href: string }> = [\n    { name: 'Home', href: '/' },\n    { name: `${packageName}@${version}`, href: `/${packageName}@${version}` },\n  ]\n\n  if (dirPath) {\n    let pathParts = dirPath.split('/')\n    let currentPath = ''\n    for (let part of pathParts) {\n      currentPath += (currentPath ? '/' : '') + part\n      parts.push({\n        name: part,\n        href: `/${packageName}@${version}/${currentPath}`,\n      })\n    }\n  }\n\n  let links = parts.map((part, i) => {\n    if (i === parts.length - 1) {\n      return html`<span>${part.name}</span>`\n    }\n    return html`<a href=\"${part.href}\">${part.name}</a>`\n  })\n\n  return html`<nav class=\"breadcrumb\">\n    ${links.map((link, i) => (i === 0 ? link : html` / ${link}`))}\n  </nav>`\n}\n"
  },
  {
    "path": "demos/unpkg/app/directory.ts",
    "content": "import { detectMimeType } from 'remix/mime'\n\nimport { renderBreadcrumb } from './breadcrumb.ts'\nimport type { PackageFile } from './utils/npm.ts'\nimport { html, render, formatBytes, icons } from './utils/render.ts'\n\nexport function renderDirectoryListing(\n  packageName: string,\n  version: string,\n  dirPath: string,\n  files: PackageFile[],\n): Response {\n  let title = dirPath ? `${packageName}@${version}/${dirPath}` : `${packageName}@${version}`\n  let breadcrumb = renderBreadcrumb(packageName, version, dirPath)\n\n  let parentRow = dirPath\n    ? html`\n        <tr>\n          <td class=\"icon\">\n            <a href=\"/${packageName}@${version}${getParentPath(dirPath)}\">${icons.directory}</a>\n          </td>\n          <td class=\"name\"><a href=\"/${packageName}@${version}${getParentPath(dirPath)}\">..</a></td>\n          <td class=\"size\"><a href=\"/${packageName}@${version}${getParentPath(dirPath)}\">-</a></td>\n          <td class=\"type\">\n            <a href=\"/${packageName}@${version}${getParentPath(dirPath)}\">directory</a>\n          </td>\n        </tr>\n      `\n    : ''\n\n  let fileRows = files.map((file) => {\n    let href = `/${packageName}@${version}/${file.path}`\n    let icon = file.type === 'directory' ? icons.directory : icons.file\n    let displayName = file.type === 'directory' ? file.name + '/' : file.name\n    let mimeType =\n      file.type === 'directory'\n        ? 'directory'\n        : (detectMimeType(file.name) ?? 'application/octet-stream')\n\n    return html`\n      <tr>\n        <td class=\"icon\"><a href=\"${href}\">${icon}</a></td>\n        <td class=\"name\"><a href=\"${href}\">${displayName}</a></td>\n        <td class=\"size\"><a href=\"${href}\">${formatBytes(file.size)}</a></td>\n        <td class=\"type\"><a href=\"${href}\">${mimeType}</a></td>\n      </tr>\n    `\n  })\n\n  return render(\n    title,\n    html`\n      <h1>${packageName}</h1>\n      ${breadcrumb}\n      <p class=\"package-info\">Version: ${version}</p>\n      <div class=\"file-browser\">\n        <table>\n          <thead>\n            <tr>\n              <th class=\"icon\"></th>\n              <th>Name</th>\n              <th class=\"size\">Size</th>\n              <th class=\"type\">Type</th>\n            </tr>\n          </thead>\n          <tbody>\n            ${parentRow} ${fileRows}\n          </tbody>\n        </table>\n      </div>\n    `,\n  )\n}\n\nfunction getParentPath(dirPath: string): string {\n  let parts = dirPath.split('/')\n  parts.pop()\n  return parts.length > 0 ? '/' + parts.join('/') : ''\n}\n"
  },
  {
    "path": "demos/unpkg/app/error.ts",
    "content": "import { html, render } from './utils/render.ts'\n\nexport function renderError(title: string, message: string): Response {\n  return render(\n    title,\n    html`\n      <h1>${title}</h1>\n      <div class=\"error\">\n        <p>${message}</p>\n      </div>\n      <p style=\"margin-top: 1rem;\">\n        <a href=\"/\">Back to home</a>\n      </p>\n    `,\n    { status: 404 },\n  )\n}\n"
  },
  {
    "path": "demos/unpkg/app/file-content.ts",
    "content": "import { renderBreadcrumb } from './breadcrumb.ts'\nimport type { PackageFile } from './utils/npm.ts'\nimport { html, render, formatBytes } from './utils/render.ts'\n\nconst TEXT_EXTENSIONS = new Set([\n  '.js',\n  '.mjs',\n  '.cjs',\n  '.ts',\n  '.mts',\n  '.cts',\n  '.tsx',\n  '.jsx',\n  '.json',\n  '.md',\n  '.markdown',\n  '.txt',\n  '.css',\n  '.scss',\n  '.less',\n  '.html',\n  '.htm',\n  '.xml',\n  '.svg',\n  '.yaml',\n  '.yml',\n  '.toml',\n  '.ini',\n  '.conf',\n  '.sh',\n  '.bash',\n  '.zsh',\n  '.fish',\n  '.ps1',\n  '.bat',\n  '.cmd',\n  '.py',\n  '.rb',\n  '.php',\n  '.java',\n  '.c',\n  '.cpp',\n  '.h',\n  '.hpp',\n  '.cs',\n  '.go',\n  '.rs',\n  '.swift',\n  '.kt',\n  '.scala',\n  '.clj',\n  '.ex',\n  '.exs',\n  '.erl',\n  '.hrl',\n  '.lua',\n  '.r',\n  '.sql',\n  '.graphql',\n  '.gql',\n  '.vue',\n  '.svelte',\n  '.astro',\n  '.prisma',\n  '.env',\n  '.gitignore',\n  '.npmignore',\n  '.eslintrc',\n  '.prettierrc',\n  '.editorconfig',\n  'LICENSE',\n  'README',\n  'CHANGELOG',\n  'Makefile',\n  'Dockerfile',\n])\n\nconst IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp'])\n\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n  '.png': 'image/png',\n  '.jpg': 'image/jpeg',\n  '.jpeg': 'image/jpeg',\n  '.gif': 'image/gif',\n  '.webp': 'image/webp',\n  '.ico': 'image/x-icon',\n  '.bmp': 'image/bmp',\n}\n\nfunction getExtension(filename: string): string {\n  let lastDot = filename.lastIndexOf('.')\n  if (lastDot === -1) return filename.toUpperCase()\n  return filename.slice(lastDot).toLowerCase()\n}\n\nfunction isTextFile(filename: string): boolean {\n  let ext = getExtension(filename)\n  return TEXT_EXTENSIONS.has(ext)\n}\n\nfunction isImageFile(filename: string): boolean {\n  let ext = getExtension(filename)\n  return IMAGE_EXTENSIONS.has(ext)\n}\n\nfunction isLikelyText(data: Uint8Array): boolean {\n  let sampleSize = Math.min(data.length, 8192)\n  for (let i = 0; i < sampleSize; i++) {\n    let byte = data[i]\n    if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {\n      return false\n    }\n  }\n  return true\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n  let binary = ''\n  for (let i = 0; i < bytes.length; i++) {\n    binary += String.fromCharCode(bytes[i])\n  }\n  return btoa(binary)\n}\n\nexport function renderFileContent(\n  packageName: string,\n  version: string,\n  filePath: string,\n  file: PackageFile,\n  data: Uint8Array,\n): Response {\n  let title = `${packageName}@${version}/${filePath}`\n  let ext = getExtension(file.name)\n  let breadcrumb = renderBreadcrumb(packageName, version, filePath)\n\n  let content\n  if (isImageFile(file.name)) {\n    let mimeType = IMAGE_MIME_TYPES[ext] || 'image/png'\n    let base64 = bytesToBase64(data)\n    content = html`\n      <div class=\"file-content\">\n        <img src=\"data:${mimeType};base64,${base64}\" alt=\"${file.name}\" />\n      </div>\n    `\n  } else if (isTextFile(file.name) || isLikelyText(data)) {\n    let text = new TextDecoder().decode(data)\n    content = html`\n      <div class=\"file-content\">\n        <pre>${text}</pre>\n      </div>\n    `\n  } else {\n    content = html`\n      <div class=\"info\">\n        <p>This file cannot be displayed. It may be a binary file.</p>\n        <p>File size: ${formatBytes(file.size)}</p>\n      </div>\n    `\n  }\n\n  return render(\n    title,\n    html`\n      <h1>${file.name}</h1>\n      ${breadcrumb}\n      <p class=\"package-info\">Size: ${formatBytes(file.size)}</p>\n      ${content}\n    `,\n  )\n}\n"
  },
  {
    "path": "demos/unpkg/app/router.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { after, before, describe, it } from 'node:test'\n\nimport {\n  installFetchMock,\n  restoreFetchMock,\n  addFetchHandler,\n  createNpmRegistryMock,\n} from '../test/mock-fetch.ts'\nimport { router } from './router.ts'\n\nbefore(() => {\n  installFetchMock()\n  addFetchHandler(\n    createNpmRegistryMock({\n      'is-number': {\n        metadata: 'is-number-metadata.json',\n        tarballs: {\n          '7.0.0': 'is-number-7.0.0.tgz',\n        },\n      },\n    }),\n  )\n})\n\nafter(() => {\n  restoreFetchMock()\n})\n\ndescribe('router', () => {\n  describe('home page', () => {\n    it('returns 200', async () => {\n      let response = await router.fetch(new Request('http://localhost/'))\n\n      assert.equal(response.status, 200)\n    })\n\n    it('returns HTML content', async () => {\n      let response = await router.fetch(new Request('http://localhost/'))\n      let text = await response.text()\n\n      assert.ok(response.headers.get('Content-Type')?.startsWith('text/html'))\n      assert.ok(text.includes('<html'))\n      assert.ok(text.includes('UNPKG'))\n    })\n\n    it('includes example links', async () => {\n      let response = await router.fetch(new Request('http://localhost/'))\n      let text = await response.text()\n\n      assert.ok(text.includes('@remix-run/cookie'))\n      assert.ok(text.includes('/react'))\n    })\n  })\n\n  describe('browse route', () => {\n    it('returns 404 for invalid package path', async () => {\n      // A path like \"@@invalid\" is not a valid npm package name\n      let response = await router.fetch(new Request('http://localhost/@@invalid'))\n      let text = await response.text()\n\n      assert.equal(response.status, 404)\n      assert.ok(text.includes('Invalid path'))\n    })\n\n    it('returns 404 for non-existent package', async () => {\n      let response = await router.fetch(new Request('http://localhost/non-existent-package'))\n      let text = await response.text()\n\n      assert.equal(response.status, 404)\n      assert.ok(text.includes('not found'))\n    })\n\n    it('returns package directory listing for valid package', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@7.0.0'))\n      let text = await response.text()\n\n      assert.equal(response.status, 200)\n      assert.ok(text.includes('is-number'))\n      assert.ok(text.includes('package.json'))\n    })\n\n    it('returns file content for valid file path', async () => {\n      let response = await router.fetch(\n        new Request('http://localhost/is-number@7.0.0/package.json'),\n      )\n      let text = await response.text()\n\n      assert.equal(response.status, 200)\n      // Content is HTML-escaped, so \"name\" becomes &quot;name&quot;\n      assert.ok(text.includes('&quot;name&quot;'))\n      assert.ok(text.includes('is-number'))\n    })\n\n    it('shows index.js in package root', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@7.0.0'))\n      let text = await response.text()\n\n      assert.equal(response.status, 200)\n      assert.ok(text.includes('index.js'))\n    })\n  })\n\n  describe('POST requests', () => {\n    it('returns 404 for non-GET requests', async () => {\n      let response = await router.fetch(new Request('http://localhost/', { method: 'POST' }))\n\n      assert.equal(response.status, 404)\n    })\n  })\n\n  describe('version redirects', () => {\n    it('redirects package without version to latest', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@7.0.0')\n    })\n\n    it('redirects dist-tag \"latest\" to resolved version', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@latest'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@7.0.0')\n    })\n\n    it('redirects partial major version to highest match', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@1'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@1.1.2')\n    })\n\n    it('redirects partial major.minor version to highest match', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@2.0'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@2.0.2')\n    })\n\n    it('redirects caret semver range to highest compatible version', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@^1.0.0'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@1.1.2')\n    })\n\n    it('redirects tilde semver range to highest patch version', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@~1.1.0'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@1.1.2')\n    })\n\n    it('redirects URL-encoded caret range', async () => {\n      // %5E is URL-encoded ^\n      let response = await router.fetch(new Request('http://localhost/is-number@%5E2.0.0'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@2.1.0')\n    })\n\n    it('redirects URL-encoded tilde range', async () => {\n      // %7E is URL-encoded ~\n      let response = await router.fetch(new Request('http://localhost/is-number@%7E2.0.0'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@2.0.2')\n    })\n\n    it('redirects complex semver range', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@>=1.0.0 <2.0.0'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@1.1.2')\n    })\n\n    it('preserves file path in redirect', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@^7/package.json'))\n\n      assert.equal(response.status, 302)\n      assert.equal(response.headers.get('Location'), '/is-number@7.0.0/package.json')\n    })\n\n    it('does not redirect fully resolved version', async () => {\n      let response = await router.fetch(new Request('http://localhost/is-number@7.0.0'))\n\n      // Should return 200 (directory listing), not a redirect\n      assert.equal(response.status, 200)\n      let text = await response.text()\n      assert.ok(text.includes('is-number'))\n    })\n  })\n})\n"
  },
  {
    "path": "demos/unpkg/app/router.ts",
    "content": "import { createRouter } from 'remix/fetch-router'\nimport { createRedirectResponse as redirect } from 'remix/response/redirect'\n\nimport { routes } from './routes.ts'\nimport { renderFileContent } from './file-content.ts'\nimport { renderDirectoryListing } from './directory.ts'\nimport { renderError } from './error.ts'\nimport {\n  parsePackagePath,\n  fetchPackageMetadata,\n  fetchPackageContents,\n  getFilesAtPath,\n  isFullyResolvedVersion,\n  resolveVersion,\n  PackageNotFoundError,\n  VersionNotFoundError,\n  InvalidPathError,\n} from './utils/npm.ts'\nimport { html, render } from './utils/render.ts'\n\nexport let router = createRouter()\n\nrouter.map(routes, {\n  actions: {\n    home() {\n      return render(\n        'UNPKG - npm package browser',\n        html`\n          <h1><span class=\"brand\">UNPKG</span></h1>\n          <div class=\"home-content\">\n            <p>Browse the contents of any npm package by entering its name in the URL.</p>\n            <p>\n              For example, visit <code>/lodash</code> to browse the latest version of lodash, or\n              <code>/react@18</code> to browse React version 18.\n            </p>\n            <p>\n              You can also browse specific files by adding the file path, like\n              <code>/lodash/package.json</code>.\n            </p>\n\n            <div class=\"examples\">\n              <h2>Try these packages:</h2>\n              <ul>\n                <li><a href=\"/@remix-run/cookie\">@remix-run/cookie</a> - scoped package</li>\n                <li><a href=\"/react\">react</a> - UI library</li>\n                <li><a href=\"/express\">express</a> - web framework</li>\n                <li><a href=\"/typescript@5\">typescript@5</a> - specific major version</li>\n              </ul>\n            </div>\n          </div>\n        `,\n      )\n    },\n\n    async browse({ params }) {\n      let path = params.path ?? ''\n\n      if (!path) {\n        return redirect('/')\n      }\n\n      try {\n        let { name, version, filePath } = parsePackagePath(path)\n\n        let metadata = await fetchPackageMetadata(name)\n\n        if (!isFullyResolvedVersion(metadata, version)) {\n          let resolvedVersion = resolveVersion(metadata, version)\n          let redirectUrl = `/${name}@${resolvedVersion}${filePath ? '/' + filePath : ''}`\n          return redirect(redirectUrl)\n        }\n\n        let contents = await fetchPackageContents(name, version)\n        let resolvedVersion = contents.metadata.version\n\n        let file = contents.files.get(filePath)\n\n        if (filePath && file?.type === 'file') {\n          let fileData = await contents.getFileContent(filePath)\n          if (!fileData) {\n            return renderError(\n              'File not found',\n              `The file \"${filePath}\" was not found in the package.`,\n            )\n          }\n          return renderFileContent(name, resolvedVersion, filePath, file, fileData)\n        }\n\n        let files = getFilesAtPath(contents.files, filePath)\n        return renderDirectoryListing(name, resolvedVersion, filePath, files)\n      } catch (error) {\n        if (error instanceof PackageNotFoundError) {\n          return renderError(\n            'Package not found',\n            `The package \"${error.packageName}\" was not found on npm.`,\n          )\n        }\n        if (error instanceof VersionNotFoundError) {\n          return renderError(\n            'Version not found',\n            `Version \"${error.version}\" of package \"${error.packageName}\" was not found.`,\n          )\n        }\n        if (error instanceof InvalidPathError) {\n          return renderError('Invalid path', `The path \"${error.path}\" is not valid.`)\n        }\n        throw error\n      }\n    },\n  },\n})\n"
  },
  {
    "path": "demos/unpkg/app/routes.ts",
    "content": "import { get, route } from 'remix/fetch-router/routes'\n\nexport let routes = route({\n  // Home page with instructions\n  home: get('/'),\n\n  // Package browser - handles all package paths\n  // Examples:\n  //   /lodash\n  //   /lodash@4.17.21\n  //   /lodash@4.17.21/package.json\n  //   /@remix-run/cookie\n  //   /@remix-run/cookie@1.0.0/src/index.ts\n  browse: get('/*path'),\n})\n"
  },
  {
    "path": "demos/unpkg/app/utils/cache.ts",
    "content": "import * as os from 'node:os'\nimport * as path from 'node:path'\nimport { createFsFileStorage } from 'remix/file-storage/fs'\n\nlet cacheDir = path.join(os.tmpdir(), 'unpkg-cache')\n\nexport let tarballCache = createFsFileStorage(cacheDir)\n\n/**\n * Get a cache key for a package tarball.\n */\nexport function getTarballCacheKey(packageName: string, version: string): string {\n  // Replace @ and / in scoped package names to make valid cache keys\n  let safeName = packageName.replace(/\\//g, '-').replace(/^@/, '')\n  return `${safeName}@${version}.tgz`\n}\n"
  },
  {
    "path": "demos/unpkg/app/utils/npm.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport {\n  parsePackagePath,\n  resolveVersion,\n  isFullyResolvedVersion,\n  getFilesAtPath,\n  PackageNotFoundError,\n  VersionNotFoundError,\n  InvalidPathError,\n  type PackageMetadata,\n  type PackageFile,\n} from './npm.ts'\n\ndescribe('parsePackagePath', () => {\n  it('parses simple package name', () => {\n    let result = parsePackagePath('lodash')\n    assert.deepEqual(result, { name: 'lodash', version: 'latest', filePath: '' })\n  })\n\n  it('parses package name with version', () => {\n    let result = parsePackagePath('lodash@4.17.21')\n    assert.deepEqual(result, { name: 'lodash', version: '4.17.21', filePath: '' })\n  })\n\n  it('parses package name with partial version', () => {\n    let result = parsePackagePath('react@18')\n    assert.deepEqual(result, { name: 'react', version: '18', filePath: '' })\n  })\n\n  it('parses package with file path', () => {\n    let result = parsePackagePath('lodash/package.json')\n    assert.deepEqual(result, { name: 'lodash', version: 'latest', filePath: 'package.json' })\n  })\n\n  it('parses package with version and file path', () => {\n    let result = parsePackagePath('lodash@4.17.21/package.json')\n    assert.deepEqual(result, { name: 'lodash', version: '4.17.21', filePath: 'package.json' })\n  })\n\n  it('parses package with nested file path', () => {\n    let result = parsePackagePath('lodash@4/lib/utils.js')\n    assert.deepEqual(result, { name: 'lodash', version: '4', filePath: 'lib/utils.js' })\n  })\n\n  it('parses scoped package name', () => {\n    let result = parsePackagePath('@remix-run/cookie')\n    assert.deepEqual(result, { name: '@remix-run/cookie', version: 'latest', filePath: '' })\n  })\n\n  it('parses scoped package with version', () => {\n    let result = parsePackagePath('@remix-run/cookie@1.0.0')\n    assert.deepEqual(result, { name: '@remix-run/cookie', version: '1.0.0', filePath: '' })\n  })\n\n  it('parses scoped package with file path', () => {\n    let result = parsePackagePath('@remix-run/cookie/src/index.ts')\n    assert.deepEqual(result, {\n      name: '@remix-run/cookie',\n      version: 'latest',\n      filePath: 'src/index.ts',\n    })\n  })\n\n  it('parses scoped package with version and file path', () => {\n    let result = parsePackagePath('@remix-run/cookie@1.0.0/src/lib/cookie.ts')\n    assert.deepEqual(result, {\n      name: '@remix-run/cookie',\n      version: '1.0.0',\n      filePath: 'src/lib/cookie.ts',\n    })\n  })\n\n  it('throws for invalid scoped package path', () => {\n    assert.throws(() => parsePackagePath('@remix-run'), {\n      name: 'InvalidPathError',\n    })\n  })\n\n  it('parses URL-encoded caret semver range', () => {\n    let result = parsePackagePath('react@%5E18.2')\n    assert.deepEqual(result, { name: 'react', version: '^18.2', filePath: '' })\n  })\n\n  it('parses URL-encoded tilde semver range', () => {\n    let result = parsePackagePath('lodash@%7E4.17')\n    assert.deepEqual(result, { name: 'lodash', version: '~4.17', filePath: '' })\n  })\n\n  it('parses unencoded semver range', () => {\n    let result = parsePackagePath('react@^18.2')\n    assert.deepEqual(result, { name: 'react', version: '^18.2', filePath: '' })\n  })\n})\n\ndescribe('resolveVersion', () => {\n  let mockMetadata: PackageMetadata = {\n    name: 'test-package',\n    'dist-tags': {\n      latest: '2.0.0',\n      beta: '3.0.0-beta.1',\n      next: '2.1.0-alpha.1',\n    },\n    versions: {\n      '1.0.0': {\n        name: 'test-package',\n        version: '1.0.0',\n        dist: { tarball: 'http://example.com/1.0.0.tgz', shasum: 'abc' },\n      },\n      '1.0.1': {\n        name: 'test-package',\n        version: '1.0.1',\n        dist: { tarball: 'http://example.com/1.0.1.tgz', shasum: 'def' },\n      },\n      '1.1.0': {\n        name: 'test-package',\n        version: '1.1.0',\n        dist: { tarball: 'http://example.com/1.1.0.tgz', shasum: 'ghi' },\n      },\n      '2.0.0': {\n        name: 'test-package',\n        version: '2.0.0',\n        dist: { tarball: 'http://example.com/2.0.0.tgz', shasum: 'jkl' },\n      },\n      '3.0.0-beta.1': {\n        name: 'test-package',\n        version: '3.0.0-beta.1',\n        dist: { tarball: 'http://example.com/3.0.0-beta.1.tgz', shasum: 'mno' },\n      },\n      '2.1.0-alpha.1': {\n        name: 'test-package',\n        version: '2.1.0-alpha.1',\n        dist: { tarball: 'http://example.com/2.1.0-alpha.1.tgz', shasum: 'pqr' },\n      },\n    },\n  }\n\n  it('resolves dist-tag to version', () => {\n    assert.equal(resolveVersion(mockMetadata, 'latest'), '2.0.0')\n    assert.equal(resolveVersion(mockMetadata, 'beta'), '3.0.0-beta.1')\n    assert.equal(resolveVersion(mockMetadata, 'next'), '2.1.0-alpha.1')\n  })\n\n  it('resolves exact version', () => {\n    assert.equal(resolveVersion(mockMetadata, '1.0.0'), '1.0.0')\n    assert.equal(resolveVersion(mockMetadata, '2.0.0'), '2.0.0')\n  })\n\n  it('resolves partial major version to highest match', () => {\n    assert.equal(resolveVersion(mockMetadata, '1'), '1.1.0')\n  })\n\n  it('resolves partial major.minor version to highest match', () => {\n    assert.equal(resolveVersion(mockMetadata, '1.0'), '1.0.1')\n  })\n\n  it('throws for non-existent version', () => {\n    assert.throws(() => resolveVersion(mockMetadata, '5.0.0'), {\n      name: 'VersionNotFoundError',\n    })\n  })\n\n  it('throws for non-existent partial version', () => {\n    assert.throws(() => resolveVersion(mockMetadata, '4'), {\n      name: 'VersionNotFoundError',\n    })\n  })\n\n  it('resolves caret range to highest matching version', () => {\n    assert.equal(resolveVersion(mockMetadata, '^1.0.0'), '1.1.0')\n  })\n\n  it('resolves tilde range to highest matching version', () => {\n    assert.equal(resolveVersion(mockMetadata, '~1.0.0'), '1.0.1')\n  })\n\n  it('resolves greater-than-or-equal range', () => {\n    assert.equal(resolveVersion(mockMetadata, '>=1.0.0'), '2.0.0')\n  })\n\n  it('resolves complex semver range', () => {\n    assert.equal(resolveVersion(mockMetadata, '>=1.0.0 <2.0.0'), '1.1.0')\n  })\n\n  it('throws for semver range with no matching version', () => {\n    assert.throws(() => resolveVersion(mockMetadata, '^5.0.0'), {\n      name: 'VersionNotFoundError',\n    })\n  })\n})\n\ndescribe('isFullyResolvedVersion', () => {\n  let mockMetadata: PackageMetadata = {\n    name: 'test-package',\n    'dist-tags': {\n      latest: '2.0.0',\n    },\n    versions: {\n      '1.0.0': {\n        name: 'test-package',\n        version: '1.0.0',\n        dist: { tarball: 'http://example.com/1.0.0.tgz', shasum: 'abc' },\n      },\n      '2.0.0': {\n        name: 'test-package',\n        version: '2.0.0',\n        dist: { tarball: 'http://example.com/2.0.0.tgz', shasum: 'def' },\n      },\n    },\n  }\n\n  it('returns true for exact version in versions', () => {\n    assert.equal(isFullyResolvedVersion(mockMetadata, '1.0.0'), true)\n    assert.equal(isFullyResolvedVersion(mockMetadata, '2.0.0'), true)\n  })\n\n  it('returns false for dist-tag', () => {\n    assert.equal(isFullyResolvedVersion(mockMetadata, 'latest'), false)\n  })\n\n  it('returns false for partial version', () => {\n    assert.equal(isFullyResolvedVersion(mockMetadata, '1'), false)\n    assert.equal(isFullyResolvedVersion(mockMetadata, '1.0'), false)\n  })\n\n  it('returns false for semver range', () => {\n    assert.equal(isFullyResolvedVersion(mockMetadata, '^1.0.0'), false)\n    assert.equal(isFullyResolvedVersion(mockMetadata, '~1.0.0'), false)\n    assert.equal(isFullyResolvedVersion(mockMetadata, '>=1.0.0'), false)\n  })\n\n  it('returns false for non-existent version', () => {\n    assert.equal(isFullyResolvedVersion(mockMetadata, '3.0.0'), false)\n  })\n})\n\ndescribe('getFilesAtPath', () => {\n  let mockFiles: Map<string, PackageFile> = new Map([\n    ['dist', { name: 'dist', path: 'dist', size: 0, type: 'directory' }],\n    ['src', { name: 'src', path: 'src', size: 0, type: 'directory' }],\n    ['LICENSE', { name: 'LICENSE', path: 'LICENSE', size: 1024, type: 'file' }],\n    ['package.json', { name: 'package.json', path: 'package.json', size: 512, type: 'file' }],\n    ['README.md', { name: 'README.md', path: 'README.md', size: 2048, type: 'file' }],\n    ['src/index.ts', { name: 'index.ts', path: 'src/index.ts', size: 256, type: 'file' }],\n    ['src/lib', { name: 'lib', path: 'src/lib', size: 0, type: 'directory' }],\n    ['src/lib/utils.ts', { name: 'utils.ts', path: 'src/lib/utils.ts', size: 128, type: 'file' }],\n    ['dist/index.js', { name: 'index.js', path: 'dist/index.js', size: 384, type: 'file' }],\n    ['dist/index.d.ts', { name: 'index.d.ts', path: 'dist/index.d.ts', size: 192, type: 'file' }],\n  ])\n\n  it('lists root level files and directories', () => {\n    let files = getFilesAtPath(mockFiles, '')\n    assert.equal(files.length, 5)\n    // Directories first, then alphabetically\n    assert.deepEqual(\n      files.map((f) => f.name),\n      ['dist', 'src', 'LICENSE', 'package.json', 'README.md'],\n    )\n  })\n\n  it('lists files in src directory', () => {\n    let files = getFilesAtPath(mockFiles, 'src')\n    assert.equal(files.length, 2)\n    assert.deepEqual(\n      files.map((f) => f.name),\n      ['lib', 'index.ts'],\n    )\n  })\n\n  it('lists files in nested directory', () => {\n    let files = getFilesAtPath(mockFiles, 'src/lib')\n    assert.equal(files.length, 1)\n    assert.equal(files[0].name, 'utils.ts')\n  })\n\n  it('lists files in dist directory', () => {\n    let files = getFilesAtPath(mockFiles, 'dist')\n    assert.equal(files.length, 2)\n    assert.deepEqual(\n      files.map((f) => f.name),\n      ['index.d.ts', 'index.js'],\n    )\n  })\n\n  it('returns empty for non-existent directory', () => {\n    let files = getFilesAtPath(mockFiles, 'nonexistent')\n    assert.equal(files.length, 0)\n  })\n\n  it('handles trailing slash in path', () => {\n    let files = getFilesAtPath(mockFiles, 'src/')\n    assert.equal(files.length, 2)\n  })\n\n  it('handles leading slash in path', () => {\n    let files = getFilesAtPath(mockFiles, '/src')\n    assert.equal(files.length, 2)\n  })\n\n  it('sorts directories before files', () => {\n    let files = getFilesAtPath(mockFiles, '')\n    let dirCount = files.filter((f) => f.type === 'directory').length\n    assert.equal(dirCount, 2)\n    // First 2 should be directories\n    assert.equal(files[0].type, 'directory')\n    assert.equal(files[1].type, 'directory')\n    // Rest should be files\n    assert.equal(files[2].type, 'file')\n  })\n})\n\ndescribe('Error classes', () => {\n  it('PackageNotFoundError has correct properties', () => {\n    let error = new PackageNotFoundError('my-package')\n    assert.equal(error.name, 'PackageNotFoundError')\n    assert.equal(error.packageName, 'my-package')\n    assert.equal(error.message, 'Package not found: my-package')\n  })\n\n  it('VersionNotFoundError has correct properties', () => {\n    let error = new VersionNotFoundError('my-package', '5.0.0')\n    assert.equal(error.name, 'VersionNotFoundError')\n    assert.equal(error.packageName, 'my-package')\n    assert.equal(error.version, '5.0.0')\n    assert.equal(error.message, 'Version not found: my-package@5.0.0')\n  })\n\n  it('InvalidPathError has correct properties', () => {\n    let error = new InvalidPathError('@incomplete')\n    assert.equal(error.name, 'InvalidPathError')\n    assert.equal(error.path, '@incomplete')\n    assert.equal(error.message, 'Invalid package path: @incomplete')\n  })\n})\n"
  },
  {
    "path": "demos/unpkg/app/utils/npm.ts",
    "content": "import * as zlib from 'node:zlib'\nimport { parseTar, type TarEntry } from 'remix/tar-parser'\nimport * as semver from 'semver'\n\nimport { tarballCache, getTarballCacheKey } from './cache.ts'\n\nconst NPM_REGISTRY = 'https://registry.npmjs.org'\n\nexport interface PackageMetadata {\n  name: string\n  'dist-tags': Record<string, string>\n  versions: Record<string, PackageVersionMetadata>\n}\n\nexport interface PackageVersionMetadata {\n  name: string\n  version: string\n  dist: {\n    tarball: string\n    shasum: string\n  }\n}\n\nexport interface PackageFile {\n  name: string\n  path: string\n  size: number\n  type: 'file' | 'directory'\n}\n\nexport interface PackageContents {\n  metadata: PackageVersionMetadata\n  files: Map<string, PackageFile>\n  getFileContent: (path: string) => Promise<Uint8Array | null>\n}\n\n/**\n * Fetch package metadata from npm registry.\n */\nexport async function fetchPackageMetadata(packageName: string): Promise<PackageMetadata> {\n  let url = `${NPM_REGISTRY}/${encodeURIComponent(packageName).replace('%40', '@')}`\n  let response = await fetch(url, {\n    headers: { Accept: 'application/json' },\n  })\n\n  if (!response.ok) {\n    if (response.status === 404) {\n      throw new PackageNotFoundError(packageName)\n    }\n    throw new Error(`Failed to fetch package metadata: ${response.status} ${response.statusText}`)\n  }\n\n  return response.json()\n}\n\n/**\n * Check if a version specifier is fully resolved (an exact version that exists).\n */\nexport function isFullyResolvedVersion(metadata: PackageMetadata, specifier: string): boolean {\n  return specifier in metadata.versions\n}\n\n/**\n * Resolve a version specifier to a concrete version.\n * Supports:\n * - Exact versions: \"1.2.3\"\n * - Partial versions: \"1\", \"1.2\" -> matches highest in range\n * - Semver ranges: \"^1.2.0\", \"~1.2.0\", \">=1.0.0 <2.0.0\"\n * - Dist tags: \"latest\", \"beta\"\n */\nexport function resolveVersion(metadata: PackageMetadata, specifier: string): string {\n  // Check if it's a dist-tag\n  if (specifier in metadata['dist-tags']) {\n    return metadata['dist-tags'][specifier]\n  }\n\n  // Check if it's an exact version\n  if (specifier in metadata.versions) {\n    return specifier\n  }\n\n  let versions = Object.keys(metadata.versions)\n\n  // Try to match as a semver range (^1.2.0, ~1.0.0, >=1.0.0, etc.)\n  if (semver.validRange(specifier)) {\n    let match = semver.maxSatisfying(versions, specifier)\n    if (match) {\n      return match\n    }\n  }\n\n  // Try to match as a partial version (e.g., \"18\" matches \"18.3.1\")\n  // Convert partial version to a range: \"18\" -> \"18.x\", \"18.2\" -> \"18.2.x\"\n  let partialRange = specifier + '.x'\n  if (semver.validRange(partialRange)) {\n    let match = semver.maxSatisfying(versions, partialRange)\n    if (match) {\n      return match\n    }\n  }\n\n  throw new VersionNotFoundError(metadata.name, specifier)\n}\n\n/**\n * Fetch and parse a package tarball, returning its contents.\n */\nexport async function fetchPackageContents(\n  packageName: string,\n  version: string,\n): Promise<PackageContents> {\n  let metadata = await fetchPackageMetadata(packageName)\n  let resolvedVersion = resolveVersion(metadata, version)\n  let versionMetadata = metadata.versions[resolvedVersion]\n\n  if (!versionMetadata) {\n    throw new VersionNotFoundError(packageName, version)\n  }\n\n  let tarballData = await fetchTarball(packageName, resolvedVersion, versionMetadata.dist.tarball)\n  let { files, contents } = await parseTarball(tarballData)\n\n  return {\n    metadata: versionMetadata,\n    files,\n    async getFileContent(path: string): Promise<Uint8Array | null> {\n      return contents.get(path) ?? null\n    },\n  }\n}\n\n/**\n * Fetch a tarball, using cache if available.\n */\nasync function fetchTarball(\n  packageName: string,\n  version: string,\n  tarballUrl: string,\n): Promise<Uint8Array> {\n  let cacheKey = getTarballCacheKey(packageName, version)\n\n  // Check cache first\n  let cached = await tarballCache.get(cacheKey)\n  if (cached) {\n    return new Uint8Array(await cached.arrayBuffer())\n  }\n\n  // Fetch from npm\n  let response = await fetch(tarballUrl)\n  if (!response.ok) {\n    throw new Error(`Failed to fetch tarball: ${response.status} ${response.statusText}`)\n  }\n\n  let compressedData = new Uint8Array(await response.arrayBuffer())\n\n  // Decompress using node:zlib (faster than DecompressionStream)\n  let decompressed = await gunzip(compressedData)\n\n  // Cache the decompressed tarball\n  let file = new File([decompressed.buffer as ArrayBuffer], `${packageName}@${version}.tar`, {\n    type: 'application/x-tar',\n  })\n  await tarballCache.set(cacheKey, file)\n\n  return decompressed\n}\n\n/**\n * Decompress gzip data using node:zlib.\n */\nfunction gunzip(data: Uint8Array): Promise<Uint8Array> {\n  return new Promise((resolve, reject) => {\n    zlib.gunzip(data, (err, result) => {\n      if (err) reject(err)\n      else resolve(new Uint8Array(result))\n    })\n  })\n}\n\n/**\n * Parse a tarball and extract file information and contents.\n */\nasync function parseTarball(\n  data: Uint8Array,\n): Promise<{ files: Map<string, PackageFile>; contents: Map<string, Uint8Array> }> {\n  let files = new Map<string, PackageFile>()\n  let contents = new Map<string, Uint8Array>()\n  let directories = new Set<string>()\n\n  await parseTar(data, async (entry: TarEntry) => {\n    // npm tarballs have a \"package/\" prefix\n    let name = entry.name.replace(/^package\\//, '')\n    if (!name) return\n\n    // Track directories from file paths\n    let parts = name.split('/')\n    for (let i = 1; i < parts.length; i++) {\n      let dirPath = parts.slice(0, i).join('/')\n      directories.add(dirPath)\n    }\n\n    if (entry.header.type === 'directory') {\n      // Remove trailing slash from directory name\n      name = name.replace(/\\/$/, '')\n      directories.add(name)\n    } else if (entry.header.type === 'file') {\n      files.set(name, {\n        name: parts[parts.length - 1],\n        path: name,\n        size: entry.size,\n        type: 'file',\n      })\n\n      // Store file contents\n      let bytes = await entry.bytes()\n      contents.set(name, bytes)\n    }\n  })\n\n  // Add directories to files map\n  for (let dirPath of directories) {\n    let parts = dirPath.split('/')\n    files.set(dirPath, {\n      name: parts[parts.length - 1],\n      path: dirPath,\n      size: 0,\n      type: 'directory',\n    })\n  }\n\n  return { files, contents }\n}\n\n/**\n * Get files at a specific directory level.\n */\nexport function getFilesAtPath(files: Map<string, PackageFile>, dirPath: string): PackageFile[] {\n  let normalizedDir = dirPath.replace(/^\\/+|\\/+$/g, '') // Remove leading/trailing slashes\n  let result: PackageFile[] = []\n  let seen = new Set<string>()\n\n  for (let [filePath, file] of files) {\n    // Skip if we've already seen this entry\n    if (seen.has(filePath)) continue\n\n    // Calculate the depth of this file\n    let fileDepth = filePath.split('/').length - 1\n\n    if (normalizedDir) {\n      // Looking inside a directory\n      if (!filePath.startsWith(normalizedDir + '/')) continue\n\n      // Get relative path from the directory\n      let relativePath = filePath.slice(normalizedDir.length + 1)\n      if (!relativePath) continue\n\n      // Only include direct children (no more slashes in relative path, except for dir trailing)\n      if (relativePath.includes('/') && file.type === 'file') continue\n      if (relativePath.includes('/')) continue\n    } else {\n      // Root level - only show top-level entries\n      if (fileDepth !== 0) continue\n    }\n\n    seen.add(filePath)\n    result.push(file)\n  }\n\n  // Sort: directories first, then alphabetically\n  result.sort((a, b) => {\n    if (a.type !== b.type) {\n      return a.type === 'directory' ? -1 : 1\n    }\n    return a.name.localeCompare(b.name)\n  })\n\n  return result\n}\n\n/**\n * Parse a package path from the URL.\n * Examples:\n *   \"lodash\" -> { name: \"lodash\", version: \"latest\", filePath: \"\" }\n *   \"lodash@4.17.21\" -> { name: \"lodash\", version: \"4.17.21\", filePath: \"\" }\n *   \"lodash@4/package.json\" -> { name: \"lodash\", version: \"4\", filePath: \"package.json\" }\n *   \"@remix-run/cookie\" -> { name: \"@remix-run/cookie\", version: \"latest\", filePath: \"\" }\n *   \"@remix-run/cookie@1.0.0/src/index.ts\" -> { name: \"@remix-run/cookie\", version: \"1.0.0\", filePath: \"src/index.ts\" }\n *   \"react@^18.2\" -> { name: \"react\", version: \"^18.2\", filePath: \"\" } (semver range)\n */\nexport function parsePackagePath(path: string): {\n  name: string\n  version: string\n  filePath: string\n} {\n  // Decode URL-encoded characters (e.g., %5E -> ^, %7E -> ~)\n  let decodedPath = decodeURIComponent(path)\n  let parts = decodedPath.split('/')\n  let name: string\n  let rest: string[]\n\n  // Handle scoped packages (@scope/name)\n  if (path.startsWith('@')) {\n    if (parts.length < 2) {\n      throw new InvalidPathError(path)\n    }\n    name = parts[0] + '/' + parts[1]\n    rest = parts.slice(2)\n  } else {\n    name = parts[0]\n    rest = parts.slice(1)\n  }\n\n  // Extract version from name if present (name@version)\n  let version = 'latest'\n  let atIndex = name.lastIndexOf('@')\n  if (atIndex > 0) {\n    // Not at position 0 (which would be scoped package start)\n    version = name.slice(atIndex + 1)\n    name = name.slice(0, atIndex)\n  }\n\n  let filePath = rest.join('/')\n\n  return { name, version, filePath }\n}\n\nexport class PackageNotFoundError extends Error {\n  packageName: string\n\n  constructor(packageName: string) {\n    super(`Package not found: ${packageName}`)\n    this.name = 'PackageNotFoundError'\n    this.packageName = packageName\n  }\n}\n\nexport class VersionNotFoundError extends Error {\n  packageName: string\n  version: string\n\n  constructor(packageName: string, version: string) {\n    super(`Version not found: ${packageName}@${version}`)\n    this.name = 'VersionNotFoundError'\n    this.packageName = packageName\n    this.version = version\n  }\n}\n\nexport class InvalidPathError extends Error {\n  path: string\n\n  constructor(path: string) {\n    super(`Invalid package path: ${path}`)\n    this.name = 'InvalidPathError'\n    this.path = path\n  }\n}\n"
  },
  {
    "path": "demos/unpkg/app/utils/render.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { formatBytes } from './render.ts'\n\ndescribe('formatBytes', () => {\n  it('returns dash for zero bytes', () => {\n    assert.equal(formatBytes(0), '-')\n  })\n\n  it('formats bytes', () => {\n    assert.equal(formatBytes(1), '1 B')\n    assert.equal(formatBytes(100), '100 B')\n    assert.equal(formatBytes(1023), '1023 B')\n  })\n\n  it('formats kilobytes', () => {\n    assert.equal(formatBytes(1024), '1.0 kB')\n    assert.equal(formatBytes(1536), '1.5 kB')\n    assert.equal(formatBytes(10240), '10.0 kB')\n    assert.equal(formatBytes(1024 * 1023), '1023.0 kB')\n  })\n\n  it('formats megabytes', () => {\n    assert.equal(formatBytes(1024 * 1024), '1.0 MB')\n    assert.equal(formatBytes(1024 * 1024 * 2.5), '2.5 MB')\n    assert.equal(formatBytes(1024 * 1024 * 100), '100.0 MB')\n  })\n})\n"
  },
  {
    "path": "demos/unpkg/app/utils/render.ts",
    "content": "import { html, type SafeHtml } from 'remix/html-template'\nimport { createHtmlResponse } from 'remix/response/html'\n\nexport { html }\n\nconst styles = /* css */ `\n  * {\n    box-sizing: border-box;\n  }\n\n  body {\n    font-family: Helvetica, Arial, sans-serif;\n    line-height: 1.5;\n    color: #24292f;\n    background: #fff;\n    margin: 0;\n    padding: 20px;\n    max-width: 1000px;\n    margin: 0 auto;\n  }\n\n  a {\n    color: #0969da;\n    text-decoration: none;\n  }\n\n  a:hover {\n    text-decoration: underline;\n  }\n\n  h1 {\n    font-size: 1.5rem;\n    font-weight: 600;\n    margin: 0 0 1rem;\n    border-bottom: 1px solid #d0d7de;\n    padding-bottom: 0.5rem;\n  }\n\n  .brand {\n    font-weight: 700;\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n  }\n\n  .breadcrumb {\n    font-size: 0.875rem;\n    margin-bottom: 1rem;\n    color: #57606a;\n  }\n\n  .breadcrumb a {\n    color: #0969da;\n  }\n\n  .file-browser {\n    border: 1px solid #d0d7de;\n    border-radius: 6px;\n    overflow: hidden;\n  }\n\n  .file-browser table {\n    width: 100%;\n    border-collapse: collapse;\n    font-size: 0.875rem;\n  }\n\n  .file-browser th {\n    text-align: left;\n    padding: 8px 16px;\n    background: #f6f8fa;\n    border-bottom: 1px solid #d0d7de;\n    font-weight: 600;\n  }\n\n  .file-browser td {\n    padding: 0;\n    border-bottom: 1px solid #d0d7de;\n  }\n\n  .file-browser tr:last-child td {\n    border-bottom: none;\n  }\n\n  .file-browser tbody tr:hover {\n    background: #f6f8fa;\n  }\n\n  .file-browser td a {\n    display: block;\n    padding: 8px 16px;\n    color: inherit;\n    text-decoration: none;\n  }\n\n  .file-browser td a:hover {\n    text-decoration: none;\n  }\n\n  .file-browser .name a {\n    color: #0969da;\n  }\n\n  .file-browser .size {\n    text-align: right;\n    width: 100px;\n  }\n\n  .file-browser .size a {\n    color: #57606a;\n  }\n\n  .file-browser .type {\n    width: 180px;\n    white-space: nowrap;\n  }\n\n  .file-browser .type a {\n    color: #57606a;\n  }\n\n  .file-browser .icon {\n    width: 32px;\n  }\n\n  .file-browser .icon a {\n    padding: 8px;\n    padding-right: 0;\n  }\n\n  .file-browser .icon svg {\n    display: block;\n    margin: 0 auto;\n  }\n\n  .icon-file, .icon-dir {\n    width: 16px;\n    height: 16px;\n  }\n\n  @media (max-width: 600px) {\n    body {\n      padding: 12px;\n    }\n\n    .file-browser .type,\n    .file-browser th.type {\n      display: none;\n    }\n\n    .file-browser .icon {\n      width: 24px;\n    }\n\n    .file-browser .icon a {\n      padding: 8px 4px;\n    }\n\n    .file-browser td a {\n      padding: 8px 12px;\n    }\n\n    .file-browser th {\n      padding: 8px 12px;\n    }\n\n    .file-browser .size {\n      width: 70px;\n    }\n  }\n\n  .file-content {\n    border: 1px solid #d0d7de;\n    border-radius: 6px;\n    overflow: hidden;\n  }\n\n  .file-content pre {\n    margin: 0;\n    padding: 16px;\n    overflow-x: auto;\n    font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n    font-size: 0.8125rem;\n    line-height: 1.45;\n    background: #f6f8fa;\n  }\n\n  .file-content img {\n    max-width: 100%;\n    padding: 16px;\n  }\n\n  .error {\n    padding: 16px;\n    background: #ffebe9;\n    border: 1px solid #ff8182;\n    border-radius: 6px;\n    color: #cf222e;\n  }\n\n  .info {\n    padding: 16px;\n    background: #ddf4ff;\n    border: 1px solid #54aeff;\n    border-radius: 6px;\n    color: #0969da;\n  }\n\n  .home-content {\n    max-width: 600px;\n  }\n\n  .home-content p {\n    margin: 1rem 0;\n    color: #57606a;\n  }\n\n  .home-content code {\n    font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n    background: #f6f8fa;\n    padding: 0.2em 0.4em;\n    border-radius: 3px;\n    font-size: 0.875em;\n  }\n\n  .examples {\n    margin-top: 2rem;\n  }\n\n  .examples h2 {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif;\n    font-size: 1rem;\n    font-weight: 600;\n    margin: 0 0 0.5rem;\n  }\n\n  .examples ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n  }\n\n  .examples li {\n    padding: 0.25rem 0;\n  }\n\n  .package-info {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif;\n    font-size: 0.875rem;\n    color: #57606a;\n    margin-bottom: 1rem;\n  }\n`\n\nexport function layout(title: string, content: SafeHtml): SafeHtml {\n  return html`\n    <html lang=\"en\">\n      <head>\n        <meta charset=\"UTF-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        <title>${title}</title>\n        <style>\n          ${styles}\n        </style>\n      </head>\n      <body>\n        ${content}\n      </body>\n    </html>\n  `\n}\n\nexport function render(title: string, content: SafeHtml, init?: ResponseInit): Response {\n  return createHtmlResponse(layout(title, content), init)\n}\n\nexport function formatBytes(bytes: number): string {\n  if (bytes === 0) return '-'\n  if (bytes < 1024) return bytes + ' B'\n  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' kB'\n  return (bytes / 1024 / 1024).toFixed(1) + ' MB'\n}\n\n// Minimal SVG icons\nexport let icons = {\n  file: html`<svg\n    class=\"icon-file\"\n    viewBox=\"0 0 16 16\"\n    fill=\"none\"\n    stroke=\"#57606a\"\n    stroke-width=\"1.5\"\n  >\n    <path d=\"M3 1.5h6.5L13 5v9.5H3z\" />\n    <path d=\"M9.5 1.5V5H13\" />\n  </svg>`,\n\n  directory: html`<svg\n    class=\"icon-dir\"\n    viewBox=\"0 0 16 16\"\n    fill=\"none\"\n    stroke=\"#57606a\"\n    stroke-width=\"1.5\"\n  >\n    <path d=\"M1.5 3.5h4l1.5 2h7.5v8h-13z\" />\n  </svg>`,\n}\n"
  },
  {
    "path": "demos/unpkg/package.json",
    "content": "{\n  \"name\": \"unpkg-demo\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"remix\": \"workspace:*\",\n    \"semver\": \"^7.6.3\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@types/semver\": \"^7.5.8\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"tsx\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"dev\": \"tsx watch server.ts\",\n    \"start\": \"tsx server.ts\",\n    \"test\": \"tsx --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "demos/unpkg/server.ts",
    "content": "import * as http from 'node:http'\nimport { createRequestListener } from 'remix/node-fetch-server'\n\nimport { router } from './app/router.ts'\n\nlet server = http.createServer(\n  createRequestListener(async (request) => {\n    try {\n      return await router.fetch(request)\n    } catch (error) {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  }),\n)\n\nlet port = process.env.PORT ? parseInt(process.env.PORT, 10) : 44100\n\nserver.listen(port, () => {\n  console.log(`unpkg demo is running on http://localhost:${port}`)\n})\n\nlet shuttingDown = false\n\nfunction shutdown() {\n  if (shuttingDown) return\n  shuttingDown = true\n  server.close(() => {\n    process.exit(0)\n  })\n}\n\nprocess.on('SIGINT', shutdown)\nprocess.on('SIGTERM', shutdown)\n"
  },
  {
    "path": "demos/unpkg/test/fixtures/is-number-metadata.json",
    "content": "{\"_id\":\"is-number\",\"_rev\":\"43-3fe877efbeead423ee53a12d5ca44395\",\"name\":\"is-number\",\"description\":\"Returns true if a number or string value is a finite number. Useful for regex matches, parsing, user input, etc.\",\"dist-tags\":{\"latest\":\"7.0.0\"},\"versions\":{\"0.1.0\":{\"name\":\"is-number\",\"description\":\"Is the value a number?\",\"version\":\"0.1.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"licenses\":[{\"type\":\"MIT\",\"url\":\"https://github.com/jonschlinkert/is-number/blob/master/LICENSE-MIT\"}],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha -R spec\"},\"devDependencies\":{\"verb-tag-jscomments\":\">= 0.2.0\",\"verb\":\">= 0.2.6\",\"mocha\":\"*\"},\"keywords\":[\"docs\",\"documentation\",\"generate\",\"generator\",\"markdown\",\"templates\",\"verb\"],\"_id\":\"is-number@0.1.0\",\"_shasum\":\"4407a37aec259352affb5548886744ba4903f8bd\",\"_from\":\".\",\"_npmVersion\":\"1.4.9\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"}],\"dist\":{\"shasum\":\"4407a37aec259352affb5548886744ba4903f8bd\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-0.1.0.tgz\",\"integrity\":\"sha512-WWl3iAruYOePW5iMf0IMcPiTsOTOu/FFsPyfx/ywixw7VmN6dI3/8/rgo4pdN6kVVjC5ByZ0Zk8xHLGEUSCdYA==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEUCIQCGbjmJymNMyy+kX9QMWNiNDpyXcRARCBG/qWG5AH3BHwIgYPJPxD0DLlmMlQ82J2/kw9xjNGwnqmZwv1iu2dhCtQo=\"}]},\"directories\":{}},\"0.1.1\":{\"name\":\"is-number\",\"description\":\"Is the value a number? Has extensive tests.\",\"version\":\"0.1.1\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"licenses\":[{\"type\":\"MIT\",\"url\":\"https://github.com/jonschlinkert/is-number/blob/master/LICENSE-MIT\"}],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha -R spec\"},\"devDependencies\":{\"verb-tag-jscomments\":\">= 0.2.0\",\"verb\":\">= 0.2.6\",\"mocha\":\"*\"},\"keywords\":[\"coerce\",\"coercion\",\"integer\",\"is\",\"istype\",\"javascript\",\"math\",\"number\",\"test\",\"type\",\"typeof\",\"util\",\"utility\",\"value\"],\"_id\":\"is-number@0.1.1\",\"_shasum\":\"69a7af116963d47206ec9bd9b48a14216f1e3806\",\"_from\":\".\",\"_npmVersion\":\"1.4.9\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"}],\"dist\":{\"shasum\":\"69a7af116963d47206ec9bd9b48a14216f1e3806\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz\",\"integrity\":\"sha512-la5kPULwIgkSSaZj9w7/A1uHqOBAgOhDUKQ5CkfL8LZ4Si6r4+2D0hI6b4o60MW4Uj2yNJARWIZUDPxlvOYQcw==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEUCIGGOUnPaovBe5O7zmgOovRl4e6Dzx6aMobYRtKHOiWV+AiEA4Qufb/miuS6rDGp7Zd20/e0JoxviUgTnqu21xGF2DhY=\"}]},\"directories\":{}},\"1.0.0\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"1.0.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":{\"type\":\"MIT\",\"url\":\"https://github.com/jonschlinkert/is-number/blob/master/LICENSE-MIT\"},\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha -R spec\"},\"files\":[\"index.js\"],\"devDependencies\":{\"benchmarked\":\"^0.1.3\",\"chalk\":\"^0.5.1\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is number\",\"is\",\"is-number\",\"istype\",\"kind of\",\"math\",\"number\",\"test\",\"type\",\"typeof\",\"value\"],\"gitHead\":\"3183207ab31bb09c65ad8999c39090a3c0530526\",\"_id\":\"is-number@1.0.0\",\"_shasum\":\"de821e3936a8996badeb879ca6f93605e769d498\",\"_from\":\".\",\"_npmVersion\":\"1.4.28\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"}],\"dist\":{\"shasum\":\"de821e3936a8996badeb879ca6f93605e769d498\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-1.0.0.tgz\",\"integrity\":\"sha512-chlxkgJp4PZIiff6kUe/MWLp5+soELWNYA2IsOTus1YwKj8d9JZS6QsU7Ryqwhb1f4i0nQ5SsoUj6d5kGgq0KQ==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEUCIQD2AOAMGpyuvqtfmVFBdULwtBCwI1cATJ1E2SXCu7KrqAIgN3FVeMU5CjRuw21s50tzEalrZmgvbJxkjizEvjLl+Xk=\"}]},\"directories\":{}},\"1.1.0\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"1.1.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":{\"type\":\"MIT\",\"url\":\"https://github.com/jonschlinkert/is-number/blob/master/LICENSE-MIT\"},\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha -R spec\"},\"files\":[\"index.js\"],\"devDependencies\":{\"benchmarked\":\"^0.1.3\",\"chalk\":\"^0.5.1\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is number\",\"is\",\"is-number\",\"istype\",\"kind of\",\"math\",\"number\",\"test\",\"type\",\"typeof\",\"value\"],\"gitHead\":\"3183207ab31bb09c65ad8999c39090a3c0530526\",\"_id\":\"is-number@1.1.0\",\"_shasum\":\"620db9e22fded44d43d8e3e47044319083d31855\",\"_from\":\".\",\"_npmVersion\":\"1.4.28\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"}],\"dist\":{\"shasum\":\"620db9e22fded44d43d8e3e47044319083d31855\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-1.1.0.tgz\",\"integrity\":\"sha512-zRllTDrNKDRvVEhjEqDVkT9eKzC7HJ+9SFbfBdZnbjBVt9t0SwbEn0F/NSwdk5GkkEdKZA6ZQ0Hfd4akty5Pfg==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEYCIQDnVnl9/SgNEemmb1M0T1tlfm4YRTqvWw3ja961FsqrVAIhANd7v0o1uT3CVfC9pzaFfvmwZcKBnxD+UHWlKvlIU4SJ\"}]},\"directories\":{}},\"1.1.1\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"1.1.1\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":{\"type\":\"MIT\",\"url\":\"https://github.com/jonschlinkert/is-number/blob/master/LICENSE\"},\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"devDependencies\":{\"benchmarked\":\"^0.1.3\",\"chalk\":\"^0.5.1\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is number\",\"is\",\"is-number\",\"istype\",\"kind of\",\"math\",\"number\",\"test\",\"type\",\"typeof\",\"value\"],\"gitHead\":\"5184d76c622b97d45486340a85147c6d59d14e25\",\"_id\":\"is-number@1.1.1\",\"_shasum\":\"e393e7ec07c17770dbb67941e7f89c675feb110f\",\"_from\":\".\",\"_npmVersion\":\"2.5.1\",\"_nodeVersion\":\"0.12.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"}],\"dist\":{\"shasum\":\"e393e7ec07c17770dbb67941e7f89c675feb110f\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-1.1.1.tgz\",\"integrity\":\"sha512-3/f2eabVTjIGjSVaQIIv33g1d2tYdSUnCNJuPpHcjwRzx8A2IPtCB2ayK2R3aS7SCF/LHlTjdaRjNmfG+RpFlA==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEQCIA1MN/tZFRjbwiG0H63P4dD6Qsem1lVPlOlJuJfdJP8iAiAoX6Lk3FLlI0dqigZPFyFZi1CXFk0W8dN9yJw7P2IW+Q==\"}]},\"directories\":{}},\"1.1.2\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"1.1.2\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":{\"type\":\"MIT\",\"url\":\"https://github.com/jonschlinkert/is-number/blob/master/LICENSE\"},\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"devDependencies\":{\"benchmarked\":\"^0.1.3\",\"chalk\":\"^0.5.1\",\"mocha\":\"^2.1.0\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is number\",\"is\",\"is-number\",\"istype\",\"kind of\",\"math\",\"number\",\"test\",\"type\",\"typeof\",\"value\"],\"gitHead\":\"a902495bca1f471beaa8deb6193ba628bf80c0e4\",\"_id\":\"is-number@1.1.2\",\"_shasum\":\"9d82409f3a8a8beecf249b1bc7dada49829966e4\",\"_from\":\".\",\"_npmVersion\":\"2.5.1\",\"_nodeVersion\":\"0.12.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"}],\"dist\":{\"shasum\":\"9d82409f3a8a8beecf249b1bc7dada49829966e4\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-1.1.2.tgz\",\"integrity\":\"sha512-dRKHHq76sZAXFf823ziHIOx5fXbuV1IrR892LugLDmyEBVMZzGoske4sY6lf+l3YPH/VyWNiKzNDXAPiQhx9Yg==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEYCIQCpH37H0WboBtQM2deQbMGTh2WMRYWVosLWdpoz+wqyoQIhAIutpIYs8sNTY2MaxDpnJmvhoUfxrNVDIUs4+J5JffHf\"}]},\"directories\":{}},\"2.0.0\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"2.0.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":{\"type\":\"MIT\",\"url\":\"https://github.com/jonschlinkert/is-number/blob/master/LICENSE\"},\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"devDependencies\":{\"benchmarked\":\"^0.1.3\",\"chalk\":\"^0.5.1\",\"mocha\":\"^2.1.0\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is number\",\"is\",\"is-number\",\"istype\",\"kind of\",\"math\",\"number\",\"test\",\"type\",\"typeof\",\"value\"],\"gitHead\":\"d5ac0584ee9ae7bd9288220a39780f155b9ad4c8\",\"_id\":\"is-number@2.0.0\",\"_shasum\":\"451c78bfe6c427f37bc2a406226e0cde449f3b5a\",\"_from\":\".\",\"_npmVersion\":\"2.5.1\",\"_nodeVersion\":\"0.12.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"}],\"dist\":{\"shasum\":\"451c78bfe6c427f37bc2a406226e0cde449f3b5a\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-2.0.0.tgz\",\"integrity\":\"sha512-LRme8p0WzIN9lrlP9/wJKs6242c9KYvDj6JlW9N9Up28wFxzyLNyNtnWu1j9nIuoWliquNRQx6+/OZ5mI2yVpQ==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEQCIEJz5PkAW3hcxfgUxotR5OLxeQFXF+vEsj3qRlIMuZj9AiAH3YXd7HMtFrJZqPGBE7DOjTqFWYWiPbm696CW+v1zDA==\"}]},\"directories\":{}},\"2.0.1\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"2.0.1\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":{\"type\":\"MIT\",\"url\":\"https://github.com/jonschlinkert/is-number/blob/master/LICENSE\"},\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"devDependencies\":{\"benchmarked\":\"^0.1.3\",\"chalk\":\"^0.5.1\",\"mocha\":\"^2.1.0\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is number\",\"is\",\"is-number\",\"istype\",\"kind of\",\"math\",\"number\",\"test\",\"type\",\"typeof\",\"value\"],\"dependencies\":{\"kind-of\":\"^1.1.0\"},\"gitHead\":\"bb9b2e19a9aa2ed4b1e7a5d27e43417c8c9570c0\",\"_id\":\"is-number@2.0.1\",\"_shasum\":\"a3754e651f0df489f290ee0a1102c87cc5a0db02\",\"_from\":\".\",\"_npmVersion\":\"2.5.1\",\"_nodeVersion\":\"0.12.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"}],\"dist\":{\"shasum\":\"a3754e651f0df489f290ee0a1102c87cc5a0db02\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-2.0.1.tgz\",\"integrity\":\"sha512-lDXqMCs22DpNe5F5HqfrhwdCWXjlMccky+nhZ1+iFjnGvL7F7R7Q6Fd1FswLUrqueFab/6/k1Wd9LkIXJcBSWA==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEQCIBlSPRMBjwALDvd9fjWuuBR4PSdICIZh8q3IucTdXt6xAiA/4FirIMH2gtXtxkCZ/FLtO2+W5CJQVYHnvPGUY6nIaA==\"}]},\"directories\":{}},\"2.0.2\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"2.0.2\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":{\"type\":\"MIT\",\"url\":\"https://github.com/jonschlinkert/is-number/blob/master/LICENSE\"},\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"devDependencies\":{\"benchmarked\":\"^0.1.3\",\"chalk\":\"^0.5.1\",\"mocha\":\"^2.1.0\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is number\",\"is\",\"is-number\",\"istype\",\"kind of\",\"math\",\"number\",\"test\",\"type\",\"typeof\",\"value\"],\"dependencies\":{\"kind-of\":\"^1.1.0\"},\"gitHead\":\"63d5b26c793194bf7f341a7203e0e5568c753539\",\"_id\":\"is-number@2.0.2\",\"_shasum\":\"c7542a0f420610655834cd3825bc2f0eb72afe21\",\"_from\":\".\",\"_npmVersion\":\"2.5.1\",\"_nodeVersion\":\"0.12.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"}],\"dist\":{\"shasum\":\"c7542a0f420610655834cd3825bc2f0eb72afe21\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-2.0.2.tgz\",\"integrity\":\"sha512-MzbGCUNsyTujsn2O9E2wL8n65cXdeWV1Jgd8khIA8VuN1q9ExsYQD6CwUwX6i8VGNLQy1skQugZ5RkvM1vbeqA==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEUCIF9XsZ2zEA+6vg8VayjaUNxJIUDkYbvrvxC4uW8u0L3iAiEAgmolYRoJJEsPK3k30eWySMUIHqn3GHaYsWysUhMsqjs=\"}]},\"directories\":{}},\"2.1.0\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"2.1.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":\"MIT\",\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"dependencies\":{\"kind-of\":\"^3.0.2\"},\"devDependencies\":{\"benchmarked\":\"^0.1.3\",\"chalk\":\"^0.5.1\",\"mocha\":\"*\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is\",\"is number\",\"is-number\",\"istype\",\"kind of\",\"math\",\"number\",\"test\",\"type\",\"typeof\",\"value\"],\"verb\":{\"related\":{\"list\":[\"kind-of\",\"is-primitive\",\"even\",\"odd\",\"is-even\",\"is-odd\"]}},\"gitHead\":\"d06c6e2cc048d3cad016cb8dfb055bb14d86fffa\",\"_id\":\"is-number@2.1.0\",\"_shasum\":\"01fcbbb393463a548f2f466cce16dece49db908f\",\"_from\":\".\",\"_npmVersion\":\"3.3.6\",\"_nodeVersion\":\"5.0.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},{\"name\":\"doowb\",\"email\":\"brian.woodward@gmail.com\"}],\"dist\":{\"shasum\":\"01fcbbb393463a548f2f466cce16dece49db908f\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz\",\"integrity\":\"sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEUCIBsAMHxq3xfm/burDf3PmG8CCLKR4UncouEdSDdRwglAAiEAxMTfaixUr2K2XsrRDCg7R6dRYawh8RqjO/VkIJSFNA8=\"}]},\"directories\":{}},\"3.0.0\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"3.0.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"contributors\":[{\"name\":\"Charlike Mike Reagent\",\"url\":\"http://www.tunnckocore.tk\"},{\"name\":\"Jon Schlinkert\",\"email\":\"jon.schlinkert@sellside.com\",\"url\":\"http://twitter.com/jonschlinkert\"}],\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":\"MIT\",\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"dependencies\":{\"kind-of\":\"^3.0.2\"},\"devDependencies\":{\"benchmarked\":\"^0.2.5\",\"chalk\":\"^1.1.3\",\"gulp-format-md\":\"^0.1.10\",\"mocha\":\"^3.0.2\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is\",\"is-nan\",\"is-num\",\"is-number\",\"istype\",\"kind\",\"math\",\"nan\",\"num\",\"number\",\"numeric\",\"test\",\"type\",\"typeof\",\"value\"],\"verb\":{\"related\":{\"list\":[\"even\",\"is-even\",\"is-odd\",\"is-primitive\",\"kind-of\",\"odd\"]},\"toc\":false,\"layout\":\"default\",\"tasks\":[\"readme\"],\"plugins\":[\"gulp-format-md\"],\"lint\":{\"reflinks\":true},\"reflinks\":[\"verb\",\"verb-generate-readme\"]},\"gitHead\":\"af885e2e890b9ef0875edd2b117305119ee5bdc5\",\"_id\":\"is-number@3.0.0\",\"_shasum\":\"24fd6201a4782cf50561c810276afc7d12d71195\",\"_from\":\".\",\"_npmVersion\":\"3.10.3\",\"_nodeVersion\":\"6.3.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"maintainers\":[{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},{\"name\":\"doowb\",\"email\":\"brian.woodward@gmail.com\"}],\"dist\":{\"shasum\":\"24fd6201a4782cf50561c810276afc7d12d71195\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz\",\"integrity\":\"sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEUCIEpkuzJnkIAtuDMAI3VLqzFNwNANap4m9nyeUneW72kRAiEAtsctBWg1OtEaj2FSH+9ViALZU1O0hQFCVzwGMWHfVnk=\"}]},\"_npmOperationalInternal\":{\"host\":\"packages-12-west.internal.npmjs.com\",\"tmp\":\"tmp/is-number-3.0.0.tgz_1473555089490_0.21388969756662846\"},\"directories\":{}},\"4.0.0\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"4.0.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"contributors\":[{\"name\":\"Jon Schlinkert\",\"url\":\"http://twitter.com/jonschlinkert\"},{\"name\":\"tunnckoCore\",\"url\":\"https://i.am.charlike.online\"}],\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":\"MIT\",\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"devDependencies\":{\"benchmarked\":\"^2.0.0\",\"chalk\":\"^2.1.0\",\"gulp-format-md\":\"^1.0.0\",\"mocha\":\"^3.0.1\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is\",\"is-nan\",\"is-num\",\"is-number\",\"istype\",\"kind\",\"math\",\"nan\",\"num\",\"number\",\"numeric\",\"test\",\"type\",\"typeof\",\"value\"],\"verb\":{\"related\":{\"list\":[\"even\",\"is-even\",\"is-odd\",\"is-primitive\",\"kind-of\",\"odd\"]},\"toc\":false,\"layout\":\"default\",\"tasks\":[\"readme\"],\"plugins\":[\"gulp-format-md\"],\"lint\":{\"reflinks\":true}},\"gitHead\":\"0c6b15a88bc10cd47f67a09506399dfc9ddc075d\",\"_id\":\"is-number@4.0.0\",\"_npmVersion\":\"5.4.2\",\"_nodeVersion\":\"8.7.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"dist\":{\"integrity\":\"sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==\",\"shasum\":\"0026e37f5454d73e356dfe6564699867c6a7f0ff\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEQCIA4Ju8vfTa5OmCg3qagXNvXHR0wp4GYpv4Jrzzx1Tr6yAiANs8hwximKkGyecUuMhc9/ipdFOP6u5xqFY1kkP37rHQ==\"}]},\"maintainers\":[{\"email\":\"brian.woodward@gmail.com\",\"name\":\"doowb\"},{\"email\":\"github@sellside.com\",\"name\":\"jonschlinkert\"}],\"_npmOperationalInternal\":{\"host\":\"s3://npm-registry-packages\",\"tmp\":\"tmp/is-number-4.0.0.tgz_1508219035603_0.08690439746715128\"},\"directories\":{}},\"5.0.0\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"5.0.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"contributors\":[{\"name\":\"Jon Schlinkert\",\"url\":\"http://twitter.com/jonschlinkert\"},{\"name\":\"Olsten Larck\",\"url\":\"https://i.am.charlike.online\"},{\"name\":\"Rouven Weßling\",\"url\":\"www.rouvenwessling.de\"}],\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":\"MIT\",\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"devDependencies\":{\"benchmarked\":\"^2.0.0\",\"chalk\":\"^2.1.0\",\"gulp-format-md\":\"^1.0.0\",\"mocha\":\"^3.0.1\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is\",\"is-nan\",\"is-num\",\"is-number\",\"istype\",\"kind\",\"math\",\"nan\",\"num\",\"number\",\"numeric\",\"test\",\"type\",\"typeof\",\"value\"],\"verb\":{\"toc\":false,\"layout\":\"default\",\"tasks\":[\"readme\"],\"related\":{\"list\":[\"isobject\",\"is-plain-object\",\"is-primitive\",\"kind-of\"]},\"plugins\":[\"gulp-format-md\"],\"lint\":{\"reflinks\":true}},\"gitHead\":\"7ed43573445149edf10f56967e8d943520ebb1db\",\"_id\":\"is-number@5.0.0\",\"_npmVersion\":\"5.6.0\",\"_nodeVersion\":\"9.1.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"dist\":{\"integrity\":\"sha512-LmVHHP5dTJwrwZg2Jjqp7K5jpvcnYvYD1LMpvGadMsMv5+WXoDSLBQ0+zmuBJmuZGh2J2K845ygj/YukxUnr4A==\",\"shasum\":\"c393bc471e65de1a10a6abcb20efeb12d2b88166\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-5.0.0.tgz\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEUCIFRVCaHiES/v9QeFGm0LXGwQmnzQ7rRyYgSkAV0/RfEIAiEAovG90UvuzqTelvVuQdatpyiQ0H43CXLZupcrA2vb2Bs=\"}]},\"maintainers\":[{\"email\":\"me@rouvenwessling.de\",\"name\":\"realityking\"},{\"email\":\"brian.woodward@gmail.com\",\"name\":\"doowb\"},{\"email\":\"github@sellside.com\",\"name\":\"jonschlinkert\"}],\"_npmOperationalInternal\":{\"host\":\"s3://npm-registry-packages\",\"tmp\":\"tmp/is-number-5.0.0.tgz_1517195201147_0.7769706172402948\"},\"directories\":{}},\"6.0.0\":{\"name\":\"is-number\",\"description\":\"Returns true if the value is a number. comprehensive tests.\",\"version\":\"6.0.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"contributors\":[{\"name\":\"Jon Schlinkert\",\"url\":\"http://twitter.com/jonschlinkert\"},{\"name\":\"Olsten Larck\",\"url\":\"https://i.am.charlike.online\"},{\"name\":\"Rouven Weßling\",\"url\":\"www.rouvenwessling.de\"}],\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":\"MIT\",\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.10.0\"},\"scripts\":{\"test\":\"mocha\"},\"devDependencies\":{\"benchmarked\":\"^2.0.0\",\"chalk\":\"^2.1.0\",\"gulp-format-md\":\"^1.0.0\",\"mocha\":\"^3.0.1\"},\"keywords\":[\"check\",\"coerce\",\"coercion\",\"integer\",\"is\",\"is-nan\",\"is-num\",\"is-number\",\"istype\",\"kind\",\"math\",\"nan\",\"num\",\"number\",\"numeric\",\"test\",\"type\",\"typeof\",\"value\"],\"verb\":{\"toc\":false,\"layout\":\"default\",\"tasks\":[\"readme\"],\"related\":{\"list\":[\"isobject\",\"is-plain-object\",\"is-primitive\",\"kind-of\"]},\"plugins\":[\"gulp-format-md\"],\"lint\":{\"reflinks\":true}},\"gitHead\":\"b0953635829711e7dceec0eaa92bc56521a546eb\",\"_id\":\"is-number@6.0.0\",\"_npmVersion\":\"5.8.0\",\"_nodeVersion\":\"9.9.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"dist\":{\"integrity\":\"sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==\",\"shasum\":\"e6d15ad31fc262887cccf217ae5f9316f81b1995\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz\",\"fileCount\":4,\"unpackedSize\":8960,\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEUCIHw0y05qyjN2G0Uw0DINiC25SvWlVSgd2UD/ddJM8scPAiEArxOqwwCq3PKUhw7VEm21DI141kJs/ktPkaANlO5cGOs=\"}]},\"maintainers\":[{\"email\":\"brian.woodward@gmail.com\",\"name\":\"doowb\"},{\"email\":\"github@sellside.com\",\"name\":\"jonschlinkert\"},{\"email\":\"me@rouvenwessling.de\",\"name\":\"realityking\"}],\"directories\":{},\"_npmOperationalInternal\":{\"host\":\"s3://npm-registry-packages\",\"tmp\":\"tmp/is-number_6.0.0_1522515759840_0.005598684369539919\"},\"_hasShrinkwrap\":false},\"7.0.0\":{\"name\":\"is-number\",\"description\":\"Returns true if a number or string value is a finite number. Useful for regex matches, parsing, user input, etc.\",\"version\":\"7.0.0\",\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"contributors\":[{\"name\":\"Jon Schlinkert\",\"url\":\"http://twitter.com/jonschlinkert\"},{\"name\":\"Olsten Larck\",\"url\":\"https://i.am.charlike.online\"},{\"name\":\"Rouven Weßling\",\"url\":\"www.rouvenwessling.de\"}],\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/jonschlinkert/is-number.git\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"license\":\"MIT\",\"files\":[\"index.js\"],\"main\":\"index.js\",\"engines\":{\"node\":\">=0.12.0\"},\"scripts\":{\"test\":\"mocha\"},\"devDependencies\":{\"ansi\":\"^0.3.1\",\"benchmark\":\"^2.1.4\",\"gulp-format-md\":\"^1.0.0\",\"mocha\":\"^3.5.3\"},\"keywords\":[\"cast\",\"check\",\"coerce\",\"coercion\",\"finite\",\"integer\",\"is\",\"isnan\",\"is-nan\",\"is-num\",\"is-number\",\"isnumber\",\"isfinite\",\"istype\",\"kind\",\"math\",\"nan\",\"num\",\"number\",\"numeric\",\"parseFloat\",\"parseInt\",\"test\",\"type\",\"typeof\",\"value\"],\"verb\":{\"toc\":false,\"layout\":\"default\",\"tasks\":[\"readme\"],\"related\":{\"list\":[\"is-plain-object\",\"is-primitive\",\"isobject\",\"kind-of\"]},\"plugins\":[\"gulp-format-md\"],\"lint\":{\"reflinks\":true}},\"gitHead\":\"98e8ff1da1a89f93d1397a24d7413ed15421c139\",\"_id\":\"is-number@7.0.0\",\"_npmVersion\":\"6.1.0\",\"_nodeVersion\":\"10.0.0\",\"_npmUser\":{\"name\":\"jonschlinkert\",\"email\":\"github@sellside.com\"},\"dist\":{\"integrity\":\"sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==\",\"shasum\":\"7535345b896734d5f80c4d06c50955527a14f12b\",\"tarball\":\"https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz\",\"fileCount\":4,\"unpackedSize\":9615,\"npm-signature\":\"-----BEGIN PGP SIGNATURE-----\\r\\nVersion: OpenPGP.js v3.0.4\\r\\nComment: https://openpgpjs.org\\r\\n\\r\\nwsFcBAEBCAAQBQJbPOMKCRA9TVsSAnZWagAABmIP/iUfV7gQnAsn2vRNKWIu\\nlxkfXFutnV1Bo5P8mlv37Zg+PN5/Ri/RATtKGEosAvHL/HK2HkFH+rLYkupg\\nCYaAVXNOJiFN9vK61YYp0TB0PBtGDQ2UwcElxDQ8OP6Djni4fl2fLjVZ+JeN\\nPF1bKPE6oMevNg3GtNMrb1QKZNGjplOKFmlmm9fe653LHwIH1zPYy6QQ+cgG\\nQ8Czmhdf+NuX8WXqR1h5mHTKCCVAqzcpJ21uX9292aoWlzlijzg9cc8KEPEY\\ncbUjJf4NMLs89/8G6tcpDP5qoVjQnWIgfM0mDXt4P7ESdqjusdyNa+3eS/Yi\\nsQuevXmth+41kvQaVy7rLbeAssWNbc7IJ+X1qpgnUqTz4rtiAaRnZ6rzAE2s\\nrr723lN2H7ALlnDVm5qzXLTTjHwbdYMYuaYHTOMovytYWeRnwhfNCpKeQcEe\\nUYR3sm2tEdxsMI5ciu5ULGj/PEKnSOsQcq2y4QjNFz4F5ReXiv+CtphQubAr\\nVkKwrjIovN97YXmyrkfJB1zCInCMUz11YgRY0wNaCJPJ4E0V4KdfWgCEyt+F\\nqKcdylewrOAma06WozqUXzRH3uNNbMTfsE1nGqbcofJPNBre/VriCwF7zS/H\\nLhGfCe6LJuY2Q+2Eel1vtwnY+vAoaWrr7QisjcvywYHvqWlRob2IxC1Ygp3A\\nvUY+\\r\\n=M7jx\\r\\n-----END PGP SIGNATURE-----\\r\\n\",\"signatures\":[{\"keyid\":\"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA\",\"sig\":\"MEUCIGex85Xx5rKhEgNSkg7z5bBu7vbcFbJnWJZbV3f9Td8GAiEA5wzgLXU41sE6ezCKGrmzsyMtm6M+2OhefJJhkzPEAoc=\"}]},\"maintainers\":[{\"email\":\"brian.woodward@gmail.com\",\"name\":\"doowb\"},{\"email\":\"github@sellside.com\",\"name\":\"jonschlinkert\"},{\"email\":\"me@rouvenwessling.de\",\"name\":\"realityking\"}],\"directories\":{},\"_npmOperationalInternal\":{\"host\":\"s3://npm-registry-packages\",\"tmp\":\"tmp/is-number_7.0.0_1530716938183_0.28831183290614004\"},\"_hasShrinkwrap\":false}},\"readme\":\"# is-number [![NPM version](https://img.shields.io/npm/v/is-number.svg?style=flat)](https://www.npmjs.com/package/is-number) [![NPM monthly downloads](https://img.shields.io/npm/dm/is-number.svg?style=flat)](https://npmjs.org/package/is-number) [![NPM total downloads](https://img.shields.io/npm/dt/is-number.svg?style=flat)](https://npmjs.org/package/is-number) [![Linux Build Status](https://img.shields.io/travis/jonschlinkert/is-number.svg?style=flat&label=Travis)](https://travis-ci.org/jonschlinkert/is-number)\\n\\n> Returns true if the value is a finite number.\\n\\nPlease consider following this project's author, [Jon Schlinkert](https://github.com/jonschlinkert), and consider starring the project to show your :heart: and support.\\n\\n## Install\\n\\nInstall with [npm](https://www.npmjs.com/):\\n\\n```sh\\n$ npm install --save is-number\\n```\\n\\n## Why is this needed?\\n\\nIn JavaScript, it's not always as straightforward as it should be to reliably check if a value is a number. It's common for devs to use `+`, `-`, or `Number()` to cast a string value to a number (for example, when values are returned from user input, regex matches, parsers, etc). But there are many non-intuitive edge cases that yield unexpected results:\\n\\n```js\\nconsole.log(+[]); //=> 0\\nconsole.log(+''); //=> 0\\nconsole.log(+'   '); //=> 0\\nconsole.log(typeof NaN); //=> 'number'\\n```\\n\\nThis library offers a performant way to smooth out edge cases like these.\\n\\n## Usage\\n\\n```js\\nconst isNumber = require('is-number');\\n```\\n\\nSee the [tests](./test.js) for more examples.\\n\\n### true\\n\\n```js\\nisNumber(5e3);               // true\\nisNumber(0xff);              // true\\nisNumber(-1.1);              // true\\nisNumber(0);                 // true\\nisNumber(1);                 // true\\nisNumber(1.1);               // true\\nisNumber(10);                // true\\nisNumber(10.10);             // true\\nisNumber(100);               // true\\nisNumber('-1.1');            // true\\nisNumber('0');               // true\\nisNumber('012');             // true\\nisNumber('0xff');            // true\\nisNumber('1');               // true\\nisNumber('1.1');             // true\\nisNumber('10');              // true\\nisNumber('10.10');           // true\\nisNumber('100');             // true\\nisNumber('5e3');             // true\\nisNumber(parseInt('012'));   // true\\nisNumber(parseFloat('012')); // true\\n```\\n\\n### False\\n\\nEverything else is false, as you would expect:\\n\\n```js\\nisNumber(Infinity);          // false\\nisNumber(NaN);               // false\\nisNumber(null);              // false\\nisNumber(undefined);         // false\\nisNumber('');                // false\\nisNumber('   ');             // false\\nisNumber('foo');             // false\\nisNumber([1]);               // false\\nisNumber([]);                // false\\nisNumber(function () {});    // false\\nisNumber({});                // false\\n```\\n\\n## Release history\\n\\n### 7.0.0\\n\\n* Refactor. Now uses `.isFinite` if it exists.\\n* Performance is about the same as v6.0 when the value is a string or number. But it's now 3x-4x faster when the value is not a string or number.\\n\\n### 6.0.0\\n\\n* Optimizations, thanks to @benaadams.\\n\\n### 5.0.0\\n\\n**Breaking changes**\\n\\n* removed support for `instanceof Number` and `instanceof String`\\n\\n## Benchmarks\\n\\nAs with all benchmarks, take these with a grain of salt. See the [benchmarks](./benchmark/index.js) for more detail.\\n\\n```\\n# all\\nv7.0 x 413,222 ops/sec ±2.02% (86 runs sampled)\\nv6.0 x 111,061 ops/sec ±1.29% (85 runs sampled)\\nparseFloat x 317,596 ops/sec ±1.36% (86 runs sampled)\\nfastest is 'v7.0'\\n\\n# string\\nv7.0 x 3,054,496 ops/sec ±1.05% (89 runs sampled)\\nv6.0 x 2,957,781 ops/sec ±0.98% (88 runs sampled)\\nparseFloat x 3,071,060 ops/sec ±1.13% (88 runs sampled)\\nfastest is 'parseFloat,v7.0'\\n\\n# number\\nv7.0 x 3,146,895 ops/sec ±0.89% (89 runs sampled)\\nv6.0 x 3,214,038 ops/sec ±1.07% (89 runs sampled)\\nparseFloat x 3,077,588 ops/sec ±1.07% (87 runs sampled)\\nfastest is 'v6.0'\\n```\\n\\n## About\\n\\n<details>\\n<summary><strong>Contributing</strong></summary>\\n\\nPull requests and stars are always welcome. For bugs and feature requests, [please create an issue](../../issues/new).\\n\\n</details>\\n\\n<details>\\n<summary><strong>Running Tests</strong></summary>\\n\\nRunning and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command:\\n\\n```sh\\n$ npm install && npm test\\n```\\n\\n</details>\\n\\n<details>\\n<summary><strong>Building docs</strong></summary>\\n\\n_(This project's readme.md is generated by [verb](https://github.com/verbose/verb-generate-readme), please don't edit the readme directly. Any changes to the readme must be made in the [.verb.md](.verb.md) readme template.)_\\n\\nTo generate the readme, run the following command:\\n\\n```sh\\n$ npm install -g verbose/verb#dev verb-generate-readme && verb\\n```\\n\\n</details>\\n\\n### Related projects\\n\\nYou might also be interested in these projects:\\n\\n* [is-plain-object](https://www.npmjs.com/package/is-plain-object): Returns true if an object was created by the `Object` constructor. | [homepage](https://github.com/jonschlinkert/is-plain-object \\\"Returns true if an object was created by the `Object` constructor.\\\")\\n* [is-primitive](https://www.npmjs.com/package/is-primitive): Returns `true` if the value is a primitive.  | [homepage](https://github.com/jonschlinkert/is-primitive \\\"Returns `true` if the value is a primitive. \\\")\\n* [isobject](https://www.npmjs.com/package/isobject): Returns true if the value is an object and not an array or null. | [homepage](https://github.com/jonschlinkert/isobject \\\"Returns true if the value is an object and not an array or null.\\\")\\n* [kind-of](https://www.npmjs.com/package/kind-of): Get the native type of a value. | [homepage](https://github.com/jonschlinkert/kind-of \\\"Get the native type of a value.\\\")\\n\\n### Contributors\\n\\n| **Commits** | **Contributor** | \\n| --- | --- |\\n| 49 | [jonschlinkert](https://github.com/jonschlinkert) |\\n| 5 | [charlike-old](https://github.com/charlike-old) |\\n| 1 | [benaadams](https://github.com/benaadams) |\\n| 1 | [realityking](https://github.com/realityking) |\\n\\n### Author\\n\\n**Jon Schlinkert**\\n\\n* [LinkedIn Profile](https://linkedin.com/in/jonschlinkert)\\n* [GitHub Profile](https://github.com/jonschlinkert)\\n* [Twitter Profile](https://twitter.com/jonschlinkert)\\n\\n### License\\n\\nCopyright © 2018, [Jon Schlinkert](https://github.com/jonschlinkert).\\nReleased under the [MIT License](LICENSE).\\n\\n***\\n\\n_This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.6.0, on June 15, 2018._\",\"maintainers\":[{\"email\":\"brian.woodward@gmail.com\",\"name\":\"doowb\"},{\"email\":\"github@sellside.com\",\"name\":\"jonschlinkert\"},{\"email\":\"me@rouvenwessling.de\",\"name\":\"realityking\"}],\"time\":{\"modified\":\"2023-05-26T16:12:33.057Z\",\"created\":\"2014-09-22T01:58:51.592Z\",\"0.1.0\":\"2014-09-22T01:58:51.592Z\",\"0.1.1\":\"2014-09-22T03:37:27.931Z\",\"1.0.0\":\"2015-01-24T10:16:54.470Z\",\"1.1.0\":\"2015-01-24T10:33:21.598Z\",\"1.1.1\":\"2015-03-05T18:37:07.633Z\",\"1.1.2\":\"2015-03-05T18:57:33.841Z\",\"2.0.0\":\"2015-05-02T08:11:57.926Z\",\"2.0.1\":\"2015-05-03T04:27:13.630Z\",\"2.0.2\":\"2015-05-03T05:59:55.976Z\",\"2.1.0\":\"2015-11-22T13:56:56.624Z\",\"3.0.0\":\"2016-09-11T00:51:30.912Z\",\"4.0.0\":\"2017-10-17T05:43:56.559Z\",\"5.0.0\":\"2018-01-29T03:06:41.225Z\",\"6.0.0\":\"2018-03-31T17:02:39.953Z\",\"7.0.0\":\"2018-07-04T15:08:58.238Z\"},\"homepage\":\"https://github.com/jonschlinkert/is-number\",\"keywords\":[\"cast\",\"check\",\"coerce\",\"coercion\",\"finite\",\"integer\",\"is\",\"isnan\",\"is-nan\",\"is-num\",\"is-number\",\"isnumber\",\"isfinite\",\"istype\",\"kind\",\"math\",\"nan\",\"num\",\"number\",\"numeric\",\"parseFloat\",\"parseInt\",\"test\",\"type\",\"typeof\",\"value\"],\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/jonschlinkert/is-number.git\"},\"author\":{\"name\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\"},\"bugs\":{\"url\":\"https://github.com/jonschlinkert/is-number/issues\"},\"readmeFilename\":\"README.md\",\"license\":\"MIT\",\"users\":{\"jonschlinkert\":true,\"antanst\":true,\"rocket0191\":true,\"papasavva\":true,\"456wyc\":true,\"maycon_ribeiro\":true,\"leix3041\":true,\"snowdream\":true,\"fizzvr\":true,\"jameskrill\":true,\"rioli\":true,\"gugadev\":true,\"fearnbuster\":true,\"flumpus-dev\":true},\"contributors\":[{\"name\":\"Jon Schlinkert\",\"url\":\"http://twitter.com/jonschlinkert\"},{\"name\":\"Olsten Larck\",\"url\":\"https://i.am.charlike.online\"},{\"name\":\"Rouven Weßling\",\"url\":\"www.rouvenwessling.de\"}]}"
  },
  {
    "path": "demos/unpkg/test/mock-fetch.ts",
    "content": "import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { mock } from 'node:test'\n\nlet fixturesDir = path.join(import.meta.dirname, 'fixtures')\n\ninterface MockResponse {\n  status: number\n  body: Uint8Array | string | object\n  headers?: Record<string, string>\n}\n\ntype FetchMockHandler = (url: string, init?: RequestInit) => MockResponse | null\n\nlet mockHandlers: FetchMockHandler[] = []\n\nfunction createMockResponse(mockResponse: MockResponse): Response {\n  let body: BodyInit\n  let headers = new Headers(mockResponse.headers)\n\n  if (mockResponse.body instanceof Uint8Array) {\n    body = mockResponse.body as BodyInit\n  } else if (typeof mockResponse.body === 'object') {\n    body = JSON.stringify(mockResponse.body)\n    if (!headers.has('Content-Type')) {\n      headers.set('Content-Type', 'application/json')\n    }\n  } else {\n    body = mockResponse.body\n  }\n\n  return new Response(body, {\n    status: mockResponse.status,\n    headers,\n  })\n}\n\nfunction mockedFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n  let url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url\n\n  for (let handler of mockHandlers) {\n    let result = handler(url, init)\n    if (result) {\n      return Promise.resolve(createMockResponse(result))\n    }\n  }\n\n  // No handler matched - return 404\n  return Promise.resolve(new Response('Not Found', { status: 404 }))\n}\n\n/**\n * Install the fetch mock. Call this in a before() hook.\n */\nexport function installFetchMock(): void {\n  mock.method(globalThis, 'fetch', mockedFetch)\n}\n\n/**\n * Restore the original fetch. Call this in an after() hook.\n */\nexport function restoreFetchMock(): void {\n  mock.reset()\n  mockHandlers = []\n}\n\n/**\n * Add a mock handler for fetch requests.\n */\nexport function addFetchHandler(handler: FetchMockHandler): void {\n  mockHandlers.push(handler)\n}\n\n/**\n * Clear all fetch handlers.\n */\nexport function clearFetchHandlers(): void {\n  mockHandlers = []\n}\n\n/**\n * Load a fixture file as a Uint8Array.\n */\nexport function loadFixture(filename: string): Uint8Array {\n  let filePath = path.join(fixturesDir, filename)\n  return new Uint8Array(fs.readFileSync(filePath))\n}\n\n/**\n * Load a fixture file as JSON.\n */\nexport function loadFixtureJson<T = unknown>(filename: string): T {\n  let filePath = path.join(fixturesDir, filename)\n  return JSON.parse(fs.readFileSync(filePath, 'utf-8'))\n}\n\n/**\n * Create a mock handler for npm registry requests using fixture files.\n */\nexport function createNpmRegistryMock(\n  packages: Record<string, { metadata: string; tarballs: Record<string, string> }>,\n): FetchMockHandler {\n  return (url: string) => {\n    // Check for package metadata requests\n    for (let [packageName, config] of Object.entries(packages)) {\n      let metadataUrl = `https://registry.npmjs.org/${packageName}`\n      if (url === metadataUrl) {\n        return {\n          status: 200,\n          body: loadFixtureJson(config.metadata),\n        }\n      }\n\n      // Check for tarball requests\n      for (let [version, tarballFile] of Object.entries(config.tarballs)) {\n        let tarballUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@.*\\//, '')}-${version}.tgz`\n        if (url === tarballUrl) {\n          return {\n            status: 200,\n            body: loadFixture(tarballFile),\n            headers: { 'Content-Type': 'application/gzip' },\n          }\n        }\n      }\n    }\n\n    // Return 404 for non-existent packages on npm registry\n    if (url.startsWith('https://registry.npmjs.org/')) {\n      return {\n        status: 404,\n        body: { error: 'Not found' },\n      }\n    }\n\n    return null\n  }\n}\n"
  },
  {
    "path": "demos/unpkg/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "build/\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"remix-the-docs\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"front-matter\": \"^4.0.2\",\n    \"marked\": \"^17.0.1\",\n    \"node-html-parser\": \"^7.0.2\",\n    \"prettier\": \"^3.5.3\",\n    \"remix\": \"workspace:*\",\n    \"semver\": \"^7.7.3\"\n  },\n  \"devDependencies\": {\n    \"@types/dom-navigation\": \"^1.0.6\",\n    \"@types/hast\": \"^3.0.4\",\n    \"@types/node\": \"catalog:\",\n    \"@types/semver\": \"^7.5.8\",\n    \"esbuild\": \"^0.25.10\",\n    \"shiki\": \"^3.22.0\",\n    \"typedoc\": \"^0.28.15\"\n  },\n  \"scripts\": {\n    \"build\": \"pnpm run --filter \\\".\\\" --parallel \\\"/^build:/\\\"\",\n    \"build:browser\": \"esbuild src/client/*.tsx --outbase=src/client --outdir=build/assets --bundle --splitting --format=esm\",\n    \"build:public\": \"mkdir -p build/assets && cp public/* build/assets\",\n    \"dev\": \"NODE_ENV=development pnpm run --filter \\\".\\\" --parallel \\\"/^dev:/\\\"\",\n    \"dev:browser\": \"pnpm run build:browser --sourcemap --watch\",\n    \"dev:server\": \"tsx --watch src/server/index.ts\",\n    \"docs\": \"node src/generate/index.ts\",\n    \"docs:debug\": \"DEBUG=1 node src/generate/index.ts\",\n    \"prerender\": \"tsx src/server/prerender.ts\",\n    \"prerender:serve\": \"npx http-server -p 3000 build/site\",\n    \"serve\": \"tsx src/server/index.ts\",\n    \"typecheck\": \"tsc --noEmit\"\n  }\n}\n"
  },
  {
    "path": "docs/public/docs.css",
    "content": ":root {\n  --spacing-xxs: 4px;\n  --spacing-xs: 8px;\n  --spacing-small: 12px;\n  --spacing-medium: 24px;\n  --spacing-large: 28px; /* +4 */\n  --spacing-xl: 32px; /* +12 */\n  --spacing-xxl: 48px; /* +16 */\n}\n\n:root {\n  /* Light Mode */\n  --surface-level-0-color: #d2d6da;\n  --surface-level-1-color: #d9dde1;\n  --surface-level-2-color: #e4e8ec;\n  --surface-level-3-color: #f0f4f7;\n  --surface-level-4-color: #f7fbff;\n  --text-primary-color: #25292d;\n  --text-secondary-color: #707477;\n  --text-tertiary-color: #a1a5a9;\n  --text-link-color: #2dacf9;\n  --button-surface-primary-color: #2dacf9;\n  --button-surface-secondary-color: #f7fbff;\n  --button-label-primary-color: #f7fbff;\n  --button-label-secondary-color: #313539;\n  --emphasis-blue-color: #8bd3ff;\n  --emphasis-green-color: #bbefb1;\n  --emphasis-yellow-color: #fbedaf;\n  --emphasis-pink-color: #fbb0ed;\n  --emphasis-red-color: #fba6a3;\n\n  /* Dark Mode */\n  @media (prefers-color-scheme: dark) {\n    --surface-level-0-color: #0d1114;\n    --surface-level-1-color: #191d21;\n    --surface-level-2-color: #25292d;\n    --surface-level-3-color: #313539;\n    --surface-level-4-color: #3e4246;\n    --text-primary-color: #ebeff2;\n    --text-secondary-color: #a1a5a9;\n    --text-tertiary-color: #63676b;\n    --text-link-color: #2dacf9;\n    --button-surface-primary-color: #2dacf9;\n    --button-surface-secondary-color: #4b4f52;\n    --button-label-primary-color: #f7fbff;\n    --button-label-secondary-color: #ebeff2;\n    --emphasis-blue-color: #156fa6;\n    --emphasis-green-color: #539441;\n    --emphasis-yellow-color: #a6913e;\n    --emphasis-pink-color: #a6428e;\n    --emphasis-red-color: #a6352f;\n  }\n}\n\n:root {\n  --primary-font: 'JetBrains Mono', monospace;\n  --h1-size: 48px;\n  --h1-font-weight: 400;\n  --h1-line-height: 140%;\n  --h2-size: 24px;\n  --h2-font-weight: 400;\n  --h2-line-height: 140%;\n  --h3-size: 20px;\n  --h3-font-weight: 600;\n  --h3-line-height: 140%;\n  --p-size: 14px;\n  --p-font-weight: 300;\n  --p-line-height: 150%;\n  --nav-heading-size: 14px;\n  --nav-heading-font-weight: 400;\n  --nav-heading-line-height: 140%;\n  --nav-size: 14px;\n  --nav-font-weight: 300;\n  --nav-line-height: 140%;\n}\n\n/* Layout */\n* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\n.visually-hidden {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\n\n.nav-toggle {\n  position: fixed;\n  opacity: 0;\n  pointer-events: none;\n}\n\n.mobile-header {\n  display: none;\n}\n\n@media (max-width: 640px) {\n  .mobile-header {\n    position: fixed;\n    top: var(--spacing-small);\n    left: var(--spacing-small);\n    display: block;\n    z-index: 99;\n  }\n}\n\n.nav-toggle-open,\n.nav-toggle-close {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 44px;\n  height: 44px;\n  font-size: 22px;\n  color: var(--text-primary-color);\n  background-color: var(--button-surface-secondary-color);\n  padding: var(--spacing-medium);\n  border: none;\n  box-shadow: 3px 3px 3px var(--surface-level-2-color);\n  cursor: pointer;\n}\n\n.nav-toggle-close {\n  display: none;\n}\n\n/* Navigation progress bar */\n#nav-overlay {\n  display: none;\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  height: 3px;\n  z-index: 200;\n  overflow: hidden;\n\n  &.active {\n    display: block;\n\n    body:has(&) .main {\n      opacity: 0.4;\n      transition: opacity 0.15s ease;\n    }\n\n    &::after {\n      content: '';\n      position: absolute;\n      top: 0;\n      left: -50%;\n      width: 50%;\n      height: 100%;\n      background: var(--text-link-color);\n      animation: nav-progress 1s ease-in-out infinite;\n    }\n  }\n}\n\n@keyframes nav-progress {\n  0% {\n    left: -50%;\n  }\n  100% {\n    left: 100%;\n  }\n}\n\n.container {\n  display: flex;\n  min-height: 100vh;\n\n  .sidebar {\n    width: 380px;\n    overflow-y: auto;\n    position: fixed;\n    top: 0;\n    left: 0;\n    height: 100vh;\n    z-index: 100;\n\n    header {\n      position: sticky;\n      top: 0;\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      padding: var(--spacing-medium);\n\n      .logo {\n        width: 60%;\n        max-width: 180px;\n\n        .light {\n          display: block;\n        }\n\n        .dark {\n          display: none;\n        }\n\n        @media (prefers-color-scheme: dark) {\n          .light {\n            display: none;\n          }\n          .dark {\n            display: block;\n          }\n        }\n      }\n\n      .sidebar-actions {\n        display: flex;\n        align-items: center;\n        gap: var(--spacing-small);\n        flex-shrink: 0;\n      }\n    }\n\n    nav {\n      padding: var(--spacing-small) var(--spacing-medium);\n    }\n  }\n\n  .main {\n    flex: 1;\n    margin-left: 380px;\n    padding: var(--spacing-xl);\n    max-width: 1200px;\n    min-width: 0;\n    overflow-x: hidden;\n\n    .content {\n      max-width: 800px;\n    }\n  }\n}\n\n@media (max-width: 1024px) and (min-width: 641px) {\n  .container .sidebar {\n    width: 300px;\n  }\n  .container .main {\n    margin-left: 300px;\n    padding: var(--spacing-medium);\n  }\n}\n\n@media (max-width: 640px) {\n  .container {\n    display: block;\n  }\n\n  .container .sidebar {\n    width: 100%;\n    max-width: none;\n    transform: translateX(-100%);\n    transition: transform 0.2s ease;\n  }\n\n  .nav-toggle:checked ~ .container .sidebar {\n    transform: translateX(0);\n  }\n\n  .container .sidebar header {\n    padding: var(--spacing-large) var(--spacing-medium) var(--spacing-medium);\n  }\n\n  .container .sidebar nav {\n    padding: var(--spacing-medium);\n  }\n\n  .nav-toggle:checked ~ .container .nav-toggle-close {\n    display: inline-flex;\n  }\n\n  .container .main {\n    margin-left: 0;\n    padding: calc(var(--spacing-xxl) + 28px) var(--spacing-medium) var(--spacing-xl);\n  }\n\n  /* .nav-toggle:checked ~ .container .main {\n    display: none;\n  } */\n}\n\n/* Global Element Theme Styles */\n\nbody {\n  font-family: var(--primary-font);\n  color: var(--primary-color);\n}\n\nh1 {\n  font-size: var(--h1-size);\n  font-weight: var(--h1-font-weight);\n  line-height: var(--h1-line-height);\n  color: var(--text-primary-color);\n  text-box-trim: trim-both;\n  text-box-edge: cap alphabetic;\n  margin-top: 0;\n  margin-bottom: var(--spacing-medium);\n\n  @media (max-width: 1024px) and (min-width: 641px) {\n    font-size: 36px;\n  }\n\n  @media (max-width: 640px) {\n    font-size: calc(var(--h1-size) * 0.65);\n  }\n}\n\nh2 {\n  font-size: var(--h2-size);\n  font-weight: var(--h2-font-weight);\n  line-height: var(--h2-line-height);\n  color: var(--text-secondary-color);\n  margin-top: var(--spacing-large);\n  margin-bottom: var(--spacing-medium);\n}\n\nh3 {\n  font-size: var(--h3-size);\n  font-weight: var(--h3-font-weight);\n  line-height: var(--h3-line-height);\n  color: var(--text-secondary-color);\n  margin-top: var(--spacing-medium);\n  margin-bottom: var(--spacing-small);\n}\n\nh4 {\n  font-size: var(--p-size);\n  color: var(--text-primary-color);\n\n  @media (max-width: 640px) {\n    font-size: calc(var(--p-size) * 0.9);\n  }\n}\n\np {\n  font-size: var(--p-size);\n  font-weight: var(--p-font-weight);\n  line-height: var(--p-line-height);\n  color: var(--text-primary-color);\n  margin-bottom: var(--spacing-medium);\n\n  @media (max-width: 640px) {\n    font-size: calc(var(--p-size) * 0.9);\n  }\n}\n\nul {\n  margin-left: var(--spacing-small);\n}\n\nli {\n  font-size: var(--p-size);\n  font-weight: var(--p-font-weight);\n  line-height: var(--p-line-height);\n  color: var(--text-primary-color);\n\n  @media (max-width: 640px) {\n    font-size: calc(var(--p-size) * 0.9);\n  }\n}\n\na {\n  color: var(--text-link-color);\n  text-decoration: none;\n\n  &:hover {\n    text-decoration: underline;\n  }\n}\n\nbutton {\n  background-color: var(--button-surface-secondary-color);\n  padding: var(--spacing-medium);\n  border: none;\n  box-shadow: 3px 3px 3px var(--surface-level-2-color);\n}\n\npre {\n  background: var(--surface-level-4-color);\n  border: 1px solid var(--surface-level-3-color);\n  border-radius: 3px;\n  margin: 0 calc(var(--spacing-medium) * -1) var(--spacing-medium);\n  padding: var(--spacing-medium);\n  overflow-x: auto;\n}\n\ncode {\n  font-family: var(--primary-font);\n  font-size: var(--p-size);\n\n  @media (max-width: 640px) {\n    font-size: calc(var(--p-size) * 0.9);\n  }\n\n  :not(a):not(pre) > & {\n    background: var(--surface-level-4-color);\n    border-radius: 2px;\n    padding: 2px 3px;\n  }\n}\n\n/* Styling */\n.container {\n  background-color: var(--surface-level-2-color);\n}\n\n.sidebar {\n  background-color: var(--surface-level-1-color);\n  font-weight: var(--nav-font-weight);\n  color: var(--text-primary-color);\n  font-size: var(--nav-size);\n  scrollbar-color: var(--surface-level-2-color) var(--surface-level-1-color);\n\n  header {\n    background-color: var(--surface-level-1-color);\n  }\n\n  summary {\n    font-weight: var(--nav-heading-font-weight);\n    color: var(--text-primary-color);\n    font-size: var(--nav-heading-size);\n    margin-bottom: var(--spacing-small);\n    cursor: pointer;\n\n    @media (max-width: 640px) {\n      margin-bottom: var(--spacing-xs);\n      padding: var(--spacing-xs) 0;\n    }\n  }\n\n  p {\n    font-size: var(--nav-subheading-size);\n    margin-bottom: var(--spacing-xs);\n    font-weight: var(--nav-subheading-font-weight);\n  }\n\n  ul {\n    list-style: none;\n    margin-bottom: var(--spacing-small);\n    margin-left: var(--spacing-medium);\n\n    @media (max-width: 640px) {\n      margin-bottom: var(--spacing-xs);\n    }\n\n    ul {\n      margin-left: var(--spacing-small);\n    }\n  }\n\n  li {\n    margin-bottom: var(--spacing-xs);\n    padding: 0;\n  }\n\n  a {\n    display: inline-block;\n\n    &.active {\n      font-weight: bold;\n    }\n\n    @media (max-width: 640px) {\n      padding: var(--spacing-small) 0;\n    }\n  }\n}\n\n.content {\n  .home {\n    text-align: center;\n    margin-top: 30%;\n\n    h1 {\n      font-size: var(--h2-size);\n    }\n  }\n\n  .error {\n    text-align: center;\n  }\n\n  a {\n    /* color: var(--text-link-color); */\n    text-decoration: none;\n\n    &:hover {\n      text-decoration: underline;\n    }\n  }\n}\n\n/* Shiki Code Highlighting */\n.shiki {\n  background-color: var(--surface-level-3-color) !important;\n\n  a {\n    color: inherit;\n  }\n\n  /* Shiki Code Highlighting Dark Mode */\n  @media (prefers-color-scheme: dark) {\n    &,\n    & span {\n      color: var(--shiki-dark) !important;\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/client/entry.tsx",
    "content": "import { run } from 'remix/component'\n\nlet app = run({\n  async loadModule(moduleUrl: string, exportName: string) {\n    let mod = await import(moduleUrl)\n    let Component = (mod as any)[exportName]\n    if (!Component) {\n      throw new Error(`Unknown component: ${moduleUrl}#${exportName}`)\n    }\n    return Component\n  },\n  async resolveFrame(src, signal) {\n    let response = await fetch(src, { headers: { accept: 'text/html' }, signal })\n    if (!response.ok) {\n      return `<pre>Frame error: ${response.status} ${response.statusText}</pre>`\n    }\n    if (response.body) return response.body\n    return response.text()\n  },\n})\n\napp.ready().catch((error: unknown) => {\n  console.error('Frame adoption failed:', error)\n})\n\nwindow.navigation.addEventListener('navigate', () => {\n  let toggle = document.getElementById('nav-toggle') as HTMLInputElement | null\n  if (toggle) {\n    toggle.checked = false\n  }\n\n  let transition = window.navigation.transition\n  if (transition) {\n    let overlay = document.getElementById('nav-overlay')\n    overlay?.classList.add('active')\n    transition.finished.finally(() => overlay?.classList.remove('active'))\n  }\n})\n"
  },
  {
    "path": "docs/src/generate/documented-api.ts",
    "content": "import * as typedoc from 'typedoc'\nimport { getApiNameFromFullName, invariant, unimplemented, warn } from './utils.ts'\nimport { MDN_SYMBOLS } from './symbols.ts'\n\nexport type DocumentedAPI =\n  | DocumentedFunction\n  | DocumentedClass\n  | DocumentedInterface\n  | DocumentedInterfaceFunction\n  | DocumentedType\n\n// Function parameter or Class property\ntype ParameterOrProperty = {\n  name: string\n  type: string\n  description: string\n}\n\n// Class Method\ntype Method = {\n  name: string\n  signature: string\n  description: string\n  parameters: ParameterOrProperty[]\n  returns: string | undefined\n}\n\n// Fields required for all types\ntype BaseDocumentedAPI = {\n  type: 'function' | 'class' | 'interface' | 'interface-function' | 'type'\n  path: string\n  source: string | undefined\n  name: string\n  aliases: string[] | undefined\n  description: string\n}\n\n// Documented function API\nexport type DocumentedFunction = BaseDocumentedAPI & {\n  type: 'function'\n  signature: string\n  parameters: ParameterOrProperty[]\n  returns: string | undefined\n  example: string | undefined\n}\n\n// Documented class API\nexport type DocumentedClass = BaseDocumentedAPI & {\n  type: 'class'\n  signature: string\n  constructor: Method | undefined\n  properties: ParameterOrProperty[] | undefined\n  accessors: ParameterOrProperty[] | undefined\n  methods: Method[] | undefined\n  example: string | undefined\n}\n\n// Documented interface API\nexport type DocumentedInterface = BaseDocumentedAPI & {\n  type: 'interface'\n  signature: string\n  properties: ParameterOrProperty[] | undefined\n  accessors: ParameterOrProperty[] | undefined\n  methods: Method[] | undefined\n}\n\nexport type DocumentedInterfaceFunction = BaseDocumentedAPI & {\n  type: 'interface-function'\n  signature: string\n  parameters: ParameterOrProperty[]\n  returns: string | undefined\n}\n\n// Documented type API\nexport type DocumentedType = BaseDocumentedAPI & {\n  type: 'type'\n  signature: string\n}\n\n// PAth docs are served at on the website\nconst WEBSITE_DOCS_PATH = '/api'\n\n// Convert a typedoc reflection for a given node into a documentable instance\nexport function getDocumentedAPI(fullName: string, node: typedoc.Reflection): DocumentedAPI {\n  try {\n    let api: DocumentedAPI | undefined\n    if (node.isSignature()) {\n      if (node.parent.kind === typedoc.ReflectionKind.Function) {\n        api = getDocumentedFunction(fullName, node)\n      } else if (node.parent.kind === typedoc.ReflectionKind.Interface) {\n        api = getDocumentedInterfaceFunction(fullName, node)\n      }\n    } else if (node.isDeclaration()) {\n      if (node.kind === typedoc.ReflectionKind.Class) {\n        api = getDocumentedClass(fullName, node)\n      } else if (node.kind === typedoc.ReflectionKind.Interface) {\n        api = getDocumentedInterface(fullName, node)\n      } else if (node.kind === typedoc.ReflectionKind.TypeAlias) {\n        api = getDocumentedType(fullName, node)\n      }\n    }\n\n    if (!api) {\n      throw new Error(`Unsupported documented API kind: ${typedoc.ReflectionKind[node.kind]}`)\n    }\n\n    warnOnInvalidImportSyntax(api)\n\n    return api\n  } catch (e) {\n    throw new Error(\n      `Error normalizing comment for ${node.getFriendlyFullName()}: ${(e as Error).message}`,\n      {\n        cause: e,\n      },\n    )\n  }\n}\n\nfunction getDocumentedFunction(\n  fullName: string,\n  node: typedoc.SignatureReflection,\n): DocumentedFunction {\n  let method = getApiMethod(fullName, node)\n  invariant(method, `Failed to get method for function: ${node.getFriendlyFullName()}`)\n  let methods = [method]\n  let signature = method.signature\n  let parameters = method.parameters\n\n  // For overloaded functions, collect all signatures and merge parameters\n  if (node.parent.signatures && node.parent.signatures.length > 1) {\n    methods = node.parent.signatures\n      .map((s) => getApiMethod(fullName, s))\n      .filter((m): m is Method => m != null)\n\n    signature = methods.map((m) => m.signature).join('\\n\\n')\n\n    // Deduplicate parameters across overloads by name (first occurrence wins)\n    methods\n      .flatMap((m) => m.parameters)\n      .forEach((param) => {\n        if (!parameters.some((p) => p.name === param.name)) {\n          parameters.push(param)\n        }\n      })\n  }\n\n  return {\n    type: 'function',\n    path: getApiFilePath(fullName, 'function'),\n    source: node.sources?.[0]?.url,\n    name: method.name,\n    aliases: node.comment ? getApiAliases(node.comment) : undefined,\n    description: node.comment ? getApiDescription(node.comment) : '',\n    signature,\n    example: node.comment?.getTag('@example')?.content\n      ? processApiComment(node.comment.getTag('@example')!.content)\n      : undefined,\n    parameters,\n    returns: method.returns,\n  }\n}\n\nfunction getDocumentedInterfaceFunction(\n  fullName: string,\n  node: typedoc.SignatureReflection,\n): DocumentedInterfaceFunction {\n  return {\n    ...getDocumentedFunction(fullName, node),\n    type: 'interface-function',\n  }\n}\n\nfunction getDocumentedClass(\n  fullName: string,\n  node: typedoc.DeclarationReflection,\n): DocumentedClass {\n  let constructor: Method | undefined\n  node.traverse((child) => {\n    if (child.isDeclaration() && child.kind === typedoc.ReflectionKind.Constructor) {\n      invariant(\n        child.getAllSignatures().length === 1,\n        `Docs only support one constructor signature at the moment: ${child.getFriendlyFullName()}`,\n      )\n      let signature = child.getAllSignatures()[0]\n      invariant(signature, `Missing constructor signature for class: ${node.getFriendlyFullName()}`)\n      constructor = getApiMethod(fullName, signature)\n    }\n  })\n\n  let { properties, accessors, methods } = getApiPropertiesAndMethods(\n    fullName,\n    node,\n    new Set([typedoc.ReflectionKind.Constructor]),\n  )\n\n  let name = getApiNameFromFullName(fullName)\n  let classDecl = node.toString().replace(/^Class /, 'class ')\n  let signature = `${classDecl} {\\n${getClassBodySignature(node)}}`\n\n  return {\n    type: 'class',\n    path: getApiFilePath(fullName, 'class'),\n    source: node.sources?.[0]?.url,\n    name,\n    aliases: getApiAliases(node.comment!),\n    description: getApiDescription(node.comment!),\n    example: node.comment?.getTag('@example')?.content\n      ? processApiComment(node.comment.getTag('@example')!.content)\n      : undefined,\n    signature,\n    constructor,\n    properties,\n    accessors,\n    methods,\n  }\n}\n\nfunction getClassBodySignature(node: typedoc.DeclarationReflection): string {\n  let constructorLine\n  let propertiesLine\n  let accessorsLine\n  let methodsLine\n\n  constructorLine = getChildrenSignature(node, (c) => c.kind === typedoc.ReflectionKind.Constructor)\n  propertiesLine = getChildrenSignature(node, (c) => c.kind === typedoc.ReflectionKind.Property)\n  accessorsLine = getChildrenSignature(node, (c) => c.kind === typedoc.ReflectionKind.Accessor)\n  methodsLine = getChildrenSignature(node, (c) => c.kind === typedoc.ReflectionKind.Method)\n\n  return [\n    constructorLine,\n    propertiesLine ? ['  // Properties', propertiesLine].join('\\n') : undefined,\n    accessorsLine ? ['  // Accessors', accessorsLine].join('\\n') : undefined,\n    methodsLine ? ['  // Methods', methodsLine].join('\\n') : undefined,\n  ]\n    .flat()\n    .filter(Boolean)\n    .join('\\n')\n}\n\nfunction getChildrenSignature(\n  node: typedoc.DeclarationReflection,\n  predicate?: (c: typedoc.Reflection) => boolean,\n): string {\n  let childrenSignature = ''\n  node.traverse((c) => {\n    if (c.isTypeParameter() || predicate?.(c) === false) {\n      return\n    }\n    if (c.kind === typedoc.ReflectionKind.Property) {\n      let childSignature = c.toString().replace(/^Property /, '')\n      if (c.flags.isOptional) {\n        childSignature = childSignature.replace(/: /, '?: ')\n      }\n      childrenSignature += `  ${childSignature}\\n`\n    } else if (c.kind === typedoc.ReflectionKind.Accessor && c.isDeclaration() && c.getSignature) {\n      let type = c.getSignature.type?.toString() ?? 'unknown'\n      childrenSignature += `  get ${c.name}(): ${type}\\n`\n    } else if (c.kind === typedoc.ReflectionKind.Method && c.isDeclaration()) {\n      c.getAllSignatures().forEach((signature) => {\n        let method = getApiMethod(c.name, signature)\n        invariant(method, `Failed to get method signature: ${c.getFriendlyFullName()}`)\n        childrenSignature += `  ${method.signature}\\n`\n      })\n    } else if (c.kind === typedoc.ReflectionKind.Constructor && c.isDeclaration()) {\n      c.getAllSignatures().forEach((signature) => {\n        let method = getApiMethod(c.name, signature)\n        invariant(method, `Failed to get constructor signature: ${c.getFriendlyFullName()}`)\n        childrenSignature += `  ${method.signature}\\n`\n      })\n    }\n  })\n  return childrenSignature\n}\n\nfunction getDocumentedInterface(\n  fullName: string,\n  node: typedoc.DeclarationReflection,\n): DocumentedInterface {\n  let { properties, accessors, methods } = getApiPropertiesAndMethods(fullName, node)\n\n  let signature = node.toString().replace(/^Interface/, 'interface')\n  let childrenSignature = getChildrenSignature(node)\n  if (childrenSignature) {\n    signature += ` {\\n${childrenSignature}\\n}`\n  }\n\n  return {\n    type: 'interface',\n    path: getApiFilePath(fullName, 'interface'),\n    source: node.sources?.[0]?.url,\n    name: getApiNameFromFullName(fullName),\n    aliases: node.comment ? getApiAliases(node.comment) : undefined,\n    description: node.comment ? getApiDescription(node.comment) : '',\n    signature,\n    properties,\n    accessors,\n    methods,\n  }\n}\n\nfunction getDocumentedType(fullName: string, node: typedoc.DeclarationReflection): DocumentedType {\n  let name = getApiNameFromFullName(fullName)\n\n  //TODO: We my need to do manual signature construction for types with generics\n  //\n  // The `Action` type here:\n  //   https://github.com/remix-run/remix/blob/ffdd2740b07b9c90518617b78831c255fa8aadd6/packages/fetch-router/src/lib/controller.ts#L40\n  //\n  //   type Action<method extends RequestMethod | 'ANY', pattern extends string> =\n  //     | RequestHandlerWithMiddleware<method, pattern>\n  //     | RequestHandler<method, Params<pattern>>\n  //\n  // Results in this via `toString()` and loses the generic `extends` stuff:\n  //\n  //   type Action<method, pattern> =\n  //     | RequestHandlerWithMiddleware<method, pattern>\n  //     | RequestHandler<method, Params<pattern>>;\n\n  let signature = node\n    .toString()\n    .replace(/^TypeAlias/, 'type')\n    .replace(new RegExp(`(${name}(<.*>)?): `), `$1 = `)\n  let childrenSignature = getChildrenSignature(node)\n  if (childrenSignature) {\n    signature += ` = {\\n${childrenSignature}\\n}`\n  }\n\n  return {\n    type: 'type',\n    path: getApiFilePath(fullName, 'type'),\n    source: node.sources?.[0]?.url,\n    name,\n    aliases: node.comment ? getApiAliases(node.comment) : undefined,\n    description: node.comment ? getApiDescription(node.comment) : '',\n    signature,\n  }\n}\n\nfunction getApiAliases(typedocComment: typedoc.Comment): string[] | undefined {\n  let tags = typedocComment.getTags('@alias')\n  if (!tags || tags.length === 0) {\n    return undefined\n  }\n  return tags.map((tag) => {\n    return tag.content.reduce((acc, part) => {\n      invariant(\n        part.kind === 'text',\n        `Invalid @alias tag content: ${typedocComment.getTags('@alias').join(', ')}`,\n      )\n      return acc + part.text\n    }, '')\n  })\n}\n\nfunction getApiFilePath(fullName: string, type: DocumentedAPI['type']): string {\n  let nameParts = fullName.split('.')\n  let name = nameParts.pop()\n  return [...nameParts.map((s) => s.replace(/^@remix-run\\//g, '')), type, `${name}.md`].join('/')\n}\n\nfunction getApiDescription(typedocComment: typedoc.Comment): string {\n  let description = typedocComment.summary\n    .map((part) => ('text' in part ? part.text : ''))\n    .join('')\n    .trim()\n  return description\n}\n\nfunction getApiPropertiesAndMethods(\n  fullName: string,\n  node: typedoc.DeclarationReflection,\n\n  handledTypes: Set<typedoc.ReflectionKind> = new Set(),\n): {\n  properties: ParameterOrProperty[]\n  accessors: ParameterOrProperty[]\n  methods: Method[]\n} {\n  let properties: ParameterOrProperty[] = []\n  let accessors: ParameterOrProperty[] = []\n  let methods: Method[] = []\n  node.traverse((child) => {\n    if (child.isDeclaration()) {\n      if (child.kind === typedoc.ReflectionKind.Property) {\n        let property = getApiParameterOrProperty(child)\n        if (property) {\n          properties.push(property)\n        }\n      } else if (child.kind === typedoc.ReflectionKind.Accessor) {\n        let accessor = getApiParameterOrProperty(child.getSignature)\n        if (accessor) {\n          accessors.push(accessor)\n        }\n      } else if (child.kind === typedoc.ReflectionKind.Method) {\n        child.getAllSignatures().forEach((signature) => {\n          let method = getApiMethod(fullName, signature)\n          if (method) {\n            methods.push(method)\n          }\n        })\n      } else if (!handledTypes.has(child.kind)) {\n        unimplemented(\n          `class child kind: ${typedoc.ReflectionKind[child.kind]} ${node.getFriendlyFullName()}`,\n        )\n      }\n    }\n  })\n  return { properties, accessors, methods }\n}\n\nfunction getApiMethod(fullName: string, node: typedoc.SignatureReflection): Method | undefined {\n  let parameters: ParameterOrProperty[] = []\n  node.traverse((child) => {\n    // Only process params, not type params (generics)\n    if (child.isParameter()) {\n      parameters = parameters.concat(getApiParameters(child))\n    } else if (child.isSignature()) {\n      child.traverse((param) => {\n        // Only process params, not type params (generics)\n        if (param.isParameter()) {\n          parameters = parameters.concat(getApiParameters(param))\n        }\n      })\n    }\n  })\n\n  let returnType = node.type ? node.type.toString() : 'void'\n\n  let typeParams = ''\n  if (node.typeParameters && node.typeParameters.length > 0) {\n    let typeParamStrs = node.typeParameters.map(\n      (tp) => tp.name + (tp.type ? ` extends ${tp.type.toString()}` : ''),\n    )\n    typeParams = `<${typeParamStrs.join(', ')}>`\n  }\n\n  let signatureParams = parameters.map((p) => `${p.name}: ${p.type}`).join(', ')\n\n  let signature: string\n  if (node.parent.kind === typedoc.ReflectionKind.Function) {\n    signature = `function ${node.name}${typeParams}(${signatureParams}): ${returnType}`\n  } else if (node.parent.kind === typedoc.ReflectionKind.Interface) {\n    signature = [\n      `interface ${node.name} {`,\n      `${typeParams}(${signatureParams}): ${returnType}`,\n      `}`,\n    ].join('\\n')\n  } else if (node.parent.kind === typedoc.ReflectionKind.Constructor) {\n    signature = `constructor(${signatureParams}): ${returnType}`\n  } else if (node.parent.kind === typedoc.ReflectionKind.Method) {\n    signature = `${node.name}${typeParams}(${signatureParams}): ${returnType}`\n  } else {\n    invariant(\n      false,\n      `Unhandled parent kind for method signature: ${typedoc.ReflectionKind[node.parent.kind]}`,\n    )\n  }\n\n  return {\n    name: getApiNameFromFullName(fullName),\n    signature,\n    description: node.comment?.summary ? processApiComment(node.comment.summary) : '',\n    parameters,\n    returns: node.comment?.getTag('@returns')?.content\n      ? processApiComment(node.comment.getTag('@returns')!.content)\n      : undefined,\n  }\n}\n\n// Get one or more parameters to document for a single function param.\n// Results in multiple params when the function param is an object with nested\n// fields. For example: `func(options: { a: boolean, b: string })`\nfunction getApiParameters(\n  node: typedoc.ParameterReflection | typedoc.ReferenceReflection,\n): ParameterOrProperty[] {\n  if (!node.isReference()) {\n    let param = getApiParameterOrProperty(node)\n    return param ? [param] : []\n  }\n\n  let api = node.getTargetReflectionDeep()\n\n  if (!api || api.kind === typedoc.ReflectionKind.TypeParameter) {\n    return []\n  }\n\n  // For now, we assume the class will be documented on it's own and we can just cross-link\n  // TODO: Cross-link to the class\n  if (api.kind === typedoc.ReflectionKind.Class) {\n    let param = getApiParameterOrProperty(node)\n    return param ? [param] : []\n  }\n\n  // Expand out individual fields of interfaces\n  if (api.kind === typedoc.ReflectionKind.Interface) {\n    let params: ParameterOrProperty[] = []\n    let param = getApiParameterOrProperty(node)\n    if (param) {\n      params.push(param)\n    }\n\n    api.traverse((child) => {\n      if (child.isDeclaration()) {\n        let childParam = getApiParameterOrProperty(child, [node.name])\n        if (childParam) {\n          params.push(childParam)\n        } else {\n          warn(`Missing comment for parameter: ${child.name} in ${api.getFriendlyFullName()}`)\n        }\n      }\n    })\n\n    return params\n  }\n\n  if (api.kind === typedoc.ReflectionKind.TypeAlias) {\n    let param = getApiParameterOrProperty(node)\n    return param ? [param] : []\n  }\n\n  throw new Error(`Unhandled parameter kind: ${typedoc.ReflectionKind[api.kind]}`)\n}\n\nfunction getApiParameterOrProperty(\n  node:\n    | typedoc.ParameterReflection\n    | typedoc.DeclarationReflection\n    | typedoc.SignatureReflection\n    | undefined,\n\n  prefix: string[] = [],\n): ParameterOrProperty | undefined {\n  invariant(node, 'Invalid node for comment')\n  return {\n    name: [...prefix, node.name].join('.'),\n    type: node.type ? node.type.toString() : 'unknown',\n    description: node.comment?.summary ? processApiComment(node.comment.summary) : '',\n  }\n}\n\nfunction processApiComment(parts: typedoc.CommentDisplayPart[]): string {\n  return parts.reduce((acc, part) => {\n    let transformed = part.text\n    if (part.kind === 'inline-tag' && part.tag === '@link') {\n      let target = part.target\n      let href\n      if (target) {\n        if (target instanceof typedoc.ReflectionSymbolId) {\n          // If it's a symbol typedoc knows about it'll find it in the typescript\n          // lib and we can use one of our MDN links\n          if (target.packageName === 'typescript') {\n            if (MDN_SYMBOLS.hasOwnProperty(target.qualifiedName)) {\n              let href = MDN_SYMBOLS[target.qualifiedName as keyof typeof MDN_SYMBOLS]!\n              transformed = `[\\`${part.text}\\`](${href})`\n            } else {\n              warn('Missing MDN link for TypeScript symbol: ', target.qualifiedName)\n            }\n          } else {\n            throw new Error(`Unsupported @link target: ${target.qualifiedName}`)\n          }\n        } else if (target instanceof typedoc.Reflection) {\n          // prettier-ignore\n          let type: DocumentedAPI['type'] | null =\n            target.kind === typedoc.ReflectionKind.Function ? 'function' :\n            target.kind === typedoc.ReflectionKind.Class ? 'class' :\n            target.kind === typedoc.ReflectionKind.TypeAlias ? 'type' :\n            target.kind === typedoc.ReflectionKind.TypeLiteral ? 'type' :\n            target.kind === typedoc.ReflectionKind.Interface ? 'interface' : null;\n\n          if (!type) {\n            throw new Error(`Unsupported @link target kind: ${typedoc.ReflectionKind[target.kind]}`)\n          }\n\n          let path = getApiFilePath(target.getFriendlyFullName(), type).replace(/\\.md$/, '')\n          href = `${WEBSITE_DOCS_PATH}/${path}/`\n          transformed = `[\\`${part.text}\\`](${href})`\n        } else {\n          throw new Error(`Missing/invalid target for @link content: ${part.text}`)\n        }\n      }\n    }\n\n    return acc + transformed\n  }, '')\n}\n\nfunction warnOnInvalidImportSyntax(api: DocumentedAPI) {\n  let str = JSON.stringify(api)\n  if (str.includes(\"from '@remix-run/\") || str.includes('from \"@remix-run/')) {\n    warn(\n      `Potential invalid import syntax in ${api.name} JSDoc. Prefer importing ` +\n        `from \\`remix/*\\` instead of \\`@remix-run/*\\`.`,\n    )\n  }\n}\n"
  },
  {
    "path": "docs/src/generate/index.ts",
    "content": "import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport util from 'node:util'\nimport { getDocumentedAPI } from './documented-api.ts'\nimport { writeMarkdownFiles } from './markdown.ts'\nimport { loadTypeDoc } from './typedoc.ts'\nimport { info } from './utils.ts'\n\nconst DOCS_DIR = path.join('build', 'md')\nconst TYPEDOC_DIR = path.join('build', 'typedoc')\n\n// Ensure we're running from the /docs directory\nlet cwd = process.cwd()\nif (!cwd.endsWith('/docs')) {\n  console.error('❌ This script must be run from the /docs directory')\n  process.exit(1)\n}\n\nlet { values: cliArgs } = util.parseArgs({\n  options: {\n    // Path to a TypeDoc JSON file to use as the input, instead of running Typedoc\n    // (mutually exclusive with `entryPoints`)\n    input: {\n      type: 'string',\n      short: 'i',\n    },\n    // Entrypoints to run typedoc against (mutually exclusive with `input`)\n    entryPoints: {\n      type: 'string',\n      short: 'e',\n      default: '../packages/*',\n    },\n    // Git tag to use for source code links from docs\n    tag: {\n      type: 'string',\n      short: 't',\n    },\n  },\n})\n\ninfo(`Clearing output directory: ${DOCS_DIR}`)\nawait fs.rm(DOCS_DIR, { recursive: true, force: true })\n\n// Load the full TypeDoc project and walk it to create a lookup map and\n// determine which APIs we want to generate documentation for\nlet { comments, apisToDocument } = await loadTypeDoc(\n  'input' in cliArgs && cliArgs.input\n    ? // When input is specified, we're operating off an existing typedoc api.json file\n      { input: cliArgs.input }\n    : // Otherwise, we run typedoc and write the output to TYPEDOC_DIR\n      { entryPoints: cliArgs.entryPoints, typedocDir: TYPEDOC_DIR, tag: cliArgs.tag },\n)\n\n// Parse JSDocs into DocumentedAPI instances we can write out to markdown\nlet documentedAPIs = [...apisToDocument].map((name) => getDocumentedAPI(name, comments.get(name)!))\n\n// Write out docs\nawait writeMarkdownFiles(documentedAPIs, DOCS_DIR)\ninfo('Documentation generation complete!')\n"
  },
  {
    "path": "docs/src/generate/markdown.ts",
    "content": "import * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport * as prettier from 'prettier'\nimport {\n  type DocumentedAPI,\n  type DocumentedClass,\n  type DocumentedFunction,\n  type DocumentedInterface,\n  type DocumentedInterfaceFunction,\n  type DocumentedType,\n} from './documented-api.ts'\nimport { debug, info, verbose, warn } from './utils.ts'\n\nexport async function writeMarkdownFiles(comments: DocumentedAPI[], docsDir: string) {\n  for (let comment of comments) {\n    let mdPath = path.join(docsDir, comment.path)\n    await fs.mkdir(path.dirname(mdPath), { recursive: true })\n    debug('Writing markdown file:', mdPath)\n    if (comment.type === 'function') {\n      await fs.writeFile(mdPath, await getFunctionMarkdown(comment))\n    } else if (comment.type === 'class') {\n      await fs.writeFile(mdPath, await getClassMarkdown(comment))\n    } else if (comment.type === 'interface') {\n      await fs.writeFile(mdPath, await getInterfaceMarkdown(comment))\n    } else if (comment.type === 'interface-function') {\n      await fs.writeFile(mdPath, await getInterfaceFunctionMarkdown(comment))\n    } else if (comment.type === 'type') {\n      await fs.writeFile(mdPath, await getTypeMarkdown(comment))\n    }\n  }\n}\n\nconst h = (level: number, heading: string, body?: string) =>\n  `${'#'.repeat(level)} ${heading}${body ? `\\n\\n${body}` : ''}`\nconst h1 = (heading: string) => h(1, heading)\nconst h2 = (heading: string, body: string) => h(2, heading, body)\nconst h3 = (heading: string, body: string) => h(3, heading, body)\nconst h4 = (heading: string, body: string) => h(4, heading, body)\nconst p = (content: string) => `${content}`\nconst pre = async (content: string, lang = 'ts') => {\n  if (content.includes('(...)')) {\n    // Prettier chokes on the ellipsis syntax in function signatures\n    info(\n      'Skipping formatting for code block with ellipsis syntax: ',\n      content.substring(0, 50) + '...',\n    )\n  } else {\n    try {\n      content = await prettier.format(content, { parser: 'typescript' })\n    } catch (e) {\n      warn(\n        'Failed to format code block, using unformatted content: ',\n        content.length > 30 ? content.substring(0, 30) + '...' : content,\n      )\n      verbose(e)\n    }\n  }\n  return `\\`\\`\\`${lang}\\n${content}\\n\\`\\`\\``\n}\n\nfunction frontmatter(comment: DocumentedAPI) {\n  return ['---', `title: ${comment.name}`, '---'].join('\\n')\n}\n\nfunction name(comment: DocumentedAPI) {\n  return h1(comment.name)\n}\n\nfunction source(comment: DocumentedAPI) {\n  return comment.source\n    ? p(`<a href=\"${comment.source}\" target=\"_blank\">View Source</a>`)\n    : undefined\n}\n\nfunction summary(comment: DocumentedAPI) {\n  return h2('Summary', comment.description)\n}\n\nfunction aliases(comment: DocumentedAPI) {\n  return comment.aliases ? h2('Aliases', comment.aliases.join(', ')) : undefined\n}\nasync function getFunctionMarkdown(comment: DocumentedFunction): Promise<string> {\n  return [\n    frontmatter(comment),\n    name(comment),\n    source(comment),\n    summary(comment),\n    aliases(comment),\n    h2('Signature', await pre(comment.signature)),\n    comment.example\n      ? h2(\n          'Example',\n          comment.example.trim().startsWith('```') ? comment.example : await pre(comment.example),\n        )\n      : undefined,\n    comment.parameters.length > 0\n      ? h2(\n          'Params',\n          comment.parameters.map((param) => h3(param.name, param.description)).join('\\n\\n'),\n        )\n      : undefined,\n    comment.returns ? h2('Returns', comment.returns) : undefined,\n  ]\n    .filter(Boolean)\n    .join('\\n\\n')\n}\n\nasync function getClassMarkdown(comment: DocumentedClass): Promise<string> {\n  return [\n    frontmatter(comment),\n    name(comment),\n    source(comment),\n    summary(comment),\n    aliases(comment),\n    h2('Signature', await pre(comment.signature)),\n    comment.example ? h2('Example', comment.example) : undefined,\n    comment.constructor\n      ? h2(\n          'Constructor Params',\n          [\n            comment.constructor.description,\n            ...comment.constructor.parameters.map((p) => h3(p.name, p.description)),\n          ]\n            .filter(Boolean)\n            .join('\\n\\n'),\n        )\n      : undefined,\n    comment.properties && comment.properties.length > 0\n      ? h2('Properties', comment.properties.map((p) => h3(p.name, p.description)).join('\\n\\n'))\n      : undefined,\n    comment.accessors && comment.accessors.length > 0\n      ? h2('Accessors', comment.accessors.map((p) => h3(p.name, p.description)).join('\\n\\n'))\n      : undefined,\n    comment.methods && comment.methods.length > 0\n      ? h2(\n          'Methods',\n          comment.methods\n            .map((m) =>\n              [\n                h3(m.signature, m.description),\n                ...m.parameters.map((p) => h4(p.name, p.description)),\n              ].join('\\n\\n'),\n            )\n            .join('\\n\\n'),\n        )\n      : undefined,\n  ]\n    .filter(Boolean)\n    .join('\\n\\n')\n}\n\nasync function getInterfaceMarkdown(comment: DocumentedInterface): Promise<string> {\n  return [\n    frontmatter(comment),\n    name(comment),\n    source(comment),\n    comment.description ? summary(comment) : null,\n    aliases(comment),\n    h2('Signature', await pre(comment.signature)),\n    comment.properties &&\n    comment.properties.length > 0 &&\n    comment.properties.some((p) => p.description)\n      ? h2('Properties', comment.properties.map((p) => h3(p.name, p.description)).join('\\n\\n'))\n      : undefined,\n    comment.accessors &&\n    comment.accessors.length > 0 &&\n    comment.accessors.some((p) => p.description)\n      ? h2('Accessors', comment.accessors.map((p) => h3(p.name, p.description)).join('\\n\\n'))\n      : undefined,\n    comment.methods && comment.methods.length > 0 && comment.methods.some((m) => m.description)\n      ? h2(\n          'Methods',\n          comment.methods\n            .map((m) =>\n              [\n                h3(m.signature, m.description),\n                ...m.parameters.map((p) => h4(p.name, p.description)),\n              ].join('\\n\\n'),\n            )\n            .join('\\n\\n'),\n        )\n      : undefined,\n  ]\n    .filter(Boolean)\n    .join('\\n\\n')\n}\n\nasync function getInterfaceFunctionMarkdown(comment: DocumentedInterfaceFunction): Promise<string> {\n  return [\n    frontmatter(comment),\n    name(comment),\n    source(comment),\n    comment.description ? summary(comment) : null,\n    aliases(comment),\n    h2('Signature', await pre(comment.signature)),\n    comment.parameters.length > 0 && comment.parameters.some((p) => p.description)\n      ? h2(\n          'Params',\n          comment.parameters.map((param) => h3(param.name, param.description)).join('\\n\\n'),\n        )\n      : undefined,\n    comment.returns ? h2('Returns', comment.returns) : undefined,\n  ]\n    .filter(Boolean)\n    .join('\\n\\n')\n}\n\nasync function getTypeMarkdown(comment: DocumentedType): Promise<string> {\n  return [\n    frontmatter(comment),\n    name(comment),\n    source(comment),\n    comment.description ? summary(comment) : null,\n    aliases(comment),\n    h2('Signature', await pre(comment.signature)),\n  ]\n    .filter(Boolean)\n    .join('\\n\\n')\n}\n"
  },
  {
    "path": "docs/src/generate/symbols.ts",
    "content": "// Ignore auto-linking for APIs of ours that conflict with built in symbols\nexport const IGNORE_SYMBOLS = new Set([\n  'any',\n  'array',\n  'bigint',\n  'boolean',\n  'map',\n  'number',\n  'object',\n  'set',\n  'string',\n  'symbol',\n])\n\nexport const MDN_SYMBOLS = {\n  AbortSignal: 'https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal',\n  ArrayBuffer:\n    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer',\n  Blob: 'https://developer.mozilla.org/en-US/docs/Web/API/Blob',\n  File: 'https://developer.mozilla.org/en-US/docs/Web/API/File',\n  FormData: 'https://developer.mozilla.org/en-US/docs/Web/API/FormData',\n  Headers: 'https://developer.mozilla.org/en-US/docs/Web/API/Headers',\n  JSON: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON',\n  Promise:\n    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise',\n  ReadableStream: 'https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream',\n  ReadableStreamDefaultReader:\n    'https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader',\n  Request: 'https://developer.mozilla.org/en-US/docs/Web/API/Request',\n  Response: 'https://developer.mozilla.org/en-US/docs/Web/API/Response',\n  SubtleCrypto: 'https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto',\n  TextDecoder: 'https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder',\n  TextEncoder: 'https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder',\n  TransformStream: 'https://developer.mozilla.org/en-US/docs/Web/API/TransformStream',\n  Uint8Array:\n    'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array',\n  URL: 'https://developer.mozilla.org/en-US/docs/Web/API/URL',\n  URLSearchParams: 'https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams',\n  WritableStream: 'https://developer.mozilla.org/en-US/docs/Web/API/WritableStream',\n}\n"
  },
  {
    "path": "docs/src/generate/typedoc.ts",
    "content": "import * as path from 'node:path'\nimport * as typedoc from 'typedoc'\nimport { debug, getApiNameFromFullName, info, invariant, verbose, warn } from './utils.ts'\n\ntype Maps = {\n  comments: Map<string, typedoc.Reflection> // full name => TypeDoc Reflection\n  apisToDocument: Set<string> // APIs we should generate docs for\n}\n\nexport async function loadTypeDoc(opts: {\n  input?: string\n  entryPoints?: string\n  typedocDir?: string\n  tag?: string\n}) {\n  // Load the full TypeDoc project and walk it to create a lookup map and\n  // determine which APIs we want to generate documentation for\n  let project = await loadTypedocJson(opts)\n\n  let { comments, apisToDocument } = createLookupMaps(project)\n\n  // Prefer `remix` package exports over other package exports\n  getDuplicateAPIs(apisToDocument).forEach((name) => apisToDocument.delete(name))\n\n  // Remove aliased APIs and only document the canonicals\n  getAliasedAPIs(comments).forEach((name) => apisToDocument.delete(name))\n\n  return { comments, apisToDocument }\n}\n\n// Load the TypeDoc JSON representation, either from a JSON file or by running\n// TypeDoc against the project\nasync function loadTypedocJson(opts: {\n  input?: string\n  entryPoints?: string\n  typedocDir?: string\n  tag?: string\n}): Promise<typedoc.ProjectReflection> {\n  if (opts.input) {\n    info(`Loading TypeDoc JSON from: ${opts.input}`)\n    let app = await typedoc.Application.bootstrap({\n      name: 'Remix',\n      entryPoints: [opts.input],\n      entryPointStrategy: 'merge',\n    })\n    let reflection = await app.convert()\n    invariant(reflection, 'Failed to generate TypeDoc reflection from JSON file')\n    return reflection\n  } else if (opts.entryPoints && opts.typedocDir) {\n    info(`Generating TypeDoc from project`)\n    let app = await typedoc.Application.bootstrap({\n      name: 'Remix',\n      entryPoints: [opts.entryPoints],\n      entryPointStrategy: 'packages',\n      packageOptions: {\n        // Allow custom tags\n        blockTags: [...typedoc.OptionDefaults.blockTags, '@alias'],\n        // Tag to use in source code links\n        gitRevision: opts.tag,\n        // exclude test files via the build config\n        tsconfig: 'tsconfig.build.json',\n        validation: {\n          // Don't warn for referenced but not exported types\n          notExported: false,\n        },\n      },\n    })\n    let reflection = await app.convert()\n    invariant(reflection, 'Failed to generate TypeDoc reflection from source code')\n\n    let outPath = path.resolve(process.cwd(), opts.typedocDir)\n    await app.renderer.render(reflection!, outPath)\n\n    let jsonPath = path.join(outPath, 'api.json')\n    await app.application.generateJson(reflection, jsonPath)\n\n    info(`HTML docs generated at: ${outPath}`)\n    info(`JSON docs generated at: ${jsonPath}`)\n\n    return reflection\n  } else {\n    throw new Error('Invalid options: must specify either `input` or `entryPoints`/`typedocDir`')\n  }\n}\n\n// Walk the TypeDoc reflection and collect all APIs we wish to document as well\n// as generate a full lookup map of JSDoc comments by API name\nexport function createLookupMaps(reflection: typedoc.ProjectReflection): Maps {\n  let comments = new Map<string, typedoc.Reflection>()\n  let apisToDocument = new Set<string>()\n\n  // Reflections we want to traverse through to find documented APIs\n  let traverseKinds = new Set<typedoc.ReflectionKind>([\n    typedoc.ReflectionKind.Module,\n    typedoc.ReflectionKind.Function,\n    typedoc.ReflectionKind.CallSignature,\n    typedoc.ReflectionKind.Class,\n    typedoc.ReflectionKind.Interface,\n    typedoc.ReflectionKind.TypeAlias,\n    // TODO: Not implemented yet - used for interactions like arrowLeft etc. so\n    // we eventually will probably want to support\n    // typedoc.ReflectionKind.Variable,\n  ])\n\n  recurse(reflection)\n\n  return { comments, apisToDocument }\n\n  function recurse(node: typedoc.Reflection, alias?: string) {\n    node.traverse((child) => {\n      let apiName = alias || child.getFriendlyFullName()\n      apiName = apiName.replace(/\\.\\.+/g, '.') // Clean up any `..` in top-level remix re-exports\n\n      if (child.kind !== typedoc.ReflectionKind.Module) {\n        comments.set(apiName, child)\n      }\n\n      let indent = '  '.repeat(apiName.split('.').length - 1)\n      let logApi = (suffix: string) =>\n        verbose(\n          [\n            `${indent}[${typedoc.ReflectionKind[child.kind]}]`,\n            apiName,\n            `(${child.id})`,\n            `(${suffix})`,\n          ].join(' '),\n        )\n\n      // Recurse into reference types\n      if (child.isReference()) {\n        logApi(`reference to ${child.getTargetReflectionDeep().getFriendlyFullName()}`)\n        let ref = child.getTargetReflection()\n        recurse(ref, child.getFriendlyFullName())\n        return\n      }\n\n      // Skip nested properties, methods, etc. that we don't intend to document standalone\n      if (!traverseKinds.has(child.kind)) {\n        logApi(`skipped`)\n        return\n      }\n\n      // Grab APIs with JSDoc comments that we should generate docs for\n      // We don't need comments for types since those can stand alone in the docs\n      if (\n        child.comment ||\n        [typedoc.ReflectionKind.Interface, typedoc.ReflectionKind.TypeAlias].includes(child.kind)\n      ) {\n        apisToDocument.add(apiName)\n        logApi(`commenting`)\n      }\n\n      // No need to traverse past signatures, do that when we generate the comment\n      if (!child.isSignature()) {\n        recurse(child)\n      }\n    })\n  }\n}\n\n// Deduplicate APIs that are exported from multiple packages, preferring the remix package\nfunction getDuplicateAPIs(apisToDocument: Set<string>): Set<string> {\n  let apisByName = new Map<string, string[]>()\n  let duplicates = new Set<string>()\n\n  // Group APIs by short name\n  for (let fullName of apisToDocument) {\n    let apiName = getApiNameFromFullName(fullName)\n    apisByName.set(apiName, [...(apisByName.get(apiName) || []), fullName])\n  }\n\n  // Process each group of APIs with the same name\n  for (let [apiName, fullNames] of apisByName) {\n    if (fullNames.length <= 1) {\n      continue\n    }\n\n    let remixAPIs = fullNames.filter(\n      (name) => name.split('.').length === 2 && name.split('.')[0] === 'remix',\n    )\n    let deepRemixAPIs = fullNames.filter(\n      (name) => name.split('.').length > 2 && name.split('.')[0] === 'remix',\n    )\n    let nonRemixAPIs = fullNames.filter((name) => name.split('.')[0] !== 'remix')\n\n    if (remixAPIs.length > 1) {\n      throw new Error(`Cannot have the same API exported from multiple packages: ${apiName}`)\n    }\n\n    if (remixAPIs.length === 1) {\n      // Remove non-remix APIs, keep the remix one\n      for (let api of [...deepRemixAPIs, ...nonRemixAPIs]) {\n        debug(`Preferring \\`remix\\` export for ${apiName}, removing: ${api}`)\n        duplicates.add(api)\n      }\n    } else if (deepRemixAPIs.length > 0) {\n      // Remove non-remix APIs, keep the remix/* one\n      for (let api of nonRemixAPIs) {\n        debug(`Preferring \\`${deepRemixAPIs[0]}\\` export, removing: ${api}`)\n        duplicates.add(api)\n      }\n    } else if (fullNames.length > 1) {\n      // Multiple non-remix packages export this API\n      warn(`Multiple packages export ${apiName}: ${fullNames.join(', ')}`)\n    }\n  }\n\n  return duplicates\n}\n\nfunction getAliasedAPIs(comments: Map<string, typedoc.Reflection>): Set<string> {\n  let aliasedAPIs = new Set<string>()\n\n  comments.forEach((reflection, name) => {\n    let parts = name.split('.')\n    let apiName = parts.pop()\n    let alias = reflection.comment?.blockTags.find((tag) => tag.tag === '@alias')\n    if (alias) {\n      // The canonical API should include `@alias`\n      // We will generate a markdown doc for the canonical API, and not the aliases\n      // The canonical doc will list the aliases names\n      let aliasName = alias.content.reduce((acc, part) => {\n        invariant(part.kind === 'text')\n        return acc + part.text\n      }, '')\n      if (apiName !== aliasName) {\n        let aliasFullName = [...parts, aliasName].join('.')\n        debug(`Preferring canonical API \\`${name}\\` over alias \\`${aliasFullName}\\``)\n        aliasedAPIs.add(aliasFullName)\n      }\n    }\n  })\n\n  return aliasedAPIs\n}\n\n// Reference TypeDoc types\n\n// export declare enum ReflectionKind {\n//     Project = 1,\n//     Module = 2,\n//     Namespace = 4,\n//     Enum = 8,\n//     EnumMember = 16,\n//     Variable = 32,\n//     Function = 64,\n//     Class = 128,\n//     Interface = 256,\n//     Constructor = 512,\n//     Property = 1024,\n//     Method = 2048,\n//     CallSignature = 4096,\n//     IndexSignature = 8192,\n//     ConstructorSignature = 16384,\n//     Parameter = 32768,\n//     TypeLiteral = 65536,\n//     TypeParameter = 131072,\n//     Accessor = 262144,\n//     GetSignature = 524288,\n//     SetSignature = 1048576,\n//     TypeAlias = 2097152,\n//     Reference = 4194304,\n//     /**\n//      * Generic non-ts content to be included in the generated docs as its own page.\n//      */\n//     Document = 8388608\n// }\n\n// export interface ReflectionVariant {\n//     declaration: DeclarationReflection;\n//     param: ParameterReflection;\n//     project: ProjectReflection;\n//     reference: ReferenceReflection;\n//     signature: SignatureReflection;\n//     typeParam: TypeParameterReflection;\n//     document: DocumentReflection;\n// }\n"
  },
  {
    "path": "docs/src/generate/utils.ts",
    "content": "export function getApiNameFromFullName(fullName: string): string {\n  return fullName.split('.').slice(-1)[0]\n}\n\nconst isVerbose = process.env.DEBUG === 'verbose' || process.env.DEBUG === '2'\n\nexport function verbose(...args: unknown[]) {\n  if (process.env.DEBUG && isVerbose) {\n    console.debug('🔎', padStart('VERBOSE'), ...args)\n  }\n}\n\nexport function debug(...args: unknown[]) {\n  if (process.env.DEBUG) {\n    console.debug('🛠️', padStart('DEBUG'), ...args)\n  }\n}\n\nexport function info(...args: unknown[]) {\n  console.log('ℹ️', padStart('INFO'), ...args)\n}\n\nexport function warn(...args: unknown[]) {\n  console.warn('⚠️', padStart('WARN'), ...args)\n}\n\nexport function unimplemented(...args: unknown[]) {\n  console.error('‼️', padStart('ERROR'), 'Unimplemented:', ...args)\n}\n\nexport function invariant(condition: unknown, message?: string): asserts condition {\n  if (!condition) {\n    throw new Error(message ?? 'Invariant violation')\n  }\n}\n\nfunction padStart(str: string, fill: string = ' '): string {\n  return str.padStart(isVerbose ? 7 : 5, fill) + ':'\n}\n"
  },
  {
    "path": "docs/src/server/components.tsx",
    "content": "import type { RemixNode } from 'remix/component/jsx-runtime'\nimport type { DocFile } from './markdown.ts'\nimport { routes } from './routes.ts'\nimport type { Handle } from 'remix/component'\n\nexport function Home() {\n  return () => {\n    return (\n      <div class=\"home\">\n        <h1>Remix API Documentation</h1>\n        <p>Select a document from the sidebar to get started.</p>\n      </div>\n    )\n  }\n}\n\nexport function NotFound() {\n  return ({ slug }: { slug: string }) => {\n    return (\n      <div class=\"error\">\n        <h2>Not Found</h2>\n        <p>The requested document was not found:</p>\n        <p>{slug}</p>\n      </div>\n    )\n  }\n}\n\nexport type ServerContext = {\n  docFiles: DocFile[]\n  versions: { version: string; crawl: boolean }[]\n  activeVersion?: string\n  slug?: string\n}\n\nexport function ServerPage(handle: Handle<ServerContext>, setup: ServerContext) {\n  handle.context.set(setup)\n  return ({ children }: { children: RemixNode | RemixNode[] }) => (\n    <Document>\n      <Layout>{children}</Layout>\n    </Document>\n  )\n}\n\nfunction Document(handle: Handle) {\n  let { activeVersion } = handle.context.get(ServerPage)\n  return ({ children }: { children: RemixNode | RemixNode[] }) => (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"UTF-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        {activeVersion != null ? (\n          <>\n            <meta name=\"robots\" content=\"noindex,nofollow\" />\n            <meta name=\"googlebot\" content=\"noindex,nofollow\" />\n          </>\n        ) : null}\n\n        <title>Remix API Documentation</title>\n        <link\n          href={routes.assets.href({ version: activeVersion, asset: 'docs.css' })}\n          rel=\"stylesheet\"\n        />\n        <script\n          async\n          type=\"module\"\n          src={routes.assets.href({ version: activeVersion, asset: 'entry.js' })}\n        />\n      </head>\n      <body>{children}</body>\n    </html>\n  )\n}\n\nfunction Layout() {\n  return ({ children }: { children: RemixNode | RemixNode[] }) => (\n    <>\n      <input class=\"nav-toggle\" id=\"nav-toggle\" type=\"checkbox\" aria-hidden=\"true\" tabIndex={-1} />\n      <div class=\"mobile-header\">\n        <label class=\"nav-toggle-open\" for=\"nav-toggle\" aria-label=\"Open navigation menu\">\n          <span aria-hidden=\"true\">☰</span>\n          <span class=\"visually-hidden\">Open navigation menu</span>\n        </label>\n      </div>\n      <div class=\"container\">\n        <main class=\"main\">\n          <div class=\"content\">{children}</div>\n        </main>\n        <div class=\"sidebar\">\n          <header>\n            <a href={routes.home.href({ version: undefined })} class=\"logo\">\n              <RemixLogoLight />\n              <RemixLogoDark />\n            </a>\n            <div class=\"sidebar-actions\">\n              <label class=\"nav-toggle-close\" for=\"nav-toggle\" aria-label=\"Close navigation menu\">\n                <span aria-hidden=\"true\">×</span>\n                <span class=\"visually-hidden\">Close navigation menu</span>\n              </label>\n            </div>\n          </header>\n          <nav>\n            <VersionDropdown />\n            <Nav />\n          </nav>\n        </div>\n        <div id=\"nav-overlay\" aria-hidden=\"true\" />\n      </div>\n    </>\n  )\n}\n\nexport function VersionDropdown(handle: Handle) {\n  let { versions, activeVersion, slug } = handle.context.get(ServerPage)\n\n  // When we're displaying an active version, only include versions up until\n  // that version in the nav\n  let navVersions = versions\n  if (activeVersion) {\n    let idx = versions.findIndex((v) => v.version === activeVersion)\n    if (idx >= 0) {\n      navVersions = versions.slice(idx)\n    }\n  }\n\n  return () => (\n    <NavDropdown title=\"Version\" open={activeVersion != null}>\n      <ul>\n        <li>\n          {...navVersions.map((version) => {\n            let latest =\n              (versions.length === 0 || version.version === versions[0]?.version) && !activeVersion\n            let active = !slug && version.version === activeVersion\n            let href = routes.home.href({ version: !latest ? version.version : undefined })\n            return (\n              <>\n                <a\n                  href={href}\n                  class={active ? 'active' : undefined}\n                  rel={!latest && !version.crawl ? 'nofollow' : undefined}\n                >\n                  {version.version}\n                </a>\n                {latest ? <span> (latest)</span> : null}\n                <br />\n              </>\n            )\n          })}\n        </li>\n      </ul>\n    </NavDropdown>\n  )\n}\n\ntype ApiTypes = {\n  type: DocFile[]\n  interface: DocFile[]\n  function: DocFile[]\n  class: DocFile[]\n}\n\nexport function Nav(handle: Handle) {\n  let { docFiles, slug } = handle.context.get(ServerPage)\n  return () => {\n    let packageGroups = new Map<string, ApiTypes>()\n    let activePackage = undefined\n\n    for (let file of docFiles) {\n      if (!packageGroups.has(file.package)) {\n        packageGroups.set(file.package, { type: [], interface: [], function: [], class: [] })\n      }\n      packageGroups.get(file.package)![file.type as keyof ApiTypes].push(file)\n      if (file.urlPath === slug) {\n        activePackage = file.package\n      }\n    }\n\n    let sortedNavItems = Array.from(packageGroups.entries()).sort((a, b) => {\n      // remix above remix/*\n      if (a[0] === 'remix' && b[0].startsWith('remix/')) return -1\n      if (b[0] === 'remix' && a[0].startsWith('remix/')) return 1\n      // remix/* alphabetical\n      if (a[0].startsWith('remix/') && b[0].startsWith('remix/')) return a[0].localeCompare(b[0])\n      // remix and remix/* above all others\n      if (a[0] === 'remix' || a[0].startsWith('remix/')) return -1\n      if (b[0] === 'remix' || b[0].startsWith('remix/')) return 1\n      // Everything else alphabetical\n      return a[0].localeCompare(b[0])\n    })\n\n    return sortedNavItems.map(([packageName, files]) => (\n      <NavDropdown title={packageName} open={packageName === activePackage}>\n        <NavDropdownSection title=\"Types\" files={files} type=\"type\" />\n        <NavDropdownSection title=\"Interfaces\" files={files} type=\"interface\" />\n        <NavDropdownSection title=\"Classes\" files={files} type=\"class\" />\n        <NavDropdownSection title=\"Functions\" files={files} type=\"function\" />\n      </NavDropdown>\n    ))\n  }\n}\n\nfunction NavDropdown(handle: Handle) {\n  return ({\n    title,\n    open,\n    children,\n  }: {\n    title: string\n    open: boolean\n    children: RemixNode | RemixNode[]\n  }) => (\n    <details open={open}>\n      <summary>{title}</summary>\n      <div class=\"items\">{children}</div>\n    </details>\n  )\n}\n\nfunction NavDropdownSection(handle: Handle) {\n  let { activeVersion: version, slug } = handle.context.get(ServerPage)\n  return ({ title, files, type }: { title: string; files: ApiTypes; type: keyof ApiTypes }) => {\n    if (files[type].length === 0) {\n      return null\n    }\n\n    return (\n      <>\n        <ul>\n          <li>{title}</li>\n          <ul>\n            {...files[type].map((file) => (\n              <li>\n                <a\n                  href={routes.docs.href({ version, slug: file.urlPath })}\n                  class={slug === file.urlPath ? 'active' : undefined}\n                >\n                  {file.name}\n                </a>\n              </li>\n            ))}\n          </ul>\n        </ul>\n      </>\n    )\n  }\n}\n\nexport function MarkdownContent() {\n  return ({ html }: { html: string }) => <div innerHTML={html} />\n}\n\nfunction RemixLogoLight(handle: Handle) {\n  let { activeVersion } = handle.context.get(ServerPage)\n  return () => {\n    return (\n      <div class=\"light\">\n        <img\n          src={routes.assets.href({\n            version: activeVersion,\n            asset: 'remix-wordmark-lightmode.svg',\n          })}\n          alt=\"Remix\"\n          style=\"width: 100%; height: 100%;\"\n        />\n      </div>\n    )\n  }\n}\n\nfunction RemixLogoDark(handle: Handle) {\n  let { activeVersion } = handle.context.get(ServerPage)\n  return () => {\n    return (\n      <div class=\"dark\">\n        <img\n          src={routes.assets.href({ version: activeVersion, asset: 'remix-wordmark-darkmode.svg' })}\n          alt=\"Remix\"\n          style=\"width: 100%; height: 100%;\"\n        />\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "docs/src/server/index.ts",
    "content": "import * as http from 'node:http'\nimport { createRequestListener } from 'remix/node-fetch-server'\nimport { createRouter, getDefaultVersions } from './router.tsx'\n\nlet router = createRouter(getDefaultVersions())\n\nlet server = http.createServer(\n  createRequestListener(async (request) => {\n    try {\n      return await router.fetch(request)\n    } catch (error) {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  }),\n)\n\nlet port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000\n\nserver.listen(port, () => {\n  console.log(`Remix API docs server running on http://localhost:${port}`)\n})\n\nlet shuttingDown = false\n\nfunction shutdown() {\n  if (shuttingDown) return\n  shuttingDown = true\n  server.close(() => {\n    process.exit(0)\n  })\n}\n\nprocess.on('SIGINT', shutdown)\nprocess.on('SIGTERM', shutdown)\n"
  },
  {
    "path": "docs/src/server/markdown.ts",
    "content": "import * as frontmatter from 'front-matter'\nimport type { Element } from 'hast'\nimport { Marked, type MarkedExtension } from 'marked'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { codeToHtml } from 'shiki'\nimport { routes } from './routes.ts'\nimport { IGNORE_SYMBOLS, MDN_SYMBOLS } from '../generate/symbols.ts'\n\n// No types exist for the `frontmatter` package\nconst parseFrontmatter = frontmatter.default as unknown as (md: string) => {\n  attributes: Record<string, any>\n  body: string\n}\n\nexport type DocFile = {\n  path: string\n  type: string\n  name: string\n  package: string\n  urlPath: string\n}\n\nexport async function discoverMarkdownFiles(\n  baseDir: string,\n): Promise<{ docFiles: DocFile[]; docFilesLookup: Map<string, DocFile> }> {\n  let files: DocFile[] = []\n  walk(baseDir)\n  let docFiles = files.sort((a, b) => a.urlPath.localeCompare(b.urlPath))\n  const docFilesLookup = new Map<string, DocFile>()\n  for (let file of docFiles) {\n    docFilesLookup.set(file.name, file)\n  }\n  return { docFiles, docFilesLookup }\n\n  function walk(dir: string) {\n    let entries = fs.readdirSync(dir, { withFileTypes: true })\n\n    for (let entry of entries) {\n      let fullPath = path.join(dir, entry.name)\n\n      if (entry.isDirectory()) {\n        walk(fullPath)\n      } else if (entry.isFile() && entry.name.endsWith('.md')) {\n        let relativePath = path.relative(baseDir, fullPath)\n        let parts = relativePath.split(path.sep)\n        let packageName = parts.slice(0, parts.length - 2).join('/')\n        let type = parts[parts.length - 2]\n        let urlPath = relativePath.replace(/\\.md$/, '').replace(/\\\\/g, '/')\n\n        files.push({\n          path: fullPath,\n          type: type || 'unknown',\n          name: entry.name.replace(/\\.md$/, ''),\n          package: packageName,\n          urlPath: urlPath,\n        })\n      }\n    }\n  }\n}\n\nexport async function renderMarkdownFile(\n  filePath: string,\n  docFilesLookup: Map<string, DocFile>,\n  version?: string,\n): Promise<string> {\n  try {\n    let markdown = fs.readFileSync(filePath, 'utf-8')\n    let { attributes, body } = parseFrontmatter(markdown)\n    let marked = new Marked(getShikiExtension(attributes.title || '', docFilesLookup, version))\n    let htmlContent = await marked.parse(body)\n    return htmlContent\n  } catch (error) {\n    return `\n      <div class=\"error\">\n        <h2>Error loading file</h2>\n        <p>Could not read file: ${filePath}</p>\n      </div>\n    `\n  }\n}\n\nfunction getShikiExtension(\n  apiName: string,\n  docFilesLookup: Map<string, DocFile>,\n  version?: string,\n): MarkedExtension {\n  return {\n    async: true,\n    async walkTokens(token) {\n      if (token.type === 'code') {\n        try {\n          token.text = await codeToHtml(token.text, {\n            lang: token.lang || 'typescript',\n            themes: {\n              light: 'github-light',\n              // See Shiki styles in docs.css for activation\n              dark: 'github-dark',\n            },\n            includeExplanation: true,\n            transformers: [\n              // Insert cross-links to known APIs\n              {\n                span(node, line, col) {\n                  // We only enhance single-symbol spans of word characters,\n                  // skipping spans for parens, braces, etc\n                  if (\n                    node.children.length !== 1 ||\n                    !('value' in node.children[0]) ||\n                    !/^[\\w ]+$/i.test(node.children[0].value)\n                  ) {\n                    return\n                  }\n\n                  let symbol = node.children[0].value\n\n                  // Capture leading/trailing spaces for later\n                  let leadingSpaces = symbol.length - symbol.trimStart().length\n                  let trailingSpaces = symbol.length - symbol.trimEnd().length\n                  symbol = symbol?.trim()\n\n                  // Don't link to the current page\n                  if (symbol === apiName) return\n                  // Don't link to anything in the ignore list\n                  if (IGNORE_SYMBOLS.has(symbol)) return\n\n                  // We don't want to auto link parameter names, function names, etc.\n                  // The things we do want to link (mostly type annotations) don't seem\n                  // to have an explanation so we use that to decide when to link\n                  if (this.tokens[line]?.[col]?.explanation != null) return\n\n                  let linkEl: Element | undefined\n                  if (docFilesLookup.has(symbol)) {\n                    linkEl = link(symbol, {\n                      href: routes.docs.href({\n                        version,\n                        slug: docFilesLookup.get(symbol)!.urlPath,\n                      }),\n                    })\n                  } else if (MDN_SYMBOLS.hasOwnProperty(symbol)) {\n                    linkEl = link(symbol, {\n                      href: MDN_SYMBOLS[symbol as keyof typeof MDN_SYMBOLS],\n                      target: '_blank',\n                    })\n                  }\n\n                  if (linkEl) {\n                    node.children = [\n                      ...(leadingSpaces ? spacer(leadingSpaces) : []),\n                      linkEl,\n                      ...(trailingSpaces ? spacer(trailingSpaces) : []),\n                    ]\n                  }\n                },\n              },\n            ],\n          })\n        } catch (error) {\n          console.error(`Shiki highlighting failed for token: ${JSON.stringify(token)}`)\n          console.error(error)\n        }\n      }\n    },\n    renderer: {\n      code(code) {\n        return code.text\n      },\n    },\n  }\n\n  // Spacer elements to preserve whitespace outside the inserted <a> elements\n  function spacer(num: number) {\n    return [\n      {\n        type: 'text',\n        value: ' '.repeat(num),\n      } as const,\n    ]\n  }\n\n  function link(\n    text: string,\n    attrs: { href: HTMLAnchorElement['href']; target?: HTMLAnchorElement['target'] },\n  ): Element {\n    return {\n      type: 'element',\n      tagName: 'a',\n      properties: attrs,\n      children: [\n        {\n          type: 'text',\n          value: text,\n        },\n      ],\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/server/prerender.ts",
    "content": "import * as cp from 'node:child_process'\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport * as util from 'node:util'\nimport { parse } from 'node-html-parser'\nimport * as semver from 'semver'\nimport { type Router } from 'remix/fetch-router'\nimport { createRouter, getDefaultVersions } from './router.tsx'\nimport type { ServerContext } from './components.tsx'\nimport { routes } from './routes.ts'\n\nlet { values: cliArgs } = util.parseArgs({\n  options: {\n    dir: {\n      type: 'string',\n      short: 'd',\n      default: 'build/site',\n    },\n  },\n})\n\nconst assetsDir = path.join(process.cwd(), 'build', 'assets')\nconst outputDir = path.join(process.cwd(), cliArgs.dir)\nconst versions = await getVersionsToBuild()\nconsole.log('Prerendering versions:\\n', JSON.stringify(versions, null, 2))\n\nconst docsRouter = createRouter(versions)\n\n// Copy static assets to the output directory\nawait fs.cp(assetsDir, path.join(outputDir, 'assets'), { recursive: true })\nfor (let version of versions || getDefaultVersions()) {\n  if (version.crawl) {\n    await fs.cp(assetsDir, path.join(outputDir, version.version, 'assets'), { recursive: true })\n  }\n}\n\nawait spider(\n  docsRouter,\n  outputDir,\n  new Set(['/', ...(versions?.filter((v) => v.crawl).map((v) => `/${v.version}/`) || [])]),\n)\n\n// Spider the website served by router, beginning at /\nasync function spider(router: Router, outputDir: string, urlQueue = new Set(['/'])) {\n  await fs.mkdir(outputDir, { recursive: true })\n\n  // Track URLs we have already downloaded to avoid loops\n  let downloadedUrls = new Set<string>()\n\n  for (let urlPath of urlQueue) {\n    if (urlPath && !downloadedUrls.has(urlPath)) {\n      let { downloadedUrl, discoveredUrls } = await crawl(router, urlPath, outputDir)\n      downloadedUrls.add(downloadedUrl)\n      discoveredUrls\n        .filter((href) => !downloadedUrls.has(href))\n        .forEach((href) => urlQueue.add(href))\n    }\n  }\n\n  console.log(`\\nCrawling complete!`)\n}\n\nasync function crawl(router: Router, urlPath: string, outputDir: string) {\n  let response\n  try {\n    response = await router.fetch(new Request(`http://localhost${urlPath}`))\n    if (!response.ok) {\n      throw new Error(`Error fetching ${urlPath}: ${response.status} ${response.statusText}`)\n    }\n  } catch (error) {\n    console.error('Error fetching', urlPath)\n    throw error\n  }\n\n  let isHtmlFile = response.headers.get('Content-Type')?.includes('text/html')\n\n  // Always put `index.html` files into directories - this leads to the best\n  // support with and without trailing slashes on github pages:\n  // https://github.com/slorber/trailing-slash-guide?tab=readme-ov-file#summary\n  let outputPath = isHtmlFile\n    ? path.join(outputDir, urlPath, 'index.html')\n    : path.join(outputDir, urlPath)\n\n  console.log(`Crawled ${urlPath} -> ./${path.relative(process.cwd(), outputPath)}`)\n\n  await fs.mkdir(path.dirname(outputPath), { recursive: true })\n\n  if (isHtmlFile) {\n    let html = await response.text()\n    await fs.writeFile(outputPath, html, 'utf-8')\n\n    // Parse HTML files for other resources/links to add to queue\n    return {\n      downloadedUrl: urlPath,\n      discoveredUrls: parse(html)\n        .querySelectorAll('a:not([rel=\"nofollow\"]),link:not([rel=\"preload\"]):not([rel=\"prefetch\"])')\n        .map((link) => link.getAttribute('href'))\n        .filter((href) => href && !isAbsoluteUrl(href))\n        .map((href) => resolveRelativeLink(href!, urlPath))\n        .flatMap((href) => {\n          let match = routes.docs.match(`http://localhost${href}`)\n          return match ? [href, routes.markdown.href(match.params)] : [href]\n        }),\n    }\n  } else {\n    let content = await response.arrayBuffer()\n    await fs.writeFile(outputPath, new Uint8Array(content))\n    return { downloadedUrl: urlPath, discoveredUrls: [] }\n  }\n}\n\nfunction isAbsoluteUrl(href: string): boolean {\n  return href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')\n}\n\nfunction resolveRelativeLink(link: string, url: string): string {\n  if (link.startsWith('/')) {\n    return link\n  }\n\n  // Handle relative paths like '../' or 'page'\n  let base = url.endsWith('/') ? url : path.dirname(url)\n  return path.posix.join(base, link)\n}\n\nasync function getVersionsToBuild(): Promise<ServerContext['versions']> {\n  // Get all Remix v3 tags, transform them to vX.Y.Z format, sort newest to oldest\n  const remixVersions = cp\n    .execSync('git tag', { encoding: 'utf-8' })\n    .trim()\n    .split('\\n')\n    .filter((tag) => tag.startsWith('remix@3'))\n    .map((tag) => tag.replace('remix@', 'v'))\n    .filter((tag) => semver.valid(tag) && !semver.prerelease(tag))\n    .sort((a, b) => semver.rcompare(a, b))\n\n  // Crawl only the most recent tag\n  return remixVersions.length > 0\n    ? remixVersions.map((tag, i) => ({ version: tag, crawl: i === 0 }))\n    : getDefaultVersions()\n}\n"
  },
  {
    "path": "docs/src/server/router.tsx",
    "content": "import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport * as semver from 'semver'\nimport { type RemixNode } from 'remix/component'\nimport { renderToStream } from 'remix/component/server'\nimport { createRouter as _createRouter, type Router } from 'remix/fetch-router'\nimport { createHtmlResponse } from 'remix/response/html'\nimport { ServerPage, Home, NotFound, type ServerContext } from './components.tsx'\nimport { discoverMarkdownFiles, renderMarkdownFile } from './markdown.ts'\nimport { routes } from './routes.ts'\nimport { createFileResponse } from 'remix/response/file'\nimport { openLazyFile } from 'remix/fs'\n\nconst DOCS_DIR = path.resolve(import.meta.dirname, '..', '..')\nconst REPO_DIR = path.resolve(DOCS_DIR, '..')\nconst BUILD_DIR = path.join(REPO_DIR, 'docs', 'build')\nconst MD_DIR = path.join(BUILD_DIR, 'md')\nconst ASSETS_DIR = path.join(BUILD_DIR, 'assets')\nconst DEV_CSS_DIR = path.join(DOCS_DIR, 'public')\nconst REMIX_PKG_JSON = path.join(REPO_DIR, 'packages', 'remix', 'package.json')\n\nconst { docFiles, docFilesLookup } = await discoverMarkdownFiles(MD_DIR)\n\nexport const getDefaultVersions = (): ServerContext['versions'] => {\n  let version = JSON.parse(fs.readFileSync(REMIX_PKG_JSON, 'utf-8')).version\n  return [{ version, crawl: semver.prerelease(version) === null }]\n}\n\nexport function createRouter(versions: ServerContext['versions']) {\n  const router = _createRouter()\n\n  const respond = {\n    async file(request: Request, filePath: string, name: string) {\n      return await createFileResponse(openLazyFile(filePath, { name }), request)\n    },\n    async document(request: Request, node: RemixNode, init?: ResponseInit) {\n      let body = await stream(router, request, node, init)\n      return createHtmlResponse(body, init)\n    },\n    async fragment(request: Request, node: RemixNode, init?: ResponseInit) {\n      let body = await stream(router, request, node, init)\n      return new Response(body, {\n        ...init,\n        headers: {\n          'Content-Type': 'text/html; charset=utf-8',\n          ...init?.headers,\n        },\n      })\n    },\n  }\n\n  router.map(routes, {\n    actions: {\n      assets: ({ request, params }) => {\n        // Replicate `staticFiles` middleware but allowing for a dynamic version param\n        let devPath = path.join(DEV_CSS_DIR, params.asset)\n        let filePath =\n          process.env.NODE_ENV === 'development' && fs.existsSync(devPath)\n            ? devPath\n            : path.join(ASSETS_DIR, params.asset)\n        return respond.file(request, filePath, params.asset)\n      },\n      async docs({ request, params }) {\n        // Docs page\n        let docFile = docFiles.find((file) => file.urlPath === params.slug)\n        let node: RemixNode\n\n        if (!docFile) {\n          node = <NotFound slug={params.slug} />\n        } else {\n          let html = await renderMarkdownFile(docFile.path, docFilesLookup, params.version)\n          node = <div innerHTML={html} />\n        }\n\n        return await respond.document(\n          request,\n          <ServerPage\n            setup={{\n              docFiles,\n              versions,\n              slug: params.slug,\n              activeVersion: params.version,\n            }}\n          >\n            {node}\n          </ServerPage>,\n        )\n      },\n      async home({ request, params }) {\n        return respond.document(\n          request,\n          <ServerPage setup={{ docFiles, versions, activeVersion: params.version }}>\n            <Home />\n          </ServerPage>,\n        )\n      },\n      async markdown({ request, params }) {\n        let docFile = docFiles.find((file) => file.urlPath === params.slug)\n        if (!docFile) {\n          return new Response('Not Found', { status: 404 })\n        }\n\n        return respond.file(request, docFile.path, params.slug)\n      },\n    },\n  })\n\n  return router\n}\n\n// Response helpers\n\nfunction stream(router: Router, request: Request, node: RemixNode, init?: ResponseInit) {\n  return renderToStream(node, {\n    async resolveFrame(src) {\n      let url = new URL(src, request.url)\n\n      // IMPORTANT: this is a server-internal fetch to get *HTML*, so do not forward\n      // Accept-Encoding — otherwise compression middleware could return compressed bytes.\n      let headers = new Headers(request.headers)\n      headers.delete('accept-encoding')\n      headers.set('accept', 'text/html')\n\n      let res = await router.fetch(\n        new Request(url, {\n          method: 'GET',\n          headers,\n          signal: request.signal,\n        }),\n      )\n\n      if (!res.ok) {\n        return `<pre>Frame error: ${res.status} ${res.statusText}</pre>`\n      }\n\n      if (res.body) {\n        return res.body\n      }\n\n      return await res.text()\n    },\n  })\n}\n"
  },
  {
    "path": "docs/src/server/routes.ts",
    "content": "import { route } from 'remix/fetch-router/routes'\n\nexport const routes = route({\n  assets: '/(:version/)assets/*asset',\n  docs: '/(:version/)api/*slug/',\n  home: '/(:version/)',\n  markdown: '/(:version/)api/*slug.md',\n})\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"remix/component\",\n    \"paths\": {\n      \"*\": [\"./*\"],\n      \"remix/component/jsx-runtime\": [\"../packages/remix/src/component/jsx-runtime.ts\"],\n      \"remix/component/jsx-dev-runtime\": [\"../packages/remix/src/component/jsx-dev-runtime.ts\"]\n    }\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import tseslint from 'typescript-eslint'\nimport importPlugin from 'eslint-plugin-import'\nimport jsdoc from 'eslint-plugin-jsdoc'\nimport preferLet from 'eslint-plugin-prefer-let'\n\n/** @type {import('eslint').Linter.Config[]} */\nexport default [\n  {\n    ignores: [\n      '**/*.d.ts',\n      '**/dist/**',\n      '**/docs/**',\n      '**/demos/bookstore/public/assets/**',\n      '**/demos/sse/public/assets/**',\n      '**/coverage/**',\n      '**/bench/**',\n      '**/examples/**',\n      '**/*.min.js',\n      '**/*.bundled.*',\n      '**/public/assets/**',\n      'node_modules/**',\n      'reference/**',\n      'packages/multipart-parser/demos/deno/**',\n    ],\n  },\n  {\n    files: ['**/*.{ts,tsx}'],\n    languageOptions: {\n      parser: tseslint.parser,\n      parserOptions: {\n        sourceType: 'module',\n        ecmaVersion: 'latest',\n        // Enable typed linting across the monorepo without listing every tsconfig\n        projectService: true,\n      },\n    },\n    plugins: {\n      '@typescript-eslint': tseslint.plugin,\n      import: importPlugin,\n    },\n    rules: {\n      // Always use `import type { X }` and keep type imports separate from value imports\n      '@typescript-eslint/consistent-type-imports': [\n        'error',\n        {\n          prefer: 'type-imports',\n          fixStyle: 'separate-type-imports',\n        },\n      ],\n\n      // Always use `export type { X }`; avoid mixing type and value exports\n      '@typescript-eslint/consistent-type-exports': [\n        'error',\n        {\n          // false => prefer splitting into separate `export type {}` and `export {}`\n          fixMixedExportsWithInlineTypeSpecifier: false,\n        },\n      ],\n\n      // Prefer native public and #private over TS accessibility modifiers\n      // Disallow `public`/`private`/`protected` on class fields, methods, and parameter properties\n      'no-restricted-syntax': [\n        'error',\n        {\n          selector: 'PropertyDefinition[accessibility]',\n          message: \"Use native class fields: omit 'public' and use '#private' for private state.\",\n        },\n        {\n          selector: 'MethodDefinition[accessibility]:not([kind=\"constructor\"])',\n          message:\n            \"Use native methods: omit 'public'; for private behavior use '#private' fields/methods.\",\n        },\n        {\n          selector: 'MethodDefinition[accessibility=\"public\"][kind=\"constructor\"]',\n          message: \"Omit 'public' on constructors; it's the default.\",\n        },\n        {\n          selector: 'TSParameterProperty[accessibility]',\n          message:\n            \"Avoid TS parameter properties; declare fields explicitly and use '#private' when needed.\",\n        },\n      ],\n\n      // Ensure no rule asks for explicit member accessibility\n      '@typescript-eslint/explicit-member-accessibility': 'off',\n\n      // Require file extensions on imports\n      'import/extensions': [\n        'error',\n        'always',\n        {\n          ignorePackages: true,\n        },\n      ],\n    },\n  },\n  {\n    files: ['**/*.{ts,tsx,js,jsx}'],\n    plugins: {\n      'prefer-let': preferLet,\n    },\n    rules: {\n      // Prefer `let` for locals; allow `const` only at module scope\n      'prefer-let/prefer-let': 'error',\n      // Disallow `var` entirely\n      'no-var': 'error',\n      // Prefer concise arrow callbacks when only returning an expression\n      'arrow-body-style': ['error', 'as-needed'],\n    },\n  },\n  {\n    files: ['packages/**/*.{ts,tsx}'],\n    ignores: ['packages/**/*.test.ts'],\n    plugins: { jsdoc },\n    settings: {\n      jsdoc: {\n        // Set our own contexts at the root to identify the types of things we care\n        // to enforce JSDoc rules on.  Mostly we care about:\n        // - exported public APIs\n        // - not private class methods\n        // - not private class properties\n        // - not anything marked `@private` (`ignorePrivate` setting)\n        contexts: [\n          // function foo() {}\n          'ClassDeclaration',\n          // function foo() {}\n          'FunctionDeclaration',\n          // let foo = function () {}\n          ':not(MethodDefinition) > FunctionExpression',\n          // Class{ foo() {} } but not Class{ #foo() {} } or Class { get foo() {} }\n          'MethodDefinition:not([kind=get]):not([key.type=PrivateIdentifier]) FunctionExpression',\n          // let foo = () => {}\n          ':not(PropertyDefinition) > ArrowFunctionExpression',\n          // Class{ foo = () => {} } but not Class{ #foo = () => {} }\n          'PropertyDefinition:not([key.type=PrivateIdentifier]) ArrowFunctionExpression',\n        ],\n        ignorePrivate: true,\n        tagNamePreference: {\n          // TODO: Temporarily allow both `@returns` and `@return`, but\n          // eventually we can find/replace to the standard `@returns` and\n          // remove this setting\n          return: 'return',\n        },\n      },\n    },\n    rules: {\n      // Using modified base rulesets from:\n      // https://github.com/gajus/eslint-plugin-jsdoc?tab=readme-ov-file#granular-flat-configs\n\n      // Modified version of jsdoc/flat/contents-typescript-error\n      'jsdoc/informative-docs': 'off',\n      'jsdoc/match-description': 'off',\n      'jsdoc/no-blank-block-descriptions': 'error',\n      'jsdoc/no-blank-blocks': 'error',\n      'jsdoc/text-escaping': 'off',\n\n      // Modified version of jsdoc/flat/logical-typescript-error\n      'jsdoc/check-access': 'error',\n      'jsdoc/check-param-names': 'error',\n      'jsdoc/check-property-names': 'error',\n      'jsdoc/check-syntax': 'error',\n      'jsdoc/check-tag-names': 'error',\n      'jsdoc/check-template-names': 'error',\n      'jsdoc/check-types': 'error',\n      'jsdoc/check-values': 'error',\n      'jsdoc/empty-tags': 'error',\n      'jsdoc/escape-inline-tags': 'error',\n      'jsdoc/implements-on-classes': 'error',\n      'jsdoc/require-returns-check': 'error',\n      'jsdoc/require-yields-check': 'error',\n      'jsdoc/no-bad-blocks': 'error',\n      'jsdoc/no-defaults': 'error',\n      'jsdoc/no-types': 'error',\n      'jsdoc/no-undefined-types': 'error',\n      'jsdoc/valid-types': 'error',\n\n      // Modified version of jsdoc/flat/stylistic-typescript-error\n      'jsdoc/check-alignment': 'error',\n      'jsdoc/check-line-alignment': 'error',\n      'jsdoc/lines-before-block': 'off',\n      'jsdoc/multiline-blocks': 'error',\n      'jsdoc/no-multi-asterisks': 'error',\n      'jsdoc/require-asterisk-prefix': 'error',\n      'jsdoc/require-hyphen-before-param-description': ['error', 'never'],\n      'jsdoc/tag-lines': 'off',\n\n      // Additional rules we manually added\n      'jsdoc/require-param': 'error',\n      'jsdoc/require-param-description': 'error',\n      'jsdoc/require-param-name': 'error',\n      'jsdoc/require-returns': 'error',\n      'jsdoc/require-returns-description': 'error',\n    },\n  },\n]\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"remix-the-web\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@10.32.1\",\n  \"dependencies\": {\n    \"@typescript/native-preview\": \"catalog:\",\n    \"eslint\": \"^9.33.0\",\n    \"eslint-plugin-import\": \"^2.32.0\",\n    \"eslint-plugin-jsdoc\": \"^61.5.0\",\n    \"eslint-plugin-prefer-let\": \"^4.0.0\",\n    \"prettier\": \"^3.3.3\",\n    \"tsx\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"typescript-eslint\": \"^8.40.0\"\n  },\n  \"engines\": {\n    \"node\": \">=24.3.0\"\n  },\n  \"scripts\": {\n    \"postinstall\": \"test -n \\\"$CI\\\" || pnpm --filter @remix-run/component exec playwright install\",\n    \"build\": \"pnpm -r build\",\n    \"changes:preview\": \"node ./scripts/changes-preview.ts\",\n    \"changes:validate\": \"node ./scripts/changes-validate.ts\",\n    \"changes:version\": \"node ./scripts/changes-version.ts\",\n    \"clean\": \"git clean -fdX -e '!/.env' .\",\n    \"codegen\": \"pnpm -r run codegen\",\n    \"docs\": \"cd docs && pnpm run docs\",\n    \"format\": \"prettier --write .\",\n    \"format:check\": \"prettier --check .\",\n    \"generate-remix\": \"node scripts/generate-remix.ts\",\n    \"lint\": \"eslint . --max-warnings=0\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"pr-preview\": \"node scripts/pr-preview.ts\",\n    \"setup-installable-branch\": \"node scripts/setup-installable-branch.ts\",\n    \"test\": \"pnpm --parallel run test\",\n    \"typecheck\": \"pnpm -r typecheck\"\n  },\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"better-sqlite3\",\n      \"@swc/core\",\n      \"esbuild\",\n      \"sharp\",\n      \"workerd\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/async-context-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/async-context-middleware/CHANGELOG.md",
    "content": "# `async-context-middleware` CHANGELOG\n\nThis 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/).\n\n## v0.1.3\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0)\n\n## v0.1.2\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0)\n\n## v0.1.1\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.1.0 (2025-11-19)\n\nInitial release extracted from `@remix-run/fetch-router` v0.9.0.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/async-context-middleware/README.md) for more details.\n"
  },
  {
    "path": "packages/async-context-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/async-context-middleware/README.md",
    "content": "# async-context-middleware\n\nRequest-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.\n\n## Features\n\n- **Request Context Access** - Read request context from anywhere in the same async execution flow\n- **Simple Router Integration** - Add a single middleware at the router level\n- **Node Async Hooks** - Built on `node:async_hooks` `AsyncLocalStorage`\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nSimply 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.\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { asyncContext, getContext } from 'remix/async-context-middleware'\n\nlet router = createRouter({\n  middleware: [asyncContext()],\n})\n\nrouter.get('/users/:id', async () => {\n  // Access context from anywhere in the async call stack\n  let context = getContext()\n  let userId = context.params.id\n\n  return new Response(`User ${userId}`)\n})\n```\n\nNote: This middleware requires support for `node:async_hooks`.\n\n## Related Packages\n\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/async-context-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/async-context-middleware\",\n  \"version\": \"0.1.3\",\n  \"description\": \"Middleware for storing request context in AsyncLocalStorage\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/async-context-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/async-context-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"async\",\n    \"context\",\n    \"async-local-storage\"\n  ]\n}\n"
  },
  {
    "path": "packages/async-context-middleware/src/index.ts",
    "content": "export { asyncContext, getContext } from './lib/async-context.ts'\n"
  },
  {
    "path": "packages/async-context-middleware/src/lib/async-context.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createRouter } from '@remix-run/fetch-router'\nimport { route } from '@remix-run/fetch-router/routes'\n\nimport { asyncContext, getContext } from './async-context.ts'\n\ndescribe('asyncContext', () => {\n  it('stores the request context in AsyncLocalStorage', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let router = createRouter({\n      middleware: [asyncContext()],\n    })\n\n    router.map(routes.home, (context) => {\n      assert.equal(context, getContext())\n      return new Response('Home')\n    })\n\n    await router.fetch('https://remix.run')\n  })\n})\n"
  },
  {
    "path": "packages/async-context-middleware/src/lib/async-context.ts",
    "content": "import { AsyncLocalStorage } from 'node:async_hooks'\n\nimport type { Middleware, RequestContext } from '@remix-run/fetch-router'\n\nconst storage = new AsyncLocalStorage<RequestContext>()\n\n/**\n * Middleware that stores the request context in `AsyncLocalStorage` so it is available\n * to all functions in the same async execution context.\n *\n * @returns A middleware function that stores the request context in `AsyncLocalStorage`\n */\nexport function asyncContext(): Middleware {\n  return (context, next) => storage.run(context, next)\n}\n\n/**\n * Get the request context from `AsyncLocalStorage`.\n *\n * @returns The request context\n */\nexport function getContext(): RequestContext {\n  let context = storage.getStore()\n\n  if (context == null) {\n    throw new Error('No request context found. Make sure the asyncContext middleware is installed.')\n  }\n\n  return context\n}\n"
  },
  {
    "path": "packages/async-context-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/async-context-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/component/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/component/.changes/minor.01-add-mixin-system-and-core-helpers.md",
    "content": "Add the new host `mix` prop and mixin authoring APIs in `@remix-run/component`.\n\nNew exports include:\n\n- `createMixin`\n- `MixinDescriptor`, `MixinHandle`, `MixinType`, `MixValue`\n- `on(...)`\n- `ref(...)`\n- `css(...)`\n\nThis enables reusable host behaviors and composable element capabilities without bespoke host props.\n"
  },
  {
    "path": "packages/component/.changes/minor.02-add-press-and-keyboard-mixins.md",
    "content": "Add new interaction mixins for normalized user input events:\n\n- `pressEvents(...)` for pointer/keyboard \"press\" interactions\n- `keysEvents(...)` for keyboard key state events\n\nThese helpers provide a consistent mixin-based interaction model for input handling.\n"
  },
  {
    "path": "packages/component/.changes/minor.03-add-animation-mixins.md",
    "content": "Add mixin-first animation APIs for host elements:\n\n- `animateEntrance(...)`\n- `animateExit(...)`\n- `animateLayout(...)`\n\nThese APIs move entrance/exit/layout animation behavior to composable mixins that can be combined with other host behaviors.\n"
  },
  {
    "path": "packages/component/.changes/minor.04-remove-legacy-on-prop.md",
    "content": "BREAKING CHANGE: remove legacy host-element `on` prop support in `@remix-run/component`.\n\nUse the `on()` mixin instead:\n\n- Old: `<button on={{ click() {} }} />`\n- New: `<button mix={[on('click', () => {})]} />`\n\nThis change removes built-in host `on` handling from runtime, typing, and host-prop composition. Component-level `handle.on(...)` remains supported.\n"
  },
  {
    "path": "packages/component/.changes/minor.05-remove-legacy-css-prop.md",
    "content": "BREAKING CHANGE: remove legacy host-element `css` prop runtime support in `@remix-run/component`.\n\nUse the `css(...)` mixin instead:\n\n- Old: `<div css={{ color: 'red' }} />`\n- New: `<div mix={[css({ color: 'red' })]} />`\n\nThis aligns styling behavior with the new mixin composition model.\n"
  },
  {
    "path": "packages/component/.changes/minor.06-remove-legacy-animate-prop.md",
    "content": "BREAKING CHANGE: remove legacy host-element `animate` prop runtime support in `@remix-run/component`.\n\nUse animation mixins instead:\n\n- Old: `<div animate={{ enter: true, exit: true, layout: true }} />`\n- New: `<div mix={[animateEntrance(), animateExit(), animateLayout()]} />`\n\nThis aligns animation behavior with the new mixin composition model.\n"
  },
  {
    "path": "packages/component/.changes/minor.07-remove-legacy-connect-prop.md",
    "content": "BREAKING CHANGE: remove legacy host-element `connect` prop support in `@remix-run/component`.\n\nUse the `ref(...)` mixin instead:\n\n- Old: `<div connect={(node, signal) => {}} />`\n- New: `<div mix={[ref((node, signal) => {})]} />`\n\nThis aligns element reference and teardown behavior with the mixin composition model.\n"
  },
  {
    "path": "packages/component/.changes/minor.08-interaction-package-removed.md",
    "content": "BREAKING CHANGE: the `@remix-run/interaction` package has been removed.\n\n`handle.on(...)` APIs were also removed from component and mixin handles.\n\nBefore/after migration:\n\n**Interaction package APIs:**\n\n- Before: `defineInteraction(...)`, `createContainer(...)`, `on(target, listeners)` from `@remix-run/interaction`.\n- After: use component APIs (`createMixin(...)`, `on(...)`, `addEventListeners(...)`) from `@remix-run/component`.\n\n```ts\n// Before\nimport { on } from '@remix-run/interaction'\n\nlet dispose = on(window, {\n  resize() {\n    console.log('resized')\n  },\n})\n\n// After\nimport { addEventListeners } from '@remix-run/component'\n\nlet controller = new AbortController()\naddEventListeners(window, controller.signal, {\n  resize() {\n    console.log('resized')\n  },\n})\n```\n\n**Component handle API:**\n\n- Before: `handle.on(target, listeners)`.\n- After: `addEventListeners(target, handle.signal, listeners)`.\n\n```tsx\n// Before\nfunction KeyboardTracker(handle: Handle) {\n  handle.on(document, {\n    keydown(event) {\n      console.log(event.key)\n    },\n  })\n  return () => null\n}\n\n// After\nimport { addEventListeners } from '@remix-run/component'\n\nfunction KeyboardTracker(handle: Handle) {\n  addEventListeners(document, handle.signal, {\n    keydown(event) {\n      console.log(event.key)\n    },\n  })\n  return () => null\n}\n```\n\n**Custom interaction patterns:**\n\n- Before: `defineInteraction(...)` + interaction setup function.\n- After: event mixins (`createMixin(...)`) that compose `on(...)` listeners and dispatch typed custom events.\n\n```tsx\n// Before\nimport { defineInteraction, type Interaction } from '@remix-run/interaction'\n\nexport let tempo = defineInteraction('my:tempo', Tempo)\n\nfunction Tempo(handle: Interaction) {\n  handle.on(handle.target, {\n    click() {\n      handle.target.dispatchEvent(new TempoEvent(bmp))\n    },\n  })\n}\n\n// App consumption (before, JSX)\nfunction TempoButtonBefore() {\n  return () => (\n    <button\n      on={{\n        [tempo](event) {\n          console.log(event.bpm)\n        },\n      }}\n    />\n  )\n}\n\n// After\nimport { createMixin, on } from '@remix-run/component'\n\nexport let tempo = 'my:tempo' as const\n\nexport let tempoEvents = createMixin<HTMLElement>((handle) => {\n  return () => (\n    <handle.element\n      mix={[\n        on('click', (event) => {\n          event.currentTarget.dispatchEvent(new TempoEvent(bpm))\n        }),\n      ]}\n    />\n  )\n})\n\n// App consumption (after)\nfunction TempoButton() {\n  return () => (\n    <button\n      mix={[\n        tempoEvents(),\n        on(tempo, (event) => {\n          console.log(event.detail.bpm)\n        }),\n      ]}\n    />\n  )\n}\n```\n\n**TypedEventTarget**\n\n`TypedEventTarget` is now exported from `@remix-run/component`.\n"
  },
  {
    "path": "packages/component/.changes/minor.09-allow-single-mix-values.md",
    "content": "Allow the `mix` prop to accept either a single mixin descriptor or an array of mixin descriptors.\n\nThis 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.\n"
  },
  {
    "path": "packages/component/.changes/minor.13-allow-remix-node-frame-content.md",
    "content": "Allow client `resolveFrame(...)` callbacks to return `RemixNode` content in addition to HTML strings and streams.\n\nThis 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.\n"
  },
  {
    "path": "packages/component/.changes/minor.frame-navigation-link-attributes.md",
    "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.\n"
  },
  {
    "path": "packages/component/.changes/minor.frame-navigation-runtime.md",
    "content": "Add imperative frame-navigation runtime APIs and a `link(href, { src, target, history })` mixin for declarative client navigations.\n\n`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.\n"
  },
  {
    "path": "packages/component/.changes/minor.remove-head-hoisting.md",
    "content": "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.\n\nThis 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.\n"
  },
  {
    "path": "packages/component/.changes/minor.resolve-frame-target.md",
    "content": "Allow `resolveFrame(src, signal, target)` to receive the named frame target for targeted reloads.\n\nThis makes it easier to distinguish targeted frame navigations when forwarding frame requests through app-specific fetch logic.\n"
  },
  {
    "path": "packages/component/.changes/minor.ssr-frame-src-context.md",
    "content": "Add SSR frame source context for nested frame rendering.\n\n`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.\n"
  },
  {
    "path": "packages/component/.changes/patch.10-preserve-live-dom-state.md",
    "content": "Preserve browser-managed live state when frame DOM diffing updates interactive elements.\n\nThis 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.\n"
  },
  {
    "path": "packages/component/.changes/patch.11-forward-client-entry-root-errors.md",
    "content": "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`.\n\nThis 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.\n"
  },
  {
    "path": "packages/component/.changes/patch.defer-mixin-lifecycle-events.md",
    "content": "Run mixin `insert`, `remove`, and `reclaimed` lifecycle events in the scheduler's commit phase instead of dispatching them inline during DOM diffing.\n\nThis 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.\n"
  },
  {
    "path": "packages/component/.changes/patch.fix-adjacent-hydration-markers.md",
    "content": "Fix full-document client reloads that could leave orphaned hydration markers behind when adjacent client entries are diffed in the same parent.\n\nThis 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.\n"
  },
  {
    "path": "packages/component/.changes/patch.fix-svg-classname-mapping.md",
    "content": "Fix SVG `className` prop normalization to render as `class` in both client DOM updates and SSR stream output.\n\nAlso add SVG regression coverage to prevent accidental `class-name` output.\n"
  },
  {
    "path": "packages/component/.changes/patch.resolve-svg-link-targets.md",
    "content": "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.\n"
  },
  {
    "path": "packages/component/.changes/patch.skip-download-link-interception.md",
    "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`.\n"
  },
  {
    "path": "packages/component/AGENTS.md",
    "content": "# Remix Component - Agent Guide\n\nThis guide provides a comprehensive overview of the Remix Component API, its runtime behavior, and practical use cases for building interactive UIs.\n\n> Note: Host-element `on` props were removed. Use `mix={[on('event', handler)]}` for DOM event listeners.\n\n## Getting Started\n\n### Creating a Root\n\nTo start using Remix Component, create a root and render your top-level component:\n\n```tsx\nimport { createRoot, on } from 'remix/component'\nimport type { Handle } from 'remix/component'\n\nfunction App(handle: Handle) {\n  return () => (\n    <div>\n      <h1>Hello, World!</h1>\n    </div>\n  )\n}\n\n// Create a root attached to a DOM element\nlet container = document.getElementById('app')!\nlet root = createRoot(container)\n\n// Render your app\nroot.render(<App />)\n```\n\nThe `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:\n\n```tsx\nfunction App(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <div>\n      <h1>Count: {count}</h1>\n      <button\n        mix={[\n          on('click', () => {\n            count++\n            handle.update()\n          }),\n        ]}\n      >\n        Increment\n      </button>\n    </div>\n  )\n}\n\nlet root = createRoot(document.body)\nroot.render(<App />)\n\n// Later, you can update the app by calling render again\n// root.render(<App />)\n```\n\n### Root Methods\n\nThe root object provides several methods:\n\n- **`render(node)`** - Renders a component tree into the root container\n- **`flush()`** - Synchronously flushes all pending updates and tasks\n- **`remove()`** - Removes the component tree and cleans up\n\n```tsx\nlet root = createRoot(document.body)\n\n// Render initial app\nroot.render(<App />)\n\n// Flush any pending updates synchronously\nroot.flush()\n\n// Later, remove the app\nroot.remove()\n```\n\n## Component Factory and Runtime Behavior\n\n### Component Structure\n\nAll components follow a consistent two-phase structure:\n\n1. **Setup Phase** - Runs once when the component is first created\n2. **Render Phase** - Runs on initial render and every update afterward\n\n```tsx\nfunction MyComponent(handle: Handle, setup: SetupType) {\n  // Setup phase: runs once\n  let state = initializeState(setup)\n\n  // Return render function: runs on every update\n  return (props: Props) => {\n    return <div>{/* render content */}</div>\n  }\n}\n```\n\n### Runtime Behavior\n\nWhen a component is rendered:\n\n1. **First Render**:\n\n   - The component function is called with `handle` and the `setup` prop\n   - The returned render function is stored\n   - The render function is called with regular props\n   - Any tasks queued via `handle.queueTask()` are executed after rendering\n\n2. **Subsequent Updates**:\n\n   - Only the render function is called\n   - Setup phase is skipped, setup closure persists for the lifetime of the component instance\n   - Props are passed to the render function\n   - The `setup` prop is stripped from props\n   - Tasks queued during the update are executed after rendering\n\n3. **Component Removal**:\n   - `handle.signal` is aborted\n   - All event listeners registered via `handle.on()` are automatically cleaned up\n   - Any queued tasks are executed with an aborted signal\n\n### Setup vs Props\n\nThe `setup` prop is special - it's only available in the setup phase and is automatically excluded from props. This prevents accidental stale captures:\n\n```tsx\nfunction Counter(handle: Handle, setup: number) {\n  // setup prop (e.g., initialCount) only available here\n  let count = setup\n\n  return (props: { label: string }) => {\n    // props only receives { label } - setup is excluded\n    return (\n      <div>\n        {props.label}: {count}\n      </div>\n    )\n  }\n}\n\n// Usage\nlet element = <Counter setup={10} label=\"Count\" />\n```\n\n## Handle API\n\nThe `Handle` object provides the component's interface to the framework:\n\n### `handle.update()`\n\nSchedules a component update and returns a promise that resolves with an `AbortSignal` after\nthe update completes.\n\n```tsx\nfunction Counter(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <button\n      mix={[\n        on('click', () => {\n          count++\n          handle.update()\n        }),\n      ]}\n    >\n      Count: {count}\n    </button>\n  )\n}\n```\n\nWaiting for the update:\n\n```tsx\nfunction Player(handle: Handle) {\n  let isPlaying = false\n  let stopButton: HTMLButtonElement\n\n  return () => (\n    <button\n      disabled={isPlaying}\n      mix={[\n        on('click', async () => {\n          isPlaying = true\n          await handle.update()\n          stopButton.focus()\n        }),\n      ]}\n    >\n      Play\n    </button>\n  )\n}\n```\n\n### `handle.queueTask(task)`\n\nSchedules a task to run after the next update. The task receives an `AbortSignal` that's aborted when:\n\n- The component re-renders (new render cycle starts)\n- The component is removed from the tree\n\n**Use `queueTask` in event handlers when work needs to happen after DOM changes:**\n\n```tsx\nfunction Form(handle: Handle) {\n  let showDetails = false\n  let detailsSection: HTMLElement\n\n  return () => (\n    <form>\n      <input\n        type=\"checkbox\"\n        checked={showDetails}\n        mix={[\n          on('change', (event) => {\n            showDetails = event.currentTarget.checked\n            handle.update()\n            if (showDetails) {\n              // Queue DOM operation after the new section renders\n              handle.queueTask(() => {\n                detailsSection.scrollIntoView({ behavior: 'smooth' })\n              })\n            }\n          }),\n        ]}\n      />\n      {showDetails && (\n        <section mix={[ref((node) => (detailsSection = node))]}>Details content</section>\n      )}\n    </form>\n  )\n}\n```\n\n**Use `queueTask` for work that needs to be reactive to prop changes:**\n\nWhen 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.\n\n**❌ Anti-pattern: Don't create states as values to \"react to\" on the next render with `queueTask`:**\n\n```tsx\n// ❌ Avoid: Creating state just to react to it in queueTask\nfunction BadExample(handle: Handle) {\n  let shouldLoad = false // Unnecessary state\n\n  return () => (\n    <div>\n      <button\n        on={{\n          click() {\n            shouldLoad = true // Setting state just to trigger queueTask\n            handle.update()\n            handle.queueTask(() => {\n              if (shouldLoad) {\n                // Do work\n              }\n            })\n          },\n        }}\n      >\n        Load\n      </button>\n    </div>\n  )\n}\n\n// ✅ Prefer: Do the work directly in the event handler or queueTask\nfunction GoodExample(handle: Handle) {\n  return () => (\n    <div>\n      <button\n        on={{\n          click() {\n            handle.queueTask(() => {\n              // Do work directly - no intermediate state needed\n            })\n          },\n        }}\n      >\n        Load\n      </button>\n    </div>\n  )\n}\n```\n\n**Pattern: await `handle.update()` when showing loading state before async work:**\n\nWhen you need to show loading UI before async work starts, set loading state, call\n`await handle.update()`, and use the returned signal for async APIs.\n\n```tsx\nfunction GoodAsyncExample(handle: Handle) {\n  let data: string[] = []\n  let loading = false\n\n  async function load() {\n    loading = true\n    let signal = await handle.update()\n    let response = await fetch('/api/data', { signal })\n    if (signal.aborted) return\n\n    data = await response.json()\n    loading = false\n    handle.update()\n  }\n\n  return () => <button on={{ click: load }}>{loading ? 'Loading...' : 'Load data'}</button>\n}\n```\n\n**Signals in events and tasks are how you manage interruptions and disconnects:**\n\nBoth event handlers and `queueTask` receive `AbortSignal` parameters that are automatically aborted when:\n\n- The component is removed from the tree\n- For event handlers: The handler is re-entered (user triggers another event)\n- For `queueTask`: The component re-renders (props changed)\n\nAlways check `signal.aborted` or pass the signal to async APIs (like `fetch`) to handle interruptions gracefully.\n\n### `handle.signal`\n\nAn `AbortSignal` that's aborted when the component is disconnected. Useful for cleanup operations.\n\n```tsx\nfunction Clock(handle: Handle) {\n  let interval = setInterval(() => {\n    if (handle.signal.aborted) {\n      clearInterval(interval)\n      return\n    }\n    handle.update()\n  }, 1000)\n\n  return () => <span>{new Date().toString()}</span>\n}\n```\n\nOr using event listeners:\n\n```tsx\nfunction Clock(handle: Handle) {\n  let interval = setInterval(handle.update, 1000)\n  handle.signal.addEventListener('abort', () => clearInterval(interval))\n\n  return () => <span>{new Date().toString()}</span>\n}\n```\n\n### `handle.on(target, listeners)`\n\nListen to an `EventTarget` with automatic cleanup when the component disconnects. Ideal for global event targets like `document` and `window`.\n\n```tsx\nfunction KeyboardTracker(handle: Handle) {\n  let keys: string[] = []\n\n  handle.on(document, {\n    keydown(event) {\n      keys.push(event.key)\n      handle.update()\n    },\n  })\n\n  return () => <div>Keys: {keys.join(', ')}</div>\n}\n```\n\n### `handle.id`\n\nStable identifier per component instance. Useful for HTML APIs like `htmlFor`, `aria-owns`, etc.\n\n```tsx\nfunction LabeledInput(handle: Handle) {\n  return () => (\n    <div>\n      <label htmlFor={handle.id}>Name</label>\n      <input id={handle.id} type=\"text\" />\n    </div>\n  )\n}\n```\n\n### `handle.context`\n\nContext API for ancestor/descendant communication. Use `handle.context.set()` to provide values and `handle.context.get()` to consume them.\n\n**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, or use an `EventTarget` on context for descendants to subscribe to changes (see the TypedEventTarget example in the Context section).\n\n```tsx\nfunction App(handle: Handle<{ theme: string }>) {\n  handle.context.set({ theme: 'dark' })\n\n  return () => (\n    <div>\n      <Header />\n      <Content />\n    </div>\n  )\n}\n\nfunction Header(handle: Handle) {\n  let { theme } = handle.context.get(App)\n  return () => <header css={{ backgroundColor: theme === 'dark' ? '#000' : '#fff' }}>Header</header>\n}\n```\n\n## Rendering and Composition\n\n### Basic Rendering\n\nThe simplest component just returns JSX:\n\n```tsx\nfunction Greeting() {\n  return (props: { name: string }) => <div>Hello, {props.name}!</div>\n}\n\nlet el = <Greeting name=\"World\" />\n```\n\n### Prop Passing\n\nProps flow from parent to child through JSX attributes:\n\n```tsx\nfunction Parent() {\n  return () => <Child message=\"Hello from parent\" count={42} />\n}\n\nfunction Child() {\n  return (props: { message: string; count: number }) => (\n    <div>\n      <p>{props.message}</p>\n      <p>Count: {props.count}</p>\n    </div>\n  )\n}\n```\n\n### Stateful Updates\n\nState is managed with plain JavaScript variables. Call `handle.update()` to trigger a re-render:\n\n```tsx\nfunction Counter(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <div>\n      <span>Count: {count}</span>\n      <button\n        on={{\n          click() {\n            count++\n            handle.update()\n          },\n        }}\n      >\n        Increment\n      </button>\n    </div>\n  )\n}\n```\n\n### State Management Best Practices\n\n#### Use Minimal Component State\n\nOnly store state that's needed for rendering. Derive computed values instead of storing them, and avoid storing input state that you don't need.\n\n**Derive computed values:**\n\n```tsx\n// ❌ Avoid: Storing computed values\nfunction TodoList(handle: Handle) {\n  let todos: string[] = []\n  let completedCount = 0 // Unnecessary state\n\n  return () => (\n    <div>\n      {todos.map((todo, i) => (\n        <div key={i}>{todo}</div>\n      ))}\n      <div>Completed: {completedCount}</div>\n    </div>\n  )\n}\n\n// ✅ Prefer: Derive computed values in render\nfunction TodoList(handle: Handle) {\n  let todos: Array<{ text: string; completed: boolean }> = []\n\n  return () => {\n    // Derive computed value in render\n    let completedCount = todos.filter((t) => t.completed).length\n\n    return (\n      <div>\n        {todos.map((todo, i) => (\n          <div key={i}>{todo.text}</div>\n        ))}\n        <div>Completed: {completedCount}</div>\n      </div>\n    )\n  }\n}\n```\n\n**Don't store input state you don't need:**\n\n```tsx\n// ❌ Avoid: Storing input value when you only need it on submit\nfunction SearchForm(handle: Handle) {\n  let query = '' // Unnecessary state\n\n  return () => (\n    <form\n      on={{\n        submit(event) {\n          event.preventDefault()\n          let formData = new FormData(event.currentTarget)\n          let query = formData.get('query') as string\n          // Use query for search\n        },\n      }}\n    >\n      <input name=\"query\" />\n      <button type=\"submit\">Search</button>\n    </form>\n  )\n}\n\n// ✅ Prefer: Read input value directly from the form\nfunction SearchForm(handle: Handle) {\n  return () => (\n    <form\n      on={{\n        submit(event) {\n          event.preventDefault()\n          let formData = new FormData(event.currentTarget)\n          let query = formData.get('query') as string\n          // Use query for search - no component state needed\n        },\n      }}\n    >\n      <input name=\"query\" />\n      <button type=\"submit\">Search</button>\n    </form>\n  )\n}\n```\n\n#### Do Work in Event Handlers\n\nDo 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.\n\n**Use event handler scope for transient state:**\n\n```tsx\n// ❌ Avoid: Storing transient state in component\nfunction FormValidator(handle: Handle) {\n  let validationError: string | null = null // Only needed during validation\n\n  return () => (\n    <form\n      on={{\n        submit(event) {\n          event.preventDefault()\n          let formData = new FormData(event.currentTarget)\n          let email = formData.get('email') as string\n\n          // Validation logic\n          if (!email.includes('@')) {\n            validationError = 'Invalid email'\n            handle.update()\n            return\n          }\n\n          // Submit form\n          validationError = null\n          handle.update()\n        },\n      }}\n    >\n      {validationError && <div>{validationError}</div>}\n      <input name=\"email\" />\n      <button type=\"submit\">Submit</button>\n    </form>\n  )\n}\n\n// ✅ Prefer: Keep transient state in event handler scope\nfunction FormValidator(handle: Handle) {\n  let validationError: string | null = null // Only stored if needed for rendering\n\n  return () => (\n    <form\n      on={{\n        submit(event) {\n          event.preventDefault()\n          let formData = new FormData(event.currentTarget)\n          let email = formData.get('email') as string\n\n          // Validation logic - keep transient state in handler scope\n          if (!email.includes('@')) {\n            validationError = 'Invalid email' // Only store if rendering needs it\n            handle.update()\n            return\n          }\n\n          // Submit form - clear error if it exists\n          if (validationError) {\n            validationError = null\n            handle.update()\n          }\n        },\n      }}\n    >\n      {validationError && <div>{validationError}</div>}\n      <input name=\"email\" />\n      <button type=\"submit\">Submit</button>\n    </form>\n  )\n}\n```\n\n**Only store state needed for rendering:**\n\n```tsx\n// ✅ Good: Store state that affects rendering\nfunction Toggle(handle: Handle) {\n  let isOpen = false // Needed for rendering conditional content\n\n  return () => (\n    <div>\n      <button\n        on={{\n          click() {\n            isOpen = !isOpen\n            handle.update()\n          },\n        }}\n      >\n        Toggle\n      </button>\n      {isOpen && <div>Content</div>}\n    </div>\n  )\n}\n\n// ✅ Good: Do work in handler, only store what renders need\nfunction SearchResults(handle: Handle) {\n  let results: string[] = [] // Needed for rendering\n  let loading = false // Needed for rendering loading state\n\n  return () => (\n    <div>\n      <input\n        on={{\n          async input(event, signal) {\n            let query = event.currentTarget.value\n            // Do work in handler scope\n            loading = true\n            handle.update()\n\n            let response = await fetch(`/search?q=${query}`, { signal })\n            let data = await response.json()\n            if (signal.aborted) return\n\n            // Only store what's needed for rendering\n            results = data.results\n            loading = false\n            handle.update()\n          },\n        }}\n      />\n      {loading && <div>Loading...</div>}\n      {results.map((result, i) => (\n        <div key={i}>{result}</div>\n      ))}\n    </div>\n  )\n}\n```\n\n### CSS Prop with Pseudo-Selectors and Descendant Selectors\n\nThe `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. Use `&` to reference the current element in pseudo-selectors and attribute selectors.\n\n#### Basic CSS Prop\n\n```tsx\nfunction Button() {\n  return () => (\n    <button\n      css={{\n        color: 'white',\n        backgroundColor: 'blue',\n        padding: '12px 24px',\n        borderRadius: '4px',\n        border: 'none',\n        cursor: 'pointer',\n      }}\n    >\n      Click me\n    </button>\n  )\n}\n```\n\n#### Performance: CSS Prop vs Style Prop\n\nThe `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:\n\n```tsx\n// ❌ Avoid: Using css prop for dynamic styles\nfunction ProgressBar(handle: Handle) {\n  let progress = 0\n\n  return () => (\n    <div\n      css={{\n        width: `${progress}%`, // Creates new CSS rule on every update\n        backgroundColor: 'blue',\n      }}\n    >\n      {progress}%\n    </div>\n  )\n}\n\n// ✅ Prefer: Using style prop for dynamic styles\nfunction ProgressBar(handle: Handle) {\n  let progress = 0\n\n  return () => (\n    <div\n      css={{\n        backgroundColor: 'blue', // Static styles in css prop\n      }}\n      style={{\n        width: `${progress}%`, // Dynamic styles in style prop\n      }}\n    >\n      {progress}%\n    </div>\n  )\n}\n```\n\n**Use the `css` prop for:**\n\n- Static styles that don't change\n- Styles that need pseudo-selectors (`:hover`, `:focus`, etc.)\n- Styles that need media queries\n\n**Use the `style` prop for:**\n\n- Dynamic styles that change based on state or props\n- Computed values that update frequently\n\n#### Pseudo-Selectors\n\nUse `&` to reference the current element in pseudo-selectors:\n\n```tsx\nfunction Button() {\n  return () => (\n    <button\n      css={{\n        color: 'white',\n        backgroundColor: 'blue',\n        padding: '12px 24px',\n        borderRadius: '4px',\n        border: 'none',\n        cursor: 'pointer',\n        '&:hover': {\n          backgroundColor: 'darkblue',\n          transform: 'translateY(-1px)',\n        },\n        '&:active': {\n          backgroundColor: 'navy',\n          transform: 'translateY(0)',\n        },\n        '&:focus': {\n          outline: '2px solid yellow',\n          outlineOffset: '2px',\n        },\n        '&:disabled': {\n          opacity: 0.5,\n          cursor: 'not-allowed',\n        },\n      }}\n    >\n      Click me\n    </button>\n  )\n}\n```\n\n#### Pseudo-Elements\n\nUse `&::before` and `&::after` for pseudo-elements:\n\n```tsx\nfunction Badge() {\n  return (props: { count: number }) => (\n    <div\n      css={{\n        position: 'relative',\n        display: 'inline-block',\n        '&::before': {\n          content: '\"\"',\n          position: 'absolute',\n          top: '-4px',\n          right: '-4px',\n          width: '8px',\n          height: '8px',\n          backgroundColor: 'red',\n          borderRadius: '50%',\n        },\n      }}\n    >\n      {props.count > 0 && <span>{props.count}</span>}\n    </div>\n  )\n}\n```\n\n#### Attribute Selectors\n\nUse `&[attribute]` for attribute selectors:\n\n```tsx\nfunction Input() {\n  return (props: { required?: boolean }) => (\n    <input\n      required={props.required}\n      css={{\n        padding: '8px',\n        border: '1px solid #ccc',\n        borderRadius: '4px',\n        '&[required]': {\n          borderColor: 'red',\n        },\n        '&[aria-invalid=\"true\"]': {\n          borderColor: 'red',\n          outline: '2px solid red',\n        },\n      }}\n    />\n  )\n}\n```\n\n#### Descendant Selectors\n\nUse class names or element selectors directly for descendant selectors:\n\n```tsx\nfunction Card() {\n  return (props: { children: RemixNode }) => (\n    <div\n      css={{\n        padding: '20px',\n        border: '1px solid #ddd',\n        borderRadius: '8px',\n        backgroundColor: 'white',\n        boxShadow: '0 2px 4px rgba(0,0,0,0.1)',\n        // Style descendants\n        '& h2': {\n          marginTop: 0,\n          fontSize: '24px',\n          fontWeight: 'bold',\n        },\n        '& p': {\n          color: '#666',\n          lineHeight: 1.6,\n        },\n        '& .icon': {\n          width: '24px',\n          height: '24px',\n          marginRight: '8px',\n        },\n        '& button': {\n          marginTop: '16px',\n        },\n      }}\n    >\n      {props.children}\n    </div>\n  )\n}\n```\n\n#### When to Use Nested Selectors\n\nUse nested selectors when **parent state affects children**. Don't nest when you can style the element directly.\n\n**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.\n\n**Use nested selectors when:**\n\n1. **Parent state affects children** - Parent hover/focus/state changes child styling (prefer this over JavaScript state management)\n2. **Styling descendant elements** - Avoid duplicating styles on every child or creating new components just for styling. Style children from the parent component instead.\n\n**Don't nest when:**\n\n- Styling the element's own pseudo-states (hover, focus, etc.)\n- The element controls its own styling\n\n**Example: Parent hover affects children** (use nested selectors, not JavaScript state):\n\n```tsx\n// ❌ Avoid: Managing hover state in JavaScript\nfunction CardWithJSState(handle: Handle) {\n  let isHovered = false\n\n  return (props: { children: RemixNode }) => (\n    <div\n      on={{\n        mouseenter() {\n          isHovered = true\n          handle.update()\n        },\n        mouseleave() {\n          isHovered = false\n          handle.update()\n        },\n      }}\n      css={{\n        border: `1px solid ${isHovered ? 'blue' : '#ddd'}`,\n        // ... more conditional styling based on isHovered\n      }}\n    >\n      <div className=\"title\" css={{ color: isHovered ? 'blue' : '#333' }}>\n        Title\n      </div>\n    </div>\n  )\n}\n\n// ✅ Prefer: CSS nested selectors handle state declaratively\nfunction Card(handle: Handle) {\n  return (props: { children: RemixNode }) => (\n    <div\n      css={{\n        border: '1px solid #ddd',\n        borderRadius: '8px',\n        padding: '20px',\n        // Parent hover affects children - use nested selector\n        '&:hover': {\n          borderColor: 'blue',\n          // Child text changes color on parent hover\n          '& .title': {\n            color: 'blue',\n          },\n          '& .description': {\n            opacity: 1,\n          },\n        },\n        '& .title': {\n          fontSize: '20px',\n          fontWeight: 'bold',\n          color: '#333',\n        },\n        '& .description': {\n          opacity: 0.7,\n          marginTop: '8px',\n        },\n      }}\n    >\n      <div className=\"title\">Title</div>\n    </div>\n  )\n}\n```\n\n**Example: Element's own hover** (style directly, no nesting needed):\n\n```tsx\nfunction Button() {\n  return () => (\n    <button\n      css={{\n        backgroundColor: 'blue',\n        color: 'white',\n        padding: '12px 24px',\n        borderRadius: '4px',\n        border: 'none',\n        cursor: 'pointer',\n        // Element's own hover - style directly, no nesting needed\n        '&:hover': {\n          backgroundColor: 'darkblue',\n        },\n        '&:active': {\n          transform: 'scale(0.98)',\n        },\n      }}\n    >\n      Click me\n    </button>\n  )\n}\n```\n\n**Example: Navigation with links** (descendant styling is appropriate):\n\n```tsx\nfunction Navigation() {\n  return () => (\n    <nav\n      css={{\n        display: 'flex',\n        gap: '16px',\n        // Styling descendant links - appropriate use of nesting\n        '& a': {\n          color: 'blue',\n          textDecoration: 'none',\n          padding: '8px 16px',\n          borderRadius: '4px',\n          // Link's own hover state - this is fine nested under '& a'\n          '&:hover': {\n            backgroundColor: '#f0f0f0',\n            color: 'darkblue',\n          },\n          '&[aria-current=\"page\"]': {\n            backgroundColor: 'blue',\n            color: 'white',\n          },\n        },\n      }}\n    >\n      <a href=\"/\">Home</a>\n      <a href=\"/about\">About</a>\n      <a href=\"/contact\">Contact</a>\n    </nav>\n  )\n}\n```\n\n#### Media Queries\n\nUse `@media` for responsive design:\n\n```tsx\nfunction ResponsiveGrid() {\n  return (props: { children: RemixNode }) => (\n    <div\n      css={{\n        display: 'grid',\n        gap: '16px',\n        gridTemplateColumns: '1fr',\n        '@media (min-width: 768px)': {\n          gridTemplateColumns: 'repeat(2, 1fr)',\n        },\n        '@media (min-width: 1024px)': {\n          gridTemplateColumns: 'repeat(3, 1fr)',\n        },\n      }}\n    >\n      {props.children}\n    </div>\n  )\n}\n```\n\n#### Combining All Features\n\nHere's a comprehensive example demonstrating parent-state-affecting-children and media queries, with styles applied directly to elements:\n\n```tsx\nfunction ProductCard() {\n  return (props: { title: string; price: number; image: string }) => (\n    <div\n      css={{\n        border: '1px solid #ddd',\n        borderRadius: '8px',\n        overflow: 'hidden',\n        transition: 'transform 0.2s, box-shadow 0.2s',\n        // Parent hover affects the card itself\n        '&:hover': {\n          transform: 'translateY(-4px)',\n          boxShadow: '0 4px 12px rgba(0,0,0,0.15)',\n          // Parent hover affects children - appropriate use of nesting\n          '& .title': {\n            color: 'blue',\n          },\n          '& button': {\n            backgroundColor: 'darkblue',\n          },\n        },\n        '@media (max-width: 768px)': {\n          '&:hover': {\n            transform: 'translateY(-2px)',\n          },\n        },\n      }}\n    >\n      <img\n        src={props.image}\n        alt={props.title}\n        css={{\n          width: '100%',\n          height: '200px',\n          objectFit: 'cover',\n          '@media (max-width: 768px)': {\n            height: '150px',\n          },\n        }}\n      />\n      <div\n        className=\"content\"\n        css={{\n          padding: '16px',\n          '@media (max-width: 768px)': {\n            padding: '12px',\n          },\n        }}\n      >\n        <h3\n          className=\"title\"\n          css={{\n            fontSize: '18px',\n            fontWeight: 'bold',\n            marginTop: 0,\n            marginBottom: '8px',\n            transition: 'color 0.2s',\n          }}\n        >\n          {props.title}\n        </h3>\n        <div\n          className=\"price\"\n          css={{\n            fontSize: '20px',\n            color: 'green',\n            fontWeight: 'bold',\n          }}\n        >\n          ${props.price}\n        </div>\n        <button\n          css={{\n            width: '100%',\n            padding: '12px',\n            backgroundColor: 'blue',\n            color: 'white',\n            border: 'none',\n            borderRadius: '4px',\n            cursor: 'pointer',\n            transition: 'background-color 0.2s',\n            '&:active': {\n              transform: 'scale(0.98)',\n            },\n          }}\n        >\n          Add to Cart\n        </button>\n      </div>\n    </div>\n  )\n}\n```\n\nThis example demonstrates:\n\n- **Parent hover affecting children**: Card hover changes title color and button background (only nested selector needed)\n- **Styles on elements themselves**: Each element (`img`, `.content`, `.title`, `.price`, `button`) has its own `css` prop\n- **Element's own states**: Button's `:active` state styled directly on the button\n- **Media queries**: Responsive adjustments applied directly to elements that need them\n\n### Ref Mixin\n\nUse the `ref(...)` mixin 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.\n\n```tsx\nfunction Form(handle: Handle) {\n  let inputRef: HTMLInputElement\n\n  return () => (\n    <form>\n      <input type=\"text\" mix={[ref((node) => (inputRef = node))]} />\n      <button\n        on={{\n          click() {\n            // Focus the input from elsewhere in the form\n            inputRef.focus()\n          },\n        }}\n      >\n        Focus Input\n      </button>\n    </form>\n  )\n}\n```\n\nThe `ref` callback receives an `AbortSignal` as its second parameter, which is aborted when the element is removed from the DOM. Use this for cleanup operations:\n\n```tsx\nfunction ResizeTracker(handle: Handle) {\n  let dimensions = { width: 0, height: 0 }\n\n  return () => (\n    <div\n      mix={[\n        ref((node, signal) => {\n          // Set up ResizeObserver\n          let observer = new ResizeObserver((entries) => {\n            let entry = entries[0]\n            if (entry) {\n              dimensions.width = Math.round(entry.contentRect.width)\n              dimensions.height = Math.round(entry.contentRect.height)\n              handle.update()\n            }\n          })\n          observer.observe(node)\n\n          // Clean up when element is removed\n          signal.addEventListener('abort', () => {\n            observer.disconnect()\n          })\n        }),\n      ]}\n    >\n      Size: {dimensions.width} × {dimensions.height}\n    </div>\n  )\n}\n```\n\nThe `ref` callback is called only once when the element is first rendered, not on every update.\n\n### Key Prop\n\nUse 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.\n\n```tsx\nfunction TodoList(handle: Handle) {\n  let todos = [\n    { id: '1', text: 'Buy milk' },\n    { id: '2', text: 'Walk dog' },\n    { id: '3', text: 'Write code' },\n  ]\n\n  return () => (\n    <ul>\n      {todos.map((todo) => (\n        <li key={todo.id}>{todo.text}</li>\n      ))}\n    </ul>\n  )\n}\n```\n\nWhen you reorder, add, or remove items, keys ensure:\n\n- **DOM nodes are reused** - Elements with matching keys are moved, not recreated\n- **Component state is preserved** - Component instances persist across reorders\n- **Focus and selection are maintained** - Input focus stays with the same element\n- **Input values are preserved** - Form values remain with their elements\n\n```tsx\nfunction ReorderableList(handle: Handle) {\n  let items = [\n    { id: 'a', label: 'Item A' },\n    { id: 'b', label: 'Item B' },\n    { id: 'c', label: 'Item C' },\n  ]\n\n  function reverse() {\n    items = [...items].reverse()\n    handle.update()\n  }\n\n  return () => (\n    <div>\n      <button\n        on={{\n          click: reverse,\n        }}\n      >\n        Reverse List\n      </button>\n      {items.map((item) => (\n        <div key={item.id}>\n          <input type=\"text\" defaultValue={item.label} />\n        </div>\n      ))}\n    </div>\n  )\n}\n```\n\nEven 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.\n\nKeys can be any type (string, number, bigint, object, symbol), but should be stable and unique within the list:\n\n```tsx\n// Good: stable, unique IDs\n{\n  items.map((item) => <Item key={item.id} item={item} />)\n}\n\n// Good: index can work if list never reorders\n{\n  items.map((item, index) => <Item key={index} item={item} />)\n}\n\n// Bad: don't use random values or values that change\n{\n  items.map((item) => <Item key={Math.random()} item={item} />)\n}\n```\n\n### Composition Through props.children\n\nComponents can compose other components via `children`:\n\n```tsx\nfunction Layout() {\n  return (props: { children: RemixNode }) => (\n    <div css={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>\n      <header>My App</header>\n      <main>{props.children}</main>\n      <footer>© 2024</footer>\n    </div>\n  )\n}\n\nfunction App() {\n  return () => (\n    <Layout>\n      <h1>Welcome</h1>\n      <p>Content goes here</p>\n    </Layout>\n  )\n}\n```\n\n### Context for Indirect Composition\n\nContext enables components to communicate without direct prop passing:\n\n#### Basic Context\n\n```tsx\nfunction ThemeProvider(handle: Handle<{ theme: 'light' | 'dark' }>) {\n  let theme: 'light' | 'dark' = 'light'\n\n  handle.context.set({ theme })\n\n  return (props: { children: RemixNode }) => (\n    <div>\n      <button\n        on={{\n          click() {\n            theme = theme === 'light' ? 'dark' : 'light'\n            handle.context.set({ theme })\n            handle.update()\n          },\n        }}\n      >\n        Toggle Theme\n      </button>\n      {props.children}\n    </div>\n  )\n}\n\nfunction ThemedContent(handle: Handle) {\n  let { theme } = handle.context.get(ThemeProvider)\n\n  return () => (\n    <div css={{ backgroundColor: theme === 'dark' ? '#000' : '#fff' }}>Current theme: {theme}</div>\n  )\n}\n```\n\n**Note:** `handle.context.set()` does not cause any updates - it simply stores a value. If you want the component tree to update when context changes, you must call `handle.update()` after setting the context (as shown above), or use an `EventTarget` on context for descendants to subscribe to changes (as shown in the TypedEventTarget example below).\n\n#### TypedEventTarget for Granular Updates\n\nFor better performance, use `TypedEventTarget` to avoid updating the entire subtree:\n\n```tsx\nimport { TypedEventTarget } from 'remix/component'\n\nclass Theme extends TypedEventTarget<{ change: Event }> {\n  #value: 'light' | 'dark' = 'light'\n\n  get value() {\n    return this.#value\n  }\n\n  setValue(value: 'light' | 'dark') {\n    this.#value = value\n    this.dispatchEvent(new Event('change'))\n  }\n}\n\nfunction ThemeProvider(handle: Handle<Theme>) {\n  let theme = new Theme()\n  handle.context.set(theme)\n\n  return (props: { children: RemixNode }) => (\n    <div>\n      <button\n        on={{\n          click() {\n            // No update needed - consumers subscribe to changes\n            theme.setValue(theme.value === 'light' ? 'dark' : 'light')\n          },\n        }}\n      >\n        Toggle Theme\n      </button>\n      {props.children}\n    </div>\n  )\n}\n\nfunction ThemedContent(handle: Handle) {\n  let theme = handle.context.get(ThemeProvider)\n\n  // Subscribe to granular updates\n  handle.on(theme, {\n    change() {\n      handle.update()\n    },\n  })\n\n  return () => (\n    <div css={{ backgroundColor: theme.value === 'dark' ? '#000' : '#fff' }}>\n      Current theme: {theme.value}\n    </div>\n  )\n}\n```\n\n## Common Patterns and Use Cases\n\n### Setup Scope Use Cases\n\nThe setup scope is perfect for one-time initialization:\n\n#### Initializing Instances\n\n```tsx\nfunction CacheExample(handle: Handle, setup: { cacheSize: number }) {\n  // Initialize cache once\n  let cache = new Map<string, any>()\n  let maxSize = setup.cacheSize\n\n  return (props: { key: string; value: any }) => {\n    // Use cache in render\n    if (cache.has(props.key)) {\n      return <div>Cached: {cache.get(props.key)}</div>\n    }\n    cache.set(props.key, props.value)\n    if (cache.size > maxSize) {\n      let firstKey = cache.keys().next().value\n      cache.delete(firstKey)\n    }\n    return <div>New: {props.value}</div>\n  }\n}\n```\n\n#### Third-Party SDKs\n\n```tsx\nfunction Analytics(handle: Handle, setup: { apiKey: string }) {\n  // Initialize SDK once\n  let analytics = new AnalyticsSDK(setup.apiKey)\n\n  // Cleanup on disconnect\n  handle.signal.addEventListener('abort', () => {\n    analytics.disconnect()\n  })\n\n  return (props: { event: string; data?: any }) => {\n    // SDK is ready to use\n    return <div>Tracking: {props.event}</div>\n  }\n}\n```\n\n#### EventEmitters\n\n```tsx\nimport { TypedEventTarget } from 'remix/component'\n\nclass DataEvent extends Event {\n  constructor(public value: string) {\n    super('data')\n  }\n}\n\nclass DataEmitter extends TypedEventTarget<{ data: DataEvent }> {\n  emitData(value: string) {\n    this.dispatchEvent(new DataEvent(value))\n  }\n}\n\nfunction EventListener(handle: Handle, setup: DataEmitter) {\n  // Set up listeners once with automatic cleanup\n  handle.on(setup, {\n    data(event) {\n      // Handle data\n      handle.update()\n    },\n  })\n\n  return () => <div>Listening for events...</div>\n}\n```\n\n#### Window/Document Event Handling\n\n```tsx\nfunction WindowResizeTracker(handle: Handle) {\n  let width = window.innerWidth\n  let height = window.innerHeight\n\n  // Set up global listeners once\n  handle.on(window, {\n    resize() {\n      width = window.innerWidth\n      height = window.innerHeight\n      handle.update()\n    },\n  })\n\n  return () => (\n    <div>\n      Window size: {width} × {height}\n    </div>\n  )\n}\n```\n\n#### Initializing State from Props\n\n```tsx\nfunction Timer(handle: Handle, setup: { initialSeconds: number }) {\n  // Initialize from setup prop\n  let seconds = setup.initialSeconds\n  let interval: number | null = null\n\n  function start() {\n    if (interval) return\n    interval = setInterval(() => {\n      seconds--\n      if (seconds <= 0) {\n        stop()\n      }\n      handle.update()\n    }, 1000)\n  }\n\n  function stop() {\n    if (interval) {\n      clearInterval(interval)\n      interval = null\n    }\n  }\n\n  // Cleanup on disconnect\n  handle.signal.addEventListener('abort', stop)\n\n  return (props: { paused?: boolean }) => {\n    if (!props.paused && !interval) {\n      start()\n    } else if (props.paused && interval) {\n      stop()\n    }\n\n    return <div>Time remaining: {seconds}s</div>\n  }\n}\n```\n\n### Focus and Scroll Management\n\nUse `handle.queueTask()` in event handlers for DOM operations that need to happen after the DOM has changed from the next update. This is the pattern for operations like focusing elements, scrolling, or measuring dimensions after conditional rendering.\n\n#### Focus Management\n\n```tsx\nfunction Modal(handle: Handle) {\n  let isOpen = false\n  let closeButton: HTMLButtonElement\n  let openButton: HTMLButtonElement\n\n  return () => (\n    <div>\n      <button\n        mix={[ref((node) => (openButton = node))]}\n        on={{\n          click() {\n            isOpen = true\n            handle.update()\n            // Queue focus operation after modal renders\n            handle.queueTask(() => {\n              closeButton.focus()\n            })\n          },\n        }}\n      >\n        Open Modal\n      </button>\n\n      {isOpen && (\n        <div role=\"dialog\">\n          <button\n            mix={[ref((node) => (closeButton = node))]}\n            on={{\n              click() {\n                isOpen = false\n                handle.update()\n                // Queue focus operation after modal closes\n                handle.queueTask(() => {\n                  openButton.focus()\n                })\n              },\n            }}\n          >\n            Close\n          </button>\n        </div>\n      )}\n    </div>\n  )\n}\n```\n\n#### Scroll Management\n\n```tsx\nfunction ScrollableList(handle: Handle) {\n  let items: string[] = []\n  let newItemInput: HTMLInputElement\n  let listContainer: HTMLElement\n\n  return () => (\n    <div>\n      <input\n        mix={[ref((node) => (newItemInput = node))]}\n        on={{\n          keydown(event) {\n            if (event.key === 'Enter') {\n              let text = event.currentTarget.value\n              if (text.trim()) {\n                items.push(text)\n                event.currentTarget.value = ''\n                handle.update()\n                // Queue scroll operation after new item renders\n                handle.queueTask(() => {\n                  listContainer.scrollTop = listContainer.scrollHeight\n                })\n              }\n            }\n          },\n        }}\n      />\n      <div\n        mix={[ref((node) => (listContainer = node))]}\n        css={{\n          maxHeight: '300px',\n          overflowY: 'auto',\n        }}\n      >\n        {items.map((item, i) => (\n          <div key={i}>{item}</div>\n        ))}\n      </div>\n    </div>\n  )\n}\n```\n\n**Key pattern:** Do the work in the event handler (update state, call `handle.update()`), then use `queueTask` to perform DOM operations that depend on the updated DOM. Don't create intermediate state just to react to it in `queueTask`.\n\n### Controlled vs Uncontrolled Inputs\n\nOnly control an input's value when something besides the user's interaction with that input can also control its state. Otherwise, let the DOM manage the input's value and read from it when needed. **This follows the principle of using minimal component state** - don't store input state you don't need.\n\n**Uncontrolled Input** (use when only the user controls the value):\n\n```tsx\nfunction SearchInput(handle: Handle) {\n  let results: string[] = []\n\n  return () => (\n    <div>\n      <input\n        type=\"text\"\n        on={{\n          async input(event, signal) {\n            // Read value directly from the input - no component state needed\n            let query = event.currentTarget.value\n            // ... use query for search\n          },\n        }}\n      />\n    </div>\n  )\n}\n```\n\n**Key principle:** Don't store the input value in component state unless you need to:\n\n- Set it programmatically (controlled input)\n- Use it for rendering (e.g., showing character count)\n- Transform/validate it before it appears in the input\n\n**Controlled Input** (use when programmatic control is needed):\n\n```tsx\nfunction SlugForm(handle: Handle) {\n  let slug = ''\n  let generatedSlug = ''\n\n  return () => (\n    <form>\n      <label>\n        <input\n          type=\"checkbox\"\n          on={{\n            change(event) {\n              if (event.currentTarget.checked) {\n                generatedSlug = crypto.randomUUID().slice(0, 8)\n              } else {\n                generatedSlug = ''\n              }\n              handle.update()\n            },\n          }}\n        />\n        Auto-generate slug\n      </label>\n      <label>\n        Slug\n        <input\n          type=\"text\"\n          value={generatedSlug || slug}\n          disabled={!!generatedSlug}\n          on={{\n            input(event) {\n              slug = event.currentTarget.value\n              handle.update()\n            },\n          }}\n        />\n      </label>\n    </form>\n  )\n}\n```\n\nUse controlled inputs when:\n\n- The value can be set programmatically (auto-generated fields, reset buttons, external state)\n- The input can be disabled and its value changed by other interactions (like the slug field above)\n- You need to validate or transform input before it appears\n- You need to prevent certain values from being entered\n\nUse uncontrolled inputs when:\n\n- Only the user can change the value through direct interaction with that input\n- You just need to read the value on events (submit, blur, etc.)\n\n### Data Loading and Updates\n\n#### Signals: Managing Interruptions and Disconnects\n\n**Signals in events and tasks are how you manage interruptions and disconnects.** Both event handlers and `queueTask` receive `AbortSignal` parameters that are automatically aborted when:\n\n- The component is removed from the tree\n- For event handlers: The handler is re-entered (user triggers another event before the previous one completes)\n- For `queueTask`: The component re-renders (props changed, triggering a new render cycle)\n\nAlways check `signal.aborted` or pass the signal to async APIs (like `fetch`) to handle interruptions gracefully.\n\n#### Using Event Handler Signals for Race Conditions\n\nEvent handlers receive an `AbortSignal` that's aborted when the handler is re-entered or the component is removed. Use this to prevent race conditions when the user is creating events faster than the async work completes:\n\n```tsx\nfunction SearchInput(handle: Handle) {\n  let results: string[] = []\n  let loading = false\n\n  return () => (\n    <div>\n      <input\n        type=\"text\"\n        on={{\n          async input(event, signal) {\n            let query = event.currentTarget.value\n            loading = true\n            handle.update()\n\n            // Passing signal automatically aborts previous requests\n            let response = await fetch(`/search?q=${query}`, { signal })\n            let data = await response.json()\n            // Manual check for APIs that don't accept a signal\n            if (signal.aborted) return\n\n            results = data.results\n            loading = false\n            handle.update()\n          },\n        }}\n      />\n      {loading && <div>Loading...</div>}\n      {!loading && results.length > 0 && (\n        <ul>\n          {results.map((result, i) => (\n            <li key={i}>{result}</li>\n          ))}\n        </ul>\n      )}\n    </div>\n  )\n}\n```\n\nThe event handler signal is aborted when:\n\n- The user triggers another input event (new search query)\n- The component is removed\n\nThis ensures only the latest search request completes, preventing stale results from overwriting newer ones.\n\n#### Using queueTask for Reactive Data Loading\n\nUse `handle.queueTask()` in the render function for reactive data loading that responds to prop changes. The signal will be aborted if props change or the component is removed:\n\n```tsx\nfunction DataLoader(handle: Handle) {\n  let data: any = null\n  let loading = false\n  let error: Error | null = null\n\n  return (props: { url: string }) => {\n    // Queue data loading task that responds to prop changes\n    handle.queueTask(async (signal) => {\n      loading = true\n      error = null\n      handle.update()\n\n      let response = await fetch(props.url, { signal })\n      let json = await response.json()\n      if (signal.aborted) return\n      data = json\n      loading = false\n      handle.update()\n    })\n\n    if (loading) return <div>Loading...</div>\n    if (error) return <div>Error: {error.message}</div>\n    if (!data) return <div>No data</div>\n\n    return <div>{JSON.stringify(data)}</div>\n  }\n}\n```\n\nThe render signal is aborted when:\n\n- The component re-renders (props changed, e.g., `url` prop changed)\n- The component is removed\n\nThis ensures only the latest data loading request completes. If the `url` prop changes while a request is in flight, the previous request is automatically cancelled.\n\n#### Using Setup Scope for Initial Data\n\nLoad initial data in the setup scope:\n\n```tsx\nfunction UserProfile(handle: Handle, setup: { userId: string }) {\n  let user: User | null = null\n  let loading = true\n\n  // Load initial data in setup scope using queueTask\n  handle.queueTask(async (signal) => {\n    let response = await fetch(`/api/users/${setup.userId}`, { signal })\n    let data = await response.json()\n    if (signal.aborted) return\n    user = data\n    loading = false\n    handle.update()\n  })\n\n  return (props: { showEmail?: boolean }) => {\n    if (loading) return <div>Loading user...</div>\n\n    return (\n      <div>\n        <h1>{user.name}</h1>\n        {props.showEmail && <p>{user.email}</p>}\n      </div>\n    )\n  }\n}\n```\n\nNote that by fetching this data in the setup scope any parent updates that change `setup.userId` will have no effect.\n\n## Testing\n\nWhen writing tests, use `root.flush()` to synchronously execute all pending updates and tasks. This ensures the DOM and component state are fully synchronized before making assertions.\n\nThe main use case is flushing after events that call `handle.update()`. Since updates are asynchronous, you need to flush to ensure the DOM reflects the changes:\n\n```tsx\nfunction Counter(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <button\n      on={{\n        click() {\n          count++\n          handle.update()\n        },\n      }}\n    >\n      Count: {count}\n    </button>\n  )\n}\n\n// In your test\nlet container = document.createElement('div')\nlet root = createRoot(container)\n\nroot.render(<Counter />)\nroot.flush() // Ensure initial render completes\n\nlet button = container.querySelector('button')\nbutton.click() // Triggers handle.update()\nroot.flush() // Flush to apply the update\n\nexpect(container.textContent).toBe('Count: 1')\n```\n\nYou should also flush after the initial `root.render()` to ensure event listeners are attached and the DOM is ready for interaction.\n\n## Summary\n\n- **Components** have two phases: setup (runs once) and render (runs after setup and on updates)\n- **State** is managed with plain JavaScript variables\n- **Updates** are explicit via `handle.update()`\n- **Setup prop** initialization values and excluded from props\n- **Context** enables indirect composition without prop drilling\n- **TypedEventTarget** provides granular updates for better performance\n- **State management best practices:**\n  - Use minimal component state - derive computed values, don't store input state you don't need\n  - Do as much work as possible in event handlers - use event handler scope for transient state, only capture to component state if used for rendering\n- **queueTask** patterns:\n  - Use in event handlers when work needs to happen after DOM changes from the next update\n  - Use in render function for work that needs to be reactive to prop changes\n  - Don't create states as values to \"react to\" on the next render with queueTask\n- **AbortSignals** in events and tasks manage interruptions and disconnects - always check `signal.aborted` or pass to async APIs\n"
  },
  {
    "path": "packages/component/CHANGELOG.md",
    "content": "# `component` CHANGELOG\n\nThis is the changelog for [`component`](https://github.com/remix-run/remix/tree/main/packages/component). It follows [semantic versioning](https://semver.org/).\n\n## v0.5.0\n\n### Minor Changes\n\n- BREAKING CHANGE: `handle.update()` now returns `Promise<AbortSignal>` instead of accepting an optional task callback.\n\n  - The promise is resolved when the update is complete (DOM is updated, tasks have run)\n  - The signal is aborted when the component updates again or is removed.\n\n  ```tsx\n  let signal = await handle.update()\n  // dom is updated\n  // focus/scroll elements\n  // do fetches, etc.\n  ```\n\n  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.\n\n  ```tsx\n  handle.update()\n  handle.queueTask(() => {\n    let rect = widthReferenceNode.getBoundingClientRect()\n    if (rect.width !== width) {\n      width = rect.width\n      handle.update()\n    }\n  })\n  ```\n\n- BREAKING CHANGE: rename virtual root teardown from `remove()` to `dispose()`.\n\n  Old -> new:\n\n  - `root.remove()` -> `root.dispose()` (for both `createRoot()` and `createRangeRoot()` roots)\n  - `app.remove()` -> `app.dispose()` when using `run(...)`\n\n  This aligns virtual root teardown with `run(...).dispose()` for full-app cleanup.\n\n- Add SSR with out-of-order streaming, selective hydration, async frames, and granular ui refresh\n\n  ADDITIONS:\n\n  - `<Frame>`\n  - `renderToStream(node, { resolveFrame })`\n  - `clientEntry`\n  - `run({ loadModule, resolveFrame })`\n  - `handle.frame`\n  - `handle.frames`\n\n### Patch Changes\n\n- Fix host prop removal to fully remove reflected attributes while still resetting runtime form control state.\n\n  Adds regression coverage for attribute removal/update behavior to prevent empty-attribute regressions.\n\n- Fix updates for nested component-to-element replacements\n\n- Harden SVG attribute normalization so canonical SVG attribute names are preserved consistently across server rendering, hydration, and client DOM updates.\n\n  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.\n\n## v0.4.0\n\n### Minor Changes\n\n- Add animation prop, spring, and tween utilities\n\n  - `animate` prop on host elements enables enter, exit, and layout (FLIP) animations\n  - `spring()` function creates spring-based animation iterators with configurable stiffness, damping, and mass\n  - `tween()` function creates time-based animation iterators with customizable duration and easing (including `easings` presets)\n\n- `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) => { ... })`.\n\n### Patch Changes\n\n- Change css processing to use data attribute instead of className\n\n- Add `aspect-ratio` to numeric CSS properties (no longer appends `px` to numeric values)\n\n- Bumped `@remix-run/*` dependencies:\n  - [`@remix-run/interaction@0.5.0`](https://github.com/remix-run/remix/releases/tag/interaction@0.5.0)\n\n## v0.3.0\n\n### Minor Changes\n\n- BREAKING CHANGE: Updated Component API\n\n  - Removed stateless components favoring a single component shape\n  - Components no longer called with `this` function context\n  - Introduced `setup` prop\n    - `setup` prop is passed to the setup function\n    - `props` are only passed to the render function\n\n  #### Example:\n\n  **Before**\n\n  ```tsx\n  function Counter(\n    // `this` binding\n    this: Handle,\n    // props available in setup scope\n    { initialCount }: { initialCount: number },\n  ) {\n    let count = initialCount\n\n    return ({ label }: { label: string }) => (\n      <button\n        on={{\n          click: () => {\n            count++\n            this.update()\n          },\n        }}\n      >\n        {label} {count}\n      </button>\n    )\n  }\n\n  let el = <Counter initialCount={10} label=\"Count\" />\n  ```\n\n  **After**\n\n  ```tsx\n  function Counter(\n    // handle is a normal parameter\n    handle: Handle,\n    // only `setup` prop available in setup scope\n    setup: number,\n  ) {\n    let count = setup\n\n    // props only available in render scope\n    return (props: { label: string }) => (\n      <button\n        on={{\n          click() {\n            count++\n            handle.update()\n          },\n        }}\n      >\n        {props.label} {count}\n      </button>\n    )\n  }\n\n  // usage\n  let el = <Counter setup={10} label=\"Count\" />\n  ```\n\n  #### Discussion:\n\n  ##### Removing stateless components\n\n  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.\n\n  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.\n\n  Additionally, the subtle difference between the two forms was hard to spot in practice.\n\n  ```tsx\n  // this has a bug\n  function Counter(this: Handle) {\n    let count = 0\n    return (\n      <button\n        on={{\n          click: () => {\n            count++\n            this.update()\n          },\n        }}\n      >\n        This has a bug.\n      </button>\n    )\n  }\n\n  // this was the fix, very hard to spot!\n  function Counter(this: Handle) {\n    let count = 0\n    return () => (\n      <button\n        on={{\n          click: () => {\n            count++\n            this.update()\n          },\n        }}\n      >\n        This doesn't\n      </button>\n    )\n  }\n  ```\n\n  The utility of being able to write `return (` instead of `() => (` has little benefit compared to the risks it created.\n\n  - Both `handle` and `props` are optional arguments.\n  - All components must return a function, there is no longer a distinction between stateful or stateless components\n\n  ```tsx\n  // \"stateless\" component before\n  function SomeLayout({ children }: { children: RemixNode }) {\n    return (\n      <div>\n        <h1>Some Title</h1>\n        <main>{children}</main>\n      </div>\n    )\n  }\n\n  // after this change (returns a render function)\n  function SomeLayout() {\n    return ({ children }: { children: RemixNode }) => (\n      <div>\n        <h1>Some Title</h1>\n        <main>{children}</main>\n      </div>\n    )\n  }\n  ```\n\n  ##### The `setup` prop\n\n  The `setup` prop exists primarily to keep regular props out of the setup scope, preventing accidental stale captures.\n\n  When props were available in the setup scope it was easy to accidentally capture the initial value and then lose updates from parents.\n\n  For example:\n\n  ```tsx\n  function Counter(\n    this: Handle,\n    // captured `label` in the wrong scope\n    props: { label: string; initialCount: number },\n  ) {\n    let count = initialCount\n\n    return () => (\n      <button\n        on={{\n          click: () => {\n            count++\n            this.update()\n          },\n        }}\n      >\n        {label /* stale! */} {count}\n      </button>\n    )\n  }\n  ```\n\n  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.\n\n  Now it's simply impossible to make these mistakes because the props aren't available in the setup scope at all.\n\n  ```tsx\n  function Counter(\n    handle: Handle,\n    // only the setup prop is passed here, no access to `label`\n    setup: { count: number },\n  ) {\n    let count = setup.count\n\n    return ({ label }: { label: string }) => (\n      <button\n        on={{\n          click() {\n            count++\n            handle.update()\n          },\n        }}\n      >\n        {label} {count}\n      </button>\n    )\n  }\n\n  let el = <Counter setup={{ count: 10 }} label=\"Count\" />\n  ```\n\n  Now, the only way to make a prop stale is to do it very intentionally:\n\n  ```tsx\n  // this is a bad example, showing the difficulty and ill-advised method of\n  // making a prop value static by moving props into the setup scope\n  function Counter(handle: Handle, setup: number) {\n    let count = setup\n    let initialLabel: string\n\n    return (props: { label: string }) => {\n      // what used to be an accident is now difficult to do on purpose\n      if (!initialLabel) {\n        initialLabel = props.label\n      }\n      return (\n        <button\n          on={{\n            click: () => {\n              count++\n              handle.update()\n            },\n          }}\n        >\n          {initialLabel} {count}\n        </button>\n      )\n    }\n  }\n  ```\n\n  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.\n\n  ##### `this` binding removal\n\n  We used `this` simply for its \"optional first position\" characteristic. Otherwise, it was difficult to decide which parameter should come first: handle or props?\n\n  ```tsx\n  // need handle but not props\n  function PropsFirst(_: PropType, handle: Handle) {}\n\n  // or with a reversed signature, need props but not handle\n  function HandleFirst(_: Handle, props: PropType) {}\n  ```\n\n  Using `this` as an optional context argument solved the problem well:\n\n  ```tsx\n  function Neither() {}\n  function Both(this: Handle, props: PropType) {}\n  function OnlyHandle(this: Handle) {}\n  function OnlyProps(props: PropType) {}\n  ```\n\n  This is no longer a concern since props have been removed from the setup scope because:\n\n  - If you need `setup` then you are likely stateful\n  - If you are stateful you need the handle\n  - Therefore `setup` isn't useful without `handle`\n\n  This affords a function signature that doesn't require skipping the first argument to get access to the second:\n\n  ```tsx\n  function OnlyHandle(handle: Handle) {}\n  function Both(handle: Handle, setup: SomeInterface) {}\n  function Neither() {}\n  function OnlySetup(_: Handle, setup: SomeInterface) {\n    // rare: unclear what setup would be used for without a handle\n  }\n  ```\n\n  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.\n\n  ```tsx\n  function Counter(handle: Handle, setup: number) {\n    let count = setup\n\n    // function declarations inside the setup scope\n    function updateCount() {\n      count++\n      handle.update()\n    }\n\n    return (props: { label: string }) => {\n      return (\n        <button\n          on={{\n            // object method shorthand\n            click() {\n              updateCount()\n            },\n          }}\n        >\n          {props.label} {count}\n        </button>\n      )\n    }\n  }\n  ```\n\n### Patch Changes\n\n- Fix SVG namespace propagation through components\n\n  Components rendered inside `<svg>` elements now correctly create SVG elements instead of HTML elements.\n\n- Remove requirement for every element to have props\n\n  Originally, `@remix/component` 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/component` apps using alternative JSX templating tools like [`htm`](https://www.npmjs.com/package/htm).\n\n## v0.2.1 (2025-12-19)\n\n- Fix node replacement\n\n  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.\n\n## v0.2.0 (2025-12-18)\n\n- This is the initial release of the component package.\n\n  See the [README](https://github.com/remix-run/remix/blob/main/packages/component/README.md) for more information.\n\n## Unreleased\n\n- Initial release\n"
  },
  {
    "path": "packages/component/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/component/README.md",
    "content": "# component\n\nA 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.\n\n## Features\n\n- **JSX Runtime** - Convenient JSX syntax\n- **Component State** - State managed with plain JavaScript variables\n- **Manual Updates** - Explicit control over when components update via `handle.update()`\n- **Real DOM Events** - Events are real DOM events using the `on()` mixin and `addEventListeners()`\n- **Inline CSS** - `css(...)` mixin with pseudo-selectors and nested rules\n- **Server Rendering** - Stream full pages or fragments with `renderToStream`\n- **Hydration** - Mark interactive components with `clientEntry` and hydrate them on the client with `run`\n- **Frames** - `<Frame>` streams partial server UI into the page and can be reloaded without a full page navigation\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Quick Start\n\n### Server\n\nRender a full page to a streaming response:\n\n```tsx\nimport { renderToStream } from 'remix/component/server'\nimport { Frame } from 'remix/component'\nimport { Counter } from './assets/counter.tsx'\n\nfunction App() {\n  return () => (\n    <html>\n      <head>\n        <title>My App</title>\n        <script async type=\"module\" src=\"/assets/entry.js\" />\n      </head>\n      <body>\n        <h1>Hello</h1>\n        <Counter setup={0} label=\"Clicks\" />\n        <Frame src=\"/sidebar\" fallback={<div>Loading...</div>} />\n      </body>\n    </html>\n  )\n}\n\nlet stream = renderToStream(<App />, {\n  resolveFrame(src, target, context) {\n    let headers = new Headers({ accept: 'text/html' })\n    if (target) headers.set('x-remix-target', target)\n    return fetch(new URL(src, context?.currentFrameSrc ?? request.url), { headers }).then((res) =>\n      res.text(),\n    )\n  },\n})\n\nreturn new Response(stream, {\n  headers: { 'Content-Type': 'text/html' },\n})\n```\n\n### Client Entry\n\nMark components that need client-side interactivity with `clientEntry`. They render on the server and hydrate on the client:\n\n```tsx\nimport { clientEntry, on, type Handle } from 'remix/component'\n\nexport let Counter = clientEntry(\n  '/assets/counter.js#Counter',\n  function Counter(handle: Handle, setup: number) {\n    let count = setup\n\n    return (props: { label: string }) => (\n      <div>\n        <span>\n          {props.label}: {count}\n        </span>\n        <button\n          mix={[\n            on('click', () => {\n              count++\n              handle.update()\n            }),\n          ]}\n        >\n          +\n        </button>\n      </div>\n    )\n  },\n)\n```\n\nThe first argument is the module URL and export name the client will use to load this component. The component renders on the server like any other component, and the client hydrates it in place, preserving the server-rendered HTML.\n\n### Client\n\nBoot the client with `run`. It finds all client entries in the page, loads their modules, and hydrates them:\n\n```tsx\nimport { run } from 'remix/component'\n\nlet app = run({\n  async loadModule(moduleUrl, exportName) {\n    let mod = await import(moduleUrl)\n    return mod[exportName]\n  },\n  async resolveFrame(src, signal, target) {\n    let headers = new Headers({ accept: 'text/html' })\n    if (target) headers.set('x-remix-target', target)\n    let res = await fetch(src, { headers, signal })\n    return res.body ?? (await res.text())\n  },\n})\n\nawait app.ready()\n```\n\n### Frames\n\n`<Frame>` renders server content into the page. Frames can stream in after the initial HTML, nest other frames, and contain client entries. They can be reloaded from the client without a full page navigation:\n\n```tsx\n<Frame src=\"/sidebar\" fallback={<div>Loading sidebar...</div>} />\n```\n\nClient entries inside a frame can trigger a reload:\n\n```tsx\nfunction RefreshButton(handle: Handle) {\n  return () => (\n    <button\n      mix={[\n        on('click', () => {\n          handle.frame.reload()\n        }),\n      ]}\n    >\n      Refresh\n    </button>\n  )\n}\n```\n\nWhen a frame reloads, its server HTML is re-fetched and diffed into the page. Client entries inside the frame receive updated props from the server while preserving their local state.\n\nYou can also name frames and reload adjacent ones:\n\n```tsx\n<Frame name=\"cart-summary\" src=\"/cart-summary\" />\n<Frame src=\"/cart-row\" />\n```\n\n```tsx\nfunction CartRow(handle: Handle) {\n  return () => (\n    <button\n      mix={[\n        on('click', async () => {\n          await handle.frames.get('cart-summary')?.reload()\n          await handle.frame.reload()\n        }),\n      ]}\n    >\n      Save\n    </button>\n  )\n}\n```\n\nWhen a frame reloads, its server HTML is re-fetched and diffed into the page. Client entries inside the frame receive updated props from the server while preserving their local state.\n\n## Components\n\nAll 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:\n\n```tsx\nfunction Counter(handle: Handle, setup: number) {\n  // Setup phase: runs once\n  let count = setup\n\n  // Return render function: runs on every update\n  return (props: { label?: string }) => (\n    <div>\n      {props.label || 'Count'}: {count}\n      <button\n        mix={[\n          on('click', () => {\n            count++\n            handle.update()\n          }),\n        ]}\n      >\n        Increment\n      </button>\n    </div>\n  )\n}\n```\n\n### Setup Prop vs Props\n\nWhen a component returns a function, it has two phases:\n\n1. **Setup phase** - The component function receives the `setup` prop and runs once. Use this for initialization.\n2. **Render phase** - The returned function receives props and runs on initial render and every update afterward. Use this for rendering.\n\nThe `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.\n\n- `setup` prop for values that initialize state (e.g., `initial`, `defaultValue`)\n- Regular props for values that change over time (e.g., `label`, `disabled`)\n\n```tsx\n// Usage: setup prop goes to setup function, regular props go to render function\nlet el = <Counter setup={5} label=\"Total\" />\n\nfunction Counter(\n  handle: Handle,\n  setup: number, // receives 5 (the setup prop value)\n) {\n  let count = setup // use setup for initialization\n\n  return (props: { label?: string }) => {\n    // props only receives { label: \"Total\" } - not the setup prop\n    return (\n      <div>\n        {props.label}: {count}\n      </div>\n    )\n  }\n}\n```\n\n## Events\n\nEvents use the `on()` mixin. Listeners receive an `AbortSignal` that's aborted when the component is disconnected or the handler is re-entered.\n\n```tsx\nfunction SearchInput(handle: Handle) {\n  let query = ''\n\n  return () => (\n    <input\n      type=\"text\"\n      value={query}\n      mix={[\n        on('input', (event, signal) => {\n          query = event.currentTarget.value\n          handle.update()\n\n          // Pass the signal to abort the fetch on re-entry or node removal\n          // This avoids race conditions in the UI and manages cleanup\n          fetch(`/search?q=${query}`, { signal })\n            .then((res) => res.json())\n            .then((results) => {\n              if (signal.aborted) return\n              // Update results\n            })\n        }),\n      ]}\n    />\n  )\n}\n```\n\nYou can also listen to global event targets like `document` or `window` using `addEventListeners()` with automatic cleanup on component removal:\n\n```tsx\nfunction KeyboardTracker(handle: Handle) {\n  let keys: string[] = []\n\n  addEventListeners(document, handle.signal, {\n    keydown: (event) => {\n      keys.push(event.key)\n      handle.update()\n    },\n  })\n\n  return () => <div>Keys: {keys.join(', ')}</div>\n}\n```\n\n## CSS Mixin\n\nUse the `css(...)` mixin for inline styles with pseudo-selectors and nested rules:\n\n```tsx\nfunction Button(handle: Handle) {\n  return () => (\n    <button\n      mix={[\n        css({\n          color: 'white',\n          backgroundColor: 'blue',\n          '&:hover': {\n            backgroundColor: 'darkblue',\n          },\n          '&:active': {\n            transform: 'scale(0.98)',\n          },\n        }),\n      ]}\n    >\n      Click me\n    </button>\n  )\n}\n```\n\nThe 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:\n\n```css\n.button {\n  color: white;\n  background-color: blue;\n\n  &:hover {\n    background-color: darkblue;\n  }\n\n  &::before {\n    content: '';\n    position: absolute;\n  }\n\n  &[aria-selected='true'] {\n    border: 2px solid yellow;\n  }\n\n  .icon {\n    width: 16px;\n    height: 16px;\n  }\n\n  @media (max-width: 768px) {\n    padding: 8px;\n  }\n}\n```\n\n```tsx\nfunction Button(handle: Handle) {\n  return () => (\n    <button\n      mix={[\n        css({\n          color: 'white',\n          backgroundColor: 'blue',\n          '&:hover': {\n            backgroundColor: 'darkblue',\n          },\n          '&::before': {\n            content: '\"\"',\n            position: 'absolute',\n          },\n          '&[aria-selected=\"true\"]': {\n            border: '2px solid yellow',\n          },\n          '.icon': {\n            width: '16px',\n            height: '16px',\n          },\n          '@media (max-width: 768px)': {\n            padding: '8px',\n          },\n        }),\n      ]}\n    >\n      <span className=\"icon\">★</span>\n      Click me\n    </button>\n  )\n}\n```\n\n## Ref Mixin\n\nUse the `ref(...)` mixin 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.\n\n```tsx\nfunction Form(handle: Handle) {\n  let inputRef: HTMLInputElement\n\n  return () => (\n    <form>\n      <input\n        type=\"text\"\n        // get the input node\n        mix={[ref((node) => (inputRef = node))]}\n      />\n      <button\n        mix={[\n          on('click', () => {\n            // Select it from other parts of the form\n            inputRef.select()\n          }),\n        ]}\n      >\n        Focus Input\n      </button>\n    </form>\n  )\n}\n```\n\nThe `ref` callback receives an `AbortSignal` as its second parameter, which is aborted when the element is removed from the DOM:\n\n```tsx\nfunction Component(handle: Handle) {\n  return () => (\n    <div\n      mix={[\n        ref((node, signal) => {\n          // Set up something that needs cleanup\n          let observer = new ResizeObserver(() => {\n            // handle resize\n          })\n          observer.observe(node)\n\n          // Clean up when element is removed\n          signal.addEventListener('abort', () => {\n            observer.disconnect()\n          })\n        }),\n      ]}\n    >\n      Content\n    </div>\n  )\n}\n```\n\n## Component Handle API\n\nComponents receive a `Handle` as their first argument with the following API:\n\n- **`handle.update()`** - Schedule an update and await completion to get an `AbortSignal`.\n- **`handle.queueTask(task)`** - Schedule a task to run after the next update. Useful for DOM operations that need to happen after rendering (e.g., moving focus, scrolling, measuring elements, etc.).\n- **`addEventListeners(target, handle.signal, listeners)`** - Listen to an event target with automatic cleanup when the component disconnects.\n- **`handle.signal`** - An `AbortSignal` that's aborted when the component is disconnected. Useful for cleanup.\n- **`handle.id`** - Stable identifier per component instance.\n- **`handle.context`** - Context API for ancestor/descendant communication.\n- **`handle.frame`** - The component's closest frame. Call `handle.frame.reload()` to refresh the frame's server content.\n- **`handle.frames.get(name)`** - Look up named frames in the current runtime tree for adjacent frame reloads.\n\n### `handle.update()`\n\nSchedule an update and optionally await completion to coordinate post-update work.\n\n```tsx\nfunction Counter(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <button\n      mix={[\n        on('click', () => {\n          count++\n          handle.update()\n        }),\n      ]}\n    >\n      Count: {count}\n    </button>\n  )\n}\n```\n\nYou can await the update before doing DOM work:\n\n```tsx\nfunction Player(handle: Handle) {\n  let isPlaying = false\n  let playButton: HTMLButtonElement\n  let stopButton: HTMLButtonElement\n\n  return () => (\n    <div>\n      <button\n        disabled={isPlaying}\n        mix={[\n          ref((node) => (playButton = node)),\n          on('click', async () => {\n            isPlaying = true\n            await handle.update()\n            // Focus the enabled button after update completes\n            stopButton.focus()\n          }),\n        ]}\n      >\n        Play\n      </button>\n      <button\n        disabled={!isPlaying}\n        mix={[\n          ref((node) => (stopButton = node)),\n          on('click', async () => {\n            isPlaying = false\n            await handle.update()\n            // Focus the enabled button after update completes\n            playButton.focus()\n          }),\n        ]}\n      >\n        Stop\n      </button>\n    </div>\n  )\n}\n```\n\n### `handle.queueTask(task)`\n\nSchedule a task to run after the next update. Useful for DOM operations that need to happen after rendering (e.g., moving focus, scrolling, measuring elements).\n\n```tsx\nfunction Form(handle: Handle) {\n  let showDetails = false\n  let detailsSection: HTMLElement\n\n  return () => (\n    <form>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={showDetails}\n          mix={[\n            on('change', (event) => {\n              showDetails = event.currentTarget.checked\n              handle.update()\n              if (showDetails) {\n                // Scroll to the expanded section after it renders\n                handle.queueTask(() => {\n                  detailsSection.scrollIntoView({ behavior: 'smooth', block: 'start' })\n                })\n              }\n            }),\n          ]}\n        />\n        Show additional details\n      </label>\n      {showDetails && (\n        <section\n          mix={[\n            css({\n              marginTop: '2rem',\n              padding: '1rem',\n              border: '1px solid #ccc',\n            }),\n            ref((node) => (detailsSection = node)),\n          ]}\n        >\n          <h2>Additional Details</h2>\n          <p>This section appears when the checkbox is checked.</p>\n        </section>\n      )}\n    </form>\n  )\n}\n```\n\n### `addEventListeners(target, handle.signal, listeners)`\n\nListen 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`.\n\n```tsx\nfunction KeyboardTracker(handle: Handle) {\n  let keys: string[] = []\n\n  addEventListeners(document, handle.signal, {\n    keydown: (event) => {\n      keys.push(event.key)\n      handle.update()\n    },\n  })\n\n  return () => <div>Keys: {keys.join(', ')}</div>\n}\n```\n\nThe listeners are automatically removed when the component is disconnected, so you don't need to manually clean up.\n\n### `handle.signal`\n\nAn `AbortSignal` that's aborted when the component is disconnected. Useful for cleanup operations.\n\n```tsx\nfunction Clock(handle: Handle) {\n  let interval = setInterval(() => {\n    // clear the interval when the component is disconnected\n    if (handle.signal.aborted) {\n      clearInterval(interval)\n      return\n    }\n    handle.update()\n  }, 1000)\n  return () => <span>{new Date().toString()}</span>\n}\n```\n\n### `handle.id`\n\nStable identifier per component instance. Useful for HTML APIs like `htmlFor`, `aria-owns`, etc. so consumers don't have to supply an id.\n\n```tsx\nfunction LabeledInput(handle: Handle) {\n  return () => (\n    <div>\n      <label htmlFor={handle.id}>Name</label>\n      <input id={handle.id} type=\"text\" />\n    </div>\n  )\n}\n```\n\n### `handle.context`\n\nContext 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.\n\n```tsx\nfunction App(handle: Handle<{ theme: string }>) {\n  handle.context.set({ theme: 'dark' })\n\n  return () => (\n    <div>\n      <Header />\n      <Content />\n    </div>\n  )\n}\n\nfunction Header(handle: Handle) {\n  // Consume context from App\n  let { theme } = handle.context.get(App)\n  return () => (\n    <header mix={[css({ backgroundColor: theme === 'dark' ? '#000' : '#fff' })]}>Header</header>\n  )\n}\n```\n\nSetting 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.\n\n```tsx\nimport { TypedEventTarget } from 'remix/component'\n\nclass Theme extends TypedEventTarget<{ change: Event }> {\n  #value: 'light' | 'dark' = 'light'\n\n  get value() {\n    return this.#value\n  }\n\n  setValue(value: string) {\n    this.#value = value\n    this.dispatchEvent(new Event('change'))\n  }\n}\n\nfunction App(handle: Handle<Theme>) {\n  let theme = new Theme()\n  handle.context.set(theme)\n\n  return () => (\n    <div>\n      <button\n        mix={[\n          on('click', () => {\n            // no updates in the parent component\n            theme.setValue(theme.value === 'light' ? 'dark' : 'light')\n          }),\n        ]}\n      >\n        Toggle Theme\n      </button>\n      <ThemedContent />\n    </div>\n  )\n}\n\nfunction ThemedContent(handle: Handle) {\n  let theme = handle.context.get(App)\n\n  // Subscribe to theme changes and update when it changes\n  addEventListeners(theme, handle.signal, { change: () => handle.update() })\n\n  return () => (\n    <div mix={[css({ backgroundColor: theme.value === 'dark' ? '#000' : '#fff' })]}>\n      Current theme: {theme.value}\n    </div>\n  )\n}\n```\n\n## Fragments\n\nUse `Fragment` to group elements without adding extra DOM nodes:\n\n```tsx\nfunction List(handle: Handle) {\n  return () => (\n    <>\n      <li>Item 1</li>\n      <li>Item 2</li>\n      <li>Item 3</li>\n    </>\n  )\n}\n```\n\n## Documentation\n\n- [Getting Started](./docs/getting-started.md)\n- [Components](./docs/components.md)\n- [Handle API](./docs/handle.md)\n- [Server Rendering](./docs/server-rendering.md)\n- [Hydration](./docs/hydration.md)\n- [Frames](./docs/frames.md)\n- [Styling](./docs/styling.md)\n- [Events](./docs/events.md)\n- [Interactions](./docs/interactions.md)\n- [Context](./docs/context.md)\n- [Composition](./docs/composition.md)\n- [Patterns](./docs/patterns.md)\n- [Testing](./docs/testing.md)\n- Animations\n  - [spring](./docs/spring.md)\n  - [tween](./docs/tween.md)\n- [Server Rendering](./docs/server-rendering.md)\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/component/bench/.gitignore",
    "content": ".last-args.json\n.remix-prev-results.json"
  },
  {
    "path": "packages/component/bench/frameworks/preact/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Preact Benchmark</title>\n    <link rel=\"stylesheet\" href=\"/styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"dist/index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/bench/frameworks/preact/index.tsx",
    "content": "import { useState, useCallback } from 'preact/hooks'\nimport {\n  get1000Rows,\n  get10000Rows,\n  remove,\n  sortRows,\n  swapRows,\n  updatedEvery10thRow,\n  buildData,\n} from '../shared.ts'\nimport type { Benchmark, Row } from '../shared.ts'\nimport { render } from 'preact'\nimport { act } from 'preact/test-utils'\n\nexport const name = 'preact'\n\n// Stateful Metric Card Component\nfunction MetricCard({\n  id,\n  label,\n  value,\n  change,\n}: {\n  id: number\n  label: string\n  value: string\n  change: string\n}) {\n  let [selected, setSelected] = useState(false)\n  let [hovered, setHovered] = useState(false)\n\n  return (\n    <div\n      class={`metric-card ${selected ? 'selected' : ''}`}\n      onClick={() => setSelected(!selected)}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={(e: any) => {\n        e.currentTarget.style.outline = '2px solid #222'\n        e.currentTarget.style.outlineOffset = '2px'\n      }}\n      onBlur={(e: any) => {\n        e.currentTarget.style.outline = ''\n      }}\n      tabIndex={0}\n      style={{\n        backgroundColor: hovered ? '#f5f5f5' : '#fff',\n        transform: hovered && !selected ? 'translateY(-2px)' : 'translateY(0)',\n        transition: 'all 0.2s',\n        padding: '20px',\n        border: '1px solid #ddd',\n        borderRadius: '8px',\n        cursor: 'pointer',\n        boxShadow: selected ? '0 4px 8px rgba(0,0,0,0.1)' : '0 2px 4px rgba(0,0,0,0.05)',\n      }}\n    >\n      <div style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}>{label}</div>\n      <div style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '4px' }}>{value}</div>\n      <div style={{ fontSize: '12px', color: change.startsWith('+') ? '#28a745' : '#dc3545' }}>\n        {change}\n      </div>\n    </div>\n  )\n}\n\n// Stateful Chart Bar Component\nfunction ChartBar({ value, index }: { value: number; index: number }) {\n  let [hovered, setHovered] = useState(false)\n\n  return (\n    <div\n      class=\"chart-bar\"\n      style={{\n        height: `${value}%`,\n        backgroundColor: hovered ? '#286090' : '#337ab7',\n        width: '30px',\n        margin: '0 2px',\n        cursor: 'pointer',\n        transition: 'all 0.2s',\n        opacity: hovered ? 0.9 : 1,\n        transform: hovered ? 'scaleY(1.1)' : 'scaleY(1)',\n      }}\n      onClick={() => {}}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={(e: any) => {\n        e.currentTarget.style.outline = '2px solid #222'\n        e.currentTarget.style.outlineOffset = '2px'\n      }}\n      onBlur={(e: any) => {\n        e.currentTarget.style.outline = ''\n      }}\n      tabIndex={0}\n    />\n  )\n}\n\n// Stateful Activity Item Component\nfunction ActivityItem({\n  id,\n  title,\n  time,\n  icon,\n}: {\n  id: number\n  title: string\n  time: string\n  icon: string\n}) {\n  let [read, setRead] = useState(false)\n  let [hovered, setHovered] = useState(false)\n\n  return (\n    <li\n      class={`activity-item ${read ? 'read' : ''}`}\n      onClick={() => setRead(!read)}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      style={{\n        padding: '12px',\n        borderBottom: '1px solid #eee',\n        cursor: 'pointer',\n        backgroundColor: hovered ? '#f5f5f5' : read ? 'rgba(245, 245, 245, 0.6)' : '#fff',\n        display: 'flex',\n        alignItems: 'center',\n        gap: '12px',\n      }}\n    >\n      <span\n        style={{\n          width: '32px',\n          height: '32px',\n          borderRadius: '50%',\n          backgroundColor: '#337ab7',\n          color: '#fff',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          fontWeight: 'bold',\n        }}\n      >\n        {icon}\n      </span>\n      <div style={{ flex: 1 }}>\n        <div style={{ fontWeight: read ? 'normal' : 'bold' }}>{title}</div>\n        <div style={{ fontSize: '12px', color: '#666' }}>{time}</div>\n      </div>\n    </li>\n  )\n}\n\n// Stateful Dropdown Menu Component\nfunction DropdownMenu({ rowId }: { rowId: number }) {\n  let [open, setOpen] = useState(false)\n  let [hovered, setHovered] = useState(false)\n\n  let actions = ['View Details', 'Edit', 'Duplicate', 'Archive', 'Delete']\n\n  return (\n    <div style={{ position: 'relative', display: 'inline-block' }}>\n      <button\n        class=\"btn btn-primary\"\n        onClick={(e: any) => {\n          e.stopPropagation()\n          setOpen(!open)\n        }}\n        onMouseEnter={() => setHovered(true)}\n        onMouseLeave={() => setHovered(false)}\n        onFocus={(e: any) => {\n          e.currentTarget.style.outline = '2px solid #222'\n          e.currentTarget.style.outlineOffset = '2px'\n        }}\n        onBlur={(e: any) => {\n          e.currentTarget.style.outline = ''\n        }}\n        style={{\n          padding: '4px 8px',\n          fontSize: '12px',\n          backgroundColor: hovered ? '#286090' : '#337ab7',\n        }}\n      >\n        ⋮\n      </button>\n      {open && (\n        <div\n          style={{\n            position: 'absolute',\n            top: '100%',\n            right: 0,\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '4px',\n            boxShadow: '0 4px 8px rgba(0,0,0,0.1)',\n            zIndex: 1000,\n            minWidth: '150px',\n            marginTop: '4px',\n          }}\n          onMouseLeave={() => setOpen(false)}\n        >\n          {actions.map((action, idx) => (\n            <div\n              key={idx}\n              onClick={(e: any) => {\n                e.stopPropagation()\n                setOpen(false)\n              }}\n              onMouseEnter={(e: any) => {\n                e.currentTarget.style.backgroundColor = '#f5f5f5'\n              }}\n              onMouseLeave={(e: any) => {\n                e.currentTarget.style.backgroundColor = '#fff'\n              }}\n              style={{\n                padding: '8px 12px',\n                cursor: 'pointer',\n                borderBottom: idx < actions.length - 1 ? '1px solid #eee' : 'none',\n              }}\n            >\n              {action}\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n\n// Stateful Dashboard Table Row Component\nfunction DashboardTableRow({ row }: { row: Row }) {\n  let [hovered, setHovered] = useState(false)\n  let [selected, setSelected] = useState(false)\n\n  return (\n    <tr\n      class={selected ? 'danger' : ''}\n      onClick={() => setSelected(!selected)}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      style={{\n        backgroundColor: hovered ? '#f5f5f5' : '#fff',\n        cursor: 'pointer',\n      }}\n    >\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>{row.id}</td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>{row.label}</td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        <span style={{ color: '#28a745' }}>Active</span>\n      </td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        ${(row.id * 10.5).toFixed(2)}\n      </td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        <DropdownMenu rowId={row.id} />\n      </td>\n    </tr>\n  )\n}\n\n// Stateful Search Input Component\nfunction SearchInput() {\n  let [value, setValue] = useState('')\n  let [focused, setFocused] = useState(false)\n\n  return (\n    <input\n      type=\"text\"\n      placeholder=\"Search...\"\n      value={value}\n      onInput={(e: any) => setValue(e.target.value)}\n      onFocus={() => setFocused(true)}\n      onBlur={() => setFocused(false)}\n      style={{\n        padding: '8px 12px',\n        border: `1px solid ${focused ? '#337ab7' : '#ddd'}`,\n        borderRadius: '4px',\n        fontSize: '14px',\n        width: '300px',\n        outline: focused ? '2px solid #337ab7' : 'none',\n        outlineOffset: '2px',\n      }}\n    />\n  )\n}\n\n// Stateful Form Widgets Component\nfunction FormWidgets() {\n  let [selectValue, setSelectValue] = useState('option1')\n  let [checkboxValues, setCheckboxValues] = useState<Set<string>>(new Set())\n  let [radioValue, setRadioValue] = useState('radio1')\n  let [toggleValue, setToggleValue] = useState(false)\n  let [progressValue, setProgressValue] = useState(45)\n\n  return (\n    <div style={{ padding: '20px', backgroundColor: '#f9f9f9', borderRadius: '8px' }}>\n      <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Settings</h3>\n      <div style={{ marginBottom: '16px' }}>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Select Option\n        </label>\n        <select\n          value={selectValue}\n          onChange={(e: any) => setSelectValue(e.target.value)}\n          onFocus={(e: any) => {\n            e.currentTarget.style.borderColor = '#337ab7'\n            e.currentTarget.style.outline = '2px solid #337ab7'\n            e.currentTarget.style.outlineOffset = '2px'\n          }}\n          onBlur={(e: any) => {\n            e.currentTarget.style.borderColor = '#ddd'\n            e.currentTarget.style.outline = 'none'\n          }}\n          style={{\n            padding: '6px 12px',\n            border: '1px solid #ddd',\n            borderRadius: '4px',\n            fontSize: '14px',\n            width: '100%',\n          }}\n        >\n          <option value=\"option1\">Option 1</option>\n          <option value=\"option2\">Option 2</option>\n          <option value=\"option3\">Option 3</option>\n          <option value=\"option4\">Option 4</option>\n        </select>\n      </div>\n      {['Checkbox 1', 'Checkbox 2', 'Checkbox 3'].map((label, idx) => (\n        <div\n          key={idx}\n          style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}\n        >\n          <input\n            type=\"checkbox\"\n            id={`checkbox-${idx}`}\n            checked={checkboxValues.has(`checkbox-${idx}`)}\n            onChange={(e: any) => {\n              let next = new Set(checkboxValues)\n              if (e.target.checked) {\n                next.add(`checkbox-${idx}`)\n              } else {\n                next.delete(`checkbox-${idx}`)\n              }\n              setCheckboxValues(next)\n            }}\n            onFocus={(e: any) => {\n              e.currentTarget.style.outline = '2px solid #337ab7'\n              e.currentTarget.style.outlineOffset = '2px'\n            }}\n            onBlur={(e: any) => {\n              e.currentTarget.style.outline = ''\n            }}\n          />\n          <label htmlFor={`checkbox-${idx}`} style={{ fontSize: '14px', cursor: 'pointer' }}>\n            {label}\n          </label>\n        </div>\n      ))}\n      <div style={{ marginBottom: '16px' }}>\n        {['Radio 1', 'Radio 2', 'Radio 3'].map((label, idx) => (\n          <label key={idx} style={{ display: 'block', marginBottom: '8px', cursor: 'pointer' }}>\n            <input\n              type=\"radio\"\n              name=\"radio-group\"\n              value={`radio${idx + 1}`}\n              checked={radioValue === `radio${idx + 1}`}\n              onChange={(e: any) => setRadioValue(e.target.value)}\n              onFocus={(e: any) => {\n                e.currentTarget.style.outline = '2px solid #337ab7'\n                e.currentTarget.style.outlineOffset = '2px'\n              }}\n              onBlur={(e: any) => {\n                e.currentTarget.style.outline = ''\n              }}\n              style={{ marginRight: '8px' }}\n            />\n            {label}\n          </label>\n        ))}\n      </div>\n      <div style={{ marginBottom: '16px' }}>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Toggle Switch\n        </label>\n        <label\n          style={{\n            display: 'inline-block',\n            position: 'relative',\n            width: '50px',\n            height: '24px',\n            cursor: 'pointer',\n          }}\n        >\n          <input\n            type=\"checkbox\"\n            checked={toggleValue}\n            onChange={(e: any) => setToggleValue(e.target.checked)}\n            onFocus={(e: any) => {\n              e.currentTarget.style.outline = '2px solid #222'\n              e.currentTarget.style.outlineOffset = '2px'\n            }}\n            onBlur={(e: any) => {\n              e.currentTarget.style.outline = ''\n            }}\n            style={{ opacity: 0, width: 0, height: 0 }}\n          />\n          <span\n            style={{\n              position: 'absolute',\n              top: 0,\n              left: 0,\n              right: 0,\n              bottom: 0,\n              backgroundColor: toggleValue ? '#337ab7' : '#ccc',\n              borderRadius: '24px',\n              transition: 'background-color 0.3s',\n            }}\n          >\n            <span\n              style={{\n                position: 'absolute',\n                content: '\"\"',\n                height: '18px',\n                width: '18px',\n                left: '3px',\n                bottom: '3px',\n                backgroundColor: '#fff',\n                borderRadius: '50%',\n                transition: 'transform 0.3s',\n                transform: toggleValue ? 'translateX(26px)' : 'translateX(0)',\n              }}\n            />\n          </span>\n        </label>\n      </div>\n      <div>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Progress Bar\n        </label>\n        <div\n          style={{\n            width: '100%',\n            height: '24px',\n            backgroundColor: '#eee',\n            borderRadius: '4px',\n            overflow: 'hidden',\n            position: 'relative',\n          }}\n        >\n          <div\n            style={{\n              width: `${progressValue}%`,\n              height: '100%',\n              backgroundColor: '#337ab7',\n              transition: 'width 0.3s',\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'center',\n              color: '#fff',\n              fontSize: '12px',\n            }}\n          >\n            {progressValue}%\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction Dashboard({ onSwitchToTable }: { onSwitchToTable: () => void }) {\n  let [dashboardRows, setDashboardRows] = useState(() => buildData(300))\n\n  let sortDashboardAsc = () => {\n    setDashboardRows((current) => sortRows(current, true))\n  }\n\n  let sortDashboardDesc = () => {\n    setDashboardRows((current) => sortRows(current, false))\n  }\n  let chartData = [65, 45, 78, 52, 89, 34, 67, 91, 43, 56, 72, 38, 55, 82, 47, 63, 71, 39, 58, 84]\n  let activities = Array.from({ length: 50 }, (_, i) => ({\n    id: i + 1,\n    title: `Activity ${i + 1}: ${['Order placed', 'Payment received', 'Shipment created', 'Customer registered', 'Product updated'][i % 5]}`,\n    time: `${i + 1} ${i === 0 ? 'minute' : 'minutes'} ago`,\n    icon: ['O', 'P', 'S', 'C', 'U'][i % 5],\n  }))\n\n  return (\n    <div class=\"container\" style={{ maxWidth: '1400px' }}>\n      <div\n        style={{\n          display: 'flex',\n          marginBottom: '20px',\n          alignItems: 'center',\n          justifyContent: 'space-between',\n        }}\n      >\n        <h1 style={{ margin: 0 }}>Dashboard</h1>\n        <button\n          id=\"switchToTable\"\n          class=\"btn btn-primary\"\n          type=\"button\"\n          onClick={onSwitchToTable}\n          onFocus={(e: any) => {\n            e.currentTarget.style.outline = '2px solid #222'\n            e.currentTarget.style.outlineOffset = '2px'\n          }}\n          onBlur={(e: any) => {\n            e.currentTarget.style.outline = ''\n          }}\n        >\n          Switch to Table\n        </button>\n      </div>\n\n      <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>\n        <div style={{ flex: 1, display: 'flex', gap: '16px' }}>\n          <MetricCard id={1} label=\"Total Sales\" value=\"$125,430\" change=\"+12.5%\" />\n          <MetricCard id={2} label=\"Orders\" value=\"1,234\" change=\"+8.2%\" />\n          <MetricCard id={3} label=\"Customers\" value=\"5,678\" change=\"+15.3%\" />\n          <MetricCard id={4} label=\"Revenue\" value=\"$89,123\" change=\"+9.7%\" />\n        </div>\n      </div>\n\n      <div\n        style={{\n          display: 'grid',\n          gridTemplateColumns: '1fr 1fr',\n          gap: '20px',\n          marginBottom: '20px',\n        }}\n      >\n        <div\n          style={{\n            padding: '20px',\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n          }}\n        >\n          <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Sales Performance</h3>\n          <div\n            style={{\n              display: 'flex',\n              alignItems: 'flex-end',\n              justifyContent: 'space-around',\n              height: '200px',\n              padding: '20px 0',\n            }}\n          >\n            {chartData.map((value, index) => (\n              <ChartBar key={index} value={value} index={index} />\n            ))}\n          </div>\n        </div>\n\n        <div\n          style={{\n            padding: '20px',\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n          }}\n        >\n          <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Recent Activity</h3>\n          <ul\n            style={{\n              listStyle: 'none',\n              padding: 0,\n              margin: 0,\n              maxHeight: '200px',\n              overflowY: 'auto',\n            }}\n          >\n            {activities.map((activity) => (\n              <ActivityItem key={activity.id} {...activity} />\n            ))}\n          </ul>\n        </div>\n      </div>\n\n      <div style={{ marginBottom: '20px' }}>\n        <div\n          style={{\n            display: 'flex',\n            justifyContent: 'space-between',\n            alignItems: 'center',\n            marginBottom: '12px',\n          }}\n        >\n          <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n            <h3 style={{ margin: 0 }}>Dashboard Items</h3>\n            <button\n              id=\"sortDashboardAsc\"\n              class=\"btn btn-primary\"\n              type=\"button\"\n              onClick={sortDashboardAsc}\n              style={{ padding: '4px 8px', fontSize: '12px' }}\n            >\n              Sort ↑\n            </button>\n            <button\n              id=\"sortDashboardDesc\"\n              class=\"btn btn-primary\"\n              type=\"button\"\n              onClick={sortDashboardDesc}\n              style={{ padding: '4px 8px', fontSize: '12px' }}\n            >\n              Sort ↓\n            </button>\n          </div>\n          <SearchInput />\n        </div>\n        <div\n          style={{\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n            overflow: 'hidden',\n          }}\n        >\n          <table style={{ width: '100%', borderCollapse: 'collapse' }}>\n            <thead>\n              <tr style={{ backgroundColor: '#f5f5f5' }}>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  ID\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Label\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Status\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Value\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Actions\n                </th>\n              </tr>\n            </thead>\n            <tbody>\n              {dashboardRows.map((row) => (\n                <DashboardTableRow key={row.id} row={row} />\n              ))}\n            </tbody>\n          </table>\n        </div>\n      </div>\n\n      <FormWidgets />\n    </div>\n  )\n}\n\nfunction App() {\n  let [rows, setRows] = useState<Row[]>([])\n  let [selected, setSelected] = useState<number | null>(null)\n  let [view, setView] = useState<'table' | 'dashboard'>('table')\n\n  let run = () => {\n    setRows(get1000Rows())\n    setSelected(null)\n  }\n\n  let runLots = () => {\n    setRows(get10000Rows())\n    setSelected(null)\n  }\n\n  let add = () => {\n    setRows((current) => [...current, ...get1000Rows()])\n  }\n\n  let update = () => {\n    setRows((current) => updatedEvery10thRow(current))\n  }\n\n  let clear = () => {\n    setRows([])\n    setSelected(null)\n  }\n\n  let swap = () => {\n    setRows((current) => swapRows(current))\n  }\n\n  let removeRow = (id: number) => {\n    setRows((current) => remove(current, id))\n  }\n\n  let sortAsc = () => {\n    setRows((current) => sortRows(current, true))\n  }\n\n  let sortDesc = () => {\n    setRows((current) => sortRows(current, false))\n  }\n\n  let switchToDashboard = () => {\n    setView('dashboard')\n  }\n\n  let switchToTable = () => {\n    setView('table')\n  }\n\n  if (view === 'dashboard') {\n    return <Dashboard onSwitchToTable={switchToTable} />\n  }\n\n  return (\n    <div class=\"container\">\n      <div class=\"jumbotron\">\n        <div class=\"row\">\n          <div class=\"col-md-6\">\n            <h1>Preact</h1>\n          </div>\n          <div class=\"col-md-6\">\n            <div class=\"row\">\n              <div class=\"col-sm-6 smallpad\">\n                <button id=\"run\" class=\"btn btn-primary btn-block\" type=\"button\" onClick={run}>\n                  Create 1,000 rows\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"runlots\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={runLots}\n                >\n                  Create 10,000 rows\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button id=\"add\" class=\"btn btn-primary btn-block\" type=\"button\" onClick={add}>\n                  Append 1,000 rows\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"update\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={update}\n                >\n                  Update every 10th row\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button id=\"clear\" class=\"btn btn-primary btn-block\" type=\"button\" onClick={clear}>\n                  Clear\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"swaprows\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={swap}\n                >\n                  Swap Rows\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"sortasc\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={sortAsc}\n                >\n                  Sort Ascending\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"sortdesc\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={sortDesc}\n                >\n                  Sort Descending\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"switchToDashboard\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={switchToDashboard}\n                >\n                  Switch to Dashboard\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <table class=\"table table-hover table-striped test-data\">\n        <tbody>\n          {rows.map((row) => {\n            let rowId = row.id\n            return (\n              <tr key={rowId} class={selected === rowId ? 'danger' : ''}>\n                <td class=\"col-md-1\">{rowId}</td>\n                <td class=\"col-md-4\">\n                  <a\n                    href=\"#\"\n                    onClick={(event) => {\n                      event.preventDefault()\n                      setSelected(rowId)\n                    }}\n                  >\n                    {row.label}\n                  </a>\n                </td>\n                <td class=\"col-md-1\">\n                  <a\n                    href=\"#\"\n                    onClick={(event) => {\n                      event.preventDefault()\n                      removeRow(rowId)\n                    }}\n                  >\n                    <span class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n                  </a>\n                </td>\n                <td class=\"col-md-6\" />\n              </tr>\n            )\n          })}\n        </tbody>\n      </table>\n      <span class=\"preloadicon glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n    </div>\n  )\n}\n\nlet el = document.getElementById('app')!\nrender(<App />, el)\n"
  },
  {
    "path": "packages/component/bench/frameworks/preact/package.json",
    "content": "{\n  \"name\": \"component-benchmark-preact\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build:prod\": \"esbuild index.tsx --bundle --minify --outfile=dist/index.js --format=esm\",\n    \"build\": \"esbuild index.tsx --bundle --outfile=dist/index.js --format=esm\",\n    \"dev\": \"esbuild index.tsx --bundle --minify --outfile=dist/index.js --format=esm --watch\"\n  },\n  \"dependencies\": {\n    \"esbuild\": \"^0.27.1\",\n    \"preact\": \"^10.28.0\"\n  }\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/preact/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\"\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/preact-signals/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Preact Signals Benchmark</title>\n    <link rel=\"stylesheet\" href=\"/styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"dist/index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/bench/frameworks/preact-signals/index.tsx",
    "content": "import { signal, batch, useComputed, type Signal } from '@preact/signals'\nimport { For } from '@preact/signals/utils'\nimport { render } from 'preact'\nimport { buildData as buildPlainData, sortRows as sortPlainRows, get1000Rows } from '../shared.ts'\nimport type { Benchmark, Row as PlainRow } from '../shared.ts'\n\nexport const name = 'preact-signals'\n\ntype Row = { id: number; label: Signal<string> }\n\nfunction buildSignalData(count: number): Row[] {\n  let plainData = buildPlainData(count)\n  return plainData.map((row) => ({\n    id: row.id,\n    label: signal(row.label),\n  }))\n}\n\n// Top-level signals for state\nlet data = signal<Row[]>([])\nlet selected = signal<number | null>(null)\nlet view = signal<'table' | 'dashboard'>('table')\n\nlet run = () => {\n  data.value = buildSignalData(1000)\n  selected.value = null\n}\n\nlet runLots = () => {\n  data.value = buildSignalData(10000)\n  selected.value = null\n}\n\nlet add = () => {\n  data.value = data.value.concat(buildSignalData(1000))\n}\n\nlet update = () => {\n  batch(() => {\n    for (let i = 0, d = data.value, len = d.length; i < len; i += 10) {\n      d[i].label.value = d[i].label.value + ' !!!'\n    }\n  })\n}\n\nlet clear = () => {\n  data.value = []\n  selected.value = null\n}\n\nlet swap = () => {\n  let d = data.value.slice()\n  if (d.length > 998) {\n    let tmp = d[1]\n    d[1] = d[998]\n    d[998] = tmp\n    data.value = d\n  }\n}\n\nlet removeRow = (id: number) => {\n  let idx = data.value.findIndex((d) => d.id === id)\n  data.value = [...data.value.slice(0, idx), ...data.value.slice(idx + 1)]\n}\n\nlet selectRow = (id: number) => {\n  selected.value = id\n}\n\nlet sortAsc = () => {\n  // Convert signal rows to plain rows, sort, then convert back\n  let plainRows: PlainRow[] = data.value.map((row) => ({\n    id: row.id,\n    label: row.label.value,\n  }))\n  let sorted = sortPlainRows(plainRows, true)\n  // Rebuild signal rows maintaining the same signal instances where possible\n  let sortedSignalRows: Row[] = sorted.map((plainRow) => {\n    let existing = data.value.find((r) => r.id === plainRow.id)\n    if (existing && existing.label.value === plainRow.label) {\n      return existing\n    }\n    return { id: plainRow.id, label: signal(plainRow.label) }\n  })\n  data.value = sortedSignalRows\n}\n\nlet sortDesc = () => {\n  // Convert signal rows to plain rows, sort, then convert back\n  let plainRows: PlainRow[] = data.value.map((row) => ({\n    id: row.id,\n    label: row.label.value,\n  }))\n  let sorted = sortPlainRows(plainRows, false)\n  // Rebuild signal rows maintaining the same signal instances where possible\n  let sortedSignalRows: Row[] = sorted.map((plainRow) => {\n    let existing = data.value.find((r) => r.id === plainRow.id)\n    if (existing && existing.label.value === plainRow.label) {\n      return existing\n    }\n    return { id: plainRow.id, label: signal(plainRow.label) }\n  })\n  data.value = sortedSignalRows\n}\n\nlet switchToDashboard = () => {\n  view.value = 'dashboard'\n}\n\nlet switchToTable = () => {\n  view.value = 'table'\n}\n\n// Stateful Metric Card Component\nfunction MetricCard({\n  id,\n  label,\n  value,\n  change,\n}: {\n  id: number\n  label: string\n  value: string\n  change: string\n}) {\n  let selected = signal(false)\n  let hovered = signal(false)\n\n  return (\n    <div\n      class={`metric-card ${selected.value ? 'selected' : ''}`}\n      onClick={() => (selected.value = !selected.value)}\n      onMouseEnter={() => (hovered.value = true)}\n      onMouseLeave={() => (hovered.value = false)}\n      onFocus={(e: any) => {\n        e.currentTarget.style.outline = '2px solid #222'\n        e.currentTarget.style.outlineOffset = '2px'\n      }}\n      onBlur={(e: any) => {\n        e.currentTarget.style.outline = ''\n      }}\n      tabIndex={0}\n      style={{\n        backgroundColor: hovered.value ? '#f5f5f5' : '#fff',\n        transform: hovered.value && !selected.value ? 'translateY(-2px)' : 'translateY(0)',\n        transition: 'all 0.2s',\n        padding: '20px',\n        border: '1px solid #ddd',\n        borderRadius: '8px',\n        cursor: 'pointer',\n        boxShadow: selected.value ? '0 4px 8px rgba(0,0,0,0.1)' : '0 2px 4px rgba(0,0,0,0.05)',\n      }}\n    >\n      <div style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}>{label}</div>\n      <div style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '4px' }}>{value}</div>\n      <div style={{ fontSize: '12px', color: change.startsWith('+') ? '#28a745' : '#dc3545' }}>\n        {change}\n      </div>\n    </div>\n  )\n}\n\n// Stateful Chart Bar Component\nfunction ChartBar({ value, index }: { value: number; index: number }) {\n  let hovered = signal(false)\n\n  return (\n    <div\n      class=\"chart-bar\"\n      style={{\n        height: `${value}%`,\n        backgroundColor: hovered.value ? '#286090' : '#337ab7',\n        width: '30px',\n        margin: '0 2px',\n        cursor: 'pointer',\n        transition: 'all 0.2s',\n        opacity: hovered.value ? 0.9 : 1,\n        transform: hovered.value ? 'scaleY(1.1)' : 'scaleY(1)',\n      }}\n      onClick={() => {}}\n      onMouseEnter={() => (hovered.value = true)}\n      onMouseLeave={() => (hovered.value = false)}\n      onFocus={(e: any) => {\n        e.currentTarget.style.outline = '2px solid #222'\n        e.currentTarget.style.outlineOffset = '2px'\n      }}\n      onBlur={(e: any) => {\n        e.currentTarget.style.outline = ''\n      }}\n      tabIndex={0}\n    />\n  )\n}\n\n// Stateful Activity Item Component\nfunction ActivityItem({\n  id,\n  title,\n  time,\n  icon,\n}: {\n  id: number\n  title: string\n  time: string\n  icon: string\n}) {\n  let read = signal(false)\n  let hovered = signal(false)\n\n  return (\n    <li\n      class={`activity-item ${read.value ? 'read' : ''}`}\n      onClick={() => (read.value = !read.value)}\n      onMouseEnter={() => (hovered.value = true)}\n      onMouseLeave={() => (hovered.value = false)}\n      style={{\n        padding: '12px',\n        borderBottom: '1px solid #eee',\n        cursor: 'pointer',\n        backgroundColor: hovered.value\n          ? '#f5f5f5'\n          : read.value\n            ? 'rgba(245, 245, 245, 0.6)'\n            : '#fff',\n        display: 'flex',\n        alignItems: 'center',\n        gap: '12px',\n      }}\n    >\n      <span\n        style={{\n          width: '32px',\n          height: '32px',\n          borderRadius: '50%',\n          backgroundColor: '#337ab7',\n          color: '#fff',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          fontWeight: 'bold',\n        }}\n      >\n        {icon}\n      </span>\n      <div style={{ flex: 1 }}>\n        <div style={{ fontWeight: read.value ? 'normal' : 'bold' }}>{title}</div>\n        <div style={{ fontSize: '12px', color: '#666' }}>{time}</div>\n      </div>\n    </li>\n  )\n}\n\n// Stateful Dropdown Menu Component\nfunction DropdownMenu({ rowId }: { rowId: number }) {\n  let open = signal(false)\n  let hovered = signal(false)\n\n  let actions = ['View Details', 'Edit', 'Duplicate', 'Archive', 'Delete']\n\n  return (\n    <div style={{ position: 'relative', display: 'inline-block' }}>\n      <button\n        class=\"btn btn-primary\"\n        onClick={(e: any) => {\n          e.stopPropagation()\n          open.value = !open.value\n        }}\n        onMouseEnter={() => (hovered.value = true)}\n        onMouseLeave={() => (hovered.value = false)}\n        onFocus={(e: any) => {\n          e.currentTarget.style.outline = '2px solid #222'\n          e.currentTarget.style.outlineOffset = '2px'\n        }}\n        onBlur={(e: any) => {\n          e.currentTarget.style.outline = ''\n        }}\n        style={{\n          padding: '4px 8px',\n          fontSize: '12px',\n          backgroundColor: hovered.value ? '#286090' : '#337ab7',\n        }}\n      >\n        ⋮\n      </button>\n      {open.value && (\n        <div\n          style={{\n            position: 'absolute',\n            top: '100%',\n            right: 0,\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '4px',\n            boxShadow: '0 4px 8px rgba(0,0,0,0.1)',\n            zIndex: 1000,\n            minWidth: '150px',\n            marginTop: '4px',\n          }}\n          onMouseLeave={() => (open.value = false)}\n        >\n          {actions.map((action, idx) => (\n            <div\n              key={idx}\n              onClick={(e: any) => {\n                e.stopPropagation()\n                open.value = false\n              }}\n              onMouseEnter={(e: any) => {\n                e.currentTarget.style.backgroundColor = '#f5f5f5'\n              }}\n              onMouseLeave={(e: any) => {\n                e.currentTarget.style.backgroundColor = '#fff'\n              }}\n              style={{\n                padding: '8px 12px',\n                cursor: 'pointer',\n                borderBottom: idx < actions.length - 1 ? '1px solid #eee' : 'none',\n              }}\n            >\n              {action}\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n\n// Stateful Dashboard Table Row Component\nfunction DashboardTableRow({ row }: { row: PlainRow }) {\n  let hovered = signal(false)\n  let selected = signal(false)\n\n  return (\n    <tr\n      class={selected.value ? 'danger' : ''}\n      onClick={() => (selected.value = !selected.value)}\n      onMouseEnter={() => (hovered.value = true)}\n      onMouseLeave={() => (hovered.value = false)}\n      style={{\n        backgroundColor: hovered.value ? '#f5f5f5' : '#fff',\n        cursor: 'pointer',\n      }}\n    >\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>{row.id}</td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>{row.label}</td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        <span style={{ color: '#28a745' }}>Active</span>\n      </td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        ${(row.id * 10.5).toFixed(2)}\n      </td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        <DropdownMenu rowId={row.id} />\n      </td>\n    </tr>\n  )\n}\n\n// Stateful Search Input Component\nfunction SearchInput() {\n  let value = signal('')\n  let focused = signal(false)\n\n  return (\n    <input\n      type=\"text\"\n      placeholder=\"Search...\"\n      value={value.value}\n      onInput={(e: any) => (value.value = e.target.value)}\n      onFocus={() => (focused.value = true)}\n      onBlur={() => (focused.value = false)}\n      style={{\n        padding: '8px 12px',\n        border: `1px solid ${focused.value ? '#337ab7' : '#ddd'}`,\n        borderRadius: '4px',\n        fontSize: '14px',\n        width: '300px',\n        outline: focused.value ? '2px solid #337ab7' : 'none',\n        outlineOffset: '2px',\n      }}\n    />\n  )\n}\n\n// Stateful Form Widgets Component\nfunction FormWidgets() {\n  let selectValue = signal('option1')\n  let checkboxValues = signal<Set<string>>(new Set())\n  let radioValue = signal('radio1')\n  let toggleValue = signal(false)\n  let progressValue = signal(45)\n\n  return (\n    <div style={{ padding: '20px', backgroundColor: '#f9f9f9', borderRadius: '8px' }}>\n      <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Settings</h3>\n      <div style={{ marginBottom: '16px' }}>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Select Option\n        </label>\n        <select\n          value={selectValue.value}\n          onChange={(e: any) => (selectValue.value = e.target.value)}\n          onFocus={(e: any) => {\n            e.currentTarget.style.borderColor = '#337ab7'\n            e.currentTarget.style.outline = '2px solid #337ab7'\n            e.currentTarget.style.outlineOffset = '2px'\n          }}\n          onBlur={(e: any) => {\n            e.currentTarget.style.borderColor = '#ddd'\n            e.currentTarget.style.outline = 'none'\n          }}\n          style={{\n            padding: '6px 12px',\n            border: '1px solid #ddd',\n            borderRadius: '4px',\n            fontSize: '14px',\n            width: '100%',\n          }}\n        >\n          <option value=\"option1\">Option 1</option>\n          <option value=\"option2\">Option 2</option>\n          <option value=\"option3\">Option 3</option>\n          <option value=\"option4\">Option 4</option>\n        </select>\n      </div>\n      {['Checkbox 1', 'Checkbox 2', 'Checkbox 3'].map((label, idx) => (\n        <div\n          key={idx}\n          style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}\n        >\n          <input\n            type=\"checkbox\"\n            id={`checkbox-${idx}`}\n            checked={checkboxValues.value.has(`checkbox-${idx}`)}\n            onChange={(e: any) => {\n              let next = new Set(checkboxValues.value)\n              if (e.target.checked) {\n                next.add(`checkbox-${idx}`)\n              } else {\n                next.delete(`checkbox-${idx}`)\n              }\n              checkboxValues.value = next\n            }}\n            onFocus={(e: any) => {\n              e.currentTarget.style.outline = '2px solid #337ab7'\n              e.currentTarget.style.outlineOffset = '2px'\n            }}\n            onBlur={(e: any) => {\n              e.currentTarget.style.outline = ''\n            }}\n          />\n          <label htmlFor={`checkbox-${idx}`} style={{ fontSize: '14px', cursor: 'pointer' }}>\n            {label}\n          </label>\n        </div>\n      ))}\n      <div style={{ marginBottom: '16px' }}>\n        {['Radio 1', 'Radio 2', 'Radio 3'].map((label, idx) => (\n          <label key={idx} style={{ display: 'block', marginBottom: '8px', cursor: 'pointer' }}>\n            <input\n              type=\"radio\"\n              name=\"radio-group\"\n              value={`radio${idx + 1}`}\n              checked={radioValue.value === `radio${idx + 1}`}\n              onChange={(e: any) => (radioValue.value = e.target.value)}\n              onFocus={(e: any) => {\n                e.currentTarget.style.outline = '2px solid #337ab7'\n                e.currentTarget.style.outlineOffset = '2px'\n              }}\n              onBlur={(e: any) => {\n                e.currentTarget.style.outline = ''\n              }}\n              style={{ marginRight: '8px' }}\n            />\n            {label}\n          </label>\n        ))}\n      </div>\n      <div style={{ marginBottom: '16px' }}>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Toggle Switch\n        </label>\n        <label\n          style={{\n            display: 'inline-block',\n            position: 'relative',\n            width: '50px',\n            height: '24px',\n            cursor: 'pointer',\n          }}\n        >\n          <input\n            type=\"checkbox\"\n            checked={toggleValue.value}\n            onChange={(e: any) => (toggleValue.value = e.target.checked)}\n            onFocus={(e: any) => {\n              e.currentTarget.style.outline = '2px solid #222'\n              e.currentTarget.style.outlineOffset = '2px'\n            }}\n            onBlur={(e: any) => {\n              e.currentTarget.style.outline = ''\n            }}\n            style={{ opacity: 0, width: 0, height: 0 }}\n          />\n          <span\n            style={{\n              position: 'absolute',\n              top: 0,\n              left: 0,\n              right: 0,\n              bottom: 0,\n              backgroundColor: toggleValue.value ? '#337ab7' : '#ccc',\n              borderRadius: '24px',\n              transition: 'background-color 0.3s',\n            }}\n          >\n            <span\n              style={{\n                position: 'absolute',\n                content: '\"\"',\n                height: '18px',\n                width: '18px',\n                left: '3px',\n                bottom: '3px',\n                backgroundColor: '#fff',\n                borderRadius: '50%',\n                transition: 'transform 0.3s',\n                transform: toggleValue.value ? 'translateX(26px)' : 'translateX(0)',\n              }}\n            />\n          </span>\n        </label>\n      </div>\n      <div>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Progress Bar\n        </label>\n        <div\n          style={{\n            width: '100%',\n            height: '24px',\n            backgroundColor: '#eee',\n            borderRadius: '4px',\n            overflow: 'hidden',\n            position: 'relative',\n          }}\n        >\n          <div\n            style={{\n              width: `${progressValue.value}%`,\n              height: '100%',\n              backgroundColor: '#337ab7',\n              transition: 'width 0.3s',\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'center',\n              color: '#fff',\n              fontSize: '12px',\n            }}\n          >\n            {progressValue.value}%\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction Row({ id, label }: { id: number; label: Signal<string> }) {\n  let rowClass = useComputed(() => (selected.value === id ? 'danger' : ''))\n  return (\n    <tr class={rowClass}>\n      <td class=\"col-md-1\">{id}</td>\n      <td class=\"col-md-4\">\n        <a\n          href=\"#\"\n          onClick={(event) => {\n            event.preventDefault()\n            selectRow(id)\n          }}\n        >\n          {label}\n        </a>\n      </td>\n      <td class=\"col-md-1\">\n        <a\n          href=\"#\"\n          onClick={(event) => {\n            event.preventDefault()\n            removeRow(id)\n          }}\n        >\n          <span class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n        </a>\n      </td>\n      <td class=\"col-md-6\" />\n    </tr>\n  )\n}\n\nfunction Dashboard({ onSwitchToTable }: { onSwitchToTable: () => void }) {\n  let dashboardRows = signal<PlainRow[]>(buildPlainData(300))\n\n  let sortDashboardAsc = () => {\n    dashboardRows.value = sortPlainRows(dashboardRows.value, true)\n  }\n\n  let sortDashboardDesc = () => {\n    dashboardRows.value = sortPlainRows(dashboardRows.value, false)\n  }\n  let chartData = [65, 45, 78, 52, 89, 34, 67, 91, 43, 56, 72, 38, 55, 82, 47, 63, 71, 39, 58, 84]\n  let activities = Array.from({ length: 50 }, (_, i) => ({\n    id: i + 1,\n    title: `Activity ${i + 1}: ${['Order placed', 'Payment received', 'Shipment created', 'Customer registered', 'Product updated'][i % 5]}`,\n    time: `${i + 1} ${i === 0 ? 'minute' : 'minutes'} ago`,\n    icon: ['O', 'P', 'S', 'C', 'U'][i % 5],\n  }))\n\n  return (\n    <div class=\"container\" style={{ maxWidth: '1400px' }}>\n      <div\n        style={{\n          display: 'flex',\n          marginBottom: '20px',\n          alignItems: 'center',\n          justifyContent: 'space-between',\n        }}\n      >\n        <h1 style={{ margin: 0 }}>Dashboard</h1>\n        <button\n          id=\"switchToTable\"\n          class=\"btn btn-primary\"\n          type=\"button\"\n          onClick={onSwitchToTable}\n          onFocus={(e: any) => {\n            e.currentTarget.style.outline = '2px solid #222'\n            e.currentTarget.style.outlineOffset = '2px'\n          }}\n          onBlur={(e: any) => {\n            e.currentTarget.style.outline = ''\n          }}\n        >\n          Switch to Table\n        </button>\n      </div>\n\n      <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>\n        <div style={{ flex: 1, display: 'flex', gap: '16px' }}>\n          <MetricCard id={1} label=\"Total Sales\" value=\"$125,430\" change=\"+12.5%\" />\n          <MetricCard id={2} label=\"Orders\" value=\"1,234\" change=\"+8.2%\" />\n          <MetricCard id={3} label=\"Customers\" value=\"5,678\" change=\"+15.3%\" />\n          <MetricCard id={4} label=\"Revenue\" value=\"$89,123\" change=\"+9.7%\" />\n        </div>\n      </div>\n\n      <div\n        style={{\n          display: 'grid',\n          gridTemplateColumns: '1fr 1fr',\n          gap: '20px',\n          marginBottom: '20px',\n        }}\n      >\n        <div\n          style={{\n            padding: '20px',\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n          }}\n        >\n          <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Sales Performance</h3>\n          <div\n            style={{\n              display: 'flex',\n              alignItems: 'flex-end',\n              justifyContent: 'space-around',\n              height: '200px',\n              padding: '20px 0',\n            }}\n          >\n            {chartData.map((value, index) => (\n              <ChartBar key={index} value={value} index={index} />\n            ))}\n          </div>\n        </div>\n\n        <div\n          style={{\n            padding: '20px',\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n          }}\n        >\n          <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Recent Activity</h3>\n          <ul\n            style={{\n              listStyle: 'none',\n              padding: 0,\n              margin: 0,\n              maxHeight: '200px',\n              overflowY: 'auto',\n            }}\n          >\n            {activities.map((activity) => (\n              <ActivityItem key={activity.id} {...activity} />\n            ))}\n          </ul>\n        </div>\n      </div>\n\n      <div style={{ marginBottom: '20px' }}>\n        <div\n          style={{\n            display: 'flex',\n            justifyContent: 'space-between',\n            alignItems: 'center',\n            marginBottom: '12px',\n          }}\n        >\n          <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n            <h3 style={{ margin: 0 }}>Dashboard Items</h3>\n            <button\n              id=\"sortDashboardAsc\"\n              class=\"btn btn-primary\"\n              type=\"button\"\n              onClick={sortDashboardAsc}\n              style={{ padding: '4px 8px', fontSize: '12px' }}\n            >\n              Sort ↑\n            </button>\n            <button\n              id=\"sortDashboardDesc\"\n              class=\"btn btn-primary\"\n              type=\"button\"\n              onClick={sortDashboardDesc}\n              style={{ padding: '4px 8px', fontSize: '12px' }}\n            >\n              Sort ↓\n            </button>\n          </div>\n          <SearchInput />\n        </div>\n        <div\n          style={{\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n            overflow: 'hidden',\n          }}\n        >\n          <table style={{ width: '100%', borderCollapse: 'collapse' }}>\n            <thead>\n              <tr style={{ backgroundColor: '#f5f5f5' }}>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  ID\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Label\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Status\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Value\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Actions\n                </th>\n              </tr>\n            </thead>\n            <tbody>\n              {dashboardRows.value.map((row) => (\n                <DashboardTableRow key={row.id} row={row} />\n              ))}\n            </tbody>\n          </table>\n        </div>\n      </div>\n\n      <FormWidgets />\n    </div>\n  )\n}\n\nfunction App() {\n  let currentView = view.value\n\n  if (currentView === 'dashboard') {\n    return <Dashboard onSwitchToTable={switchToTable} />\n  }\n\n  return (\n    <div class=\"container\">\n      <div class=\"jumbotron\">\n        <div class=\"row\">\n          <div class=\"col-md-6\">\n            <h1>Preact Signals</h1>\n          </div>\n          <div class=\"col-md-6\">\n            <div class=\"row\">\n              <div class=\"col-sm-6 smallpad\">\n                <button id=\"run\" class=\"btn btn-primary btn-block\" type=\"button\" onClick={run}>\n                  Create 1,000 rows\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"runlots\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={runLots}\n                >\n                  Create 10,000 rows\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button id=\"add\" class=\"btn btn-primary btn-block\" type=\"button\" onClick={add}>\n                  Append 1,000 rows\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"update\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={update}\n                >\n                  Update every 10th row\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button id=\"clear\" class=\"btn btn-primary btn-block\" type=\"button\" onClick={clear}>\n                  Clear\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"swaprows\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={swap}\n                >\n                  Swap Rows\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"sortasc\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={sortAsc}\n                >\n                  Sort Ascending\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"sortdesc\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={sortDesc}\n                >\n                  Sort Descending\n                </button>\n              </div>\n              <div class=\"col-sm-6 smallpad\">\n                <button\n                  id=\"switchToDashboard\"\n                  class=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={switchToDashboard}\n                >\n                  Switch to Dashboard\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <table class=\"table table-hover table-striped test-data\">\n        <tbody>\n          <For each={data}>{(row) => <Row key={row.id} id={row.id} label={row.label} />}</For>\n        </tbody>\n      </table>\n      <span class=\"preloadicon glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n    </div>\n  )\n}\n\nlet el = document.getElementById('app')!\nrender(<App />, el)\n"
  },
  {
    "path": "packages/component/bench/frameworks/preact-signals/package.json",
    "content": "{\n  \"name\": \"component-benchmark-preact-signals\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"esbuild index.tsx --bundle --minify --outfile=dist/index.js --format=esm\",\n    \"dev\": \"esbuild index.tsx --bundle --minify --outfile=dist/index.js --format=esm --watch\"\n  },\n  \"dependencies\": {\n    \"@preact/signals\": \"^2.0.4\",\n    \"esbuild\": \"^0.27.1\",\n    \"preact\": \"^10.28.0\"\n  }\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/preact-signals/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\"\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/react/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>React Benchmark</title>\n    <link rel=\"stylesheet\" href=\"/styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"dist/index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/bench/frameworks/react/index.tsx",
    "content": "import { useState } from 'react'\nimport {\n  get1000Rows,\n  get10000Rows,\n  remove,\n  sortRows,\n  swapRows,\n  updatedEvery10thRow,\n  buildData,\n} from '../shared.ts'\nimport type { Benchmark, Row } from '../shared.ts'\nimport { createRoot, type Root } from 'react-dom/client'\nimport { flushSync } from 'react-dom'\n\nexport const name = 'react'\n\n// Stateful Metric Card Component\nfunction MetricCard({\n  id,\n  label,\n  value,\n  change,\n}: {\n  id: number\n  label: string\n  value: string\n  change: string\n}) {\n  let [selected, setSelected] = useState(false)\n  let [hovered, setHovered] = useState(false)\n\n  return (\n    <div\n      className={`metric-card ${selected ? 'selected' : ''}`}\n      onClick={() => setSelected(!selected)}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={(e) => {\n        e.currentTarget.style.outline = '2px solid #222'\n        e.currentTarget.style.outlineOffset = '2px'\n      }}\n      onBlur={(e) => {\n        e.currentTarget.style.outline = ''\n      }}\n      tabIndex={0}\n      style={{\n        backgroundColor: hovered ? '#f5f5f5' : '#fff',\n        transform: hovered && !selected ? 'translateY(-2px)' : 'translateY(0)',\n        transition: 'all 0.2s',\n        padding: '20px',\n        border: '1px solid #ddd',\n        borderRadius: '8px',\n        cursor: 'pointer',\n        boxShadow: selected ? '0 4px 8px rgba(0,0,0,0.1)' : '0 2px 4px rgba(0,0,0,0.05)',\n      }}\n    >\n      <div style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}>{label}</div>\n      <div style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '4px' }}>{value}</div>\n      <div style={{ fontSize: '12px', color: change.startsWith('+') ? '#28a745' : '#dc3545' }}>\n        {change}\n      </div>\n    </div>\n  )\n}\n\n// Stateful Chart Bar Component\nfunction ChartBar({ value, index }: { value: number; index: number }) {\n  let [hovered, setHovered] = useState(false)\n\n  return (\n    <div\n      className=\"chart-bar\"\n      style={{\n        height: `${value}%`,\n        backgroundColor: hovered ? '#286090' : '#337ab7',\n        width: '30px',\n        margin: '0 2px',\n        cursor: 'pointer',\n        transition: 'all 0.2s',\n        opacity: hovered ? 0.9 : 1,\n        transform: hovered ? 'scaleY(1.1)' : 'scaleY(1)',\n      }}\n      onClick={() => {}}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={(e) => {\n        e.currentTarget.style.outline = '2px solid #222'\n        e.currentTarget.style.outlineOffset = '2px'\n      }}\n      onBlur={(e) => {\n        e.currentTarget.style.outline = ''\n      }}\n      tabIndex={0}\n    />\n  )\n}\n\n// Stateful Activity Item Component\nfunction ActivityItem({\n  id,\n  title,\n  time,\n  icon,\n}: {\n  id: number\n  title: string\n  time: string\n  icon: string\n}) {\n  let [read, setRead] = useState(false)\n  let [hovered, setHovered] = useState(false)\n\n  return (\n    <li\n      className={`activity-item ${read ? 'read' : ''}`}\n      onClick={() => setRead(!read)}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      style={{\n        padding: '12px',\n        borderBottom: '1px solid #eee',\n        cursor: 'pointer',\n        backgroundColor: hovered ? '#f5f5f5' : read ? 'rgba(245, 245, 245, 0.6)' : '#fff',\n        display: 'flex',\n        alignItems: 'center',\n        gap: '12px',\n      }}\n    >\n      <span\n        style={{\n          width: '32px',\n          height: '32px',\n          borderRadius: '50%',\n          backgroundColor: '#337ab7',\n          color: '#fff',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          fontWeight: 'bold',\n        }}\n      >\n        {icon}\n      </span>\n      <div style={{ flex: 1 }}>\n        <div style={{ fontWeight: read ? 'normal' : 'bold' }}>{title}</div>\n        <div style={{ fontSize: '12px', color: '#666' }}>{time}</div>\n      </div>\n    </li>\n  )\n}\n\n// Stateful Dropdown Menu Component\nfunction DropdownMenu({ rowId }: { rowId: number }) {\n  let [open, setOpen] = useState(false)\n  let [hovered, setHovered] = useState(false)\n\n  let actions = ['View Details', 'Edit', 'Duplicate', 'Archive', 'Delete']\n\n  return (\n    <div style={{ position: 'relative', display: 'inline-block' }}>\n      <button\n        className=\"btn btn-primary\"\n        onClick={(e) => {\n          e.stopPropagation()\n          setOpen(!open)\n        }}\n        onMouseEnter={() => setHovered(true)}\n        onMouseLeave={() => setHovered(false)}\n        onFocus={(e) => {\n          e.currentTarget.style.outline = '2px solid #222'\n          e.currentTarget.style.outlineOffset = '2px'\n        }}\n        onBlur={(e) => {\n          e.currentTarget.style.outline = ''\n        }}\n        style={{\n          padding: '4px 8px',\n          fontSize: '12px',\n          backgroundColor: hovered ? '#286090' : '#337ab7',\n        }}\n      >\n        ⋮\n      </button>\n      {open && (\n        <div\n          style={{\n            position: 'absolute',\n            top: '100%',\n            right: 0,\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '4px',\n            boxShadow: '0 4px 8px rgba(0,0,0,0.1)',\n            zIndex: 1000,\n            minWidth: '150px',\n            marginTop: '4px',\n          }}\n          onMouseLeave={() => setOpen(false)}\n        >\n          {actions.map((action, idx) => (\n            <div\n              key={idx}\n              onClick={(e) => {\n                e.stopPropagation()\n                setOpen(false)\n              }}\n              onMouseEnter={(e) => {\n                e.currentTarget.style.backgroundColor = '#f5f5f5'\n              }}\n              onMouseLeave={(e) => {\n                e.currentTarget.style.backgroundColor = '#fff'\n              }}\n              style={{\n                padding: '8px 12px',\n                cursor: 'pointer',\n                borderBottom: idx < actions.length - 1 ? '1px solid #eee' : 'none',\n              }}\n            >\n              {action}\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n\n// Stateful Dashboard Table Row Component\nfunction DashboardTableRow({ row }: { row: Row }) {\n  let [hovered, setHovered] = useState(false)\n  let [selected, setSelected] = useState(false)\n\n  return (\n    <tr\n      className={selected ? 'danger' : ''}\n      onClick={() => setSelected(!selected)}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      style={{\n        backgroundColor: hovered ? '#f5f5f5' : '#fff',\n        cursor: 'pointer',\n      }}\n    >\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>{row.id}</td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>{row.label}</td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        <span style={{ color: '#28a745' }}>Active</span>\n      </td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        ${(row.id * 10.5).toFixed(2)}\n      </td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        <DropdownMenu rowId={row.id} />\n      </td>\n    </tr>\n  )\n}\n\n// Stateful Search Input Component\nfunction SearchInput() {\n  let [value, setValue] = useState('')\n  let [focused, setFocused] = useState(false)\n\n  return (\n    <input\n      type=\"text\"\n      placeholder=\"Search...\"\n      value={value}\n      onChange={(e) => setValue(e.target.value)}\n      onFocus={() => setFocused(true)}\n      onBlur={() => setFocused(false)}\n      style={{\n        padding: '8px 12px',\n        border: `1px solid ${focused ? '#337ab7' : '#ddd'}`,\n        borderRadius: '4px',\n        fontSize: '14px',\n        width: '300px',\n        outline: focused ? '2px solid #337ab7' : 'none',\n        outlineOffset: '2px',\n      }}\n    />\n  )\n}\n\n// Stateful Form Widgets Component\nfunction FormWidgets() {\n  let [selectValue, setSelectValue] = useState('option1')\n  let [checkboxValues, setCheckboxValues] = useState<Set<string>>(new Set())\n  let [radioValue, setRadioValue] = useState('radio1')\n  let [toggleValue, setToggleValue] = useState(false)\n  let [progressValue, setProgressValue] = useState(45)\n\n  return (\n    <div style={{ padding: '20px', backgroundColor: '#f9f9f9', borderRadius: '8px' }}>\n      <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Settings</h3>\n      <div style={{ marginBottom: '16px' }}>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Select Option\n        </label>\n        <select\n          value={selectValue}\n          onChange={(e) => setSelectValue(e.target.value)}\n          onFocus={(e) => {\n            e.currentTarget.style.borderColor = '#337ab7'\n            e.currentTarget.style.outline = '2px solid #337ab7'\n            e.currentTarget.style.outlineOffset = '2px'\n          }}\n          onBlur={(e) => {\n            e.currentTarget.style.borderColor = '#ddd'\n            e.currentTarget.style.outline = 'none'\n          }}\n          style={{\n            padding: '6px 12px',\n            border: '1px solid #ddd',\n            borderRadius: '4px',\n            fontSize: '14px',\n            width: '100%',\n          }}\n        >\n          <option value=\"option1\">Option 1</option>\n          <option value=\"option2\">Option 2</option>\n          <option value=\"option3\">Option 3</option>\n          <option value=\"option4\">Option 4</option>\n        </select>\n      </div>\n      {['Checkbox 1', 'Checkbox 2', 'Checkbox 3'].map((label, idx) => (\n        <div\n          key={idx}\n          style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}\n        >\n          <input\n            type=\"checkbox\"\n            id={`checkbox-${idx}`}\n            checked={checkboxValues.has(`checkbox-${idx}`)}\n            onChange={(e) => {\n              let next = new Set(checkboxValues)\n              if (e.target.checked) {\n                next.add(`checkbox-${idx}`)\n              } else {\n                next.delete(`checkbox-${idx}`)\n              }\n              setCheckboxValues(next)\n            }}\n            onFocus={(e) => {\n              e.currentTarget.style.outline = '2px solid #337ab7'\n              e.currentTarget.style.outlineOffset = '2px'\n            }}\n            onBlur={(e) => {\n              e.currentTarget.style.outline = ''\n            }}\n          />\n          <label htmlFor={`checkbox-${idx}`} style={{ fontSize: '14px', cursor: 'pointer' }}>\n            {label}\n          </label>\n        </div>\n      ))}\n      <div style={{ marginBottom: '16px' }}>\n        {['Radio 1', 'Radio 2', 'Radio 3'].map((label, idx) => (\n          <label key={idx} style={{ display: 'block', marginBottom: '8px', cursor: 'pointer' }}>\n            <input\n              type=\"radio\"\n              name=\"radio-group\"\n              value={`radio${idx + 1}`}\n              checked={radioValue === `radio${idx + 1}`}\n              onChange={(e) => setRadioValue(e.target.value)}\n              onFocus={(e) => {\n                e.currentTarget.style.outline = '2px solid #337ab7'\n                e.currentTarget.style.outlineOffset = '2px'\n              }}\n              onBlur={(e) => {\n                e.currentTarget.style.outline = ''\n              }}\n              style={{ marginRight: '8px' }}\n            />\n            {label}\n          </label>\n        ))}\n      </div>\n      <div style={{ marginBottom: '16px' }}>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Toggle Switch\n        </label>\n        <label\n          style={{\n            display: 'inline-block',\n            position: 'relative',\n            width: '50px',\n            height: '24px',\n            cursor: 'pointer',\n          }}\n        >\n          <input\n            type=\"checkbox\"\n            checked={toggleValue}\n            onChange={(e) => setToggleValue(e.target.checked)}\n            onFocus={(e) => {\n              e.currentTarget.style.outline = '2px solid #222'\n              e.currentTarget.style.outlineOffset = '2px'\n            }}\n            onBlur={(e) => {\n              e.currentTarget.style.outline = ''\n            }}\n            style={{ opacity: 0, width: 0, height: 0 }}\n          />\n          <span\n            style={{\n              position: 'absolute',\n              top: 0,\n              left: 0,\n              right: 0,\n              bottom: 0,\n              backgroundColor: toggleValue ? '#337ab7' : '#ccc',\n              borderRadius: '24px',\n              transition: 'background-color 0.3s',\n            }}\n          >\n            <span\n              style={{\n                position: 'absolute',\n                content: '\"\"',\n                height: '18px',\n                width: '18px',\n                left: '3px',\n                bottom: '3px',\n                backgroundColor: '#fff',\n                borderRadius: '50%',\n                transition: 'transform 0.3s',\n                transform: toggleValue ? 'translateX(26px)' : 'translateX(0)',\n              }}\n            />\n          </span>\n        </label>\n      </div>\n      <div>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Progress Bar\n        </label>\n        <div\n          style={{\n            width: '100%',\n            height: '24px',\n            backgroundColor: '#eee',\n            borderRadius: '4px',\n            overflow: 'hidden',\n            position: 'relative',\n          }}\n        >\n          <div\n            style={{\n              width: `${progressValue}%`,\n              height: '100%',\n              backgroundColor: '#337ab7',\n              transition: 'width 0.3s',\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'center',\n              color: '#fff',\n              fontSize: '12px',\n            }}\n          >\n            {progressValue}%\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction Dashboard({ onSwitchToTable }: { onSwitchToTable: () => void }) {\n  let [dashboardRows, setDashboardRows] = useState(() => buildData(300))\n\n  let sortDashboardAsc = () => {\n    setDashboardRows((current) => sortRows(current, true))\n  }\n\n  let sortDashboardDesc = () => {\n    setDashboardRows((current) => sortRows(current, false))\n  }\n  let chartData = [65, 45, 78, 52, 89, 34, 67, 91, 43, 56, 72, 38, 55, 82, 47, 63, 71, 39, 58, 84]\n  let activities = Array.from({ length: 50 }, (_, i) => ({\n    id: i + 1,\n    title: `Activity ${i + 1}: ${['Order placed', 'Payment received', 'Shipment created', 'Customer registered', 'Product updated'][i % 5]}`,\n    time: `${i + 1} ${i === 0 ? 'minute' : 'minutes'} ago`,\n    icon: ['O', 'P', 'S', 'C', 'U'][i % 5],\n  }))\n\n  return (\n    <div className=\"container\" style={{ maxWidth: '1400px' }}>\n      <div\n        style={{\n          display: 'flex',\n          marginBottom: '20px',\n          alignItems: 'center',\n          justifyContent: 'space-between',\n        }}\n      >\n        <h1 style={{ margin: 0 }}>Dashboard</h1>\n        <button\n          id=\"switchToTable\"\n          className=\"btn btn-primary\"\n          type=\"button\"\n          onClick={onSwitchToTable}\n          onFocus={(e) => {\n            e.currentTarget.style.outline = '2px solid #222'\n            e.currentTarget.style.outlineOffset = '2px'\n          }}\n          onBlur={(e) => {\n            e.currentTarget.style.outline = ''\n          }}\n        >\n          Switch to Table\n        </button>\n      </div>\n\n      <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>\n        <div style={{ flex: 1, display: 'flex', gap: '16px' }}>\n          <MetricCard id={1} label=\"Total Sales\" value=\"$125,430\" change=\"+12.5%\" />\n          <MetricCard id={2} label=\"Orders\" value=\"1,234\" change=\"+8.2%\" />\n          <MetricCard id={3} label=\"Customers\" value=\"5,678\" change=\"+15.3%\" />\n          <MetricCard id={4} label=\"Revenue\" value=\"$89,123\" change=\"+9.7%\" />\n        </div>\n      </div>\n\n      <div\n        style={{\n          display: 'grid',\n          gridTemplateColumns: '1fr 1fr',\n          gap: '20px',\n          marginBottom: '20px',\n        }}\n      >\n        <div\n          style={{\n            padding: '20px',\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n          }}\n        >\n          <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Sales Performance</h3>\n          <div\n            style={{\n              display: 'flex',\n              alignItems: 'flex-end',\n              justifyContent: 'space-around',\n              height: '200px',\n              padding: '20px 0',\n            }}\n          >\n            {chartData.map((value, index) => (\n              <ChartBar key={index} value={value} index={index} />\n            ))}\n          </div>\n        </div>\n\n        <div\n          style={{\n            padding: '20px',\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n          }}\n        >\n          <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Recent Activity</h3>\n          <ul\n            style={{\n              listStyle: 'none',\n              padding: 0,\n              margin: 0,\n              maxHeight: '200px',\n              overflowY: 'auto',\n            }}\n          >\n            {activities.map((activity) => (\n              <ActivityItem key={activity.id} {...activity} />\n            ))}\n          </ul>\n        </div>\n      </div>\n\n      <div style={{ marginBottom: '20px' }}>\n        <div\n          style={{\n            display: 'flex',\n            justifyContent: 'space-between',\n            alignItems: 'center',\n            marginBottom: '12px',\n          }}\n        >\n          <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n            <h3 style={{ margin: 0 }}>Dashboard Items</h3>\n            <button\n              id=\"sortDashboardAsc\"\n              className=\"btn btn-primary\"\n              type=\"button\"\n              onClick={sortDashboardAsc}\n              style={{ padding: '4px 8px', fontSize: '12px' }}\n            >\n              Sort ↑\n            </button>\n            <button\n              id=\"sortDashboardDesc\"\n              className=\"btn btn-primary\"\n              type=\"button\"\n              onClick={sortDashboardDesc}\n              style={{ padding: '4px 8px', fontSize: '12px' }}\n            >\n              Sort ↓\n            </button>\n          </div>\n          <SearchInput />\n        </div>\n        <div\n          style={{\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n            overflow: 'hidden',\n          }}\n        >\n          <table style={{ width: '100%', borderCollapse: 'collapse' }}>\n            <thead>\n              <tr style={{ backgroundColor: '#f5f5f5' }}>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  ID\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Label\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Status\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Value\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Actions\n                </th>\n              </tr>\n            </thead>\n            <tbody>\n              {dashboardRows.map((row) => (\n                <DashboardTableRow key={row.id} row={row} />\n              ))}\n            </tbody>\n          </table>\n        </div>\n      </div>\n\n      <FormWidgets />\n    </div>\n  )\n}\n\nfunction App() {\n  let [rows, setRows] = useState<Row[]>([])\n  let [selected, setSelected] = useState<number | null>(null)\n  let [view, setView] = useState<'table' | 'dashboard'>('table')\n\n  let run = () => {\n    setRows(get1000Rows())\n    setSelected(null)\n  }\n\n  let runLots = () => {\n    setRows(get10000Rows())\n    setSelected(null)\n  }\n\n  let add = () => {\n    setRows((current) => [...current, ...get1000Rows()])\n  }\n\n  let update = () => {\n    setRows((current) => updatedEvery10thRow(current))\n  }\n\n  let clear = () => {\n    setRows([])\n    setSelected(null)\n  }\n\n  let swap = () => {\n    setRows((current) => swapRows(current))\n  }\n\n  let removeRow = (id: number) => {\n    setRows((current) => remove(current, id))\n  }\n\n  let sortAsc = () => {\n    setRows((current) => sortRows(current, true))\n  }\n\n  let sortDesc = () => {\n    setRows((current) => sortRows(current, false))\n  }\n\n  let switchToDashboard = () => {\n    setView('dashboard')\n  }\n\n  let switchToTable = () => {\n    setView('table')\n  }\n\n  if (view === 'dashboard') {\n    return <Dashboard onSwitchToTable={switchToTable} />\n  }\n\n  return (\n    <div className=\"container\">\n      <div className=\"jumbotron\">\n        <div className=\"row\">\n          <div className=\"col-md-6\">\n            <h1>React</h1>\n          </div>\n          <div className=\"col-md-6\">\n            <div className=\"row\">\n              <div className=\"col-sm-6 smallpad\">\n                <button id=\"run\" className=\"btn btn-primary btn-block\" type=\"button\" onClick={run}>\n                  Create 1,000 rows\n                </button>\n              </div>\n              <div className=\"col-sm-6 smallpad\">\n                <button\n                  id=\"runlots\"\n                  className=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={runLots}\n                >\n                  Create 10,000 rows\n                </button>\n              </div>\n              <div className=\"col-sm-6 smallpad\">\n                <button id=\"add\" className=\"btn btn-primary btn-block\" type=\"button\" onClick={add}>\n                  Append 1,000 rows\n                </button>\n              </div>\n              <div className=\"col-sm-6 smallpad\">\n                <button\n                  id=\"update\"\n                  className=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={update}\n                >\n                  Update every 10th row\n                </button>\n              </div>\n              <div className=\"col-sm-6 smallpad\">\n                <button\n                  id=\"clear\"\n                  className=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={clear}\n                >\n                  Clear\n                </button>\n              </div>\n              <div className=\"col-sm-6 smallpad\">\n                <button\n                  id=\"swaprows\"\n                  className=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={swap}\n                >\n                  Swap Rows\n                </button>\n              </div>\n              <div className=\"col-sm-6 smallpad\">\n                <button\n                  id=\"sortasc\"\n                  className=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={sortAsc}\n                >\n                  Sort Ascending\n                </button>\n              </div>\n              <div className=\"col-sm-6 smallpad\">\n                <button\n                  id=\"sortdesc\"\n                  className=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={sortDesc}\n                >\n                  Sort Descending\n                </button>\n              </div>\n              <div className=\"col-sm-6 smallpad\">\n                <button\n                  id=\"switchToDashboard\"\n                  className=\"btn btn-primary btn-block\"\n                  type=\"button\"\n                  onClick={switchToDashboard}\n                >\n                  Switch to Dashboard\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <table className=\"table table-hover table-striped test-data\">\n        <tbody>\n          {rows.map((row) => {\n            let rowId = row.id\n            return (\n              <tr key={rowId} className={selected === rowId ? 'danger' : ''}>\n                <td className=\"col-md-1\">{rowId}</td>\n                <td className=\"col-md-4\">\n                  <a\n                    href=\"#\"\n                    onClick={(event) => {\n                      event.preventDefault()\n                      setSelected(rowId)\n                    }}\n                  >\n                    {row.label}\n                  </a>\n                </td>\n                <td className=\"col-md-1\">\n                  <a\n                    href=\"#\"\n                    onClick={(event) => {\n                      event.preventDefault()\n                      removeRow(rowId)\n                    }}\n                  >\n                    <span className=\"glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n                  </a>\n                </td>\n                <td className=\"col-md-6\" />\n              </tr>\n            )\n          })}\n        </tbody>\n      </table>\n      <span className=\"preloadicon glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n    </div>\n  )\n}\n\nlet el = document.getElementById('app')!\nlet root = createRoot(el)\nroot.render(<App />)\n"
  },
  {
    "path": "packages/component/bench/frameworks/react/package.json",
    "content": "{\n  \"name\": \"component-benchmark-remix\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"esbuild index.tsx --bundle --minify --outfile=dist/index.js --format=esm\",\n    \"dev\": \"esbuild index.tsx --bundle --minify --outfile=dist/index.js --format=esm --watch\"\n  },\n  \"dependencies\": {\n    \"esbuild\": \"^0.27.1\",\n    \"react\": \"^19.2.1\",\n    \"react-dom\": \"^19.2.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^19.2.7\",\n    \"@types/react-dom\": \"^19.2.3\"\n  }\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/react/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/remix/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Remix Benchmark</title>\n    <link rel=\"stylesheet\" href=\"/styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"dist/index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/bench/frameworks/remix/index.tsx",
    "content": "import {\n  get1000Rows,\n  get10000Rows,\n  remove,\n  sortRows,\n  swapRows,\n  updatedEvery10thRow,\n  buildData,\n} from '../shared.ts'\nimport type { Benchmark, Row } from '../shared.ts'\nimport { createRoot, on } from '@remix-run/component'\nimport type { Handle } from '@remix-run/component'\n\nexport const name = 'remix'\n\nfunction Button() {\n  return ({ id, text, fn }: { id: string; text: string; fn: () => void }) => (\n    <div class=\"col-sm-6 smallpad\">\n      <button id={id} class=\"btn btn-primary btn-block\" type=\"button\" mix={[on('click', fn)]}>\n        {text}\n      </button>\n    </div>\n  )\n}\n\n// Stateful Metric Card Component\nfunction MetricCard(handle: Handle) {\n  let selected = false\n  let hovered = false\n\n  return ({\n    id,\n    label,\n    value,\n    change,\n  }: {\n    id: number\n    label: string\n    value: string\n    change: string\n  }) => (\n    <div\n      class={`metric-card ${selected ? 'selected' : ''}`}\n      mix={[\n        on('click', () => {\n          selected = !selected\n          handle.update()\n        }),\n        on('mouseenter', () => {\n          hovered = true\n          handle.update()\n        }),\n        on('mouseleave', () => {\n          hovered = false\n          handle.update()\n        }),\n        on('focus', (e: any) => {\n          e.currentTarget.style.outline = '2px solid #222'\n          e.currentTarget.style.outlineOffset = '2px'\n        }),\n        on('blur', (e: any) => {\n          e.currentTarget.style.outline = ''\n        }),\n      ]}\n      tabIndex={0}\n      style={{\n        backgroundColor: hovered ? '#f5f5f5' : '#fff',\n        transform: hovered && !selected ? 'translateY(-2px)' : 'translateY(0)',\n        transition: 'all 0.2s',\n        padding: '20px',\n        border: '1px solid #ddd',\n        borderRadius: '8px',\n        cursor: 'pointer',\n        boxShadow: selected ? '0 4px 8px rgba(0,0,0,0.1)' : '0 2px 4px rgba(0,0,0,0.05)',\n      }}\n    >\n      <div style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}>{label}</div>\n      <div style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '4px' }}>{value}</div>\n      <div style={{ fontSize: '12px', color: change.startsWith('+') ? '#28a745' : '#dc3545' }}>\n        {change}\n      </div>\n    </div>\n  )\n}\n\n// Stateful Chart Bar Component\nfunction ChartBar(handle: Handle) {\n  let hovered = false\n\n  return ({ value, index }: { value: number; index: number }) => (\n    <div\n      class=\"chart-bar\"\n      mix={[\n        on('click', () => {}),\n        on('mouseenter', () => {\n          hovered = true\n          handle.update()\n        }),\n        on('mouseleave', () => {\n          hovered = false\n          handle.update()\n        }),\n        on('focus', (e: any) => {\n          e.currentTarget.style.outline = '2px solid #222'\n          e.currentTarget.style.outlineOffset = '2px'\n        }),\n        on('blur', (e: any) => {\n          e.currentTarget.style.outline = ''\n        }),\n      ]}\n      style={{\n        height: `${value}%`,\n        backgroundColor: hovered ? '#286090' : '#337ab7',\n        width: '30px',\n        margin: '0 2px',\n        cursor: 'pointer',\n        transition: 'all 0.2s',\n        opacity: hovered ? 0.9 : 1,\n        transform: hovered ? 'scaleY(1.1)' : 'scaleY(1)',\n      }}\n      tabIndex={0}\n    />\n  )\n}\n\n// Stateful Activity Item Component\nfunction ActivityItem(handle: Handle) {\n  let read = false\n  let hovered = false\n\n  return ({ id, title, time, icon }: { id: number; title: string; time: string; icon: string }) => (\n    <li\n      class={`activity-item ${read ? 'read' : ''}`}\n      mix={[\n        on('click', () => {\n          read = !read\n          handle.update()\n        }),\n        on('mouseenter', () => {\n          hovered = true\n          handle.update()\n        }),\n        on('mouseleave', () => {\n          hovered = false\n          handle.update()\n        }),\n      ]}\n      style={{\n        padding: '12px',\n        borderBottom: '1px solid #eee',\n        cursor: 'pointer',\n        backgroundColor: hovered ? '#f5f5f5' : read ? 'rgba(245, 245, 245, 0.6)' : '#fff',\n        display: 'flex',\n        alignItems: 'center',\n        gap: '12px',\n      }}\n    >\n      <span\n        style={{\n          width: '32px',\n          height: '32px',\n          borderRadius: '50%',\n          backgroundColor: '#337ab7',\n          color: '#fff',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          fontWeight: 'bold',\n        }}\n      >\n        {icon}\n      </span>\n      <div style={{ flex: 1 }}>\n        <div style={{ fontWeight: read ? 'normal' : 'bold' }}>{title}</div>\n        <div style={{ fontSize: '12px', color: '#666' }}>{time}</div>\n      </div>\n    </li>\n  )\n}\n\n// Stateful Dropdown Menu Component\nfunction DropdownMenu(handle: Handle) {\n  let open = false\n  let hovered = false\n\n  let actions = ['View Details', 'Edit', 'Duplicate', 'Archive', 'Delete']\n\n  return ({ rowId }: { rowId: number }) => (\n    <div style={{ position: 'relative', display: 'inline-block' }}>\n      <button\n        class=\"btn btn-primary\"\n        mix={[\n          on('click', (e: any) => {\n            e.stopPropagation()\n            open = !open\n            handle.update()\n          }),\n          on('mouseenter', () => {\n            hovered = true\n            handle.update()\n          }),\n          on('mouseleave', () => {\n            hovered = false\n            handle.update()\n          }),\n          on('focus', (e: any) => {\n            e.currentTarget.style.outline = '2px solid #222'\n            e.currentTarget.style.outlineOffset = '2px'\n          }),\n          on('blur', (e: any) => {\n            e.currentTarget.style.outline = ''\n          }),\n        ]}\n        style={{\n          padding: '4px 8px',\n          fontSize: '12px',\n          backgroundColor: hovered ? '#286090' : '#337ab7',\n        }}\n      >\n        ⋮\n      </button>\n      {open && (\n        <div\n          style={{\n            position: 'absolute',\n            top: '100%',\n            right: 0,\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '4px',\n            boxShadow: '0 4px 8px rgba(0,0,0,0.1)',\n            zIndex: 1000,\n            minWidth: '150px',\n            marginTop: '4px',\n          }}\n          mix={[\n            on('mouseleave', () => {\n              open = false\n              handle.update()\n            }),\n          ]}\n        >\n          {actions.map((action, idx) => (\n            <div\n              key={idx}\n              mix={[\n                on('click', (e: any) => {\n                  e.stopPropagation()\n                  open = false\n                  handle.update()\n                }),\n                on('mouseenter', (e: any) => {\n                  e.currentTarget.style.backgroundColor = '#f5f5f5'\n                }),\n                on('mouseleave', (e: any) => {\n                  e.currentTarget.style.backgroundColor = '#fff'\n                }),\n              ]}\n              style={{\n                padding: '8px 12px',\n                cursor: 'pointer',\n                borderBottom: idx < actions.length - 1 ? '1px solid #eee' : 'none',\n              }}\n            >\n              {action}\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n\n// Stateful Dashboard Table Row Component\nfunction DashboardTableRow(handle: Handle) {\n  let hovered = false\n  let selected = false\n\n  return ({ row }: { row: Row }) => (\n    <tr\n      class={selected ? 'danger' : ''}\n      mix={[\n        on('click', () => {\n          selected = !selected\n          handle.update()\n        }),\n        on('mouseenter', () => {\n          hovered = true\n          handle.update()\n        }),\n        on('mouseleave', () => {\n          hovered = false\n          handle.update()\n        }),\n      ]}\n      style={{\n        backgroundColor: hovered ? '#f5f5f5' : '#fff',\n        cursor: 'pointer',\n      }}\n    >\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>{row.id}</td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>{row.label}</td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        <span style={{ color: '#28a745' }}>Active</span>\n      </td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        ${(row.id * 10.5).toFixed(2)}\n      </td>\n      <td style={{ padding: '12px', borderTop: '1px solid #ddd' }}>\n        <DropdownMenu rowId={row.id} />\n      </td>\n    </tr>\n  )\n}\n\n// Stateful Search Input Component\nfunction SearchInput(handle: Handle) {\n  let value = ''\n  let focused = false\n\n  return () => (\n    <input\n      type=\"text\"\n      placeholder=\"Search...\"\n      value={value}\n      mix={[\n        on('input', (e: any) => {\n          value = e.target.value\n          handle.update()\n        }),\n        on('focus', () => {\n          focused = true\n          handle.update()\n        }),\n        on('blur', () => {\n          focused = false\n          handle.update()\n        }),\n      ]}\n      style={{\n        padding: '8px 12px',\n        border: `1px solid ${focused ? '#337ab7' : '#ddd'}`,\n        borderRadius: '4px',\n        fontSize: '14px',\n        width: '300px',\n        outline: focused ? '2px solid #337ab7' : 'none',\n        outlineOffset: '2px',\n      }}\n    />\n  )\n}\n\n// Stateful Form Widgets Component\nfunction FormWidgets(handle: Handle) {\n  let selectValue = 'option1'\n  let checkboxValues = new Set<string>()\n  let radioValue = 'radio1'\n  let toggleValue = false\n  let progressValue = 45\n\n  return () => (\n    <div style={{ padding: '20px', backgroundColor: '#f9f9f9', borderRadius: '8px' }}>\n      <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Settings</h3>\n      <div style={{ marginBottom: '16px' }}>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Select Option\n        </label>\n        <select\n          value={selectValue}\n          mix={[\n            on('change', (e: any) => {\n              selectValue = e.target.value\n              handle.update()\n            }),\n            on('focus', (e: any) => {\n              e.currentTarget.style.borderColor = '#337ab7'\n              e.currentTarget.style.outline = '2px solid #337ab7'\n              e.currentTarget.style.outlineOffset = '2px'\n            }),\n            on('blur', (e: any) => {\n              e.currentTarget.style.borderColor = '#ddd'\n              e.currentTarget.style.outline = 'none'\n            }),\n          ]}\n          style={{\n            padding: '6px 12px',\n            border: '1px solid #ddd',\n            borderRadius: '4px',\n            fontSize: '14px',\n            width: '100%',\n          }}\n        >\n          <option value=\"option1\">Option 1</option>\n          <option value=\"option2\">Option 2</option>\n          <option value=\"option3\">Option 3</option>\n          <option value=\"option4\">Option 4</option>\n        </select>\n      </div>\n      {['Checkbox 1', 'Checkbox 2', 'Checkbox 3'].map((label, idx) => (\n        <div\n          key={idx}\n          style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}\n        >\n          <input\n            type=\"checkbox\"\n            id={`checkbox-${idx}`}\n            checked={checkboxValues.has(`checkbox-${idx}`)}\n            mix={[\n              on('change', (e: any) => {\n                if (e.target.checked) {\n                  checkboxValues.add(`checkbox-${idx}`)\n                } else {\n                  checkboxValues.delete(`checkbox-${idx}`)\n                }\n                handle.update()\n              }),\n              on('focus', (e: any) => {\n                e.currentTarget.style.outline = '2px solid #337ab7'\n                e.currentTarget.style.outlineOffset = '2px'\n              }),\n              on('blur', (e: any) => {\n                e.currentTarget.style.outline = ''\n              }),\n            ]}\n          />\n          <label for={`checkbox-${idx}`} style={{ fontSize: '14px', cursor: 'pointer' }}>\n            {label}\n          </label>\n        </div>\n      ))}\n      <div style={{ marginBottom: '16px' }}>\n        {['Radio 1', 'Radio 2', 'Radio 3'].map((label, idx) => (\n          <label key={idx} style={{ display: 'block', marginBottom: '8px', cursor: 'pointer' }}>\n            <input\n              type=\"radio\"\n              name=\"radio-group\"\n              value={`radio${idx + 1}`}\n              checked={radioValue === `radio${idx + 1}`}\n              mix={[\n                on('change', (e: any) => {\n                  radioValue = e.target.value\n                  handle.update()\n                }),\n                on('focus', (e: any) => {\n                  e.currentTarget.style.outline = '2px solid #337ab7'\n                  e.currentTarget.style.outlineOffset = '2px'\n                }),\n                on('blur', (e: any) => {\n                  e.currentTarget.style.outline = ''\n                }),\n              ]}\n              style={{ marginRight: '8px' }}\n            />\n            {label}\n          </label>\n        ))}\n      </div>\n      <div style={{ marginBottom: '16px' }}>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Toggle Switch\n        </label>\n        <label\n          style={{\n            display: 'inline-block',\n            position: 'relative',\n            width: '50px',\n            height: '24px',\n            cursor: 'pointer',\n          }}\n        >\n          <input\n            type=\"checkbox\"\n            checked={toggleValue}\n            mix={[\n              on('change', (e: any) => {\n                toggleValue = e.target.checked\n                handle.update()\n              }),\n              on('focus', (e: any) => {\n                e.currentTarget.style.outline = '2px solid #222'\n                e.currentTarget.style.outlineOffset = '2px'\n              }),\n              on('blur', (e: any) => {\n                e.currentTarget.style.outline = ''\n              }),\n            ]}\n            style={{ opacity: 0, width: 0, height: 0 }}\n          />\n          <span\n            style={{\n              position: 'absolute',\n              top: 0,\n              left: 0,\n              right: 0,\n              bottom: 0,\n              backgroundColor: toggleValue ? '#337ab7' : '#ccc',\n              borderRadius: '24px',\n              transition: 'background-color 0.3s',\n            }}\n          >\n            <span\n              style={{\n                position: 'absolute',\n                content: '\"\"',\n                height: '18px',\n                width: '18px',\n                left: '3px',\n                bottom: '3px',\n                backgroundColor: '#fff',\n                borderRadius: '50%',\n                transition: 'transform 0.3s',\n                transform: toggleValue ? 'translateX(26px)' : 'translateX(0)',\n              }}\n            />\n          </span>\n        </label>\n      </div>\n      <div>\n        <label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>\n          Progress Bar\n        </label>\n        <div\n          style={{\n            width: '100%',\n            height: '24px',\n            backgroundColor: '#eee',\n            borderRadius: '4px',\n            overflow: 'hidden',\n            position: 'relative',\n          }}\n        >\n          <div\n            style={{\n              width: `${progressValue}%`,\n              height: '100%',\n              backgroundColor: '#337ab7',\n              transition: 'width 0.3s',\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'center',\n              color: '#fff',\n              fontSize: '12px',\n            }}\n          >\n            {progressValue}%\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction Dashboard(handle: Handle) {\n  let dashboardRows = buildData(300)\n\n  let sortDashboardAsc = () => {\n    dashboardRows = sortRows(dashboardRows, true)\n    handle.update()\n  }\n\n  let sortDashboardDesc = () => {\n    dashboardRows = sortRows(dashboardRows, false)\n    handle.update()\n  }\n  let chartData = [65, 45, 78, 52, 89, 34, 67, 91, 43, 56, 72, 38, 55, 82, 47, 63, 71, 39, 58, 84]\n  let activities = Array.from({ length: 50 }, (_, i) => ({\n    id: i + 1,\n    title: `Activity ${i + 1}: ${['Order placed', 'Payment received', 'Shipment created', 'Customer registered', 'Product updated'][i % 5]}`,\n    time: `${i + 1} ${i === 0 ? 'minute' : 'minutes'} ago`,\n    icon: ['O', 'P', 'S', 'C', 'U'][i % 5],\n  }))\n\n  return ({ onSwitchToTable }: { onSwitchToTable: () => void }) => (\n    <div class=\"container\" style={{ maxWidth: '1400px' }}>\n      <div\n        style={{\n          display: 'flex',\n          marginBottom: '20px',\n          alignItems: 'center',\n          justifyContent: 'space-between',\n        }}\n      >\n        <h1 style={{ margin: 0 }}>Dashboard</h1>\n        <button\n          id=\"switchToTable\"\n          class=\"btn btn-primary\"\n          type=\"button\"\n          mix={[on('click', onSwitchToTable)]}\n        >\n          Switch to Table\n        </button>\n      </div>\n\n      <div style={{ display: 'flex', gap: '20px', marginBottom: '20px' }}>\n        <div style={{ flex: 1, display: 'flex', gap: '16px' }}>\n          <MetricCard id={1} label=\"Total Sales\" value=\"$125,430\" change=\"+12.5%\" />\n          <MetricCard id={2} label=\"Orders\" value=\"1,234\" change=\"+8.2%\" />\n          <MetricCard id={3} label=\"Customers\" value=\"5,678\" change=\"+15.3%\" />\n          <MetricCard id={4} label=\"Revenue\" value=\"$89,123\" change=\"+9.7%\" />\n        </div>\n      </div>\n\n      <div\n        style={{\n          display: 'grid',\n          gridTemplateColumns: '1fr 1fr',\n          gap: '20px',\n          marginBottom: '20px',\n        }}\n      >\n        <div\n          style={{\n            padding: '20px',\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n          }}\n        >\n          <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Sales Performance</h3>\n          <div\n            style={{\n              display: 'flex',\n              alignItems: 'flex-end',\n              justifyContent: 'space-around',\n              height: '200px',\n              padding: '20px 0',\n            }}\n          >\n            {chartData.map((value, index) => (\n              <ChartBar key={index} value={value} index={index} />\n            ))}\n          </div>\n        </div>\n\n        <div\n          style={{\n            padding: '20px',\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n          }}\n        >\n          <h3 style={{ marginTop: 0, marginBottom: '16px' }}>Recent Activity</h3>\n          <ul\n            style={{\n              listStyle: 'none',\n              padding: 0,\n              margin: 0,\n              maxHeight: '200px',\n              overflowY: 'auto',\n            }}\n          >\n            {activities.map((activity) => (\n              <ActivityItem key={activity.id} {...activity} />\n            ))}\n          </ul>\n        </div>\n      </div>\n\n      <div style={{ marginBottom: '20px' }}>\n        <div\n          style={{\n            display: 'flex',\n            justifyContent: 'space-between',\n            alignItems: 'center',\n            marginBottom: '12px',\n          }}\n        >\n          <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n            <h3 style={{ margin: 0 }}>Dashboard Items</h3>\n            <button\n              id=\"sortDashboardAsc\"\n              class=\"btn btn-primary\"\n              type=\"button\"\n              mix={[on('click', sortDashboardAsc)]}\n              style={{ padding: '4px 8px', fontSize: '12px' }}\n            >\n              Sort ↑\n            </button>\n            <button\n              id=\"sortDashboardDesc\"\n              class=\"btn btn-primary\"\n              type=\"button\"\n              mix={[on('click', sortDashboardDesc)]}\n              style={{ padding: '4px 8px', fontSize: '12px' }}\n            >\n              Sort ↓\n            </button>\n          </div>\n          <SearchInput />\n        </div>\n        <div\n          style={{\n            backgroundColor: '#fff',\n            border: '1px solid #ddd',\n            borderRadius: '8px',\n            overflow: 'hidden',\n          }}\n        >\n          <table style={{ width: '100%', borderCollapse: 'collapse' }}>\n            <thead>\n              <tr style={{ backgroundColor: '#f5f5f5' }}>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  ID\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Label\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Status\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Value\n                </th>\n                <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>\n                  Actions\n                </th>\n              </tr>\n            </thead>\n            <tbody>\n              {dashboardRows.map((row) => (\n                <DashboardTableRow key={row.id} row={row} />\n              ))}\n            </tbody>\n          </table>\n        </div>\n      </div>\n\n      <FormWidgets />\n    </div>\n  )\n}\n\nfunction App(handle: Handle) {\n  let rows: Row[] = []\n  let selected: number | null = null\n  let view: 'table' | 'dashboard' = 'table'\n\n  let setRows = (newRows: Row[]) => {\n    rows = newRows\n    handle.update()\n  }\n\n  let setSelected = (newSelected: number | null) => {\n    selected = newSelected\n    handle.update()\n  }\n\n  let switchToDashboard = () => {\n    view = 'dashboard'\n    handle.update()\n  }\n\n  let switchToTable = () => {\n    view = 'table'\n    handle.update()\n  }\n\n  return () => {\n    if (view === 'dashboard') {\n      return <Dashboard onSwitchToTable={switchToTable} />\n    }\n\n    return (\n      <div class=\"container\">\n        <div class=\"jumbotron\">\n          <div class=\"row\">\n            <div class=\"col-md-6\">\n              <h1>Remix</h1>\n            </div>\n            <div class=\"col-md-6\">\n              <div class=\"row\">\n                <Button\n                  id=\"run\"\n                  text=\"Create 1,000 rows\"\n                  fn={() => {\n                    rows = get1000Rows()\n                    selected = null\n                    handle.update()\n                  }}\n                />\n                <Button\n                  id=\"runlots\"\n                  text=\"Create 10,000 rows\"\n                  fn={() => {\n                    rows = get10000Rows()\n                    selected = null\n                    handle.update()\n                  }}\n                />\n                <Button\n                  id=\"add\"\n                  text=\"Append 1,000 rows\"\n                  fn={() => {\n                    setRows([...rows, ...get1000Rows()])\n                  }}\n                />\n                <Button\n                  id=\"update\"\n                  text=\"Update every 10th row\"\n                  fn={() => {\n                    setRows(updatedEvery10thRow(rows))\n                  }}\n                />\n                <Button\n                  id=\"clear\"\n                  text=\"Clear\"\n                  fn={() => {\n                    rows = []\n                    selected = null\n                    handle.update()\n                  }}\n                />\n                <Button\n                  id=\"swaprows\"\n                  text=\"Swap Rows\"\n                  fn={() => {\n                    setRows(swapRows(rows))\n                  }}\n                />\n                <Button\n                  id=\"sortasc\"\n                  text=\"Sort Ascending\"\n                  fn={() => {\n                    setRows(sortRows(rows, true))\n                  }}\n                />\n                <Button\n                  id=\"sortdesc\"\n                  text=\"Sort Descending\"\n                  fn={() => {\n                    setRows(sortRows(rows, false))\n                  }}\n                />\n                <Button id=\"switchToDashboard\" text=\"Switch to Dashboard\" fn={switchToDashboard} />\n              </div>\n            </div>\n          </div>\n        </div>\n        <table class=\"table table-hover table-striped test-data\">\n          <tbody>\n            {rows.map((row) => {\n              let rowId = row.id\n              return (\n                <tr key={rowId} class={selected === rowId ? 'danger' : ''}>\n                  <td class=\"col-md-1\">{rowId}</td>\n                  <td class=\"col-md-4\">\n                    <a\n                      mix={[\n                        on('click', () => {\n                          setSelected(rowId)\n                        }),\n                      ]}\n                    >\n                      {row.label}\n                    </a>\n                  </td>\n                  <td class=\"col-md-1\">\n                    <a\n                      mix={[\n                        on('click', () => {\n                          setRows(remove(rows, rowId))\n                        }),\n                      ]}\n                    >\n                      <span class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n                    </a>\n                  </td>\n                  <td class=\"col-md-6\" />\n                </tr>\n              )\n            })}\n          </tbody>\n        </table>\n        <span class=\"preloadicon glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n      </div>\n    )\n  }\n}\n\nlet el = document.getElementById('app')!\nlet root = createRoot(el)\nroot.render(<App />)\n"
  },
  {
    "path": "packages/component/bench/frameworks/remix/package.json",
    "content": "{\n  \"name\": \"component-benchmark-remix\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build:prod\": \"esbuild index.tsx --bundle --minify --outfile=dist/index.js --format=esm\",\n    \"build\": \"esbuild index.tsx --bundle --outfile=dist/index.js --format=esm\",\n    \"dev\": \"esbuild index.tsx --bundle --minify --outfile=dist/index.js --format=esm --watch\"\n  },\n  \"dependencies\": {\n    \"@remix-run/component\": \"workspace:^\",\n    \"esbuild\": \"^0.27.1\"\n  }\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/remix/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@remix-run/component\"\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/shared.ts",
    "content": "export interface Benchmark {\n  run(): void\n  teardown(): void\n}\n\nexport interface Framework {\n  name: string\n  insert: () => Benchmark\n  // swap: Benchmark\n  // update: Benchmark\n  // replace: Benchmark\n}\n\nexport type Row = { id: number; label: string }\n\nlet idCounter = 1\n\nconst A = [\n    'pretty',\n    'large',\n    'big',\n    'small',\n    'tall',\n    'short',\n    'long',\n    'handsome',\n    'plain',\n    'quaint',\n    'clean',\n    'elegant',\n    'easy',\n    'angry',\n    'crazy',\n    'helpful',\n    'mushy',\n    'odd',\n    'unsightly',\n    'adorable',\n    'important',\n    'inexpensive',\n    'cheap',\n    'expensive',\n    'fancy',\n  ],\n  C = [\n    'red',\n    'yellow',\n    'blue',\n    'green',\n    'pink',\n    'brown',\n    'purple',\n    'brown',\n    'white',\n    'black',\n    'orange',\n  ],\n  N = [\n    'table',\n    'chair',\n    'house',\n    'bbq',\n    'desk',\n    'car',\n    'pony',\n    'cookie',\n    'sandwich',\n    'burger',\n    'pizza',\n    'mouse',\n    'keyboard',\n  ]\n\nexport function buildData(count: number) {\n  let data = new Array(count)\n\n  for (let i = 0; i < count; i++) {\n    // Use deterministic selection based on index to ensure same data every time\n    data[i] = {\n      id: idCounter++,\n      label: `${A[i % A.length]} ${C[i % C.length]} ${N[i % N.length]}`,\n    }\n  }\n\n  return data\n}\n\nexport function get1000Rows(): Row[] {\n  return buildData(1000)\n}\n\nexport function get10000Rows(): Row[] {\n  return buildData(10000)\n}\n\nexport function updatedEvery10thRow(data: Row[]): Row[] {\n  let newData = data.slice(0)\n  for (let i = 0, d = data, len = d.length; i < len; i += 10) {\n    newData[i] = { id: data[i].id, label: data[i].label + ' !!!' }\n  }\n  return newData\n}\n\nexport function swapRows(data: Row[]): Row[] {\n  let d = data.slice()\n  if (d.length > 998) {\n    let tmp = d[1]\n    d[1] = d[998]\n    d[998] = tmp\n  }\n  return d\n}\n\nexport function remove(data: Row[], id: number): Row[] {\n  return data.filter((d) => d.id !== id)\n}\n\nexport function sortRows(data: Row[], ascending: boolean = true): Row[] {\n  let sorted = data.slice().sort((a, b) => {\n    if (ascending) {\n      return a.label.localeCompare(b.label)\n    } else {\n      return b.label.localeCompare(a.label)\n    }\n  })\n  return sorted\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/solid/build.js",
    "content": "import { build, context } from 'esbuild'\nimport { solidPlugin } from 'esbuild-plugin-solid'\n\nlet isProduction = process.env.NODE_ENV === 'production'\nlet isWatch = process.argv.includes('--watch')\n\nlet buildOptions = {\n  entryPoints: ['index.tsx'],\n  bundle: true,\n  outfile: 'dist/index.js',\n  format: 'esm',\n  plugins: [solidPlugin()],\n  minify: isProduction,\n}\n\nif (isWatch) {\n  let ctx = await context(buildOptions)\n  await ctx.watch()\n  console.log('Watching...')\n} else {\n  await build(buildOptions)\n  console.log('Build complete')\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/solid/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>SolidJS Benchmark</title>\n    <link rel=\"stylesheet\" href=\"/styles.css\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"dist/index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/bench/frameworks/solid/index.tsx",
    "content": "import { createSignal, createSelector, For } from 'solid-js'\nimport { render } from 'solid-js/web'\nimport {\n  get1000Rows,\n  get10000Rows,\n  remove,\n  sortRows,\n  swapRows,\n  updatedEvery10thRow,\n  buildData,\n} from '../shared.ts'\nimport type { Benchmark, Row } from '../shared.ts'\n\nexport const name = 'solid'\n\n// Stateful Metric Card Component\nfunction MetricCard(props: { id: number; label: string; value: string; change: string }) {\n  let [selected, setSelected] = createSignal(false)\n  let [hovered, setHovered] = createSignal(false)\n\n  return (\n    <div\n      class={`metric-card ${selected() ? 'selected' : ''}`}\n      onClick={() => setSelected(!selected())}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={(e) => {\n        e.currentTarget.style.outline = '2px solid #222'\n        e.currentTarget.style.outlineOffset = '2px'\n      }}\n      onBlur={(e) => {\n        e.currentTarget.style.outline = ''\n      }}\n      tabIndex={0}\n      style={{\n        'background-color': hovered() ? '#f5f5f5' : '#fff',\n        transform: hovered() && !selected() ? 'translateY(-2px)' : 'translateY(0)',\n        transition: 'all 0.2s',\n        padding: '20px',\n        border: '1px solid #ddd',\n        'border-radius': '8px',\n        cursor: 'pointer',\n        'box-shadow': selected() ? '0 4px 8px rgba(0,0,0,0.1)' : '0 2px 4px rgba(0,0,0,0.05)',\n      }}\n    >\n      <div style={{ 'font-size': '14px', color: '#666', 'margin-bottom': '8px' }}>\n        {props.label}\n      </div>\n      <div style={{ 'font-size': '24px', 'font-weight': 'bold', 'margin-bottom': '4px' }}>\n        {props.value}\n      </div>\n      <div\n        style={{ 'font-size': '12px', color: props.change.startsWith('+') ? '#28a745' : '#dc3545' }}\n      >\n        {props.change}\n      </div>\n    </div>\n  )\n}\n\n// Stateful Chart Bar Component\nfunction ChartBar(props: { value: number; index: number }) {\n  let [hovered, setHovered] = createSignal(false)\n\n  return (\n    <div\n      class=\"chart-bar\"\n      style={{\n        height: `${props.value}%`,\n        'background-color': hovered() ? '#286090' : '#337ab7',\n        width: '30px',\n        margin: '0 2px',\n        cursor: 'pointer',\n        transition: 'all 0.2s',\n        opacity: hovered() ? 0.9 : 1,\n        transform: hovered() ? 'scaleY(1.1)' : 'scaleY(1)',\n      }}\n      onClick={() => {}}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={(e) => {\n        e.currentTarget.style.outline = '2px solid #222'\n        e.currentTarget.style.outlineOffset = '2px'\n      }}\n      onBlur={(e) => {\n        e.currentTarget.style.outline = ''\n      }}\n      tabIndex={0}\n    />\n  )\n}\n\n// Stateful Activity Item Component\nfunction ActivityItem(props: { id: number; title: string; time: string; icon: string }) {\n  let [read, setRead] = createSignal(false)\n  let [hovered, setHovered] = createSignal(false)\n\n  return (\n    <li\n      class={`activity-item ${read() ? 'read' : ''}`}\n      onClick={() => setRead(!read())}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      style={{\n        padding: '12px',\n        'border-bottom': '1px solid #eee',\n        cursor: 'pointer',\n        'background-color': hovered() ? '#f5f5f5' : read() ? 'rgba(245, 245, 245, 0.6)' : '#fff',\n        display: 'flex',\n        'align-items': 'center',\n        gap: '12px',\n      }}\n    >\n      <span\n        style={{\n          width: '32px',\n          height: '32px',\n          'border-radius': '50%',\n          'background-color': '#337ab7',\n          color: '#fff',\n          display: 'flex',\n          'align-items': 'center',\n          'justify-content': 'center',\n          'font-weight': 'bold',\n        }}\n      >\n        {props.icon}\n      </span>\n      <div style={{ flex: 1 }}>\n        <div style={{ 'font-weight': read() ? 'normal' : 'bold' }}>{props.title}</div>\n        <div style={{ 'font-size': '12px', color: '#666' }}>{props.time}</div>\n      </div>\n    </li>\n  )\n}\n\n// Stateful Dropdown Menu Component\nfunction DropdownMenu(props: { rowId: number }) {\n  let [open, setOpen] = createSignal(false)\n  let [hovered, setHovered] = createSignal(false)\n\n  let actions = ['View Details', 'Edit', 'Duplicate', 'Archive', 'Delete']\n\n  return (\n    <div style={{ position: 'relative', display: 'inline-block' }}>\n      <button\n        class=\"btn btn-primary\"\n        onClick={(e) => {\n          e.stopPropagation()\n          setOpen(!open())\n        }}\n        onMouseEnter={() => setHovered(true)}\n        onMouseLeave={() => setHovered(false)}\n        onFocus={(e) => {\n          e.currentTarget.style.outline = '2px solid #222'\n          e.currentTarget.style.outlineOffset = '2px'\n        }}\n        onBlur={(e) => {\n          e.currentTarget.style.outline = ''\n        }}\n        style={{\n          padding: '4px 8px',\n          'font-size': '12px',\n          'background-color': hovered() ? '#286090' : '#337ab7',\n        }}\n      >\n        ⋮\n      </button>\n      {open() && (\n        <div\n          style={{\n            position: 'absolute',\n            top: '100%',\n            right: 0,\n            'background-color': '#fff',\n            border: '1px solid #ddd',\n            'border-radius': '4px',\n            'box-shadow': '0 4px 8px rgba(0,0,0,0.1)',\n            'z-index': 1000,\n            'min-width': '150px',\n            'margin-top': '4px',\n          }}\n          onMouseLeave={() => setOpen(false)}\n        >\n          <For each={actions}>\n            {(action, idx) => (\n              <div\n                onClick={(e) => {\n                  e.stopPropagation()\n                  setOpen(false)\n                }}\n                onMouseEnter={(e) => {\n                  e.currentTarget.style.backgroundColor = '#f5f5f5'\n                }}\n                onMouseLeave={(e) => {\n                  e.currentTarget.style.backgroundColor = '#fff'\n                }}\n                style={{\n                  padding: '8px 12px',\n                  cursor: 'pointer',\n                  'border-bottom': idx() < actions.length - 1 ? '1px solid #eee' : 'none',\n                }}\n              >\n                {action}\n              </div>\n            )}\n          </For>\n        </div>\n      )}\n    </div>\n  )\n}\n\n// Stateful Dashboard Table Row Component\nfunction DashboardTableRow(props: { row: Row }) {\n  let [hovered, setHovered] = createSignal(false)\n  let [selected, setSelected] = createSignal(false)\n\n  return (\n    <tr\n      class={selected() ? 'danger' : ''}\n      onClick={() => setSelected(!selected())}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      style={{\n        'background-color': hovered() ? '#f5f5f5' : '#fff',\n        cursor: 'pointer',\n      }}\n    >\n      <td style={{ padding: '12px', 'border-top': '1px solid #ddd' }}>{props.row.id}</td>\n      <td style={{ padding: '12px', 'border-top': '1px solid #ddd' }}>{props.row.label}</td>\n      <td style={{ padding: '12px', 'border-top': '1px solid #ddd' }}>\n        <span style={{ color: '#28a745' }}>Active</span>\n      </td>\n      <td style={{ padding: '12px', 'border-top': '1px solid #ddd' }}>\n        ${(props.row.id * 10.5).toFixed(2)}\n      </td>\n      <td style={{ padding: '12px', 'border-top': '1px solid #ddd' }}>\n        <DropdownMenu rowId={props.row.id} />\n      </td>\n    </tr>\n  )\n}\n\n// Stateful Search Input Component\nfunction SearchInput() {\n  let [value, setValue] = createSignal('')\n  let [focused, setFocused] = createSignal(false)\n\n  return (\n    <input\n      type=\"text\"\n      placeholder=\"Search...\"\n      value={value()}\n      onInput={(e) => setValue(e.currentTarget.value)}\n      onFocus={() => setFocused(true)}\n      onBlur={() => setFocused(false)}\n      style={{\n        padding: '8px 12px',\n        border: `1px solid ${focused() ? '#337ab7' : '#ddd'}`,\n        'border-radius': '4px',\n        'font-size': '14px',\n        width: '300px',\n        outline: focused() ? '2px solid #337ab7' : 'none',\n        'outline-offset': '2px',\n      }}\n    />\n  )\n}\n\n// Stateful Form Widgets Component\nfunction FormWidgets() {\n  let [selectValue, setSelectValue] = createSignal('option1')\n  let [checkboxValues, setCheckboxValues] = createSignal<Set<string>>(new Set())\n  let [radioValue, setRadioValue] = createSignal('radio1')\n  let [toggleValue, setToggleValue] = createSignal(false)\n  let [progressValue, setProgressValue] = createSignal(45)\n\n  let checkboxLabels = ['Checkbox 1', 'Checkbox 2', 'Checkbox 3']\n  let radioLabels = ['Radio 1', 'Radio 2', 'Radio 3']\n\n  return (\n    <div style={{ padding: '20px', 'background-color': '#f9f9f9', 'border-radius': '8px' }}>\n      <h3 style={{ 'margin-top': 0, 'margin-bottom': '16px' }}>Settings</h3>\n      <div style={{ 'margin-bottom': '16px' }}>\n        <label style={{ display: 'block', 'margin-bottom': '4px', 'font-size': '14px' }}>\n          Select Option\n        </label>\n        <select\n          value={selectValue()}\n          onChange={(e) => setSelectValue(e.currentTarget.value)}\n          onFocus={(e) => {\n            e.currentTarget.style.borderColor = '#337ab7'\n            e.currentTarget.style.outline = '2px solid #337ab7'\n            e.currentTarget.style.outlineOffset = '2px'\n          }}\n          onBlur={(e) => {\n            e.currentTarget.style.borderColor = '#ddd'\n            e.currentTarget.style.outline = 'none'\n          }}\n          style={{\n            padding: '6px 12px',\n            border: '1px solid #ddd',\n            'border-radius': '4px',\n            'font-size': '14px',\n            width: '100%',\n          }}\n        >\n          <option value=\"option1\">Option 1</option>\n          <option value=\"option2\">Option 2</option>\n          <option value=\"option3\">Option 3</option>\n          <option value=\"option4\">Option 4</option>\n        </select>\n      </div>\n      <For each={checkboxLabels}>\n        {(label, idx) => (\n          <div\n            style={{\n              'margin-bottom': '12px',\n              display: 'flex',\n              'align-items': 'center',\n              gap: '8px',\n            }}\n          >\n            <input\n              type=\"checkbox\"\n              id={`checkbox-${idx()}`}\n              checked={checkboxValues().has(`checkbox-${idx()}`)}\n              onChange={(e) => {\n                let next = new Set(checkboxValues())\n                if (e.target.checked) {\n                  next.add(`checkbox-${idx()}`)\n                } else {\n                  next.delete(`checkbox-${idx()}`)\n                }\n                setCheckboxValues(next)\n              }}\n              onFocus={(e) => {\n                e.currentTarget.style.outline = '2px solid #337ab7'\n                e.currentTarget.style.outlineOffset = '2px'\n              }}\n              onBlur={(e) => {\n                e.currentTarget.style.outline = ''\n              }}\n            />\n            <label for={`checkbox-${idx()}`} style={{ 'font-size': '14px', cursor: 'pointer' }}>\n              {label}\n            </label>\n          </div>\n        )}\n      </For>\n      <div style={{ 'margin-bottom': '16px' }}>\n        <For each={radioLabels}>\n          {(label, idx) => (\n            <label style={{ display: 'block', 'margin-bottom': '8px', cursor: 'pointer' }}>\n              <input\n                type=\"radio\"\n                name=\"radio-group\"\n                value={`radio${idx() + 1}`}\n                checked={radioValue() === `radio${idx() + 1}`}\n                onChange={(e) => setRadioValue(e.target.value)}\n                onFocus={(e) => {\n                  e.currentTarget.style.outline = '2px solid #337ab7'\n                  e.currentTarget.style.outlineOffset = '2px'\n                }}\n                onBlur={(e) => {\n                  e.currentTarget.style.outline = ''\n                }}\n                style={{ 'margin-right': '8px' }}\n              />\n              {label}\n            </label>\n          )}\n        </For>\n      </div>\n      <div style={{ 'margin-bottom': '16px' }}>\n        <label style={{ display: 'block', 'margin-bottom': '4px', 'font-size': '14px' }}>\n          Toggle Switch\n        </label>\n        <label\n          style={{\n            display: 'inline-block',\n            position: 'relative',\n            width: '50px',\n            height: '24px',\n            cursor: 'pointer',\n          }}\n        >\n          <input\n            type=\"checkbox\"\n            checked={toggleValue()}\n            onChange={(e) => setToggleValue(e.target.checked)}\n            onFocus={(e) => {\n              e.currentTarget.style.outline = '2px solid #222'\n              e.currentTarget.style.outlineOffset = '2px'\n            }}\n            onBlur={(e) => {\n              e.currentTarget.style.outline = ''\n            }}\n            style={{ opacity: 0, width: 0, height: 0 }}\n          />\n          <span\n            style={{\n              position: 'absolute',\n              top: 0,\n              left: 0,\n              right: 0,\n              bottom: 0,\n              'background-color': toggleValue() ? '#337ab7' : '#ccc',\n              'border-radius': '24px',\n              transition: 'background-color 0.3s',\n            }}\n          >\n            <span\n              style={{\n                position: 'absolute',\n                content: '\"\"',\n                height: '18px',\n                width: '18px',\n                left: '3px',\n                bottom: '3px',\n                'background-color': '#fff',\n                'border-radius': '50%',\n                transition: 'transform 0.3s',\n                transform: toggleValue() ? 'translateX(26px)' : 'translateX(0)',\n              }}\n            />\n          </span>\n        </label>\n      </div>\n      <div>\n        <label style={{ display: 'block', 'margin-bottom': '4px', 'font-size': '14px' }}>\n          Progress Bar\n        </label>\n        <div\n          style={{\n            width: '100%',\n            height: '24px',\n            'background-color': '#eee',\n            'border-radius': '4px',\n            overflow: 'hidden',\n            position: 'relative',\n          }}\n        >\n          <div\n            style={{\n              width: `${progressValue()}%`,\n              height: '100%',\n              'background-color': '#337ab7',\n              transition: 'width 0.3s',\n              display: 'flex',\n              'align-items': 'center',\n              'justify-content': 'center',\n              color: '#fff',\n              'font-size': '12px',\n            }}\n          >\n            {progressValue()}%\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction Dashboard(props: { onSwitchToTable: () => void }) {\n  let [dashboardRows, setDashboardRows] = createSignal(buildData(300))\n\n  let sortDashboardAsc = () => {\n    setDashboardRows((current) => sortRows(current, true))\n  }\n\n  let sortDashboardDesc = () => {\n    setDashboardRows((current) => sortRows(current, false))\n  }\n  let chartData = [65, 45, 78, 52, 89, 34, 67, 91, 43, 56, 72, 38, 55, 82, 47, 63, 71, 39, 58, 84]\n  let activities = Array.from({ length: 50 }, (_, i) => ({\n    id: i + 1,\n    title: `Activity ${i + 1}: ${['Order placed', 'Payment received', 'Shipment created', 'Customer registered', 'Product updated'][i % 5]}`,\n    time: `${i + 1} ${i === 0 ? 'minute' : 'minutes'} ago`,\n    icon: ['O', 'P', 'S', 'C', 'U'][i % 5],\n  }))\n\n  return (\n    <div class=\"container\" style={{ 'max-width': '1400px' }}>\n      <div\n        style={{\n          display: 'flex',\n          'margin-bottom': '20px',\n          'align-items': 'center',\n          'justify-content': 'space-between',\n        }}\n      >\n        <h1 style={{ margin: 0 }}>Dashboard</h1>\n        <button\n          id=\"switchToTable\"\n          class=\"btn btn-primary\"\n          type=\"button\"\n          onClick={props.onSwitchToTable}\n          onFocus={(e) => {\n            e.currentTarget.style.outline = '2px solid #222'\n            e.currentTarget.style.outlineOffset = '2px'\n          }}\n          onBlur={(e) => {\n            e.currentTarget.style.outline = ''\n          }}\n        >\n          Switch to Table\n        </button>\n      </div>\n\n      <div style={{ display: 'flex', gap: '20px', 'margin-bottom': '20px' }}>\n        <div style={{ flex: 1, display: 'flex', gap: '16px' }}>\n          <MetricCard id={1} label=\"Total Sales\" value=\"$125,430\" change=\"+12.5%\" />\n          <MetricCard id={2} label=\"Orders\" value=\"1,234\" change=\"+8.2%\" />\n          <MetricCard id={3} label=\"Customers\" value=\"5,678\" change=\"+15.3%\" />\n          <MetricCard id={4} label=\"Revenue\" value=\"$89,123\" change=\"+9.7%\" />\n        </div>\n      </div>\n\n      <div\n        style={{\n          display: 'grid',\n          'grid-template-columns': '1fr 1fr',\n          gap: '20px',\n          'margin-bottom': '20px',\n        }}\n      >\n        <div\n          style={{\n            padding: '20px',\n            'background-color': '#fff',\n            border: '1px solid #ddd',\n            'border-radius': '8px',\n          }}\n        >\n          <h3 style={{ 'margin-top': 0, 'margin-bottom': '16px' }}>Sales Performance</h3>\n          <div\n            style={{\n              display: 'flex',\n              'align-items': 'flex-end',\n              'justify-content': 'space-around',\n              height: '200px',\n              padding: '20px 0',\n            }}\n          >\n            <For each={chartData}>\n              {(value, index) => <ChartBar value={value} index={index()} />}\n            </For>\n          </div>\n        </div>\n\n        <div\n          style={{\n            padding: '20px',\n            'background-color': '#fff',\n            border: '1px solid #ddd',\n            'border-radius': '8px',\n          }}\n        >\n          <h3 style={{ 'margin-top': 0, 'margin-bottom': '16px' }}>Recent Activity</h3>\n          <ul\n            style={{\n              'list-style': 'none',\n              padding: 0,\n              margin: 0,\n              'max-height': '200px',\n              'overflow-y': 'auto',\n            }}\n          >\n            <For each={activities}>{(activity) => <ActivityItem {...activity} />}</For>\n          </ul>\n        </div>\n      </div>\n\n      <div style={{ 'margin-bottom': '20px' }}>\n        <div\n          style={{\n            display: 'flex',\n            'justify-content': 'space-between',\n            'align-items': 'center',\n            'margin-bottom': '12px',\n          }}\n        >\n          <div style={{ display: 'flex', 'align-items': 'center', gap: '12px' }}>\n            <h3 style={{ margin: 0 }}>Dashboard Items</h3>\n            <button\n              id=\"sortDashboardAsc\"\n              class=\"btn btn-primary\"\n              type=\"button\"\n              onClick={sortDashboardAsc}\n              style={{ padding: '4px 8px', 'font-size': '12px' }}\n            >\n              Sort ↑\n            </button>\n            <button\n              id=\"sortDashboardDesc\"\n              class=\"btn btn-primary\"\n              type=\"button\"\n              onClick={sortDashboardDesc}\n              style={{ padding: '4px 8px', 'font-size': '12px' }}\n            >\n              Sort ↓\n            </button>\n          </div>\n          <SearchInput />\n        </div>\n        <div\n          style={{\n            'background-color': '#fff',\n            border: '1px solid #ddd',\n            'border-radius': '8px',\n            overflow: 'hidden',\n          }}\n        >\n          <table style={{ width: '100%', 'border-collapse': 'collapse' }}>\n            <thead>\n              <tr style={{ 'background-color': '#f5f5f5' }}>\n                <th\n                  style={{\n                    padding: '12px',\n                    'text-align': 'left',\n                    'border-bottom': '2px solid #ddd',\n                  }}\n                >\n                  ID\n                </th>\n                <th\n                  style={{\n                    padding: '12px',\n                    'text-align': 'left',\n                    'border-bottom': '2px solid #ddd',\n                  }}\n                >\n                  Label\n                </th>\n                <th\n                  style={{\n                    padding: '12px',\n                    'text-align': 'left',\n                    'border-bottom': '2px solid #ddd',\n                  }}\n                >\n                  Status\n                </th>\n                <th\n                  style={{\n                    padding: '12px',\n                    'text-align': 'left',\n                    'border-bottom': '2px solid #ddd',\n                  }}\n                >\n                  Value\n                </th>\n                <th\n                  style={{\n                    padding: '12px',\n                    'text-align': 'left',\n                    'border-bottom': '2px solid #ddd',\n                  }}\n                >\n                  Actions\n                </th>\n              </tr>\n            </thead>\n            <tbody>\n              <For each={dashboardRows()}>{(row) => <DashboardTableRow row={row} />}</For>\n            </tbody>\n          </table>\n        </div>\n      </div>\n\n      <FormWidgets />\n    </div>\n  )\n}\n\nfunction App() {\n  let [rows, setRows] = createSignal<Row[]>([])\n  let [selected, setSelected] = createSignal<number | null>(null)\n  let [view, setView] = createSignal<'table' | 'dashboard'>('table')\n\n  let run = () => {\n    setRows(get1000Rows())\n    setSelected(null)\n  }\n\n  let runLots = () => {\n    setRows(get10000Rows())\n    setSelected(null)\n  }\n\n  let add = () => {\n    setRows((current) => [...current, ...get1000Rows()])\n  }\n\n  let update = () => {\n    setRows((current) => updatedEvery10thRow(current))\n  }\n\n  let clear = () => {\n    setRows([])\n    setSelected(null)\n  }\n\n  let swap = () => {\n    setRows((current) => swapRows(current))\n  }\n\n  let removeRow = (id: number) => {\n    setRows((current) => remove(current, id))\n  }\n\n  let sortAsc = () => {\n    setRows((current) => sortRows(current, true))\n  }\n\n  let sortDesc = () => {\n    setRows((current) => sortRows(current, false))\n  }\n\n  let switchToDashboard = () => {\n    setView('dashboard')\n  }\n\n  let switchToTable = () => {\n    setView('table')\n  }\n\n  let isSelected = createSelector(selected)\n\n  return (\n    <>\n      {view() === 'dashboard' ? (\n        <Dashboard onSwitchToTable={switchToTable} />\n      ) : (\n        <div class=\"container\">\n          <div class=\"jumbotron\">\n            <div class=\"row\">\n              <div class=\"col-md-6\">\n                <h1>SolidJS</h1>\n              </div>\n              <div class=\"col-md-6\">\n                <div class=\"row\">\n                  <div class=\"col-sm-6 smallpad\">\n                    <button id=\"run\" class=\"btn btn-primary btn-block\" type=\"button\" onClick={run}>\n                      Create 1,000 rows\n                    </button>\n                  </div>\n                  <div class=\"col-sm-6 smallpad\">\n                    <button\n                      id=\"runlots\"\n                      class=\"btn btn-primary btn-block\"\n                      type=\"button\"\n                      onClick={runLots}\n                    >\n                      Create 10,000 rows\n                    </button>\n                  </div>\n                  <div class=\"col-sm-6 smallpad\">\n                    <button id=\"add\" class=\"btn btn-primary btn-block\" type=\"button\" onClick={add}>\n                      Append 1,000 rows\n                    </button>\n                  </div>\n                  <div class=\"col-sm-6 smallpad\">\n                    <button\n                      id=\"update\"\n                      class=\"btn btn-primary btn-block\"\n                      type=\"button\"\n                      onClick={update}\n                    >\n                      Update every 10th row\n                    </button>\n                  </div>\n                  <div class=\"col-sm-6 smallpad\">\n                    <button\n                      id=\"clear\"\n                      class=\"btn btn-primary btn-block\"\n                      type=\"button\"\n                      onClick={clear}\n                    >\n                      Clear\n                    </button>\n                  </div>\n                  <div class=\"col-sm-6 smallpad\">\n                    <button\n                      id=\"swaprows\"\n                      class=\"btn btn-primary btn-block\"\n                      type=\"button\"\n                      onClick={swap}\n                    >\n                      Swap Rows\n                    </button>\n                  </div>\n                  <div class=\"col-sm-6 smallpad\">\n                    <button\n                      id=\"sortasc\"\n                      class=\"btn btn-primary btn-block\"\n                      type=\"button\"\n                      onClick={sortAsc}\n                    >\n                      Sort Ascending\n                    </button>\n                  </div>\n                  <div class=\"col-sm-6 smallpad\">\n                    <button\n                      id=\"sortdesc\"\n                      class=\"btn btn-primary btn-block\"\n                      type=\"button\"\n                      onClick={sortDesc}\n                    >\n                      Sort Descending\n                    </button>\n                  </div>\n                  <div class=\"col-sm-6 smallpad\">\n                    <button\n                      id=\"switchToDashboard\"\n                      class=\"btn btn-primary btn-block\"\n                      type=\"button\"\n                      onClick={switchToDashboard}\n                    >\n                      Switch to Dashboard\n                    </button>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n          <table class=\"table table-hover table-striped test-data\">\n            <tbody>\n              <For each={rows()}>\n                {(row) => {\n                  let rowId = row.id\n                  return (\n                    <tr class={isSelected(rowId) ? 'danger' : ''}>\n                      <td class=\"col-md-1\">{rowId}</td>\n                      <td class=\"col-md-4\">\n                        <a\n                          href=\"#\"\n                          onClick={(event) => {\n                            event.preventDefault()\n                            setSelected(rowId)\n                          }}\n                        >\n                          {row.label}\n                        </a>\n                      </td>\n                      <td class=\"col-md-1\">\n                        <a\n                          href=\"#\"\n                          onClick={(event) => {\n                            event.preventDefault()\n                            removeRow(rowId)\n                          }}\n                        >\n                          <span class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n                        </a>\n                      </td>\n                      <td class=\"col-md-6\" />\n                    </tr>\n                  )\n                }}\n              </For>\n            </tbody>\n          </table>\n          <span class=\"preloadicon glyphicon glyphicon-remove\" aria-hidden=\"true\" />\n        </div>\n      )}\n    </>\n  )\n}\n\nlet el = document.getElementById('app')!\nrender(() => <App />, el)\n"
  },
  {
    "path": "packages/component/bench/frameworks/solid/package.json",
    "content": "{\n  \"name\": \"component-benchmark-solid\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build:prod\": \"NODE_ENV=production node build.js\",\n    \"build\": \"node build.js\",\n    \"dev\": \"node build.js --watch\"\n  },\n  \"dependencies\": {\n    \"esbuild\": \"^0.27.1\",\n    \"solid-js\": \"^1.9.3\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"esbuild-plugin-solid\": \"^0.6.0\"\n  }\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/solid/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"solid-js\"\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/component/bench/frameworks/styles.css",
    "content": "body {\n  margin: 0;\n  padding: 10px 0 0 0;\n  overflow-y: scroll;\n  font-family:\n    system-ui,\n    -apple-system,\n    BlinkMacSystemFont,\n    sans-serif;\n  background: #ffffff;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n.container {\n  max-width: 960px;\n  margin: 0 auto;\n  padding: 0 15px 40px;\n}\n\n.row {\n  display: flex;\n  flex-wrap: wrap;\n  margin-left: -5px;\n  margin-right: -5px;\n}\n\n.col-md-6,\n.col-sm-6 {\n  padding-left: 5px;\n  padding-right: 5px;\n}\n\n@media (min-width: 768px) {\n  .col-md-6 {\n    width: 50%;\n  }\n}\n\n@media (min-width: 576px) {\n  .col-sm-6 {\n    width: 50%;\n  }\n}\n\n.jumbotron {\n  padding: 10px 20px;\n  margin-bottom: 20px;\n  background-color: #eeeeee;\n  border-radius: 6px;\n}\n\n.jumbotron .row h1 {\n  margin: 0;\n  font-size: 40px;\n  font-weight: 500;\n}\n\n.smallpad {\n  padding: 5px;\n}\n\n.btn {\n  display: inline-block;\n  margin-bottom: 0;\n  font-weight: 400;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: middle;\n  background-image: none;\n  border: 1px solid transparent;\n  padding: 6px 12px;\n  font-size: 14px;\n  line-height: 1.42857143;\n  border-radius: 4px;\n  user-select: none;\n}\n\n.btn:focus-visible {\n  outline: 2px solid #222;\n  outline-offset: 2px;\n}\n\n.btn-primary {\n  color: #fff;\n  background-color: #337ab7;\n  border-color: #2e6da4;\n}\n\n.btn-primary:hover {\n  background-color: #286090;\n  border-color: #204d74;\n}\n\n.btn-block {\n  display: block;\n  width: 100%;\n}\n\ntable {\n  width: 100%;\n  border-collapse: collapse;\n  margin-bottom: 20px;\n  /* contain: strict; */\n}\n\ntr {\n  /* contain-intrinsic-size: auto 36px;\n  content-visibility: auto; */\n}\n\n.table > tbody > tr > td {\n  padding: 8px;\n  border-top: 1px solid #dddddd;\n  white-space: nowrap;\n}\n\n.table-hover > tbody > tr:hover {\n  background-color: #f5f5f5;\n}\n\n.table-striped > tbody > tr:nth-child(odd) {\n  background-color: #f9f9f9;\n}\n\n.table > tbody > tr.danger > td {\n  background-color: #f2dede;\n}\n\n.test-data a {\n  display: block;\n  color: #337ab7;\n  text-decoration: none;\n  cursor: pointer;\n}\n\n.test-data a:hover {\n  text-decoration: underline;\n}\n\n.glyphicon {\n  position: relative;\n  display: inline-block;\n}\n\n.glyphicon-remove::before {\n  content: '×';\n  font-size: 16px;\n  line-height: 1;\n}\n\n.preloadicon {\n  position: absolute;\n  top: -20px;\n  left: -20px;\n}\n"
  },
  {
    "path": "packages/component/bench/package.json",
    "content": "{\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/node-fetch-server\": \"workspace:*\",\n    \"@remix-run/static-middleware\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"playwright\": \"^1.49.1\",\n    \"tsx\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"start\": \"tsx server.ts\",\n    \"dev\": \"tsx watch server.ts\",\n    \"build-frameworks\": \"pnpm --filter \\\"component-benchmark-*\\\" run build\",\n    \"bench\": \"tsx runner.ts\"\n  }\n}\n"
  },
  {
    "path": "packages/component/bench/runner.ts",
    "content": "import { spawn, type ChildProcess } from 'node:child_process'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { parseArgs } from 'node:util'\n\nimport { chromium, type Browser, type Page } from 'playwright'\n\nconst PORT = 44100\nconst BASE_URL = `http://localhost:${PORT}`\nconst REMIX_RESULTS_FILE = path.join(import.meta.dirname, '.remix-prev-results.json')\nconst LAST_ARGS_FILE = path.join(import.meta.dirname, '.last-args.json')\n\ninterface SavedArgs {\n  cpu: string\n  runs: string\n  warmups: string\n  headless: boolean\n  table: boolean\n  profile: boolean\n  allocProfile: boolean\n  framework: string[]\n  benchmark: string[]\n}\n\nfunction saveArgs(args: SavedArgs): void {\n  fs.writeFileSync(LAST_ARGS_FILE, JSON.stringify(args, null, 2))\n}\n\nfunction loadLastArgs(): SavedArgs | null {\n  try {\n    if (fs.existsSync(LAST_ARGS_FILE)) {\n      return JSON.parse(fs.readFileSync(LAST_ARGS_FILE, 'utf-8'))\n    }\n  } catch {\n    // Ignore errors\n  }\n  return null\n}\n\n// Check for 'repeat' command\nlet isRepeat = process.argv[2] === 'repeat'\n\n// Parse command line arguments\nlet { values: args } = parseArgs({\n  options: {\n    cpu: { type: 'string', default: '4' },\n    runs: { type: 'string', default: '5' },\n    warmups: { type: 'string', default: '2' },\n    headless: { type: 'boolean', default: false },\n    table: { type: 'boolean', default: false },\n    profile: { type: 'boolean', default: false },\n    'alloc-profile': { type: 'boolean', default: false },\n    framework: {\n      type: 'string',\n      multiple: true,\n      short: 'f',\n      // default: ['remix', 'preact'],\n    },\n    benchmark: { type: 'string', multiple: true, short: 'b' },\n  },\n  allowPositionals: true,\n})\n\n// If repeating, load saved args\nif (isRepeat) {\n  let savedArgs = loadLastArgs()\n  if (savedArgs) {\n    args = { ...args, ...savedArgs }\n    if ('allocProfile' in savedArgs) {\n      args['alloc-profile'] = savedArgs.allocProfile\n    }\n    console.log('Repeating with saved options:', savedArgs)\n  } else {\n    console.error('No previous run found. Run a benchmark first.')\n    process.exit(1)\n  }\n} else {\n  // Save current args for repeat\n  saveArgs({\n    cpu: args.cpu!,\n    runs: args.runs!,\n    warmups: args.warmups!,\n    headless: args.headless!,\n    table: args.table!,\n    profile: args.profile!,\n    allocProfile: args['alloc-profile']!,\n    framework: args.framework || [],\n    benchmark: args.benchmark || [],\n  })\n}\n\nlet cpuThrottling = parseInt(args.cpu!, 10)\nlet benchmarkRuns = parseInt(args.runs!, 10)\nlet warmupRuns = parseInt(args.warmups!, 10)\nlet headless = args.headless!\nlet useTable = args.table!\nlet showProfile = args.profile!\nlet showAllocProfile = args['alloc-profile']!\nlet frameworkFilter = args.framework || []\nlet benchmarkFilter = args.benchmark || []\n\ninterface FunctionProfile {\n  name: string\n  time: number\n  percentage: number\n}\n\ninterface AllocationProfile {\n  name: string\n  bytes: number\n  percentage: number\n}\n\ninterface TimingResult {\n  scripting: number\n  total: number\n  profile?: FunctionProfile[]\n  allocProfile?: AllocationProfile[]\n}\n\ninterface BenchmarkResult {\n  framework: string\n  operation: string\n  scripting: { times: number[]; mean: number; median: number; min: number; max: number }\n  total: { times: number[]; mean: number; median: number; min: number; max: number }\n}\n\ninterface Operation {\n  name: string\n  setup?: (page: Page) => Promise<void>\n  action: (page: Page) => Promise<TimingResult>\n  teardown?: (page: Page) => Promise<void>\n}\n\nconst EVENT_TIMING_TIMEOUT_MS = 5000\n\n// Click an element and measure time until next paint using Event Timing API\n// Also captures Chrome DevTools Profiler data for detailed function-level analysis\nasync function clickAndMeasure(\n  page: Page,\n  selector: string,\n  operationName?: string,\n): Promise<TimingResult> {\n  // Set up the observer before clicking (using string to avoid tsx transformation issues)\n  await page.evaluate(`\n    window.__benchResult = null;\n    window.__benchObserver = new PerformanceObserver(function(list) {\n      var entries = list.getEntries();\n      for (var i = 0; i < entries.length; i++) {\n        var entry = entries[i];\n        if (entry.entryType === 'event' && entry.name === 'click') {\n          window.__benchResult = {\n            scripting: entry.processingEnd - entry.processingStart,\n            total: entry.duration\n          };\n          window.__benchObserver.disconnect();\n          return;\n        }\n      }\n    });\n    window.__benchObserver.observe({ type: 'event', buffered: false, durationThreshold: 0 });\n  `)\n\n  // Start Chrome DevTools Profiler\n  let cdp = await page.context().newCDPSession(page)\n  if (showProfile) {\n    await cdp.send('Profiler.enable')\n    await cdp.send('Profiler.start')\n  }\n  if (showAllocProfile) {\n    await cdp.send('HeapProfiler.enable')\n    await cdp.send('HeapProfiler.startSampling', {\n      samplingInterval: 32768,\n      includeObjectsCollectedByMajorGC: true,\n      includeObjectsCollectedByMinorGC: true,\n    })\n  }\n\n  // Use Playwright's click which fires real pointer events\n  await page.click(selector)\n\n  let timing: TimingResult\n  let cpuProfileResult: any\n  let allocProfileResult: any\n  try {\n    let operationLabel = operationName ?? 'unknown'\n    timing = (await page.evaluate(`\n      new Promise(function(resolve, reject) {\n        var timeoutMs = ${EVENT_TIMING_TIMEOUT_MS}\n        var selector = ${JSON.stringify(selector)}\n        var operationName = ${JSON.stringify(operationLabel)}\n        var timeoutId = setTimeout(function() {\n          if (window.__benchObserver) {\n            window.__benchObserver.disconnect()\n          }\n          reject(new Error(\n            'Timed out waiting for Event Timing click entry after ' +\n            timeoutMs +\n            'ms (selector=\"' +\n            selector +\n            '\", operation=\"' +\n            operationName +\n            '\")'\n          ))\n        }, timeoutMs)\n\n        function check() {\n          if (window.__benchResult !== null) {\n            clearTimeout(timeoutId)\n            resolve(window.__benchResult)\n            return\n          }\n          requestAnimationFrame(check)\n        }\n\n        requestAnimationFrame(check)\n      })\n    `)) as TimingResult\n  } finally {\n    if (showProfile) {\n      cpuProfileResult = await cdp.send('Profiler.stop').catch(() => null)\n      await cdp.send('Profiler.disable').catch(() => undefined)\n    }\n    if (showAllocProfile) {\n      allocProfileResult = await cdp.send('HeapProfiler.stopSampling').catch(() => null)\n      await cdp.send('HeapProfiler.disable').catch(() => undefined)\n    }\n  }\n\n  // Process profiling data\n  let profileData: FunctionProfile[] | undefined\n  if (showProfile && cpuProfileResult && cpuProfileResult.profile) {\n    let profile = cpuProfileResult.profile as any\n    let nodes = profile.nodes || []\n    let samples = profile.samples || []\n    let timeDeltas = profile.timeDeltas || []\n\n    // Calculate self time for each function\n    let functionTimes = new Map<number, number>()\n    let functionNames = new Map<number, string>()\n\n    // Build function name map\n    for (let node of nodes) {\n      let name = node.callFrame?.functionName || node.callFrame?.url || 'unknown'\n      if (name.includes('node_modules')) continue // Skip node_modules\n      functionNames.set(node.id, name)\n    }\n\n    // Calculate time spent in each function (self time = time when this function is on top of stack)\n    let totalTime = 0\n    for (let i = 0; i < samples.length; i++) {\n      let sampleId = samples[i]\n      let delta = timeDeltas[i] || 0\n      totalTime += delta\n\n      // Self time is when this function is the top of the stack\n      let current = functionTimes.get(sampleId) || 0\n      functionTimes.set(sampleId, current + delta)\n    }\n\n    // Sort by self time and get top 30\n    profileData = Array.from(functionTimes.entries())\n      .map(([id, time]) => ({\n        name: functionNames.get(id) || 'unknown',\n        time: time / 1000, // Convert to ms\n        percentage: totalTime > 0 ? (time / totalTime) * 100 : 0,\n      }))\n      .filter((item) => !item.name.includes('node_modules'))\n      .sort((a, b) => b.time - a.time)\n      .slice(0, 30)\n  }\n\n  let allocProfileData: AllocationProfile[] | undefined\n  if (showAllocProfile && allocProfileResult && allocProfileResult.profile) {\n    allocProfileData = buildAllocationProfile(allocProfileResult.profile as any)\n  }\n\n  return { ...timing, profile: profileData, allocProfile: allocProfileData }\n}\n\n// Wait for the main thread to be idle (no pending tasks)\nasync function waitForIdle(page: Page): Promise<void> {\n  await page.evaluate(`\n    new Promise(function(resolve) {\n      // First wait for paint to complete\n      requestAnimationFrame(function() {\n        requestAnimationFrame(function() {\n          // Then wait for the main thread to be idle\n          requestIdleCallback(function() {\n            // Double-check with another idle callback to ensure cleanup is done\n            requestIdleCallback(resolve, { timeout: 100 });\n          }, { timeout: 100 });\n        });\n      });\n    })\n  `)\n}\n\n// Click without measuring (for setup/teardown)\nasync function click(page: Page, selector: string): Promise<void> {\n  await page.click(selector)\n  // Wait for paint and idle to complete before continuing\n  await waitForIdle(page)\n}\n\n// Clear all rows\nasync function clear(page: Page): Promise<void> {\n  await click(page, '#clear')\n}\n\n// Create 1000 rows\nasync function create1k(page: Page): Promise<void> {\n  await click(page, '#run')\n}\n\n// Define all benchmark operations\nconst operations: Operation[] = [\n  {\n    name: 'create1k',\n    setup: clear,\n    action: (page) => clickAndMeasure(page, '#run', 'create1k'),\n  },\n  // {\n  //   name: 'create10k',\n  //   setup: clear,\n  //   action: (page) => clickAndMeasure(page, '#runlots', 'create10k'),\n  // },\n  {\n    name: 'append1k',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, '#add', 'append1k'),\n    teardown: clear,\n  },\n  {\n    name: 'update',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, '#update', 'update'),\n    teardown: clear,\n  },\n  {\n    name: 'clear',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, '#clear', 'clear'),\n  },\n  {\n    name: 'swapRows',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, '#swaprows', 'swapRows'),\n    teardown: clear,\n  },\n  {\n    name: 'selectRow',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, 'tbody tr:first-child td.col-md-4 a', 'selectRow'),\n    teardown: clear,\n  },\n  {\n    name: 'removeRow',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, 'tbody tr:first-child td.col-md-1 a', 'removeRow'),\n    teardown: clear,\n  },\n  {\n    name: 'replace1k',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, '#run', 'replace1k'),\n    teardown: clear,\n  },\n  {\n    name: 'sortAsc',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, '#sortasc', 'sortAsc'),\n    teardown: clear,\n  },\n  {\n    name: 'sortDesc',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, '#sortdesc', 'sortDesc'),\n    teardown: clear,\n  },\n  {\n    name: 'switchToDashboard',\n    setup: create1k,\n    action: (page) => clickAndMeasure(page, '#switchToDashboard', 'switchToDashboard'),\n    teardown: async (page) => {\n      await click(page, '#switchToTable')\n      await clear(page)\n    },\n  },\n  {\n    name: 'renderDashboard',\n    setup: clear,\n    action: (page) => clickAndMeasure(page, '#switchToDashboard', 'renderDashboard'),\n    teardown: async (page) => {\n      await click(page, '#switchToTable')\n      await clear(page)\n    },\n  },\n  {\n    name: 'teardownDashboard',\n    setup: async (page) => {\n      await clear(page)\n      await click(page, '#switchToDashboard')\n    },\n    action: (page) => clickAndMeasure(page, '#switchToTable', 'teardownDashboard'),\n    teardown: clear,\n  },\n  {\n    name: 'sortDashboardAsc',\n    setup: async (page) => {\n      await clear(page)\n      await click(page, '#switchToDashboard')\n    },\n    action: (page) => clickAndMeasure(page, '#sortDashboardAsc', 'sortDashboardAsc'),\n    teardown: async (page) => {\n      await click(page, '#switchToTable')\n      await clear(page)\n    },\n  },\n  {\n    name: 'sortDashboardDesc',\n    setup: async (page) => {\n      await clear(page)\n      await click(page, '#switchToDashboard')\n    },\n    action: (page) => clickAndMeasure(page, '#sortDashboardDesc', 'sortDashboardDesc'),\n    teardown: async (page) => {\n      await click(page, '#switchToTable')\n      await clear(page)\n    },\n  },\n]\n\n// Start the benchmark server\nfunction startServer(): Promise<ChildProcess> {\n  return new Promise((resolve, reject) => {\n    let server = spawn('tsx', ['server.ts'], {\n      cwd: import.meta.dirname,\n      stdio: ['ignore', 'pipe', 'pipe'],\n    })\n\n    let started = false\n\n    server.stdout?.on('data', (data: Buffer) => {\n      let output = data.toString()\n      if (output.includes('Benchmark server running') && !started) {\n        started = true\n        resolve(server)\n      }\n    })\n\n    server.stderr?.on('data', (data: Buffer) => {\n      if (!started) {\n        reject(new Error(`Server error: ${data.toString()}`))\n      }\n    })\n\n    server.on('error', reject)\n\n    // Timeout if server doesn't start\n    setTimeout(() => {\n      if (!started) {\n        server.kill()\n        reject(new Error('Server failed to start within timeout'))\n      }\n    }, 10000)\n  })\n}\n\n// Stop the server\nfunction stopServer(server: ChildProcess): Promise<void> {\n  return new Promise((resolve) => {\n    server.on('close', () => resolve())\n    server.kill('SIGTERM')\n  })\n}\n\n// Get list of frameworks\nfunction getFrameworks(): string[] {\n  let frameworksDir = path.join(import.meta.dirname, 'frameworks')\n  let entries = fs.readdirSync(frameworksDir, { withFileTypes: true })\n  return entries\n    .filter((entry) => entry.isDirectory())\n    .map((entry) => entry.name)\n    .sort()\n}\n\n// Save remix results to file for comparison with next run\nfunction saveRemixResults(results: BenchmarkResult[]): void {\n  let remixResults = results.filter((r) => r.framework === 'remix')\n  if (remixResults.length > 0) {\n    fs.writeFileSync(REMIX_RESULTS_FILE, JSON.stringify(remixResults, null, 2))\n  }\n}\n\n// Load previous remix results if they exist\nfunction loadPreviousRemixResults(): BenchmarkResult[] {\n  try {\n    if (fs.existsSync(REMIX_RESULTS_FILE)) {\n      let data = fs.readFileSync(REMIX_RESULTS_FILE, 'utf-8')\n      let results: BenchmarkResult[] = JSON.parse(data)\n      // Rename framework to \"remix (prev)\"\n      return results.map((r) => ({ ...r, framework: 'remix (prev)' }))\n    }\n  } catch {\n    // Ignore errors loading previous results\n  }\n  return []\n}\n\n// Run a single operation and measure time\nasync function measureOperation(page: Page, operation: Operation): Promise<TimingResult> {\n  // Run setup if defined\n  if (operation.setup) {\n    await operation.setup(page)\n  }\n\n  // Wait for idle before measuring to ensure no pending work from setup\n  await waitForIdle(page)\n\n  // Measure the action (returns timing from Event Timing API)\n  let timing = await operation.action(page)\n\n  // Run teardown if defined\n  if (operation.teardown) {\n    await operation.teardown(page)\n  }\n\n  // Wait for idle after teardown to ensure cleanup is complete before next operation\n  await waitForIdle(page)\n\n  return timing\n}\n\n// Calculate statistics for an array of numbers\nfunction calcStats(times: number[]) {\n  let sorted = [...times].sort((a, b) => a - b)\n  return {\n    times,\n    mean: times.reduce((a, b) => a + b, 0) / times.length,\n    median: sorted[Math.floor(sorted.length / 2)],\n    min: sorted[0],\n    max: sorted[sorted.length - 1],\n  }\n}\n\n// Aggregate profiling data and calculate medians\nfunction aggregateProfiles(\n  profiles: FunctionProfile[][],\n  operationName: string,\n): FunctionProfile[] | null {\n  if (profiles.length === 0) return null\n\n  // Collect all function names across all runs\n  let functionMap = new Map<string, number[]>()\n  for (let profile of profiles) {\n    for (let func of profile) {\n      if (!functionMap.has(func.name)) {\n        functionMap.set(func.name, [])\n      }\n      functionMap.get(func.name)!.push(func.time)\n    }\n  }\n\n  // Calculate median time for each function\n  // Also calculate average percentage across runs\n  let aggregated: FunctionProfile[] = []\n  for (let [name, times] of functionMap.entries()) {\n    let sorted = [...times].sort((a, b) => a - b)\n    let median = sorted[Math.floor(sorted.length / 2)]\n\n    // Calculate average percentage across all runs\n    let percentages: number[] = []\n    for (let profile of profiles) {\n      let func = profile.find((f) => f.name === name)\n      if (func) {\n        percentages.push(func.percentage)\n      }\n    }\n    let avgPercentage =\n      percentages.length > 0 ? percentages.reduce((a, b) => a + b, 0) / percentages.length : 0\n\n    aggregated.push({\n      name,\n      time: median,\n      percentage: avgPercentage,\n    })\n  }\n\n  // Sort by median time and return top 30\n  return aggregated.sort((a, b) => b.time - a.time).slice(0, 30)\n}\n\nfunction aggregateAllocationProfiles(profiles: AllocationProfile[][]): AllocationProfile[] | null {\n  if (profiles.length === 0) return null\n\n  let functionMap = new Map<string, number[]>()\n  for (let profile of profiles) {\n    for (let func of profile) {\n      if (!functionMap.has(func.name)) {\n        functionMap.set(func.name, [])\n      }\n      functionMap.get(func.name)!.push(func.bytes)\n    }\n  }\n\n  let aggregated: AllocationProfile[] = []\n  for (let [name, bytes] of functionMap.entries()) {\n    let sorted = [...bytes].sort((a, b) => a - b)\n    let median = sorted[Math.floor(sorted.length / 2)]\n\n    let percentages: number[] = []\n    for (let profile of profiles) {\n      let func = profile.find((f) => f.name === name)\n      if (func) {\n        percentages.push(func.percentage)\n      }\n    }\n    let avgPercentage =\n      percentages.length > 0 ? percentages.reduce((a, b) => a + b, 0) / percentages.length : 0\n\n    aggregated.push({\n      name,\n      bytes: median,\n      percentage: avgPercentage,\n    })\n  }\n\n  return aggregated.sort((a, b) => b.bytes - a.bytes).slice(0, 30)\n}\n\n// Print profiling table\nfunction printProfileTable(profile: FunctionProfile[], operationName: string): void {\n  if (profile.length === 0) return\n\n  console.log(`\\n${operationName}`)\n  console.log('📊 Top functions by self time (median):')\n  console.log('═'.repeat(90))\n  console.log(`${'Function'.padEnd(70)} ${'Time (ms)'.padStart(10)} ${'%'.padStart(8)}`)\n  console.log('─'.repeat(90))\n  for (let item of profile) {\n    let name = item.name.length > 68 ? '...' + item.name.slice(-65) : item.name\n    console.log(\n      `${name.padEnd(70)} ${item.time.toFixed(2).padStart(10)} ${item.percentage.toFixed(1).padStart(7)}%`,\n    )\n  }\n  console.log('═'.repeat(90))\n}\n\nfunction printAllocationProfileTable(profile: AllocationProfile[], operationName: string): void {\n  if (profile.length === 0) return\n\n  console.log(`\\n${operationName}`)\n  console.log('🧠 Top functions by allocated heap (median):')\n  console.log('═'.repeat(100))\n  console.log(\n    `${'Function'.padEnd(68)} ${'Bytes'.padStart(14)} ${'KB'.padStart(10)} ${'%'.padStart(8)}`,\n  )\n  console.log('─'.repeat(100))\n  for (let item of profile) {\n    let name = item.name.length > 66 ? '...' + item.name.slice(-63) : item.name\n    let kb = item.bytes / 1024\n    console.log(\n      `${name.padEnd(68)} ${item.bytes.toFixed(0).padStart(14)} ${kb.toFixed(2).padStart(10)} ${item.percentage.toFixed(1).padStart(7)}%`,\n    )\n  }\n  console.log('═'.repeat(100))\n}\n\n// Run benchmark for a single framework\nasync function benchmarkFramework(\n  page: Page,\n  framework: string,\n): Promise<{\n  results: BenchmarkResult[]\n  profiles: Map<string, FunctionProfile[][]>\n  allocProfiles: Map<string, AllocationProfile[][]>\n}> {\n  let results: BenchmarkResult[] = []\n  let profiles = new Map<string, FunctionProfile[][]>()\n  let allocProfiles = new Map<string, AllocationProfile[][]>()\n\n  let url = `${BASE_URL}/${framework}/index.html`\n\n  // Filter operations if benchmark filter is specified\n  let filteredOperations =\n    benchmarkFilter.length > 0\n      ? operations.filter((op) => benchmarkFilter.some((filter) => op.name.includes(filter)))\n      : operations\n\n  for (let operation of filteredOperations) {\n    let scriptingTimes: number[] = []\n    let totalTimes: number[] = []\n    let runProfiles: FunctionProfile[][] = []\n    let runAllocProfiles: AllocationProfile[][] = []\n\n    // Reload page before each operation to reset all JS state (idCounter, etc.)\n    await page.goto(url)\n    await page.waitForSelector('#run')\n\n    // Warmup runs (not recorded)\n    for (let i = 0; i < warmupRuns; i++) {\n      await measureOperation(page, operation)\n    }\n\n    // Benchmark runs\n    for (let i = 0; i < benchmarkRuns; i++) {\n      let timing = await measureOperation(page, operation)\n      scriptingTimes.push(timing.scripting)\n      totalTimes.push(timing.total)\n      if (showProfile && timing.profile) {\n        runProfiles.push(timing.profile)\n      }\n      if (showAllocProfile && timing.allocProfile) {\n        runAllocProfiles.push(timing.allocProfile)\n      }\n    }\n\n    results.push({\n      framework,\n      operation: operation.name,\n      scripting: calcStats(scriptingTimes),\n      total: calcStats(totalTimes),\n    })\n\n    if (showProfile && runProfiles.length > 0) {\n      profiles.set(operation.name, runProfiles)\n    }\n    if (showAllocProfile && runAllocProfiles.length > 0) {\n      allocProfiles.set(operation.name, runAllocProfiles)\n    }\n\n    process.stdout.write('.')\n  }\n\n  return { results, profiles, allocProfiles }\n}\n\n// ANSI color codes\nconst RESET = '\\x1b[0m'\nconst RED = '\\x1b[31m'\nconst GREEN = '\\x1b[32m'\nconst YELLOW = '\\x1b[33m'\nconst WHITE = '\\x1b[97m'\nconst BG_GRAY = '\\x1b[48;5;240m'\nconst BOLD = '\\x1b[1m'\nconst DIM = '\\x1b[2m'\n\n// Print combined bar graph with scripting (yellow) and total bars\nfunction printBarGraph(allResults: BenchmarkResult[]): void {\n  let operationNames = [...new Set(allResults.map((r) => r.operation))]\n  let frameworks = [...new Set(allResults.map((r) => r.framework))]\n  let hasRemix = frameworks.includes('remix')\n\n  // Put remix first if it exists\n  if (hasRemix) {\n    frameworks = ['remix', ...frameworks.filter((f) => f !== 'remix')]\n  }\n\n  // Get terminal width (default to 100, max 120)\n  let termWidth = Math.min(process.stdout.columns || 100, 120)\n\n  // Find max framework name length for label padding\n  let maxNameLen = Math.max(...frameworks.map((f) => f.length))\n  let labelWidth = maxNameLen + 4 // \"  name: \"\n\n  // Reserve space for label + bar + \" scripting_value \" + \" total_value (ratio)\"\n  let suffixWidth = 25\n  let barMaxWidth = termWidth - labelWidth - suffixWidth\n\n  // Calculate global max value across all operations (use total since it's always >= scripting)\n  let globalMax = Math.max(...allResults.map((r) => r.total.median))\n\n  for (let opName of operationNames) {\n    console.log(`${DIM}${opName}${RESET}`)\n\n    let remixResult = hasRemix\n      ? allResults.find((r) => r.framework === 'remix' && r.operation === opName)\n      : null\n    let remixTotal = remixResult ? remixResult.total.median : null\n\n    for (let fw of frameworks) {\n      let result = allResults.find((r) => r.framework === fw && r.operation === opName)\n      let scriptingValue = result ? result.scripting.median : 0\n      let totalValue = result ? result.total.median : 0\n      let scriptingRounded = Math.round(scriptingValue * 10) / 10\n      let totalRounded = Math.round(totalValue * 10) / 10\n\n      // Calculate bar lengths (scaled to global max)\n      let scriptingBarLen = Math.round((scriptingValue / globalMax) * barMaxWidth)\n      let totalBarLen = Math.round((totalValue / globalMax) * barMaxWidth)\n      let remainingBarLen = totalBarLen - scriptingBarLen\n\n      // Build the scripting bar (yellow)\n      let scriptingBar = '█'.repeat(scriptingBarLen)\n      // Build the remaining bar (default color, total - scripting)\n      let remainingBar = '█'.repeat(Math.max(0, remainingBarLen))\n\n      // Scripting value with gray background and white text (fixed width for alignment)\n      let scriptingText = ` ${String(scriptingRounded).padStart(5)} `\n\n      // Build total suffix with ratio\n      let totalSuffix = String(totalRounded)\n      if (fw !== 'remix' && remixTotal !== null && remixTotal > 0) {\n        let ratio = Math.round((totalValue / remixTotal) * 10) / 10\n        let color = ratio < 1 ? RED : ratio > 1 ? GREEN : ''\n        totalSuffix += ` ${color}(${ratio}x)${color ? RESET : ''}`\n      }\n\n      // Print single combined line: label + yellow bars + scripting value (gray bg) + remaining bars + total\n      let label = ('  ' + fw + ':').padEnd(labelWidth)\n      console.log(\n        `${label}${YELLOW}${scriptingBar}${RESET}${BG_GRAY}${WHITE}${scriptingText}${RESET}${remainingBar} ${totalSuffix}`,\n      )\n    }\n    console.log('')\n  }\n}\n\n// Print results as two tables (scripting and total)\nfunction printTable(allResults: BenchmarkResult[]): void {\n  let operationNames = [...new Set(allResults.map((r) => r.operation))]\n  let frameworks = [...new Set(allResults.map((r) => r.framework))]\n\n  // Put remix first if it exists\n  if (frameworks.includes('remix')) {\n    frameworks = ['remix', ...frameworks.filter((f) => f !== 'remix')]\n  }\n\n  // Build scripting table with flags for slow operations\n  let scriptingData: Record<string, Record<string, number>> = {}\n  for (let opName of operationNames) {\n    let remixResult = allResults.find((r) => r.framework === 'remix' && r.operation === opName)\n    let remixValue = remixResult ? remixResult.scripting.median : 0\n    let otherValues = frameworks\n      .filter((fw) => fw !== 'remix')\n      .map((fw) => {\n        let result = allResults.find((r) => r.framework === fw && r.operation === opName)\n        return result ? result.scripting.median : 0\n      })\n      .filter((v) => v > 0)\n\n    // Check if remix is significantly slower (2x longer than fastest other)\n    let isSlow = false\n    if (remixValue && otherValues.length > 0) {\n      let fastestOther = Math.min(...otherValues)\n      isSlow = remixValue > fastestOther * 2.0\n    }\n\n    let displayName = isSlow ? `${opName} 🚩` : opName\n    scriptingData[displayName] = {}\n    for (let fw of frameworks) {\n      let result = allResults.find((r) => r.framework === fw && r.operation === opName)\n      scriptingData[displayName][fw] = result ? Math.round(result.scripting.median * 10) / 10 : 0\n    }\n  }\n\n  // Build total table with flags for slow operations\n  let totalData: Record<string, Record<string, number>> = {}\n  for (let opName of operationNames) {\n    let remixResult = allResults.find((r) => r.framework === 'remix' && r.operation === opName)\n    let remixValue = remixResult ? remixResult.total.median : 0\n    let otherValues = frameworks\n      .filter((fw) => fw !== 'remix')\n      .map((fw) => {\n        let result = allResults.find((r) => r.framework === fw && r.operation === opName)\n        return result ? result.total.median : 0\n      })\n      .filter((v) => v > 0)\n\n    // Check if remix is significantly slower (>20% slower than fastest other)\n    let isSlow = false\n    if (remixValue && otherValues.length > 0) {\n      let fastestOther = Math.min(...otherValues)\n      isSlow = remixValue > fastestOther * 1.2\n    }\n\n    let displayName = isSlow ? `${opName} 🚩` : opName\n    totalData[displayName] = {}\n    for (let fw of frameworks) {\n      let result = allResults.find((r) => r.framework === fw && r.operation === opName)\n      totalData[displayName][fw] = result ? Math.round(result.total.median * 10) / 10 : 0\n    }\n  }\n\n  console.log('Scripting Time (ms):')\n  console.table(scriptingData)\n  console.log('')\n  console.log('Total Time (ms):')\n  console.table(totalData)\n}\n\n// Print results as bar graphs or tables\nfunction printResults(allResults: BenchmarkResult[]): void {\n  if (useTable) {\n    printTable(allResults)\n  } else {\n    printBarGraph(allResults)\n  }\n}\n\n// Main benchmark runner\nasync function main(): Promise<void> {\n  let server: ChildProcess | null = null\n  let browser: Browser | null = null\n\n  try {\n    console.log('Starting benchmark server...')\n    server = await startServer()\n\n    console.log('Launching browser...')\n    browser = await chromium.launch({ headless })\n    let page = await browser.newPage()\n\n    // Enable CPU throttling via CDP\n    let client = await page.context().newCDPSession(page)\n    await client.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottling })\n\n    let allFrameworks = getFrameworks()\n    let frameworks = allFrameworks\n\n    // Filter frameworks if specified\n    if (frameworkFilter.length > 0) {\n      let invalidFrameworks = frameworkFilter.filter((f) => !allFrameworks.includes(f))\n      if (invalidFrameworks.length > 0) {\n        console.error(`Error: Invalid framework(s): ${invalidFrameworks.join(', ')}`)\n        console.error(`Available frameworks: ${allFrameworks.join(', ')}`)\n        process.exit(1)\n      }\n      frameworks = frameworkFilter\n    }\n\n    let allResults: BenchmarkResult[] = []\n    let allProfiles = new Map<string, FunctionProfile[][]>()\n    let allAllocProfiles = new Map<string, AllocationProfile[][]>()\n\n    // Load previous remix results if remix is being benchmarked\n    let hasRemix = frameworks.includes('remix')\n    let previousRemixResults: BenchmarkResult[] = []\n    if (hasRemix) {\n      previousRemixResults = loadPreviousRemixResults()\n      if (previousRemixResults.length > 0) {\n        console.log('Loaded previous remix results for comparison')\n      }\n    }\n\n    console.log(`Benchmarking ${frameworks.length} frameworks: ${frameworks.join(', ')}`)\n    console.log(`${warmupRuns} warmup runs, ${benchmarkRuns} benchmark runs per operation`)\n    console.log(`CPU throttling: ${cpuThrottling}x`)\n    console.log('')\n\n    for (let framework of frameworks) {\n      process.stdout.write(`  ${framework}: `)\n      let { results, profiles, allocProfiles } = await benchmarkFramework(page, framework)\n      allResults.push(...results)\n      // Store profiles keyed by framework-operation name\n      for (let [operationName, runProfiles] of profiles.entries()) {\n        let key = `${framework}-${operationName}`\n        allProfiles.set(key, runProfiles)\n      }\n      for (let [operationName, runAllocProfiles] of allocProfiles.entries()) {\n        let key = `${framework}-${operationName}`\n        allAllocProfiles.set(key, runAllocProfiles)\n      }\n      console.log(' done')\n    }\n\n    // Save current remix results for next run\n    if (hasRemix) {\n      saveRemixResults(allResults)\n    }\n\n    // Add previous remix results to display only when remix is the only framework\n    // (When comparing against other frameworks, we don't need to show previous remix)\n    if (previousRemixResults.length > 0 && frameworks.length === 1) {\n      allResults.push(...previousRemixResults)\n    }\n\n    // Print aggregated profiling tables first\n    if (showProfile && allProfiles.size > 0) {\n      for (let [key, runProfiles] of allProfiles.entries()) {\n        let aggregated = aggregateProfiles(runProfiles, key)\n        if (aggregated) {\n          printProfileTable(aggregated, key)\n        }\n      }\n    }\n    if (showAllocProfile && allAllocProfiles.size > 0) {\n      for (let [key, runAllocProfiles] of allAllocProfiles.entries()) {\n        let aggregated = aggregateAllocationProfiles(runAllocProfiles)\n        if (aggregated) {\n          printAllocationProfileTable(aggregated, key)\n        }\n      }\n    }\n\n    // Print benchmark results after profiles\n    printResults(allResults)\n\n    console.log('Benchmark complete!')\n  } finally {\n    console.log('Cleaning up...')\n    if (browser) {\n      await browser.close()\n    }\n    if (server) {\n      await stopServer(server)\n    }\n  }\n}\n\nmain().catch((error) => {\n  console.error('Benchmark failed:', error)\n  process.exit(1)\n})\n\nfunction buildAllocationProfile(rawProfile: any): AllocationProfile[] {\n  let bytesByName = new Map<string, number>()\n  let totalBytes = 0\n\n  function walk(node: any) {\n    if (!node || typeof node !== 'object') return\n\n    let selfSize = Number(node.selfSize || 0)\n    let name =\n      node.callFrame?.functionName || node.callFrame?.url || node.callFrame?.scriptId || 'unknown'\n    if (!name.includes('node_modules')) {\n      let current = bytesByName.get(name) || 0\n      bytesByName.set(name, current + selfSize)\n      totalBytes += selfSize\n    }\n\n    let children = Array.isArray(node.children) ? node.children : []\n    for (let child of children) {\n      walk(child)\n    }\n  }\n\n  walk(rawProfile.head)\n\n  return Array.from(bytesByName.entries())\n    .map(([name, bytes]) => ({\n      name,\n      bytes,\n      percentage: totalBytes > 0 ? (bytes / totalBytes) * 100 : 0,\n    }))\n    .sort((a, b) => b.bytes - a.bytes)\n    .slice(0, 30)\n}\n"
  },
  {
    "path": "packages/component/bench/server.ts",
    "content": "import * as fs from 'node:fs'\nimport * as http from 'node:http'\nimport * as path from 'node:path'\n\nimport { createRouter } from '@remix-run/fetch-router'\nimport { route } from '@remix-run/fetch-router/routes'\nimport { createRequestListener } from '@remix-run/node-fetch-server'\nimport { staticFiles } from '@remix-run/static-middleware'\n\nlet frameworksDir = path.resolve(import.meta.dirname, 'frameworks')\n\nlet routes = route({\n  index: '/',\n})\n\nlet router = createRouter({\n  middleware: [staticFiles('./frameworks')],\n})\n\nlet html = String.raw\n\nrouter.get(routes.index, () => {\n  let entries = fs.readdirSync(frameworksDir, { withFileTypes: true })\n  let frameworks = entries\n    .filter((entry) => entry.isDirectory())\n    .map((entry) => entry.name)\n    .sort()\n\n  let links = frameworks\n    .map((name) => `<li><a href=\"/${name}/index.html\">${name}</a></li>`)\n    .join('')\n\n  return new Response(\n    html`<!doctype html>\n      <html>\n        <head>\n          <title>Benchmarks</title>\n          <style>\n            body {\n              font-family:\n                system-ui,\n                -apple-system,\n                BlinkMacSystemFont,\n                sans-serif;\n              max-width: 600px;\n              margin: 40px auto;\n              padding: 0 20px;\n            }\n            h1 {\n              margin-bottom: 24px;\n            }\n            ul {\n              list-style: none;\n              padding: 0;\n            }\n            li {\n              margin: 8px 0;\n            }\n            a {\n              color: #337ab7;\n              text-decoration: none;\n              font-size: 18px;\n            }\n            a:hover {\n              text-decoration: underline;\n            }\n          </style>\n        </head>\n        <body>\n          <h1>Benchmarks</h1>\n          <ul>\n            ${links}\n          </ul>\n        </body>\n      </html>`,\n    { headers: { 'Content-Type': 'text/html' } },\n  )\n})\n\nlet server = http.createServer(\n  createRequestListener(async (request) => {\n    return await router.fetch(request)\n  }),\n)\n\nserver.listen(44100, () => {\n  console.log('Benchmark server running at http://localhost:44100')\n})\n\nfunction shutdown() {\n  server.close(() => {\n    process.exit(0)\n  })\n}\n\nprocess.on('SIGINT', shutdown)\nprocess.on('SIGTERM', shutdown)\n"
  },
  {
    "path": "packages/component/bench/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\"]\n  },\n  \"exclude\": [\"frameworks\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/component/demos/.gitignore",
    "content": "*.bundled.*"
  },
  {
    "path": "packages/component/demos/animation/aspect-ratio.tsx",
    "content": "import { type Handle } from 'remix/component'\nimport { css, on, spring } from 'remix/component'\n\nexport function AspectRatio(handle: Handle) {\n  let aspectRatio = 1\n  let width = 100\n\n  return () => (\n    <div\n      mix={[\n        css({\n          display: 'flex',\n          alignItems: 'center',\n          flexDirection: 'column',\n          gap: 20,\n        }),\n      ]}\n    >\n      <div\n        mix={[\n          css({\n            display: 'flex',\n            justifyContent: 'center',\n            alignItems: 'center',\n            width: 180,\n            height: 180,\n          }),\n        ]}\n      >\n        <div\n          mix={[\n            css({\n              backgroundColor: '#8df0cc',\n              borderRadius: 10,\n              transition: spring.transition(['width', 'aspect-ratio'], 'bouncy'),\n            }),\n          ]}\n          style={{\n            width,\n            aspectRatio,\n          }}\n        />\n      </div>\n      <div mix={[css({ display: 'flex', flexDirection: 'column', gap: 12, width: '100%' })]}>\n        <label\n          mix={[\n            css({\n              display: 'flex',\n              alignItems: 'center',\n              gap: 10,\n              fontSize: 13,\n              color: '#666',\n            }),\n          ]}\n        >\n          <span mix={[css({ width: 50, flexShrink: 0 })]}>Ratio</span>\n          <input\n            type=\"range\"\n            value={aspectRatio}\n            min={0.2}\n            max={3}\n            step={0.1}\n            mix={[\n              css(rangeInputCss),\n              on('input', (event, signal) => {\n                let value = event.currentTarget.value\n\n                setTimeout(() => {\n                  if (signal.aborted) return\n                  aspectRatio = parseFloat(value)\n                  handle.update()\n                }, 200)\n              }),\n            ]}\n          />\n          <span mix={[css({ width: 32, textAlign: 'right', fontVariantNumeric: 'tabular-nums' })]}>\n            {aspectRatio.toFixed(1)}\n          </span>\n        </label>\n        <label\n          mix={[\n            css({\n              display: 'flex',\n              alignItems: 'center',\n              gap: 10,\n              fontSize: 13,\n              color: '#666',\n            }),\n          ]}\n        >\n          <span mix={[css({ width: 50, flexShrink: 0 })]}>Width</span>\n          <input\n            type=\"range\"\n            value={width}\n            min={20}\n            max={160}\n            step={5}\n            mix={[\n              css(rangeInputCss),\n              on('input', (event, signal) => {\n                let value = event.currentTarget.value\n\n                setTimeout(() => {\n                  if (signal.aborted) return\n                  width = parseFloat(value)\n                  handle.update()\n                }, 200)\n              }),\n            ]}\n          />\n          <span mix={[css({ width: 32, textAlign: 'right', fontVariantNumeric: 'tabular-nums' })]}>\n            {width}\n          </span>\n        </label>\n      </div>\n    </div>\n  )\n}\n\nlet rangeInputCss = {\n  flex: 1,\n  accentColor: '#8df0cc',\n  cursor: 'pointer',\n} as const\n"
  },
  {
    "path": "packages/component/demos/animation/bouncy-switch.tsx",
    "content": "import { type Handle } from 'remix/component'\nimport { css, on, spring } from 'remix/component'\n\nlet bounceEasing = `linear(0, 0.258 12%, 0.424 18.3%, 0.633 24.4%, 0.999 33.3%, 0.783 39.8%, 0.733 42.5%, 0.716 45.1%, 0.731 47.6%, 0.777 50.2%, 0.999 57.7%, 0.906 61.7%, 0.883 63.5%, 0.876 65.2%, 0.901 68.7%, 0.999 74.5%, 0.964 77.4%, 0.953 80.1%, 0.961 82.6%, 1 88.2%, 0.99 91.9%, 1)`\n\nexport function BouncySwitch(handle: Handle) {\n  let isOn = true\n\n  return () => (\n    <div\n      mix={[\n        css({\n          height: 160,\n          backgroundColor: '#ff6b35',\n          display: 'flex',\n          flexDirection: 'column',\n          justifyContent: 'flex-end',\n          borderRadius: 50,\n          padding: 10,\n          cursor: 'pointer',\n        }),\n        on('click', () => {\n          isOn = !isOn\n          handle.update()\n        }),\n      ]}\n    >\n      <div\n        mix={[\n          css({\n            width: 60,\n            height: 60,\n            backgroundColor: 'white',\n            borderRadius: 30,\n            willChange: 'transform',\n          }),\n        ]}\n        style={{\n          transform: isOn ? 'translateY(-100px)' : 'translateY(0)',\n          transition: isOn ? `transform ${spring()}` : `transform 800ms ${bounceEasing}`,\n        }}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/color-interpolation.tsx",
    "content": "import { css } from 'remix/component'\n\n/*\n * A comparison of color interpolation methods.\n *\n * The \"dimming effect\" in browser animations happens because sRGB is\n * gamma-encoded, so linear interpolation passes through desaturated colors.\n *\n * The OKLCH box uses the @property hack: register a custom property as <number>,\n * animate it 0→1, then use color-mix(in oklch, ...) with that number.\n */\nexport function ColorInterpolation() {\n  return () => (\n    <div mix={[css({ display: 'flex', gap: 30, alignItems: 'center', justifyContent: 'center' })]}>\n      <style>\n        {`\n          @property --color-t {\n            syntax: '<number>';\n            inherits: false;\n            initial-value: 0;\n          }\n\n          @keyframes color-t-anim {\n            0%, 100% { --color-t: 0; }\n            50% { --color-t: 1; }\n          }\n\n          .oklch-box {\n            animation: color-t-anim 4s linear infinite;\n            background-color: color-mix(\n              in oklch,\n              #ff0088 calc((1 - var(--color-t)) * 100%),\n              #0d63f8 calc(var(--color-t) * 100%)\n            );\n          }\n        `}\n      </style>\n\n      <div mix={[css({ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 })]}>\n        <div\n          mix={[\n            css({\n              width: 100,\n              height: 100,\n              borderRadius: 8,\n              backgroundColor: '#ff0088',\n              '@keyframes srgb-color': {\n                '0%, 100%': { backgroundColor: '#ff0088' },\n                '50%': { backgroundColor: '#0d63f8' },\n              },\n              animation: 'srgb-color 4s linear infinite',\n            }),\n          ]}\n        />\n        <div mix={[css({ fontSize: 14, color: '#666' })]}>sRGB</div>\n      </div>\n\n      <div mix={[css({ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 })]}>\n        <div\n          className=\"oklch-box\"\n          mix={[\n            css({\n              width: 100,\n              height: 100,\n              borderRadius: 8,\n            }),\n          ]}\n        />\n        <div mix={[css({ fontSize: 14, color: '#666' })]}>OKLCH</div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/cube.tsx",
    "content": "import type { Handle } from 'remix/component'\nimport { css, ref } from 'remix/component'\n\nexport function Cube(handle: Handle) {\n  let cube: HTMLDivElement\n\n  function animate(t: number) {\n    if (handle.signal.aborted) return\n\n    let rotate = Math.sin(t / 10000) * 200\n    let y = (1 + Math.sin(t / 1000)) * -25\n    cube.style.transform = `translateY(${y}px) rotateX(${rotate}deg) rotateY(${rotate}deg)`\n\n    requestAnimationFrame(animate)\n  }\n\n  return () => (\n    <div\n      mix={[\n        css({\n          perspective: '400px',\n          width: '100px',\n          height: '100px',\n        }),\n      ]}\n    >\n      <div\n        mix={[\n          ref((node) => {\n            cube = node\n            requestAnimationFrame(animate)\n          }),\n          css({\n            width: '100px',\n            height: '100px',\n            position: 'relative',\n            transformStyle: 'preserve-3d',\n          }),\n        ]}\n      >\n        <div\n          mix={[\n            css({\n              position: 'absolute',\n              width: '100%',\n              height: '100%',\n              opacity: 0.6,\n              transform: 'rotateY(0deg) translateZ(50px)',\n              backgroundColor: '#ff0055',\n            }),\n          ]}\n        />\n        <div\n          mix={[\n            css({\n              position: 'absolute',\n              width: '100%',\n              height: '100%',\n              opacity: 0.6,\n              transform: 'rotateY(90deg) translateZ(50px)',\n              backgroundColor: '#0099ff',\n            }),\n          ]}\n        />\n        <div\n          mix={[\n            css({\n              position: 'absolute',\n              width: '100%',\n              height: '100%',\n              opacity: 0.6,\n              transform: 'rotateY(180deg) translateZ(50px)',\n              backgroundColor: '#22cc88',\n            }),\n          ]}\n        />\n        <div\n          mix={[\n            css({\n              position: 'absolute',\n              width: '100%',\n              height: '100%',\n              opacity: 0.6,\n              transform: 'rotateY(-90deg) translateZ(50px)',\n              backgroundColor: '#ffaa00',\n            }),\n          ]}\n        />\n        <div\n          mix={[\n            css({\n              position: 'absolute',\n              width: '100%',\n              height: '100%',\n              opacity: 0.6,\n              transform: 'rotateX(90deg) translateZ(50px)',\n              backgroundColor: '#aa00ff',\n            }),\n          ]}\n        />\n        <div\n          mix={[\n            css({\n              position: 'absolute',\n              width: '100%',\n              height: '100%',\n              opacity: 0.6,\n              transform: 'rotateX(-90deg) translateZ(50px)',\n              backgroundColor: '#ff00aa',\n            }),\n          ]}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/default-animate.tsx",
    "content": "import { animateEntrance, animateLayout, css, on, type Handle } from 'remix/component'\n\nlet nextId = 1\nfunction createItem() {\n  return { id: nextId++, label: `Item ${nextId - 1}` }\n}\n\nexport function DefaultAnimate(handle: Handle) {\n  let items = [createItem(), createItem()]\n\n  return () => (\n    <div\n      mix={[\n        css({\n          display: 'flex',\n          flexDirection: 'column',\n          gap: 10,\n          width: 200,\n          alignSelf: 'flex-start',\n        }),\n      ]}\n    >\n      <div mix={[css({ display: 'flex', gap: 8, marginBottom: 8 })]}>\n        <button\n          mix={[\n            css({\n              flex: 1,\n              padding: '8px 12px',\n              border: 'none',\n              borderRadius: 6,\n              backgroundColor: '#10b981',\n              color: 'white',\n              cursor: 'pointer',\n              fontWeight: 500,\n              '&:hover': { backgroundColor: '#059669' },\n            }),\n            on('click', () => {\n              items.unshift(createItem())\n              handle.update()\n            }),\n          ]}\n        >\n          Add\n        </button>\n        <button\n          mix={[\n            css({\n              flex: 1,\n              padding: '8px 12px',\n              border: 'none',\n              borderRadius: 6,\n              backgroundColor: '#6366f1',\n              color: 'white',\n              cursor: 'pointer',\n              fontWeight: 500,\n              '&:hover': { backgroundColor: '#4f46e5' },\n            }),\n            on('click', () => {\n              // Shuffle the array\n              for (let i = items.length - 1; i > 0; i--) {\n                let j = Math.floor(Math.random() * (i + 1))\n                ;[items[i], items[j]] = [items[j], items[i]]\n              }\n              handle.update()\n            }),\n          ]}\n        >\n          Shuffle\n        </button>\n      </div>\n      {items.map((item) => (\n        <div\n          key={item.id}\n          mix={[\n            animateEntrance(),\n            animateLayout(),\n            css({\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'space-between',\n              padding: '12px 16px',\n              backgroundColor: '#f1f5f9',\n              borderRadius: 8,\n              fontSize: 14,\n              fontWeight: 500,\n              color: '#334155',\n            }),\n          ]}\n        >\n          <span>{item.label}</span>\n          <button\n            mix={[\n              css({\n                width: 20,\n                height: 20,\n                padding: 0,\n                border: 'none',\n                borderRadius: 4,\n                backgroundColor: 'transparent',\n                color: '#94a3b8',\n                cursor: 'pointer',\n                display: 'flex',\n                alignItems: 'center',\n                justifyContent: 'center',\n                '&:hover': { backgroundColor: '#e2e8f0', color: '#64748b' },\n              }),\n              on('click', () => {\n                items = items.filter((i) => i.id !== item.id)\n                handle.update()\n              }),\n            ]}\n          >\n            <svg\n              width=\"12\"\n              height=\"12\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2.5\"\n            >\n              <path d=\"M18 6L6 18M6 6l12 12\" />\n            </svg>\n          </button>\n        </div>\n      ))}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/enter.tsx",
    "content": "import { animateEntrance, css, spring } from 'remix/component'\n\nexport function EnterAnimation() {\n  return () => (\n    <div\n      mix={[\n        css({\n          width: 100,\n          height: 100,\n          backgroundColor: '#dd00ee',\n          borderRadius: '50%',\n        }),\n        animateEntrance({\n          opacity: 0,\n          transform: 'scale(0)',\n          ...spring({ duration: 400, bounce: 0.5 }),\n        }),\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/entry.tsx",
    "content": "import { createRoot, css, on, type Handle, type RemixNode } from 'remix/component'\nimport { DefaultAnimate } from './default-animate.tsx'\nimport { EnterAnimation } from './enter.tsx'\nimport { ExitAnimation } from './exit.tsx'\nimport { Press } from './press.tsx'\nimport { HTMLContent } from './html-content.tsx'\nimport { Keyframes } from './keyframes.tsx'\nimport { InterruptibleKeyframes } from './interruptible-keyframes.tsx'\nimport { RollingSquare } from './rolling-square.tsx'\nimport { Rotate } from './rotate.tsx'\nimport { TransitionOptions } from './transition-options.tsx'\nimport { Cube } from './cube.tsx'\nimport { SharedLayout } from './shared-layout.tsx'\nimport { AspectRatio } from './aspect-ratio.tsx'\nimport { BouncySwitch } from './bouncy-switch.tsx'\nimport { ColorInterpolation } from './color-interpolation.tsx'\nimport { FlipToggle } from './flip-toggle.tsx'\nimport { Reordering } from './reordering.tsx'\nimport { MultiStateBadge } from './multi-state-badge.tsx'\nimport { HoldToConfirm } from './hold-to-confirm.tsx'\nimport { MaterialRipple } from './material-ripple.tsx'\n\nfunction Tile(handle: Handle) {\n  let remountKey = 0\n\n  return ({ title, children, notes }: { title: string; children: RemixNode; notes?: string }) => (\n    <div\n      mix={[\n        css({\n          backgroundColor: 'white',\n          padding: '40px',\n          borderRadius: 12,\n          boxShadow: '0 0 10px 0 rgba(0, 0, 0, 0.1)',\n          display: 'flex',\n          alignItems: 'center',\n          flexDirection: 'column',\n          gap: 12,\n          position: 'relative',\n        }),\n      ]}\n    >\n      <button\n        mix={[\n          css({\n            position: 'absolute',\n            bottom: 8,\n            right: 8,\n            width: 18,\n            height: 18,\n            padding: 0,\n            border: 'none',\n            background: 'transparent',\n            cursor: 'pointer',\n            opacity: 0.4,\n            '&:hover': {\n              opacity: 1,\n            },\n          }),\n          on('click', () => {\n            remountKey++\n            handle.update()\n          }),\n        ]}\n        title=\"Replay animation\"\n      >\n        <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n          <path d=\"M1 4v6h6M23 20v-6h-6\" />\n          <path d=\"M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15\" />\n        </svg>\n      </button>\n      <h3 mix={[css({ margin: 0 })]}>{title}</h3>\n      <div\n        key={remountKey}\n        mix={[\n          css({\n            flex: 1,\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n            minHeight: 280,\n          }),\n        ]}\n      >\n        {children}\n      </div>\n      {notes && (\n        <p\n          mix={[\n            css({\n              margin: 0,\n              fontSize: 12,\n              color: '#666',\n              textAlign: 'center',\n              maxWidth: '200px',\n            }),\n          ]}\n        >\n          {notes}\n        </p>\n      )}\n    </div>\n  )\n}\n\ncreateRoot(document.body).render(\n  <>\n    <h1 mix={[css({ marginBottom: 0, '& + p': { marginTop: 0 } })]}>Animations</h1>\n    <p>\n      Most animations are adapted from <a href=\"https://www.motion.dev\">Motion</a>. Thank you for\n      your work <a href=\"https://motion.dev/@matt\">Matt Perry</a>!\n    </p>\n    <div\n      mix={[\n        css({\n          display: 'grid',\n          gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',\n          gap: 24,\n          marginTop: 40,\n        }),\n      ]}\n    >\n      <Tile title=\"Default Animate\" notes=\"animateEntrance + animateLayout defaults\">\n        <DefaultAnimate />\n      </Tile>\n      <Tile title=\"Rolling Square\" notes=\"CSS transition with spring() timing function\">\n        <RollingSquare />\n      </Tile>\n      <Tile title=\"Enter Animation\" notes=\"animateEntrance() with spring physics\">\n        <EnterAnimation />\n      </Tile>\n      <Tile title=\"Exit Animation\" notes=\"animateEntrance() + animateExit()\">\n        <ExitAnimation />\n      </Tile>\n      <Tile title=\"Press Interaction\" notes=\"CSS transition + pressEvents.down/up events\">\n        <Press />\n      </Tile>\n      <Tile title=\"HTML Content\" notes=\"rAF loop with spring iterator for text\">\n        <HTMLContent />\n      </Tile>\n      <Tile title=\"Keyframes\" notes=\"CSS @keyframes with infinite loop\">\n        <Keyframes />\n      </Tile>\n      <Tile title=\"Interruptible Keyframes\" notes=\"Web Animations API with commitStyles()\">\n        <InterruptibleKeyframes />\n      </Tile>\n      <Tile title=\"Rotate\" notes=\"CSS @keyframes (one-shot)\">\n        <Rotate />\n      </Tile>\n      <Tile title=\"Transition Options\" notes=\"animateEntrance() with cubic-bezier + delay\">\n        <TransitionOptions />\n      </Tile>\n      <Tile title=\"3D Cube\" notes=\"rAF loop with direct style manipulation\">\n        <Cube />\n      </Tile>\n      <Tile title=\"Shared Layout\" notes=\"CSS Grid overlap for simultaneous enter/exit\">\n        <SharedLayout />\n      </Tile>\n      <Tile title=\"Aspect Ratio\">\n        <AspectRatio />\n      </Tile>\n      <Tile title=\"Bouncy Switch\" notes=\"Spring up, bounce down with CSS linear()\">\n        <BouncySwitch />\n      </Tile>\n      <Tile title=\"FLIP Toggle\" notes=\"animateLayout() with interruptible WAAPI\">\n        <FlipToggle />\n      </Tile>\n      <Tile title=\"Reordering\" notes=\"animateLayout() with auto-shuffling list\">\n        <Reordering />\n      </Tile>\n      <Tile title=\"Color Interpolation\" notes=\"sRGB vs OKLCH color space\">\n        <ColorInterpolation />\n      </Tile>\n      <Tile title=\"Multi-State Badge\" notes=\"Animated icon/label swap with WAAPI shake\">\n        <MultiStateBadge />\n      </Tile>\n      <Tile title=\"Hold to Confirm\" notes=\"Custom interaction with progress tracking\">\n        <HoldToConfirm />\n      </Tile>\n      <Tile title=\"Material Ripple\" notes=\"Pointer-tracked ripples with enter/exit animations\">\n        <MaterialRipple />\n      </Tile>\n    </div>\n  </>,\n)\n"
  },
  {
    "path": "packages/component/demos/animation/exit.tsx",
    "content": "import type { Handle } from 'remix/component'\nimport { animateEntrance, animateExit, css, on, spring } from 'remix/component'\n\nexport function ExitAnimation(handle: Handle) {\n  let isVisible = true\n\n  let shouldAnimate = false\n  handle.queueTask(() => {\n    shouldAnimate = true\n  })\n\n  return () => (\n    <div\n      mix={[\n        css({\n          display: 'flex',\n          flexDirection: 'column',\n          width: '100px',\n          height: '160px',\n          position: 'relative',\n        }),\n      ]}\n    >\n      {isVisible && (\n        <div\n          key=\"exit-animation\"\n          mix={[\n            css({\n              width: '100px',\n              height: '100px',\n              backgroundColor: '#0cdcf7',\n              borderRadius: '10px',\n            }),\n            animateEntrance(\n              shouldAnimate && {\n                opacity: 0,\n                transform: 'scale(0)',\n                ...spring('snappy'),\n              },\n            ),\n            animateExit({\n              opacity: 0,\n              transform: 'scale(0)',\n              ...spring(),\n            }),\n          ]}\n        />\n      )}\n      <button\n        mix={[\n          css({\n            backgroundColor: '#0cdcf7',\n            borderRadius: '10px',\n            padding: '10px 20px',\n            color: '#0f1115',\n            border: 'none',\n            cursor: 'pointer',\n            position: 'absolute',\n            bottom: 0,\n            left: 0,\n            right: 0,\n            transition: `transform 100ms ease-in-out`,\n            '&:active': {\n              transform: 'translateY(1px)',\n            },\n          }),\n          on('click', () => {\n            isVisible = !isVisible\n            handle.update()\n          }),\n        ]}\n      >\n        {isVisible ? 'Hide' : 'Show'}\n      </button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/flip-toggle.tsx",
    "content": "import { animateLayout, css, on, type Handle } from 'remix/component'\n\nexport function FlipToggle(handle: Handle) {\n  let isOn = false\n\n  return () => (\n    <button\n      mix={[\n        css({\n          width: 90,\n          height: 50,\n          backgroundColor: 'rgba(153, 17, 255, 0.2)',\n          borderRadius: 50,\n          cursor: 'pointer',\n          display: 'flex',\n          padding: 10,\n          border: 'none',\n        }),\n        on('click', () => {\n          isOn = !isOn\n          handle.update()\n        }),\n      ]}\n      style={{\n        // The actual layout property that changes\n        justifyContent: isOn ? 'flex-start' : 'flex-end',\n      }}\n    >\n      <div\n        mix={[\n          css({\n            width: 30,\n            height: 30,\n            backgroundColor: '#9911ff',\n            borderRadius: '50%',\n          }),\n          animateLayout({\n            duration: 200,\n            easing: 'ease-in-out',\n          }),\n        ]}\n      />\n    </button>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/hold-to-confirm.tsx",
    "content": "import {\n  animateEntrance,\n  animateExit,\n  createMixin,\n  css,\n  on,\n  pressEvents,\n  spring,\n  type Handle,\n} from 'remix/component'\n\n// Demo\nlet buttonExitAnimation = {\n  opacity: 0,\n  transform: 'scale(1.15)',\n  duration: 100,\n  easing: 'ease-in',\n}\n\nlet confirmationEnterAnimation = {\n  opacity: 0,\n  transform: 'scale(0.9)',\n  duration: 200,\n  easing: 'ease-out',\n}\n\nexport function HoldToConfirm(handle: Handle) {\n  let confirmed = false\n\n  return () => (\n    <div\n      mix={[\n        css({\n          display: 'grid',\n          placeItems: 'center',\n          minHeight: 140,\n          // so children can animate in the same position\n          '& > *': { gridArea: '1 / 1' },\n        }),\n      ]}\n    >\n      {!confirmed && (\n        <HoldButton\n          key=\"hold-button\"\n          onConfirm={() => {\n            confirmed = true\n            handle.update()\n          }}\n        />\n      )}\n      {confirmed && (\n        <Confirmation\n          key=\"confirmed\"\n          onReset={() => {\n            confirmed = false\n            handle.update()\n          }}\n        />\n      )}\n    </div>\n  )\n}\n\nfunction HoldButton(handle: Handle) {\n  let confirming = false\n\n  return (props: { onConfirm: () => void }) => (\n    <button\n      mix={[\n        animateExit(buttonExitAnimation),\n        css({\n          position: 'relative',\n          overflow: 'hidden',\n          width: 200,\n          height: 56,\n          border: 'none',\n          borderRadius: 12,\n          backgroundColor: '#dc2626',\n          color: 'white',\n          fontSize: 16,\n          fontWeight: 600,\n          cursor: 'pointer',\n          userSelect: 'none',\n          transition: 'transform 150ms ease',\n          '&:focus': {\n            outline: '3px solid rgba(220, 38, 38, 0.4)',\n            outlineOffset: 2,\n          },\n          '&:active': {\n            transform: 'scale(0.98)',\n          },\n        }),\n        confirmPress(),\n        on(confirmPress.start, () => {\n          confirming = true\n          handle.update()\n        }),\n        on(confirmPress.cancel, () => {\n          if (!confirming) return\n          confirming = false\n          handle.update()\n        }),\n        on(confirmPress.end, () => {\n          props.onConfirm()\n        }),\n      ]}\n    >\n      <div\n        mix={[\n          css({\n            position: 'absolute',\n            inset: 0,\n            backgroundColor: 'rgba(0, 0, 0, 0.3)',\n            transformOrigin: 'left',\n          }),\n        ]}\n        style={{\n          transform: confirming ? 'scaleX(1)' : 'scaleX(0)',\n          transition: confirming\n            ? `transform ${PRESS_CONFIRM_TIME}ms linear`\n            : `transform ${spring({ duration: 100, bounce: 0 })}`,\n        }}\n      />\n\n      <span\n        mix={[\n          css({\n            position: 'relative',\n            zIndex: 1,\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n            gap: 8,\n          }),\n        ]}\n      >\n        <TrashIcon />\n        Hold to Delete\n      </span>\n    </button>\n  )\n}\n\nfunction Confirmation() {\n  return (props: { onReset: () => void }) => (\n    <div\n      mix={[\n        animateEntrance(confirmationEnterAnimation),\n        css({\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          gap: 16,\n        }),\n      ]}\n    >\n      <div\n        mix={[\n          css({\n            display: 'flex',\n            alignItems: 'center',\n            gap: 8,\n            color: '#22c55e',\n            fontSize: 18,\n            fontWeight: 600,\n          }),\n        ]}\n      >\n        <CheckIcon />\n        Deleted\n      </div>\n\n      <button\n        mix={[\n          css({\n            padding: '8px 16px',\n            border: '1px solid #e2e8f0',\n            borderRadius: 8,\n            backgroundColor: 'white',\n            color: '#64748b',\n            fontSize: 14,\n            cursor: 'pointer',\n            transition: 'all 150ms ease',\n            '&:hover': {\n              backgroundColor: '#f8fafc',\n              borderColor: '#cbd5e1',\n            },\n          }),\n          on('click', props.onReset),\n        ]}\n      >\n        Reset Demo\n      </button>\n    </div>\n  )\n}\n\nfunction TrashIcon() {\n  return () => (\n    <svg\n      width={16}\n      height={16}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <polyline points=\"3 6 5 6 21 6\" />\n      <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n    </svg>\n  )\n}\n\nfunction CheckIcon() {\n  return () => (\n    <svg\n      width={16}\n      height={16}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2.5}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <polyline points=\"20 6 9 17 4 12\" />\n    </svg>\n  )\n}\n\nconst PRESS_CONFIRM_TIME = 2000\nlet pressConfirmStartEventType = 'demo:press-confirm-start' as const\nlet pressConfirmCancelEventType = 'demo:press-confirm-cancel' as const\nlet pressConfirmEndEventType = 'demo:press-confirm-end' as const\n\ndeclare global {\n  interface HTMLElementEventMap {\n    [pressConfirmStartEventType]: Event\n    [pressConfirmCancelEventType]: Event\n    [pressConfirmEndEventType]: Event\n  }\n}\n\nlet baseConfirmPress = createMixin<HTMLElement>((handle) => {\n  let timer = 0\n\n  let clearTimer = () => {\n    if (timer) {\n      clearTimeout(timer)\n      timer = 0\n    }\n  }\n\n  handle.addEventListener('remove', clearTimer)\n\n  return (props) => (\n    <handle.element\n      {...props}\n      mix={[\n        pressEvents(),\n        on(pressEvents.down, (event) => {\n          let target = event.currentTarget\n          clearTimer()\n          target.dispatchEvent(new Event(pressConfirmStartEventType, { bubbles: true }))\n          timer = window.setTimeout(() => {\n            target.dispatchEvent(new Event(pressConfirmEndEventType, { bubbles: true }))\n          }, PRESS_CONFIRM_TIME)\n        }),\n        on(pressEvents.up, (event) => {\n          clearTimer()\n          event.currentTarget.dispatchEvent(\n            new Event(pressConfirmCancelEventType, { bubbles: true }),\n          )\n        }),\n        on(pressEvents.cancel, (event) => {\n          clearTimer()\n          event.currentTarget.dispatchEvent(\n            new Event(pressConfirmCancelEventType, { bubbles: true }),\n          )\n        }),\n      ]}\n    />\n  )\n})\n\ntype ConfirmPressMixin = typeof baseConfirmPress & {\n  readonly start: typeof pressConfirmStartEventType\n  readonly cancel: typeof pressConfirmCancelEventType\n  readonly end: typeof pressConfirmEndEventType\n}\n\nlet confirmPress: ConfirmPressMixin = Object.assign(baseConfirmPress, {\n  start: pressConfirmStartEventType,\n  cancel: pressConfirmCancelEventType,\n  end: pressConfirmEndEventType,\n})\n"
  },
  {
    "path": "packages/component/demos/animation/html-content.tsx",
    "content": "import type { Handle } from 'remix/component'\nimport { css, spring } from 'remix/component'\n\nexport function HTMLContent(handle: Handle) {\n  let count = 0\n  let from = 0\n  let to = 100\n\n  let counter = spring({ duration: 3000, bounce: 0 })\n\n  function animate() {\n    if (handle.signal.aborted) return\n\n    let { value: t, done } = counter.next()\n    if (done) return\n\n    count = Math.round(from + (to - from) * t)\n    handle.update()\n    requestAnimationFrame(animate)\n  }\n\n  handle.queueTask(() => {\n    requestAnimationFrame(animate)\n  })\n\n  return () => (\n    <pre\n      mix={[\n        css({\n          fontSize: '64px',\n          margin: 0,\n          color: '#8df0cc',\n        }),\n      ]}\n    >\n      {count}\n    </pre>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Animation</title>\n    <style>\n      body {\n        font-family:\n          system-ui,\n          -apple-system,\n          sans-serif;\n        background-color: #eee;\n        padding: 20px;\n        margin: 0;\n        box-sizing: border-box;\n        color: #333;\n        line-height: 1.5;\n        font-size: 16px;\n        font-weight: 400;\n      }\n    </style>\n  </head>\n  <body>\n    <script type=\"module\" src=\"./entry.bundled.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/demos/animation/interruptible-keyframes.tsx",
    "content": "import type { Handle } from 'remix/component'\nimport { css, on, ref } from 'remix/component'\n\nexport function InterruptibleKeyframes(handle: Handle) {\n  let box: HTMLDivElement\n  let currentAnimation: Animation | null = null\n\n  function getCurrentScale(): number {\n    let matrix = new DOMMatrix(getComputedStyle(box).transform)\n    return matrix.a\n  }\n\n  function interruptAnimation() {\n    if (currentAnimation) {\n      currentAnimation.commitStyles()\n      currentAnimation.cancel()\n      currentAnimation = null\n    }\n  }\n\n  return () => (\n    <div\n      mix={[\n        ref((node) => (box = node)),\n        css({\n          width: 100,\n          height: 100,\n          backgroundColor: '#0cdcf7',\n          borderRadius: 5,\n        }),\n        on('pointerenter', () => {\n          interruptAnimation()\n          let startScale = getCurrentScale()\n\n          currentAnimation = box.animate(\n            [\n              { transform: `scale(${startScale})`, offset: 0, easing: 'ease-in-out' },\n              { transform: 'scale(1.1)', offset: 0.6, easing: 'ease-out' },\n              { transform: 'scale(1.6)', offset: 1 },\n            ],\n            {\n              duration: 500,\n              fill: 'forwards',\n            },\n          )\n        }),\n        on('pointerleave', () => {\n          interruptAnimation()\n          let startScale = getCurrentScale()\n\n          currentAnimation = box.animate(\n            [{ transform: `scale(${startScale})` }, { transform: 'scale(1)' }],\n            {\n              duration: 300,\n              easing: 'ease-out',\n              fill: 'forwards',\n            },\n          )\n        }),\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/keyframes.tsx",
    "content": "import { css } from 'remix/component'\n\nexport function Keyframes() {\n  return () => (\n    <div mix={[css({ padding: 60 })]}>\n      <div\n        mix={[\n          css({\n            width: 100,\n            height: 100,\n            backgroundColor: '#888',\n            borderRadius: 5,\n            // 2s animation + 1s delay = 3s total cycle\n            // Original times [0, 0.2, 0.5, 0.8, 1] scaled to first 66.67% of animation\n            '@keyframes box-animation': {\n              '0%': { transform: 'scale(1) rotate(0deg)', borderRadius: '0%' },\n              '13.33%': { transform: 'scale(2) rotate(0deg)', borderRadius: '0%' },\n              '33.33%': { transform: 'scale(2) rotate(180deg)', borderRadius: '50%' },\n              '53.33%': { transform: 'scale(1) rotate(180deg)', borderRadius: '50%' },\n              '66.67%': { transform: 'scale(1) rotate(0deg)', borderRadius: '0%' },\n              '100%': { transform: 'scale(1) rotate(0deg)', borderRadius: '0%' },\n            },\n            animation: 'box-animation 3s ease-in-out infinite',\n          }),\n        ]}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/material-ripple.tsx",
    "content": "import type { Handle } from 'remix/component'\nimport { animateEntrance, animateExit, css, on, pressEvents, ref } from 'remix/component'\n\ntype Ripple = {\n  id: number\n  x: number\n  y: number\n  size: number\n}\n\nexport function MaterialRipple(handle: Handle) {\n  let ripples: Ripple[] = []\n  let idCounter = 0\n  let buttonEl: HTMLButtonElement | null = null\n\n  function createRipple(originX: number, originY: number) {\n    if (!buttonEl) return\n\n    let rect = buttonEl.getBoundingClientRect()\n    let localX = originX - rect.left\n    let localY = originY - rect.top\n    let dx = Math.max(localX, rect.width - localX)\n    let dy = Math.max(localY, rect.height - localY)\n    let radius = Math.sqrt(dx * dx + dy * dy)\n    let size = radius * 2\n\n    let id = ++idCounter\n    ripples = [...ripples, { id, x: localX, y: localY, size }]\n    handle.update()\n  }\n\n  function removeAllRipples() {\n    if (ripples.length > 0) {\n      ripples = []\n      handle.update()\n    }\n  }\n\n  return () => (\n    <button\n      mix={[\n        ref((el) => {\n          buttonEl = el\n        }),\n        css({\n          position: 'relative',\n          display: 'inline-flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          padding: '10px 20px',\n          borderRadius: 4,\n          textTransform: 'uppercase',\n          backgroundColor: 'transparent',\n          color: '#7c3aed',\n          border: '1px solid #7c3aed',\n          userSelect: 'none',\n          cursor: 'pointer',\n          overflow: 'hidden',\n          letterSpacing: '0.2px',\n          WebkitTapHighlightColor: 'transparent',\n          transition: 'border-color 200ms linear, background-color 200ms linear',\n          '&:hover': {\n            borderColor: '#6d28d9',\n            backgroundColor: '#7c3aed20',\n          },\n          '&:focus-visible': {\n            outline: '2px solid #7c3aed80',\n            outlineOffset: 2,\n          },\n        }),\n        pressEvents(),\n        on(pressEvents.down, (event) => {\n          if (!buttonEl) return\n          let rect = buttonEl.getBoundingClientRect()\n          let x = event.clientX || rect.left + rect.width / 2\n          let y = event.clientY || rect.top + rect.height / 2\n          createRipple(x, y)\n        }),\n        on(pressEvents.up, removeAllRipples),\n        on(pressEvents.cancel, removeAllRipples),\n      ]}\n    >\n      Click me\n      <span\n        aria-hidden=\"true\"\n        mix={[\n          css({\n            position: 'absolute',\n            inset: 0,\n            overflow: 'hidden',\n            borderRadius: 'inherit',\n            pointerEvents: 'none',\n          }),\n        ]}\n      >\n        {ripples.map((ripple) => (\n          // Outer span: handles exit (fade out)\n          <span\n            key={ripple.id}\n            mix={[\n              css({\n                position: 'absolute',\n                borderRadius: '50%',\n              }),\n              animateExit({\n                opacity: 0,\n                duration: 550,\n                easing: 'ease-out',\n              }),\n            ]}\n            style={{\n              width: ripple.size,\n              height: ripple.size,\n              left: ripple.x - ripple.size / 2,\n              top: ripple.y - ripple.size / 2,\n            }}\n          >\n            {/* Inner span: handles enter (scale) so it doesn't get reversed when removed */}\n            <span\n              mix={[\n                css({\n                  display: 'block',\n                  width: '100%',\n                  height: '100%',\n                  borderRadius: 'inherit',\n                  backgroundColor: 'currentColor',\n                  opacity: 0.4,\n                }),\n                animateEntrance({\n                  opacity: 0,\n                  transform: 'scale(0)',\n                  duration: 300,\n                  easing: 'ease-out',\n                }),\n              ]}\n            />\n          </span>\n        ))}\n      </span>\n    </button>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/mixin-presence-list.tsx",
    "content": "import type { Handle } from 'remix/component'\nimport { animateEntrance, animateExit, css, on } from 'remix/component'\n\nlet nextId = 1\nfunction createItem() {\n  return { id: nextId++, label: `Row ${nextId - 1}` }\n}\n\nexport function MixinPresenceList(handle: Handle) {\n  let items = [createItem(), createItem(), createItem()]\n\n  return () => (\n    <div\n      mix={[\n        css({\n          display: 'flex',\n          flexDirection: 'column',\n          gap: 10,\n          width: 220,\n        }),\n      ]}\n    >\n      <div mix={[css({ display: 'flex', gap: 8 })]}>\n        <button\n          mix={[\n            css({\n              flex: 1,\n              padding: '8px 10px',\n              border: 'none',\n              borderRadius: 6,\n              backgroundColor: '#0ea5e9',\n              color: 'white',\n              cursor: 'pointer',\n              '&:hover': { backgroundColor: '#0284c7' },\n            }),\n            on('click', () => {\n              items.unshift(createItem())\n              handle.update()\n            }),\n          ]}\n        >\n          Add\n        </button>\n        <button\n          mix={[\n            css({\n              flex: 1,\n              padding: '8px 10px',\n              border: 'none',\n              borderRadius: 6,\n              backgroundColor: '#ef4444',\n              color: 'white',\n              cursor: 'pointer',\n              '&:hover': { backgroundColor: '#dc2626' },\n            }),\n            on('click', () => {\n              items = items.slice(0, Math.max(0, items.length - 1))\n              handle.update()\n            }),\n          ]}\n        >\n          Remove\n        </button>\n      </div>\n      {items.map((item) => (\n        <div\n          key={String(item.id)}\n          mix={[\n            animateEntrance({\n              opacity: 0,\n              transform: 'translateY(-10px) scale(0.98)',\n              duration: 220,\n              easing: 'ease-out',\n            }),\n            animateExit({\n              opacity: 0,\n              transform: 'translateY(10px) scale(0.98)',\n              duration: 180,\n              easing: 'ease-in',\n            }),\n            css({\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'space-between',\n              padding: '10px 12px',\n              borderRadius: 8,\n              backgroundColor: '#f8fafc',\n              color: '#334155',\n              border: '1px solid #e2e8f0',\n            }),\n          ]}\n        >\n          <span>{item.label}</span>\n          <button\n            mix={[\n              css({\n                width: 24,\n                height: 24,\n                padding: 0,\n                border: 'none',\n                borderRadius: 4,\n                backgroundColor: 'transparent',\n                color: '#64748b',\n                cursor: 'pointer',\n                '&:hover': { backgroundColor: '#e2e8f0' },\n              }),\n              on('click', () => {\n                items = items.filter((entry) => entry.id !== item.id)\n                handle.update()\n              }),\n            ]}\n          >\n            ×\n          </button>\n        </div>\n      ))}\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/mixin-reclaim.tsx",
    "content": "import type { Handle } from 'remix/component'\nimport { animateEntrance, animateExit, css, on } from 'remix/component'\n\nexport function MixinReclaim(handle: Handle) {\n  let visible = true\n  let interruptTimer: number | undefined\n\n  function clearInterruptTimer() {\n    if (interruptTimer === undefined) return\n    window.clearTimeout(interruptTimer)\n    interruptTimer = undefined\n  }\n\n  function scheduleInterrupt() {\n    clearInterruptTimer()\n    visible = false\n    handle.update()\n    interruptTimer = window.setTimeout(() => {\n      visible = true\n      handle.update()\n      interruptTimer = undefined\n    }, 140)\n  }\n\n  return () => (\n    <div mix={[css({ display: 'flex', flexDirection: 'column', gap: 12, width: 240 })]}>\n      <div mix={[css({ display: 'flex', gap: 8 })]}>\n        <button\n          mix={[\n            css({\n              flex: 1,\n              padding: '8px 10px',\n              border: 'none',\n              borderRadius: 6,\n              backgroundColor: '#10b981',\n              color: 'white',\n              cursor: 'pointer',\n              '&:hover': { backgroundColor: '#059669' },\n            }),\n            on('click', () => {\n              clearInterruptTimer()\n              visible = true\n              handle.update()\n            }),\n          ]}\n        >\n          Show\n        </button>\n        <button\n          mix={[\n            css({\n              flex: 1,\n              padding: '8px 10px',\n              border: 'none',\n              borderRadius: 6,\n              backgroundColor: '#f59e0b',\n              color: 'white',\n              cursor: 'pointer',\n              '&:hover': { backgroundColor: '#d97706' },\n            }),\n            on('click', scheduleInterrupt),\n          ]}\n        >\n          Interrupt\n        </button>\n      </div>\n\n      <button\n        mix={[\n          css({\n            padding: '8px 10px',\n            border: 'none',\n            borderRadius: 6,\n            backgroundColor: '#ef4444',\n            color: 'white',\n            cursor: 'pointer',\n            '&:hover': { backgroundColor: '#dc2626' },\n          }),\n          on('click', () => {\n            clearInterruptTimer()\n            visible = false\n            handle.update()\n          }),\n        ]}\n      >\n        Hide\n      </button>\n\n      <div mix={[css({ minHeight: 100, display: 'grid', placeItems: 'center' })]}>\n        {visible && (\n          <div\n            key=\"reclaim-card\"\n            mix={[\n              animateEntrance({\n                opacity: 0,\n                transform: 'translateY(12px) scale(0.94)',\n                duration: 260,\n                easing: 'ease-out',\n              }),\n              animateExit({\n                opacity: 0,\n                transform: 'translateY(-12px) scale(0.94)',\n                duration: 260,\n                easing: 'ease-in',\n              }),\n              css({\n                width: 200,\n                padding: '14px 16px',\n                borderRadius: 10,\n                background: 'linear-gradient(135deg, #7c3aed, #3b82f6)',\n                color: 'white',\n                textAlign: 'center',\n                fontWeight: 600,\n              }),\n            ]}\n          >\n            Reclaim Me Mid-Exit\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/multi-state-badge.tsx",
    "content": "import type { Handle } from 'remix/component'\nimport { animateEntrance, animateExit, css, on, ref } from 'remix/component'\nimport { spring } from '../../src/lib/spring.ts'\n\nconst STATES = {\n  idle: 'Start',\n  processing: 'Processing',\n  success: 'Done',\n  error: 'Something went wrong',\n} as const\n\ntype State = keyof typeof STATES\n\nfunction getNextState(state: State): State {\n  let states = Object.keys(STATES) as State[]\n  let nextIndex = (states.indexOf(state) + 1) % states.length\n  return states[nextIndex]\n}\n\nconst ICON_SIZE = 20\nconst STROKE_WIDTH = 1.5\nconst VIEW_BOX_SIZE = 24\n\nlet iconEnterAnimation = {\n  transform: 'translateY(-40px) scale(0.5)',\n  filter: 'blur(6px)',\n  duration: 150,\n  easing: 'ease-out',\n}\n\nlet iconExitAnimation = {\n  transform: 'translateY(40px) scale(0.5)',\n  filter: 'blur(6px)',\n  duration: 150,\n  easing: 'ease-in',\n}\n\nexport function MultiStateBadge(handle: Handle) {\n  let state: State = 'idle'\n\n  return () => (\n    <div\n      mix={[\n        css({\n          display: 'flex',\n          flexDirection: 'column',\n          justifyContent: 'space-between',\n          alignItems: 'center',\n          padding: 16,\n          minHeight: 80,\n        }),\n      ]}\n    >\n      <button\n        mix={[\n          css({\n            background: 'none',\n            border: 'none',\n            cursor: 'pointer',\n            padding: 0,\n          }),\n          on('click', () => {\n            state = getNextState(state)\n            handle.update()\n          }),\n        ]}\n      >\n        <Badge state={state} />\n      </button>\n    </div>\n  )\n}\n\nfunction Badge(handle: Handle) {\n  let badgeEl: HTMLDivElement\n  let prevState: State | null = null\n\n  return (props: { state: State }) => {\n    // Trigger shake/scale animations on state change\n    if (prevState !== null && prevState !== props.state) {\n      handle.queueTask(() => {\n        if (props.state === 'error') {\n          badgeEl.animate(\n            {\n              transform: [\n                'translateX(0)',\n                'translateX(-6px)',\n                'translateX(6px)',\n                'translateX(-6px)',\n                'translateX(0)',\n              ],\n            },\n            { duration: 300, easing: 'ease-in-out', delay: 100 },\n          )\n        } else if (props.state === 'success') {\n          badgeEl.animate(\n            { transform: ['scale(1)', 'scale(1.2)', 'scale(1)'] },\n            { duration: 300, easing: 'ease-in-out' },\n          )\n        }\n      })\n    }\n    prevState = props.state\n\n    return (\n      <div\n        mix={[\n          ref((node) => (badgeEl = node)),\n          css({\n            backgroundColor: '#e2e8f0',\n            color: '#0f1115',\n            display: 'flex',\n            overflow: 'hidden',\n            alignItems: 'center',\n            justifyContent: 'center',\n            padding: '12px 20px',\n            fontSize: 16,\n            borderRadius: 999,\n            willChange: 'transform, filter',\n            transition: `gap ${spring('snappy')}`,\n          }),\n        ]}\n        style={{ gap: props.state === 'idle' ? '0px' : '8px' }}\n      >\n        <Icon state={props.state} />\n        <Label state={props.state} />\n      </div>\n    )\n  }\n}\n\nfunction Icon() {\n  return (props: { state: State }) => (\n    <span\n      mix={[\n        css({\n          height: ICON_SIZE,\n          position: 'relative',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          overflow: 'hidden',\n          transition: `width ${spring({ duration: 200, bounce: 0.2 })}`,\n        }),\n      ]}\n      style={{ width: props.state === 'idle' ? 0 : ICON_SIZE }}\n    >\n      {props.state === 'processing' && (\n        <span\n          key=\"loader\"\n          mix={[\n            css({ position: 'absolute', left: 0, top: 0 }),\n            animateEntrance(iconEnterAnimation),\n            animateExit(iconExitAnimation),\n          ]}\n        >\n          <Loader />\n        </span>\n      )}\n      {props.state === 'success' && (\n        <span\n          key=\"check\"\n          mix={[\n            css({ position: 'absolute', left: 0, top: 0 }),\n            animateEntrance(iconEnterAnimation),\n            animateExit(iconExitAnimation),\n          ]}\n        >\n          <Check />\n        </span>\n      )}\n      {props.state === 'error' && (\n        <span\n          key=\"x\"\n          mix={[\n            css({ position: 'absolute', left: 0, top: 0 }),\n            animateEntrance(iconEnterAnimation),\n            animateExit(iconExitAnimation),\n          ]}\n        >\n          <X />\n        </span>\n      )}\n    </span>\n  )\n}\n\nfunction Loader() {\n  return () => (\n    <div\n      mix={[\n        ref((node) => {\n          node.animate(\n            { transform: ['rotate(0deg)', 'rotate(360deg)'] },\n            { duration: 1000, iterations: Infinity },\n          )\n        }),\n        css({\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          width: ICON_SIZE,\n          height: ICON_SIZE,\n        }),\n      ]}\n    >\n      <svg\n        width={ICON_SIZE}\n        height={ICON_SIZE}\n        viewBox={`0 0 ${VIEW_BOX_SIZE} ${VIEW_BOX_SIZE}`}\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={STROKE_WIDTH}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      >\n        <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n      </svg>\n    </div>\n  )\n}\n\nfunction Check() {\n  return () => (\n    <svg\n      width={ICON_SIZE}\n      height={ICON_SIZE}\n      viewBox={`0 0 ${VIEW_BOX_SIZE} ${VIEW_BOX_SIZE}`}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={STROKE_WIDTH}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <polyline\n        points=\"4 12 9 17 20 6\"\n        mix={[\n          ref((node) => {\n            let length = node.getTotalLength()\n            node.style.strokeDasharray = `${length}`\n            node.style.strokeDashoffset = `${length}`\n            node.animate(\n              { strokeDashoffset: [length, 0] },\n              { ...spring({ duration: 300, bounce: 0.1 }), fill: 'forwards' },\n            )\n          }),\n        ]}\n      />\n    </svg>\n  )\n}\n\nfunction X() {\n  return () => (\n    <svg\n      width={ICON_SIZE}\n      height={ICON_SIZE}\n      viewBox={`0 0 ${VIEW_BOX_SIZE} ${VIEW_BOX_SIZE}`}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={STROKE_WIDTH}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <line\n        x1=\"6\"\n        y1=\"6\"\n        x2=\"18\"\n        y2=\"18\"\n        mix={[\n          ref((node) => {\n            let length = node.getTotalLength()\n            node.style.strokeDasharray = `${length}`\n            node.style.strokeDashoffset = `${length}`\n            node.animate(\n              { strokeDashoffset: [length, 0] },\n              { ...spring({ duration: 300, bounce: 0.1 }), fill: 'forwards' },\n            )\n          }),\n        ]}\n      />\n      <line\n        x1=\"18\"\n        y1=\"6\"\n        x2=\"6\"\n        y2=\"18\"\n        mix={[\n          ref((node) => {\n            let length = node.getTotalLength()\n            node.style.strokeDasharray = `${length}`\n            node.style.strokeDashoffset = `${length}`\n            node.animate(\n              { strokeDashoffset: [length, 0] },\n              { ...spring({ duration: 300, bounce: 0.1 }), delay: 100, fill: 'forwards' },\n            )\n          }),\n        ]}\n      />\n    </svg>\n  )\n}\n\nfunction Label(handle: Handle) {\n  let measureEl: HTMLSpanElement\n  let labelWidth = 0\n  let labelHeight = 0\n\n  // Don't animate the label on initial render\n  let isFirstRender = true\n  handle.queueTask(() => {\n    isFirstRender = false\n  })\n\n  return (props: { state: State }) => {\n    // Measure label dimensions after render\n    handle.queueTask(() => {\n      if (measureEl) {\n        let rect = measureEl.getBoundingClientRect()\n        if (rect.width !== labelWidth || rect.height !== labelHeight) {\n          labelWidth = rect.width\n          labelHeight = rect.height\n          handle.update()\n        }\n      }\n    })\n\n    let labelMix = [\n      animateExit({\n        transform: 'translateY(20px)',\n        opacity: 0,\n        filter: 'blur(10px)',\n        duration: 200,\n        easing: 'ease-in-out',\n      }),\n    ]\n\n    if (!isFirstRender) {\n      labelMix.unshift(\n        animateEntrance({\n          transform: 'translateY(-20px)',\n          opacity: 0,\n          filter: 'blur(10px)',\n          duration: 200,\n          easing: 'ease-in-out',\n        }),\n      )\n    }\n\n    return (\n      <span\n        mix={[\n          css({\n            position: 'relative',\n            display: 'inline-block',\n            transition: `width ${spring({ duration: 200, bounce: 0.1 })}`,\n          }),\n        ]}\n        style={{\n          width: labelWidth || 'auto',\n          height: labelHeight || 'auto',\n        }}\n      >\n        {/* Hidden measurement element */}\n        <span\n          mix={[\n            ref((node) => (measureEl = node)),\n            css({ position: 'absolute', visibility: 'hidden', whiteSpace: 'nowrap' }),\n          ]}\n        >\n          {STATES[props.state]}\n        </span>\n\n        {props.state === 'idle' && (\n          <span\n            key=\"idle\"\n            mix={[\n              css({ whiteSpace: 'nowrap', position: 'absolute', left: 0, top: 0 }),\n              ...labelMix,\n            ]}\n          >\n            {STATES.idle}\n          </span>\n        )}\n        {props.state === 'processing' && (\n          <span\n            key=\"processing\"\n            mix={[\n              css({ whiteSpace: 'nowrap', position: 'absolute', left: 0, top: 0 }),\n              ...labelMix,\n            ]}\n          >\n            {STATES.processing}\n          </span>\n        )}\n        {props.state === 'success' && (\n          <span\n            key=\"success\"\n            mix={[\n              css({ whiteSpace: 'nowrap', position: 'absolute', left: 0, top: 0 }),\n              ...labelMix,\n            ]}\n          >\n            {STATES.success}\n          </span>\n        )}\n        {props.state === 'error' && (\n          <span\n            key=\"error\"\n            mix={[\n              css({ whiteSpace: 'nowrap', position: 'absolute', left: 0, top: 0 }),\n              ...labelMix,\n            ]}\n          >\n            {STATES.error}\n          </span>\n        )}\n      </span>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/component/demos/animation/press.tsx",
    "content": "import { css, on, pressEvents, spring, type Handle } from 'remix/component'\n\nexport function Press(handle: Handle) {\n  let pressed = false\n  return () => (\n    <div\n      tabIndex={0}\n      mix={[\n        css({\n          width: 100,\n          height: 100,\n          backgroundColor: '#9911ff',\n          borderRadius: 5,\n          transition: `transform ${spring()}`,\n          '&:focus': {\n            outline: '4px solid rgba(0,120,255,0.7)',\n            outlineOffset: 1,\n          },\n          '&:hover, &:focus': {\n            transform: pressed ? 'scale(0.8)' : 'scale(1.2)',\n          },\n          // or use default browser :active but lose keyboard \"down\" press states\n          // '&:active': {\n          //   transform: 'scale(0.8)',\n          // },\n        }),\n        pressEvents(),\n        on(pressEvents.down, () => {\n          pressed = true\n          handle.update()\n        }),\n        on(pressEvents.up, () => {\n          pressed = false\n          handle.update()\n        }),\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/reordering.tsx",
    "content": "import { animateLayout, css, type Handle, spring } from 'remix/component'\n\nlet initialOrder = ['#ff0088', '#dd00ee', '#9911ff', '#0d63f8']\n\nfunction shuffle<T>(array: T[]): T[] {\n  let result = [...array]\n  for (let i = result.length - 1; i > 0; i--) {\n    let j = Math.floor(Math.random() * (i + 1))\n    ;[result[i], result[j]] = [result[j], result[i]]\n  }\n  return result\n}\n\nexport function Reordering(handle: Handle) {\n  let order = initialOrder\n\n  function scheduleNextShuffle() {\n    setTimeout(() => {\n      if (handle.signal.aborted) return\n      order = shuffle(order)\n      handle.update()\n      scheduleNextShuffle()\n    }, 1000)\n  }\n\n  scheduleNextShuffle()\n\n  return () => (\n    <ul\n      mix={[\n        css({\n          listStyle: 'none',\n          padding: 0,\n          margin: 0,\n          position: 'relative',\n          display: 'flex',\n          flexWrap: 'wrap',\n          gap: 10,\n          width: 220,\n          flexDirection: 'row',\n          justifyContent: 'center',\n          alignItems: 'center',\n        }),\n      ]}\n    >\n      {order.map((backgroundColor) => (\n        <li\n          key={backgroundColor}\n          mix={[\n            css({\n              width: 100,\n              height: 100,\n              borderRadius: 10,\n            }),\n            animateLayout({\n              ...spring({ duration: 600, bounce: 0.2 }),\n            }),\n          ]}\n          style={{ backgroundColor }}\n        />\n      ))}\n    </ul>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/rolling-square.tsx",
    "content": "import { type Handle } from 'remix/component'\nimport { css, on, spring } from 'remix/component'\n\nexport function RollingSquare(handle: Handle) {\n  let toggled = false\n\n  return () => (\n    <div\n      mix={[\n        css({\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          gap: '20px',\n          minWidth: '300px',\n        }),\n      ]}\n    >\n      <div\n        mix={[\n          css({\n            width: '80px',\n            height: '80px',\n            backgroundColor: '#8df0cc',\n            borderRadius: '10px',\n            transition: `transform ${spring({ duration: 500, bounce: 0.5 })}`,\n          }),\n        ]}\n        style={{\n          transform: toggled ? 'translateX(100%) rotate(180deg)' : 'translateX(-100%)',\n        }}\n      />\n      <button\n        mix={[\n          css({\n            backgroundColor: '#8df0cc',\n            color: '#0f1115',\n            borderRadius: '5px',\n            padding: '10px',\n            margin: '10px',\n            border: 'none',\n            cursor: 'pointer',\n          }),\n          on('click', () => {\n            toggled = !toggled\n            handle.update()\n          }),\n        ]}\n      >\n        Toggle position\n      </button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/rotate.tsx",
    "content": "import { css, spring } from 'remix/component'\n\nexport function Rotate() {\n  return () => (\n    <div\n      mix={[\n        css({\n          width: 100,\n          height: 100,\n          backgroundColor: '#ff0088',\n          borderRadius: 5,\n          '@keyframes rotate-demo': {\n            '0%': { transform: 'rotate(0deg)' },\n            '100%': { transform: 'rotate(360deg)' },\n          },\n          animation: `rotate-demo 1s ease-in-out 1`,\n        }),\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/shared-layout.tsx",
    "content": "import {\n  animateEntrance,\n  animateExit,\n  css,\n  on,\n  type Handle,\n  type Props,\n  type RemixNode,\n} from 'remix/component'\n\nlet ease = 'cubic-bezier(0.26, 0.02, 0.23, 0.94)'\n\nfunction OverlapExample(handle: Handle) {\n  let shouldAnimate = false\n  handle.queueTask(() => {\n    shouldAnimate = true\n  })\n\n  return ({ state }: { state: boolean }) => {\n    let animationMix = [\n      animateExit({\n        opacity: 0,\n        transform: 'scale(0.8)',\n        duration: 300,\n        easing: ease,\n      }),\n    ]\n    if (shouldAnimate) {\n      animationMix.unshift(\n        animateEntrance({\n          opacity: 0,\n          transform: 'scale(0.6)',\n          duration: 300,\n          easing: ease,\n        }),\n      )\n    }\n\n    return (\n      <div\n        // grid layout so children render in the same position\n        mix={[css({ display: 'grid', width: 80, height: 80, '& > *': { gridArea: '1 / 1' } })]}\n      >\n        {state ? (\n          <div key=\"filled\" mix={animationMix}>\n            <Circle filled>\n              <FilledIcon />\n            </Circle>\n          </div>\n        ) : (\n          <div key=\"outline\" mix={animationMix}>\n            <Circle>\n              <OutlineIcon />\n            </Circle>\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n\nfunction WaitExample(handle: Handle) {\n  let shouldAnimate = false\n  handle.queueTask(() => {\n    shouldAnimate = true\n  })\n\n  return ({ state }: { state: boolean }) => {\n    let animationMix = [\n      animateExit({\n        opacity: 0,\n        transform: 'scale(0.8)',\n        duration: 300,\n        easing: ease,\n      }),\n    ]\n    if (shouldAnimate) {\n      animationMix.unshift(\n        animateEntrance({\n          opacity: 0,\n          transform: 'scale(0.6)',\n          duration: 300,\n          easing: ease,\n          delay: 300,\n        }),\n      )\n    }\n\n    return (\n      <div\n        // grid layout so children render in the same position\n        mix={[css({ display: 'grid', width: 80, height: 80, '& > *': { gridArea: '1 / 1' } })]}\n      >\n        {state ? (\n          <div key=\"filled\" mix={animationMix}>\n            <Circle filled>\n              <FilledIcon />\n            </Circle>\n          </div>\n        ) : (\n          <div key=\"outline\" mix={animationMix}>\n            <Circle>\n              <OutlineIcon />\n            </Circle>\n          </div>\n        )}\n      </div>\n    )\n  }\n}\n\nexport function SharedLayout(handle: Handle) {\n  let state = true\n  let shouldAnimate = false\n  handle.queueTask(() => {\n    shouldAnimate = true\n  })\n\n  return () => (\n    <div\n      mix={[\n        css({\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          gap: 16,\n        }),\n      ]}\n    >\n      <div mix={[css({ display: 'flex', gap: 16 })]}>\n        <OverlapExample state={state} />\n        <WaitExample state={state} />\n      </div>\n      <button\n        mix={[\n          css({\n            backgroundColor: '#0f1115',\n            color: '#f5f5f5',\n            border: 'none',\n            borderRadius: 8,\n            padding: '12px 32px',\n            fontSize: 14,\n            fontWeight: 500,\n            cursor: 'pointer',\n            transition: 'transform 100ms ease-in-out',\n            '&:active': {\n              transform: 'scale(0.95)',\n            },\n          }),\n          on('click', () => {\n            state = !state\n            handle.update()\n          }),\n        ]}\n      >\n        Switch\n      </button>\n    </div>\n  )\n}\n\nfunction Circle() {\n  return (props: { filled?: boolean; children: RemixNode }) => (\n    <div\n      mix={[\n        css({\n          width: 80,\n          height: 80,\n          borderRadius: '50%',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          boxSizing: 'border-box',\n          backgroundColor: props.filled ? '#0f1115' : 'transparent',\n          color: props.filled ? '#f5f5f5' : '#0f1115',\n          border: props.filled ? '2px solid #0f1115' : '2px solid #0f1115',\n        }),\n      ]}\n    >\n      {props.children}\n    </div>\n  )\n}\n\nfunction FilledIcon() {\n  return () => (\n    <svg\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d=\"M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6\" />\n      <path d=\"m21 3-9 9\" />\n      <path d=\"M15 3h6v6\" />\n    </svg>\n  )\n}\n\nfunction OutlineIcon() {\n  return () => (\n    <svg\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n      <path d=\"M9 12h6\" />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/animation/transition-options.tsx",
    "content": "import { animateEntrance, css } from 'remix/component'\n\nexport function TransitionOptions() {\n  return () => (\n    <div\n      mix={[\n        css({\n          width: 100,\n          height: 100,\n          borderRadius: '50%',\n          backgroundColor: '#9911ff',\n        }),\n        animateEntrance({\n          opacity: 0,\n          transform: 'scale(0.5)',\n          duration: 800,\n          delay: 500,\n          easing: 'cubic-bezier(0, 0.71, 0.2, 1.01)',\n        }),\n      ]}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/basic/entry.tsx",
    "content": "import { createRoot, on, type Handle } from 'remix/component'\n\nfunction App(handle: Handle) {\n  let count = 0\n  return () => (\n    <button\n      mix={[\n        on('click', () => {\n          count++\n          handle.update()\n        }),\n      ]}\n    >\n      Ye ol' counter: {count}\n    </button>\n  )\n}\n\ncreateRoot(document.body).render(<App />)\n"
  },
  {
    "path": "packages/component/demos/basic/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Basic</title>\n  </head>\n  <body>\n    <script type=\"module\" src=\"./entry.bundled.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/demos/controlled-uncontrolled-values/entry.tsx",
    "content": "import { createRoot, on, type Handle } from 'remix/component'\n\nfunction App(handle: Handle) {\n  let controlledText = 'hello'\n  let controlledChecked = true\n  let uncontrolledTextSnapshot = 'type to update this'\n  let uncontrolledCheckedSnapshot = true\n  let renderCount = 0\n  let uncontrolledVersion = 0\n\n  let rerender = () => {\n    renderCount++\n    handle.update()\n  }\n\n  let resetControlled = () => {\n    controlledText = 'hello'\n    controlledChecked = true\n    rerender()\n  }\n\n  let remountUncontrolled = () => {\n    uncontrolledVersion++\n    uncontrolledTextSnapshot = 'type to update this'\n    uncontrolledCheckedSnapshot = true\n    rerender()\n  }\n\n  return () => (\n    <main\n      style={{\n        fontFamily: 'system-ui, sans-serif',\n        maxWidth: '860px',\n        margin: '24px auto',\n        padding: '0 16px',\n        lineHeight: 1.45,\n      }}\n    >\n      <h1>Controlled vs Uncontrolled Values</h1>\n\n      <p>\n        Render count: <strong>{renderCount}</strong>\n      </p>\n\n      <div style={{ display: 'flex', gap: '10px', marginBottom: '18px' }}>\n        <button mix={[on('click', rerender)]}>Force Re-render</button>\n        <button mix={[on('click', resetControlled)]}>Reset Controlled</button>\n        <button mix={[on('click', remountUncontrolled)]}>Remount Uncontrolled</button>\n      </div>\n\n      <section\n        style={{\n          border: '1px solid #d0d7de',\n          borderRadius: '8px',\n          padding: '12px',\n          marginBottom: '12px',\n        }}\n      >\n        <h2>Controlled</h2>\n        <p>\n          These values come from component state. The text input allows everything except digits,\n          and invalid input does not call update.\n        </p>\n\n        <label style={{ display: 'block', marginBottom: '8px' }}>\n          Text:\n          <input\n            style={{ marginLeft: '8px' }}\n            value={controlledText}\n            mix={[\n              on('input', (event) => {\n                let nextValue = event.currentTarget.value\n                if (/\\d/.test(nextValue)) {\n                  return\n                }\n                controlledText = nextValue\n                rerender()\n              }),\n            ]}\n          />\n        </label>\n\n        <label style={{ display: 'block', marginBottom: '8px' }}>\n          <input\n            type=\"checkbox\"\n            checked={controlledChecked}\n            mix={[\n              on('change', (event) => {\n                controlledChecked = event.currentTarget.checked\n                rerender()\n              }),\n            ]}\n          />{' '}\n          Checked\n        </label>\n\n        <div>\n          State snapshot: text=<code>{JSON.stringify(controlledText)}</code>, checked=\n          <code>{String(controlledChecked)}</code>\n        </div>\n      </section>\n\n      <section\n        key={`uncontrolled-${uncontrolledVersion}`}\n        style={{\n          border: '1px solid #d0d7de',\n          borderRadius: '8px',\n          padding: '12px',\n        }}\n      >\n        <h2>Uncontrolled</h2>\n        <p>\n          These initialize from <code>defaultValue/defaultChecked</code> once and then keep their\n          own DOM state.\n        </p>\n\n        <label style={{ display: 'block', marginBottom: '8px' }}>\n          Text:\n          <input\n            style={{ marginLeft: '8px' }}\n            defaultValue=\"type to update this\"\n            mix={[\n              on('input', (event) => {\n                uncontrolledTextSnapshot = event.currentTarget.value\n                rerender()\n              }),\n            ]}\n          />\n        </label>\n\n        <label style={{ display: 'block', marginBottom: '8px' }}>\n          <input\n            type=\"checkbox\"\n            defaultChecked={true}\n            mix={[\n              on('change', (event) => {\n                uncontrolledCheckedSnapshot = event.currentTarget.checked\n                rerender()\n              }),\n            ]}\n          />{' '}\n          Checked\n        </label>\n\n        <div>\n          Last DOM snapshot: text=<code>{JSON.stringify(uncontrolledTextSnapshot)}</code>, checked=\n          <code>{String(uncontrolledCheckedSnapshot)}</code>\n        </div>\n      </section>\n    </main>\n  )\n}\n\nlet root = createRoot(document.body)\nroot.render(<App />)\n"
  },
  {
    "path": "packages/component/demos/controlled-uncontrolled-values/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Controlled vs Uncontrolled Values</title>\n  </head>\n  <body>\n    <script type=\"module\" src=\"./entry.bundled.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/demos/draggable/draggable.tsx",
    "content": "import { createMixin, on } from 'remix/component'\n\nexport type DragDetail = {\n  left: number\n  top: number\n}\n\nexport let dragStartEvent = 'rmx:dragstart' as const\nexport let dragEndEvent = 'rmx:dragend' as const\n\ntype DraggableProps = {\n  on?: Record<string, (event: Event) => void>\n}\n\nlet baseDraggable = createMixin<HTMLElement, [boolean], DraggableProps>((handle) => {\n  let node: HTMLElement | null = null\n  let enabled = true\n  let pointerId: number | null = null\n  let startLeft = 0\n  let startTop = 0\n  let startClientX = 0\n  let startClientY = 0\n\n  handle.addEventListener('insert', (event) => {\n    node = event.node\n  })\n\n  handle.addEventListener('remove', stopDrag)\n\n  return (nextEnabled: boolean = true, props) => {\n    enabled = nextEnabled\n    if (!enabled) {\n      stopDrag()\n    }\n\n    return <handle.element {...props} mix={[on('pointerdown', (event) => onPointerDown(event))]} />\n  }\n\n  function onPointerDown(event: PointerEvent) {\n    if (event.button !== 0) return\n    if (!enabled) return\n    if (!node) return\n\n    let style = getComputedStyle(node)\n    if (style.position === 'static') {\n      node.style.position = 'relative'\n    }\n    node.style.cursor = 'grabbing'\n\n    startLeft = readPx(node.style.left)\n    startTop = readPx(node.style.top)\n    startClientX = event.clientX\n    startClientY = event.clientY\n    pointerId = event.pointerId\n\n    try {\n      node.setPointerCapture(event.pointerId)\n    } catch {}\n\n    window.addEventListener('pointermove', onPointerMove)\n    window.addEventListener('pointerup', onPointerDone)\n    window.addEventListener('pointercancel', onPointerDone)\n    dispatchDragEvent(node, dragStartEvent)\n  }\n\n  function onPointerMove(event: PointerEvent) {\n    if (!node) return\n    if (pointerId == null) return\n    if (event.pointerId !== pointerId) return\n    let dx = event.clientX - startClientX\n    let dy = event.clientY - startClientY\n    node.style.left = `${startLeft + dx}px`\n    node.style.top = `${startTop + dy}px`\n    void handle.update()\n  }\n\n  function onPointerDone(event: PointerEvent) {\n    if (!node) return\n    if (pointerId == null) return\n    if (event.pointerId !== pointerId) return\n    stopDrag()\n    dispatchDragEvent(node, dragEndEvent)\n  }\n\n  function stopDrag() {\n    if (!node) return\n    if (pointerId == null) return\n    pointerId = null\n    node.style.cursor = 'grab'\n    window.removeEventListener('pointermove', onPointerMove)\n    window.removeEventListener('pointerup', onPointerDone)\n    window.removeEventListener('pointercancel', onPointerDone)\n  }\n})\n\nfunction dispatchDragEvent(node: HTMLElement, type: string) {\n  node.dispatchEvent(\n    new CustomEvent<DragDetail>(type, {\n      detail: {\n        left: readPx(node.style.left),\n        top: readPx(node.style.top),\n      },\n      bubbles: true,\n    }),\n  )\n}\n\nfunction readPx(value: string) {\n  if (!value) return 0\n  let parsed = Number.parseFloat(value)\n  if (!Number.isFinite(parsed)) return 0\n  return parsed\n}\n\ntype DraggableMixin = typeof baseDraggable & {\n  readonly start: typeof dragStartEvent\n  readonly end: typeof dragEndEvent\n}\n\nexport let draggable: DraggableMixin = Object.assign(baseDraggable, {\n  start: dragStartEvent,\n  end: dragEndEvent,\n})\n\ndeclare global {\n  interface HTMLElementEventMap {\n    [dragStartEvent]: CustomEvent<DragDetail>\n    [dragEndEvent]: CustomEvent<DragDetail>\n  }\n}\n"
  },
  {
    "path": "packages/component/demos/draggable/entry.tsx",
    "content": "import { createRoot } from 'remix/component'\nimport type { Handle } from 'remix/component'\nimport { draggable } from './draggable.tsx'\n\nfunction App(_handle: Handle) {\n  return () => (\n    <main style={{ fontFamily: 'system-ui, sans-serif', padding: '24px' }}>\n      <h1>Draggable mixin demo</h1>\n      <p>Drag the box with your mouse or trackpad.</p>\n      <div\n        style={{\n          position: 'relative',\n          width: '100%',\n          maxWidth: '720px',\n          height: '420px',\n          border: '1px dashed #c2c2c2',\n          borderRadius: '8px',\n          overflow: 'hidden',\n          backgroundColor: '#fafafa',\n        }}\n      >\n        <div\n          mix={[draggable(true)]}\n          style={{\n            position: 'absolute',\n            left: '24px',\n            top: '24px',\n            width: '180px',\n            padding: '14px 16px',\n            borderRadius: '10px',\n            backgroundColor: '#2563eb',\n            color: 'white',\n            boxShadow: '0 8px 20px rgba(37, 99, 235, 0.35)',\n            userSelect: 'none',\n            touchAction: 'none',\n            cursor: 'grab',\n          }}\n        >\n          drag me\n        </div>\n      </div>\n    </main>\n  )\n}\n\ncreateRoot(document.body).render(<App />)\n"
  },
  {
    "path": "packages/component/demos/draggable/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Component Draggable Demo</title>\n  </head>\n  <body>\n    <script type=\"module\" src=\"./entry.bundled.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/demos/drummer/app.tsx",
    "content": "import { css, addEventListeners, on, pressEvents, ref, type Handle } from 'remix/component'\nimport { Drummer } from './drummer.ts'\nimport { tempoEvents } from './tempo-interaction.tsx'\nimport {\n  BPMDisplay,\n  Button,\n  ControlGroup,\n  EqualizerBar,\n  EqualizerLayout,\n  Layout,\n  TempoButton,\n  TempoButtons,\n  TempoLayout,\n} from './components.tsx'\nimport { createVoiceLooper } from './voice-looper.ts'\n\nexport function App(handle: Handle<Drummer>) {\n  let drummer = new Drummer(80)\n\n  handle.context.set(drummer)\n\n  handle.queueTask(() => {\n    document.addEventListener('keydown', (event) => {\n      if (event.key === ' ') {\n        drummer.toggle()\n      }\n      if (event.key === 'ArrowUp') {\n        drummer.setTempo(drummer.bpm + 1)\n      }\n      if (event.key === 'ArrowDown') {\n        drummer.setTempo(drummer.bpm - 1)\n      }\n    })\n  })\n\n  return () => (\n    <Layout>\n      <Equalizer />\n      <DrumControls />\n    </Layout>\n  )\n}\n\nexport function Equalizer(handle: Handle) {\n  let drummer = handle.context.get(App)\n\n  let kickVolumes = [0.4, 0.8, 0.3, 0.1]\n  let snareVolumes = [0.4, 1, 0.7]\n  let hatVolumes = [0.1, 0.8]\n\n  let createVoice = createVoiceLooper(handle.update)\n\n  let kick = createVoice()\n  let snare = createVoice()\n  let hat = createVoice()\n\n  addEventListeners(drummer, handle.signal, {\n    kick: () => kick.trigger(1),\n    snare: () => snare.trigger(1),\n    hat: () => hat.trigger(1),\n  })\n\n  return () => {\n    // get values from all the generators\n    let kicks = kickVolumes.map((volume) => kick.value * volume)\n    let snares = snareVolumes.map((volume) => snare.value * volume)\n    let hats = hatVolumes.map((volume) => hat.value * volume)\n\n    return (\n      <EqualizerLayout>\n        {/* kick */}\n        <EqualizerBar volume={kicks[0]} />\n        <EqualizerBar volume={kicks[1]} />\n        <EqualizerBar volume={kicks[2]} />\n        <EqualizerBar volume={kicks[3]} />\n\n        {/* snare */}\n        <EqualizerBar volume={snares[0]} />\n        <EqualizerBar volume={snares[1]} />\n        <EqualizerBar volume={snares[2]} />\n\n        {/* hat */}\n        <EqualizerBar volume={hats[0]} />\n        <EqualizerBar volume={hats[1]} />\n      </EqualizerLayout>\n    )\n  }\n}\n\nfunction DrumControls(handle: Handle) {\n  let drummer = handle.context.get(App)\n  let stop: HTMLButtonElement\n  let play: HTMLButtonElement\n\n  addEventListeners(drummer, handle.signal, {\n    change: () => {\n      handle.update()\n    },\n  })\n\n  return () => (\n    <ControlGroup>\n      <Button\n        mix={[\n          tempoEvents(),\n          on(tempoEvents.type, (event) => {\n            drummer.play(event.bpm)\n          }),\n        ]}\n      >\n        SET TEMPO\n      </Button>\n\n      <TempoDisplay />\n\n      <Button\n        disabled={drummer.isPlaying}\n        mix={[\n          ref((node: HTMLButtonElement) => (play = node)),\n          on('click', () => {\n            drummer.play()\n            handle.queueTask(() => {\n              stop.focus()\n            })\n          }),\n        ]}\n      >\n        PLAY\n      </Button>\n\n      <Button\n        disabled={!drummer.isPlaying}\n        mix={[\n          ref((node: HTMLButtonElement) => (stop = node)),\n          pressEvents(),\n          on(pressEvents.down, () => {\n            drummer.stop()\n            handle.queueTask(() => {\n              play.focus()\n            })\n          }),\n        ]}\n      >\n        STOP\n      </Button>\n    </ControlGroup>\n  )\n}\n\nfunction TempoDisplay(handle: Handle) {\n  let drummer = handle.context.get(App)\n  return () => (\n    <TempoLayout>\n      <BPMDisplay bpm={drummer.bpm} />\n      <TempoButtons>\n        <TempoButton\n          orientation=\"up\"\n          mix={[\n            css({ borderTopRightRadius: '18px' }),\n            pressEvents(),\n            on(pressEvents.down, () => {\n              drummer.setTempo(drummer.bpm + 1)\n            }),\n          ]}\n        />\n        <TempoButton\n          mix={[\n            css({ borderBottomRightRadius: '18px' }),\n            pressEvents(),\n            on(pressEvents.down, () => {\n              drummer.setTempo(drummer.bpm - 1)\n            }),\n          ]}\n          orientation=\"down\"\n        />\n      </TempoButtons>\n    </TempoLayout>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/drummer/components.tsx",
    "content": "import type { Handle, RemixNode, Props } from 'remix/component'\nimport { css } from 'remix/component'\n\nexport function Layout() {\n  return ({ children }: { children: RemixNode }) => (\n    <div\n      mix={[\n        css({\n          boxSizing: 'border-box',\n          '& *': {\n            boxSizing: 'border-box',\n          },\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '44px',\n          width: 735,\n          margin: '4.5rem auto',\n          background: '#2D2D2D',\n          color: 'white',\n          borderRadius: '54px',\n          padding: '44px 48px 54px 48px',\n          '@media (prefers-color-scheme: light)': {\n            background: '#F5F5F5',\n            color: 'black',\n          },\n        }),\n      ]}\n    >\n      <header\n        mix={[\n          css({\n            display: 'flex',\n            justifyContent: 'space-between',\n          }),\n        ]}\n      >\n        <Logo />\n        <div\n          mix={[\n            css({\n              display: 'flex',\n              alignItems: 'end',\n              lineHeight: '0.88',\n              textAlign: 'right',\n              fontSize: '30px',\n              fontWeight: 700,\n              position: 'relative',\n              top: '1px',\n            }),\n          ]}\n        >\n          REMIX 3<br />\n          DRUM MACHINE\n        </div>\n      </header>\n\n      {children}\n    </div>\n  )\n}\n\nexport function EqualizerLayout() {\n  return ({ children }: { children: RemixNode }) => (\n    <div\n      mix={[\n        css({\n          display: 'flex',\n          background: 'black',\n          borderRadius: '18px',\n          padding: '18px',\n          height: 339,\n          gap: '3px',\n          '@media (prefers-color-scheme: light)': {\n            background: '#FFFFFF',\n          },\n        }),\n      ]}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport function TempoLayout() {\n  return ({ children }: { children: RemixNode }) => (\n    <div\n      mix={[\n        css({\n          display: 'flex',\n          flexDirection: 'row',\n          gap: '8px',\n          alignItems: 'flex-end',\n          height: 120,\n        }),\n      ]}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport function TempoButtons() {\n  return ({ children }: { children: RemixNode }) => (\n    <div\n      mix={[\n        css({\n          width: 56,\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '9px',\n          height: '100%',\n          justifyContent: 'space-between',\n        }),\n      ]}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport function BPMDisplay() {\n  return ({ bpm }: { bpm: number }) => (\n    <div\n      mix={[\n        css({\n          height: '100%',\n          display: 'flex',\n          flex: 1,\n          background: '#0B1B05',\n          color: '#64C146',\n          padding: '32px',\n          borderTopLeftRadius: '18px',\n          borderBottomLeftRadius: '18px',\n          alignItems: 'end',\n          '@media (prefers-color-scheme: light)': {\n            background: '#EAF7E6',\n          },\n        }),\n      ]}\n    >\n      <div\n        mix={[\n          css({\n            fontSize: '18px',\n            fontWeight: 700,\n            width: '33%',\n          }),\n        ]}\n      >\n        BPM\n      </div>\n      <div\n        mix={[\n          css({\n            flex: 1,\n            fontSize: '69px',\n            fontWeight: 700,\n            position: 'relative',\n            top: 17,\n            textAlign: 'right',\n            fontFamily: 'JetBrains Mono, monospace',\n          }),\n        ]}\n      >\n        {bpm}\n      </div>\n    </div>\n  )\n}\n\nexport function EqualizerBar(handle: Handle) {\n  let colors = [\n    '#FF3000',\n    '#FF3000',\n    '#E561C3',\n    '#E561C3',\n    '#FFD400',\n    '#FFD400',\n    '#64C146',\n    '#64C146',\n    '#1A72FF',\n    '#1A72FF',\n  ]\n\n  return ({ volume }: { volume: number /* 0-1 */ }) => {\n    let startIndexToShow = 10 - Math.round(volume * 10)\n    return (\n      <div\n        mix={[\n          css({\n            flex: 1,\n            display: 'flex',\n            flexDirection: 'column',\n            gap: '4px',\n          }),\n        ]}\n      >\n        {Array.from({ length: 10 }).map((_, index) => (\n          <div\n            mix={[\n              css({\n                flex: 1,\n                width: '100%',\n                borderRadius: '3px',\n                background: colors[index],\n              }),\n            ]}\n            style={{\n              opacity: index >= startIndexToShow ? 1 : 0.25,\n            }}\n          />\n        ))}\n      </div>\n    )\n  }\n}\n\nexport function ControlGroup() {\n  return ({ children, mix: mixOverride, ...rest }: Props<'div'>) => (\n    <div\n      {...rest}\n      mix={[\n        css({\n          display: 'grid',\n          gridTemplateColumns: '1fr 1fr',\n          gridTemplateRows: '1fr 1fr',\n          gap: '18px',\n          alignItems: 'center',\n          justifyContent: 'center',\n          '& button:focus-visible': {\n            outline: '2px solid #2684FF',\n            outlineOffset: '2px',\n          },\n        }),\n        ...(mixOverride ?? []),\n      ]}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport function Button() {\n  return ({ children, mix, ...rest }: Props<'button'>) => (\n    <button\n      {...rest}\n      mix={[\n        css({\n          all: 'unset',\n          letterSpacing: 0.9,\n          height: 120,\n          display: 'flex',\n          alignItems: 'end',\n          background: '#666',\n          borderRadius: '18px',\n          padding: '32px',\n          fontSize: '18px',\n          fontWeight: 700,\n          '&:disabled': {\n            opacity: 0.25,\n          },\n          '&:active': {\n            background: '#555',\n          },\n          '@media (prefers-color-scheme: light)': {\n            background: '#E0E0E0',\n            '&:active': {\n              background: '#D0D0D0',\n            },\n          },\n        }),\n        ...(mix ?? []),\n      ]}\n    >\n      {children}\n    </button>\n  )\n}\n\nexport function Triangle() {\n  return ({ label, orientation }: { label: string; orientation: 'up' | 'down' }) => {\n    let up = '5,1.34 9.33,8.66 0.67,8.66'\n    let down = '5,8.66 9.33,1.34 0.67,1.34'\n    return (\n      <svg\n        aria-label={label}\n        viewBox=\"0 0 10 10\"\n        style={{\n          width: 14,\n          height: 14,\n        }}\n      >\n        <polygon points={orientation === 'up' ? up : down} fill=\"currentColor\" />\n      </svg>\n    )\n  }\n}\n\ninterface TempoButtonProps extends Props<'button'> {\n  orientation: 'up' | 'down'\n}\n\nexport function TempoButton() {\n  return ({ orientation, mix: mixOverride, ...rest }: TempoButtonProps) => (\n    <button\n      {...rest}\n      mix={[\n        css({\n          all: 'unset',\n          flex: 1,\n          background: '#666',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          '&:active': {\n            background: '#555',\n          },\n          '@media (prefers-color-scheme: light)': {\n            background: '#E0E0E0',\n            '&:active': {\n              background: '#D0D0D0',\n            },\n          },\n          '&:first-child': {\n            borderTopRightRadius: '18px',\n          },\n          '&:last-child': {\n            borderBottomRightRadius: '18px',\n          },\n        }),\n        ...(mixOverride ?? []),\n      ]}\n    >\n      <Triangle label={orientation} orientation={orientation} />\n    </button>\n  )\n}\n\nexport function Logo() {\n  return () => (\n    <svg\n      height=\"65\"\n      viewBox=\"0 0 400 143\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      mix={[\n        css({\n          '@media (prefers-color-scheme: light)': {\n            \"& [fill='white']\": { fill: 'black' },\n          },\n        }),\n      ]}\n    >\n      <g clip-path=\"url(#clip0_836_2441)\">\n        <path d=\"M82.8363 111.891H58.0045L49.7144 142.85H74.5342L82.8363 111.891Z\" fill=\"#FFD400\" />\n        <path d=\"M68.846 71.4047L60.7856 101.501H85.6204L93.6897 71.4047H68.846Z\" fill=\"#FFD400\" />\n        <path d=\"M96.707 60.1494L104.776 30.0588H79.9207L71.8604 60.1494H96.707Z\" fill=\"#FFD400\" />\n        <path d=\"M57.9867 111.891H33.1549L24.8647 142.85H49.6876L57.9867 111.891Z\" fill=\"#64C146\" />\n        <path\n          d=\"M71.8604 60.1494L79.9267 30.0588H55.0741L47.0137 60.1494H71.8604Z\"\n          fill=\"#64C146\"\n        />\n        <path d=\"M43.9994 71.4047L35.939 101.501H60.7737L68.8431 71.4047H43.9994Z\" fill=\"#64C146\" />\n        <path d=\"M33.122 111.891H8.29019L0 142.85H24.8228L33.122 111.891Z\" fill=\"#1A72FF\" />\n        <path\n          d=\"M19.1351 71.4047L11.0747 101.501H35.9095L43.9758 71.4047H19.1351Z\"\n          fill=\"#1A72FF\"\n        />\n        <path d=\"M46.9956 60.1494L55.062 30.0588H30.2093L22.1489 60.1494H46.9956Z\" fill=\"#1A72FF\" />\n        <path\n          d=\"M132.401 111.891L124.111 142.85H203.626L210.459 117.33C211.194 114.587 209.129 111.891 206.288 111.891H132.398H132.401Z\"\n          fill=\"white\"\n        />\n        <path\n          d=\"M339.31 30.0618H154.317L146.257 60.1554H296.095C301.732 60.1554 305.627 62.6752 304.791 65.7831C303.959 68.8909 298.715 71.4107 293.081 71.4107H143.243L135.183 101.507H220.729C223.158 101.507 225.536 102.203 227.586 103.511L287.823 142.859H380.394L317.089 101.51H320.172C350.31 101.51 378.347 88.0368 382.796 71.4167L385.813 60.1614C390.266 43.5412 369.445 30.0677 339.307 30.0677L339.31 30.0618Z\"\n          fill=\"white\"\n        />\n        <path d=\"M107.375 111.891H82.5436L74.2534 142.85H99.0762L107.375 111.891Z\" fill=\"#E561C3\" />\n        <path\n          d=\"M93.3885 71.4047L85.3281 101.501H110.163L118.229 71.4047H93.3885Z\"\n          fill=\"#E561C3\"\n        />\n        <path\n          d=\"M121.249 60.1494L129.315 30.0588H104.463L96.4023 60.1494H121.249Z\"\n          fill=\"#E561C3\"\n        />\n        <path d=\"M132.222 111.891H107.39L99.1001 142.85H123.92L132.222 111.891Z\" fill=\"#FF3000\" />\n        <path\n          d=\"M118.232 71.4047L110.172 101.501H135.007L143.076 71.4047H118.232Z\"\n          fill=\"#FF3000\"\n        />\n        <path\n          d=\"M146.093 60.1494L154.162 30.0588H129.306L121.246 60.1494H146.093Z\"\n          fill=\"#FF3000\"\n        />\n        <path\n          d=\"M386.574 12.3877C386.574 11.4174 385.905 10.9338 384.935 10.9338C383.691 10.9338 382.906 11.7638 382.652 13.1729H378.731C379.238 9.68884 381.614 7.70648 385.282 7.70648C388.442 7.70648 390.564 9.20519 390.564 11.8354C390.564 13.5879 389.618 14.8119 388.072 15.5046C389.316 16.1733 389.985 17.3048 389.985 18.9199C389.985 22.2428 387.194 24.5506 383.204 24.5506C379.492 24.5506 376.77 22.5652 377.415 18.8304H381.336C381.038 20.4903 381.96 21.3233 383.41 21.3233C384.861 21.3233 385.971 20.4455 385.971 19.1319C385.971 17.9795 385.141 17.3794 383.688 17.3794H382.464L382.673 16.227L382.996 14.382H384.219C385.604 14.382 386.571 13.5759 386.571 12.3967L386.574 12.3877Z\"\n          fill=\"white\"\n        />\n        <path\n          d=\"M383.942 3.04615C391.152 3.04615 397.016 8.91262 397.016 16.1255C397.016 23.3385 391.152 29.2049 383.942 29.2049C376.732 29.2049 370.868 23.3385 370.868 16.1255C370.868 8.91262 376.732 3.04615 383.942 3.04615ZM383.942 0.0606689C375.073 0.0606689 367.884 7.25269 367.884 16.1255C367.884 24.9984 375.073 32.1904 383.942 32.1904C392.811 32.1904 400 24.9984 400 16.1255C400 7.25269 392.811 0.0606689 383.942 0.0606689Z\"\n          fill=\"white\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_836_2441\">\n          <rect width=\"400\" height=\"142.79\" fill=\"white\" transform=\"translate(0 0.0606689)\" />\n        </clipPath>\n      </defs>\n    </svg>\n  )\n}\n"
  },
  {
    "path": "packages/component/demos/drummer/drummer.ts",
    "content": "import { TypedEventTarget } from 'remix/component'\n\ninterface DrummerEventMap {\n  kick: DrumEvent\n  snare: DrumEvent\n  hat: DrumEvent\n  play: DrumEvent\n  stop: DrumEvent\n  tempoChange: DrumEvent\n  change: DrumEvent\n}\n\nclass DrumEvent extends Event {\n  tempo: number\n\n  constructor(type: keyof DrummerEventMap, tempo: number) {\n    super(type)\n    this.tempo = tempo\n  }\n}\n\nexport class Drummer extends TypedEventTarget<DrummerEventMap> {\n  #audioCtx: AudioContext | null = null\n  #masterGain: GainNode | null = null\n  #noiseBuffer: AudioBuffer | null = null\n\n  #_isPlaying = false\n  #tempoBpm = 90\n  #current16th = 0\n  #nextNoteTime = 0\n  #intervalId: number | null = null\n\n  // Scheduler settings\n  readonly #lookaheadMs = 25 // how frequently to check (ms)\n  readonly #scheduleAheadS = 0.1 // how far ahead to schedule (s)\n\n  constructor(tempoBpm: number = 90) {\n    super()\n    this.#tempoBpm = tempoBpm\n  }\n\n  get isPlaying() {\n    return this.#_isPlaying\n  }\n\n  get bpm() {\n    return this.#tempoBpm\n  }\n\n  async toggle() {\n    if (this.isPlaying) {\n      await this.stop()\n    } else {\n      await this.play()\n    }\n  }\n\n  setTempo(bpm: number) {\n    this.#tempoBpm = Math.max(30, Math.min(300, Math.floor(bpm || this.#tempoBpm)))\n    this.dispatchEvent(new DrumEvent('tempoChange', this.#tempoBpm))\n    this.dispatchEvent(new DrumEvent('change', this.#tempoBpm))\n  }\n\n  async play(bpm?: number) {\n    this.#ensureContext()\n    if (!this.#audioCtx) return\n    if (bpm) {\n      this.setTempo(bpm)\n    }\n    await this.#audioCtx.resume()\n    if (this.#_isPlaying) return\n    this.#_isPlaying = true\n    this.#nextNoteTime = this.#audioCtx.currentTime\n    // don't reset current16th so setTempo can adjust mid-groove if restarted\n    if (this.#intervalId != null) window.clearInterval(this.#intervalId)\n    this.#intervalId = window.setInterval(this.#scheduler, this.#lookaheadMs)\n    this.dispatchEvent(new DrumEvent('play', this.#tempoBpm))\n    this.dispatchEvent(new DrumEvent('change', this.#tempoBpm))\n  }\n\n  async stop() {\n    if (!this.#audioCtx) return\n    if (this.#intervalId != null) {\n      window.clearInterval(this.#intervalId)\n      this.#intervalId = null\n    }\n    this.#_isPlaying = false\n    this.#current16th = 0\n    this.#nextNoteTime = this.#audioCtx.currentTime\n    this.dispatchEvent(new DrumEvent('stop', this.#tempoBpm))\n    this.dispatchEvent(new DrumEvent('change', this.#tempoBpm))\n  }\n\n  #ensureContext() {\n    if (!this.#audioCtx) {\n      let Ctx = (window as any).AudioContext || (window as any).webkitAudioContext\n      let ctx: AudioContext = new Ctx()\n      this.#audioCtx = ctx\n      this.#masterGain = ctx.createGain()\n      this.#masterGain.gain.value = 0.8\n      this.#masterGain.connect(ctx.destination)\n      this.#noiseBuffer = this.#createNoiseBuffer(ctx)\n    }\n  }\n\n  #secondsPer16th(): number {\n    return 60 / Math.max(1, this.#tempoBpm) / 4\n  }\n\n  #createNoiseBuffer(ctx: AudioContext): AudioBuffer {\n    let length = ctx.sampleRate // 1 second\n    let buffer = ctx.createBuffer(1, length, ctx.sampleRate)\n    let data = buffer.getChannelData(0)\n    for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1\n    return buffer\n  }\n\n  #playKick(time: number) {\n    if (!this.#audioCtx || !this.#masterGain) return\n    let osc = this.#audioCtx.createOscillator()\n    let gain = this.#audioCtx.createGain()\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(150, time)\n    osc.frequency.exponentialRampToValueAtTime(50, time + 0.1)\n    gain.gain.setValueAtTime(1, time)\n    gain.gain.exponentialRampToValueAtTime(0.001, time + 0.15)\n    osc.connect(gain).connect(this.#masterGain)\n    osc.start(time)\n    osc.stop(time + 0.2)\n    this.dispatchEvent(new DrumEvent('kick', this.#tempoBpm))\n    this.dispatchEvent(new DrumEvent('change', this.#tempoBpm))\n  }\n\n  #playSnare(time: number) {\n    if (!this.#audioCtx || !this.#masterGain || !this.#noiseBuffer) return\n    // Noise component\n    let noise = this.#audioCtx.createBufferSource()\n    noise.buffer = this.#noiseBuffer\n    let band = this.#audioCtx.createBiquadFilter()\n    band.type = 'bandpass'\n    band.frequency.value = 1800\n    let noiseGain = this.#audioCtx.createGain()\n    noiseGain.gain.setValueAtTime(1, time)\n    noiseGain.gain.exponentialRampToValueAtTime(0.01, time + 0.2)\n    noise.connect(band).connect(noiseGain).connect(this.#masterGain)\n    noise.start(time)\n    noise.stop(time + 0.2)\n\n    // Body/tonal component\n    let osc = this.#audioCtx.createOscillator()\n    let oscGain = this.#audioCtx.createGain()\n    osc.type = 'triangle'\n    osc.frequency.setValueAtTime(200, time)\n    oscGain.gain.setValueAtTime(0.6, time)\n    oscGain.gain.exponentialRampToValueAtTime(0.01, time + 0.12)\n    osc.connect(oscGain).connect(this.#masterGain)\n    osc.start(time)\n    osc.stop(time + 0.15)\n    this.dispatchEvent(new DrumEvent('snare', this.#tempoBpm))\n    this.dispatchEvent(new DrumEvent('change', this.#tempoBpm))\n  }\n\n  #playHiHat(time: number) {\n    if (!this.#audioCtx || !this.#masterGain || !this.#noiseBuffer) return\n    let noise = this.#audioCtx.createBufferSource()\n    noise.buffer = this.#noiseBuffer\n    let hp = this.#audioCtx.createBiquadFilter()\n    hp.type = 'highpass'\n    hp.frequency.value = 7000\n    let gain = this.#audioCtx.createGain()\n    gain.gain.setValueAtTime(0.5, time)\n    gain.gain.exponentialRampToValueAtTime(0.001, time + 0.04)\n    noise.connect(hp).connect(gain).connect(this.#masterGain)\n    noise.start(time)\n    noise.stop(time + 0.05)\n    this.dispatchEvent(new DrumEvent('hat', this.#tempoBpm))\n    this.dispatchEvent(new DrumEvent('change', this.#tempoBpm))\n  }\n\n  // Simple \"boom bap\" pattern over 16 steps\n  // Kick: 1 and 3 -> steps 0, 8\n  // Snare: 2 and 4 -> steps 4, 12\n  // Hi-hat: eighth notes -> steps 0,2,4,6,8,10,12,14\n  #scheduleStep(step: number, time: number) {\n    if (step === 0 || step === 10) this.#playKick(time)\n    if (step === 4 || step === 12) this.#playSnare(time)\n    if (step % 2 === 0) this.#playHiHat(time)\n    if (step === 7 || step === 9) this.#playHiHat(time)\n  }\n\n  #advanceNote() {\n    this.#nextNoteTime += this.#secondsPer16th()\n    this.#current16th = (this.#current16th + 1) % 16\n  }\n\n  #scheduler = () => {\n    if (!this.#audioCtx) return\n    while (this.#nextNoteTime < this.#audioCtx.currentTime + this.#scheduleAheadS) {\n      this.#scheduleStep(this.#current16th, this.#nextNoteTime)\n      this.#advanceNote()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/component/demos/drummer/entry.tsx",
    "content": "import { createRoot } from 'remix/component'\nimport { App } from './app.tsx'\n\ncreateRoot(document.body).render(<App />)\n"
  },
  {
    "path": "packages/component/demos/drummer/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Remix Jam</title>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    <link\n      href=\"https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap\"\n      rel=\"stylesheet\"\n    />\n  </head>\n  <style>\n    body {\n      font-family: 'Inter', sans-serif;\n      background: #000;\n      color: #eee;\n    }\n    @media (prefers-color-scheme: light) {\n      body {\n        background: #fff;\n        color: #000;\n      }\n    }\n    * {\n      box-sizing: border-box;\n    }\n  </style>\n  <body>\n    <script async type=\"module\" src=\"./entry.bundled.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/demos/drummer/tempo-interaction.tsx",
    "content": "import { createMixin, on } from 'remix/component'\n\ndeclare global {\n  interface HTMLElementEventMap {\n    [tempoEventType]: TempoEvent\n  }\n}\n\nexport class TempoEvent extends Event {\n  bpm: number\n\n  constructor(type: typeof tempoEventType, bpm: number) {\n    super(type)\n    this.bpm = bpm\n  }\n}\n\nexport let tempoEventType = 'my:tempo' as const\n\nlet baseTempoEvents = createMixin<HTMLElement>((handle) => {\n  let taps: number[] = []\n  let resetTimer = 0\n\n  let handleTap = (node: HTMLElement) => {\n    clearTimeout(resetTimer)\n\n    taps.push(Date.now())\n    taps = taps.filter((tap) => Date.now() - tap < 4000)\n\n    if (taps.length >= 4) {\n      let intervals = []\n      for (let i = 1; i < taps.length; i++) {\n        intervals.push(taps[i] - taps[i - 1])\n      }\n      let bpm = intervals.map((interval) => 60000 / interval)\n      let avgBpm = Math.round(bpm.reduce((sum, value) => sum + value, 0) / bpm.length)\n      node.dispatchEvent(new TempoEvent(tempoEventType, avgBpm))\n    }\n\n    resetTimer = window.setTimeout(() => {\n      taps = []\n    }, 4000)\n  }\n\n  return (props) => (\n    <handle.element\n      {...props}\n      mix={[\n        on('pointerdown', (event) => {\n          console.log('pointerdown', event)\n          handleTap(event.currentTarget)\n        }),\n        on('keydown', (event) => {\n          if (event.repeat) return\n          if (event.key === 'Enter' || event.key === ' ') {\n            handleTap(event.currentTarget)\n          }\n        }),\n      ]}\n    />\n  )\n})\n\ntype TempoEventsMixin = typeof baseTempoEvents & {\n  readonly type: typeof tempoEventType\n}\n\nexport let tempoEvents: TempoEventsMixin = Object.assign(baseTempoEvents, {\n  type: tempoEventType,\n})\n"
  },
  {
    "path": "packages/component/demos/drummer/voice-looper.ts",
    "content": "export type DecayGenerator = Generator<number, number, number>\n\nexport function createExponentialDecayGenerator(\n  halfLifeMs: number,\n  startValue: number,\n  startMs: number,\n): DecayGenerator {\n  let localEpsilon = 0.001\n  function* decay(): Generator<number, number, number> {\n    let value = startValue\n    let lastMs = startMs\n    while (value > localEpsilon) {\n      let input = yield value\n      let nowMs = typeof input === 'number' ? input : performance.now()\n      let deltaMs = Math.max(0, nowMs - lastMs)\n      lastMs = nowMs\n      let decayFactor = Math.pow(0.5, deltaMs / halfLifeMs)\n      value = value * decayFactor\n    }\n    return 0\n  }\n  return decay()\n}\n\nexport function createVoiceLooper(render: () => void, epsilon: number = 0.001) {\n  let frameId: number | null = null\n\n  type EnvelopeState = {\n    value: number\n    halfLifeMs: number\n    gen: DecayGenerator | null\n  }\n\n  let envelopes: EnvelopeState[] = []\n\n  function ensureLoop() {\n    if (frameId == null) {\n      frameId = requestAnimationFrame(tick)\n      render()\n    }\n  }\n\n  function tick(now: number) {\n    let anyActive = false\n    for (let i = 0; i < envelopes.length; i++) {\n      let state = envelopes[i]\n      if (state.gen) {\n        let result = state.gen.next(now)\n        state.value = result.value ?? 0\n        if (result.done) {\n          state.gen = null\n          state.value = 0\n        } else if (state.value > epsilon) {\n          anyActive = true\n        }\n      }\n    }\n    if (anyActive) {\n      render()\n      frameId = requestAnimationFrame(tick)\n    } else {\n      frameId = null\n    }\n  }\n\n  function createVoice(halfLifeMs: number = 220) {\n    let state: EnvelopeState = {\n      value: 0,\n      halfLifeMs,\n      gen: null,\n    }\n    envelopes.push(state)\n    return {\n      get value() {\n        return state.value\n      },\n      trigger(amplitude: number = 1) {\n        let now = performance.now()\n        state.value = amplitude\n        state.gen = createExponentialDecayGenerator(state.halfLifeMs, amplitude, now)\n        void state.gen.next()\n        ensureLoop()\n      },\n    }\n  }\n\n  return createVoice\n}\n"
  },
  {
    "path": "packages/component/demos/keyed-list/entry.tsx",
    "content": "import { createRoot, on, type Handle } from 'remix/component'\n\ntype ListItem = {\n  id: string\n  label: string\n}\n\nfunction App(handle: Handle) {\n  let items: ListItem[] = [\n    { id: 'a', label: 'Item A' },\n    { id: 'b', label: 'Item B' },\n    { id: 'c', label: 'Item C' },\n    { id: 'd', label: 'Item D' },\n  ]\n\n  let shuffleInterval: ReturnType<typeof setInterval> | null = null\n\n  let moveUp = (index: number) => {\n    if (index === 0) return\n    let newItems = [...items]\n    ;[newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]]\n    items = newItems\n    handle.update()\n  }\n\n  let moveDown = (index: number) => {\n    if (index === items.length - 1) return\n    let newItems = [...items]\n    ;[newItems[index], newItems[index + 1]] = [newItems[index + 1], newItems[index]]\n    items = newItems\n    handle.update()\n  }\n\n  let reverse = () => {\n    items = [...items].reverse()\n    handle.update()\n  }\n\n  let shuffle = () => {\n    let newItems = [...items]\n    for (let i = newItems.length - 1; i > 0; i--) {\n      let j = Math.floor(Math.random() * (i + 1))\n      ;[newItems[i], newItems[j]] = [newItems[j], newItems[i]]\n    }\n    items = newItems\n    handle.update()\n  }\n\n  let toggleAutoShuffle = () => {\n    if (shuffleInterval !== null) {\n      clearInterval(shuffleInterval)\n      shuffleInterval = null\n    } else {\n      shuffleInterval = setInterval(() => {\n        shuffle()\n      }, 1000)\n    }\n    handle.update()\n  }\n\n  return () => (\n    <div>\n      <div className=\"controls\">\n        <button mix={[on('click', reverse)]}>Reverse List</button>\n        <button mix={[on('click', shuffle)]}>Shuffle List</button>\n        <button mix={[on('click', toggleAutoShuffle)]}>\n          {shuffleInterval !== null ? 'Stop Auto-Shuffle' : 'Start Auto-Shuffle'}\n        </button>\n      </div>\n\n      {items.map((item, index) => (\n        <div key={item.id} className=\"list-item\">\n          <input type=\"text\" placeholder={item.label} defaultValue={item.label} />\n          <button\n            // disabled={index === 0}\n            mix={[on('click', () => moveUp(index))]}\n          >\n            ↑\n          </button>\n          <button\n            // disabled={index === items.length - 1}\n            mix={[on('click', () => moveDown(index))]}\n          >\n            ↓\n          </button>\n        </div>\n      ))}\n    </div>\n  )\n}\n\nlet container = document.getElementById('app')\nif (container) {\n  createRoot(container).render(<App />)\n}\n"
  },
  {
    "path": "packages/component/demos/keyed-list/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Keyed List Demo</title>\n    <style>\n      body {\n        font-family:\n          system-ui,\n          -apple-system,\n          sans-serif;\n        max-width: 800px;\n        margin: 2rem auto;\n        padding: 0 1rem;\n      }\n      .list-item {\n        display: flex;\n        align-items: center;\n        gap: 0.5rem;\n        padding: 0.75rem;\n        margin: 0.5rem 0;\n        border: 2px solid #ddd;\n        border-radius: 4px;\n        background: #f9f9f9;\n      }\n      .list-item:focus-within {\n        border-color: #0066cc;\n        background: #f0f7ff;\n      }\n      .list-item input {\n        flex: 1;\n        padding: 0.5rem;\n        border: 1px solid #ccc;\n        border-radius: 3px;\n        font-size: 1rem;\n      }\n      .list-item input:focus {\n        outline: 2px solid #0066cc;\n        outline-offset: 2px;\n      }\n      .list-item button {\n        padding: 0.5rem 1rem;\n        border: 1px solid #ccc;\n        border-radius: 3px;\n        background: white;\n        cursor: pointer;\n        font-size: 0.875rem;\n      }\n      .list-item button:hover {\n        background: #f0f0f0;\n      }\n      .controls {\n        margin: 2rem 0;\n        padding: 1rem;\n        background: #f0f0f0;\n        border-radius: 4px;\n      }\n      .controls button {\n        padding: 0.75rem 1.5rem;\n        margin: 0.25rem;\n        border: none;\n        border-radius: 4px;\n        background: #0066cc;\n        color: white;\n        cursor: pointer;\n        font-size: 1rem;\n      }\n      .controls button:hover {\n        background: #0052a3;\n      }\n      .info {\n        margin: 1rem 0;\n        padding: 1rem;\n        background: #e8f4f8;\n        border-left: 4px solid #0066cc;\n        border-radius: 4px;\n      }\n      .info code {\n        background: #d0e8f0;\n        padding: 0.2rem 0.4rem;\n        border-radius: 2px;\n        font-family: 'Courier New', monospace;\n      }\n    </style>\n  </head>\n  <body>\n    <h1>Keyed List Demo</h1>\n    <div class=\"info\">\n      <p>\n        <strong>Try this:</strong> Type in the inputs, focus one, then click a reorder button.\n        Notice that:\n      </p>\n      <ul>\n        <li>Input values are preserved</li>\n        <li>Focus stays on the same input element</li>\n        <li>DOM nodes are reused (not recreated)</li>\n      </ul>\n      <p>This works because each item has a <code>key</code> prop that uniquely identifies it.</p>\n    </div>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./entry.bundled.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/demos/package.json",
    "content": "{\n  \"name\": \"component-demos\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"motion\": \"^12.28.1\",\n    \"remix\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@types/dom-navigation\": \"^1.0.7\",\n    \"@types/node\": \"catalog:\",\n    \"concurrently\": \"^9.2.1\",\n    \"esbuild\": \"^0.25.11\"\n  },\n  \"scripts\": {\n    \"dev\": \"concurrently \\\"pnpm dev:browser\\\" \\\"pnpm dev:server\\\"\",\n    \"dev:browser\": \"esbuild ./*/entry.tsx --bundle --outdir=. --out-extension:.js=.bundled.js --format=esm --platform=browser --target=es2020 --sourcemap=inline --watch\",\n    \"dev:server\": \"node --experimental-strip-types --watch server.ts\",\n    \"build\": \"esbuild ./*/entry.tsx --bundle --outdir=. --out-extension:.js=.bundled.js --format=esm --platform=browser --target=es2020\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"start\": \"node --experimental-strip-types server.ts\"\n  }\n}\n"
  },
  {
    "path": "packages/component/demos/readme/entry.tsx",
    "content": "import {\n  addEventListeners,\n  createRoot,\n  css,\n  on,\n  ref,\n  TypedEventTarget,\n  type Handle,\n  type RemixNode,\n} from 'remix/component'\n\n// ============================================================================\n// Getting Started - Basic App Example\n// ============================================================================\nfunction App(handle: Handle) {\n  let count = 0\n  return () => (\n    <button\n      mix={[\n        on('click', () => {\n          count++\n          handle.update()\n        }),\n      ]}\n    >\n      Count: {count}\n    </button>\n  )\n}\n\n// ============================================================================\n// Component State and Updates - Counter\n// ============================================================================\nfunction Counter(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <div>\n      <span>Count: {count}</span>\n      <button\n        mix={[\n          on('click', () => {\n            count++\n            handle.update()\n          }),\n        ]}\n      >\n        Increment\n      </button>\n    </div>\n  )\n}\n\n// ============================================================================\n// Components - Greeting\n// ============================================================================\nfunction Greeting(handle: Handle) {\n  return (props: { name: string }) => <h1>Hello, {props.name}!</h1>\n}\n\n// ============================================================================\n// Stateful Components - CounterWithSetup\n// ============================================================================\nfunction CounterWithSetup(handle: Handle, setup: number) {\n  // Setup phase: runs once\n  let count = setup\n\n  // Return render function: runs on every update\n  return (props: { label?: string }) => (\n    <div>\n      {props.label || 'Count'}: {count}\n      <button\n        mix={[\n          on('click', () => {\n            count++\n            handle.update()\n          }),\n        ]}\n      >\n        Increment\n      </button>\n    </div>\n  )\n}\n\n// ============================================================================\n// Setup Prop vs Props - CounterWithLabel\n// ============================================================================\nfunction CounterWithLabel(handle: Handle, setup: number) {\n  let count = setup // use setup for initialization\n\n  return (props: { label?: string }) => (\n    // props only contains render-time values\n    <div>\n      {props.label}: {count}\n      <button\n        mix={[\n          on('click', () => {\n            count++\n            handle.update()\n          }),\n        ]}\n      >\n        +\n      </button>\n    </div>\n  )\n}\n\n// ============================================================================\n// Events - SearchInput\n// ============================================================================\nfunction SearchInput(handle: Handle) {\n  let query = ''\n  let results: string[] = []\n  let loading = false\n\n  return () => (\n    <div>\n      <input\n        type=\"text\"\n        value={query}\n        placeholder=\"Type to search...\"\n        mix={[\n          on('input', (event, signal) => {\n            query = event.currentTarget.value\n            loading = true\n            handle.update()\n\n            // Simulated search with timeout\n            setTimeout(() => {\n              if (signal.aborted) return\n              results = query ? [`Result for \"${query}\" 1`, `Result for \"${query}\" 2`] : []\n              loading = false\n              handle.update()\n            }, 300)\n          }),\n        ]}\n      />\n      {loading && <div>Loading...</div>}\n      {!loading && results.length > 0 && (\n        <ul>\n          {results.map((r) => (\n            <li>{r}</li>\n          ))}\n        </ul>\n      )}\n    </div>\n  )\n}\n\n// ============================================================================\n// Controlled Input - Slug Form\n// ============================================================================\nfunction SlugForm(handle: Handle) {\n  let slug = ''\n  let generatedSlug = ''\n\n  return () => (\n    <form>\n      <label mix={[css({ display: 'flex', alignItems: 'center', gap: '8px' })]}>\n        <input\n          type=\"checkbox\"\n          mix={[\n            on('change', (event) => {\n              if (event.currentTarget.checked) {\n                generatedSlug = crypto.randomUUID().slice(0, 8)\n              } else {\n                generatedSlug = ''\n              }\n              handle.update()\n            }),\n          ]}\n        />\n        Auto-generate slug\n      </label>\n      <label mix={[css({ display: 'flex', flexDirection: 'column', gap: '4px' })]}>\n        Slug\n        <input\n          type=\"text\"\n          value={generatedSlug || slug}\n          disabled={!!generatedSlug}\n          mix={[\n            on('input', (event) => {\n              slug = event.currentTarget.value\n              handle.update()\n            }),\n          ]}\n        />\n      </label>\n    </form>\n  )\n}\n\n// ============================================================================\n// Global Events - KeyboardTracker\n// ============================================================================\nfunction KeyboardTracker(handle: Handle) {\n  let keys: string[] = []\n\n  addEventListeners(document, handle.signal, {\n    keydown: (event) => {\n      keys.push(event.key)\n      if (keys.length > 10) keys.shift()\n      handle.update()\n    },\n  })\n\n  return () => <div>Keys: {keys.join(', ') || '(press some keys)'}</div>\n}\n\n// ============================================================================\n// CSS Prop - Button (Basic)\n// ============================================================================\nfunction ButtonBasic(handle: Handle) {\n  return () => (\n    <button\n      mix={[\n        css({\n          color: 'white',\n          backgroundColor: 'rgb(54, 113, 246)',\n          border: 'none',\n          padding: '8px 16px',\n          borderRadius: '4px',\n          cursor: 'pointer',\n          '&:hover': {\n            backgroundColor: 'rgb(37, 90, 210)',\n          },\n          '&:active': {\n            transform: 'scale(0.98)',\n          },\n        }),\n      ]}\n    >\n      Click me\n    </button>\n  )\n}\n\n// ============================================================================\n// CSS Prop - Button (Advanced with nested rules)\n// ============================================================================\nfunction ButtonAdvanced(handle: Handle) {\n  return () => (\n    <button\n      mix={[\n        css({\n          color: 'white',\n          backgroundColor: 'rgb(54, 113, 246)',\n          border: 'none',\n          padding: '10px 20px',\n          borderRadius: '6px',\n          cursor: 'pointer',\n          position: 'relative',\n          display: 'inline-flex',\n          alignItems: 'center',\n          gap: '8px',\n          '&:hover': {\n            backgroundColor: 'rgb(37, 90, 210)',\n          },\n          '&::before': {\n            content: '\"\"',\n            position: 'absolute',\n            inset: '-2px',\n            borderRadius: '8px',\n            background: 'linear-gradient(45deg, rgb(54, 113, 246), rgb(99, 179, 255))',\n            zIndex: -1,\n            opacity: 0,\n            transition: 'opacity 0.2s',\n          },\n          '&:hover::before': {\n            opacity: 1,\n          },\n          '.icon': {\n            width: '16px',\n            height: '16px',\n          },\n        }),\n      ]}\n    >\n      <span className=\"icon\">★</span>\n      Click me\n    </button>\n  )\n}\n\n// ============================================================================\n// Ref Mixin - Form (Basic)\n// ============================================================================\nfunction FormBasic(handle: Handle) {\n  let inputRef: HTMLInputElement\n\n  return () => (\n    <div>\n      <input\n        type=\"text\"\n        placeholder=\"Click the button to select this\"\n        // capture the input node\n        mix={[ref((node) => (inputRef = node)), css({ marginRight: '8px', padding: '4px 8px' })]}\n      />\n      <button\n        mix={[\n          css({ padding: '4px 12px' }),\n          on('click', () => {\n            // Select it from other parts of the form\n            inputRef.select()\n          }),\n        ]}\n      >\n        Select Input\n      </button>\n    </div>\n  )\n}\n\n// ============================================================================\n// Ref Mixin with AbortSignal - ResizeObserver Component\n// ============================================================================\nfunction ResizeComponent(handle: Handle) {\n  let dimensions = { width: 0, height: 0 }\n\n  return () => (\n    <div\n      mix={[\n        ref((node, signal) => {\n          // Set up something that needs cleanup\n          let observer = new ResizeObserver((entries) => {\n            let entry = entries[0]\n            if (entry) {\n              dimensions.width = Math.round(entry.contentRect.width)\n              dimensions.height = Math.round(entry.contentRect.height)\n              handle.update()\n            }\n          })\n          observer.observe(node)\n\n          // Clean up when element is removed\n          signal.addEventListener('abort', () => {\n            observer.disconnect()\n          })\n        }),\n        css({\n          padding: '20px',\n          backgroundColor: 'rgba(255, 255, 255, 0.1)',\n          borderRadius: '8px',\n          resize: 'both',\n          overflow: 'auto',\n          minWidth: '100px',\n          minHeight: '60px',\n          border: '1px solid rgb(209, 213, 219)',\n        }),\n      ]}\n    >\n      Resize me! ({dimensions.width} × {dimensions.height})\n    </div>\n  )\n}\n\n// ============================================================================\n// handle.update() - Player\n// ============================================================================\nfunction Player(handle: Handle) {\n  let isPlaying = false\n  let playButton: HTMLButtonElement\n  let stopButton: HTMLButtonElement\n\n  return () => (\n    <div mix={[css({ display: 'flex', gap: '8px' })]}>\n      <button\n        disabled={isPlaying}\n        mix={[\n          ref((node) => (playButton = node)),\n          css({\n            padding: '8px 16px',\n            opacity: isPlaying ? 0.5 : 1,\n          }),\n          on('click', async () => {\n            isPlaying = true\n            await handle.update()\n            // Focus the enabled button after update completes\n            stopButton.focus()\n          }),\n        ]}\n      >\n        ▶ Play\n      </button>\n      <button\n        disabled={!isPlaying}\n        mix={[\n          ref((node) => (stopButton = node)),\n          css({\n            padding: '8px 16px',\n            opacity: !isPlaying ? 0.5 : 1,\n          }),\n          on('click', async () => {\n            isPlaying = false\n            await handle.update()\n            // Focus the enabled button after update completes\n            playButton.focus()\n          }),\n        ]}\n      >\n        ⏹ Stop\n      </button>\n    </div>\n  )\n}\n\n// ============================================================================\n// handle.queueTask - Form with scroll\n// ============================================================================\nfunction FormWithScroll(handle: Handle) {\n  let showDetails = false\n  let detailsSection: HTMLElement\n\n  return () => (\n    <div>\n      <label mix={[css({ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' })]}>\n        <input\n          type=\"checkbox\"\n          checked={showDetails}\n          mix={[\n            on('change', (event) => {\n              showDetails = event.currentTarget.checked\n              handle.update()\n              if (showDetails) {\n                // Scroll to the expanded section after it renders\n                handle.queueTask(() => {\n                  detailsSection.scrollIntoView({ behavior: 'smooth', block: 'start' })\n                })\n              }\n            }),\n          ]}\n        />\n        Show additional details\n      </label>\n      {showDetails && (\n        <section\n          mix={[\n            ref((node) => (detailsSection = node)),\n            css({\n              marginTop: '1rem',\n              padding: '1rem',\n              border: '1px solid rgba(255, 255, 255, 0.2)',\n              borderRadius: '8px',\n              backgroundColor: 'rgba(255, 255, 255, 0.05)',\n            }),\n          ]}\n        >\n          <h3 mix={[css({ margin: '0 0 0.5rem 0' })]}>Additional Details</h3>\n          <p mix={[css({ margin: 0 })]}>This section appears when the checkbox is checked.</p>\n        </section>\n      )}\n    </div>\n  )\n}\n\n// ============================================================================\n// handle.signal - Clock\n// ============================================================================\nfunction Clock(handle: Handle) {\n  let interval = setInterval(() => {\n    // clear the interval when the component is disconnected\n    if (handle.signal.aborted) {\n      clearInterval(interval)\n      return\n    }\n    handle.update()\n  }, 1000)\n  return () => <span>{new Date().toLocaleTimeString()}</span>\n}\n\n// ============================================================================\n// handle.id - LabeledInput\n// ============================================================================\nfunction LabeledInput(handle: Handle) {\n  return () => (\n    <div mix={[css({ display: 'flex', flexDirection: 'column', gap: '4px' })]}>\n      <label htmlFor={handle.id}>Name</label>\n      <input\n        id={handle.id}\n        type=\"text\"\n        mix={[\n          css({\n            padding: '4px 8px',\n            borderRadius: '4px',\n            border: '1px solid rgba(255,255,255,0.3)',\n          }),\n        ]}\n      />\n    </div>\n  )\n}\n\n// ============================================================================\n// Context API - Theme Provider and Consumer\n// ============================================================================\nfunction ThemeProvider(handle: Handle<{ theme: string }>) {\n  handle.context.set({ theme: 'dark' })\n\n  return () => (\n    <div mix={[css({ display: 'flex', flexDirection: 'column', gap: '8px' })]}>\n      <ThemedHeader />\n    </div>\n  )\n}\n\nfunction ThemedHeader(handle: Handle) {\n  // Consume context from ThemeProvider\n  let { theme } = handle.context.get(ThemeProvider)\n\n  return () => (\n    <header\n      mix={[\n        css({\n          backgroundColor: theme === 'dark' ? '#000' : '#fff',\n          color: theme === 'dark' ? '#fff' : '#000',\n        }),\n      ]}\n    >\n      Header\n    </header>\n  )\n}\n\n// ============================================================================\n// Context API with EventTarget - Advanced Theme\n// ============================================================================\nclass Theme extends TypedEventTarget<{ change: Event }> {\n  #value: 'light' | 'dark' = 'light'\n\n  get value() {\n    return this.#value\n  }\n\n  setValue(value: 'light' | 'dark') {\n    this.#value = value\n    this.dispatchEvent(new Event('change'))\n  }\n}\n\nfunction ThemeProviderAdvanced(handle: Handle<Theme>) {\n  let theme = new Theme()\n  handle.context.set(theme)\n\n  return () => (\n    <div mix={[css({ display: 'flex', flexDirection: 'column', gap: '8px' })]}>\n      <button\n        mix={[\n          css({ padding: '8px 16px', alignSelf: 'flex-start' }),\n          on('click', () => {\n            // no updates in the parent component\n            theme.setValue(theme.value === 'light' ? 'dark' : 'light')\n          }),\n        ]}\n      >\n        Toggle Theme (EventTarget)\n      </button>\n      <ThemedContent />\n    </div>\n  )\n}\n\nfunction ThemedContent(handle: Handle) {\n  let theme = handle.context.get(ThemeProviderAdvanced)\n\n  // Subscribe to theme changes and update when it changes\n  addEventListeners(theme, handle.signal, {\n    change() {\n      handle.update()\n    },\n  })\n\n  return () => (\n    <div\n      mix={[\n        css({\n          padding: '12px',\n          borderRadius: '6px',\n          backgroundColor: theme.value === 'dark' ? '#1a1a1a' : '#f0f0f0',\n          color: theme.value === 'dark' ? '#fff' : '#000',\n        }),\n      ]}\n    >\n      Current theme: {theme.value}\n    </div>\n  )\n}\n\n// ============================================================================\n// Fragments - List\n// ============================================================================\nfunction ListWithFragment(handle: Handle) {\n  return () => (\n    <ul mix={[css({ margin: 0, paddingLeft: '20px' })]}>\n      <>\n        <li>Item 1</li>\n        <li>Item 2</li>\n        <li>Item 3</li>\n      </>\n    </ul>\n  )\n}\n\n// ============================================================================\n// Example Container Component\n// ============================================================================\nfunction Example(handle: Handle) {\n  return (props: { title: string; children: RemixNode }) => (\n    <div className=\"example\">\n      <h2>{props.title}</h2>\n      <div className=\"example-content\">{props.children}</div>\n    </div>\n  )\n}\n\n// ============================================================================\n// Main Demo App\n// ============================================================================\nfunction DemoApp(handle: Handle) {\n  return () => (\n    <div className=\"examples-grid\">\n      <Example title=\"Getting Started - Counter\">\n        <App />\n      </Example>\n\n      <Example title=\"Component State - Counter\">\n        <Counter />\n      </Example>\n\n      <Example title=\"Greeting\">\n        <Greeting name=\"World\" />\n      </Example>\n\n      <Example title=\"Counter with Setup\">\n        <CounterWithSetup setup={10} label=\"Total\" />\n      </Example>\n\n      <Example title=\"Setup vs Props\">\n        <CounterWithLabel setup={5} label=\"Score\" />\n      </Example>\n\n      <Example title=\"Events - Search Input\">\n        <SearchInput />\n      </Example>\n\n      <Example title=\"Controlled Input - Slug Form\">\n        <SlugForm />\n      </Example>\n\n      <Example title=\"Global Events - Keyboard Tracker\">\n        <KeyboardTracker />\n      </Example>\n\n      <Example title=\"CSS Prop - Basic Button\">\n        <ButtonBasic />\n      </Example>\n\n      <Example title=\"CSS Prop - Advanced Button\">\n        <ButtonAdvanced />\n      </Example>\n\n      <Example title=\"Ref Mixin - Form\">\n        <FormBasic />\n      </Example>\n\n      <Example title=\"Ref with AbortSignal - Resize Observer\">\n        <ResizeComponent />\n      </Example>\n\n      <Example title=\"handle.update() - Player\">\n        <Player />\n      </Example>\n\n      <Example title=\"handle.queueTask - Scroll to Section\">\n        <FormWithScroll />\n      </Example>\n\n      <Example title=\"handle.signal - Clock\">\n        <Clock />\n      </Example>\n\n      <Example title=\"handle.id - Labeled Input\">\n        <LabeledInput />\n      </Example>\n\n      <Example title=\"Context API - Theme Provider\">\n        <ThemeProvider />\n      </Example>\n\n      <Example title=\"Context with EventTarget\">\n        <ThemeProviderAdvanced />\n      </Example>\n\n      <Example title=\"Fragments - List\">\n        <ListWithFragment />\n      </Example>\n    </div>\n  )\n}\n\ncreateRoot(document.getElementById('app')!).render(<DemoApp />)\n"
  },
  {
    "path": "packages/component/demos/readme/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>README Examples</title>\n    <style>\n      * {\n        box-sizing: border-box;\n      }\n      body {\n        font-family: 'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif;\n        background: #f8f9fa;\n        min-height: 100vh;\n        margin: 0;\n        padding: 2rem;\n        color: #2d3748;\n      }\n      h1 {\n        color: #1a202c;\n        text-align: center;\n        margin-bottom: 2rem;\n        font-weight: 600;\n        letter-spacing: -0.02em;\n        font-size: 2rem;\n      }\n      .examples-grid {\n        display: grid;\n        grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n        gap: 1.5rem;\n        max-width: 1400px;\n        margin: 0 auto;\n      }\n      .example {\n        background: #fff;\n        border: 1px solid #e2e8f0;\n        border-radius: 12px;\n        padding: 1.5rem;\n        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);\n      }\n      .example h2 {\n        color: rgb(54, 113, 246);\n        font-size: 0.85rem;\n        margin: 0 0 1rem 0;\n        padding-bottom: 0.5rem;\n        border-bottom: 1px solid #e2e8f0;\n        font-weight: 600;\n        letter-spacing: 0.02em;\n      }\n      .example-content {\n        min-height: 60px;\n      }\n      button {\n        font-family: inherit;\n      }\n      input {\n        font-family: inherit;\n      }\n    </style>\n  </head>\n  <body>\n    <h1>README Examples</h1>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./entry.bundled.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/demos/server.ts",
    "content": "import * as fs from 'node:fs'\nimport * as http from 'node:http'\nimport * as path from 'node:path'\n\nimport { createRouter } from 'remix/fetch-router'\nimport { route } from 'remix/fetch-router/routes'\nimport { createRequestListener } from 'remix/node-fetch-server'\nimport { staticFiles } from 'remix/static-middleware'\n\nlet demosDir = path.resolve(import.meta.dirname)\n\nlet routes = route({\n  index: '/',\n})\n\nlet router = createRouter({\n  middleware: [staticFiles('.')],\n})\n\nlet html = String.raw\n\nrouter.get(routes.index, () => {\n  let entries = fs.readdirSync(demosDir, { withFileTypes: true })\n  let demos = entries\n    .filter((entry) => entry.isDirectory() && entry.name !== 'node_modules')\n    .map((entry) => entry.name)\n    .sort()\n\n  let links = demos.map((name) => `<li><a href=\"/${name}/index.html\">${name}</a></li>`).join('')\n\n  return new Response(\n    html`<!doctype html>\n      <html>\n        <head>\n          <title>Component Demos</title>\n          <style>\n            body {\n              font-family:\n                system-ui,\n                -apple-system,\n                BlinkMacSystemFont,\n                sans-serif;\n              max-width: 600px;\n              margin: 40px auto;\n              padding: 0 20px;\n            }\n            h1 {\n              margin-bottom: 24px;\n            }\n            ul {\n              list-style: none;\n              padding: 0;\n            }\n            li {\n              margin: 8px 0;\n            }\n            a {\n              color: #337ab7;\n              text-decoration: none;\n              font-size: 18px;\n            }\n            a:hover {\n              text-decoration: underline;\n            }\n          </style>\n        </head>\n        <body>\n          <h1>Component Demos</h1>\n          <ul>\n            ${links}\n          </ul>\n        </body>\n      </html>`,\n    { headers: { 'Content-Type': 'text/html' } },\n  )\n})\n\nlet server = http.createServer(\n  createRequestListener(async (request) => await router.fetch(request)),\n)\n\nserver.listen(44100, () => {\n  console.log('Demos server running at http://localhost:44100')\n})\n\nfunction shutdown() {\n  server.close(() => {\n    process.exit(0)\n  })\n}\n\nprocess.on('SIGINT', shutdown)\nprocess.on('SIGTERM', shutdown)\n"
  },
  {
    "path": "packages/component/demos/spring/drag-release.ts",
    "content": "import { addEventListeners, createMixin } from 'remix/component'\n\nexport let dragVelocityReleaseEventType = 'rmx:drag-velocity-release' as const\n\ndeclare global {\n  interface HTMLElementEventMap {\n    [dragVelocityReleaseEventType]: DragVelocityEvent\n  }\n}\n\nexport class DragVelocityEvent extends Event {\n  clientX: number\n  clientY: number\n  velocityX: number // px/s\n  velocityY: number // px/s\n\n  constructor(\n    type: typeof dragVelocityReleaseEventType,\n    init: { clientX: number; clientY: number; velocityX: number; velocityY: number },\n  ) {\n    super(type, { bubbles: true, cancelable: true })\n    this.clientX = init.clientX\n    this.clientY = init.clientY\n    this.velocityX = init.velocityX\n    this.velocityY = init.velocityY\n  }\n}\n\nlet baseDragVelocityEvents = createMixin<HTMLElement>((handle) => {\n  let target: HTMLElement\n  let isTracking = false\n  let pointerId: number | null = null\n  let lastX = 0\n  let lastY = 0\n  let lastTime = 0\n  let velocityX = 0\n  let velocityY = 0\n\n  let onPointerDown = (event: PointerEvent) => {\n    if (!event.isPrimary) return\n    isTracking = true\n    pointerId = event.pointerId\n    lastX = event.clientX\n    lastY = event.clientY\n    lastTime = performance.now()\n    velocityX = 0\n    velocityY = 0\n    target.setPointerCapture(event.pointerId)\n  }\n\n  let onPointerMove = (event: PointerEvent) => {\n    if (!isTracking) return\n    if (!event.isPrimary) return\n    if (pointerId != null && event.pointerId !== pointerId) return\n\n    let now = performance.now()\n    let dt = (now - lastTime) / 1000 // seconds\n\n    if (dt > 0) {\n      // Smooth velocity with some decay of previous velocity\n      let newVelocityX = (event.clientX - lastX) / dt\n      let newVelocityY = (event.clientY - lastY) / dt\n      velocityX = velocityX * 0.5 + newVelocityX * 0.5\n      velocityY = velocityY * 0.5 + newVelocityY * 0.5\n    }\n\n    lastX = event.clientX\n    lastY = event.clientY\n    lastTime = now\n  }\n\n  let onPointerUp = (event: PointerEvent) => {\n    if (!isTracking) return\n    if (!event.isPrimary) return\n    if (pointerId != null && event.pointerId !== pointerId) return\n    isTracking = false\n    pointerId = null\n\n    // If too much time passed since last move, velocity is zero\n    let timeSinceLastMove = (performance.now() - lastTime) / 1000\n    if (timeSinceLastMove > 0.1) {\n      velocityX = 0\n      velocityY = 0\n    }\n\n    target.dispatchEvent(\n      new DragVelocityEvent(dragVelocityReleaseEventType, {\n        clientX: event.clientX,\n        clientY: event.clientY,\n        velocityX,\n        velocityY,\n      }),\n    )\n  }\n\n  let onPointerCancel = () => {\n    isTracking = false\n    pointerId = null\n  }\n\n  handle.addEventListener('insert', (event) => {\n    target = event.node\n    addEventListeners(target, handle.signal, {\n      pointerdown: onPointerDown,\n      pointermove: onPointerMove,\n      pointerup: onPointerUp,\n      pointercancel: onPointerCancel,\n    })\n  })\n})\n\ntype DragVelocityEventsMixin = typeof baseDragVelocityEvents & {\n  readonly release: typeof dragVelocityReleaseEventType\n}\n\nexport let dragVelocityEvents: DragVelocityEventsMixin = Object.assign(baseDragVelocityEvents, {\n  release: dragVelocityReleaseEventType,\n})\n"
  },
  {
    "path": "packages/component/demos/spring/entry.tsx",
    "content": "import { addEventListeners, createRoot, css, on, ref, type Handle } from 'remix/component'\n\nimport { dragVelocityEvents } from './drag-release.ts'\nimport { spring, type SpringPreset } from 'remix/component'\n\ninterface TrailPoint {\n  x: number\n  y: number\n  time: number\n}\n\nfunction PointerTrail(handle: Handle) {\n  let canvas: HTMLCanvasElement\n  let points: TrailPoint[] = []\n  let isDown = false\n  let releaseTime = 0\n  let animationId: number | null = null\n\n  let maxAge = 150 // ms - how long points stay in the trail while dragging\n  let fadeDuration = 800 // ms - how long the trail fades after release\n\n  function draw() {\n    if (!canvas) return\n    let ctx = canvas.getContext('2d')\n    if (!ctx) return\n\n    let now = performance.now()\n    ctx.clearRect(0, 0, canvas.width, canvas.height)\n\n    // Calculate overall opacity based on release time\n    let overallOpacity = 1\n    if (!isDown && releaseTime > 0) {\n      let elapsed = now - releaseTime\n      overallOpacity = Math.max(0, 1 - elapsed / fadeDuration)\n      if (overallOpacity <= 0) {\n        points = []\n        animationId = null\n        return\n      }\n    }\n\n    // Filter out old points while dragging\n    if (isDown) {\n      points = points.filter((p) => now - p.time < maxAge)\n    }\n\n    if (points.length < 2) {\n      if (isDown || overallOpacity > 0) {\n        animationId = requestAnimationFrame(draw)\n      } else {\n        animationId = null\n      }\n      return\n    }\n\n    // Draw the trail as a tapered path\n    ctx.beginPath()\n    ctx.lineCap = 'round'\n    ctx.lineJoin = 'round'\n\n    for (let i = 1; i < points.length; i++) {\n      let prev = points[i - 1]\n      let curr = points[i]\n\n      // Age-based opacity (older = more transparent)\n      let age = isDown ? (now - prev.time) / maxAge : 0\n      let segmentOpacity = (1 - age) * overallOpacity\n\n      // Thickness tapers from thin (old) to thick (new)\n      let progress = i / (points.length - 1)\n      let thickness = 2 + progress * 8\n\n      ctx.beginPath()\n      ctx.moveTo(prev.x, prev.y)\n      ctx.lineTo(curr.x, curr.y)\n      ctx.strokeStyle = `rgba(14, 165, 233, ${segmentOpacity * 0.6})`\n      ctx.lineWidth = thickness\n      ctx.stroke()\n    }\n\n    // Draw a red glow at the end to show direction\n    if (points.length > 0) {\n      let last = points[points.length - 1]\n      let gradient = ctx.createRadialGradient(last.x, last.y, 0, last.x, last.y, 20)\n      gradient.addColorStop(0, `rgba(239, 68, 68, ${overallOpacity * 0.6})`)\n      gradient.addColorStop(1, 'rgba(239, 68, 68, 0)')\n      ctx.fillStyle = gradient\n      ctx.beginPath()\n      ctx.arc(last.x, last.y, 20, 0, Math.PI * 2)\n      ctx.fill()\n    }\n\n    animationId = requestAnimationFrame(draw)\n  }\n\n  function startDrawing() {\n    if (!animationId) {\n      animationId = requestAnimationFrame(draw)\n    }\n  }\n\n  addEventListeners(document, handle.signal, {\n    pointerdown(event) {\n      if (!(event.target as HTMLElement).closest('.draggable')) return\n      isDown = true\n      releaseTime = 0\n      points = [{ x: event.clientX, y: event.clientY, time: performance.now() }]\n      startDrawing()\n    },\n\n    pointermove(event) {\n      if (!isDown) return\n      points.push({ x: event.clientX, y: event.clientY, time: performance.now() })\n    },\n\n    pointerup() {\n      if (!isDown) return\n      isDown = false\n      releaseTime = performance.now()\n    },\n\n    pointercancel() {\n      isDown = false\n      releaseTime = performance.now()\n    },\n  })\n\n  return () => (\n    <canvas\n      mix={[\n        ref((node) => {\n          canvas = node\n          canvas.width = window.innerWidth\n          canvas.height = window.innerHeight\n        }),\n        css({\n          position: 'absolute',\n          inset: 0,\n          pointerEvents: 'none',\n          zIndex: 5,\n        }),\n      ]}\n    />\n  )\n}\n\nfunction SpringDemo(handle: Handle) {\n  // Target circle position (click to move)\n  let targetX = window.innerWidth / 2\n  let targetY = window.innerHeight / 2\n\n  // Draggable circle state\n  let dragX = window.innerWidth / 2 - 150\n  let dragY = window.innerHeight / 2\n  let isDragging = false\n  let isAnimating = false\n\n  // Current spring transitions (separate for X and Y to capture 2D velocity)\n  let transitionX = ''\n  let transitionY = ''\n\n  let selectedPreset: SpringPreset = 'bouncy'\n\n  // Get default spring transition for target circle\n  let springValue = spring(selectedPreset)\n\n  addEventListeners(document, handle.signal, {\n    click(event) {\n      // Ignore clicks on controls or when dragging\n      if ((event.target as HTMLElement).closest('.controls')) return\n      if ((event.target as HTMLElement).closest('.draggable')) return\n\n      targetX = event.clientX\n      targetY = event.clientY\n      handle.update()\n    },\n  })\n\n  return () => (\n    <div\n      mix={[\n        css({\n          position: 'fixed',\n          inset: 0,\n          backgroundColor: '#1a1a2e',\n          cursor: 'crosshair',\n          overflow: 'hidden',\n        }),\n      ]}\n    >\n      {/* Pointer trail */}\n      <PointerTrail />\n\n      {/* Target circle */}\n      <div\n        mix={[\n          css({\n            position: 'absolute',\n            width: '80px',\n            height: '80px',\n            borderRadius: '50%',\n            backgroundColor: 'transparent',\n            border: '3px dashed rgba(233, 69, 96, 0.5)',\n            transform: 'translate(-50%, -50%)',\n            pointerEvents: 'none',\n          }),\n        ]}\n        style={{\n          left: `${targetX}px`,\n          top: `${targetY}px`,\n          transition: `left ${springValue}, top ${springValue}`,\n        }}\n      />\n\n      {/* Draggable circle */}\n      <div\n        className=\"draggable\"\n        mix={[\n          css({\n            position: 'absolute',\n            width: '60px',\n            height: '60px',\n            borderRadius: '50%',\n            backgroundColor: '#0ea5e9',\n            boxShadow: '0 0 20px rgba(14, 165, 233, 0.5)',\n            transform: 'translate(-50%, -50%)',\n            userSelect: 'none',\n            touchAction: 'none',\n            zIndex: 10,\n          }),\n          on('transitionend', () => {\n            isAnimating = false\n            handle.update()\n          }),\n          on('pointerdown', (event) => {\n            event.preventDefault()\n            isDragging = true\n            isAnimating = false\n            handle.update()\n          }),\n          on('pointermove', (event) => {\n            if (!isDragging) return\n            dragX = event.clientX\n            dragY = event.clientY\n            handle.update()\n          }),\n          dragVelocityEvents(),\n          on(dragVelocityEvents.release, (event) => {\n            isDragging = false\n\n            // Calculate distance to target on each axis\n            let distX = targetX - dragX\n            let distY = targetY - dragY\n\n            if (Math.abs(distX) < 1 && Math.abs(distY) < 1) {\n              handle.update()\n              return\n            }\n\n            // Create separate springs for X and Y with their own normalized velocities\n            // normalizedVelocity = velocity / distance (with sign!) for each axis\n            // The sign matters: positive = moving toward target, negative = moving away\n            if (Math.abs(distX) >= 1) {\n              let normalizedVelocityX = event.velocityX / distX\n              normalizedVelocityX = Math.max(-20, Math.min(20, normalizedVelocityX))\n              transitionX = String(spring(selectedPreset, { velocity: normalizedVelocityX }))\n            } else {\n              transitionX = String(spring(selectedPreset))\n            }\n\n            if (Math.abs(distY) >= 1) {\n              let normalizedVelocityY = event.velocityY / distY\n              normalizedVelocityY = Math.max(-20, Math.min(20, normalizedVelocityY))\n              transitionY = String(spring(selectedPreset, { velocity: normalizedVelocityY }))\n            } else {\n              transitionY = String(spring(selectedPreset))\n            }\n\n            // Animate to target\n            isAnimating = true\n            dragX = targetX\n            dragY = targetY\n            handle.update()\n          }),\n        ]}\n        style={{\n          left: `${dragX}px`,\n          top: `${dragY}px`,\n          cursor: isDragging ? 'grabbing' : 'grab',\n          transition: isAnimating ? `left ${transitionX}, top ${transitionY}` : 'none',\n        }}\n      />\n\n      {/* Controls */}\n      <div\n        className=\"controls\"\n        mix={[\n          css({\n            position: 'absolute',\n            top: '20px',\n            left: '50%',\n            transform: 'translateX(-50%)',\n            display: 'flex',\n            gap: '4px',\n            padding: '4px',\n            backgroundColor: 'rgba(255, 255, 255, 0.1)',\n            borderRadius: '8px',\n            fontFamily: 'system-ui, sans-serif',\n            fontSize: '14px',\n            cursor: 'default',\n          }),\n        ]}\n      >\n        {(Object.keys(spring.presets) as SpringPreset[]).map((preset) => (\n          <label\n            key={preset}\n            mix={[\n              css({\n                display: 'flex',\n                alignItems: 'center',\n                gap: '6px',\n                padding: '8px 12px',\n                borderRadius: '6px',\n                color: '#fff',\n                cursor: 'pointer',\n                transition: 'background-color 150ms ease',\n                '&:hover': {\n                  backgroundColor: 'rgba(255, 255, 255, 0.1)',\n                },\n                '&:has(input:checked)': {\n                  backgroundColor: 'rgba(14, 165, 233, 0.3)',\n                },\n              }),\n            ]}\n          >\n            <input\n              type=\"radio\"\n              name=\"spring-preset\"\n              value={preset}\n              checked={selectedPreset === preset}\n              mix={[\n                css({ accentColor: '#0ea5e9' }),\n                on('change', () => {\n                  selectedPreset = preset\n                  springValue = spring(selectedPreset)\n                  handle.update()\n                }),\n              ]}\n            />\n            {preset}\n          </label>\n        ))}\n      </div>\n\n      {/* Instructions */}\n      <div\n        mix={[\n          css({\n            position: 'absolute',\n            bottom: '20px',\n            left: '50%',\n            transform: 'translateX(-50%)',\n            color: '#ffffff80',\n            fontFamily: 'system-ui, sans-serif',\n            fontSize: '14px',\n            textAlign: 'center',\n          }),\n        ]}\n      >\n        Click to move target • Drag the blue circle and release to see velocity-based spring\n      </div>\n    </div>\n  )\n}\n\ncreateRoot(document.body).render(<SpringDemo />)\n"
  },
  {
    "path": "packages/component/demos/spring/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Spring Animation</title>\n  </head>\n  <body>\n    <script type=\"module\" src=\"./entry.bundled.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/component/demos/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\", \"DOM.AsyncIterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"types\": [\"node\", \"dom-navigation\"],\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"remix/component\",\n    \"paths\": {\n      \"*\": [\"./*\"],\n      \"remix/component/jsx-runtime\": [\"../../remix/src/component/jsx-runtime.ts\"],\n      \"remix/component/jsx-dev-runtime\": [\"../../remix/src/component/jsx-dev-runtime.ts\"]\n    }\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/component/docs/components.md",
    "content": "# Components\n\nAll components follow a consistent two-phase structure.\n\n## Component Structure\n\n1. **Setup Phase** - Runs once when the component is first created\n2. **Render Phase** - Runs on initial render and every update afterward\n\n```tsx\nfunction MyComponent(handle: Handle, setup: SetupType) {\n  // Setup phase: runs once\n  let state = initializeState(setup)\n\n  // Return render function: runs on every update\n  return (props: Props) => {\n    return <div>{/* render content */}</div>\n  }\n}\n```\n\n## Runtime Behavior\n\nWhen a component is rendered:\n\n1. **First Render**:\n\n   - The component function is called with `handle` and the `setup` prop\n   - The returned render function is stored\n   - The render function is called with regular props\n   - Any tasks queued via `handle.queueTask()` are executed after rendering\n\n2. **Subsequent Updates**:\n\n   - Only the render function is called\n   - Setup phase is skipped, setup closure persists for the lifetime of the component instance\n   - Props are passed to the render function\n   - The `setup` prop is stripped from props\n   - Tasks queued during the update are executed after rendering\n\n3. **Component Removal**:\n   - `handle.signal` is aborted\n   - All event listeners registered via `addEventListeners()` are automatically cleaned up\n   - Any queued tasks are executed with an aborted signal\n\n## Setup vs Props\n\nThe `setup` prop is special—it's only available in the setup phase and is automatically excluded from props. This prevents accidental stale captures:\n\n```tsx\nfunction Counter(handle: Handle, setup: number) {\n  // setup prop (e.g., initialCount) only available here\n  let count = setup\n\n  return (props: { label: string }) => {\n    // props only receives { label } - setup is excluded\n    return (\n      <div>\n        {props.label}: {count}\n      </div>\n    )\n  }\n}\n\n// Usage\nlet element = <Counter setup={10} label=\"Count\" />\n```\n\n## Basic Rendering\n\nThe simplest component just returns JSX:\n\n```tsx\nfunction Greeting() {\n  return (props: { name: string }) => <div>Hello, {props.name}!</div>\n}\n\nlet el = <Greeting name=\"World\" />\n```\n\n## Prop Passing\n\nProps flow from parent to child through JSX attributes:\n\n```tsx\nfunction Parent() {\n  return () => <Child message=\"Hello from parent\" count={42} />\n}\n\nfunction Child() {\n  return (props: { message: string; count: number }) => (\n    <div>\n      <p>{props.message}</p>\n      <p>Count: {props.count}</p>\n    </div>\n  )\n}\n```\n\n## Stateful Updates\n\nState is managed with plain JavaScript variables. Call `handle.update()` to trigger a re-render:\n\n```tsx\nfunction Counter(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <div>\n      <span>Count: {count}</span>\n      <button\n        mix={[\n          on('click', () => {\n            count++\n            handle.update()\n          }),\n        ]}\n      >\n        Increment\n      </button>\n    </div>\n  )\n}\n```\n\n## See Also\n\n- [Handle API](./handle.md) - Complete handle API reference\n- [Patterns](./patterns.md) - State management best practices\n"
  },
  {
    "path": "packages/component/docs/composition.md",
    "content": "# Composition\n\nBuilding component trees with props, children, refs, and keys.\n\n## Props\n\nProps flow from parent to child through JSX attributes:\n\n```tsx\nfunction Parent() {\n  return () => <Child message=\"Hello from parent\" count={42} />\n}\n\nfunction Child() {\n  return (props: { message: string; count: number }) => (\n    <div>\n      <p>{props.message}</p>\n      <p>Count: {props.count}</p>\n    </div>\n  )\n}\n```\n\n## Children\n\nComponents can compose other components via `children`:\n\n```tsx\nfunction Layout() {\n  return (props: { children: RemixNode }) => (\n    <div mix={[css({ padding: '20px', maxWidth: '1200px', margin: '0 auto' })]}>\n      <header>My App</header>\n      <main>{props.children}</main>\n      <footer>© 2024</footer>\n    </div>\n  )\n}\n\nfunction App() {\n  return () => (\n    <Layout>\n      <h1>Welcome</h1>\n      <p>Content goes here</p>\n    </Layout>\n  )\n}\n```\n\n## Ref Mixin\n\nUse the `ref(...)` mixin 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.\n\n```tsx\nfunction Form(handle: Handle) {\n  let inputRef: HTMLInputElement\n\n  return () => (\n    <form>\n      <input type=\"text\" mix={[ref((node) => (inputRef = node))]} />\n      <button\n        mix={[\n          on('click', () => {\n            // Focus the input from elsewhere in the form\n            inputRef.focus()\n          }),\n        ]}\n      >\n        Focus Input\n      </button>\n    </form>\n  )\n}\n```\n\nThe `ref` callback receives an `AbortSignal` as its second parameter, which is aborted when the element is removed from the DOM. Use this for cleanup operations:\n\n```tsx\nfunction ResizeTracker(handle: Handle) {\n  let dimensions = { width: 0, height: 0 }\n\n  return () => (\n    <div\n      mix={[\n        ref((node, signal) => {\n          // Set up ResizeObserver\n          let observer = new ResizeObserver((entries) => {\n            let entry = entries[0]\n            if (entry) {\n              dimensions.width = Math.round(entry.contentRect.width)\n              dimensions.height = Math.round(entry.contentRect.height)\n              handle.update()\n            }\n          })\n          observer.observe(node)\n\n          // Clean up when element is removed\n          signal.addEventListener('abort', () => {\n            observer.disconnect()\n          })\n        }),\n      ]}\n    >\n      Size: {dimensions.width} x {dimensions.height}\n    </div>\n  )\n}\n```\n\nThe `ref` callback is called only once when the element is first rendered, not on every update.\n\n## Key Prop\n\nUse 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.\n\n```tsx\nfunction TodoList(handle: Handle) {\n  let todos = [\n    { id: '1', text: 'Buy milk' },\n    { id: '2', text: 'Walk dog' },\n    { id: '3', text: 'Write code' },\n  ]\n\n  return () => (\n    <ul>\n      {todos.map((todo) => (\n        <li key={todo.id}>{todo.text}</li>\n      ))}\n    </ul>\n  )\n}\n```\n\nWhen you reorder, add, or remove items, keys ensure:\n\n- **DOM nodes are reused** - Elements with matching keys are moved, not recreated\n- **Component state is preserved** - Component instances persist across reorders\n- **Focus and selection are maintained** - Input focus stays with the same element\n- **Input values are preserved** - Form values remain with their elements\n\n```tsx\nfunction ReorderableList(handle: Handle) {\n  let items = [\n    { id: 'a', label: 'Item A' },\n    { id: 'b', label: 'Item B' },\n    { id: 'c', label: 'Item C' },\n  ]\n\n  function reverse() {\n    items = [...items].reverse()\n    handle.update()\n  }\n\n  return () => (\n    <div>\n      <button mix={[on('click', reverse)]}>Reverse List</button>\n      {items.map((item) => (\n        <div key={item.id}>\n          <input type=\"text\" defaultValue={item.label} />\n        </div>\n      ))}\n    </div>\n  )\n}\n```\n\nEven 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.\n\nKeys can be any type (string, number, bigint, object, symbol), but should be stable and unique within the list:\n\n```tsx\n// Good: stable, unique IDs\n{\n  items.map((item) => <Item key={item.id} item={item} />)\n}\n\n// Good: index can work if list never reorders\n{\n  items.map((item, index) => <Item key={index} item={item} />)\n}\n\n// Bad: don't use random values or values that change\n{\n  items.map((item) => <Item key={Math.random()} item={item} />)\n}\n```\n\n## See Also\n\n- [Context](./context.md) - Indirect composition without prop drilling\n"
  },
  {
    "path": "packages/component/docs/context.md",
    "content": "# Context\n\nContext enables components to communicate without direct prop passing.\n\n## Basic Context\n\nUse `handle.context.set()` to provide values and `handle.context.get()` to consume them:\n\n```tsx\nfunction ThemeProvider(handle: Handle<{ theme: 'light' | 'dark' }>) {\n  let theme: 'light' | 'dark' = 'light'\n\n  handle.context.set({ theme })\n\n  return (props: { children: RemixNode }) => (\n    <div>\n      <button\n        mix={[\n          on('click', () => {\n            theme = theme === 'light' ? 'dark' : 'light'\n            handle.context.set({ theme })\n            handle.update()\n          }),\n        ]}\n      >\n        Toggle Theme\n      </button>\n      {props.children}\n    </div>\n  )\n}\n\nfunction ThemedContent(handle: Handle) {\n  let { theme } = handle.context.get(ThemeProvider)\n\n  return () => (\n    <div mix={[css({ backgroundColor: theme === 'dark' ? '#000' : '#fff' })]}>\n      Current theme: {theme}\n    </div>\n  )\n}\n```\n\n**Important:** `handle.context.set()` does not cause any updates—it simply stores a value. If you want the component tree to update when context changes, you must call `handle.update()` after setting the context (as shown above).\n\n## TypedEventTarget for Granular Updates\n\nFor better performance, use `TypedEventTarget` to avoid updating the entire subtree. This allows descendants to subscribe to specific changes rather than re-rendering on every parent update:\n\n```tsx\nimport { TypedEventTarget } from 'remix/component'\n\nclass Theme extends TypedEventTarget<{ change: Event }> {\n  #value: 'light' | 'dark' = 'light'\n\n  get value() {\n    return this.#value\n  }\n\n  setValue(value: 'light' | 'dark') {\n    this.#value = value\n    this.dispatchEvent(new Event('change'))\n  }\n}\n\nfunction ThemeProvider(handle: Handle<Theme>) {\n  let theme = new Theme()\n  handle.context.set(theme)\n\n  return (props: { children: RemixNode }) => (\n    <div>\n      <button\n        mix={[\n          on('click', () => {\n            // No update needed - consumers subscribe to changes\n            theme.setValue(theme.value === 'light' ? 'dark' : 'light')\n          }),\n        ]}\n      >\n        Toggle Theme\n      </button>\n      {props.children}\n    </div>\n  )\n}\n\nfunction ThemedContent(handle: Handle) {\n  let theme = handle.context.get(ThemeProvider)\n\n  // Subscribe to granular updates\n  addEventListeners(theme, handle.signal, {\n    change() {\n      handle.update()\n    },\n  })\n\n  return () => (\n    <div mix={[css({ backgroundColor: theme.value === 'dark' ? '#000' : '#fff' })]}>\n      Current theme: {theme.value}\n    </div>\n  )\n}\n```\n\nBenefits of this pattern:\n\n- **No unnecessary re-renders**: Only components that subscribe to changes are updated\n- **Decoupled updates**: The provider doesn't need to call `handle.update()` when context changes\n- **Type-safe events**: `TypedEventTarget` ensures event handlers receive the correct event types\n\n## Context with Multiple Values\n\nProvide multiple related values through context:\n\n```tsx\nclass AppContext extends TypedEventTarget<{ userChange: Event; settingsChange: Event }> {\n  #user: User | null = null\n  #settings: Settings = defaultSettings\n\n  get user() {\n    return this.#user\n  }\n\n  get settings() {\n    return this.#settings\n  }\n\n  setUser(user: User | null) {\n    this.#user = user\n    this.dispatchEvent(new Event('userChange'))\n  }\n\n  setSettings(settings: Settings) {\n    this.#settings = settings\n    this.dispatchEvent(new Event('settingsChange'))\n  }\n}\n\nfunction AppProvider(handle: Handle<AppContext>) {\n  let context = new AppContext()\n  handle.context.set(context)\n\n  return (props: { children: RemixNode }) => props.children\n}\n\n// Components can subscribe to only the events they care about\nfunction UserDisplay(handle: Handle) {\n  let context = handle.context.get(AppProvider)\n\n  addEventListeners(context, handle.signal, {\n    userChange() {\n      handle.update()\n    },\n  })\n\n  return () => <div>{context.user?.name ?? 'Not logged in'}</div>\n}\n```\n\n## See Also\n\n- [Handle API](./handle.md) - `handle.context` reference\n- [Events](./events.md) - `addEventListeners()` for subscribing to EventTargets\n"
  },
  {
    "path": "packages/component/docs/events.md",
    "content": "# Events\n\nEvent handling with the `on()` mixin and signal-based interruption management.\n\n## Basic Event Handling\n\nUse the `on()` mixin to attach event listeners to elements:\n\n```tsx\nfunction Button(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <button\n      mix={[\n        on('click', () => {\n          count++\n          handle.update()\n        }),\n      ]}\n    >\n      Clicked {count} times\n    </button>\n  )\n}\n```\n\n## Event Handler Signature\n\nEvent handlers receive the event object and an optional `AbortSignal`:\n\n```tsx\nmix={[on('click', (event) => {\n    // event is the DOM event\n    event.preventDefault()\n  }), on('input', async (event, signal) => {\n    // signal is aborted when handler is re-entered or component removed\n    let response = await fetch('/api', { signal })\n  })]}\n```\n\n## Signals in Event Handlers\n\nEvent handlers receive an `AbortSignal` that's automatically aborted when:\n\n- The handler is re-entered (user triggers another event before the previous one completes)\n- The component is removed from the tree\n\nThis prevents race conditions when users create events faster than async work completes:\n\n```tsx\nfunction SearchInput(handle: Handle) {\n  let results: string[] = []\n  let loading = false\n\n  return () => (\n    <div>\n      <input\n        type=\"text\"\n        mix={[\n          on('input', async (event, signal) => {\n            let query = event.currentTarget.value\n            loading = true\n            handle.update()\n\n            // Passing signal automatically aborts previous requests\n            let response = await fetch(`/search?q=${query}`, { signal })\n            let data = await response.json()\n            // Manual check for APIs that don't accept a signal\n            if (signal.aborted) return\n\n            results = data.results\n            loading = false\n            handle.update()\n          }),\n        ]}\n      />\n      {loading && <div>Loading...</div>}\n      {!loading && results.length > 0 && (\n        <ul>\n          {results.map((result, i) => (\n            <li key={i}>{result}</li>\n          ))}\n        </ul>\n      )}\n    </div>\n  )\n}\n```\n\nThe signal ensures only the latest search request completes, preventing stale results from overwriting newer ones.\n\n## Multiple Event Types\n\nHandle multiple events on the same element:\n\n```tsx\nfunction InteractiveBox(handle: Handle) {\n  let state = 'idle'\n\n  return () => (\n    <div\n      mix={[\n        on('mouseenter', () => {\n          state = 'hovered'\n          handle.update()\n        }),\n        on('mouseleave', () => {\n          state = 'idle'\n          handle.update()\n        }),\n        on('click', () => {\n          state = 'clicked'\n          handle.update()\n        }),\n      ]}\n    >\n      State: {state}\n    </div>\n  )\n}\n```\n\n## Form Events\n\nCommon form event patterns:\n\n```tsx\nfunction Form(handle: Handle) {\n  return () => (\n    <form\n      mix={[\n        on('submit', (event) => {\n          event.preventDefault()\n          let formData = new FormData(event.currentTarget)\n          // Process form data\n        }),\n      ]}\n    >\n      <input\n        name=\"email\"\n        mix={[\n          on('blur', (event) => {\n            // Validate on blur\n            let value = event.currentTarget.value\n            if (!value.includes('@')) {\n              event.currentTarget.setCustomValidity('Invalid email')\n            }\n          }),\n          on('input', (event) => {\n            // Clear validation on input\n            event.currentTarget.setCustomValidity('')\n          }),\n        ]}\n      />\n      <button type=\"submit\">Submit</button>\n    </form>\n  )\n}\n```\n\n## Keyboard Events\n\nHandle keyboard interactions:\n\n```tsx\nfunction KeyboardNav(handle: Handle) {\n  let selectedIndex = 0\n  let items = ['Apple', 'Banana', 'Cherry']\n\n  return () => (\n    <ul\n      tabIndex={0}\n      mix={[\n        on('keydown', (event) => {\n          switch (event.key) {\n            case 'ArrowDown':\n              event.preventDefault()\n              selectedIndex = Math.min(selectedIndex + 1, items.length - 1)\n              handle.update()\n              break\n            case 'ArrowUp':\n              event.preventDefault()\n              selectedIndex = Math.max(selectedIndex - 1, 0)\n              handle.update()\n              break\n          }\n        }),\n      ]}\n    >\n      {items.map((item, i) => (\n        <li key={i} mix={[css({ backgroundColor: i === selectedIndex ? '#eee' : 'transparent' })]}>\n          {item}\n        </li>\n      ))}\n    </ul>\n  )\n}\n```\n\n## Global Event Listeners\n\nUse `addEventListeners()` for global event targets with automatic cleanup:\n\n```tsx\nfunction WindowResizeTracker(handle: Handle) {\n  let width = window.innerWidth\n  let height = window.innerHeight\n\n  // Set up global listeners once in setup\n  addEventListeners(window, handle.signal, {\n    resize() {\n      width = window.innerWidth\n      height = window.innerHeight\n      handle.update()\n    },\n  })\n\n  return () => (\n    <div>\n      Window size: {width} x {height}\n    </div>\n  )\n}\n```\n\n```tsx\nfunction KeyboardTracker(handle: Handle) {\n  let keys: string[] = []\n\n  addEventListeners(document, handle.signal, {\n    keydown(event) {\n      keys.push(event.key)\n      handle.update()\n    },\n  })\n\n  return () => <div>Keys: {keys.join(', ')}</div>\n}\n```\n\n## Best Practices\n\n### Prefer Press Events Over Click\n\nFor interactive elements, prefer `press` events over `click`. Press events provide better cross-device behavior:\n\n- Fire on both mouse and touch interactions\n- Handle keyboard activation (Enter/Space) automatically\n- Prevent ghost clicks on touch devices\n- Support press-and-hold patterns\n\n```tsx\n// ❌ Avoid: click doesn't handle all interaction modes well\n<button mix={[on('click', () => { doAction() })]}>Action</button>\n\n// ✅ Prefer: press handles mouse, touch, and keyboard uniformly\n<button mix={[pressEvents(), on('press', () => { doAction() })]}>Action</button>\n```\n\nUse `click` only when you specifically need mouse-click behavior (e.g., detecting right-clicks or modifier keys).\n\n### Do Work in Event Handlers\n\nDo as much work as possible in event handlers. Use the event handler scope for transient state:\n\n```tsx\n// ✅ Good: Do work in handler, only store what renders need\nfunction SearchResults(handle: Handle) {\n  let results: string[] = [] // Needed for rendering\n  let loading = false // Needed for rendering loading state\n\n  return () => (\n    <div>\n      <input\n        mix={[\n          on('input', async (event, signal) => {\n            let query = event.currentTarget.value\n            // Do work in handler scope\n            loading = true\n            handle.update()\n\n            let response = await fetch(`/search?q=${query}`, { signal })\n            let data = await response.json()\n            if (signal.aborted) return\n\n            // Only store what's needed for rendering\n            results = data.results\n            loading = false\n            handle.update()\n          }),\n        ]}\n      />\n      {loading && <div>Loading...</div>}\n      {results.map((result, i) => (\n        <div key={i}>{result}</div>\n      ))}\n    </div>\n  )\n}\n```\n\n### Always Check signal.aborted\n\nFor async work, always check the signal or pass it to APIs that support it:\n\n```tsx\nmix={[on('click', async (event, signal) => {\n    // Option 1: Pass signal to fetch\n    let response = await fetch('/api', { signal })\n\n    // Option 2: Manual check after await\n    let data = await someAsyncOperation()\n    if (signal.aborted) return\n\n    // Safe to update state\n    handle.update()\n  })]}\n```\n\n## See Also\n\n- [Handle API](./handle.md) - `addEventListeners()` for global listeners\n- [Patterns](./patterns.md) - Data loading and async patterns\n"
  },
  {
    "path": "packages/component/docs/frames.md",
    "content": "# Frames\n\nA `<Frame>` renders server content into the page. Frames can stream in after the initial HTML, nest inside other frames, contain client entries, and be reloaded from the client without a full page navigation.\n\n## Basic usage\n\n```tsx\nimport { Frame } from 'remix/component'\n\nfunction App() {\n  return () => (\n    <div>\n      <h1>Dashboard</h1>\n      <Frame src=\"/sidebar\" fallback={<div>Loading sidebar...</div>} />\n      <Frame src=\"/main-content\" />\n    </div>\n  )\n}\n```\n\n### Props\n\n- **`src`** (required) - The URL to fetch the frame content from.\n- **`fallback`** (optional) - Content to show while the frame is loading. When provided, the frame streams non-blocking (the initial page renders immediately with the fallback, and the real content arrives later). Without a fallback, the frame blocks rendering until its content resolves.\n- **`name`** (optional) - Registers the frame for lookup via `handle.frames.get(name)` from client entries.\n\n## Blocking vs non-blocking\n\nThe presence of a `fallback` prop determines streaming behavior:\n\n**Blocking** (no fallback): The server waits for the frame content before sending the initial HTML chunk. Use this for content that must be visible immediately.\n\n```tsx\n<Frame src=\"/critical-header\" />\n```\n\n**Non-blocking** (with fallback): The fallback renders in the initial chunk. The real content streams in later and replaces the fallback. Use this for content that can load progressively.\n\n```tsx\n<Frame src=\"/recommendations\" fallback={<div>Loading...</div>} />\n```\n\n## Resolving frame content\n\nOn the server, `renderToStream` calls your `resolveFrame` function to get the HTML for each frame:\n\n```tsx\nimport { renderToStream } from 'remix/component/server'\n\nlet stream = renderToStream(<App />, {\n  frameSrc: request.url,\n  async resolveFrame(src, _target, context) {\n    let res = await fetch(new URL(src, context?.currentFrameSrc ?? request.url))\n    return res.body // or res.text() for a string\n  },\n})\n```\n\n`resolveFrame` can return:\n\n- A string of HTML\n- A `ReadableStream<Uint8Array>`\n- A promise of either\n\nFrame content is itself rendered with `renderToStream`, so frames can contain other frames and client entries. The hydration data from nested frames is merged into the parent response automatically.\n\nWhen a server frame response is itself rendered with `renderToStream()`, pass `frameSrc` for that frame's URL and forward `topFrameSrc` from `resolveFrame()` if you want nested SSR components to keep seeing the outer document URL through `handle.frames.top.src`.\n\n## Reloading frames\n\nClient entries inside a frame can trigger a reload via `handle.frame.reload()`:\n\n```tsx\nimport { clientEntry, on, type Handle } from 'remix/component'\n\nexport let RefreshButton = clientEntry(\n  '/assets/refresh.js#RefreshButton',\n  function RefreshButton(handle: Handle) {\n    return () => (\n      <button\n        mix={[\n          on('click', () => {\n            handle.frame.reload()\n          }),\n        ]}\n      >\n        Refresh\n      </button>\n    )\n  },\n)\n```\n\nYou can also reload adjacent named frames:\n\n```tsx\n<Frame name=\"cart-summary\" src=\"/cart-summary\" />\n<Frame name=\"cart-empty\" src=\"/cart-empty\" />\n<Frame src=\"/cart-row\" />\n```\n\n```tsx\nfunction CartRow(handle: Handle) {\n  return () => (\n    <button\n      mix={[\n        on('click', async () => {\n          await handle.frames.get('cart-summary')?.reload()\n          await handle.frames.get('cart-empty')?.reload()\n          await handle.frame.reload()\n        }),\n      ]}\n    >\n      Save\n    </button>\n  )\n}\n```\n\n`handle.frames.get(name)` returns `undefined` when no named frame is mounted.\n\nWhen a frame reloads:\n\n1. The frame's `src` is re-fetched via `resolveFrame` on the client.\n2. The new HTML is parsed and diffed against the current frame content.\n3. Matching DOM nodes are updated in place. New nodes are inserted, removed nodes are cleaned up.\n4. Client entries inside the frame receive updated props from the server while preserving their local component state.\n\nThis means a counter inside a reloading frame keeps its count, but sees any new props the server sends.\n\n## Nested frames\n\nFrames can nest. Each frame owns its own region of the DOM and hydrates its client entries independently:\n\n```tsx\nfunction App() {\n  return () => (\n    <div>\n      <Frame src=\"/outer\" fallback={<div>Loading outer...</div>} />\n    </div>\n  )\n}\n\n// /outer response:\nfunction OuterFrame() {\n  return () => (\n    <div>\n      <h2>Outer</h2>\n      <Frame src=\"/inner\" fallback={<div>Loading inner...</div>} />\n    </div>\n  )\n}\n```\n\nNested frames stream independently. The outer frame can resolve and render while the inner frame is still loading.\n\nDuring SSR, `handle.frame.src` should point at the frame currently being rendered, while `handle.frames.top.src` should stay fixed at the outer document URL. Use `renderToStream({ frameSrc, topFrameSrc })` inside nested `resolveFrame()` handlers to preserve that distinction.\n\n## Client-resolved frames\n\nOn the client, `run` accepts an optional `resolveFrame` implementation:\n\n```tsx\nlet app = run({\n  loadModule: ...,\n  async resolveFrame(src, signal, target) {\n    let headers = new Headers({ accept: 'text/html' })\n    if (target) headers.set('x-remix-target', target)\n    let response = await fetch(src, { headers, signal })\n    return response.body ?? (await response.text())\n  },\n})\n```\n\nThis is used both for initial hydration of pending frames and for `handle.frame.reload()` calls. If omitted, frames resolve to `<p>resolve frame unimplemented</p>`. Because this function defines the trust boundary for frame HTML, only return content from sources you trust.\n\n## Frame lifecycle\n\n1. **Server render** - Frame content is resolved via `resolveFrame` and serialized into the HTML stream. Frame metadata is stored in the `rmx-data` script.\n2. **Client boot** - `run` discovers frame boundaries, hydrates client entries inside them, and sets up observers for any pending (non-blocking) frames still streaming.\n3. **Reload** - `handle.frame.reload()` re-fetches the frame's `src`, diffs the new content into the DOM, and re-hydrates any client entries with updated props.\n4. **Dispose** - When a frame is removed (e.g., parent re-render), its client entries are cleaned up and sub-frames are disposed recursively.\n\n## See Also\n\n- [Server Rendering](./server-rendering.md) - Streaming HTML with `renderToStream`\n- [Hydration](./hydration.md) - Client entries and the `run` function\n"
  },
  {
    "path": "packages/component/docs/getting-started.md",
    "content": "# Getting Started\n\nCreate interactive UIs with Remix Component using a two-phase component model: setup runs once, render runs on every update.\n\n## Client-Only Root\n\nTo start using Remix Component on the client, create a root and render your top-level component:\n\n```tsx\nimport { createRoot } from 'remix/component'\nimport type { Handle } from 'remix/component'\n\nfunction App(handle: Handle) {\n  return () => (\n    <div>\n      <h1>Hello, World!</h1>\n    </div>\n  )\n}\n\n// Create a root attached to a DOM element\nlet container = document.getElementById('app')!\nlet root = createRoot(container)\n\n// Render your app\nroot.render(<App />)\n```\n\nThe `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:\n\n```tsx\nfunction App(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <div>\n      <h1>Count: {count}</h1>\n      <button\n        mix={[\n          on('click', () => {\n            count++\n            handle.update()\n          }),\n        ]}\n      >\n        Increment\n      </button>\n    </div>\n  )\n}\n\nlet root = createRoot(document.body)\nroot.render(<App />)\n```\n\n## Root Methods\n\nThe root object provides several methods:\n\n- **`render(node)`** - Renders a component tree into the root container\n- **`flush()`** - Synchronously flushes all pending updates and tasks\n- **`dispose()`** - Removes the component tree and cleans up\n\n```tsx\nlet root = createRoot(document.body)\n\n// Render initial app\nroot.render(<App />)\n\n// Flush any pending updates synchronously\nroot.flush()\n\n// Later, remove the app\nroot.dispose()\n```\n\n## Server-Rendered App\n\nFor a server-rendered app, define your page as a component, render it with `renderToStream`, and hydrate client entries on the client:\n\n### Server\n\n```tsx\nimport { renderToStream } from 'remix/component/server'\nimport { Frame } from 'remix/component'\nimport { Counter } from './assets/counter.tsx'\n\nfunction App() {\n  return () => (\n    <html>\n      <head>\n        <title>My App</title>\n        <script async type=\"module\" src=\"/assets/entry.js\" />\n      </head>\n      <body>\n        <h1>Hello</h1>\n        <Counter setup={0} label=\"Clicks\" />\n        <Frame src=\"/sidebar\" fallback={<div>Loading...</div>} />\n      </body>\n    </html>\n  )\n}\n\nlet stream = renderToStream(<App />, {\n  resolveFrame: (src) => fetchFrameHtml(src),\n})\n\nreturn new Response(stream, {\n  headers: { 'Content-Type': 'text/html; charset=utf-8' },\n})\n```\n\n### Client entry module\n\n```tsx\n// assets/entry.tsx\nimport { run } from 'remix/component'\n\nlet app = run({\n  async loadModule(moduleUrl, exportName) {\n    let mod = await import(moduleUrl)\n    return mod[exportName]\n  },\n  async resolveFrame(src, signal) {\n    let res = await fetch(src, { headers: { accept: 'text/html' }, signal })\n    return res.body ?? (await res.text())\n  },\n})\n\nawait app.ready()\n```\n\n### Client entry component\n\n```tsx\n// assets/counter.tsx\nimport { clientEntry, on, type Handle } from 'remix/component'\n\nexport let Counter = clientEntry(\n  '/assets/counter.js#Counter',\n  function Counter(handle: Handle, setup: number) {\n    let count = setup\n\n    return (props: { label: string }) => (\n      <div>\n        <span>\n          {props.label}: {count}\n        </span>\n        <button\n          mix={[\n            on('click', () => {\n              count++\n              handle.update()\n            }),\n          ]}\n        >\n          +\n        </button>\n      </div>\n    )\n  },\n)\n```\n\n## Next Steps\n\n- [Components](./components.md) - Component structure and runtime behavior\n- [Handle API](./handle.md) - The component's interface to the framework\n- [Server Rendering](./server-rendering.md) - `renderToString` and `renderToStream`\n- [Hydration](./hydration.md) - `clientEntry` and `run`\n- [Frames](./frames.md) - Streaming partial server UI with `<Frame>`\n- [Styling](./styling.md) - CSS mixin for inline styling\n- [Events](./events.md) - Event handling patterns\n"
  },
  {
    "path": "packages/component/docs/handle.md",
    "content": "# Handle API\n\nThe `Handle` object provides the component's interface to the framework.\n\n## `handle.update()`\n\nSchedules a component update and returns a promise that resolves with an `AbortSignal` after\nthe update completes.\n\n```tsx\nfunction Counter(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <button\n      mix={[\n        on('click', () => {\n          count++\n          handle.update()\n        }),\n      ]}\n    >\n      Count: {count}\n    </button>\n  )\n}\n```\n\nWaiting for the update:\n\n```tsx\nfunction Player(handle: Handle) {\n  let isPlaying = false\n  let stopButton: HTMLButtonElement\n\n  return () => (\n    <button\n      disabled={isPlaying}\n      mix={[\n        on('click', async () => {\n          isPlaying = true\n          await handle.update()\n          stopButton.focus()\n        }),\n      ]}\n    >\n      Play\n    </button>\n  )\n}\n```\n\n## `handle.queueTask(task)`\n\nSchedules a task to run after the next update. The task receives an `AbortSignal` that's aborted when:\n\n- The component re-renders (new render cycle starts)\n- The component is removed from the tree\n\n**Use `queueTask` in event handlers when work needs to happen after DOM changes:**\n\n```tsx\nfunction Form(handle: Handle) {\n  let showDetails = false\n  let detailsSection: HTMLElement\n\n  return () => (\n    <form>\n      <input\n        type=\"checkbox\"\n        checked={showDetails}\n        mix={[\n          on('change', (event) => {\n            showDetails = event.currentTarget.checked\n            handle.update()\n            if (showDetails) {\n              // Queue DOM operation after the new section renders\n              handle.queueTask(() => {\n                detailsSection.scrollIntoView({ behavior: 'smooth' })\n              })\n            }\n          }),\n        ]}\n      />\n      {showDetails && (\n        <section mix={[ref((node) => (detailsSection = node))]}>Details content</section>\n      )}\n    </form>\n  )\n}\n```\n\n**Use `queueTask` for work that needs to be reactive to prop changes:**\n\nWhen 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.\n\n### Anti-patterns\n\n**Don't create states as values to \"react to\" on the next render with `queueTask`:**\n\n```tsx\n// ❌ Avoid: Creating state just to react to it in queueTask\nfunction BadExample(handle: Handle) {\n  let shouldLoad = false // Unnecessary state\n\n  return () => (\n    <div>\n      <button\n        mix={[\n          on('click', () => {\n            shouldLoad = true // Setting state just to trigger queueTask\n            handle.update()\n            handle.queueTask(() => {\n              if (shouldLoad) {\n                // Do work\n              }\n            })\n          }),\n        ]}\n      >\n        Load\n      </button>\n    </div>\n  )\n}\n\n// ✅ Prefer: Do the work directly in the event handler or queueTask\nfunction GoodExample(handle: Handle) {\n  return () => (\n    <div>\n      <button\n        mix={[\n          on('click', () => {\n            handle.queueTask(() => {\n              // Do work directly - no intermediate state needed\n            })\n          }),\n        ]}\n      >\n        Load\n      </button>\n    </div>\n  )\n}\n```\n\n**When showing loading state before async work, await `handle.update()` and use the returned signal:**\n\n```tsx\nfunction AsyncExample(handle: Handle) {\n  let data: string[] = []\n  let loading = false\n\n  async function load() {\n    loading = true\n    let signal = await handle.update()\n\n    let response = await fetch('/api/data', { signal })\n    if (signal.aborted) return\n\n    data = await response.json()\n    loading = false\n    handle.update()\n  }\n\n  return () => <button mix={[on('click', load)]}>{loading ? 'Loading...' : 'Load data'}</button>\n}\n```\n\n## `handle.signal`\n\nAn `AbortSignal` that's aborted when the component is disconnected. Useful for cleanup operations.\n\n```tsx\nfunction Clock(handle: Handle) {\n  let interval = setInterval(() => {\n    if (handle.signal.aborted) {\n      clearInterval(interval)\n      return\n    }\n    handle.update()\n  }, 1000)\n\n  return () => <span>{new Date().toString()}</span>\n}\n```\n\nOr using event listeners:\n\n```tsx\nfunction Clock(handle: Handle) {\n  let interval = setInterval(handle.update, 1000)\n  handle.signal.addEventListener('abort', () => clearInterval(interval))\n\n  return () => <span>{new Date().toString()}</span>\n}\n```\n\n## `addEventListeners(target, handle.signal, listeners)`\n\nListen to an `EventTarget` with automatic cleanup when the component disconnects. Ideal for global event targets like `document` and `window`.\n\n```tsx\nfunction KeyboardTracker(handle: Handle) {\n  let keys: string[] = []\n\n  addEventListeners(document, handle.signal, {\n    keydown(event) {\n      keys.push(event.key)\n      handle.update()\n    },\n  })\n\n  return () => <div>Keys: {keys.join(', ')}</div>\n}\n```\n\n## `handle.frames.top`\n\nThe root frame for the current runtime tree. This is useful when nested components need to reload the entire page/frame tree instead of only their nearest frame.\n\nWhen server rendering with `renderToStream()`, pass the `frameSrc` option to populate `handle.frames.top.src` during SSR. For nested frame renders, also pass `topFrameSrc` to keep the top-frame URL fixed while `handle.frame.src` changes per frame.\n\n```tsx\nfunction RefreshAllButton(handle: Handle) {\n  return () => (\n    <button\n      mix={[\n        on('click', async () => {\n          await handle.frames.top.reload()\n        }),\n      ]}\n    >\n      Refresh everything\n    </button>\n  )\n}\n```\n\n## `handle.frames.get(name)`\n\nLook up a named frame in the current runtime tree. This is useful when one frame action should refresh adjacent frame content.\n\nReturn value:\n\n- `FrameHandle` when a frame with that `name` is currently mounted\n- `undefined` when no such frame is mounted\n\n```tsx\nfunction CartRow(handle: Handle) {\n  return () => (\n    <button\n      mix={[\n        on('click', async () => {\n          await handle.frames.get('cart-summary')?.reload()\n          await handle.frame.reload()\n        }),\n      ]}\n    >\n      Update Cart\n    </button>\n  )\n}\n```\n\nIf multiple mounted frames share the same name, the most recently mounted frame is returned.\n\n## `handle.id`\n\nStable identifier per component instance. Useful for HTML APIs like `htmlFor`, `aria-owns`, etc.\n\n```tsx\nfunction LabeledInput(handle: Handle) {\n  return () => (\n    <div>\n      <label htmlFor={handle.id}>Name</label>\n      <input id={handle.id} type=\"text\" />\n    </div>\n  )\n}\n```\n\n## `handle.context`\n\nContext API for ancestor/descendant communication. See [Context](./context.md) for full documentation.\n\n```tsx\nfunction App(handle: Handle<{ theme: string }>) {\n  handle.context.set({ theme: 'dark' })\n\n  return () => (\n    <div>\n      <Header />\n      <Content />\n    </div>\n  )\n}\n\nfunction Header(handle: Handle) {\n  let { theme } = handle.context.get(App)\n  return () => (\n    <header mix={[css({ backgroundColor: theme === 'dark' ? '#000' : '#fff' })]}>Header</header>\n  )\n}\n```\n\n**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.\n\n## See Also\n\n- [Events](./events.md) - Event handling patterns with signals\n- [Context](./context.md) - Context API with TypedEventTarget\n- [Patterns](./patterns.md) - Common usage patterns\n"
  },
  {
    "path": "packages/component/docs/hydration.md",
    "content": "# Hydration\n\nHydration makes server-rendered HTML interactive on the client. You mark specific components as **client entries**, and the client runtime finds them in the page, loads their code, and hydrates them in place.\n\nOnly the components you mark are hydrated. The rest of the page stays as static HTML.\n\n## Defining a client entry\n\nUse `clientEntry` to mark a component for hydration. The first argument is the module URL and export name the client will use to load the component:\n\n```tsx\nimport { clientEntry, on, type Handle } from 'remix/component'\n\nexport let Counter = clientEntry(\n  '/assets/counter.js#Counter',\n  function Counter(handle: Handle, setup: number) {\n    let count = setup\n\n    return (props: { label: string }) => (\n      <div>\n        <span>\n          {props.label}: {count}\n        </span>\n        <button\n          mix={[\n            on('click', () => {\n              count++\n              handle.update()\n            }),\n          ]}\n        >\n          +\n        </button>\n      </div>\n    )\n  },\n)\n```\n\nThe format is `moduleUrl#ExportName`. If you omit the export name, the function's name is used as a fallback.\n\nOn the server, `clientEntry` components render like any other component. The server wraps their output in comment markers and serializes their props into a `<script type=\"application/json\">` tag so the client knows what to hydrate and with what data.\n\n## Booting the client\n\nUse `run` to start the client. It scans the document for client entry markers, loads the corresponding modules, and hydrates each one:\n\n```tsx\nimport { run } from 'remix/component'\n\nlet app = run({\n  async loadModule(moduleUrl, exportName) {\n    let mod = await import(moduleUrl)\n    return mod[exportName]\n  },\n  async resolveFrame(src, signal) {\n    let res = await fetch(src, { headers: { accept: 'text/html' }, signal })\n    return res.body ?? (await res.text())\n  },\n})\n\nawait app.ready()\n```\n\n### `run` options\n\n- **`loadModule(moduleUrl, exportName)`** (required) - Called for each client entry found in the page. Return the component function. Typically uses dynamic `import()`.\n- **`resolveFrame(src, signal, target)`** (optional) - Called when a `<Frame>` needs to load or reload content. The examples here only use `src` and `signal`, but `target` is also available when frame targeting matters. If omitted, Remix Component uses a placeholder HTML response (`<p>resolve frame unimplemented</p>`). See [Frames](./frames.md) for details.\n\n### `app` methods\n\n- **`app.ready()`** - Returns a promise that resolves when all initial client entries have been hydrated.\n- **`app.flush()`** - Synchronously flushes all pending updates.\n- **`app.dispose()`** - Tears down all hydrated components and cleans up.\n\n`app` is also an `EventTarget`. You can listen for errors from any hydrated component:\n\n```tsx\napp.addEventListener('error', (event) => {\n  console.error('Component error:', event.error)\n})\n```\n\n## What gets serialized\n\nClient entry props are serialized to JSON. Supported prop types:\n\n- Strings, numbers, booleans, `null`, `undefined`\n- Plain objects and arrays of the above\n- JSX elements (serialized as descriptors and revived on the client)\n- `<Frame>` elements in props (serialized as frame descriptors)\n\nFunctions, class instances, and other non-serializable values cannot be passed as props to client entries.\n\n## How hydration works\n\n1. The server renders client entry components and wraps their output in `<!-- rmx:h:id -->` / `<!-- /rmx:h -->` comment markers.\n2. Props and module metadata are collected into a `<script type=\"application/json\" id=\"rmx-data\">` tag.\n3. On the client, `run` parses the data script, discovers the markers, and calls `loadModule` for each entry.\n4. Once a module loads, the component is hydrated against the existing DOM. Matching elements are adopted in place; mismatches are patched.\n\nThis means:\n\n- The page is fully rendered and interactive as soon as modules load. No blank flash.\n- Only marked components ship JavaScript. Static content stays static.\n- Client entries can appear anywhere in the tree, including inside frames.\n\n## See Also\n\n- [Server Rendering](./server-rendering.md) - Rendering components to HTML\n- [Frames](./frames.md) - Streaming partial server UI\n- [Components](./components.md) - Component model and lifecycle\n"
  },
  {
    "path": "packages/component/docs/interactions.md",
    "content": "# Event Mixins\n\nBuild reusable event behavior with mixins that compose normal DOM events into semantic custom\nevents.\n\n> **Note:** Most app code should stick with `on('click', ...)` and other native events. Reach for\n> custom event mixins when the behavior is complex and reused in multiple places.\n\n## When to Create an Event Mixin\n\nCreate one when:\n\n- You combine multiple low-level events into one semantic event.\n- The pattern is reused across components.\n- You want one place for timing/gesture state.\n\nSkip it when:\n\n- Native events are already clear enough.\n- The behavior is used once.\n\n## Example: Drag Release Mixin\n\n```tsx\nimport { createMixin, on } from 'remix/component'\n\nexport let dragReleaseType = 'myapp:drag-release' as const\n\ndeclare global {\n  interface HTMLElementEventMap {\n    [dragReleaseType]: DragReleaseEvent\n  }\n}\n\nexport class DragReleaseEvent extends Event {\n  velocityX: number\n  velocityY: number\n\n  constructor(init: { velocityX: number; velocityY: number }) {\n    super(dragReleaseType, { bubbles: true, cancelable: true })\n    this.velocityX = init.velocityX\n    this.velocityY = init.velocityY\n  }\n}\n\nexport let dragRelease = createMixin<HTMLElement>((handle) => {\n  let node: HTMLElement | undefined\n  let tracking = false\n  let velocityX = 0\n  let velocityY = 0\n  let lastX = 0\n  let lastY = 0\n  let lastT = 0\n\n  handle.addEventListener('insert', (event) => {\n    node = event.node\n  })\n\n  return () => (\n    <handle.element\n      mix={[\n        on('pointerdown', (event) => {\n          if (!event.isPrimary) return\n          tracking = true\n          lastX = event.clientX\n          lastY = event.clientY\n          lastT = event.timeStamp\n          velocityX = 0\n          velocityY = 0\n          node?.setPointerCapture(event.pointerId)\n        }),\n        on('pointermove', (event) => {\n          if (!tracking) return\n          let dt = Math.max(1, event.timeStamp - lastT)\n          velocityX = (event.clientX - lastX) / dt\n          velocityY = (event.clientY - lastY) / dt\n          lastX = event.clientX\n          lastY = event.clientY\n          lastT = event.timeStamp\n        }),\n        on('pointerup', () => {\n          if (!tracking) return\n          tracking = false\n          node?.dispatchEvent(new DragReleaseEvent({ velocityX, velocityY }))\n        }),\n      ]}\n    />\n  )\n})\n```\n\nConsume it like any other mixin/custom event:\n\n```tsx\nfunction DraggableCard() {\n  return () => (\n    <div\n      mix={[\n        dragRelease(),\n        on(dragReleaseType, (event) => {\n          console.log('released with velocity:', event.velocityX, event.velocityY)\n        }),\n      ]}\n    />\n  )\n}\n```\n\n## Example: Tap Tempo Mixin\n\n```tsx\nimport { createMixin, on } from 'remix/component'\n\nexport let tempoType = 'myapp:tempo' as const\n\ndeclare global {\n  interface HTMLElementEventMap {\n    [tempoType]: TempoEvent\n  }\n}\n\nexport class TempoEvent extends Event {\n  bpm: number\n  constructor(bpm: number) {\n    super(tempoType)\n    this.bpm = bpm\n  }\n}\n\nexport let tempo = createMixin<HTMLElement>((handle) => {\n  let node: HTMLElement | undefined\n  let taps: number[] = []\n  let resetTimer = 0\n\n  handle.addEventListener('insert', (event) => {\n    node = event.node\n  })\n\n  handle.addEventListener('remove', () => {\n    clearTimeout(resetTimer)\n    taps = []\n  })\n\n  function handleTap() {\n    clearTimeout(resetTimer)\n    taps.push(Date.now())\n    taps = taps.filter((tap) => Date.now() - tap < 4000)\n    if (taps.length < 4) {\n      resetTimer = window.setTimeout(() => (taps = []), 4000)\n      return\n    }\n    let intervals: number[] = []\n    for (let i = 1; i < taps.length; i++) intervals.push(taps[i] - taps[i - 1])\n    let averageMs = intervals.reduce((sum, value) => sum + value, 0) / intervals.length\n    node?.dispatchEvent(new TempoEvent(Math.round(60000 / averageMs)))\n    resetTimer = window.setTimeout(() => (taps = []), 4000)\n  }\n\n  return () => (\n    <handle.element\n      mix={[\n        on('pointerdown', handleTap),\n        on('keydown', (event) => {\n          if (event.repeat) return\n          if (event.key === 'Enter' || event.key === ' ') handleTap()\n        }),\n      ]}\n    />\n  )\n})\n```\n\n## Best Practices\n\n1. Namespace custom event names (`myapp:*`) to avoid collisions.\n2. Keep state in the mixin setup scope, not in globals.\n3. Dispatch typed custom events with the data consumers need.\n4. Use `handle.addEventListener('remove', ...)` for timer/listener cleanup (GC will take care of scoped variables).\n\n## See Also\n\n- [Events](./events.md)\n- [Mixins](./mixins.md)\n"
  },
  {
    "path": "packages/component/docs/patterns.md",
    "content": "# Patterns\n\nCommon patterns and best practices for building components.\n\n## State Management\n\n### Use Minimal Component State\n\nOnly store state that's needed for rendering. Derive computed values instead of storing them, and avoid storing input state that you don't need.\n\n**Derive computed values:**\n\n```tsx\n// ❌ Avoid: Storing computed values\nfunction TodoList(handle: Handle) {\n  let todos: string[] = []\n  let completedCount = 0 // Unnecessary state\n\n  return () => (\n    <div>\n      {todos.map((todo, i) => (\n        <div key={i}>{todo}</div>\n      ))}\n      <div>Completed: {completedCount}</div>\n    </div>\n  )\n}\n\n// ✅ Prefer: Derive computed values in render\nfunction TodoList(handle: Handle) {\n  let todos: Array<{ text: string; completed: boolean }> = []\n\n  return () => {\n    // Derive computed value in render\n    let completedCount = todos.filter((t) => t.completed).length\n\n    return (\n      <div>\n        {todos.map((todo, i) => (\n          <div key={i}>{todo.text}</div>\n        ))}\n        <div>Completed: {completedCount}</div>\n      </div>\n    )\n  }\n}\n```\n\n**Don't store input state you don't need:**\n\n```tsx\n// ❌ Avoid: Storing input value when you only need it on submit\nfunction SearchForm(handle: Handle) {\n  let query = '' // Unnecessary state\n\n  return () => (\n    <form\n      mix={[\n        on('submit', (event) => {\n          event.preventDefault()\n          let formData = new FormData(event.currentTarget)\n          let query = formData.get('query') as string\n          // Use query for search\n        }),\n      ]}\n    >\n      <input name=\"query\" />\n      <button type=\"submit\">Search</button>\n    </form>\n  )\n}\n\n// ✅ Prefer: Read input value directly from the form\nfunction SearchForm(handle: Handle) {\n  return () => (\n    <form\n      mix={[\n        on('submit', (event) => {\n          event.preventDefault()\n          let formData = new FormData(event.currentTarget)\n          let query = formData.get('query') as string\n          // Use query for search - no component state needed\n        }),\n      ]}\n    >\n      <input name=\"query\" />\n      <button type=\"submit\">Search</button>\n    </form>\n  )\n}\n```\n\n### Do Work in Event Handlers\n\nDo 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.\n\n```tsx\n// ✅ Good: Store state that affects rendering\nfunction Toggle(handle: Handle) {\n  let isOpen = false // Needed for rendering conditional content\n\n  return () => (\n    <div>\n      <button\n        mix={[\n          on('click', () => {\n            isOpen = !isOpen\n            handle.update()\n          }),\n        ]}\n      >\n        Toggle\n      </button>\n      {isOpen && <div>Content</div>}\n    </div>\n  )\n}\n```\n\n## Setup Scope Use Cases\n\nThe setup scope is perfect for one-time initialization:\n\n### Initializing Instances\n\n```tsx\nfunction CacheExample(handle: Handle, setup: { cacheSize: number }) {\n  // Initialize cache once\n  let cache = new Map<string, any>()\n  let maxSize = setup.cacheSize\n\n  return (props: { key: string; value: any }) => {\n    // Use cache in render\n    if (cache.has(props.key)) {\n      return <div>Cached: {cache.get(props.key)}</div>\n    }\n    cache.set(props.key, props.value)\n    if (cache.size > maxSize) {\n      let firstKey = cache.keys().next().value\n      cache.delete(firstKey)\n    }\n    return <div>New: {props.value}</div>\n  }\n}\n```\n\n### Third-Party SDKs\n\n```tsx\nfunction Analytics(handle: Handle, setup: { apiKey: string }) {\n  // Initialize SDK once\n  let analytics = new AnalyticsSDK(setup.apiKey)\n\n  // Cleanup on disconnect\n  handle.signal.addEventListener('abort', () => {\n    analytics.disconnect()\n  })\n\n  return (props: { event: string; data?: any }) => {\n    // SDK is ready to use\n    return <div>Tracking: {props.event}</div>\n  }\n}\n```\n\n### EventEmitters\n\n```tsx\nimport { TypedEventTarget } from 'remix/component'\n\nclass DataEvent extends Event {\n  constructor(public value: string) {\n    super('data')\n  }\n}\n\nclass DataEmitter extends TypedEventTarget<{ data: DataEvent }> {\n  emitData(value: string) {\n    this.dispatchEvent(new DataEvent(value))\n  }\n}\n\nfunction EventListener(handle: Handle, setup: DataEmitter) {\n  // Set up listeners once with automatic cleanup\n  addEventListeners(setup, handle.signal, {\n    data(event) {\n      // Handle data\n      handle.update()\n    },\n  })\n\n  return () => <div>Listening for events...</div>\n}\n```\n\n### Window/Document Event Handling\n\n```tsx\nfunction WindowResizeTracker(handle: Handle) {\n  let width = window.innerWidth\n  let height = window.innerHeight\n\n  // Set up global listeners once\n  addEventListeners(window, handle.signal, {\n    resize() {\n      width = window.innerWidth\n      height = window.innerHeight\n      handle.update()\n    },\n  })\n\n  return () => (\n    <div>\n      Window size: {width} x {height}\n    </div>\n  )\n}\n```\n\n### Initializing State from Props\n\n```tsx\nfunction Timer(handle: Handle, setup: { initialSeconds: number }) {\n  // Initialize from setup prop\n  let seconds = setup.initialSeconds\n  let interval: number | null = null\n\n  function start() {\n    if (interval) return\n    interval = setInterval(() => {\n      seconds--\n      if (seconds <= 0) {\n        stop()\n      }\n      handle.update()\n    }, 1000)\n  }\n\n  function stop() {\n    if (interval) {\n      clearInterval(interval)\n      interval = null\n    }\n  }\n\n  // Cleanup on disconnect\n  handle.signal.addEventListener('abort', stop)\n\n  return (props: { paused?: boolean }) => {\n    if (!props.paused && !interval) {\n      start()\n    } else if (props.paused && interval) {\n      stop()\n    }\n\n    return <div>Time remaining: {seconds}s</div>\n  }\n}\n```\n\n## Focus and Scroll Management\n\nUse `handle.queueTask()` in event handlers for DOM operations that need to happen after the DOM has changed from the next update.\n\n### Focus Management\n\n```tsx\nfunction Modal(handle: Handle) {\n  let isOpen = false\n  let closeButton: HTMLButtonElement\n  let openButton: HTMLButtonElement\n\n  return () => (\n    <div>\n      <button\n        mix={[\n          ref((node) => (openButton = node)),\n          on('click', () => {\n            isOpen = true\n            handle.update()\n            // Queue focus operation after modal renders\n            handle.queueTask(() => {\n              closeButton.focus()\n            })\n          }),\n        ]}\n      >\n        Open Modal\n      </button>\n\n      {isOpen && (\n        <div role=\"dialog\">\n          <button\n            mix={[\n              ref((node) => (closeButton = node)),\n              on('click', () => {\n                isOpen = false\n                handle.update()\n                // Queue focus operation after modal closes\n                handle.queueTask(() => {\n                  openButton.focus()\n                })\n              }),\n            ]}\n          >\n            Close\n          </button>\n        </div>\n      )}\n    </div>\n  )\n}\n```\n\n### Scroll Management\n\n```tsx\nfunction ScrollableList(handle: Handle) {\n  let items: string[] = []\n  let newItemInput: HTMLInputElement\n  let listContainer: HTMLElement\n\n  return () => (\n    <div>\n      <input\n        mix={[\n          ref((node) => (newItemInput = node)),\n          on('keydown', (event) => {\n            if (event.key === 'Enter') {\n              let text = event.currentTarget.value\n              if (text.trim()) {\n                items.push(text)\n                event.currentTarget.value = ''\n                handle.update()\n                // Queue scroll operation after new item renders\n                handle.queueTask(() => {\n                  listContainer.scrollTop = listContainer.scrollHeight\n                })\n              }\n            }\n          }),\n        ]}\n      />\n      <div\n        mix={[\n          ref((node) => (listContainer = node)),\n          css({\n            maxHeight: '300px',\n            overflowY: 'auto',\n          }),\n        ]}\n      >\n        {items.map((item, i) => (\n          <div key={i}>{item}</div>\n        ))}\n      </div>\n    </div>\n  )\n}\n```\n\n## Controlled vs Uncontrolled Inputs\n\nOnly control an input's value when something besides the user's interaction with that input can also control its state.\n\n**Uncontrolled Input** (use when only the user controls the value):\n\n```tsx\nfunction SearchInput(handle: Handle) {\n  let results: string[] = []\n\n  return () => (\n    <div>\n      <input\n        type=\"text\"\n        mix={[\n          on('input', async (event, signal) => {\n            // Read value directly from the input - no component state needed\n            let query = event.currentTarget.value\n            // ... use query for search\n          }),\n        ]}\n      />\n    </div>\n  )\n}\n```\n\n**Controlled Input** (use when programmatic control is needed):\n\n```tsx\nfunction SlugForm(handle: Handle) {\n  let slug = ''\n  let generatedSlug = ''\n\n  return () => (\n    <form>\n      <label>\n        <input\n          type=\"checkbox\"\n          mix={[\n            on('change', (event) => {\n              if (event.currentTarget.checked) {\n                generatedSlug = crypto.randomUUID().slice(0, 8)\n              } else {\n                generatedSlug = ''\n              }\n              handle.update()\n            }),\n          ]}\n        />\n        Auto-generate slug\n      </label>\n      <label>\n        Slug\n        <input\n          type=\"text\"\n          value={generatedSlug || slug}\n          disabled={!!generatedSlug}\n          mix={[\n            on('input', (event) => {\n              slug = event.currentTarget.value\n              handle.update()\n            }),\n          ]}\n        />\n      </label>\n    </form>\n  )\n}\n```\n\n**Use controlled inputs when:**\n\n- The value can be set programmatically (auto-generated fields, reset buttons, external state)\n- The input can be disabled and its value changed by other interactions\n- You need to validate or transform input before it appears\n- You need to prevent certain values from being entered\n\n**Use uncontrolled inputs when:**\n\n- Only the user can change the value through direct interaction with that input\n- You just need to read the value on events (submit, blur, etc.)\n\n## Data Loading\n\n### Using Event Handler Signals\n\nEvent handlers receive an `AbortSignal` that's aborted when the handler is re-entered:\n\n```tsx\nfunction SearchInput(handle: Handle) {\n  let results: string[] = []\n  let loading = false\n\n  return () => (\n    <div>\n      <input\n        type=\"text\"\n        mix={[\n          on('input', async (event, signal) => {\n            let query = event.currentTarget.value\n            loading = true\n            handle.update()\n\n            let response = await fetch(`/search?q=${query}`, { signal })\n            let data = await response.json()\n            if (signal.aborted) return\n\n            results = data.results\n            loading = false\n            handle.update()\n          }),\n        ]}\n      />\n      {loading && <div>Loading...</div>}\n      {!loading && results.length > 0 && (\n        <ul>\n          {results.map((result, i) => (\n            <li key={i}>{result}</li>\n          ))}\n        </ul>\n      )}\n    </div>\n  )\n}\n```\n\n### Using queueTask for Reactive Data Loading\n\nUse `handle.queueTask()` in the render function for reactive data loading that responds to prop changes:\n\n```tsx\nfunction DataLoader(handle: Handle) {\n  let data: any = null\n  let loading = false\n  let error: Error | null = null\n\n  return (props: { url: string }) => {\n    // Queue data loading task that responds to prop changes\n    handle.queueTask(async (signal) => {\n      loading = true\n      error = null\n      handle.update()\n\n      let response = await fetch(props.url, { signal })\n      let json = await response.json()\n      if (signal.aborted) return\n      data = json\n      loading = false\n      handle.update()\n    })\n\n    if (loading) return <div>Loading...</div>\n    if (error) return <div>Error: {error.message}</div>\n    if (!data) return <div>No data</div>\n\n    return <div>{JSON.stringify(data)}</div>\n  }\n}\n```\n\n### Using Setup Scope for Initial Data\n\nLoad initial data in the setup scope:\n\n```tsx\nfunction UserProfile(handle: Handle, setup: { userId: string }) {\n  let user: User | null = null\n  let loading = true\n\n  // Load initial data in setup scope using queueTask\n  handle.queueTask(async (signal) => {\n    let response = await fetch(`/api/users/${setup.userId}`, { signal })\n    let data = await response.json()\n    if (signal.aborted) return\n    user = data\n    loading = false\n    handle.update()\n  })\n\n  return (props: { showEmail?: boolean }) => {\n    if (loading) return <div>Loading user...</div>\n\n    return (\n      <div>\n        <h1>{user.name}</h1>\n        {props.showEmail && <p>{user.email}</p>}\n      </div>\n    )\n  }\n}\n```\n\nNote that by fetching this data in the setup scope any parent updates that change `setup.userId` will have no effect.\n\n## See Also\n\n- [Handle API](./handle.md) - `handle.queueTask()` and `handle.signal`\n- [Events](./events.md) - Event handler signals\n- [Components](./components.md) - Setup vs render phases\n"
  },
  {
    "path": "packages/component/docs/server-rendering.md",
    "content": "# Server Rendering\n\nRemix Component can render to HTML on the server using two APIs:\n\n- `renderToString` - Returns a complete HTML string. Simple, but buffers the entire response.\n- `renderToStream` - Returns a `ReadableStream<Uint8Array>`. Sends the initial HTML immediately and streams frame content as it resolves.\n\nBoth are exported from `remix/component/server`.\n\n## renderToString\n\nRenders a component tree to a complete HTML string. Use this when you need the full output before responding (e.g., generating static pages or embedding HTML in an email).\n\n```tsx\nimport { renderToString } from 'remix/component/server'\n\nlet html = await renderToString(<App />)\n```\n\n## renderToStream\n\nRenders a component tree to a streaming response. The initial HTML is sent immediately. Any `<Frame>` components with a `fallback` prop will render the fallback first, then stream the resolved content as it becomes available.\n\n```tsx\nimport { renderToStream } from 'remix/component/server'\n\nlet stream = renderToStream(<App />, {\n  frameSrc: request.url,\n  resolveFrame(src, _target, context) {\n    let frameUrl = new URL(src, context?.currentFrameSrc ?? request.url)\n    return fetchHtml(frameUrl)\n  },\n  onError(error) {\n    console.error(error)\n  },\n})\n\nreturn new Response(stream, {\n  headers: { 'Content-Type': 'text/html; charset=utf-8' },\n})\n```\n\n### Options\n\n- **`frameSrc`** - Seeds SSR frame state for the current render. When provided, server-rendered components can read `handle.frame.src` and `handle.frames.top.src` during SSR.\n- **`topFrameSrc`** - Overrides the root frame URL used for `handle.frames.top.src`. This is mainly useful when calling `renderToStream()` from inside `resolveFrame()` for a nested frame render.\n- **`resolveFrame(src, target, context)`** - Called when a `<Frame>` needs its content. Return a string of HTML, a `ReadableStream<Uint8Array>`, or a promise of either. `context.currentFrameSrc` is the URL for the frame that contains the `<Frame>`, and `context.topFrameSrc` is the outer document URL. Required if your component tree contains `<Frame>` elements.\n- **`onError(error)`** - Called when a rendering error occurs. If not provided, the stream rejects with the error.\n\nWhen you render nested frame responses with `renderToStream()` inside `resolveFrame()`, pass `frameSrc` for the frame being rendered and carry `topFrameSrc` forward from the parent context. That preserves `handle.frames.top.src` across the whole SSR frame tree.\n\n### Streaming behavior\n\nWhen the stream encounters a `<Frame>` component:\n\n- **Without `fallback`** (blocking): The frame content is awaited before the initial HTML chunk is sent. The resolved content appears inline.\n- **With `fallback`** (non-blocking): The fallback is rendered inline in the initial chunk. Once the frame resolves, a `<template>` element containing the real content is streamed at the end of the response. The client swaps it in automatically.\n\nThis means the first chunk always contains a complete, renderable page. Slow data sources don't block the initial paint.\n\n## Head content\n\nTo render content into the document head during SSR, use an explicit `<head>` element:\n\n```tsx\nfunction ProductPage() {\n  return () => (\n    <html>\n      <head>\n        <title>Product Name</title>\n        <meta name=\"description\" content=\"A great product\" />\n      </head>\n      <body>\n        <h1>Product Name</h1>\n      </body>\n    </html>\n  )\n}\n```\n\n## CSS\n\nComponents using the `css` prop have their styles collected during rendering and emitted as a single `<style>` tag in the `<head>`. No client-side style injection is needed for server-rendered content.\n\n## See Also\n\n- [Hydration](./hydration.md) - Making server-rendered components interactive on the client\n- [Frames](./frames.md) - Streaming partial server UI with `<Frame>`\n"
  },
  {
    "path": "packages/component/docs/spring.md",
    "content": "# Spring API\n\nA physics-based spring animation function that returns an iterator with CSS easing.\n\n## Basic Usage\n\n```tsx\nimport { spring } from './spring.ts'\n\n// Using a preset\nspring('bouncy') // bouncy with overshoot\nspring('snappy') // quick, no overshoot (default)\nspring('smooth') // gentle, overdamped\n\n// Custom spring\nspring({ duration: 400, bounce: 0.3 })\n```\n\n## Return Value\n\n`spring()` returns a `SpringIterator`:\n\n```ts\ninterface SpringIterator extends IterableIterator<number> {\n  duration: number // CSS duration in ms (e.g., 550)\n  easing: string // CSS linear() function\n  toString(): string // \"550ms linear(...)\"\n}\n```\n\nThe iterator can be:\n\n- **Iterated** to get position values (0→1) for JS animations\n- **Spread** into objects (for animate/WAAPI)\n- **Stringified** via template literals or `String()` (for CSS transitions)\n\n## CSS Transitions\n\n### Template literal\n\n```tsx\nmix={[css({\n  transition: `width ${spring('bouncy')}`\n})]}\n// → \"width 550ms linear(...)\"\n```\n\n### Multiple properties (same spring)\n\n```tsx\nmix={[css({\n  transition: `transform ${spring('bouncy')}, opacity ${spring('bouncy')}`\n})]}\n```\n\n### Using the helper\n\n```tsx\nmix={[css({\n  transition: spring.transition('width', 'bouncy')\n})]}\n// → \"width 550ms linear(...)\"\n\nmix={[css({\n  transition: spring.transition(['left', 'top'], 'snappy')\n})]}\n// → \"left 385ms linear(...), top 385ms linear(...)\"\n```\n\n## Animation Mixins\n\nSpread the spring value to get both `duration` and `easing`:\n\n```tsx\nmix={[\n  animateEntrance({\n    opacity: 0,\n    transform: 'scale(0.9)',\n    ...spring('bouncy')\n  }),\n  animateExit({\n    opacity: 0,\n    ...spring('snappy')\n  }),\n]}\n```\n\n## Presets\n\n| Preset   | Bounce | Duration | Character                   |\n| -------- | ------ | -------- | --------------------------- |\n| `smooth` | -0.3   | 400ms    | Overdamped, no overshoot    |\n| `snappy` | 0      | 200ms    | Critically damped, quick    |\n| `bouncy` | 0.3    | 300ms    | Underdamped, visible bounce |\n\n### Override preset duration\n\n```tsx\nspring('bouncy', { duration: 300 }) // faster bouncy\nspring('smooth', { duration: 800 }) // slower smooth\n```\n\n## Custom Springs\n\n### Parameters\n\n```tsx\nspring({\n  duration: 500, // perceived duration in milliseconds\n  bounce: 0.3, // -1 to 1 (negative = overdamped, 0 = critical, positive = bouncy)\n  velocity: 0, // initial velocity in units per second\n})\n```\n\n### Bounce values\n\n- `bounce < 0`: Overdamped (slower settling, no overshoot)\n- `bounce = 0`: Critically damped (fastest settling without overshoot)\n- `bounce > 0`: Underdamped (bouncy, overshoots target)\n\n```tsx\nspring({ bounce: -0.5 }) // very smooth, slow\nspring({ bounce: 0 }) // snappy, no bounce\nspring({ bounce: 0.3 }) // slight bounce\nspring({ bounce: 0.7 }) // very bouncy\n```\n\n## Velocity\n\nUse `velocity` to continue momentum from a gesture:\n\n```tsx\n// Positive = moving toward target (more overshoot)\n// Negative = moving away from target (takes longer)\n\nspring('bouncy', { velocity: 2 }) // fast start\nspring('bouncy', { velocity: -1 }) // initially going backward\n```\n\n### Calculating velocity from drag\n\n```tsx\n// velocity is in px/s, distance is in px\nlet normalizedVelocity = velocityTowardTarget / distanceToTarget\n\nspring('bouncy', { velocity: normalizedVelocity })\n```\n\n## Iterating for JS Animations\n\nThe spring iterator yields position values from 0 to 1, one per frame (~60fps):\n\n```tsx\nlet s = spring('bouncy')\n\nfor (let t of s) {\n  console.log(t) // 0, 0.015, 0.058, 0.121, ... 1\n}\n```\n\n### Interpolating between values\n\nUse the 0→1 progress to interpolate any value:\n\n```tsx\nlet from = 100\nlet to = 500\n\nfor (let t of spring('bouncy')) {\n  let value = from + (to - from) * t // 100 → 500\n  updateSomething(value)\n  await nextFrame()\n}\n```\n\n### Canvas animation\n\n```tsx\nlet s = spring('bouncy')\n\nfunction draw() {\n  let { value, done } = s.next()\n\n  ctx.clearRect(0, 0, canvas.width, canvas.height)\n  ctx.beginPath()\n  ctx.arc(value * 400, 100, 20, 0, Math.PI * 2) // x: 0 → 400\n  ctx.fill()\n\n  if (!done) requestAnimationFrame(draw)\n}\n\ndraw()\n```\n\n### Animating multiple properties\n\n```tsx\nlet fromX = 0,\n  toX = 200\nlet fromY = 0,\n  toY = 100\nlet fromScale = 0.5,\n  toScale = 1\n\nfor (let t of spring('bouncy')) {\n  let x = fromX + (toX - fromX) * t\n  let y = fromY + (toY - fromY) * t\n  let scale = fromScale + (toScale - fromScale) * t\n\n  render({ x, y, scale })\n  await nextFrame()\n}\n```\n\n### Color interpolation\n\n```tsx\nlet fromRGB = [255, 0, 0] // red\nlet toRGB = [0, 0, 255] // blue\n\nfor (let t of spring('smooth')) {\n  let r = Math.round(fromRGB[0] + (toRGB[0] - fromRGB[0]) * t)\n  let g = Math.round(fromRGB[1] + (toRGB[1] - fromRGB[1]) * t)\n  let b = Math.round(fromRGB[2] + (toRGB[2] - fromRGB[2]) * t)\n\n  element.style.backgroundColor = `rgb(${r}, ${g}, ${b})`\n  await nextFrame()\n}\n```\n\n## Accessing Raw Values\n\n```tsx\nlet { duration, easing } = spring('bouncy')\n\n// duration: 550 (ms)\n// easing: \"linear(0.0000, 0.0156, ...)\"\n```\n\n## Accessing Preset Defaults\n\n```tsx\nspring.presets\n// {\n//   smooth: { duration: 400, bounce: -0.3 },\n//   snappy: { duration: 200, bounce: 0 },\n//   bouncy: { duration: 300, bounce: 0.3 }\n// }\n```\n\n## Web Animations API\n\n```tsx\nelement.animate(keyframes, {\n  ...spring('bouncy'),\n})\n```\n\n## Complete Example\n\n```tsx\nfunction AnimatedCard(handle: Handle) {\n  let isExpanded = false\n\n  return () => (\n    <div\n      mix={[\n        css({\n          transition: spring.transition(['width', 'height'], 'bouncy'),\n        }),\n        on('click', () => {\n          isExpanded = !isExpanded\n          handle.update()\n        }),\n      ]}\n      style={{\n        width: isExpanded ? '300px' : '100px',\n        height: isExpanded ? '200px' : '100px',\n      }}\n    >\n      Click me\n    </div>\n  )\n}\n```\n"
  },
  {
    "path": "packages/component/docs/styling.md",
    "content": "# Styling\n\nThe `css(...)` mixin provides inline styling with support for pseudo-selectors, pseudo-elements, attribute selectors, descendant selectors, and media queries. It follows modern CSS nesting selector rules.\n\n## Basic CSS Mixin\n\n```tsx\nfunction Button() {\n  return () => (\n    <button\n      mix={[\n        css({\n          color: 'white',\n          backgroundColor: 'blue',\n          padding: '12px 24px',\n          borderRadius: '4px',\n          border: 'none',\n          cursor: 'pointer',\n        }),\n      ]}\n    >\n      Click me\n    </button>\n  )\n}\n```\n\n## CSS Mixin vs Style Prop\n\nThe `css(...)` mixin 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:\n\n```tsx\n// ❌ Avoid: Using css(...) for dynamic styles\nfunction ProgressBar(handle: Handle) {\n  let progress = 0\n\n  return () => (\n    <div\n      mix={[\n        css({\n          width: `${progress}%`, // Creates new CSS rule on every update\n          backgroundColor: 'blue',\n        }),\n      ]}\n    >\n      {progress}%\n    </div>\n  )\n}\n\n// ✅ Prefer: Using style prop for dynamic styles\nfunction ProgressBar(handle: Handle) {\n  let progress = 0\n\n  return () => (\n    <div\n      mix={[\n        css({\n          backgroundColor: 'blue', // Static styles in css(...)\n        }),\n      ]}\n      style={{\n        width: `${progress}%`, // Dynamic styles in style prop\n      }}\n    >\n      {progress}%\n    </div>\n  )\n}\n```\n\n**Use the `css(...)` mixin for:**\n\n- Static styles that don't change\n- Styles that need pseudo-selectors (`:hover`, `:focus`, etc.)\n- Styles that need media queries\n\n**Use the `style` prop for:**\n\n- Dynamic styles that change based on state or props\n- Computed values that update frequently\n\n## Pseudo-Selectors\n\nUse `&` to reference the current element in pseudo-selectors:\n\n```tsx\nfunction Button() {\n  return () => (\n    <button\n      mix={[\n        css({\n          color: 'white',\n          backgroundColor: 'blue',\n          padding: '12px 24px',\n          borderRadius: '4px',\n          border: 'none',\n          cursor: 'pointer',\n          '&:hover': {\n            backgroundColor: 'darkblue',\n            transform: 'translateY(-1px)',\n          },\n          '&:active': {\n            backgroundColor: 'navy',\n            transform: 'translateY(0)',\n          },\n          '&:focus': {\n            outline: '2px solid yellow',\n            outlineOffset: '2px',\n          },\n          '&:disabled': {\n            opacity: 0.5,\n            cursor: 'not-allowed',\n          },\n        }),\n      ]}\n    >\n      Click me\n    </button>\n  )\n}\n```\n\n## Pseudo-Elements\n\nUse `&::before` and `&::after` for pseudo-elements:\n\n```tsx\nfunction Badge() {\n  return (props: { count: number }) => (\n    <div\n      mix={[\n        css({\n          position: 'relative',\n          display: 'inline-block',\n          '&::before': {\n            content: '\"\"',\n            position: 'absolute',\n            top: '-4px',\n            right: '-4px',\n            width: '8px',\n            height: '8px',\n            backgroundColor: 'red',\n            borderRadius: '50%',\n          },\n        }),\n      ]}\n    >\n      {props.count > 0 && <span>{props.count}</span>}\n    </div>\n  )\n}\n```\n\n## Attribute Selectors\n\nUse `&[attribute]` for attribute selectors:\n\n```tsx\nfunction Input() {\n  return (props: { required?: boolean }) => (\n    <input\n      required={props.required}\n      mix={[\n        css({\n          padding: '8px',\n          border: '1px solid #ccc',\n          borderRadius: '4px',\n          '&[required]': {\n            borderColor: 'red',\n          },\n          '&[aria-invalid=\"true\"]': {\n            borderColor: 'red',\n            outline: '2px solid red',\n          },\n        }),\n      ]}\n    />\n  )\n}\n```\n\n## Descendant Selectors\n\nUse class names or element selectors directly for descendant selectors:\n\n```tsx\nfunction Card() {\n  return (props: { children: RemixNode }) => (\n    <div\n      mix={[\n        css({\n          padding: '20px',\n          border: '1px solid #ddd',\n          borderRadius: '8px',\n          backgroundColor: 'white',\n          boxShadow: '0 2px 4px rgba(0,0,0,0.1)',\n          // Style descendants\n          '& h2': {\n            marginTop: 0,\n            fontSize: '24px',\n            fontWeight: 'bold',\n          },\n          '& p': {\n            color: '#666',\n            lineHeight: 1.6,\n          },\n          '& .icon': {\n            width: '24px',\n            height: '24px',\n            marginRight: '8px',\n          },\n          '& button': {\n            marginTop: '16px',\n          },\n        }),\n      ]}\n    >\n      {props.children}\n    </div>\n  )\n}\n```\n\n## When to Use Nested Selectors\n\nUse nested selectors when **parent state affects children**. Don't nest when you can style the element directly.\n\n**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.\n\n**Use nested selectors when:**\n\n1. **Parent state affects children** - Parent hover/focus/state changes child styling (prefer this over JavaScript state management)\n2. **Styling descendant elements** - Avoid duplicating styles on every child or creating new components just for styling\n\n**Don't nest when:**\n\n- Styling the element's own pseudo-states (hover, focus, etc.)\n- The element controls its own styling\n\n**Example: Parent hover affects children** (use nested selectors, not JavaScript state):\n\n```tsx\n// ❌ Avoid: Managing hover state in JavaScript\nfunction CardWithJSState(handle: Handle) {\n  let isHovered = false\n\n  return (props: { children: RemixNode }) => (\n    <div\n      mix={[\n        on('mouseenter', () => {\n          isHovered = true\n          handle.update()\n        }),\n        on('mouseleave', () => {\n          isHovered = false\n          handle.update()\n        }),\n        css({\n          border: `1px solid ${isHovered ? 'blue' : '#ddd'}`,\n          // ... more conditional styling based on isHovered\n        }),\n      ]}\n    >\n      <div className=\"title\" mix={[css({ color: isHovered ? 'blue' : '#333' })]}>\n        Title\n      </div>\n    </div>\n  )\n}\n\n// ✅ Prefer: CSS nested selectors handle state declaratively\nfunction Card(handle: Handle) {\n  return (props: { children: RemixNode }) => (\n    <div\n      mix={[\n        css({\n          border: '1px solid #ddd',\n          borderRadius: '8px',\n          padding: '20px',\n          // Parent hover affects children - use nested selector\n          '&:hover': {\n            borderColor: 'blue',\n            // Child text changes color on parent hover\n            '& .title': {\n              color: 'blue',\n            },\n            '& .description': {\n              opacity: 1,\n            },\n          },\n          '& .title': {\n            fontSize: '20px',\n            fontWeight: 'bold',\n            color: '#333',\n          },\n          '& .description': {\n            opacity: 0.7,\n            marginTop: '8px',\n          },\n        }),\n      ]}\n    >\n      <div className=\"title\">Title</div>\n    </div>\n  )\n}\n```\n\n**Example: Element's own hover** (style directly, no nesting needed):\n\n```tsx\nfunction Button() {\n  return () => (\n    <button\n      mix={[\n        css({\n          backgroundColor: 'blue',\n          color: 'white',\n          padding: '12px 24px',\n          borderRadius: '4px',\n          border: 'none',\n          cursor: 'pointer',\n          // Element's own hover - style directly, no nesting needed\n          '&:hover': {\n            backgroundColor: 'darkblue',\n          },\n          '&:active': {\n            transform: 'scale(0.98)',\n          },\n        }),\n      ]}\n    >\n      Click me\n    </button>\n  )\n}\n```\n\n**Example: Navigation with links** (descendant styling is appropriate):\n\n```tsx\nfunction Navigation() {\n  return () => (\n    <nav\n      mix={[\n        css({\n          display: 'flex',\n          gap: '16px',\n          // Styling descendant links - appropriate use of nesting\n          '& a': {\n            color: 'blue',\n            textDecoration: 'none',\n            padding: '8px 16px',\n            borderRadius: '4px',\n            // Link's own hover state - this is fine nested under '& a'\n            '&:hover': {\n              backgroundColor: '#f0f0f0',\n              color: 'darkblue',\n            },\n            '&[aria-current=\"page\"]': {\n              backgroundColor: 'blue',\n              color: 'white',\n            },\n          },\n        }),\n      ]}\n    >\n      <a href=\"/\">Home</a>\n      <a href=\"/about\">About</a>\n      <a href=\"/contact\">Contact</a>\n    </nav>\n  )\n}\n```\n\n## Media Queries\n\nUse `@media` for responsive design:\n\n```tsx\nfunction ResponsiveGrid() {\n  return (props: { children: RemixNode }) => (\n    <div\n      mix={[\n        css({\n          display: 'grid',\n          gap: '16px',\n          gridTemplateColumns: '1fr',\n          '@media (min-width: 768px)': {\n            gridTemplateColumns: 'repeat(2, 1fr)',\n          },\n          '@media (min-width: 1024px)': {\n            gridTemplateColumns: 'repeat(3, 1fr)',\n          },\n        }),\n      ]}\n    >\n      {props.children}\n    </div>\n  )\n}\n```\n\n## Complete Example\n\nHere's a comprehensive example demonstrating parent-state-affecting-children and media queries:\n\n```tsx\nfunction ProductCard() {\n  return (props: { title: string; price: number; image: string }) => (\n    <div\n      mix={[\n        css({\n          border: '1px solid #ddd',\n          borderRadius: '8px',\n          overflow: 'hidden',\n          transition: 'transform 0.2s, box-shadow 0.2s',\n          // Parent hover affects the card itself\n          '&:hover': {\n            transform: 'translateY(-4px)',\n            boxShadow: '0 4px 12px rgba(0,0,0,0.15)',\n            // Parent hover affects children - appropriate use of nesting\n            '& .title': {\n              color: 'blue',\n            },\n            '& button': {\n              backgroundColor: 'darkblue',\n            },\n          },\n          '@media (max-width: 768px)': {\n            '&:hover': {\n              transform: 'translateY(-2px)',\n            },\n          },\n        }),\n      ]}\n    >\n      <img\n        src={props.image}\n        alt={props.title}\n        mix={[\n          css({\n            width: '100%',\n            height: '200px',\n            objectFit: 'cover',\n            '@media (max-width: 768px)': {\n              height: '150px',\n            },\n          }),\n        ]}\n      />\n      <div\n        className=\"content\"\n        mix={[\n          css({\n            padding: '16px',\n            '@media (max-width: 768px)': {\n              padding: '12px',\n            },\n          }),\n        ]}\n      >\n        <h3\n          className=\"title\"\n          mix={[\n            css({\n              fontSize: '18px',\n              fontWeight: 'bold',\n              marginTop: 0,\n              marginBottom: '8px',\n              transition: 'color 0.2s',\n            }),\n          ]}\n        >\n          {props.title}\n        </h3>\n        <div\n          className=\"price\"\n          mix={[\n            css({\n              fontSize: '20px',\n              color: 'green',\n              fontWeight: 'bold',\n            }),\n          ]}\n        >\n          ${props.price}\n        </div>\n        <button\n          mix={[\n            css({\n              width: '100%',\n              padding: '12px',\n              backgroundColor: 'blue',\n              color: 'white',\n              border: 'none',\n              borderRadius: '4px',\n              cursor: 'pointer',\n              transition: 'background-color 0.2s',\n              '&:active': {\n                transform: 'scale(0.98)',\n              },\n            }),\n          ]}\n        >\n          Add to Cart\n        </button>\n      </div>\n    </div>\n  )\n}\n```\n\nThis example demonstrates:\n\n- **Parent hover affecting children**: Card hover changes title color and button background\n- **Styles on elements themselves**: Each element has its own `css(...)` mixin\n- **Element's own states**: Button's `:active` state styled directly on the button\n- **Media queries**: Responsive adjustments applied directly to elements\n\n## See Also\n\n- [Spring API](./spring.md) - Physics-based animation easing\n"
  },
  {
    "path": "packages/component/docs/testing.md",
    "content": "# Testing\n\nWhen writing tests, use `root.flush()` to synchronously execute all pending updates and tasks. This ensures the DOM and component state are fully synchronized before making assertions.\n\n## Basic Testing Pattern\n\nThe main use case is flushing after events that call `handle.update()`. Since updates are asynchronous, you need to flush to ensure the DOM reflects the changes:\n\n```tsx\nfunction Counter(handle: Handle) {\n  let count = 0\n\n  return () => (\n    <button\n      mix={[\n        on('click', () => {\n          count++\n          handle.update()\n        }),\n      ]}\n    >\n      Count: {count}\n    </button>\n  )\n}\n\n// In your test\nlet container = document.createElement('div')\nlet root = createRoot(container)\n\nroot.render(<Counter />)\nroot.flush() // Ensure initial render completes\n\nlet button = container.querySelector('button')\nbutton.click() // Triggers handle.update()\nroot.flush() // Flush to apply the update\n\nexpect(container.textContent).toBe('Count: 1')\n```\n\n## Why Flush After Initial Render?\n\nYou should also flush after the initial `root.render()` to ensure event listeners are attached and the DOM is ready for interaction:\n\n```tsx\nlet root = createRoot(container)\nroot.render(<MyComponent />)\nroot.flush() // Event listeners now attached\n\n// Safe to interact\ncontainer.querySelector('button').click()\n```\n\n## Testing Async Operations\n\nFor components with async operations in `queueTask`, flush after each step:\n\n```tsx\nfunction AsyncLoader(handle: Handle) {\n  let data: string | null = null\n\n  handle.queueTask(async (signal) => {\n    let response = await fetch('/api/data', { signal })\n    let json = await response.json()\n    if (signal.aborted) return\n    data = json.value\n    handle.update()\n  })\n\n  return () => <div>{data ?? 'Loading...'}</div>\n}\n\n// In your test (with mocked fetch)\nlet root = createRoot(container)\nroot.render(<AsyncLoader />)\nroot.flush()\n\nexpect(container.textContent).toBe('Loading...')\n\n// After fetch resolves\nawait waitForFetch()\nroot.flush()\n\nexpect(container.textContent).toBe('Expected data')\n```\n\n## Testing Component Removal\n\nUse `root.dispose()` to clean up and verify cleanup behavior:\n\n```tsx\nlet root = createRoot(container)\nroot.render(<MyComponent />)\nroot.flush()\n\n// Verify setup behavior\nexpect(container.querySelector('.content')).toBeTruthy()\n\n// Remove and verify cleanup\nroot.dispose()\nexpect(container.innerHTML).toBe('')\n```\n\n## See Also\n\n- [Getting Started](./getting-started.md) - Root methods reference\n- [Handle API](./handle.md) - `handle.queueTask()` behavior\n"
  },
  {
    "path": "packages/component/docs/tween.md",
    "content": "# Tween API\n\nA generator-based tween function for animating values over time with cubic bezier easing.\n\n## Basic Usage\n\n```tsx\nimport { tween, easings } from 'remix/component'\n\nlet animation = tween({\n  from: 0,\n  to: 100,\n  duration: 1000,\n  curve: easings.easeInOut,\n})\n\n// Initialize generator\nanimation.next()\n\nfunction animate(timestamp: number) {\n  let { value, done } = animation.next(timestamp)\n  element.style.transform = `translateX(${value}px)`\n  if (!done) requestAnimationFrame(animate)\n}\n\nrequestAnimationFrame(animate)\n```\n\n## How It Works\n\nThe `tween` function returns a generator that:\n\n1. Yields the current interpolated value on each iteration\n2. Receives the current timestamp via `next(timestamp)`\n3. Returns `done: true` when the duration has elapsed\n\nThe generator uses cubic bezier curves to map linear time progress to eased value progress, matching CSS `cubic-bezier()` timing functions.\n\n## Easing Presets\n\nBuilt-in easing curves matching CSS timing functions:\n\n```tsx\nimport { easings } from 'remix/component'\n\neasings.linear // { x1: 0, y1: 0, x2: 1, y2: 1 }\neasings.ease // { x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 }\neasings.easeIn // { x1: 0.42, y1: 0, x2: 1, y2: 1 }\neasings.easeOut // { x1: 0, y1: 0, x2: 0.58, y2: 1 }\neasings.easeInOut // { x1: 0.42, y1: 0, x2: 0.58, y2: 1 }\n```\n\n## Custom Curves\n\nDefine custom bezier curves with control points:\n\n```tsx\nlet customCurve = {\n  x1: 0.68,\n  y1: -0.55,\n  x2: 0.265,\n  y2: 1.55,\n}\n\nlet animation = tween({\n  from: 0,\n  to: 100,\n  duration: 500,\n  curve: customCurve,\n})\n```\n\nThe control points match CSS `cubic-bezier(x1, y1, x2, y2)` syntax.\n\n## In Components\n\nUse tween with `handle.signal` for automatic cleanup:\n\n```tsx\nfunction AnimatedValue(handle: Handle) {\n  let value = 0\n\n  function animateTo(target: number) {\n    let animation = tween({\n      from: value,\n      to: target,\n      duration: 300,\n      curve: easings.easeOut,\n    })\n\n    animation.next() // Initialize\n\n    function tick(timestamp: number) {\n      if (handle.signal.aborted) return\n\n      let result = animation.next(timestamp)\n      value = result.value\n      handle.update()\n\n      if (!result.done) {\n        requestAnimationFrame(tick)\n      }\n    }\n\n    requestAnimationFrame(tick)\n  }\n\n  return () => (\n    <div>\n      <div style={{ transform: `translateX(${value}px)` }}>Moving</div>\n      <button\n        mix={[\n          pressEvents(),\n          on('press', () => {\n            animateTo(200)\n          }),\n        ]}\n      >\n        Animate\n      </button>\n    </div>\n  )\n}\n```\n\n## Multiple Properties\n\nAnimate multiple values with separate tweens:\n\n```tsx\nlet xAnimation = tween({ from: 0, to: 100, duration: 500, curve: easings.easeOut })\nlet yAnimation = tween({ from: 0, to: 50, duration: 500, curve: easings.easeOut })\nlet scaleAnimation = tween({ from: 1, to: 1.5, duration: 500, curve: easings.easeOut })\n\nxAnimation.next()\nyAnimation.next()\nscaleAnimation.next()\n\nfunction animate(timestamp: number) {\n  let x = xAnimation.next(timestamp)\n  let y = yAnimation.next(timestamp)\n  let scale = scaleAnimation.next(timestamp)\n\n  element.style.transform = `translate(${x.value}px, ${y.value}px) scale(${scale.value})`\n\n  if (!x.done || !y.done || !scale.done) {\n    requestAnimationFrame(animate)\n  }\n}\n\nrequestAnimationFrame(animate)\n```\n\n## API Reference\n\n### `tween(options)`\n\nCreates a generator that interpolates between values over time.\n\n```ts\ninterface TweenOptions {\n  from: number // Starting value\n  to: number // Ending value\n  duration: number // Duration in milliseconds\n  curve: BezierCurve // Easing curve\n}\n\ninterface BezierCurve {\n  x1: number // First control point X (0-1)\n  y1: number // First control point Y\n  x2: number // Second control point X (0-1)\n  y2: number // Second control point Y\n}\n```\n\n**Returns:** `Generator<number, number, number>` - Yields current value, returns final value when done.\n\n### `easings`\n\nObject containing preset bezier curves:\n\n| Preset      | Description               |\n| ----------- | ------------------------- |\n| `linear`    | No easing, constant speed |\n| `ease`      | Default CSS ease          |\n| `easeIn`    | Slow start, fast end      |\n| `easeOut`   | Fast start, slow end      |\n| `easeInOut` | Slow start and end        |\n\n## When to Use\n\nUse `tween` for:\n\n- Imperative animations driven by `requestAnimationFrame`\n- Canvas/WebGL animations\n- Animating non-CSS properties\n- Complex sequenced animations\n\nFor most UI animations, prefer animation mixins (`animateEntrance`, `animateExit`, `animateLayout`)\nor CSS transitions with [`spring`](./spring.md).\n\n## See Also\n\n- [Spring API](./spring.md) - Physics-based easing for CSS\n"
  },
  {
    "path": "packages/component/package.json",
    "content": "{\n  \"name\": \"@remix-run/component\",\n  \"version\": \"0.5.0\",\n  \"description\": \"UI components for Remix\",\n  \"author\": \"Ryan Florence <rpflorence@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/component\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/component#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"AGENTS.md\",\n    \"dist\",\n    \"src\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./server\": \"./src/server.ts\",\n    \"./jsx-runtime\": \"./src/jsx-runtime.ts\",\n    \"./jsx-dev-runtime\": \"./src/jsx-dev-runtime.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./server\": {\n        \"types\": \"./dist/server.d.ts\",\n        \"default\": \"./dist/server.js\"\n      },\n      \"./jsx-runtime\": {\n        \"types\": \"./dist/jsx-runtime.d.ts\",\n        \"default\": \"./dist/jsx-runtime.js\"\n      },\n      \"./jsx-dev-runtime\": {\n        \"types\": \"./dist/jsx-dev-runtime.d.ts\",\n        \"default\": \"./dist/jsx-dev-runtime.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"size\": \"esbuild src/index.ts --bundle --minify --outfile=.temp-bundle.js --format=esm && echo \\\"📦 Bundle sizes:\\\" && echo \\\"   Uncompressed: $(ls -lah .temp-bundle.js | awk '{print $5}')\\\" && echo \\\"   Gzipped: $(gzip -c .temp-bundle.js | wc -c | tr -d ' ') bytes\\\" && rm .temp-bundle.js\",\n    \"test\": \"vitest run\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"test:watch\": \"vitest\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"prepublishOnly\": \"pnpm build\"\n  },\n  \"dependencies\": {\n    \"@types/dom-navigation\": \"^1.0.7\"\n  },\n  \"devDependencies\": {\n    \"@vitest/browser\": \"^3.2.4\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"esbuild\": \"^0.25.5\",\n    \"playwright\": \"catalog:\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^3.2.4\"\n  }\n}\n"
  },
  {
    "path": "packages/component/skills/animate-elements/SKILL.md",
    "content": "---\nname: animate-elements\ndescription: Build UI animations in Remix using mixins (`mix`, `animateEntrance`, `animateExit`, `animateLayout`). Use when implementing enter/exit transitions, FLIP reordering, shared-layout swaps, or animation-heavy app interactions.\n---\n\n# Animating Elements (`remix/component`)\n\nUse this skill when building animations.\n\n## Quick Start\n\nImport mixin helpers from `remix/component` and apply them via `mix`.\n\n```tsx\nimport { animateEntrance, animateExit, animateLayout, spring } from 'remix/component'\n\nlet el = (\n  <div\n    key=\"card\"\n    mix={[\n      animateEntrance({ opacity: 0, transform: 'scale(0.95)', ...spring('snappy') }),\n      animateExit({ opacity: 0, transform: 'scale(0.98)', duration: 120, easing: 'ease-in' }),\n      animateLayout({ duration: 220, easing: 'ease-out' }),\n    ]}\n  />\n)\n```\n\n## Core Patterns\n\n### 1) Enter-only element\n\n```tsx\n<div\n  mix={[\n    animateEntrance({\n      opacity: 0,\n      transform: 'translateY(8px)',\n      duration: 180,\n      easing: 'ease-out',\n    }),\n  ]}\n/>\n```\n\n### 2) Toggle visibility with enter + exit\n\n```tsx\n{\n  isVisible && (\n    <div\n      key=\"panel\"\n      mix={[\n        animateEntrance({ opacity: 0, transform: 'scale(0.98)', duration: 180 }),\n        animateExit({ opacity: 0, transform: 'scale(0.98)', duration: 120, easing: 'ease-in' }),\n      ]}\n    />\n  )\n}\n```\n\n### 3) Reordering/list layout animation\n\n```tsx\n{\n  items.map((item) => (\n    <li\n      key={item.id}\n      mix={[\n        animateLayout({\n          ...spring({ duration: 500, bounce: 0.2 }),\n        }),\n      ]}\n    />\n  ))\n}\n```\n\n### 4) Shared-layout swap (same slot, different keyed child)\n\n```tsx\n<div css={{ display: 'grid', '& > *': { gridArea: '1 / 1' } }}>\n  {state ? (\n    <div key=\"a\" mix={[animateEntrance({ opacity: 0 }), animateExit({ opacity: 0 })]} />\n  ) : (\n    <div key=\"b\" mix={[animateEntrance({ opacity: 0 }), animateExit({ opacity: 0 })]} />\n  )}\n</div>\n```\n\n## Practical Guidance\n\n- Always key conditional/switching elements you expect to transition.\n- Use `animateLayout` on the element whose position/size changes.\n- Prefer one clear transition intent per mixin:\n  - entrance starts from a defined initial style\n  - exit ends at a defined final style\n- For spring-style timing, spread `spring(...)` into the mixin config.\n- Default to `...spring()` for duration/easing in most cases.\n- Keep effectful DOM work (WAAPI shake, measurements) in `handle.queueTask(...)` or `ref(...)`, not in pure render math.\n\n## Animation Checklist\n\n- [ ] Animated elements have stable keys where needed.\n- [ ] `animateLayout` is only on moving/resizing nodes.\n- [ ] No unnecessary custom state machines when simple mixins suffice.\n"
  },
  {
    "path": "packages/component/skills/create-mixins/SKILL.md",
    "content": "---\nname: create-mixins\ndescription: Create @remix-run/component mixins using createMixin with lifecycle-first semantics. Use when adding new mixins with correct runtime behavior and type flow.\n---\n\n# Creating Mixins (`@remix-run/component`)\n\nUse this skill when authoring new mixins in `packages/component`.\n\nThe key principle: **model the real runtime contract first, then write the smallest code that matches it**.\n\n## Core Runtime Semantics\n\nTreat these as constraints, not suggestions:\n\n1. A mixin handle is tied to one mounted host node lifecycle.\n2. `insert` is the host-node availability point for imperative setup.\n3. `remove` is teardown for that same lifecycle.\n4. `queueTask` runs post-commit and receives `(node, signal)` for mixins.\n5. Mixin render functions should stay pure; side effects belong in `insert`, `remove`, or queued work.\n\n```tsx\ncreateMixin<NodeType>((handle) => {\n  // Setup runs once per handle lifecycle.\n  handle.addEventListener('insert', (event) => {\n    // event.node is the mounted host node for this lifecycle.\n    // Attach imperative effects here.\n  })\n\n  handle.addEventListener('remove', () => {\n    // Teardown for the same lifecycle.\n    // Remove listeners, abort work, release resources.\n  })\n\n  return (props) => {\n    // Render stays pure: derive props/JSX only.\n    // Post-commit work goes in queueTask when needed.\n    handle.queueTask((node) => {\n      // Runs after commit with the concrete host node.\n    })\n    return <handle.element {...props} />\n  }\n})\n```\n\nIf your implementation assumes semantics that do not exist (node swapping, repeated insert for the same handle, etc.), remove that logic.\n\n## Authoring Rules\n\n1. Start with lifecycle truth:\n   - Use `insert` for attach/setup.\n   - Use `remove` for detach/cleanup.\n2. Keep state minimal and intentional:\n   - Do not keep mutable state \"just in case\" if runtime guarantees make it unnecessary.\n3. Be precise with defensive checks:\n   - Use `invariant(...)` when a condition is guaranteed by runtime and violation means framework bug.\n   - Use soft guards only when nullability is genuinely part of valid runtime flow.\n4. Use `queueTask((node, signal) => ...)` for post-commit DOM work.\n   - In most mixins, only `node` is needed.\n   - Reach for `signal` only when work is async or cancellation-sensitive.\n5. Do not add `signal.aborted` checks for purely synchronous work.\n6. Favor function expression for helpers in scope.\n7. Avoid speculative runtime assumptions.\n\n## Preferred Patterns\n\n### 1) Pure prop transform mixin\n\n```tsx\nlet withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (\n  <handle.element {...props} title={title} />\n))\n```\n\n### 2) Lifecycle-managed imperative listener\n\n```tsx\nlet withFocus = createMixin<HTMLElement>((handle) => {\n  handle.addEventListener('insert', (event) => {\n    event.node.focus()\n  })\n  return (props) => <handle.element {...props} />\n})\n```\n\n### 3) Post-commit rebind with node provided by queueTask\n\n```tsx\nhandle.queueTask((node) => {\n  node.removeEventListener(prevType, stableHandler, prevCapture)\n  node.addEventListener(nextType, stableHandler, nextCapture)\n})\n```\n\n## Anti-Patterns\n\n- Adding state for hypothetical runtime scenarios.\n- Broad defensive null checks where runtime guarantees presence.\n- Mixing setup/cleanup side effects into render-only code paths.\n- Using `signal.aborted` in synchronous non-racy code as boilerplate.\n- Hiding semantic uncertainty with casts instead of fixing types/contracts.\n\n## Mixin Creation Checklist\n\n- [ ] Runtime assumptions are stated and match reconciler behavior.\n- [ ] Lifecycle wiring uses `insert`/`remove` directly.\n- [ ] State is minimal; no \"might change later\" scaffolding.\n- [ ] `queueTask` used only where post-commit timing is required.\n- [ ] Type flow from `createMixin<ThisType>` is preserved.\n- [ ] Tests cover ordering, teardown, and type inference contracts.\n"
  },
  {
    "path": "packages/component/src/index.ts",
    "content": "/// <reference types=\"dom-navigation\" preserve=\"true\" />\n\n// -- Roots --\nexport { run } from './lib/run.ts'\nexport type { AppRuntime, AppRuntimeEventMap, RunInit } from './lib/run.ts'\nexport type { ComponentErrorEvent } from './lib/error-event.ts'\n\nexport { createRoot, createRangeRoot, createScheduler } from './lib/vdom.ts'\nexport type { VirtualRoot, VirtualRootEventMap, VirtualRootOptions, Scheduler } from './lib/vdom.ts'\n\n// -- Client Entries --\nexport { clientEntry } from './lib/client-entries.ts'\nexport type {\n  SerializablePrimitive,\n  SerializableObject,\n  SerializableArray,\n  SerializableValue,\n  SerializableProps,\n  EntryComponent,\n} from './lib/client-entries.ts'\n\n// -- Components --\nexport { Fragment, Frame } from './lib/component.ts'\nexport type {\n  Task,\n  Handle,\n  Context,\n  FrameHandleEventMap,\n  FrameContent,\n  FrameHandle,\n  FrameProps,\n} from './lib/component.ts'\nexport type { LoadModule, ResolveFrame } from './lib/frame.ts'\n\n// -- Elements/JSX/Props --\nexport { createElement } from './lib/create-element.ts'\nexport type {\n  ElementType,\n  ElementProps,\n  RemixElement,\n  Renderable,\n  RemixNode,\n  Props,\n} from './lib/jsx.ts'\nexport type { HostProps, LayoutAnimationConfig } from './lib/dom.ts'\nexport { createMixin } from './lib/mixin.ts'\nexport type { MixinDescriptor, MixinHandle, MixinType, MixValue } from './lib/mixin.ts'\nexport { TypedEventTarget } from './lib/typed-event-target.ts'\nexport { addEventListeners } from './lib/event-listeners.ts'\nexport { on } from './lib/mixins/on-mixin.tsx'\nexport type { Dispatched } from './lib/mixins/on-mixin.tsx'\nexport { link } from './lib/mixins/link-mixin.tsx'\nexport { keysEvents } from './lib/mixins/keys-mixin.tsx'\nexport { pressEvents } from './lib/mixins/press-mixin.tsx'\nexport type { PressEvent } from './lib/mixins/press-mixin.tsx'\nexport { ref } from './lib/mixins/ref-mixin.tsx'\nexport type { RefCallback } from './lib/mixins/ref-mixin.tsx'\nexport { css } from './lib/mixins/css-mixin.tsx'\nexport { animateEntrance, animateExit } from './lib/mixins/animate-mixins.tsx'\nexport { animateLayout } from './lib/mixins/animate-layout-mixin.tsx'\n\n// -- Animation --\nexport { spring } from './lib/spring.ts'\nexport type { SpringIterator, SpringPreset, SpringOptions } from './lib/spring.ts'\n\nexport { tween, easings } from './lib/tween.ts'\nexport type { TweenOptions, BezierCurve } from './lib/tween.ts'\n\n// -- Navigation --\nexport { navigate } from './lib/navigation.ts'\nexport type { NavigationOptions } from './lib/navigation.ts'\n"
  },
  {
    "path": "packages/component/src/jsx-dev-runtime.ts",
    "content": "export * from './jsx-runtime.ts'\n"
  },
  {
    "path": "packages/component/src/jsx-runtime.ts",
    "content": "export * from './lib/jsx.ts'\nexport { Fragment } from './lib/component.ts'\n"
  },
  {
    "path": "packages/component/src/lib/client-entries.ts",
    "content": "import type { Handle, NoContext, RemixNode } from './component.ts'\n\n/**\n * Serializable primitive types that can be passed as props to entry components\n */\nexport type SerializablePrimitive = string | number | boolean | null | undefined\n\n/**\n * Serializable object types that can be passed as props to entry components\n */\nexport type SerializableObject = {\n  [key: string]: SerializableValue\n}\n\n/**\n * Serializable array types that can be passed as props to entry components\n */\nexport type SerializableArray = SerializableValue[]\n\n/**\n * All serializable values that can be passed as props to entry components.\n * This includes primitives, objects, arrays, and Remix Elements.\n */\nexport type SerializableValue =\n  | SerializablePrimitive\n  | SerializableObject\n  | SerializableArray\n  | RemixNode\n\n/**\n * Constraint that ensures all properties in an object are serializable.\n */\nexport type SerializableProps = {\n  [K in string]: SerializableValue\n}\n\n/**\n * Metadata added to entry components\n */\nexport type EntryMetadata = {\n  $entry: true\n  $moduleUrl: string\n  $exportName: string\n}\n\n/**\n * An entry component preserves the exact function type with added metadata\n */\nexport type EntryComponent<context = NoContext, setup = undefined, props = {}> = ((\n  handle: Handle<context>,\n  setup: setup,\n) => (props: props) => RemixNode) &\n  EntryMetadata\n\n/**\n * Marks a component as a client entry for client-side hydration.\n *\n * @param href Module URL with optional export name (format: \"/js/module.js#ExportName\")\n * @param component Component function that will be hydrated on the client\n * @returns The component augmented with entry metadata\n *\n * @example\n * ```tsx\n * export const Counter = clientEntry(\n *   '/js/counter.js#Counter',\n *   (handle: Handle, setup: number) {\n *     let count = setup\n *\n *     return ({ label }: { label: string }) => (\n *       <button\n *         type=\"button\"\n *         mix={[\n *           on('click', () => {\n *             count++\n *             handle.update()\n *           }),\n *         ]}\n *       >\n *         {label} {count}\n *       </button>\n *     )\n *   }\n * )\n * ```\n */\nexport function clientEntry<\n  context = NoContext,\n  setup extends SerializableValue = undefined,\n  props extends SerializableProps = {},\n>(\n  href: string,\n  component: (handle: Handle<context>, setup: setup) => (props: props) => RemixNode,\n): EntryComponent<context, setup, props>\n\n// Implementation\nexport function clientEntry(href: string, component: any): any {\n  // Parse module URL and export name\n  let [moduleUrl, exportName] = href.split('#')\n\n  if (!moduleUrl) {\n    throw new Error('clientEntry() requires a module URL')\n  }\n\n  // Use component name as fallback if no export name provided\n  let finalExportName = exportName || component.name\n\n  if (!finalExportName) {\n    throw new Error(\n      'clientEntry() requires either an export name in the href (e.g., \"/js/module.js#ComponentName\") or a named component function',\n    )\n  }\n\n  // Augment the component with entry metadata\n  component.$entry = true\n  component.$moduleUrl = moduleUrl\n  component.$exportName = finalExportName\n\n  return component\n}\n\n/**\n * Type guard to check if a component is an entry component\n *\n * @param component The component to check\n * @returns True if the component has entry metadata\n */\nexport function isEntry(component: unknown): component is EntryComponent {\n  return Boolean(component && typeof component === 'function' && (component as any).$entry === true)\n}\n\n/**\n * Logs a client-hydration mismatch to the console.\n *\n * @param msg Message parts to forward to the logger.\n */\nexport function logHydrationMismatch(...msg: any[]) {\n  console.error('Hydration mismatch:', ...msg)\n}\n\n/**\n * Advances a DOM cursor past consecutive comment nodes.\n *\n * @param cursor Starting DOM node.\n * @returns The first non-comment node, or `null` when none remains.\n */\nexport function skipComments(cursor: Node | null): Node | null {\n  while (cursor && cursor.nodeType === Node.COMMENT_NODE) {\n    cursor = cursor.nextSibling\n  }\n  return cursor\n}\n"
  },
  {
    "path": "packages/component/src/lib/component.ts",
    "content": "import type { ElementProps, ElementType, RemixNode, Renderable } from './jsx.ts'\nimport { TypedEventTarget } from './typed-event-target.ts'\n\n/**\n * Task queued to run after a component update completes.\n */\nexport type Task = (signal: AbortSignal) => void\n\n/**\n * Runtime handle passed to component setup functions.\n */\nexport interface Handle<C = Record<string, never>> {\n  /**\n   * Stable identifier per component instance. Useful for HTML APIs like\n   * htmlFor, aria-owns, etc. so consumers don't have to supply an id.\n   */\n  id: string\n\n  /**\n   * Set and get values in an element tree for indirect ancestor/descendant\n   * communication.\n   */\n  context: Context<C>\n\n  /**\n   * Schedules an update for the component to render again. Returns a promise\n   * that resolves with an AbortSignal after the update completes. The signal\n   * is aborted when the component re-renders or is removed.\n   *\n   * @returns A promise that resolves with an AbortSignal after the update\n   */\n  update(): Promise<AbortSignal>\n\n  /**\n   * Schedules a task to run after the next update.\n   *\n   * @param task\n   */\n  queueTask(task: Task): void\n\n  /**\n   * The component's closest frame\n   */\n  frame: FrameHandle\n\n  /**\n   * Access named frames in the current runtime tree.\n   */\n  frames: {\n    /**\n     * The root frame for the current runtime tree.\n     */\n    readonly top: FrameHandle\n    get(name: string): FrameHandle | undefined\n  }\n\n  /**\n   * A signal indicating the connected status of the component. When the\n   * component is disconnected from the tree the signal will be aborted.\n   * Useful for setup scope cleanup.\n   *\n   * @example Clear a timer\n   * ```ts\n   * function Clock(handle: Handle) {\n   *   let interval = setInterval(() => {\n   *     if (handle.signal.aborted) {\n   *       clearInterval(interval)\n   *       return\n   *     }\n   *     handle.update()\n   *   }, 1000)\n   *   return () => <span>{new Date().toString()}</span>\n   * }\n   * ```\n   *\n   * Because signals are event targets, you can also add an event instead.\n   * ```ts\n   * function Clock(handle: Handle) {\n   *   let interval = setInterval(handle.update)\n   *   handle.signal.addEventListener(\"abort\", () => clearInterval(interval))\n   *   return () => <span>{new Date().toString()}</span>\n   * }\n   * ```\n   *\n   * You don't need to check both this.signal and a render/event signal as\n   * render/event signals are aborted when the component disconnects.\n   */\n  signal: AbortSignal\n}\n\n/**\n * Default Handle context so types must be declared explicitly.\n */\nexport type NoContext = Record<string, never>\n\n/**\n * Component factory shape used by the Remix component runtime.\n */\nexport type Component<Context = NoContext, Setup = undefined, Props = ElementProps> = (\n  handle: Handle<Context>,\n  setup: Setup,\n) => (props: Props) => RemixNode\n\n/**\n * Infers the context provided by a component or handle-compatible function.\n */\nexport type ContextFrom<ComponentType> =\n  ComponentType extends Component<infer Provided, any, any>\n    ? Provided\n    : ComponentType extends (handle: Handle<infer Provided>, ...args: any[]) => any\n      ? Provided\n      : never\n\n/**\n * Context storage API exposed on component handles.\n */\nexport interface Context<C> {\n  /** Replaces the current context value for this component instance. */\n  set(values: C): void\n  /** Reads the context value associated with the given component type. */\n  get<ComponentType>(component: ComponentType): ContextFrom<ComponentType>\n  /** Reads the context value associated with the given component key. */\n  get(component: ElementType | symbol): unknown | undefined\n}\n\n/**\n * Content that can be rendered into a frame.\n */\nexport type FrameContent = ReadableStream<Uint8Array> | string | RemixNode\n\n/**\n * Events emitted by frame handles during reloads.\n */\nexport type FrameHandleEventMap = {\n  reloadStart: Event\n  reloadComplete: Event\n}\n\n/**\n * Public API for interacting with a frame instance.\n */\nexport type FrameHandle = TypedEventTarget<FrameHandleEventMap> & {\n  src: string\n  reload(): Promise<AbortSignal>\n  replace(content: FrameContent): Promise<void>\n  // Internal runtime context used by client-rendered Frame reconciliation.\n  $runtime?: unknown\n}\n\n/**\n * Props accepted by the built-in {@link Frame} component.\n */\nexport interface FrameProps {\n  /** Optional frame name used for targeted navigation and lookups. */\n  name?: string\n  /** Source URL used when the frame loads or reloads its content. */\n  src: string\n  /** Fallback content to render while the frame is pending. */\n  fallback?: Renderable\n  /** Event handlers invoked for events dispatched from the frame element. */\n  on?: Record<string, (event: Event, signal: AbortSignal) => void | Promise<void>>\n}\n\n/**\n * Component factory function that receives setup input and returns a render function.\n */\nexport type ComponentFn<Context = NoContext, Setup = undefined, Props = Record<string, never>> = (\n  handle: Handle<Context>,\n  setup: Setup,\n) => RenderFn<Props>\n\n/**\n * Render function returned by a component factory.\n */\nexport type RenderFn<P = {}> = (props: P) => RemixNode\n\nexport type { RemixNode } from './jsx.ts'\n\n// Handle is already exported as an interface above, no need to re-export\n\n/**\n * Props accepted by the built-in {@link Fragment} component.\n */\nexport interface FragmentProps {\n  /** Child nodes to render without adding an extra host element. */\n  children?: RemixNode\n}\n\n/**\n * Mapping of built-in component names to their prop shapes.\n */\nexport interface BuiltinElements {\n  /** Props accepted by the built-in fragment component. */\n  Fragment: FragmentProps\n  /** Props accepted by the built-in frame component. */\n  Frame: FrameProps\n}\n\n/**\n * Key type used to stabilize host elements and components during reconciliation.\n */\nexport type Key = string | number | bigint\n\ntype ComponentConfig = {\n  id: string\n  type: Function\n  frame: FrameHandle\n  getContext: (type: Component) => unknown\n  getFrameByName: (name: string) => FrameHandle | undefined\n  getTopFrame?: () => FrameHandle | undefined\n}\n\n/**\n * Runtime handle returned by {@link createComponent}.\n */\nexport type ComponentHandle = ReturnType<typeof createComponent>\n\n/**\n * Creates the internal runtime wrapper for a component instance.\n *\n * @param config Component runtime configuration.\n * @returns Component runtime helpers used by the reconciler.\n */\nexport function createComponent<C = NoContext>(config: ComponentConfig) {\n  let taskQueue: Task[] = []\n  let renderCtrl: AbortController | null = null\n  let connectedCtrl: AbortController | null = null\n  let contextValue: C | undefined = undefined\n\n  function getConnectedSignal() {\n    if (!connectedCtrl) connectedCtrl = new AbortController()\n    return connectedCtrl.signal\n  }\n\n  let getContent: null | ((props: ElementProps) => RemixNode) = null\n  let scheduleUpdate: () => void = () => {\n    throw new Error('scheduleUpdate not implemented')\n  }\n\n  let context: Context<C> = {\n    set: (value: C) => {\n      contextValue = value\n    },\n    get: (type: Component) => config.getContext(type),\n  }\n\n  let handle: Handle<C> = {\n    id: config.id,\n    update: () =>\n      new Promise((resolve) => {\n        taskQueue.push((signal) => resolve(signal))\n        scheduleUpdate()\n      }),\n    queueTask: (task: Task) => {\n      taskQueue.push(task)\n    },\n    frame: config.frame,\n    frames: {\n      get top() {\n        return config.getTopFrame?.() ?? config.frame\n      },\n      get(name: string) {\n        return config.getFrameByName(name)\n      },\n    },\n    context: context,\n    get signal() {\n      return getConnectedSignal()\n    },\n  }\n\n  function dequeueTasks(): (() => void)[] {\n    // Only create render controller if any task expects a signal (has length >= 1)\n    let needsSignal = taskQueue.some((task) => task.length >= 1)\n    if (needsSignal && !renderCtrl) {\n      renderCtrl = new AbortController()\n    }\n    let signal = renderCtrl?.signal\n    return taskQueue.splice(0, taskQueue.length).map((task) => () => task(signal!))\n  }\n\n  function render(props: ElementProps): [RemixNode, Array<() => void>] {\n    if (connectedCtrl?.signal.aborted) {\n      console.warn('render called after component was removed, potential application memory leak')\n      return [null, []]\n    }\n\n    // Only abort render controller if it was initialized\n    if (renderCtrl) {\n      renderCtrl.abort()\n      renderCtrl = null\n    }\n\n    if (!getContent) {\n      // Extract setup prop (passed to component setup function, not render)\n      let { setup, ...propsWithoutSetup } = props as { setup?: unknown }\n      let result = config.type(handle, setup)\n      if (typeof result !== 'function') {\n        let name = config.type.name || 'Anonymous'\n        throw new Error(`${name} must return a render function, received ${typeof result}`)\n      } else {\n        getContent = (props) => {\n          // Strip setup from props since it's only for setup\n          let { setup: _, ...rest } = props as { setup?: unknown }\n          return result(rest)\n        }\n      }\n    }\n\n    let node = getContent(props)\n    return [node, dequeueTasks()]\n  }\n\n  function remove(): (() => void)[] {\n    connectedCtrl?.abort()\n    renderCtrl?.abort()\n    return dequeueTasks()\n  }\n\n  function setScheduleUpdate(_scheduleUpdate: () => void) {\n    scheduleUpdate = _scheduleUpdate\n  }\n\n  function getContextValue(): C | undefined {\n    return contextValue\n  }\n\n  return { render, remove, setScheduleUpdate, frame: config.frame, getContextValue }\n}\n\n/**\n * Built-in component used to render nested frame content.\n *\n * @param handle Component handle for the frame instance.\n * @returns A placeholder render function handled by the reconciler.\n */\nexport function Frame(handle: Handle<FrameHandle>) {\n  return (_: FrameProps) => null // reconciler renders\n}\n\n/**\n * Built-in component used to group children without adding a host element.\n *\n * @returns A placeholder render function handled by the reconciler.\n */\nexport function Fragment() {\n  return (_: FragmentProps) => null // reconciler renders\n}\n\n/**\n * Creates a frame handle with default no-op implementations for testing and internal wiring.\n *\n * @param def Partial frame-handle implementation to merge with the defaults.\n * @returns A frame handle object.\n */\nexport function createFrameHandle(\n  def?: Partial<{\n    src: string\n    replace: FrameHandle['replace']\n    reload: FrameHandle['reload']\n    $runtime: FrameHandle['$runtime']\n  }>,\n): FrameHandle {\n  return Object.assign(\n    new TypedEventTarget<FrameHandleEventMap>(),\n    {\n      src: '/',\n      replace: notImplemented('replace not implemented'),\n      reload: notImplemented('reload not implemented'),\n    },\n    def,\n  )\n}\n\nfunction notImplemented(msg: string) {\n  return (): never => {\n    throw new Error(msg)\n  }\n}\n"
  },
  {
    "path": "packages/component/src/lib/create-element.ts",
    "content": "import { jsx } from './jsx.ts'\nimport type { RemixElement } from './jsx.ts'\n\n/**\n * Creates a Remix virtual element from a JSX-like call signature.\n *\n * @param type Host tag or component function.\n * @param props Element props.\n * @param children Child nodes.\n * @returns A Remix virtual element.\n */\nexport function createElement(\n  type: string,\n  props?: Record<string, any>,\n  ...children: any[]\n): RemixElement {\n  if (props?.key != null) {\n    let { key, ...rest } = props\n    return jsx(type, { ...rest, children }, key)\n  }\n\n  return jsx(type, { ...props, children })\n}\n"
  },
  {
    "path": "packages/component/src/lib/diff-dom.ts",
    "content": "import { invariant } from './invariant.ts'\nimport type { FrameContext } from './frame.ts'\n\nexport function diffNodes(curr: Node[], next: Node[], context: FrameContext) {\n  let parent = curr[0]?.parentNode ?? context.regionParent ?? null\n  invariant(parent, 'Parent node not found')\n\n  // When diffing a bounded region (e.g. between frame comments), we should insert new\n  // nodes before the region tail ref rather than appending to the parent.\n  let regionTailRef: ChildNode | null =\n    context.regionTailRef ??\n    (curr.length > 0 ? (curr[curr.length - 1].nextSibling as ChildNode | null) : null)\n\n  let max = Math.max(curr.length, next.length)\n  for (let i = 0; i < max; i++) {\n    let c = curr[i]\n    let n = next[i]\n\n    if (!c && n) {\n      if (regionTailRef) {\n        parent.insertBefore(n, regionTailRef)\n      } else {\n        parent.appendChild(n)\n      }\n    } else if (c && !n) {\n      disposeRemovedSubFrames(c, context)\n      parent.removeChild(c)\n    } else if (c && n) {\n      // Skip hydrated client-entry boundary ranges; hydration pass re-renders\n      // roots with new props from incoming payload\n      if (isVirtualRootStartMarker(c) && isVirtualRootStartMarker(n)) {\n        let currentEnd = findHydrationEndMarker(c)\n        let nextEnd = findHydrationEndMarker(n)\n        let nextData = n.data\n        if (c.data !== nextData) c.data = nextData\n\n        let currentEndIndex = curr.indexOf(currentEnd)\n        let nextEndIndex = next.indexOf(nextEnd)\n        i = Math.max(currentEndIndex, nextEndIndex)\n        continue\n      }\n\n      let cursor = diffNode(c, n, context)\n      if (cursor) {\n        i = next.indexOf(cursor)\n      }\n    }\n  }\n}\n\nfunction diffNode(current: Node, next: Node, context: FrameContext): ChildNode | undefined {\n  // Text -> Text\n  if (isTextNode(current) && isTextNode(next)) {\n    let newText = next.textContent || ''\n    if (current.textContent !== newText) current.textContent = newText\n    return\n  }\n\n  // Hydration boundary -> Hydration boundary\n  if (isVirtualRootStartMarker(current) && isVirtualRootStartMarker(next)) {\n    let nextData = next.data\n    if (current.data !== nextData) {\n      current.data = nextData\n    }\n\n    let end = findHydrationEndMarker(next)\n    // Fast-forward across this hydrated region.\n    return end\n  }\n\n  // Comment -> Comment\n  if (isCommentNode(current) && isCommentNode(next)) {\n    let newData = next.data\n    if (current.data !== newData) current.data = newData\n    return\n  }\n\n  // Element -> Element\n  if (isElement(current) && isElement(next)) {\n    // Different tags: replace\n    if (current.tagName !== next.tagName) {\n      let parent = current.parentNode\n      if (parent) parent.replaceChild(next, current)\n      return\n    }\n\n    // Same tag: update attributes then children\n    diffElementAttributes(current, next)\n    if (shouldPreserveElementChildren(current, next)) return\n    diffElementChildren(current, next, context)\n    return\n  }\n\n  // Type mismatch: replace\n  let parent = current.parentNode\n  if (parent) parent.replaceChild(next, current)\n}\n\nfunction diffElementAttributes(current: Element, next: Element): void {\n  let prevAttrNames = current.getAttributeNames()\n  let nextAttrNames = next.getAttributeNames()\n\n  let nextNameSet = new Set(nextAttrNames)\n\n  // Removals\n  for (let name of prevAttrNames) {\n    if (!nextNameSet.has(name)) {\n      if (shouldPreserveLiveAttribute(current, next, name)) continue\n      current.removeAttribute(name)\n    }\n  }\n\n  // Additions/updates\n  for (let name of nextAttrNames) {\n    let prevVal = current.getAttribute(name)\n    let nextVal = next.getAttribute(name)\n    if (prevVal !== nextVal) {\n      if (shouldPreserveLiveAttribute(current, next, name)) continue\n      current.setAttribute(name, nextVal == null ? '' : String(nextVal))\n    }\n  }\n}\n\nfunction shouldPreserveLiveAttribute(current: Element, next: Element, name: string): boolean {\n  if (name === 'open') {\n    if (current instanceof HTMLDetailsElement && next instanceof HTMLDetailsElement) {\n      return current.open !== next.open\n    }\n\n    if (current instanceof HTMLDialogElement && next instanceof HTMLDialogElement) {\n      return current.open !== next.open\n    }\n  }\n\n  if (name === 'checked') {\n    if (current instanceof HTMLInputElement && next instanceof HTMLInputElement) {\n      return current.checked !== next.checked\n    }\n  }\n\n  if (name === 'value') {\n    if (\n      current instanceof HTMLInputElement &&\n      next instanceof HTMLInputElement &&\n      shouldPreserveInputValue(current)\n    ) {\n      return current.value !== next.value\n    }\n  }\n\n  if (name === 'selected') {\n    if (current instanceof HTMLOptionElement && next instanceof HTMLOptionElement) {\n      return current.selected !== next.selected\n    }\n  }\n\n  if (name === 'popover') {\n    return isPopoverOpen(current) !== isPopoverOpen(next)\n  }\n\n  return false\n}\n\nfunction shouldPreserveElementChildren(current: Element, next: Element): boolean {\n  if (current instanceof HTMLTextAreaElement && next instanceof HTMLTextAreaElement) {\n    return current.value !== next.value\n  }\n\n  return false\n}\n\nfunction shouldPreserveInputValue(input: HTMLInputElement): boolean {\n  return (\n    input.type !== 'button' &&\n    input.type !== 'checkbox' &&\n    input.type !== 'hidden' &&\n    input.type !== 'image' &&\n    input.type !== 'radio' &&\n    input.type !== 'reset' &&\n    input.type !== 'submit'\n  )\n}\n\nfunction isPopoverOpen(element: Element): boolean {\n  try {\n    return element.matches(':popover-open')\n  } catch {\n    return false\n  }\n}\n\nfunction diffElementChildren(current: Element, next: Element, context: FrameContext): void {\n  let currentChildren = Array.from(current.childNodes)\n  let nextChildren = Array.from(next.childNodes)\n\n  // Keyed map by data-key for current children\n  let keyToIndex = new Map<string, number>()\n  for (let i = 0; i < currentChildren.length; i++) {\n    let node = currentChildren[i]\n    if (isElement(node)) {\n      let key = node.getAttribute('data-key')\n      if (key != null) keyToIndex.set(key, i)\n    }\n  }\n\n  let used = new Array<boolean>(currentChildren.length).fill(false)\n  let matchIndexForNext = new Array<number>(nextChildren.length).fill(-1)\n\n  for (let i = 0; i < nextChildren.length; i++) {\n    let nextChild = nextChildren[i]\n    let matchIndex = -1\n\n    if (isElement(nextChild)) {\n      let key = nextChild.getAttribute('data-key')\n      if (key != null && keyToIndex.has(key)) {\n        let idx = keyToIndex.get(key)!\n        if (!used[idx]) matchIndex = idx\n      }\n    }\n\n    if (matchIndex === -1) {\n      let candidateIndex = i\n      if (\n        candidateIndex < currentChildren.length &&\n        !used[candidateIndex] &&\n        nodeTypesComparable(currentChildren[candidateIndex], nextChild)\n      ) {\n        matchIndex = candidateIndex\n      }\n    }\n\n    if (matchIndex !== -1) used[matchIndex] = true\n    matchIndexForNext[i] = matchIndex\n  }\n\n  // Forward pass: update matched, collect committed\n  let committed: Array<Node | undefined> = new Array(nextChildren.length)\n  for (let i = 0; i < nextChildren.length; i++) {\n    let mi = matchIndexForNext[i]\n    if (mi !== -1) {\n      let curChild = currentChildren[mi]\n      let cursor = diffNode(curChild, nextChildren[i], context)\n      if (cursor) {\n        // Fast-forward across a hydrated virtual root region.\n        let nextEndIdx = nextChildren.indexOf(cursor)\n        let currEndIdx = findHydrationEndIndex(currentChildren, mi)\n\n        // Adjacent hydration regions can pre-match into the next region and leave\n        // an orphaned `<!-- /rmx:h -->` behind. Clear those stale matches first.\n        for (let j = i + 1; j <= nextEndIdx; j++) {\n          let matchedIndex = matchIndexForNext[j]\n          if (matchedIndex > currEndIdx) {\n            used[matchedIndex] = false\n          }\n          matchIndexForNext[j] = -1\n        }\n\n        // Mark the entire current region as used to avoid removals.\n        for (let k = mi; k <= currEndIdx; k++) used[k] = true\n\n        // Preserve both boundary markers in committed; skip interior in reorder pass.\n        committed[i] = curChild // start marker\n        committed[nextEndIdx] = currentChildren[currEndIdx] // end marker\n        for (let j = i + 1; j < nextEndIdx; j++) committed[j] = undefined\n\n        // Jump to end of region.\n        i = nextEndIdx\n        continue\n      }\n      committed[i] = curChild\n    } else {\n      committed[i] = nextChildren[i]\n    }\n  }\n\n  // Backward pass: reorder via inserts while avoiding redundant moves\n  let anchor: Node | undefined = undefined\n  for (let i = committed.length - 1; i >= 0; i--) {\n    let node = committed[i]\n    if (!node) continue\n\n    // Use only an anchor that is actually a child of the current parent\n    let ref = anchor && anchor.parentNode === current ? anchor : null\n\n    // Do not move hydration boundary markers; keep region stable.\n    // If a boundary marker is new, ensure it is inserted before using it as an anchor.\n    if (isVirtualRootStartMarker(node) || isVirtualRootEndMarker(node)) {\n      if (node.parentNode !== current) {\n        current.insertBefore(node, ref)\n      }\n      anchor = node\n      continue\n    }\n\n    if (node.parentNode === current) {\n      // Node already in parent: move only if its nextSibling is not the desired ref.\n      let targetNext = ref\n      let alreadyInPlace =\n        (targetNext === null && node.nextSibling === null) || node.nextSibling === targetNext\n      if (!alreadyInPlace) {\n        current.insertBefore(node, targetNext)\n      }\n    } else {\n      // New node: insert relative to a valid ref or append\n      current.insertBefore(node, ref)\n    }\n\n    // Advance anchor only after the node is placed in the correct parent\n    if (node.parentNode === current) {\n      anchor = node\n    }\n  }\n\n  // Remove any current children not used\n  for (let i = 0; i < currentChildren.length; i++) {\n    if (!used[i]) {\n      let nodeToRemove = currentChildren[i]\n      disposeRemovedSubFrames(nodeToRemove, context)\n      current.removeChild(currentChildren[i])\n    }\n  }\n}\n\nfunction nodeTypesComparable(a: Node, b: Node): boolean {\n  if (isTextNode(a) && isTextNode(b)) return true\n  if (isElement(a) && isElement(b)) return a.tagName === b.tagName\n  if (isVirtualRootStartMarker(a) && isVirtualRootStartMarker(b)) return true\n  if (isVirtualRootEndMarker(a) && isVirtualRootEndMarker(b)) return true\n  if (isCommentNode(a) && isCommentNode(b)) return true\n  return false\n}\n\nfunction isHydrationEndComment(node: Node): node is Comment {\n  return isCommentNode(node) && node.data.trim() === '/rmx:h'\n}\n\nfunction findHydrationEndMarker(start: Comment): Comment {\n  let node: Node | null = start.nextSibling\n  let depth = 1\n\n  while (node) {\n    if (isCommentNode(node)) {\n      if (isVirtualRootStartMarker(node)) depth++\n      if (isVirtualRootEndMarker(node)) {\n        depth--\n        if (depth === 0) return node\n      }\n    }\n    node = node.nextSibling\n  }\n\n  throw new Error('Hydration end marker not found')\n}\n\nfunction findHydrationEndIndex(nodes: Node[], startIdx: number): number {\n  for (let j = startIdx + 1; j < nodes.length; j++) {\n    if (isHydrationEndComment(nodes[j])) return j\n  }\n  return startIdx\n}\n\nfunction isTextNode(node: Node): node is Text {\n  return node.nodeType === Node.TEXT_NODE\n}\n\nfunction isElement(node: Node): node is Element {\n  return node.nodeType === Node.ELEMENT_NODE\n}\n\nfunction isCommentNode(node: Node): node is Comment {\n  return node.nodeType === Node.COMMENT_NODE\n}\n\nfunction isFrameStartMarker(node: Node): node is Comment {\n  return node instanceof Comment && node.data.trim().startsWith('rmx:f:')\n}\n\nfunction disposeRemovedSubFrames(node: Node, context: FrameContext): void {\n  let stack: Node[] = [node]\n  while (stack.length > 0) {\n    let next = stack.pop()\n    if (!next) continue\n\n    if (isFrameStartMarker(next)) {\n      let subFrame = context.frameInstances.get(next)\n      if (subFrame) {\n        subFrame.dispose()\n        context.frameInstances.delete(next)\n      }\n    }\n\n    for (let child of Array.from(next.childNodes)) {\n      stack.push(child)\n    }\n  }\n}\n\nfunction isVirtualRootStartMarker(node: Node): node is Comment {\n  return isCommentNode(node) && node.data.trim().startsWith('rmx:h:')\n}\n\nfunction isVirtualRootEndMarker(node: Node): node is Comment {\n  return isCommentNode(node) && node.data.trim() === '/rmx:h'\n}\n"
  },
  {
    "path": "packages/component/src/lib/diff-props.ts",
    "content": "import { invariant } from './invariant.ts'\nimport { createStyleManager, normalizeCssValue } from './style/index.ts'\nimport type { StyleManager } from './style/index.ts'\nimport type { ElementProps } from './jsx.ts'\nimport { normalizeSvgAttribute } from './svg-attributes.ts'\n\nconst SVG_NS = 'http://www.w3.org/2000/svg'\n\nlet globalStyleManager =\n  typeof window !== 'undefined' ? createStyleManager() : (null as unknown as StyleManager)\n\nexport { type StyleManager }\n\nexport let defaultStyleManager: StyleManager = globalStyleManager\n\n// Preact excludes certain attributes from the property path due to browser quirks\nconst ATTRIBUTE_FALLBACK_NAMES = new Set([\n  'width',\n  'height',\n  'href',\n  'list',\n  'form',\n  'tabIndex',\n  'download',\n  'rowSpan',\n  'colSpan',\n  'role',\n  'popover',\n])\n\n// Determine if we should use the property path for a given name.\n// Also acts as a type guard to allow bracket assignment without casts.\nfunction canUseProperty(\n  dom: Element,\n  name: string,\n  isSvg: boolean,\n): dom is Element & Record<string, unknown> {\n  if (isSvg) return false\n  if (ATTRIBUTE_FALLBACK_NAMES.has(name)) return false\n  return name in dom\n}\n\nfunction isFrameworkProp(name: string): boolean {\n  return (\n    name === 'children' ||\n    name === 'mix' ||\n    name === 'key' ||\n    name === 'setup' ||\n    name === 'animate' ||\n    name === 'innerHTML'\n  )\n}\n\n// TODO: would rather actually diff el.style object directly instead of writing\n// to the style attribute\nfunction serializeStyleObject(style: Record<string, unknown>): string {\n  let parts: string[] = []\n  for (let [key, value] of Object.entries(style)) {\n    if (value == null) continue\n    if (typeof value === 'boolean') continue\n    if (typeof value === 'number' && !Number.isFinite(value)) continue\n\n    let cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)\n\n    let cssValue = Array.isArray(value)\n      ? (value as unknown[]).join(', ')\n      : normalizeCssValue(key, value)\n\n    parts.push(`${cssKey}: ${cssValue};`)\n  }\n  return parts.join(' ')\n}\n\nfunction normalizePropName(name: string, isSvg: boolean): { ns?: string; attr: string } {\n  // aria-/data- pass through\n  if (name.startsWith('aria-') || name.startsWith('data-')) return { attr: name }\n\n  // DOM property -> HTML mappings\n  if (name === 'className') return { attr: 'class' }\n  if (!isSvg) {\n    if (name === 'htmlFor') return { attr: 'for' }\n    if (name === 'tabIndex') return { attr: 'tabindex' }\n    if (name === 'acceptCharset') return { attr: 'accept-charset' }\n    if (name === 'httpEquiv') return { attr: 'http-equiv' }\n    return { attr: name.toLowerCase() }\n  }\n\n  return normalizeSvgAttribute(name)\n}\n\nfunction toLocalName(attrName: string): string {\n  let separatorIndex = attrName.indexOf(':')\n  if (separatorIndex === -1) return attrName\n  return attrName.slice(separatorIndex + 1)\n}\n\nfunction clearRuntimePropertyOnRemoval(dom: Element & Record<string, unknown>, name: string): void {\n  try {\n    if (name === 'value' || name === 'defaultValue') {\n      dom[name] = ''\n      return\n    }\n    if (name === 'checked' || name === 'defaultChecked' || name === 'selected') {\n      dom[name] = false\n      return\n    }\n    if (name === 'selectedIndex') {\n      dom[name] = -1\n    }\n  } catch {}\n}\n\nfunction getMergedClassName(props: ElementProps): string | undefined {\n  let classAttr = typeof props.class === 'string' ? props.class : ''\n  let className = typeof props.className === 'string' ? props.className : ''\n  let mergedClassName =\n    classAttr && className ? `${classAttr} ${className}` : classAttr || className\n  return mergedClassName || undefined\n}\n\nexport function diffHostProps(curr: ElementProps, next: ElementProps, dom: Element) {\n  let isSvg = dom.namespaceURI === SVG_NS\n  let currClassName = getMergedClassName(curr)\n  let nextClassName = getMergedClassName(next)\n\n  if (currClassName !== nextClassName) {\n    if (nextClassName) {\n      dom.setAttribute('class', nextClassName)\n    } else {\n      dom.removeAttribute('class')\n    }\n  }\n\n  // Removals\n  for (let name in curr) {\n    if (isFrameworkProp(name)) continue\n    if (name === 'class' || name === 'className') continue\n    if (!(name in next) || next[name] == null) {\n      // Clear runtime state for form-like props where removing the attribute is not enough.\n      if (canUseProperty(dom, name, isSvg)) {\n        clearRuntimePropertyOnRemoval(dom, name)\n      }\n\n      let { ns, attr } = normalizePropName(name, isSvg)\n      if (ns) dom.removeAttributeNS(ns, toLocalName(attr))\n      else dom.removeAttribute(attr)\n    }\n  }\n\n  // Additions/updates\n  for (let name in next) {\n    if (isFrameworkProp(name)) continue\n    if (name === 'class' || name === 'className') continue\n    let nextValue = next[name]\n    if (nextValue == null) continue\n    let prevValue = curr[name]\n    if (prevValue !== nextValue) {\n      let { ns, attr } = normalizePropName(name, isSvg)\n\n      // Object style: serialize to attribute for now\n      if (\n        attr === 'style' &&\n        typeof nextValue === 'object' &&\n        nextValue &&\n        !Array.isArray(nextValue)\n      ) {\n        dom.setAttribute('style', serializeStyleObject(nextValue))\n        continue\n      }\n\n      // Prefer property assignment when possible (HTML only, not SVG)\n      if (canUseProperty(dom, name, isSvg)) {\n        try {\n          dom[name] = nextValue == null ? '' : nextValue\n          continue\n        } catch {}\n      }\n\n      // Attribute path\n      if (typeof nextValue === 'function') {\n        // Never serialize functions as attribute values\n        continue\n      }\n\n      let isAriaOrData = name.startsWith('aria-') || name.startsWith('data-')\n      if (nextValue != null && (nextValue !== false || isAriaOrData)) {\n        // Special-case popover: true => presence only\n        let attrValue = name === 'popover' && nextValue === true ? '' : String(nextValue)\n        if (ns) dom.setAttributeNS(ns, attr, attrValue)\n        else dom.setAttribute(attr, attrValue)\n      } else {\n        if (ns) dom.removeAttributeNS(ns, toLocalName(attr))\n        else dom.removeAttribute(attr)\n      }\n    }\n  }\n}\n\n/**\n * Reset the global style state. For testing only - not exported from index.ts.\n */\nexport function resetStyleState() {\n  invariant(\n    typeof window !== 'undefined',\n    'resetStyleState() is only available in a browser environment',\n  )\n  globalStyleManager.dispose()\n  globalStyleManager = createStyleManager()\n  defaultStyleManager = globalStyleManager\n}\n"
  },
  {
    "path": "packages/component/src/lib/document-state.ts",
    "content": "/**\n * Adapted from https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/client/ReactInputSelection.js\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n * MIT License\n *\n * Eventually should be able to use moveBefore and won't need this at all.\n */\n\ntype SelectionInformation = {\n  focusedElem: Element | null\n  selectionRange: { start: number; end: number } | null\n}\n\nexport function createDocumentState(_doc?: Document) {\n  let doc = _doc ?? document\n\n  function getActiveElement(): Element | null {\n    return doc.activeElement || doc.body\n  }\n\n  function hasSelectionCapabilities(elem: Element): boolean {\n    let nodeName = elem.nodeName.toLowerCase()\n    return (\n      (nodeName === 'input' &&\n        'type' in elem &&\n        (elem.type === 'text' ||\n          elem.type === 'search' ||\n          elem.type === 'tel' ||\n          elem.type === 'url' ||\n          elem.type === 'password')) ||\n      nodeName === 'textarea' ||\n      (elem instanceof HTMLElement && elem.contentEditable === 'true')\n    )\n  }\n\n  function getSelection(input: Element): { start: number; end: number } | null {\n    if (\n      'selectionStart' in input &&\n      typeof input.selectionStart === 'number' &&\n      'selectionEnd' in input\n    ) {\n      let htmlInput = input as HTMLInputElement | HTMLTextAreaElement\n      return {\n        start: htmlInput.selectionStart ?? 0,\n        end: htmlInput.selectionEnd ?? htmlInput.selectionStart ?? 0,\n      }\n    }\n    // For contentEditable, we'd need more complex logic, but for now return null\n    return null\n  }\n\n  function setSelection(input: Element, offsets: { start: number; end: number }): void {\n    if ('selectionStart' in input && 'selectionEnd' in input) {\n      try {\n        let htmlInput = input as HTMLInputElement | HTMLTextAreaElement\n        htmlInput.selectionStart = offsets.start\n        htmlInput.selectionEnd = Math.min(offsets.end, htmlInput.value?.length ?? 0)\n      } catch {\n        // Ignore errors setting selection\n      }\n    }\n  }\n\n  function isInDocument(node: Node): boolean {\n    return doc.documentElement.contains(node)\n  }\n\n  function getSelectionInformation(): SelectionInformation {\n    let focusedElem = getActiveElement()\n    return {\n      focusedElem,\n      selectionRange:\n        focusedElem && hasSelectionCapabilities(focusedElem) ? getSelection(focusedElem) : null,\n    }\n  }\n\n  function restoreSelection(priorSelectionInformation: SelectionInformation): void {\n    let curFocusedElem = getActiveElement()\n    let priorFocusedElem = priorSelectionInformation.focusedElem\n    let priorSelectionRange = priorSelectionInformation.selectionRange\n\n    if (curFocusedElem !== priorFocusedElem && priorFocusedElem && isInDocument(priorFocusedElem)) {\n      // Save scroll positions before focusing (focusing can change scroll)\n      let ancestors: Array<{ element: Element; left: number; top: number }> = []\n      let ancestor: Node | null = priorFocusedElem\n      while (ancestor) {\n        if (ancestor.nodeType === Node.ELEMENT_NODE) {\n          let el = ancestor as Element\n          ancestors.push({\n            element: el,\n            left: el.scrollLeft ?? 0,\n            top: el.scrollTop ?? 0,\n          })\n        }\n        ancestor = ancestor.parentNode\n      }\n\n      // Restore selection if applicable\n      if (priorSelectionRange !== null && hasSelectionCapabilities(priorFocusedElem)) {\n        setSelection(priorFocusedElem, priorSelectionRange)\n      }\n\n      // Restore focus\n      if (priorFocusedElem instanceof HTMLElement && typeof priorFocusedElem.focus === 'function') {\n        priorFocusedElem.focus()\n      }\n\n      // Restore scroll positions\n      for (let info of ancestors) {\n        info.element.scrollLeft = info.left\n        info.element.scrollTop = info.top\n      }\n    }\n  }\n\n  let selectionInfo: SelectionInformation | null = null\n\n  function capture() {\n    selectionInfo = getSelectionInformation()\n  }\n\n  function restore() {\n    if (selectionInfo !== null) {\n      restoreSelection(selectionInfo)\n    }\n    selectionInfo = null\n  }\n\n  return { capture, restore }\n}\n"
  },
  {
    "path": "packages/component/src/lib/dom.ts",
    "content": "import type { StyleProps } from './style/lib/style.ts'\nimport type { RemixNode } from './jsx.ts'\nimport type { MixValue } from './mixin.ts'\n\n/**\n * Adapted from Preact:\n * - Source: https://github.com/preactjs/preact/blob/eee0c6ef834534498e433f0f7a3ef679efd24380/src/dom.d.ts\n * - License: MIT https://github.com/preactjs/preact/blob/eee0c6ef834534498e433f0f7a3ef679efd24380/LICENSE\n * - Copyright (c) 2015-present Jason Miller\n */\ntype Booleanish = boolean | 'true' | 'false'\n\n/**\n * Layout animation configuration for FLIP-based position animations.\n * All properties are optional - defaults are applied when `true` or `{}` is used.\n */\nexport interface LayoutAnimationConfig {\n  /** Animation duration in milliseconds (default: 200) */\n  duration?: number\n  /** CSS easing function (default: spring 'snappy' easing) */\n  easing?: string\n}\n\n/**\n * Shared host-element props accepted by all built-in DOM element types.\n */\nexport interface HostProps<eventTarget extends EventTarget> {\n  /** The reconciliation key for the element. */\n  key?: any\n  /** Child nodes to render inside the element. */\n  children?: RemixNode\n  /** Mixins to apply to the element. */\n  mix?: MixValue<eventTarget>\n  /**\n   * Set the innerHTML of the element directly.\n   * When provided, children are ignored.\n   * Use with caution as this can expose XSS vulnerabilities if the content is not sanitized.\n   */\n  innerHTML?: string\n}\n\n/**\n * Value wrapper used by host prop types that participate in tracked updates.\n */\nexport type Trackable<T> = T\n\n/**\n * Props accepted by SVG elements.\n */\nexport interface SVGProps<eventTarget extends EventTarget = SVGElement>\n  extends HTMLProps<eventTarget> {\n  /** The `accentHeight` SVG attribute. */\n  accentHeight?: Trackable<number | string | undefined>\n  /** The `accumulate` SVG attribute. */\n  accumulate?: Trackable<'none' | 'sum' | undefined>\n  /** The `additive` SVG attribute. */\n  additive?: Trackable<'replace' | 'sum' | undefined>\n  /** The `alignmentBaseline` SVG attribute. */\n  alignmentBaseline?: Trackable<\n    | 'auto'\n    | 'baseline'\n    | 'before-edge'\n    | 'text-before-edge'\n    | 'middle'\n    | 'central'\n    | 'after-edge'\n    | 'text-after-edge'\n    | 'ideographic'\n    | 'alphabetic'\n    | 'hanging'\n    | 'mathematical'\n    | 'inherit'\n    | undefined\n  >\n  /** The `alignment-baseline` SVG attribute. */\n  'alignment-baseline'?: Trackable<\n    | 'auto'\n    | 'baseline'\n    | 'before-edge'\n    | 'text-before-edge'\n    | 'middle'\n    | 'central'\n    | 'after-edge'\n    | 'text-after-edge'\n    | 'ideographic'\n    | 'alphabetic'\n    | 'hanging'\n    | 'mathematical'\n    | 'inherit'\n    | undefined\n  >\n  /** The `allowReorder` SVG attribute. */\n  allowReorder?: Trackable<'no' | 'yes' | undefined>\n  /** The `allow-reorder` SVG attribute. */\n  'allow-reorder'?: Trackable<'no' | 'yes' | undefined>\n  /** The `alphabetic` SVG attribute. */\n  alphabetic?: Trackable<number | string | undefined>\n  /** The `amplitude` SVG attribute. */\n  amplitude?: Trackable<number | string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/arabic-form */\n  arabicForm?: Trackable<'initial' | 'medial' | 'terminal' | 'isolated' | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/arabic-form */\n  'arabic-form'?: Trackable<'initial' | 'medial' | 'terminal' | 'isolated' | undefined>\n  /** The `ascent` SVG attribute. */\n  ascent?: Trackable<number | string | undefined>\n  /** The `attributeName` SVG attribute. */\n  attributeName?: Trackable<string | undefined>\n  /** The `attributeType` SVG attribute. */\n  attributeType?: Trackable<string | undefined>\n  /** The `azimuth` SVG attribute. */\n  azimuth?: Trackable<number | string | undefined>\n  /** The `baseFrequency` SVG attribute. */\n  baseFrequency?: Trackable<number | string | undefined>\n  /** The `baselineShift` SVG attribute. */\n  baselineShift?: Trackable<number | string | undefined>\n  /** The `baseline-shift` SVG attribute. */\n  'baseline-shift'?: Trackable<number | string | undefined>\n  /** The `baseProfile` SVG attribute. */\n  baseProfile?: Trackable<number | string | undefined>\n  /** The `bbox` SVG attribute. */\n  bbox?: Trackable<number | string | undefined>\n  /** The `begin` SVG attribute. */\n  begin?: Trackable<number | string | undefined>\n  /** The `bias` SVG attribute. */\n  bias?: Trackable<number | string | undefined>\n  /** The `by` SVG attribute. */\n  by?: Trackable<number | string | undefined>\n  /** The `calcMode` SVG attribute. */\n  calcMode?: Trackable<number | string | undefined>\n  /** The `capHeight` SVG attribute. */\n  capHeight?: Trackable<number | string | undefined>\n  /** The `cap-height` SVG attribute. */\n  'cap-height'?: Trackable<number | string | undefined>\n  /** The `clip` SVG attribute. */\n  clip?: Trackable<number | string | undefined>\n  /** The `clipPath` SVG attribute. */\n  clipPath?: Trackable<string | undefined>\n  /** The `clip-path` SVG attribute. */\n  'clip-path'?: Trackable<string | undefined>\n  /** The `clipPathUnits` SVG attribute. */\n  clipPathUnits?: Trackable<number | string | undefined>\n  /** The `clipRule` SVG attribute. */\n  clipRule?: Trackable<number | string | undefined>\n  /** The `clip-rule` SVG attribute. */\n  'clip-rule'?: Trackable<number | string | undefined>\n  /** The `colorInterpolation` SVG attribute. */\n  colorInterpolation?: Trackable<number | string | undefined>\n  /** The `color-interpolation` SVG attribute. */\n  'color-interpolation'?: Trackable<number | string | undefined>\n  /** The `colorInterpolationFilters` SVG attribute. */\n  colorInterpolationFilters?: Trackable<'auto' | 'sRGB' | 'linearRGB' | 'inherit' | undefined>\n  /** The `color-interpolation-filters` SVG attribute. */\n  'color-interpolation-filters'?: Trackable<'auto' | 'sRGB' | 'linearRGB' | 'inherit' | undefined>\n  /** The `colorProfile` SVG attribute. */\n  colorProfile?: Trackable<number | string | undefined>\n  /** The `color-profile` SVG attribute. */\n  'color-profile'?: Trackable<number | string | undefined>\n  /** The `colorRendering` SVG attribute. */\n  colorRendering?: Trackable<number | string | undefined>\n  /** The `color-rendering` SVG attribute. */\n  'color-rendering'?: Trackable<number | string | undefined>\n  /** The `contentScriptType` SVG attribute. */\n  contentScriptType?: Trackable<number | string | undefined>\n  /** The `content-script-type` SVG attribute. */\n  'content-script-type'?: Trackable<number | string | undefined>\n  /** The `contentStyleType` SVG attribute. */\n  contentStyleType?: Trackable<number | string | undefined>\n  /** The `content-style-type` SVG attribute. */\n  'content-style-type'?: Trackable<number | string | undefined>\n  /** The `cursor` SVG attribute. */\n  cursor?: Trackable<number | string | undefined>\n  /** The `cx` SVG attribute. */\n  cx?: Trackable<number | string | undefined>\n  /** The `cy` SVG attribute. */\n  cy?: Trackable<number | string | undefined>\n  /** The `d` SVG attribute. */\n  d?: Trackable<string | undefined>\n  /** The `decelerate` SVG attribute. */\n  decelerate?: Trackable<number | string | undefined>\n  /** The `descent` SVG attribute. */\n  descent?: Trackable<number | string | undefined>\n  /** The `diffuseConstant` SVG attribute. */\n  diffuseConstant?: Trackable<number | string | undefined>\n  /** The `direction` SVG attribute. */\n  direction?: Trackable<number | string | undefined>\n  /** The `display` SVG attribute. */\n  display?: Trackable<number | string | undefined>\n  /** The `divisor` SVG attribute. */\n  divisor?: Trackable<number | string | undefined>\n  /** The `dominantBaseline` SVG attribute. */\n  dominantBaseline?: Trackable<number | string | undefined>\n  /** The `dominant-baseline` SVG attribute. */\n  'dominant-baseline'?: Trackable<number | string | undefined>\n  /** The `dur` SVG attribute. */\n  dur?: Trackable<number | string | undefined>\n  /** The `dx` SVG attribute. */\n  dx?: Trackable<number | string | undefined>\n  /** The `dy` SVG attribute. */\n  dy?: Trackable<number | string | undefined>\n  /** The `edgeMode` SVG attribute. */\n  edgeMode?: Trackable<number | string | undefined>\n  /** The `elevation` SVG attribute. */\n  elevation?: Trackable<number | string | undefined>\n  /** The `enableBackground` SVG attribute. */\n  enableBackground?: Trackable<number | string | undefined>\n  /** The `enable-background` SVG attribute. */\n  'enable-background'?: Trackable<number | string | undefined>\n  /** The `end` SVG attribute. */\n  end?: Trackable<number | string | undefined>\n  /** The `exponent` SVG attribute. */\n  exponent?: Trackable<number | string | undefined>\n  /** The `externalResourcesRequired` SVG attribute. */\n  externalResourcesRequired?: Trackable<number | string | undefined>\n  /** The `fill` SVG attribute. */\n  fill?: Trackable<string | undefined>\n  /** The `fillOpacity` SVG attribute. */\n  fillOpacity?: Trackable<number | string | undefined>\n  /** The `fill-opacity` SVG attribute. */\n  'fill-opacity'?: Trackable<number | string | undefined>\n  /** The `fillRule` SVG attribute. */\n  fillRule?: Trackable<'nonzero' | 'evenodd' | 'inherit' | undefined>\n  /** The `fill-rule` SVG attribute. */\n  'fill-rule'?: Trackable<'nonzero' | 'evenodd' | 'inherit' | undefined>\n  /** The `filter` SVG attribute. */\n  filter?: Trackable<string | undefined>\n  /** The `filterRes` SVG attribute. */\n  filterRes?: Trackable<number | string | undefined>\n  /** The `filterUnits` SVG attribute. */\n  filterUnits?: Trackable<number | string | undefined>\n  /** The `floodColor` SVG attribute. */\n  floodColor?: Trackable<number | string | undefined>\n  /** The `flood-color` SVG attribute. */\n  'flood-color'?: Trackable<number | string | undefined>\n  /** The `floodOpacity` SVG attribute. */\n  floodOpacity?: Trackable<number | string | undefined>\n  /** The `flood-opacity` SVG attribute. */\n  'flood-opacity'?: Trackable<number | string | undefined>\n  /** The `focusable` SVG attribute. */\n  focusable?: Trackable<number | string | undefined>\n  /** The `fontFamily` SVG attribute. */\n  fontFamily?: Trackable<string | undefined>\n  /** The `font-family` SVG attribute. */\n  'font-family'?: Trackable<string | undefined>\n  /** The `fontSize` SVG attribute. */\n  fontSize?: Trackable<number | string | undefined>\n  /** The `font-size` SVG attribute. */\n  'font-size'?: Trackable<number | string | undefined>\n  /** The `fontSizeAdjust` SVG attribute. */\n  fontSizeAdjust?: Trackable<number | string | undefined>\n  /** The `font-size-adjust` SVG attribute. */\n  'font-size-adjust'?: Trackable<number | string | undefined>\n  /** The `fontStretch` SVG attribute. */\n  fontStretch?: Trackable<number | string | undefined>\n  /** The `font-stretch` SVG attribute. */\n  'font-stretch'?: Trackable<number | string | undefined>\n  /** The `fontStyle` SVG attribute. */\n  fontStyle?: Trackable<number | string | undefined>\n  /** The `font-style` SVG attribute. */\n  'font-style'?: Trackable<number | string | undefined>\n  /** The `fontVariant` SVG attribute. */\n  fontVariant?: Trackable<number | string | undefined>\n  /** The `font-variant` SVG attribute. */\n  'font-variant'?: Trackable<number | string | undefined>\n  /** The `fontWeight` SVG attribute. */\n  fontWeight?: Trackable<number | string | undefined>\n  /** The `font-weight` SVG attribute. */\n  'font-weight'?: Trackable<number | string | undefined>\n  /** The `format` SVG attribute. */\n  format?: Trackable<number | string | undefined>\n  /** The `from` SVG attribute. */\n  from?: Trackable<number | string | undefined>\n  /** The `fx` SVG attribute. */\n  fx?: Trackable<number | string | undefined>\n  /** The `fy` SVG attribute. */\n  fy?: Trackable<number | string | undefined>\n  /** The `g1` SVG attribute. */\n  g1?: Trackable<number | string | undefined>\n  /** The `g2` SVG attribute. */\n  g2?: Trackable<number | string | undefined>\n  /** The `glyphName` SVG attribute. */\n  glyphName?: Trackable<number | string | undefined>\n  /** The `glyph-name` SVG attribute. */\n  'glyph-name'?: Trackable<number | string | undefined>\n  /** The `glyphOrientationHorizontal` SVG attribute. */\n  glyphOrientationHorizontal?: Trackable<number | string | undefined>\n  /** The `glyph-orientation-horizontal` SVG attribute. */\n  'glyph-orientation-horizontal'?: Trackable<number | string | undefined>\n  /** The `glyphOrientationVertical` SVG attribute. */\n  glyphOrientationVertical?: Trackable<number | string | undefined>\n  /** The `glyph-orientation-vertical` SVG attribute. */\n  'glyph-orientation-vertical'?: Trackable<number | string | undefined>\n  /** The `glyphRef` SVG attribute. */\n  glyphRef?: Trackable<number | string | undefined>\n  /** The `gradientTransform` SVG attribute. */\n  gradientTransform?: Trackable<string | undefined>\n  /** The `gradientUnits` SVG attribute. */\n  gradientUnits?: Trackable<string | undefined>\n  /** The `hanging` SVG attribute. */\n  hanging?: Trackable<number | string | undefined>\n  /** The `height` SVG attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `horizAdvX` SVG attribute. */\n  horizAdvX?: Trackable<number | string | undefined>\n  /** The `horiz-adv-x` SVG attribute. */\n  'horiz-adv-x'?: Trackable<number | string | undefined>\n  /** The `horizOriginX` SVG attribute. */\n  horizOriginX?: Trackable<number | string | undefined>\n  /** The `horiz-origin-x` SVG attribute. */\n  'horiz-origin-x'?: Trackable<number | string | undefined>\n  /** The `href` SVG attribute. */\n  href?: Trackable<string | undefined>\n  /** The `hreflang` SVG attribute. */\n  hreflang?: Trackable<string | undefined>\n  /** The `hrefLang` SVG attribute. */\n  hrefLang?: Trackable<string | undefined>\n  /** The `ideographic` SVG attribute. */\n  ideographic?: Trackable<number | string | undefined>\n  /** The `imageRendering` SVG attribute. */\n  imageRendering?: Trackable<number | string | undefined>\n  /** The `image-rendering` SVG attribute. */\n  'image-rendering'?: Trackable<number | string | undefined>\n  /** The `in2` SVG attribute. */\n  in2?: Trackable<number | string | undefined>\n  /** The `in` SVG attribute. */\n  in?: Trackable<string | undefined>\n  /** The `intercept` SVG attribute. */\n  intercept?: Trackable<number | string | undefined>\n  /** The `k1` SVG attribute. */\n  k1?: Trackable<number | string | undefined>\n  /** The `k2` SVG attribute. */\n  k2?: Trackable<number | string | undefined>\n  /** The `k3` SVG attribute. */\n  k3?: Trackable<number | string | undefined>\n  /** The `k4` SVG attribute. */\n  k4?: Trackable<number | string | undefined>\n  /** The `k` SVG attribute. */\n  k?: Trackable<number | string | undefined>\n  /** The `kernelMatrix` SVG attribute. */\n  kernelMatrix?: Trackable<number | string | undefined>\n  /** The `kernelUnitLength` SVG attribute. */\n  kernelUnitLength?: Trackable<number | string | undefined>\n  /** The `kerning` SVG attribute. */\n  kerning?: Trackable<number | string | undefined>\n  /** The `keyPoints` SVG attribute. */\n  keyPoints?: Trackable<number | string | undefined>\n  /** The `keySplines` SVG attribute. */\n  keySplines?: Trackable<number | string | undefined>\n  /** The `keyTimes` SVG attribute. */\n  keyTimes?: Trackable<number | string | undefined>\n  /** The `lengthAdjust` SVG attribute. */\n  lengthAdjust?: Trackable<number | string | undefined>\n  /** The `letterSpacing` SVG attribute. */\n  letterSpacing?: Trackable<number | string | undefined>\n  /** The `letter-spacing` SVG attribute. */\n  'letter-spacing'?: Trackable<number | string | undefined>\n  /** The `lightingColor` SVG attribute. */\n  lightingColor?: Trackable<number | string | undefined>\n  /** The `lighting-color` SVG attribute. */\n  'lighting-color'?: Trackable<number | string | undefined>\n  /** The `limitingConeAngle` SVG attribute. */\n  limitingConeAngle?: Trackable<number | string | undefined>\n  /** The `local` SVG attribute. */\n  local?: Trackable<number | string | undefined>\n  /** The `markerEnd` SVG attribute. */\n  markerEnd?: Trackable<string | undefined>\n  /** The `marker-end` SVG attribute. */\n  'marker-end'?: Trackable<string | undefined>\n  /** The `markerHeight` SVG attribute. */\n  markerHeight?: Trackable<number | string | undefined>\n  /** The `markerMid` SVG attribute. */\n  markerMid?: Trackable<string | undefined>\n  /** The `marker-mid` SVG attribute. */\n  'marker-mid'?: Trackable<string | undefined>\n  /** The `markerStart` SVG attribute. */\n  markerStart?: Trackable<string | undefined>\n  /** The `marker-start` SVG attribute. */\n  'marker-start'?: Trackable<string | undefined>\n  /** The `markerUnits` SVG attribute. */\n  markerUnits?: Trackable<number | string | undefined>\n  /** The `markerWidth` SVG attribute. */\n  markerWidth?: Trackable<number | string | undefined>\n  /** The `mask` SVG attribute. */\n  mask?: Trackable<string | undefined>\n  /** The `maskContentUnits` SVG attribute. */\n  maskContentUnits?: Trackable<number | string | undefined>\n  /** The `maskUnits` SVG attribute. */\n  maskUnits?: Trackable<number | string | undefined>\n  /** The `mathematical` SVG attribute. */\n  mathematical?: Trackable<number | string | undefined>\n  /** The `mode` SVG attribute. */\n  mode?: Trackable<number | string | undefined>\n  /** The `numOctaves` SVG attribute. */\n  numOctaves?: Trackable<number | string | undefined>\n  /** The `offset` SVG attribute. */\n  offset?: Trackable<number | string | undefined>\n  /** The `opacity` SVG attribute. */\n  opacity?: Trackable<number | string | undefined>\n  /** The `operator` SVG attribute. */\n  operator?: Trackable<number | string | undefined>\n  /** The `order` SVG attribute. */\n  order?: Trackable<number | string | undefined>\n  /** The `orient` SVG attribute. */\n  orient?: Trackable<number | string | undefined>\n  /** The `orientation` SVG attribute. */\n  orientation?: Trackable<number | string | undefined>\n  /** The `origin` SVG attribute. */\n  origin?: Trackable<number | string | undefined>\n  /** The `overflow` SVG attribute. */\n  overflow?: Trackable<number | string | undefined>\n  /** The `overlinePosition` SVG attribute. */\n  overlinePosition?: Trackable<number | string | undefined>\n  /** The `overline-position` SVG attribute. */\n  'overline-position'?: Trackable<number | string | undefined>\n  /** The `overlineThickness` SVG attribute. */\n  overlineThickness?: Trackable<number | string | undefined>\n  /** The `overline-thickness` SVG attribute. */\n  'overline-thickness'?: Trackable<number | string | undefined>\n  /** The `paintOrder` SVG attribute. */\n  paintOrder?: Trackable<number | string | undefined>\n  /** The `paint-order` SVG attribute. */\n  'paint-order'?: Trackable<number | string | undefined>\n  /** The `panose1` SVG attribute. */\n  panose1?: Trackable<number | string | undefined>\n  /** The `panose-1` SVG attribute. */\n  'panose-1'?: Trackable<number | string | undefined>\n  /** The `pathLength` SVG attribute. */\n  pathLength?: Trackable<number | string | undefined>\n  /** The `patternContentUnits` SVG attribute. */\n  patternContentUnits?: Trackable<string | undefined>\n  /** The `patternTransform` SVG attribute. */\n  patternTransform?: Trackable<number | string | undefined>\n  /** The `patternUnits` SVG attribute. */\n  patternUnits?: Trackable<string | undefined>\n  /** The `pointerEvents` SVG attribute. */\n  pointerEvents?: Trackable<number | string | undefined>\n  /** The `pointer-events` SVG attribute. */\n  'pointer-events'?: Trackable<number | string | undefined>\n  /** The `points` SVG attribute. */\n  points?: Trackable<string | undefined>\n  /** The `pointsAtX` SVG attribute. */\n  pointsAtX?: Trackable<number | string | undefined>\n  /** The `pointsAtY` SVG attribute. */\n  pointsAtY?: Trackable<number | string | undefined>\n  /** The `pointsAtZ` SVG attribute. */\n  pointsAtZ?: Trackable<number | string | undefined>\n  /** The `preserveAlpha` SVG attribute. */\n  preserveAlpha?: Trackable<number | string | undefined>\n  /** The `preserveAspectRatio` SVG attribute. */\n  preserveAspectRatio?: Trackable<string | undefined>\n  /** The `primitiveUnits` SVG attribute. */\n  primitiveUnits?: Trackable<number | string | undefined>\n  /** The `r` SVG attribute. */\n  r?: Trackable<number | string | undefined>\n  /** The `radius` SVG attribute. */\n  radius?: Trackable<number | string | undefined>\n  /** The `refX` SVG attribute. */\n  refX?: Trackable<number | string | undefined>\n  /** The `refY` SVG attribute. */\n  refY?: Trackable<number | string | undefined>\n  /** The `renderingIntent` SVG attribute. */\n  renderingIntent?: Trackable<number | string | undefined>\n  /** The `rendering-intent` SVG attribute. */\n  'rendering-intent'?: Trackable<number | string | undefined>\n  /** The `repeatCount` SVG attribute. */\n  repeatCount?: Trackable<number | string | undefined>\n  /** The `repeat-count` SVG attribute. */\n  'repeat-count'?: Trackable<number | string | undefined>\n  /** The `repeatDur` SVG attribute. */\n  repeatDur?: Trackable<number | string | undefined>\n  /** The `repeat-dur` SVG attribute. */\n  'repeat-dur'?: Trackable<number | string | undefined>\n  /** The `requiredExtensions` SVG attribute. */\n  requiredExtensions?: Trackable<number | string | undefined>\n  /** The `requiredFeatures` SVG attribute. */\n  requiredFeatures?: Trackable<number | string | undefined>\n  /** The `restart` SVG attribute. */\n  restart?: Trackable<number | string | undefined>\n  /** The `result` SVG attribute. */\n  result?: Trackable<string | undefined>\n  /** The `rotate` SVG attribute. */\n  rotate?: Trackable<number | string | undefined>\n  /** The `rx` SVG attribute. */\n  rx?: Trackable<number | string | undefined>\n  /** The `ry` SVG attribute. */\n  ry?: Trackable<number | string | undefined>\n  /** The `scale` SVG attribute. */\n  scale?: Trackable<number | string | undefined>\n  /** The `seed` SVG attribute. */\n  seed?: Trackable<number | string | undefined>\n  /** The `shapeRendering` SVG attribute. */\n  shapeRendering?: Trackable<number | string | undefined>\n  /** The `shape-rendering` SVG attribute. */\n  'shape-rendering'?: Trackable<number | string | undefined>\n  /** The `slope` SVG attribute. */\n  slope?: Trackable<number | string | undefined>\n  /** The `spacing` SVG attribute. */\n  spacing?: Trackable<number | string | undefined>\n  /** The `specularConstant` SVG attribute. */\n  specularConstant?: Trackable<number | string | undefined>\n  /** The `specularExponent` SVG attribute. */\n  specularExponent?: Trackable<number | string | undefined>\n  /** The `speed` SVG attribute. */\n  speed?: Trackable<number | string | undefined>\n  /** The `spreadMethod` SVG attribute. */\n  spreadMethod?: Trackable<string | undefined>\n  /** The `startOffset` SVG attribute. */\n  startOffset?: Trackable<number | string | undefined>\n  /** The `stdDeviation` SVG attribute. */\n  stdDeviation?: Trackable<number | string | undefined>\n  /** The `stemh` SVG attribute. */\n  stemh?: Trackable<number | string | undefined>\n  /** The `stemv` SVG attribute. */\n  stemv?: Trackable<number | string | undefined>\n  /** The `stitchTiles` SVG attribute. */\n  stitchTiles?: Trackable<number | string | undefined>\n  /** The `stopColor` SVG attribute. */\n  stopColor?: Trackable<string | undefined>\n  /** The `stop-color` SVG attribute. */\n  'stop-color'?: Trackable<string | undefined>\n  /** The `stopOpacity` SVG attribute. */\n  stopOpacity?: Trackable<number | string | undefined>\n  /** The `stop-opacity` SVG attribute. */\n  'stop-opacity'?: Trackable<number | string | undefined>\n  /** The `strikethroughPosition` SVG attribute. */\n  strikethroughPosition?: Trackable<number | string | undefined>\n  /** The `strikethrough-position` SVG attribute. */\n  'strikethrough-position'?: Trackable<number | string | undefined>\n  /** The `strikethroughThickness` SVG attribute. */\n  strikethroughThickness?: Trackable<number | string | undefined>\n  /** The `strikethrough-thickness` SVG attribute. */\n  'strikethrough-thickness'?: Trackable<number | string | undefined>\n  /** The `string` SVG attribute. */\n  string?: Trackable<number | string | undefined>\n  /** The `stroke` SVG attribute. */\n  stroke?: Trackable<string | undefined>\n  /** The `strokeDasharray` SVG attribute. */\n  strokeDasharray?: Trackable<string | number | undefined>\n  /** The `stroke-dasharray` SVG attribute. */\n  'stroke-dasharray'?: Trackable<string | number | undefined>\n  /** The `strokeDashoffset` SVG attribute. */\n  strokeDashoffset?: Trackable<string | number | undefined>\n  /** The `stroke-dashoffset` SVG attribute. */\n  'stroke-dashoffset'?: Trackable<string | number | undefined>\n  /** The `strokeLinecap` SVG attribute. */\n  strokeLinecap?: Trackable<'butt' | 'round' | 'square' | 'inherit' | undefined>\n  /** The `stroke-linecap` SVG attribute. */\n  'stroke-linecap'?: Trackable<'butt' | 'round' | 'square' | 'inherit' | undefined>\n  /** The `strokeLinejoin` SVG attribute. */\n  strokeLinejoin?: Trackable<'miter' | 'round' | 'bevel' | 'inherit' | undefined>\n  /** The `stroke-linejoin` SVG attribute. */\n  'stroke-linejoin'?: Trackable<'miter' | 'round' | 'bevel' | 'inherit' | undefined>\n  /** The `strokeMiterlimit` SVG attribute. */\n  strokeMiterlimit?: Trackable<string | number | undefined>\n  /** The `stroke-miterlimit` SVG attribute. */\n  'stroke-miterlimit'?: Trackable<string | number | undefined>\n  /** The `strokeOpacity` SVG attribute. */\n  strokeOpacity?: Trackable<number | string | undefined>\n  /** The `stroke-opacity` SVG attribute. */\n  'stroke-opacity'?: Trackable<number | string | undefined>\n  /** The `strokeWidth` SVG attribute. */\n  strokeWidth?: Trackable<number | string | undefined>\n  /** The `stroke-width` SVG attribute. */\n  'stroke-width'?: Trackable<number | string | undefined>\n  /** The `surfaceScale` SVG attribute. */\n  surfaceScale?: Trackable<number | string | undefined>\n  /** The `systemLanguage` SVG attribute. */\n  systemLanguage?: Trackable<number | string | undefined>\n  /** The `tableValues` SVG attribute. */\n  tableValues?: Trackable<number | string | undefined>\n  /** The `targetX` SVG attribute. */\n  targetX?: Trackable<number | string | undefined>\n  /** The `targetY` SVG attribute. */\n  targetY?: Trackable<number | string | undefined>\n  /** The `textAnchor` SVG attribute. */\n  textAnchor?: Trackable<string | undefined>\n  /** The `text-anchor` SVG attribute. */\n  'text-anchor'?: Trackable<string | undefined>\n  /** The `textDecoration` SVG attribute. */\n  textDecoration?: Trackable<number | string | undefined>\n  /** The `text-decoration` SVG attribute. */\n  'text-decoration'?: Trackable<number | string | undefined>\n  /** The `textLength` SVG attribute. */\n  textLength?: Trackable<number | string | undefined>\n  /** The `textRendering` SVG attribute. */\n  textRendering?: Trackable<number | string | undefined>\n  /** The `text-rendering` SVG attribute. */\n  'text-rendering'?: Trackable<number | string | undefined>\n  /** The `to` SVG attribute. */\n  to?: Trackable<number | string | undefined>\n  /** The `transform` SVG attribute. */\n  transform?: Trackable<string | undefined>\n  /** The `transformOrigin` SVG attribute. */\n  transformOrigin?: Trackable<string | undefined>\n  /** The `transform-origin` SVG attribute. */\n  'transform-origin'?: Trackable<string | undefined>\n  /** The `type` SVG attribute. */\n  type?: Trackable<string | undefined>\n  /** The `u1` SVG attribute. */\n  u1?: Trackable<number | string | undefined>\n  /** The `u2` SVG attribute. */\n  u2?: Trackable<number | string | undefined>\n  /** The `underlinePosition` SVG attribute. */\n  underlinePosition?: Trackable<number | string | undefined>\n  /** The `underline-position` SVG attribute. */\n  'underline-position'?: Trackable<number | string | undefined>\n  /** The `underlineThickness` SVG attribute. */\n  underlineThickness?: Trackable<number | string | undefined>\n  /** The `underline-thickness` SVG attribute. */\n  'underline-thickness'?: Trackable<number | string | undefined>\n  /** The `unicode` SVG attribute. */\n  unicode?: Trackable<number | string | undefined>\n  /** The `unicodeBidi` SVG attribute. */\n  unicodeBidi?: Trackable<number | string | undefined>\n  /** The `unicode-bidi` SVG attribute. */\n  'unicode-bidi'?: Trackable<number | string | undefined>\n  /** The `unicodeRange` SVG attribute. */\n  unicodeRange?: Trackable<number | string | undefined>\n  /** The `unicode-range` SVG attribute. */\n  'unicode-range'?: Trackable<number | string | undefined>\n  /** The `unitsPerEm` SVG attribute. */\n  unitsPerEm?: Trackable<number | string | undefined>\n  /** The `units-per-em` SVG attribute. */\n  'units-per-em'?: Trackable<number | string | undefined>\n  /** The `vAlphabetic` SVG attribute. */\n  vAlphabetic?: Trackable<number | string | undefined>\n  /** The `v-alphabetic` SVG attribute. */\n  'v-alphabetic'?: Trackable<number | string | undefined>\n  /** The `values` SVG attribute. */\n  values?: Trackable<string | undefined>\n  /** The `vectorEffect` SVG attribute. */\n  vectorEffect?: Trackable<number | string | undefined>\n  /** The `vector-effect` SVG attribute. */\n  'vector-effect'?: Trackable<number | string | undefined>\n  /** The `version` SVG attribute. */\n  version?: Trackable<string | undefined>\n  /** The `vertAdvY` SVG attribute. */\n  vertAdvY?: Trackable<number | string | undefined>\n  /** The `vert-adv-y` SVG attribute. */\n  'vert-adv-y'?: Trackable<number | string | undefined>\n  /** The `vertOriginX` SVG attribute. */\n  vertOriginX?: Trackable<number | string | undefined>\n  /** The `vert-origin-x` SVG attribute. */\n  'vert-origin-x'?: Trackable<number | string | undefined>\n  /** The `vertOriginY` SVG attribute. */\n  vertOriginY?: Trackable<number | string | undefined>\n  /** The `vert-origin-y` SVG attribute. */\n  'vert-origin-y'?: Trackable<number | string | undefined>\n  /** The `vHanging` SVG attribute. */\n  vHanging?: Trackable<number | string | undefined>\n  /** The `v-hanging` SVG attribute. */\n  'v-hanging'?: Trackable<number | string | undefined>\n  /** The `vIdeographic` SVG attribute. */\n  vIdeographic?: Trackable<number | string | undefined>\n  /** The `v-ideographic` SVG attribute. */\n  'v-ideographic'?: Trackable<number | string | undefined>\n  /** The `viewBox` SVG attribute. */\n  viewBox?: Trackable<string | undefined>\n  /** The `viewTarget` SVG attribute. */\n  viewTarget?: Trackable<number | string | undefined>\n  /** The `visibility` SVG attribute. */\n  visibility?: Trackable<number | string | undefined>\n  /** The `vMathematical` SVG attribute. */\n  vMathematical?: Trackable<number | string | undefined>\n  /** The `v-mathematical` SVG attribute. */\n  'v-mathematical'?: Trackable<number | string | undefined>\n  /** The `width` SVG attribute. */\n  width?: Trackable<number | string | undefined>\n  /** The `wordSpacing` SVG attribute. */\n  wordSpacing?: Trackable<number | string | undefined>\n  /** The `word-spacing` SVG attribute. */\n  'word-spacing'?: Trackable<number | string | undefined>\n  /** The `writingMode` SVG attribute. */\n  writingMode?: Trackable<number | string | undefined>\n  /** The `writing-mode` SVG attribute. */\n  'writing-mode'?: Trackable<number | string | undefined>\n  /** The `x1` SVG attribute. */\n  x1?: Trackable<number | string | undefined>\n  /** The `x2` SVG attribute. */\n  x2?: Trackable<number | string | undefined>\n  /** The `x` SVG attribute. */\n  x?: Trackable<number | string | undefined>\n  /** The `xChannelSelector` SVG attribute. */\n  xChannelSelector?: Trackable<string | undefined>\n  /** The `xHeight` SVG attribute. */\n  xHeight?: Trackable<number | string | undefined>\n  /** The `x-height` SVG attribute. */\n  'x-height'?: Trackable<number | string | undefined>\n  /** The `xlinkActuate` SVG attribute. */\n  xlinkActuate?: Trackable<string | undefined>\n  /** The `xlink:actuate` SVG attribute. */\n  'xlink:actuate'?: Trackable<SVGProps['xlinkActuate']>\n  /** The `xlinkArcrole` SVG attribute. */\n  xlinkArcrole?: Trackable<string | undefined>\n  /** The `xlink:arcrole` SVG attribute. */\n  'xlink:arcrole'?: Trackable<string | undefined>\n  /** The `xlinkHref` SVG attribute. */\n  xlinkHref?: Trackable<string | undefined>\n  /** The `xlink:href` SVG attribute. */\n  'xlink:href'?: Trackable<string | undefined>\n  /** The `xlinkRole` SVG attribute. */\n  xlinkRole?: Trackable<string | undefined>\n  /** The `xlink:role` SVG attribute. */\n  'xlink:role'?: Trackable<string | undefined>\n  /** The `xlinkShow` SVG attribute. */\n  xlinkShow?: Trackable<string | undefined>\n  /** The `xlink:show` SVG attribute. */\n  'xlink:show'?: Trackable<string | undefined>\n  /** The `xlinkTitle` SVG attribute. */\n  xlinkTitle?: Trackable<string | undefined>\n  /** The `xlink:title` SVG attribute. */\n  'xlink:title'?: Trackable<string | undefined>\n  /** The `xlinkType` SVG attribute. */\n  xlinkType?: Trackable<string | undefined>\n  /** The `xlink:type` SVG attribute. */\n  'xlink:type'?: Trackable<string | undefined>\n  /** The `xmlBase` SVG attribute. */\n  xmlBase?: Trackable<string | undefined>\n  /** The `xml:base` SVG attribute. */\n  'xml:base'?: Trackable<string | undefined>\n  /** The `xmlLang` SVG attribute. */\n  xmlLang?: Trackable<string | undefined>\n  /** The `xml:lang` SVG attribute. */\n  'xml:lang'?: Trackable<string | undefined>\n  /** The `xmlns` SVG attribute. */\n  xmlns?: Trackable<string | undefined>\n  /** The `xmlnsXlink` SVG attribute. */\n  xmlnsXlink?: Trackable<string | undefined>\n  /** The `xmlSpace` SVG attribute. */\n  xmlSpace?: Trackable<string | undefined>\n  /** The `xml:space` SVG attribute. */\n  'xml:space'?: Trackable<string | undefined>\n  /** The `y1` SVG attribute. */\n  y1?: Trackable<number | string | undefined>\n  /** The `y2` SVG attribute. */\n  y2?: Trackable<number | string | undefined>\n  /** The `y` SVG attribute. */\n  y?: Trackable<number | string | undefined>\n  /** The `yChannelSelector` SVG attribute. */\n  yChannelSelector?: Trackable<string | undefined>\n  /** The `z` SVG attribute. */\n  z?: Trackable<number | string | undefined>\n  /** The `zoomAndPan` SVG attribute. */\n  zoomAndPan?: Trackable<string | undefined>\n}\n\n/**\n * Minimal props for SVG path data.\n */\nexport interface PathProps {\n  /** The SVG path data string. */\n  d: string\n}\n\n// All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/\n/**\n * WAI-ARIA attributes accepted by host elements.\n */\nexport interface AriaProps {\n  /** Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. */\n  'aria-activedescendant'?: Trackable<string | undefined>\n  /** Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute. */\n  'aria-atomic'?: Trackable<Booleanish | undefined>\n  /**\n   * Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be\n   * presented if they are made.\n   */\n  'aria-autocomplete'?: Trackable<'none' | 'inline' | 'list' | 'both' | undefined>\n  /**\n   * Defines a string value that labels the current element, which is intended to be converted into Braille.\n   * @see aria-label.\n   */\n  'aria-braillelabel'?: Trackable<string | undefined>\n  /**\n   * Defines a human-readable, author-localized abbreviated description for the role of an element, which is intended to be converted into Braille.\n   * @see aria-roledescription.\n   */\n  'aria-brailleroledescription'?: Trackable<string | undefined>\n  /** Indicates an element is being modified and that assistive technologies MAY want to wait until the modifications are complete before exposing them to the user. */\n  'aria-busy'?: Trackable<Booleanish | undefined>\n  /**\n   * Indicates the current \"checked\" state of checkboxes, radio buttons, and other widgets.\n   * @see aria-pressed\n   * @see aria-selected.\n   */\n  'aria-checked'?: Trackable<Booleanish | 'mixed' | undefined>\n  /**\n   * Defines the total number of columns in a table, grid, or treegrid.\n   * @see aria-colindex.\n   */\n  'aria-colcount'?: Trackable<number | undefined>\n  /**\n   * Defines an element's column index or position with respect to the total number of columns within a table, grid, or treegrid.\n   * @see aria-colcount\n   * @see aria-colspan.\n   */\n  'aria-colindex'?: Trackable<number | undefined>\n  /**\n   * Defines a human readable text alternative of aria-colindex.\n   * @see aria-rowindextext.\n   */\n  'aria-colindextext'?: Trackable<string | undefined>\n  /**\n   * Defines the number of columns spanned by a cell or gridcell within a table, grid, or treegrid.\n   * @see aria-colindex\n   * @see aria-rowspan.\n   */\n  'aria-colspan'?: Trackable<number | undefined>\n  /**\n   * Identifies the element (or elements) whose contents or presence are controlled by the current element.\n   * @see aria-owns.\n   */\n  'aria-controls'?: Trackable<string | undefined>\n  /** Indicates the element that represents the current item within a container or set of related elements. */\n  'aria-current'?: Trackable<\n    Booleanish | 'page' | 'step' | 'location' | 'date' | 'time' | undefined\n  >\n  /**\n   * Identifies the element (or elements) that describes the object.\n   * @see aria-labelledby\n   */\n  'aria-describedby'?: Trackable<string | undefined>\n  /**\n   * Defines a string value that describes or annotates the current element.\n   * @see related aria-describedby.\n   */\n  'aria-description'?: Trackable<string | undefined>\n  /**\n   * Identifies the element that provides a detailed, extended description for the object.\n   * @see aria-describedby.\n   */\n  'aria-details'?: Trackable<string | undefined>\n  /**\n   * Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable.\n   * @see aria-hidden\n   * @see aria-readonly.\n   */\n  'aria-disabled'?: Trackable<Booleanish | undefined>\n  /**\n   * Indicates what functions can be performed when a dragged object is released on the drop target.\n   * @deprecated in ARIA 1.1\n   */\n  'aria-dropeffect'?: Trackable<'none' | 'copy' | 'execute' | 'link' | 'move' | 'popup' | undefined>\n  /**\n   * Identifies the element that provides an error message for the object.\n   * @see aria-invalid\n   * @see aria-describedby.\n   */\n  'aria-errormessage'?: Trackable<string | undefined>\n  /** Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. */\n  'aria-expanded'?: Trackable<Booleanish | undefined>\n  /**\n   * Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion,\n   * allows assistive technology to override the general default of reading in document source order.\n   */\n  'aria-flowto'?: Trackable<string | undefined>\n  /**\n   * Indicates an element's \"grabbed\" state in a drag-and-drop operation.\n   * @deprecated in ARIA 1.1\n   */\n  'aria-grabbed'?: Trackable<Booleanish | undefined>\n  /** Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. */\n  'aria-haspopup'?: Trackable<\n    Booleanish | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | undefined\n  >\n  /**\n   * Indicates whether the element is exposed to an accessibility API.\n   * @see aria-disabled.\n   */\n  'aria-hidden'?: Trackable<Booleanish | undefined>\n  /**\n   * Indicates the entered value does not conform to the format expected by the application.\n   * @see aria-errormessage.\n   */\n  'aria-invalid'?: Trackable<Booleanish | 'grammar' | 'spelling' | undefined>\n  /** Indicates keyboard shortcuts that an author has implemented to activate or give focus to an element. */\n  'aria-keyshortcuts'?: Trackable<string | undefined>\n  /**\n   * Defines a string value that labels the current element.\n   * @see aria-labelledby.\n   */\n  'aria-label'?: Trackable<string | undefined>\n  /**\n   * Identifies the element (or elements) that labels the current element.\n   * @see aria-describedby.\n   */\n  'aria-labelledby'?: Trackable<string | undefined>\n  /** Defines the hierarchical level of an element within a structure. */\n  'aria-level'?: Trackable<number | undefined>\n  /** Indicates that an element will be updated, and describes the types of updates the user agents, assistive technologies, and user can expect from the live region. */\n  'aria-live'?: Trackable<'off' | 'assertive' | 'polite' | undefined>\n  /** Indicates whether an element is modal when displayed. */\n  'aria-modal'?: Trackable<Booleanish | undefined>\n  /** Indicates whether a text box accepts multiple lines of input or only a single line. */\n  'aria-multiline'?: Trackable<Booleanish | undefined>\n  /** Indicates that the user may select more than one item from the current selectable descendants. */\n  'aria-multiselectable'?: Trackable<Booleanish | undefined>\n  /** Indicates whether the element's orientation is horizontal, vertical, or unknown/ambiguous. */\n  'aria-orientation'?: Trackable<'horizontal' | 'vertical' | undefined>\n  /**\n   * Identifies an element (or elements) in order to define a visual, functional, or contextual parent/child relationship\n   * between DOM elements where the DOM hierarchy cannot be used to represent the relationship.\n   * @see aria-controls.\n   */\n  'aria-owns'?: Trackable<string | undefined>\n  /**\n   * Defines a short hint (a word or short phrase) intended to aid the user with data entry when the control has no value.\n   * A hint could be a sample value or a brief description of the expected format.\n   */\n  'aria-placeholder'?: Trackable<string | undefined>\n  /**\n   * Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.\n   * @see aria-setsize.\n   */\n  'aria-posinset'?: Trackable<number | undefined>\n  /**\n   * Indicates the current \"pressed\" state of toggle buttons.\n   * @see aria-checked\n   * @see aria-selected.\n   */\n  'aria-pressed'?: Trackable<Booleanish | 'mixed' | undefined>\n  /**\n   * Indicates that the element is not editable, but is otherwise operable.\n   * @see aria-disabled.\n   */\n  'aria-readonly'?: Trackable<Booleanish | undefined>\n  /**\n   * Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified.\n   * @see aria-atomic.\n   */\n  'aria-relevant'?: Trackable<\n    | 'additions'\n    | 'additions removals'\n    | 'additions text'\n    | 'all'\n    | 'removals'\n    | 'removals additions'\n    | 'removals text'\n    | 'text'\n    | 'text additions'\n    | 'text removals'\n    | undefined\n  >\n  /** Indicates that user input is required on the element before a form may be submitted. */\n  'aria-required'?: Trackable<Booleanish | undefined>\n  /** Defines a human-readable, author-localized description for the role of an element. */\n  'aria-roledescription'?: Trackable<string | undefined>\n  /**\n   * Defines the total number of rows in a table, grid, or treegrid.\n   * @see aria-rowindex.\n   */\n  'aria-rowcount'?: Trackable<number | undefined>\n  /**\n   * Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid.\n   * @see aria-rowcount\n   * @see aria-rowspan.\n   */\n  'aria-rowindex'?: Trackable<number | undefined>\n  /**\n   * Defines a human readable text alternative of aria-rowindex.\n   * @see aria-colindextext.\n   */\n  'aria-rowindextext'?: Trackable<string | undefined>\n  /**\n   * Defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid.\n   * @see aria-rowindex\n   * @see aria-colspan.\n   */\n  'aria-rowspan'?: Trackable<number | undefined>\n  /**\n   * Indicates the current \"selected\" state of various widgets.\n   * @see aria-checked\n   * @see aria-pressed.\n   */\n  'aria-selected'?: Trackable<Booleanish | undefined>\n  /**\n   * Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.\n   * @see aria-posinset.\n   */\n  'aria-setsize'?: Trackable<number | undefined>\n  /** Indicates if items in a table or grid are sorted in ascending or descending order. */\n  'aria-sort'?: Trackable<'none' | 'ascending' | 'descending' | 'other' | undefined>\n  /** Defines the maximum allowed value for a range widget. */\n  'aria-valuemax'?: Trackable<number | undefined>\n  /** Defines the minimum allowed value for a range widget. */\n  'aria-valuemin'?: Trackable<number | undefined>\n  /**\n   * Defines the current value for a range widget.\n   * @see aria-valuetext.\n   */\n  'aria-valuenow'?: Trackable<number | undefined>\n  /** Defines the human readable text alternative of aria-valuenow for a range widget. */\n  'aria-valuetext'?: Trackable<string | undefined>\n}\n\n// All the WAI-ARIA 1.2 role attribute values from https://www.w3.org/TR/wai-aria-1.2/#role_definitions\nexport type WAIAriaRole =\n  | 'alert'\n  | 'alertdialog'\n  | 'application'\n  | 'article'\n  | 'banner'\n  | 'blockquote'\n  | 'button'\n  | 'caption'\n  | 'cell'\n  | 'checkbox'\n  | 'code'\n  | 'columnheader'\n  | 'combobox'\n  | 'command'\n  | 'complementary'\n  | 'composite'\n  | 'contentinfo'\n  | 'definition'\n  | 'deletion'\n  | 'dialog'\n  | 'directory'\n  | 'document'\n  | 'emphasis'\n  | 'feed'\n  | 'figure'\n  | 'form'\n  | 'grid'\n  | 'gridcell'\n  | 'group'\n  | 'heading'\n  | 'img'\n  | 'input'\n  | 'insertion'\n  | 'landmark'\n  | 'link'\n  | 'list'\n  | 'listbox'\n  | 'listitem'\n  | 'log'\n  | 'main'\n  | 'marquee'\n  | 'math'\n  | 'meter'\n  | 'menu'\n  | 'menubar'\n  | 'menuitem'\n  | 'menuitemcheckbox'\n  | 'menuitemradio'\n  | 'navigation'\n  | 'none'\n  | 'note'\n  | 'option'\n  | 'paragraph'\n  | 'presentation'\n  | 'progressbar'\n  | 'radio'\n  | 'radiogroup'\n  | 'range'\n  | 'region'\n  | 'roletype'\n  | 'row'\n  | 'rowgroup'\n  | 'rowheader'\n  | 'scrollbar'\n  | 'search'\n  | 'searchbox'\n  | 'section'\n  | 'sectionhead'\n  | 'select'\n  | 'separator'\n  | 'slider'\n  | 'spinbutton'\n  | 'status'\n  | 'strong'\n  | 'structure'\n  | 'subscript'\n  | 'superscript'\n  | 'switch'\n  | 'tab'\n  | 'table'\n  | 'tablist'\n  | 'tabpanel'\n  | 'term'\n  | 'textbox'\n  | 'time'\n  | 'timer'\n  | 'toolbar'\n  | 'tooltip'\n  | 'tree'\n  | 'treegrid'\n  | 'treeitem'\n  | 'widget'\n  | 'window'\n  | 'none presentation'\n\n// All the Digital Publishing WAI-ARIA 1.0 role attribute values from https://www.w3.org/TR/dpub-aria-1.0/#role_definitions\nexport type DPubAriaRole =\n  | 'doc-abstract'\n  | 'doc-acknowledgments'\n  | 'doc-afterword'\n  | 'doc-appendix'\n  | 'doc-backlink'\n  | 'doc-biblioentry'\n  | 'doc-bibliography'\n  | 'doc-biblioref'\n  | 'doc-chapter'\n  | 'doc-colophon'\n  | 'doc-conclusion'\n  | 'doc-cover'\n  | 'doc-credit'\n  | 'doc-credits'\n  | 'doc-dedication'\n  | 'doc-endnote'\n  | 'doc-endnotes'\n  | 'doc-epigraph'\n  | 'doc-epilogue'\n  | 'doc-errata'\n  | 'doc-example'\n  | 'doc-footnote'\n  | 'doc-foreword'\n  | 'doc-glossary'\n  | 'doc-glossref'\n  | 'doc-index'\n  | 'doc-introduction'\n  | 'doc-noteref'\n  | 'doc-notice'\n  | 'doc-pagebreak'\n  | 'doc-pagelist'\n  | 'doc-part'\n  | 'doc-preface'\n  | 'doc-prologue'\n  | 'doc-pullquote'\n  | 'doc-qna'\n  | 'doc-subtitle'\n  | 'doc-tip'\n  | 'doc-toc'\n\nexport type AriaRole = WAIAriaRole | DPubAriaRole\n\n/**\n * Comprehensive HTML attributes shared by specialized element prop interfaces.\n */\nexport interface AllHTMLProps<eventTarget extends EventTarget = EventTarget>\n  extends HostProps<eventTarget>,\n    AriaProps {\n  // Standard HTML Attributes\n  /** The `accept` HTML attribute. */\n  accept?: Trackable<string | undefined>\n  /** The `acceptCharset` HTML attribute. */\n  acceptCharset?: Trackable<string | undefined>\n  /** The `accept-charset` HTML attribute. */\n  'accept-charset'?: Trackable<AllHTMLProps['acceptCharset']>\n  /** The `accessKey` HTML attribute. */\n  accessKey?: Trackable<string | undefined>\n  /** The `accesskey` HTML attribute. */\n  accesskey?: Trackable<AllHTMLProps['accessKey']>\n  /** The `action` HTML attribute. */\n  action?: Trackable<string | undefined>\n  /** The `allow` HTML attribute. */\n  allow?: Trackable<string | undefined>\n  /** The `allowFullScreen` HTML attribute. */\n  allowFullScreen?: Trackable<boolean | undefined>\n  /** The `allowTransparency` HTML attribute. */\n  allowTransparency?: Trackable<boolean | undefined>\n  /** The `alt` HTML attribute. */\n  alt?: Trackable<string | undefined>\n  /** The `as` HTML attribute. */\n  as?: Trackable<string | undefined>\n  /** The `async` HTML attribute. */\n  async?: Trackable<boolean | undefined>\n  /** The `autocomplete` HTML attribute. */\n  autocomplete?: Trackable<string | undefined>\n  /** The `autoComplete` HTML attribute. */\n  autoComplete?: Trackable<string | undefined>\n  /** The `autocorrect` HTML attribute. */\n  autocorrect?: Trackable<string | undefined>\n  /** The `autoCorrect` HTML attribute. */\n  autoCorrect?: Trackable<string | undefined>\n  /** The `autofocus` HTML attribute. */\n  autofocus?: Trackable<boolean | undefined>\n  /** The `autoFocus` HTML attribute. */\n  autoFocus?: Trackable<boolean | undefined>\n  /** The `autoPlay` HTML attribute. */\n  autoPlay?: Trackable<boolean | undefined>\n  /** The `autoplay` HTML attribute. */\n  autoplay?: Trackable<boolean | undefined>\n  /** The `capture` HTML attribute. */\n  capture?: Trackable<boolean | string | undefined>\n  /** The `cellPadding` HTML attribute. */\n  cellPadding?: Trackable<number | string | undefined>\n  /** The `cellSpacing` HTML attribute. */\n  cellSpacing?: Trackable<number | string | undefined>\n  /** The `charSet` HTML attribute. */\n  charSet?: Trackable<string | undefined>\n  /** The `charset` HTML attribute. */\n  charset?: Trackable<string | undefined>\n  /** The `challenge` HTML attribute. */\n  challenge?: Trackable<string | undefined>\n  /** The `checked` HTML attribute. */\n  checked?: Trackable<boolean | undefined>\n  /** The `cite` HTML attribute. */\n  cite?: Trackable<string | undefined>\n  /** The `class` HTML attribute. */\n  class?: Trackable<string | undefined>\n  /** The `className` HTML attribute. */\n  className?: Trackable<string | undefined>\n  /** The `cols` HTML attribute. */\n  cols?: Trackable<number | undefined>\n  /** The `colSpan` HTML attribute. */\n  colSpan?: Trackable<number | undefined>\n  /** The `colspan` HTML attribute. */\n  colspan?: Trackable<number | undefined>\n  /** The `content` HTML attribute. */\n  content?: Trackable<string | undefined>\n  /** The `contentEditable` HTML attribute. */\n  contentEditable?: Trackable<Booleanish | '' | 'plaintext-only' | 'inherit' | undefined>\n  /** The `contenteditable` HTML attribute. */\n  contenteditable?: Trackable<AllHTMLProps['contentEditable']>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contextmenu */\n  contextMenu?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contextmenu */\n  contextmenu?: Trackable<string | undefined>\n  /** The `controls` HTML attribute. */\n  controls?: Trackable<boolean | undefined>\n  /** The `controlslist` HTML attribute. */\n  controlslist?: Trackable<string | undefined>\n  /** The `controlsList` HTML attribute. */\n  controlsList?: Trackable<string | undefined>\n  /** The `coords` HTML attribute. */\n  coords?: Trackable<string | undefined>\n  /** The `crossOrigin` HTML attribute. */\n  crossOrigin?: Trackable<string | undefined>\n  /** The `crossorigin` HTML attribute. */\n  crossorigin?: Trackable<string | undefined>\n  /** The `currentTime` HTML attribute. */\n  currentTime?: Trackable<number | undefined>\n  /** The `data` HTML attribute. */\n  data?: Trackable<string | undefined>\n  /** The `dateTime` HTML attribute. */\n  dateTime?: Trackable<string | undefined>\n  /** The `datetime` HTML attribute. */\n  datetime?: Trackable<string | undefined>\n  /** The `default` HTML attribute. */\n  default?: Trackable<boolean | undefined>\n  /** The `defaultChecked` HTML attribute. */\n  defaultChecked?: Trackable<boolean | undefined>\n  /** The `defaultMuted` HTML attribute. */\n  defaultMuted?: Trackable<boolean | undefined>\n  /** The `defaultPlaybackRate` HTML attribute. */\n  defaultPlaybackRate?: Trackable<number | undefined>\n  /** The `defaultValue` HTML attribute. */\n  defaultValue?: Trackable<string | undefined>\n  /** The `defer` HTML attribute. */\n  defer?: Trackable<boolean | undefined>\n  /** The `dir` HTML attribute. */\n  dir?: Trackable<'auto' | 'rtl' | 'ltr' | undefined>\n  /** The `disabled` HTML attribute. */\n  disabled?: Trackable<boolean | undefined>\n  /** The `disableremoteplayback` HTML attribute. */\n  disableremoteplayback?: Trackable<boolean | undefined>\n  /** The `disableRemotePlayback` HTML attribute. */\n  disableRemotePlayback?: Trackable<boolean | undefined>\n  /** The `download` HTML attribute. */\n  download?: Trackable<any | undefined>\n  /** The `decoding` HTML attribute. */\n  decoding?: Trackable<'sync' | 'async' | 'auto' | undefined>\n  /** The `draggable` HTML attribute. */\n  draggable?: Trackable<boolean | undefined>\n  /** The `encType` HTML attribute. */\n  encType?: Trackable<string | undefined>\n  /** The `enctype` HTML attribute. */\n  enctype?: Trackable<string | undefined>\n  /** The `enterkeyhint` HTML attribute. */\n  enterkeyhint?: Trackable<\n    'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined\n  >\n  /** The `elementTiming` HTML attribute. */\n  elementTiming?: Trackable<string | undefined>\n  /** The `elementtiming` HTML attribute. */\n  elementtiming?: Trackable<AllHTMLProps['elementTiming']>\n  /** The `exportparts` HTML attribute. */\n  exportparts?: Trackable<string | undefined>\n  /** The `for` HTML attribute. */\n  for?: Trackable<string | undefined>\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `formAction` HTML attribute. */\n  formAction?: Trackable<string | undefined>\n  /** The `formaction` HTML attribute. */\n  formaction?: Trackable<string | undefined>\n  /** The `formEncType` HTML attribute. */\n  formEncType?: Trackable<string | undefined>\n  /** The `formenctype` HTML attribute. */\n  formenctype?: Trackable<string | undefined>\n  /** The `formMethod` HTML attribute. */\n  formMethod?: Trackable<string | undefined>\n  /** The `formmethod` HTML attribute. */\n  formmethod?: Trackable<string | undefined>\n  /** The `formNoValidate` HTML attribute. */\n  formNoValidate?: Trackable<boolean | undefined>\n  /** The `formnovalidate` HTML attribute. */\n  formnovalidate?: Trackable<boolean | undefined>\n  /** The `formTarget` HTML attribute. */\n  formTarget?: Trackable<string | undefined>\n  /** The `formtarget` HTML attribute. */\n  formtarget?: Trackable<string | undefined>\n  /** The `frameBorder` HTML attribute. */\n  frameBorder?: Trackable<number | string | undefined>\n  /** The `frameborder` HTML attribute. */\n  frameborder?: Trackable<number | string | undefined>\n  /** The `headers` HTML attribute. */\n  headers?: Trackable<string | undefined>\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `hidden` HTML attribute. */\n  hidden?: Trackable<boolean | 'hidden' | 'until-found' | undefined>\n  /** The `high` HTML attribute. */\n  high?: Trackable<number | undefined>\n  /** The `href` HTML attribute. */\n  href?: Trackable<string | undefined>\n  /** The `hrefLang` HTML attribute. */\n  hrefLang?: Trackable<string | undefined>\n  /** The `hreflang` HTML attribute. */\n  hreflang?: Trackable<string | undefined>\n  /** The `htmlFor` HTML attribute. */\n  htmlFor?: Trackable<string | undefined>\n  /** The `httpEquiv` HTML attribute. */\n  httpEquiv?: Trackable<string | undefined>\n  /** The `http-equiv` HTML attribute. */\n  'http-equiv'?: Trackable<string | undefined>\n  /** The `icon` HTML attribute. */\n  icon?: Trackable<string | undefined>\n  /** The `id` HTML attribute. */\n  id?: Trackable<string | undefined>\n  /** The `indeterminate` HTML attribute. */\n  indeterminate?: Trackable<boolean | undefined>\n  /** The `inert` HTML attribute. */\n  inert?: Trackable<boolean | undefined>\n  /** The `inputMode` HTML attribute. */\n  inputMode?: Trackable<string | undefined>\n  /** The `inputmode` HTML attribute. */\n  inputmode?: Trackable<string | undefined>\n  /** The `integrity` HTML attribute. */\n  integrity?: Trackable<string | undefined>\n  /** The `is` HTML attribute. */\n  is?: Trackable<string | undefined>\n  /** The `keyParams` HTML attribute. */\n  keyParams?: Trackable<string | undefined>\n  /** The `keyType` HTML attribute. */\n  keyType?: Trackable<string | undefined>\n  /** The `kind` HTML attribute. */\n  kind?: Trackable<string | undefined>\n  /** The `label` HTML attribute. */\n  label?: Trackable<string | undefined>\n  /** The `lang` HTML attribute. */\n  lang?: Trackable<string | undefined>\n  /** The `list` HTML attribute. */\n  list?: Trackable<string | undefined>\n  /** The `loading` HTML attribute. */\n  loading?: Trackable<'eager' | 'lazy' | undefined>\n  /** The `loop` HTML attribute. */\n  loop?: Trackable<boolean | undefined>\n  /** The `low` HTML attribute. */\n  low?: Trackable<number | undefined>\n  /** The `manifest` HTML attribute. */\n  manifest?: Trackable<string | undefined>\n  /** The `marginHeight` HTML attribute. */\n  marginHeight?: Trackable<number | undefined>\n  /** The `marginWidth` HTML attribute. */\n  marginWidth?: Trackable<number | undefined>\n  /** The `max` HTML attribute. */\n  max?: Trackable<number | string | undefined>\n  /** The `maxLength` HTML attribute. */\n  maxLength?: Trackable<number | undefined>\n  /** The `maxlength` HTML attribute. */\n  maxlength?: Trackable<number | undefined>\n  /** The `media` HTML attribute. */\n  media?: Trackable<string | undefined>\n  /** The `mediaGroup` HTML attribute. */\n  mediaGroup?: Trackable<string | undefined>\n  /** The `method` HTML attribute. */\n  method?: Trackable<string | undefined>\n  /** The `min` HTML attribute. */\n  min?: Trackable<number | string | undefined>\n  /** The `minLength` HTML attribute. */\n  minLength?: Trackable<number | undefined>\n  /** The `minlength` HTML attribute. */\n  minlength?: Trackable<number | undefined>\n  /** The `multiple` HTML attribute. */\n  multiple?: Trackable<boolean | undefined>\n  /** The `muted` HTML attribute. */\n  muted?: Trackable<boolean | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `nomodule` HTML attribute. */\n  nomodule?: Trackable<boolean | undefined>\n  /** The `nonce` HTML attribute. */\n  nonce?: Trackable<string | undefined>\n  /** The `noValidate` HTML attribute. */\n  noValidate?: Trackable<boolean | undefined>\n  /** The `novalidate` HTML attribute. */\n  novalidate?: Trackable<boolean | undefined>\n  /** The `open` HTML attribute. */\n  open?: Trackable<boolean | undefined>\n  /** The `optimum` HTML attribute. */\n  optimum?: Trackable<number | undefined>\n  /** The `part` HTML attribute. */\n  part?: Trackable<string | undefined>\n  /** The `pattern` HTML attribute. */\n  pattern?: Trackable<string | undefined>\n  /** The `ping` HTML attribute. */\n  ping?: Trackable<string | undefined>\n  /** The `placeholder` HTML attribute. */\n  placeholder?: Trackable<string | undefined>\n  /** The `playsInline` HTML attribute. */\n  playsInline?: Trackable<boolean | undefined>\n  /** The `playsinline` HTML attribute. */\n  playsinline?: Trackable<boolean | undefined>\n  /** The `playbackRate` HTML attribute. */\n  playbackRate?: Trackable<number | undefined>\n  /** The `popover` HTML attribute. */\n  popover?: Trackable<'auto' | 'hint' | 'manual' | boolean | undefined>\n  /** The `popovertarget` HTML attribute. */\n  popovertarget?: Trackable<string | undefined>\n  /** The `popoverTarget` HTML attribute. */\n  popoverTarget?: Trackable<string | undefined>\n  /** The `popovertargetaction` HTML attribute. */\n  popovertargetaction?: Trackable<'hide' | 'show' | 'toggle' | undefined>\n  /** The `popoverTargetAction` HTML attribute. */\n  popoverTargetAction?: Trackable<'hide' | 'show' | 'toggle' | undefined>\n  /** The `poster` HTML attribute. */\n  poster?: Trackable<string | undefined>\n  /** The `preload` HTML attribute. */\n  preload?: Trackable<'auto' | 'metadata' | 'none' | undefined>\n  /** The `preservesPitch` HTML attribute. */\n  preservesPitch?: Trackable<boolean | undefined>\n  /** The `radioGroup` HTML attribute. */\n  radioGroup?: Trackable<string | undefined>\n  /** The `readonly` HTML attribute. */\n  readonly?: Trackable<boolean | undefined>\n  /** The `readOnly` HTML attribute. */\n  readOnly?: Trackable<boolean | undefined>\n  /** The `referrerpolicy` HTML attribute. */\n  referrerpolicy?: Trackable<\n    | 'no-referrer'\n    | 'no-referrer-when-downgrade'\n    | 'origin'\n    | 'origin-when-cross-origin'\n    | 'same-origin'\n    | 'strict-origin'\n    | 'strict-origin-when-cross-origin'\n    | 'unsafe-url'\n    | undefined\n  >\n  /** The `rel` HTML attribute. */\n  rel?: Trackable<string | undefined>\n  /** The `required` HTML attribute. */\n  required?: Trackable<boolean | undefined>\n  /** The `reversed` HTML attribute. */\n  reversed?: Trackable<boolean | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<AriaRole | undefined>\n  /** The `rows` HTML attribute. */\n  rows?: Trackable<number | undefined>\n  /** The `rowSpan` HTML attribute. */\n  rowSpan?: Trackable<number | undefined>\n  /** The `rowspan` HTML attribute. */\n  rowspan?: Trackable<number | undefined>\n  /** The `sandbox` HTML attribute. */\n  sandbox?: Trackable<string | undefined>\n  /** The `scope` HTML attribute. */\n  scope?: Trackable<string | undefined>\n  /** The `scoped` HTML attribute. */\n  scoped?: Trackable<boolean | undefined>\n  /** The `scrolling` HTML attribute. */\n  scrolling?: Trackable<string | undefined>\n  /** The `seamless` HTML attribute. */\n  seamless?: Trackable<boolean | undefined>\n  /** The `selected` HTML attribute. */\n  selected?: Trackable<boolean | undefined>\n  /** The `shape` HTML attribute. */\n  shape?: Trackable<string | undefined>\n  /** The `size` HTML attribute. */\n  size?: Trackable<number | undefined>\n  /** The `sizes` HTML attribute. */\n  sizes?: Trackable<string | undefined>\n  /** The `slot` HTML attribute. */\n  slot?: Trackable<string | undefined>\n  /** The `span` HTML attribute. */\n  span?: Trackable<number | undefined>\n  /** The `spellcheck` HTML attribute. */\n  spellcheck?: Trackable<boolean | undefined>\n  /** The `src` HTML attribute. */\n  src?: Trackable<string | undefined>\n  /** The `srcDoc` HTML attribute. */\n  srcDoc?: Trackable<string | undefined>\n  /** The `srcdoc` HTML attribute. */\n  srcdoc?: Trackable<string | undefined>\n  /** The `srcLang` HTML attribute. */\n  srcLang?: Trackable<string | undefined>\n  /** The `srclang` HTML attribute. */\n  srclang?: Trackable<string | undefined>\n  /** The `srcSet` HTML attribute. */\n  srcSet?: Trackable<string | undefined>\n  /** The `srcset` HTML attribute. */\n  srcset?: Trackable<string | undefined>\n  /** The `srcObject` HTML attribute. */\n  srcObject?: Trackable<MediaStream | MediaSource | Blob | File | null>\n  /** The `start` HTML attribute. */\n  start?: Trackable<number | undefined>\n  /** The `step` HTML attribute. */\n  step?: Trackable<number | string | undefined>\n  /** The `style` HTML attribute. */\n  style?: Trackable<string | StyleProps | undefined>\n  /** The `summary` HTML attribute. */\n  summary?: Trackable<string | undefined>\n  /** The `tabIndex` HTML attribute. */\n  tabIndex?: Trackable<number | undefined>\n  /** The `tabindex` HTML attribute. */\n  tabindex?: Trackable<number | undefined>\n  /** The `target` HTML attribute. */\n  target?: Trackable<string | undefined>\n  /** The `title` HTML attribute. */\n  title?: Trackable<string | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<string | undefined>\n  /** The `useMap` HTML attribute. */\n  useMap?: Trackable<string | undefined>\n  /** The `usemap` HTML attribute. */\n  usemap?: Trackable<string | undefined>\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | string[] | number | undefined>\n  /** The `volume` HTML attribute. */\n  volume?: Trackable<string | number | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n  /** The `wmode` HTML attribute. */\n  wmode?: Trackable<string | undefined>\n  /** The `wrap` HTML attribute. */\n  wrap?: Trackable<string | undefined>\n\n  // Non-standard Attributes\n  /** The `autocapitalize` HTML attribute. */\n  autocapitalize?: Trackable<\n    'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters' | undefined\n  >\n  /** The `autoCapitalize` HTML attribute. */\n  autoCapitalize?: Trackable<\n    'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters' | undefined\n  >\n  /** The `disablePictureInPicture` HTML attribute. */\n  disablePictureInPicture?: Trackable<boolean | undefined>\n  /** The `results` HTML attribute. */\n  results?: Trackable<number | undefined>\n  /** The `translate` HTML attribute. */\n  translate?: Trackable<boolean | undefined>\n\n  // RDFa Attributes\n  /** The `about` HTML attribute. */\n  about?: Trackable<string | undefined>\n  /** The `datatype` HTML attribute. */\n  datatype?: Trackable<string | undefined>\n  /** The `inlist` HTML attribute. */\n  inlist?: Trackable<any>\n  /** The `prefix` HTML attribute. */\n  prefix?: Trackable<string | undefined>\n  /** The `property` HTML attribute. */\n  property?: Trackable<string | undefined>\n  /** The `resource` HTML attribute. */\n  resource?: Trackable<string | undefined>\n  /** The `typeof` HTML attribute. */\n  typeof?: Trackable<string | undefined>\n  /** The `vocab` HTML attribute. */\n  vocab?: Trackable<string | undefined>\n\n  // Microdata Attributes\n  /** The `itemProp` HTML attribute. */\n  itemProp?: Trackable<string | undefined>\n  /** The `itemprop` HTML attribute. */\n  itemprop?: Trackable<string | undefined>\n  /** The `itemScope` HTML attribute. */\n  itemScope?: Trackable<boolean | undefined>\n  /** The `itemscope` HTML attribute. */\n  itemscope?: Trackable<boolean | undefined>\n  /** The `itemType` HTML attribute. */\n  itemType?: Trackable<string | undefined>\n  /** The `itemtype` HTML attribute. */\n  itemtype?: Trackable<string | undefined>\n  /** The `itemID` HTML attribute. */\n  itemID?: Trackable<string | undefined>\n  /** The `itemid` HTML attribute. */\n  itemid?: Trackable<string | undefined>\n  /** The `itemRef` HTML attribute. */\n  itemRef?: Trackable<string | undefined>\n  /** The `itemref` HTML attribute. */\n  itemref?: Trackable<string | undefined>\n}\n\n/**\n * Core global HTML attributes accepted by most host elements.\n */\nexport interface HTMLProps<eventTarget extends EventTarget = EventTarget>\n  extends HostProps<eventTarget>,\n    AriaProps {\n  // Standard HTML Attributes\n  /** The `accesskey` HTML attribute. */\n  accesskey?: Trackable<string | undefined>\n  /** The `accessKey` HTML attribute. */\n  accessKey?: Trackable<string | undefined>\n  /** The `autocapitalize` HTML attribute. */\n  autocapitalize?: Trackable<\n    'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters' | undefined\n  >\n  /** The `autoCapitalize` HTML attribute. */\n  autoCapitalize?: Trackable<\n    'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters' | undefined\n  >\n  /** The `autocorrect` HTML attribute. */\n  autocorrect?: Trackable<string | undefined>\n  /** The `autoCorrect` HTML attribute. */\n  autoCorrect?: Trackable<string | undefined>\n  /** The `autofocus` HTML attribute. */\n  autofocus?: Trackable<boolean | undefined>\n  /** The `autoFocus` HTML attribute. */\n  autoFocus?: Trackable<boolean | undefined>\n  /** The `class` HTML attribute. */\n  class?: Trackable<string | undefined>\n  /** The `className` HTML attribute. */\n  className?: Trackable<string | undefined>\n  /** The `contenteditable` HTML attribute. */\n  contenteditable?: Trackable<Booleanish | '' | 'plaintext-only' | 'inherit' | undefined>\n  /** The `contentEditable` HTML attribute. */\n  contentEditable?: Trackable<Booleanish | '' | 'plaintext-only' | 'inherit' | undefined>\n  /** The `dir` HTML attribute. */\n  dir?: Trackable<'auto' | 'rtl' | 'ltr' | undefined>\n  /** The `draggable` HTML attribute. */\n  draggable?: Trackable<boolean | undefined>\n  /** The `enterkeyhint` HTML attribute. */\n  enterkeyhint?: Trackable<\n    'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined\n  >\n  /** The `exportparts` HTML attribute. */\n  exportparts?: Trackable<string | undefined>\n  /** The `hidden` HTML attribute. */\n  hidden?: Trackable<boolean | 'hidden' | 'until-found' | undefined>\n  /** The `id` HTML attribute. */\n  id?: Trackable<string | undefined>\n  /** The `inert` HTML attribute. */\n  inert?: Trackable<boolean | undefined>\n  /** The `inputmode` HTML attribute. */\n  inputmode?: Trackable<string | undefined>\n  /** The `inputMode` HTML attribute. */\n  inputMode?: Trackable<string | undefined>\n  /** The `is` HTML attribute. */\n  is?: Trackable<string | undefined>\n  /** The `lang` HTML attribute. */\n  lang?: Trackable<string | undefined>\n  /** The `nonce` HTML attribute. */\n  nonce?: Trackable<string | undefined>\n  /** The `part` HTML attribute. */\n  part?: Trackable<string | undefined>\n  /** The `popover` HTML attribute. */\n  popover?: Trackable<'auto' | 'hint' | 'manual' | boolean | undefined>\n  /** The `slot` HTML attribute. */\n  slot?: Trackable<string | undefined>\n  /** The `spellcheck` HTML attribute. */\n  spellcheck?: Trackable<boolean | undefined>\n  /** The `style` HTML attribute. */\n  style?: Trackable<string | StyleProps | undefined>\n  /** The `tabindex` HTML attribute. */\n  tabindex?: Trackable<number | undefined>\n  /** The `tabIndex` HTML attribute. */\n  tabIndex?: Trackable<number | undefined>\n  /** The `title` HTML attribute. */\n  title?: Trackable<string | undefined>\n  /** The `translate` HTML attribute. */\n  translate?: Trackable<boolean | undefined>\n\n  // WAI-ARIA Attributes\n  // Most elements only allow a subset of roles and so this\n  // is overwritten in many of the per-element interfaces below\n  /** The `role` HTML attribute. */\n  role?: Trackable<AriaRole | undefined>\n\n  // Non-standard Attributes\n  /** The `disablePictureInPicture` HTML attribute. */\n  disablePictureInPicture?: Trackable<boolean | undefined>\n  /** The `elementtiming` HTML attribute. */\n  elementtiming?: Trackable<string | undefined>\n  /** The `elementTiming` HTML attribute. */\n  elementTiming?: Trackable<string | undefined>\n  /** The `results` HTML attribute. */\n  results?: Trackable<number | undefined>\n\n  // RDFa Attributes\n  /** The `about` HTML attribute. */\n  about?: Trackable<string | undefined>\n  /** The `datatype` HTML attribute. */\n  datatype?: Trackable<string | undefined>\n  /** The `inlist` HTML attribute. */\n  inlist?: Trackable<any>\n  /** The `prefix` HTML attribute. */\n  prefix?: Trackable<string | undefined>\n  /** The `property` HTML attribute. */\n  property?: Trackable<string | undefined>\n  /** The `resource` HTML attribute. */\n  resource?: Trackable<string | undefined>\n  /** The `typeof` HTML attribute. */\n  typeof?: Trackable<string | undefined>\n  /** The `vocab` HTML attribute. */\n  vocab?: Trackable<string | undefined>\n\n  // Microdata Attributes\n  /** The `itemid` HTML attribute. */\n  itemid?: Trackable<string | undefined>\n  /** The `itemID` HTML attribute. */\n  itemID?: Trackable<string | undefined>\n  /** The `itemprop` HTML attribute. */\n  itemprop?: Trackable<string | undefined>\n  /** The `itemProp` HTML attribute. */\n  itemProp?: Trackable<string | undefined>\n  /** The `itemref` HTML attribute. */\n  itemref?: Trackable<string | undefined>\n  /** The `itemRef` HTML attribute. */\n  itemRef?: Trackable<string | undefined>\n  /** The `itemscope` HTML attribute. */\n  itemscope?: Trackable<boolean | undefined>\n  /** The `itemScope` HTML attribute. */\n  itemScope?: Trackable<boolean | undefined>\n  /** The `itemtype` HTML attribute. */\n  itemtype?: Trackable<string | undefined>\n  /** The `itemType` HTML attribute. */\n  itemType?: Trackable<string | undefined>\n}\n\nexport type HTMLAttributeReferrerPolicy =\n  | ''\n  | 'no-referrer'\n  | 'no-referrer-when-downgrade'\n  | 'origin'\n  | 'origin-when-cross-origin'\n  | 'same-origin'\n  | 'strict-origin'\n  | 'strict-origin-when-cross-origin'\n  | 'unsafe-url'\n\nexport type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {})\n\n/**\n * Props accepted by `<anchor>` elements.\n */\nexport interface PartialAnchorHTMLProps<eventTarget extends EventTarget>\n  extends HTMLProps<eventTarget> {\n  /** The `download` HTML attribute. */\n  download?: Trackable<any>\n  /** The `hreflang` HTML attribute. */\n  hreflang?: Trackable<string | undefined>\n  /** The `hrefLang` HTML attribute. */\n  hrefLang?: Trackable<string | undefined>\n  /** The `media` HTML attribute. */\n  media?: Trackable<string | undefined>\n  /** The `ping` HTML attribute. */\n  ping?: Trackable<string | undefined>\n  /** The `rel` HTML attribute. */\n  rel?: Trackable<string | undefined>\n  /** The `target` HTML attribute. */\n  target?: Trackable<HTMLAttributeAnchorTarget | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<string | undefined>\n  /** The `referrerpolicy` HTML attribute. */\n  referrerpolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `referrerPolicy` HTML attribute. */\n  referrerPolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n\n  // Non-standard Attributes\n  /** The `rmx-target` HTML attribute. */\n  'rmx-target'?: Trackable<string | undefined>\n  /** The `rmx-src` HTML attribute. */\n  'rmx-src'?: Trackable<string | undefined>\n  /** The `rmx-reset-scroll` HTML attribute. */\n  'rmx-reset-scroll'?: Trackable<string | undefined>\n}\n\nexport type AnchorAriaRoles =\n  | {\n      href: Trackable<string>\n      role?: Trackable<\n        | 'link'\n        | 'button'\n        | 'checkbox'\n        | 'menuitem'\n        | 'menuitemcheckbox'\n        | 'menuitemradio'\n        | 'option'\n        | 'radio'\n        | 'switch'\n        | 'tab'\n        | 'treeitem'\n        | 'doc-backlink'\n        | 'doc-biblioref'\n        | 'doc-glossref'\n        | 'doc-noteref'\n        | undefined\n      >\n    }\n  | {\n      href?: never\n      role?: Trackable<AriaRole | undefined>\n    }\n\n/**\n * Props accepted by `<anchor>` elements.\n */\nexport type AccessibleAnchorHTMLProps<eventTarget extends EventTarget = HTMLAnchorElement> = Omit<\n  PartialAnchorHTMLProps<eventTarget>,\n  'role'\n> &\n  AnchorAriaRoles\n\n/**\n * Props accepted by `<anchor>` elements.\n */\nexport interface AnchorHTMLProps<eventTarget extends EventTarget = HTMLAnchorElement>\n  extends PartialAnchorHTMLProps<eventTarget> {\n  /** The `href` HTML attribute. */\n  href?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<AriaRole | undefined>\n}\n\n/**\n * Props accepted by `<area>` elements.\n */\nexport interface PartialAreaHTMLProps<eventTarget extends EventTarget>\n  extends HTMLProps<eventTarget> {\n  /** The `alt` HTML attribute. */\n  alt?: Trackable<string | undefined>\n  /** The `coords` HTML attribute. */\n  coords?: Trackable<string | undefined>\n  /** The `download` HTML attribute. */\n  download?: Trackable<any>\n  /** The `hreflang` HTML attribute. */\n  hreflang?: Trackable<string | undefined>\n  /** The `hrefLang` HTML attribute. */\n  hrefLang?: Trackable<string | undefined>\n  /** The `media` HTML attribute. */\n  media?: Trackable<string | undefined>\n  /** The `referrerpolicy` HTML attribute. */\n  referrerpolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `referrerPolicy` HTML attribute. */\n  referrerPolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `rel` HTML attribute. */\n  rel?: Trackable<string | undefined>\n  /** The `shape` HTML attribute. */\n  shape?: Trackable<string | undefined>\n  /** The `target` HTML attribute. */\n  target?: Trackable<HTMLAttributeAnchorTarget | undefined>\n}\n\nexport type AreaAriaRoles =\n  | {\n      href: Trackable<string>\n      role?: Trackable<'link' | undefined>\n    }\n  | {\n      href?: never\n      role?: Trackable<'button' | 'link' | undefined>\n    }\n\n/**\n * Props accepted by `<area>` elements.\n */\nexport type AccessibleAreaHTMLProps<eventTarget extends EventTarget = HTMLAreaElement> = Omit<\n  PartialAreaHTMLProps<eventTarget>,\n  'role'\n> &\n  AreaAriaRoles\n\n/**\n * Props accepted by `<area>` elements.\n */\nexport interface AreaHTMLProps<eventTarget extends EventTarget = HTMLAreaElement>\n  extends PartialAreaHTMLProps<eventTarget> {\n  /** The `href` HTML attribute. */\n  href?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'button' | 'link' | undefined>\n}\n\n/**\n * Props accepted by `<article>` elements.\n */\nexport interface ArticleHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<\n    | 'article'\n    | 'application'\n    | 'document'\n    | 'feed'\n    | 'main'\n    | 'none'\n    | 'presentation'\n    | 'region'\n    | undefined\n  >\n}\n\n/**\n * Props accepted by `<aside>` elements.\n */\nexport interface AsideHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<\n    | 'complementary'\n    | 'feed'\n    | 'none'\n    | 'note'\n    | 'presentation'\n    | 'region'\n    | 'search'\n    | 'doc-dedication'\n    | 'doc-example'\n    | 'doc-footnote'\n    | 'doc-glossary'\n    | 'doc-pullquote'\n    | 'doc-tip'\n    | undefined\n  >\n}\n\n/**\n * Props accepted by `<audio>` elements.\n */\nexport interface AudioHTMLProps<eventTarget extends EventTarget = HTMLAudioElement>\n  extends MediaHTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'application' | undefined>\n}\n\n/**\n * Props accepted by `<base>` elements.\n */\nexport interface BaseHTMLProps<eventTarget extends EventTarget = HTMLBaseElement>\n  extends HTMLProps<eventTarget> {\n  /** The `href` HTML attribute. */\n  href?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n  /** The `target` HTML attribute. */\n  target?: Trackable<HTMLAttributeAnchorTarget | undefined>\n}\n\n/**\n * Props accepted by `<blockquote>` elements.\n */\nexport interface BlockquoteHTMLProps<eventTarget extends EventTarget = HTMLQuoteElement>\n  extends HTMLProps<eventTarget> {\n  /** The `cite` HTML attribute. */\n  cite?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<br>` elements.\n */\nexport interface BrHTMLProps<eventTarget extends EventTarget = HTMLBRElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'none' | 'presentation' | undefined>\n}\n\n/**\n * Props accepted by `<button>` elements.\n */\nexport interface ButtonHTMLProps<eventTarget extends EventTarget = HTMLButtonElement>\n  extends HTMLProps<eventTarget> {\n  /** The `command` HTML attribute. */\n  command?: Trackable<string | undefined>\n  /** The `commandfor` HTML attribute. */\n  commandfor?: Trackable<string | undefined>\n  /** The `commandFor` HTML attribute. */\n  commandFor?: Trackable<string | undefined>\n  /** The `disabled` HTML attribute. */\n  disabled?: Trackable<boolean | undefined>\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `formaction` HTML attribute. */\n  formaction?: Trackable<string | undefined>\n  /** The `formAction` HTML attribute. */\n  formAction?: Trackable<string | undefined>\n  /** The `formenctype` HTML attribute. */\n  formenctype?: Trackable<string | undefined>\n  /** The `formEncType` HTML attribute. */\n  formEncType?: Trackable<string | undefined>\n  /** The `formmethod` HTML attribute. */\n  formmethod?: Trackable<string | undefined>\n  /** The `formMethod` HTML attribute. */\n  formMethod?: Trackable<string | undefined>\n  /** The `formnovalidate` HTML attribute. */\n  formnovalidate?: Trackable<boolean | undefined>\n  /** The `formNoValidate` HTML attribute. */\n  formNoValidate?: Trackable<boolean | undefined>\n  /** The `formtarget` HTML attribute. */\n  formtarget?: Trackable<string | undefined>\n  /** The `formTarget` HTML attribute. */\n  formTarget?: Trackable<string | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `popovertarget` HTML attribute. */\n  popovertarget?: Trackable<string | undefined>\n  /** The `popoverTarget` HTML attribute. */\n  popoverTarget?: Trackable<string | undefined>\n  /** The `popovertargetaction` HTML attribute. */\n  popovertargetaction?: Trackable<'hide' | 'show' | 'toggle' | undefined>\n  /** The `popoverTargetAction` HTML attribute. */\n  popoverTargetAction?: Trackable<'hide' | 'show' | 'toggle' | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<\n    | 'button'\n    | 'checkbox'\n    | 'combobox'\n    | 'gridcell'\n    | 'link'\n    | 'menuitem'\n    | 'menuitemcheckbox'\n    | 'menuitemradio'\n    | 'option'\n    | 'radio'\n    | 'separator'\n    | 'slider'\n    | 'switch'\n    | 'tab'\n    | 'treeitem'\n    | undefined\n  >\n  /** The `type` HTML attribute. */\n  type?: Trackable<'submit' | 'reset' | 'button' | undefined>\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n}\n\n/**\n * Props accepted by `<canvas>` elements.\n */\nexport interface CanvasHTMLProps<eventTarget extends EventTarget = HTMLCanvasElement>\n  extends HTMLProps<eventTarget> {\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n}\n\n/**\n * Props accepted by `<caption>` elements.\n */\nexport interface CaptionHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: 'caption'\n}\n\n/**\n * Props accepted by `<col>` elements.\n */\nexport interface ColHTMLProps<eventTarget extends EventTarget = HTMLTableColElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: never\n  /** The `span` HTML attribute. */\n  span?: Trackable<number | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n}\n\n/**\n * Props accepted by `<colgroup>` elements.\n */\nexport interface ColgroupHTMLProps<eventTarget extends EventTarget = HTMLTableColElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: never\n  /** The `span` HTML attribute. */\n  span?: Trackable<number | undefined>\n}\n\n/**\n * Props accepted by `<data>` elements.\n */\nexport interface DataHTMLProps<eventTarget extends EventTarget = HTMLDataElement>\n  extends HTMLProps<eventTarget> {\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n}\n\n/**\n * Props accepted by `<datalist>` elements.\n */\nexport interface DataListHTMLProps<eventTarget extends EventTarget = HTMLDataListElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'listbox' | undefined>\n}\n\n/**\n * Props accepted by `<dd>` elements.\n */\nexport interface DdHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<del>` elements.\n */\nexport interface DelHTMLProps<eventTarget extends EventTarget = HTMLModElement>\n  extends HTMLProps<eventTarget> {\n  /** The `cite` HTML attribute. */\n  cite?: Trackable<string | undefined>\n  /** The `datetime` HTML attribute. */\n  datetime?: Trackable<string | undefined>\n  /** The `dateTime` HTML attribute. */\n  dateTime?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<details>` elements.\n */\nexport interface DetailsHTMLProps<eventTarget extends EventTarget = HTMLDetailsElement>\n  extends HTMLProps<eventTarget> {\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `open` HTML attribute. */\n  open?: Trackable<boolean | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'group' | undefined>\n}\n\n/**\n * Props accepted by `<dialog>` elements.\n */\nexport interface DialogHTMLProps<eventTarget extends EventTarget = HTMLDialogElement>\n  extends HTMLProps<eventTarget> {\n  /** The `open` HTML attribute. */\n  open?: Trackable<boolean | undefined>\n  /** The `closedby` HTML attribute. */\n  closedby?: Trackable<'none' | 'closerequest' | 'any' | undefined>\n  /** The `closedBy` HTML attribute. */\n  closedBy?: Trackable<'none' | 'closerequest' | 'any' | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'dialog' | 'alertdialog' | undefined>\n}\n\n/**\n * Props accepted by `<dl>` elements.\n */\nexport interface DlHTMLProps<eventTarget extends EventTarget = HTMLDListElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'group' | 'list' | 'none' | 'presentation' | undefined>\n}\n\n/**\n * Props accepted by `<dt>` elements.\n */\nexport interface DtHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'listitem' | undefined>\n}\n\n/**\n * Props accepted by `<embed>` elements.\n */\nexport interface EmbedHTMLProps<eventTarget extends EventTarget = HTMLEmbedElement>\n  extends HTMLProps<eventTarget> {\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'application' | 'document' | 'img' | 'none' | 'presentation' | undefined>\n  /** The `src` HTML attribute. */\n  src?: Trackable<string | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n}\n\n/**\n * Props accepted by `<fieldset>` elements.\n */\nexport interface FieldsetHTMLProps<eventTarget extends EventTarget = HTMLFieldSetElement>\n  extends HTMLProps<eventTarget> {\n  /** The `disabled` HTML attribute. */\n  disabled?: Trackable<boolean | undefined>\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'group' | 'none' | 'presentation' | 'radiogroup' | undefined>\n}\n\n/**\n * Props accepted by `<figcaption>` elements.\n */\nexport interface FigcaptionHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'group' | 'none' | 'presentation' | undefined>\n}\n\n/**\n * Props accepted by `<footer>` elements.\n */\nexport interface FooterHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'contentinfo' | 'group' | 'none' | 'presentation' | 'doc-footnote' | undefined>\n}\n\n/**\n * Props accepted by `<form>` elements.\n */\nexport interface FormHTMLProps<eventTarget extends EventTarget = HTMLFormElement>\n  extends HTMLProps<eventTarget> {\n  /** The `accept-charset` HTML attribute. */\n  'accept-charset'?: Trackable<string | undefined>\n  /** The `acceptCharset` HTML attribute. */\n  acceptCharset?: Trackable<string | undefined>\n  /** The `action` HTML attribute. */\n  action?: Trackable<string | undefined>\n  /** The `autocomplete` HTML attribute. */\n  autocomplete?: Trackable<string | undefined>\n  /** The `autoComplete` HTML attribute. */\n  autoComplete?: Trackable<string | undefined>\n  /** The `enctype` HTML attribute. */\n  enctype?: Trackable<string | undefined>\n  /** The `encType` HTML attribute. */\n  encType?: Trackable<string | undefined>\n  /** The `method` HTML attribute. */\n  method?: Trackable<string | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `novalidate` HTML attribute. */\n  novalidate?: Trackable<boolean | undefined>\n  /** The `noValidate` HTML attribute. */\n  noValidate?: Trackable<boolean | undefined>\n  /** The `rel` HTML attribute. */\n  rel?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'form' | 'none' | 'presentation' | 'search' | undefined>\n  /** The `target` HTML attribute. */\n  target?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<heading>` elements.\n */\nexport interface HeadingHTMLProps<eventTarget extends EventTarget = HTMLHeadingElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'heading' | 'none' | 'presentation' | 'tab' | 'doc-subtitle' | undefined>\n}\n\n/**\n * Props accepted by `<head>` elements.\n */\nexport interface HeadHTMLProps<eventTarget extends EventTarget = HTMLHeadElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<header>` elements.\n */\nexport interface HeaderHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'banner' | 'group' | 'none' | 'presentation' | undefined>\n}\n\n/**\n * Props accepted by `<hr>` elements.\n */\nexport interface HrHTMLProps<eventTarget extends EventTarget = HTMLHRElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'separator' | 'none' | 'presentation' | 'doc-pagebreak' | undefined>\n}\n\n/**\n * Props accepted by `<html>` elements.\n */\nexport interface HtmlHTMLProps<eventTarget extends EventTarget = HTMLHtmlElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'document' | undefined>\n}\n\n/**\n * Props accepted by `<iframe>` elements.\n */\nexport interface IframeHTMLProps<eventTarget extends EventTarget = HTMLIFrameElement>\n  extends HTMLProps<eventTarget> {\n  /** The `allow` HTML attribute. */\n  allow?: Trackable<string | undefined>\n  /** The `allowFullScreen` HTML attribute. */\n  allowFullScreen?: Trackable<boolean | undefined>\n  /** The `allowTransparency` HTML attribute. */\n  allowTransparency?: Trackable<boolean | undefined>\n  /** @deprecated */\n  frameborder?: Trackable<number | string | undefined>\n  /** @deprecated */\n  frameBorder?: Trackable<number | string | undefined>\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `loading` HTML attribute. */\n  loading?: Trackable<'eager' | 'lazy' | undefined>\n  /** @deprecated */\n  marginHeight?: Trackable<number | undefined>\n  /** @deprecated */\n  marginWidth?: Trackable<number | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `referrerpolicy` HTML attribute. */\n  referrerpolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `referrerPolicy` HTML attribute. */\n  referrerPolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'application' | 'document' | 'img' | 'none' | 'presentation' | undefined>\n  /** The `sandbox` HTML attribute. */\n  sandbox?: Trackable<string | undefined>\n  /** @deprecated */\n  scrolling?: Trackable<string | undefined>\n  /** The `seamless` HTML attribute. */\n  seamless?: Trackable<boolean | undefined>\n  /** The `src` HTML attribute. */\n  src?: Trackable<string | undefined>\n  /** The `srcdoc` HTML attribute. */\n  srcdoc?: Trackable<string | undefined>\n  /** The `srcDoc` HTML attribute. */\n  srcDoc?: Trackable<string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n}\n\nexport type HTMLAttributeCrossOrigin = 'anonymous' | 'use-credentials'\n\n/**\n * Props accepted by `<img>` elements.\n */\nexport interface PartialImgHTMLProps<eventTarget extends EventTarget>\n  extends HTMLProps<eventTarget> {\n  /** The `crossorigin` HTML attribute. */\n  crossorigin?: Trackable<HTMLAttributeCrossOrigin>\n  /** The `crossOrigin` HTML attribute. */\n  crossOrigin?: Trackable<HTMLAttributeCrossOrigin>\n  /** The `decoding` HTML attribute. */\n  decoding?: Trackable<'async' | 'auto' | 'sync' | undefined>\n  /** The `fetchpriority` HTML attribute. */\n  fetchpriority?: Trackable<'high' | 'auto' | 'low' | undefined>\n  /** The `fetchPriority` HTML attribute. */\n  fetchPriority?: Trackable<'high' | 'auto' | 'low' | undefined>\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `loading` HTML attribute. */\n  loading?: Trackable<'eager' | 'lazy' | undefined>\n  /** The `referrerpolicy` HTML attribute. */\n  referrerpolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `referrerPolicy` HTML attribute. */\n  referrerPolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `sizes` HTML attribute. */\n  sizes?: Trackable<string | undefined>\n  /** The `src` HTML attribute. */\n  src?: Trackable<string | undefined>\n  /** The `srcset` HTML attribute. */\n  srcset?: Trackable<string | undefined>\n  /** The `srcSet` HTML attribute. */\n  srcSet?: Trackable<string | undefined>\n  /** The `usemap` HTML attribute. */\n  usemap?: Trackable<string | undefined>\n  /** The `useMap` HTML attribute. */\n  useMap?: Trackable<string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n}\n\nexport type ImgAriaRolesAccessibleName = Trackable<\n  | 'img'\n  | 'button'\n  | 'checkbox'\n  | 'link'\n  | 'menuitem'\n  | 'menuitemcheckbox'\n  | 'menuitemradio'\n  | 'meter'\n  | 'option'\n  | 'progressbar'\n  | 'radio'\n  | 'scrollbar'\n  | 'separator'\n  | 'slider'\n  | 'switch'\n  | 'tab'\n  | 'treeitem'\n  | 'doc-cover'\n  | undefined\n>\n\nexport type ImgAriaRoles =\n  | {\n      'aria-label': Trackable<string>\n      role?: ImgAriaRolesAccessibleName\n    }\n  | {\n      'aria-labelledby': Trackable<string>\n      role?: ImgAriaRolesAccessibleName\n    }\n  | {\n      alt: Trackable<string>\n      role?: ImgAriaRolesAccessibleName\n    }\n  | {\n      title: Trackable<string>\n      role?: ImgAriaRolesAccessibleName\n    }\n  | {\n      'aria-label'?: never\n      'aria-labelledby'?: never\n      alt?: never\n      title?: never\n      role?: Trackable<'img' | 'none' | 'presentation' | undefined>\n    }\n\n/**\n * Props accepted by `<img>` elements.\n */\nexport type AccessibleImgHTMLProps<eventTarget extends EventTarget = HTMLImageElement> = Omit<\n  PartialImgHTMLProps<eventTarget>,\n  'role' | 'aria-label' | 'aria-labelledby' | 'title'\n> &\n  ImgAriaRoles\n\n/**\n * Props accepted by `<img>` elements.\n */\nexport interface ImgHTMLProps<eventTarget extends EventTarget = HTMLImageElement>\n  extends PartialImgHTMLProps<eventTarget> {\n  /** The `alt` HTML attribute. */\n  alt?: Trackable<string | undefined>\n  /** The `aria-label` HTML attribute. */\n  'aria-label'?: Trackable<string | undefined>\n  /** The `aria-labelledby` HTML attribute. */\n  'aria-labelledby'?: Trackable<string | undefined>\n  /** The `href` HTML attribute. */\n  href?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: ImgAriaRolesAccessibleName | Trackable<'img' | 'none' | 'presentation' | undefined>\n  /** The `title` HTML attribute. */\n  title?: Trackable<string | undefined>\n}\n\nexport type HTMLInputTypeAttribute =\n  | 'button'\n  | 'checkbox'\n  | 'color'\n  | 'date'\n  | 'datetime-local'\n  | 'email'\n  | 'file'\n  | 'hidden'\n  | 'image'\n  | 'month'\n  | 'number'\n  | 'password'\n  | 'radio'\n  | 'range'\n  | 'reset'\n  | 'search'\n  | 'submit'\n  | 'tel'\n  | 'text'\n  | 'time'\n  | 'url'\n  | 'week'\n\n/**\n * Props accepted by `<input>` elements.\n */\nexport interface PartialInputHTMLProps<eventTarget extends EventTarget>\n  extends HTMLProps<eventTarget> {\n  /** The `accept` HTML attribute. */\n  accept?: Trackable<string | undefined>\n  /** The `alt` HTML attribute. */\n  alt?: Trackable<string | undefined>\n  /** The `autocomplete` HTML attribute. */\n  autocomplete?: Trackable<string | undefined>\n  /** The `autoComplete` HTML attribute. */\n  autoComplete?: Trackable<string | undefined>\n  /** The `capture` HTML attribute. */\n  capture?: Trackable<'user' | 'environment' | undefined> // https://www.w3.org/TR/html-media-capture/#the-capture-attribute\n  /** The `checked` HTML attribute. */\n  checked?: Trackable<boolean | undefined>\n  /** The `defaultChecked` HTML attribute. */\n  defaultChecked?: Trackable<boolean | undefined>\n  /** The `defaultValue` HTML attribute. */\n  defaultValue?: Trackable<string | number | undefined>\n  /** The `disabled` HTML attribute. */\n  disabled?: Trackable<boolean | undefined>\n  /** The `enterKeyHint` HTML attribute. */\n  enterKeyHint?: Trackable<\n    'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined\n  >\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `formaction` HTML attribute. */\n  formaction?: Trackable<string | undefined>\n  /** The `formAction` HTML attribute. */\n  formAction?: Trackable<string | undefined>\n  /** The `formenctype` HTML attribute. */\n  formenctype?: Trackable<string | undefined>\n  /** The `formEncType` HTML attribute. */\n  formEncType?: Trackable<string | undefined>\n  /** The `formmethod` HTML attribute. */\n  formmethod?: Trackable<string | undefined>\n  /** The `formMethod` HTML attribute. */\n  formMethod?: Trackable<string | undefined>\n  /** The `formnovalidate` HTML attribute. */\n  formnovalidate?: Trackable<boolean | undefined>\n  /** The `formNoValidate` HTML attribute. */\n  formNoValidate?: Trackable<boolean | undefined>\n  /** The `formtarget` HTML attribute. */\n  formtarget?: Trackable<string | undefined>\n  /** The `formTarget` HTML attribute. */\n  formTarget?: Trackable<string | undefined>\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `indeterminate` HTML attribute. */\n  indeterminate?: Trackable<boolean | undefined>\n  /** The `max` HTML attribute. */\n  max?: Trackable<number | string | undefined>\n  /** The `maxlength` HTML attribute. */\n  maxlength?: Trackable<number | undefined>\n  /** The `maxLength` HTML attribute. */\n  maxLength?: Trackable<number | undefined>\n  /** The `min` HTML attribute. */\n  min?: Trackable<number | string | undefined>\n  /** The `minlength` HTML attribute. */\n  minlength?: Trackable<number | undefined>\n  /** The `minLength` HTML attribute. */\n  minLength?: Trackable<number | undefined>\n  /** The `multiple` HTML attribute. */\n  multiple?: Trackable<boolean | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `pattern` HTML attribute. */\n  pattern?: Trackable<string | undefined>\n  /** The `placeholder` HTML attribute. */\n  placeholder?: Trackable<string | undefined>\n  /** The `readonly` HTML attribute. */\n  readonly?: Trackable<boolean | undefined>\n  /** The `readOnly` HTML attribute. */\n  readOnly?: Trackable<boolean | undefined>\n  /** The `required` HTML attribute. */\n  required?: Trackable<boolean | undefined>\n  /** The `size` HTML attribute. */\n  size?: Trackable<number | undefined>\n  /** The `src` HTML attribute. */\n  src?: Trackable<string | undefined>\n  /** The `step` HTML attribute. */\n  step?: Trackable<number | string | undefined>\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n}\n\nexport type InputAriaRoles =\n  | {\n      type: Trackable<'button'>\n      role?: Trackable<\n        | 'button'\n        | 'checkbox'\n        | 'combobox'\n        | 'gridcell'\n        | 'link'\n        | 'menuitem'\n        | 'menuitemcheckbox'\n        | 'menuitemradio'\n        | 'option'\n        | 'radio'\n        | 'separator'\n        | 'slider'\n        | 'switch'\n        | 'tab'\n        | 'treeitem'\n        | undefined\n      >\n    }\n  | {\n      type: Trackable<'checkbox'>\n      role?: Trackable<'checkbox' | 'button' | 'menuitemcheckbox' | 'option' | 'switch' | undefined>\n    }\n  | {\n      type: Trackable<'email'>\n      list?: never\n      role?: Trackable<'textbox' | undefined>\n    }\n  | {\n      type: Trackable<'image'>\n      role?: Trackable<\n        | 'button'\n        | 'checkbox'\n        | 'gridcell'\n        | 'link'\n        | 'menuitem'\n        | 'menuitemcheckbox'\n        | 'menuitemradio'\n        | 'option'\n        | 'separator'\n        | 'slider'\n        | 'switch'\n        | 'tab'\n        | 'treeitem'\n        | undefined\n      >\n    }\n  | {\n      type: Trackable<'number'>\n      role?: Trackable<'spinbutton' | undefined>\n    }\n  | {\n      type: Trackable<'radio'>\n      role?: Trackable<'radio' | 'menuitemradio' | undefined>\n    }\n  | {\n      type: Trackable<'range'>\n      role?: Trackable<'slider' | undefined>\n    }\n  | {\n      type: Trackable<'reset'>\n      role?: Trackable<\n        | 'button'\n        | 'checkbox'\n        | 'combobox'\n        | 'gridcell'\n        | 'link'\n        | 'menuitem'\n        | 'menuitemcheckbox'\n        | 'menuitemradio'\n        | 'option'\n        | 'radio'\n        | 'separator'\n        | 'slider'\n        | 'switch'\n        | 'tab'\n        | 'treeitem'\n        | undefined\n      >\n    }\n  | {\n      type: Trackable<'search'>\n      list?: never\n      role?: Trackable<'searchbox' | undefined>\n    }\n  | {\n      type: Trackable<'submit'>\n      role?: Trackable<\n        | 'button'\n        | 'checkbox'\n        | 'combobox'\n        | 'gridcell'\n        | 'link'\n        | 'menuitem'\n        | 'menuitemcheckbox'\n        | 'menuitemradio'\n        | 'option'\n        | 'radio'\n        | 'separator'\n        | 'slider'\n        | 'switch'\n        | 'tab'\n        | 'treeitem'\n        | undefined\n      >\n    }\n  | {\n      type: Trackable<'tel'>\n      list?: never\n      role?: Trackable<'textbox' | undefined>\n    }\n  | {\n      type?: Trackable<'text'>\n      list?: never\n      role?: Trackable<'textbox' | 'combobox' | 'searchbox' | 'spinbutton' | undefined>\n    }\n  | {\n      type?: Trackable<'text' | 'search' | 'tel' | 'url' | 'email'>\n      list: Trackable<string | undefined>\n      role?: Trackable<'combobox' | undefined>\n    }\n  | {\n      type: Trackable<'url'>\n      list?: never\n      role?: Trackable<'textbox' | undefined>\n    }\n  | {\n      type: Trackable<\n        | 'color'\n        | 'date'\n        | 'datetime-local'\n        | 'file'\n        | 'hidden'\n        | 'month'\n        | 'password'\n        | 'time'\n        | 'week'\n      >\n      role?: never\n    }\n\n/**\n * Props accepted by `<input>` elements.\n */\nexport type AccessibleInputHTMLProps<eventTarget extends EventTarget = HTMLInputElement> = Omit<\n  PartialInputHTMLProps<eventTarget>,\n  'role'\n> &\n  InputAriaRoles\n\n/**\n * Props accepted by `<input>` elements.\n */\nexport interface InputHTMLProps<eventTarget extends EventTarget = HTMLInputElement>\n  extends PartialInputHTMLProps<eventTarget> {\n  /** The `type` HTML attribute. */\n  type?: Trackable<HTMLInputTypeAttribute | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<\n    | 'button'\n    | 'checkbox'\n    | 'combobox'\n    | 'gridcell'\n    | 'link'\n    | 'menuitem'\n    | 'menuitemcheckbox'\n    | 'menuitemradio'\n    | 'option'\n    | 'radio'\n    | 'searchbox'\n    | 'separator'\n    | 'slider'\n    | 'spinbutton'\n    | 'switch'\n    | 'tab'\n    | 'textbox'\n    | 'treeitem'\n    | undefined\n  >\n}\n\n/**\n * Props accepted by `<ins>` elements.\n */\nexport interface InsHTMLProps<eventTarget extends EventTarget = HTMLModElement>\n  extends HTMLProps<eventTarget> {\n  /** The `cite` HTML attribute. */\n  cite?: Trackable<string | undefined>\n  /** The `datetime` HTML attribute. */\n  datetime?: Trackable<string | undefined>\n  /** The `dateTime` HTML attribute. */\n  dateTime?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<keygen>` elements.\n */\nexport interface KeygenHTMLProps<eventTarget extends EventTarget = HTMLUnknownElement>\n  extends HTMLProps<eventTarget> {\n  /** The `challenge` HTML attribute. */\n  challenge?: Trackable<string | undefined>\n  /** The `disabled` HTML attribute. */\n  disabled?: Trackable<boolean | undefined>\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `keyType` HTML attribute. */\n  keyType?: Trackable<string | undefined>\n  /** The `keyParams` HTML attribute. */\n  keyParams?: Trackable<string | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<label>` elements.\n */\nexport interface LabelHTMLProps<eventTarget extends EventTarget = HTMLLabelElement>\n  extends HTMLProps<eventTarget> {\n  /** The `for` HTML attribute. */\n  for?: Trackable<string | undefined>\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `htmlFor` HTML attribute. */\n  htmlFor?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<legend>` elements.\n */\nexport interface LegendHTMLProps<eventTarget extends EventTarget = HTMLLegendElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<li>` elements.\n */\nexport interface LiHTMLProps<eventTarget extends EventTarget = HTMLLIElement>\n  extends HTMLProps<eventTarget> {\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n}\n\n/**\n * Props accepted by `<link>` elements.\n */\nexport interface LinkHTMLProps<eventTarget extends EventTarget = HTMLLinkElement>\n  extends HTMLProps<eventTarget> {\n  /** The `as` HTML attribute. */\n  as?: Trackable<string | undefined>\n  /** The `crossorigin` HTML attribute. */\n  crossorigin?: Trackable<HTMLAttributeCrossOrigin>\n  /** The `crossOrigin` HTML attribute. */\n  crossOrigin?: Trackable<HTMLAttributeCrossOrigin>\n  /** The `fetchpriority` HTML attribute. */\n  fetchpriority?: Trackable<'high' | 'low' | 'auto' | undefined>\n  /** The `fetchPriority` HTML attribute. */\n  fetchPriority?: Trackable<'high' | 'low' | 'auto' | undefined>\n  /** The `href` HTML attribute. */\n  href?: Trackable<string | undefined>\n  /** The `hreflang` HTML attribute. */\n  hreflang?: Trackable<string | undefined>\n  /** The `hrefLang` HTML attribute. */\n  hrefLang?: Trackable<string | undefined>\n  /** The `integrity` HTML attribute. */\n  integrity?: Trackable<string | undefined>\n  /** The `media` HTML attribute. */\n  media?: Trackable<string | undefined>\n  /** The `imageSrcSet` HTML attribute. */\n  imageSrcSet?: Trackable<string | undefined>\n  /** The `referrerpolicy` HTML attribute. */\n  referrerpolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `referrerPolicy` HTML attribute. */\n  referrerPolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `rel` HTML attribute. */\n  rel?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n  /** The `sizes` HTML attribute. */\n  sizes?: Trackable<string | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<string | undefined>\n  /** The `charset` HTML attribute. */\n  charset?: Trackable<string | undefined>\n  /** The `charSet` HTML attribute. */\n  charSet?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<main>` elements.\n */\nexport interface MainHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'main' | undefined>\n}\n\n/**\n * Props accepted by `<map>` elements.\n */\nexport interface MapHTMLProps<eventTarget extends EventTarget = HTMLMapElement>\n  extends HTMLProps<eventTarget> {\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<marquee>` elements.\n */\nexport interface MarqueeHTMLProps<eventTarget extends EventTarget = HTMLMarqueeElement>\n  extends HTMLProps<eventTarget> {\n  /** The `behavior` HTML attribute. */\n  behavior?: Trackable<'scroll' | 'slide' | 'alternate' | undefined>\n  /** The `bgColor` HTML attribute. */\n  bgColor?: Trackable<string | undefined>\n  /** The `direction` HTML attribute. */\n  direction?: Trackable<'left' | 'right' | 'up' | 'down' | undefined>\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `hspace` HTML attribute. */\n  hspace?: Trackable<number | string | undefined>\n  /** The `loop` HTML attribute. */\n  loop?: Trackable<number | string | undefined>\n  /** The `scrollAmount` HTML attribute. */\n  scrollAmount?: Trackable<number | string | undefined>\n  /** The `scrollDelay` HTML attribute. */\n  scrollDelay?: Trackable<number | string | undefined>\n  /** The `trueSpeed` HTML attribute. */\n  trueSpeed?: Trackable<boolean | undefined>\n  /** The `vspace` HTML attribute. */\n  vspace?: Trackable<number | string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n}\n\n/**\n * Props accepted by `<media>` elements.\n */\nexport interface MediaHTMLProps<eventTarget extends EventTarget = HTMLMediaElement>\n  extends HTMLProps<eventTarget> {\n  /** The `autoplay` HTML attribute. */\n  autoplay?: Trackable<boolean | undefined>\n  /** The `autoPlay` HTML attribute. */\n  autoPlay?: Trackable<boolean | undefined>\n  /** The `controls` HTML attribute. */\n  controls?: Trackable<boolean | undefined>\n  /** The `controlslist` HTML attribute. */\n  controlslist?: Trackable<string | undefined>\n  /** The `controlsList` HTML attribute. */\n  controlsList?: Trackable<string | undefined>\n  /** The `crossorigin` HTML attribute. */\n  crossorigin?: Trackable<HTMLAttributeCrossOrigin>\n  /** The `crossOrigin` HTML attribute. */\n  crossOrigin?: Trackable<HTMLAttributeCrossOrigin>\n  /** The `currentTime` HTML attribute. */\n  currentTime?: Trackable<number | undefined>\n  /** The `defaultMuted` HTML attribute. */\n  defaultMuted?: Trackable<boolean | undefined>\n  /** The `defaultPlaybackRate` HTML attribute. */\n  defaultPlaybackRate?: Trackable<number | undefined>\n  /** The `disableremoteplayback` HTML attribute. */\n  disableremoteplayback?: Trackable<boolean | undefined>\n  /** The `disableRemotePlayback` HTML attribute. */\n  disableRemotePlayback?: Trackable<boolean | undefined>\n  /** The `loop` HTML attribute. */\n  loop?: Trackable<boolean | undefined>\n  /** The `mediaGroup` HTML attribute. */\n  mediaGroup?: Trackable<string | undefined>\n  /** The `muted` HTML attribute. */\n  muted?: Trackable<boolean | undefined>\n  /** The `playbackRate` HTML attribute. */\n  playbackRate?: Trackable<number | undefined>\n  /** The `preload` HTML attribute. */\n  preload?: Trackable<'auto' | 'metadata' | 'none' | undefined>\n  /** The `preservesPitch` HTML attribute. */\n  preservesPitch?: Trackable<boolean | undefined>\n  /** The `src` HTML attribute. */\n  src?: Trackable<string | undefined>\n  /** The `srcObject` HTML attribute. */\n  srcObject?: Trackable<MediaStream | MediaSource | Blob | File | null>\n  /** The `volume` HTML attribute. */\n  volume?: Trackable<string | number | undefined>\n}\n\n/**\n * Props accepted by `<menu>` elements.\n */\nexport interface MenuHTMLProps<eventTarget extends EventTarget = HTMLMenuElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role:\n    | 'list'\n    | 'group'\n    | 'listbox'\n    | 'menu'\n    | 'menubar'\n    | 'none'\n    | 'presentation'\n    | 'radiogroup'\n    | 'tablist'\n    | 'toolbar'\n    | 'tree'\n  /** The `type` HTML attribute. */\n  type?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<meta>` elements.\n */\nexport interface MetaHTMLProps<eventTarget extends EventTarget = HTMLMetaElement>\n  extends HTMLProps<eventTarget> {\n  /** The `charset` HTML attribute. */\n  charset?: Trackable<string | undefined>\n  /** The `charSet` HTML attribute. */\n  charSet?: Trackable<string | undefined>\n  /** The `content` HTML attribute. */\n  content?: Trackable<string | undefined>\n  /** The `http-equiv` HTML attribute. */\n  'http-equiv'?: Trackable<string | undefined>\n  /** The `httpEquiv` HTML attribute. */\n  httpEquiv?: Trackable<string | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `media` HTML attribute. */\n  media?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<meter>` elements.\n */\nexport interface MeterHTMLProps<eventTarget extends EventTarget = HTMLMeterElement>\n  extends HTMLProps<eventTarget> {\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `high` HTML attribute. */\n  high?: Trackable<number | undefined>\n  /** The `low` HTML attribute. */\n  low?: Trackable<number | undefined>\n  /** The `max` HTML attribute. */\n  max?: Trackable<number | string | undefined>\n  /** The `min` HTML attribute. */\n  min?: Trackable<number | string | undefined>\n  /** The `optimum` HTML attribute. */\n  optimum?: Trackable<number | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'meter' | undefined>\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n}\n\n/**\n * Props accepted by `<nav>` elements.\n */\nexport interface NavHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<\n    'navigation' | 'menu' | 'menubar' | 'none' | 'presentation' | 'tablist' | undefined\n  >\n}\n\n/**\n * Props accepted by `<noscript>` elements.\n */\nexport interface NoScriptHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<object>` elements.\n */\nexport interface ObjectHTMLProps<eventTarget extends EventTarget = HTMLObjectElement>\n  extends HTMLProps<eventTarget> {\n  /** The `classID` HTML attribute. */\n  classID?: Trackable<string | undefined>\n  /** The `data` HTML attribute. */\n  data?: Trackable<string | undefined>\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'application' | 'document' | 'img' | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<string | undefined>\n  /** The `usemap` HTML attribute. */\n  usemap?: Trackable<string | undefined>\n  /** The `useMap` HTML attribute. */\n  useMap?: Trackable<string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n  /** The `wmode` HTML attribute. */\n  wmode?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<ol>` elements.\n */\nexport interface OlHTMLProps<eventTarget extends EventTarget = HTMLOListElement>\n  extends HTMLProps<eventTarget> {\n  /** The `reversed` HTML attribute. */\n  reversed?: Trackable<boolean | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<\n    | 'list'\n    | 'group'\n    | 'listbox'\n    | 'menu'\n    | 'menubar'\n    | 'none'\n    | 'presentation'\n    | 'radiogroup'\n    | 'tablist'\n    | 'toolbar'\n    | 'tree'\n    | undefined\n  >\n  /** The `start` HTML attribute. */\n  start?: Trackable<number | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<'1' | 'a' | 'A' | 'i' | 'I' | undefined>\n}\n\n/**\n * Props accepted by `<optgroup>` elements.\n */\nexport interface OptgroupHTMLProps<eventTarget extends EventTarget = HTMLOptGroupElement>\n  extends HTMLProps<eventTarget> {\n  /** The `disabled` HTML attribute. */\n  disabled?: Trackable<boolean | undefined>\n  /** The `label` HTML attribute. */\n  label?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'group' | undefined>\n}\n\n/**\n * Props accepted by `<option>` elements.\n */\nexport interface OptionHTMLProps<eventTarget extends EventTarget = HTMLOptionElement>\n  extends HTMLProps<eventTarget> {\n  /** The `disabled` HTML attribute. */\n  disabled?: Trackable<boolean | undefined>\n  /** The `label` HTML attribute. */\n  label?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'option' | undefined>\n  /** The `selected` HTML attribute. */\n  selected?: Trackable<boolean | undefined>\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n}\n\n/**\n * Props accepted by `<output>` elements.\n */\nexport interface OutputHTMLProps<eventTarget extends EventTarget = HTMLOutputElement>\n  extends HTMLProps<eventTarget> {\n  /** The `for` HTML attribute. */\n  for?: Trackable<string | undefined>\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `htmlFor` HTML attribute. */\n  htmlFor?: Trackable<string | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<param>` elements.\n */\nexport interface ParamHTMLProps<eventTarget extends EventTarget = HTMLParamElement>\n  extends HTMLProps<eventTarget> {\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n}\n\n/**\n * Props accepted by `<picture>` elements.\n */\nexport interface PictureHTMLProps<eventTarget extends EventTarget = HTMLPictureElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<progress>` elements.\n */\nexport interface ProgressHTMLProps<eventTarget extends EventTarget = HTMLProgressElement>\n  extends HTMLProps<eventTarget> {\n  /** The `max` HTML attribute. */\n  max?: Trackable<number | string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'progressbar' | undefined>\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n}\n\n/**\n * Props accepted by `<quote>` elements.\n */\nexport interface QuoteHTMLProps<eventTarget extends EventTarget = HTMLQuoteElement>\n  extends HTMLProps<eventTarget> {\n  /** The `cite` HTML attribute. */\n  cite?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<script>` elements.\n */\nexport interface ScriptHTMLProps<eventTarget extends EventTarget = HTMLScriptElement>\n  extends HTMLProps<eventTarget> {\n  /** The `async` HTML attribute. */\n  async?: Trackable<boolean | undefined>\n  /** @deprecated */\n  charset?: Trackable<string | undefined>\n  /** @deprecated */\n  charSet?: Trackable<string | undefined>\n  /** The `crossorigin` HTML attribute. */\n  crossorigin?: Trackable<HTMLAttributeCrossOrigin>\n  /** The `crossOrigin` HTML attribute. */\n  crossOrigin?: Trackable<HTMLAttributeCrossOrigin>\n  /** The `defer` HTML attribute. */\n  defer?: Trackable<boolean | undefined>\n  /** The `integrity` HTML attribute. */\n  integrity?: Trackable<string | undefined>\n  /** The `nomodule` HTML attribute. */\n  nomodule?: Trackable<boolean | undefined>\n  /** The `noModule` HTML attribute. */\n  noModule?: Trackable<boolean | undefined>\n  /** The `referrerpolicy` HTML attribute. */\n  referrerpolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `referrerPolicy` HTML attribute. */\n  referrerPolicy?: Trackable<HTMLAttributeReferrerPolicy | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n  /** The `src` HTML attribute. */\n  src?: Trackable<string | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<search>` elements.\n */\nexport interface SearchHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'search' | 'form' | 'group' | 'none' | 'presentation' | 'region' | undefined>\n}\n\n/**\n * Props accepted by `<select>` elements.\n */\nexport interface PartialSelectHTMLProps<eventTarget extends EventTarget>\n  extends HTMLProps<eventTarget> {\n  /** The `autocomplete` HTML attribute. */\n  autocomplete?: Trackable<string | undefined>\n  /** The `autoComplete` HTML attribute. */\n  autoComplete?: Trackable<string | undefined>\n  /** The `defaultValue` HTML attribute. */\n  defaultValue?: Trackable<string | number | undefined>\n  /** The `disabled` HTML attribute. */\n  disabled?: Trackable<boolean | undefined>\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `required` HTML attribute. */\n  required?: Trackable<boolean | undefined>\n  /** The `size` HTML attribute. */\n  size?: Trackable<number | undefined>\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n}\n\nexport type SelectAriaRoles =\n  | {\n      multiple?: never\n      // Spec states this branch is limited to \"no `multiple` attribute AND no `size` attribute greater than 1\".\n      // `1` as a default, however, caused some web compat issues and forced Firefox to default to `0` instead.\n      size?: 0 | 1 | never\n      role?: Trackable<'combobox' | 'menu' | undefined>\n    }\n  | {\n      multiple?: Trackable<boolean | undefined>\n      size?: Trackable<number | undefined>\n      role?: Trackable<'listbox' | undefined>\n    }\n\n/**\n * Props accepted by `<select>` elements.\n */\nexport type AccessibleSelectHTMLProps<eventTarget extends EventTarget = HTMLSelectElement> = Omit<\n  PartialSelectHTMLProps<eventTarget>,\n  'role'\n> &\n  SelectAriaRoles\n\n/**\n * Props accepted by `<select>` elements.\n */\nexport interface SelectHTMLProps<eventTarget extends EventTarget = HTMLSelectElement>\n  extends PartialSelectHTMLProps<eventTarget> {\n  /** The `multiple` HTML attribute. */\n  multiple?: Trackable<boolean | undefined>\n  /** The `size` HTML attribute. */\n  size?: Trackable<number | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<HTMLInputTypeAttribute | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'combobox' | 'listbox' | 'menu' | undefined>\n}\n\n/**\n * Props accepted by `<slot>` elements.\n */\nexport interface SlotHTMLProps<eventTarget extends EventTarget = HTMLSlotElement>\n  extends HTMLProps<eventTarget> {\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<source>` elements.\n */\nexport interface SourceHTMLProps<eventTarget extends EventTarget = HTMLSourceElement>\n  extends HTMLProps<eventTarget> {\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `media` HTML attribute. */\n  media?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n  /** The `sizes` HTML attribute. */\n  sizes?: Trackable<string | undefined>\n  /** The `src` HTML attribute. */\n  src?: Trackable<string | undefined>\n  /** The `srcset` HTML attribute. */\n  srcset?: Trackable<string | undefined>\n  /** The `srcSet` HTML attribute. */\n  srcSet?: Trackable<string | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n}\n\n/**\n * Props accepted by `<style>` elements.\n */\nexport interface StyleHTMLProps<eventTarget extends EventTarget = HTMLStyleElement>\n  extends HTMLProps<eventTarget> {\n  /** The `media` HTML attribute. */\n  media?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n  /** The `scoped` HTML attribute. */\n  scoped?: Trackable<boolean | undefined>\n  /** The `type` HTML attribute. */\n  type?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<table>` elements.\n */\nexport interface TableHTMLProps<eventTarget extends EventTarget = HTMLTableElement>\n  extends HTMLProps<eventTarget> {\n  /** The `cellPadding` HTML attribute. */\n  cellPadding?: Trackable<string | undefined>\n  /** The `cellSpacing` HTML attribute. */\n  cellSpacing?: Trackable<string | undefined>\n  /** The `summary` HTML attribute. */\n  summary?: Trackable<string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n}\n\n/**\n * Props accepted by `<td>` elements.\n */\nexport interface TdHTMLProps<eventTarget extends EventTarget = HTMLTableCellElement>\n  extends HTMLProps<eventTarget> {\n  /** The `align` HTML attribute. */\n  align?: Trackable<'left' | 'center' | 'right' | 'justify' | 'char' | undefined>\n  /** The `colspan` HTML attribute. */\n  colspan?: Trackable<number | undefined>\n  /** The `colSpan` HTML attribute. */\n  colSpan?: Trackable<number | undefined>\n  /** The `headers` HTML attribute. */\n  headers?: Trackable<string | undefined>\n  /** The `rowspan` HTML attribute. */\n  rowspan?: Trackable<number | undefined>\n  /** The `rowSpan` HTML attribute. */\n  rowSpan?: Trackable<number | undefined>\n  /** The `scope` HTML attribute. */\n  scope?: Trackable<string | undefined>\n  /** The `abbr` HTML attribute. */\n  abbr?: Trackable<string | undefined>\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n  /** The `valign` HTML attribute. */\n  valign?: Trackable<'top' | 'middle' | 'bottom' | 'baseline' | undefined>\n}\n\n/**\n * Props accepted by `<template>` elements.\n */\nexport interface TemplateHTMLProps<eventTarget extends EventTarget = HTMLTemplateElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<textarea>` elements.\n */\nexport interface TextareaHTMLProps<eventTarget extends EventTarget = HTMLTextAreaElement>\n  extends HTMLProps<eventTarget> {\n  /** The `autocomplete` HTML attribute. */\n  autocomplete?: Trackable<string | undefined>\n  /** The `autoComplete` HTML attribute. */\n  autoComplete?: Trackable<string | undefined>\n  /** The `cols` HTML attribute. */\n  cols?: Trackable<number | undefined>\n  /** The `defaultValue` HTML attribute. */\n  defaultValue?: Trackable<string | number | undefined>\n  /** The `dirName` HTML attribute. */\n  dirName?: Trackable<string | undefined>\n  /** The `disabled` HTML attribute. */\n  disabled?: Trackable<boolean | undefined>\n  /** The `form` HTML attribute. */\n  form?: Trackable<string | undefined>\n  /** The `maxlength` HTML attribute. */\n  maxlength?: Trackable<number | undefined>\n  /** The `maxLength` HTML attribute. */\n  maxLength?: Trackable<number | undefined>\n  /** The `minlength` HTML attribute. */\n  minlength?: Trackable<number | undefined>\n  /** The `minLength` HTML attribute. */\n  minLength?: Trackable<number | undefined>\n  /** The `name` HTML attribute. */\n  name?: Trackable<string | undefined>\n  /** The `placeholder` HTML attribute. */\n  placeholder?: Trackable<string | undefined>\n  /** The `readOnly` HTML attribute. */\n  readOnly?: Trackable<boolean | undefined>\n  /** The `required` HTML attribute. */\n  required?: Trackable<boolean | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'textbox' | undefined>\n  /** The `rows` HTML attribute. */\n  rows?: Trackable<number | undefined>\n  /** The `value` HTML attribute. */\n  value?: Trackable<string | number | undefined>\n  /** The `wrap` HTML attribute. */\n  wrap?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<th>` elements.\n */\nexport interface ThHTMLProps<eventTarget extends EventTarget = HTMLTableCellElement>\n  extends HTMLProps<eventTarget> {\n  /** The `align` HTML attribute. */\n  align?: Trackable<'left' | 'center' | 'right' | 'justify' | 'char' | undefined>\n  /** The `colspan` HTML attribute. */\n  colspan?: Trackable<number | undefined>\n  /** The `colSpan` HTML attribute. */\n  colSpan?: Trackable<number | undefined>\n  /** The `headers` HTML attribute. */\n  headers?: Trackable<string | undefined>\n  /** The `rowspan` HTML attribute. */\n  rowspan?: Trackable<number | undefined>\n  /** The `rowSpan` HTML attribute. */\n  rowSpan?: Trackable<number | undefined>\n  /** The `scope` HTML attribute. */\n  scope?: Trackable<string | undefined>\n  /** The `abbr` HTML attribute. */\n  abbr?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<time>` elements.\n */\nexport interface TimeHTMLProps<eventTarget extends EventTarget = HTMLTimeElement>\n  extends HTMLProps<eventTarget> {\n  /** The `datetime` HTML attribute. */\n  datetime?: Trackable<string | undefined>\n  /** The `dateTime` HTML attribute. */\n  dateTime?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<title>` elements.\n */\nexport interface TitleHTMLProps<eventTarget extends EventTarget = HTMLTitleElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: never\n}\n\n/**\n * Props accepted by `<track>` elements.\n */\nexport interface TrackHTMLProps<eventTarget extends EventTarget = HTMLTrackElement>\n  extends MediaHTMLProps<eventTarget> {\n  /** The `default` HTML attribute. */\n  default?: Trackable<boolean | undefined>\n  /** The `kind` HTML attribute. */\n  kind?: Trackable<string | undefined>\n  /** The `label` HTML attribute. */\n  label?: Trackable<string | undefined>\n  /** The `role` HTML attribute. */\n  role?: never\n  /** The `srclang` HTML attribute. */\n  srclang?: Trackable<string | undefined>\n  /** The `srcLang` HTML attribute. */\n  srcLang?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<ul>` elements.\n */\nexport interface UlHTMLProps<eventTarget extends EventTarget = HTMLUListElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<\n    | 'list'\n    | 'group'\n    | 'listbox'\n    | 'menu'\n    | 'menubar'\n    | 'none'\n    | 'presentation'\n    | 'radiogroup'\n    | 'tablist'\n    | 'toolbar'\n    | 'tree'\n    | undefined\n  >\n}\n\n/**\n * Props accepted by `<video>` elements.\n */\nexport interface VideoHTMLProps<eventTarget extends EventTarget = HTMLVideoElement>\n  extends MediaHTMLProps<eventTarget> {\n  /** The `disablePictureInPicture` HTML attribute. */\n  disablePictureInPicture?: Trackable<boolean | undefined>\n  /** The `height` HTML attribute. */\n  height?: Trackable<number | string | undefined>\n  /** The `playsinline` HTML attribute. */\n  playsinline?: Trackable<boolean | undefined>\n  /** The `playsInline` HTML attribute. */\n  playsInline?: Trackable<boolean | undefined>\n  /** The `poster` HTML attribute. */\n  poster?: Trackable<string | undefined>\n  /** The `width` HTML attribute. */\n  width?: Trackable<number | string | undefined>\n  /** The `role` HTML attribute. */\n  role?: Trackable<'application' | undefined>\n}\n\n/**\n * Props accepted by `<wbr>` elements.\n */\nexport interface WbrHTMLProps<eventTarget extends EventTarget = HTMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `role` HTML attribute. */\n  role?: Trackable<'none' | 'presentation' | undefined>\n}\n\n/**\n * Props accepted by `<detailed>` elements.\n */\nexport type DetailedHTMLProps<\n  HA extends HTMLProps<RefType>,\n  RefType extends EventTarget = EventTarget,\n> = HA\n\n/**\n * Props accepted by MathML elements.\n */\nexport interface MathMLProps<eventTarget extends EventTarget = MathMLElement>\n  extends HTMLProps<eventTarget> {\n  /** The `dir` MathML attribute. */\n  dir?: Trackable<'ltr' | 'rtl' | undefined>\n  /** The `displaystyle` MathML attribute. */\n  displaystyle?: Trackable<boolean | undefined>\n  /** @deprecated This feature is non-standard. See https://developer.mozilla.org/en-US/docs/Web/MathML/Global_attributes/href  */\n  href?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Global_attributes/mathbackground */\n  mathbackground?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Global_attributes/mathcolor */\n  mathcolor?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Global_attributes/mathsize */\n  mathsize?: Trackable<string | undefined>\n  /** The `nonce` MathML attribute. */\n  nonce?: Trackable<string | undefined>\n  /** The `scriptlevel` MathML attribute. */\n  scriptlevel?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<annotation>` MathML elements.\n */\nexport interface AnnotationMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** The `encoding` MathML attribute. */\n  encoding?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/semantics#src */\n  src?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<annotation-xml>` MathML elements.\n */\nexport interface AnnotationXmlMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** The `encoding` MathML attribute. */\n  encoding?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/semantics#src */\n  src?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<maction>` MathML elements.\n */\nexport interface MActionMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/maction#actiontype */\n  actiontype?: Trackable<'statusline' | 'toggle' | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/maction#selection */\n  selection?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<math>` MathML elements.\n */\nexport interface MathMathMLProps<eventTarget extends EventTarget> extends MathMLProps<eventTarget> {\n  /** The `display` MathML attribute. */\n  display?: Trackable<'block' | 'inline' | undefined>\n}\n\n/**\n * Props accepted by `<menclose>` MathML elements.\n */\nexport interface MEncloseMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** The `notation` MathML attribute. */\n  notation?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<merror>` MathML elements.\n */\nexport interface MErrorMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {}\n\n/**\n * Props accepted by `<mfenced>` MathML elements.\n */\nexport interface MFencedMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** The `close` MathML attribute. */\n  close?: Trackable<string | undefined>\n  /** The `open` MathML attribute. */\n  open?: Trackable<string | undefined>\n  /** The `separators` MathML attribute. */\n  separators?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<mfrac>` MathML elements.\n */\nexport interface MFracMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mfrac#denomalign */\n  denomalign?: Trackable<'center' | 'left' | 'right' | undefined>\n  /** The `linethickness` MathML attribute. */\n  linethickness?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mfrac#numalign */\n  numalign?: Trackable<'center' | 'left' | 'right' | undefined>\n}\n\n/**\n * Props accepted by `<mi>` MathML elements.\n */\nexport interface MiMathMLProps<eventTarget extends EventTarget> extends MathMLProps<eventTarget> {\n  /**\n   * The only value allowed in the current specification is normal (case insensitive)\n   * See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mi#mathvariant\n   */\n  mathvariant?: Trackable<\n    | 'normal'\n    | 'bold'\n    | 'italic'\n    | 'bold-italic'\n    | 'double-struck'\n    | 'bold-fraktur'\n    | 'script'\n    | 'bold-script'\n    | 'fraktur'\n    | 'sans-serif'\n    | 'bold-sans-serif'\n    | 'sans-serif-italic'\n    | 'sans-serif-bold-italic'\n    | 'monospace'\n    | 'initial'\n    | 'tailed'\n    | 'looped'\n    | 'stretched'\n    | undefined\n  >\n}\n\n/**\n * Props accepted by `<mmultiscripts>` MathML elements.\n */\nexport interface MmultiScriptsMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mmultiscripts#subscriptshift */\n  subscriptshift?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mmultiscripts#superscriptshift */\n  superscriptshift?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<mn>` MathML elements.\n */\nexport interface MNMathMLProps<eventTarget extends EventTarget> extends MathMLProps<eventTarget> {}\n\n/**\n * Props accepted by `<mo>` MathML elements.\n */\nexport interface MOMathMLProps<eventTarget extends EventTarget> extends MathMLProps<eventTarget> {\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mo#accent */\n  accent?: Trackable<boolean | undefined>\n  /** The `fence` MathML attribute. */\n  fence?: Trackable<boolean | undefined>\n  /** The `largeop` MathML attribute. */\n  largeop?: Trackable<boolean | undefined>\n  /** The `lspace` MathML attribute. */\n  lspace?: Trackable<string | undefined>\n  /** The `maxsize` MathML attribute. */\n  maxsize?: Trackable<string | undefined>\n  /** The `minsize` MathML attribute. */\n  minsize?: Trackable<string | undefined>\n  /** The `movablelimits` MathML attribute. */\n  movablelimits?: Trackable<boolean | undefined>\n  /** The `rspace` MathML attribute. */\n  rspace?: Trackable<string | undefined>\n  /** The `separator` MathML attribute. */\n  separator?: Trackable<boolean | undefined>\n  /** The `stretchy` MathML attribute. */\n  stretchy?: Trackable<boolean | undefined>\n  /** The `symmetric` MathML attribute. */\n  symmetric?: Trackable<boolean | undefined>\n}\n\n/**\n * Props accepted by `<mover>` MathML elements.\n */\nexport interface MOverMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** The `accent` MathML attribute. */\n  accent?: Trackable<boolean | undefined>\n}\n\n/**\n * Props accepted by `<mpadded>` MathML elements.\n */\nexport interface MPaddedMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** The `depth` MathML attribute. */\n  depth?: Trackable<string | undefined>\n  /** The `height` MathML attribute. */\n  height?: Trackable<string | undefined>\n  /** The `lspace` MathML attribute. */\n  lspace?: Trackable<string | undefined>\n  /** The `voffset` MathML attribute. */\n  voffset?: Trackable<string | undefined>\n  /** The `width` MathML attribute. */\n  width?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<mphantom>` MathML elements.\n */\nexport interface MPhantomMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {}\n\n/**\n * Props accepted by `<mprescripts>` MathML elements.\n */\nexport interface MPrescriptsMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {}\n\n/**\n * Props accepted by `<mroot>` MathML elements.\n */\nexport interface MRootMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {}\n\n/**\n * Props accepted by `<mrow>` MathML elements.\n */\nexport interface MRowMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {}\n\n/**\n * Props accepted by `<ms>` MathML elements.\n */\nexport interface MSMathMLProps<eventTarget extends EventTarget> extends MathMLProps<eventTarget> {\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/ms#browser_compatibility */\n  lquote?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/ms#browser_compatibility */\n  rquote?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<mspace>` MathML elements.\n */\nexport interface MSpaceMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** The `depth` MathML attribute. */\n  depth?: Trackable<string | undefined>\n  /** The `height` MathML attribute. */\n  height?: Trackable<string | undefined>\n  /** The `width` MathML attribute. */\n  width?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<msqrt>` MathML elements.\n */\nexport interface MSqrtMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {}\n\n/**\n * Props accepted by `<mstyle>` MathML elements.\n */\nexport interface MStyleMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mstyle#background */\n  background?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mstyle#color */\n  color?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mstyle#fontsize */\n  fontsize?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mstyle#fontstyle */\n  fontstyle?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mstyle#fontweight */\n  fontweight?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mstyle#scriptminsize */\n  scriptminsize?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mstyle#scriptsizemultiplier */\n  scriptsizemultiplier?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<msub>` MathML elements.\n */\nexport interface MSubMathMLProps<eventTarget extends EventTarget> extends MathMLProps<eventTarget> {\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/msub#subscriptshift */\n  subscriptshift?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<msubsup>` MathML elements.\n */\nexport interface MSubsupMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/msubsup#subscriptshift */\n  subscriptshift?: Trackable<string | undefined>\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/msubsup#superscriptshift */\n  superscriptshift?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<msup>` MathML elements.\n */\nexport interface MSupMathMLProps<eventTarget extends EventTarget> extends MathMLProps<eventTarget> {\n  /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/msup#superscriptshift */\n  superscriptshift?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<mtable>` MathML elements.\n */\nexport interface MTableMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#align */\n  align?: Trackable<'axis' | 'baseline' | 'bottom' | 'center' | 'top' | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#columnalign */\n  columnalign?: Trackable<'center' | 'left' | 'right' | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#columnlines */\n  columnlines?: Trackable<'dashed' | 'none' | 'solid' | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#columnspacing */\n  columnspacing?: Trackable<string | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#frame */\n  frame?: Trackable<'dashed' | 'none' | 'solid' | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#framespacing */\n  framespacing?: Trackable<string | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#rowalign */\n  rowalign?: Trackable<'axis' | 'baseline' | 'bottom' | 'center' | 'top' | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#rowlines */\n  rowlines?: Trackable<'dashed' | 'none' | 'solid' | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#rowspacing */\n  rowspacing?: Trackable<string | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable#width */\n  width?: Trackable<string | undefined>\n}\n\n/**\n * Props accepted by `<mtd>` MathML elements.\n */\nexport interface MTdMathMLProps<eventTarget extends EventTarget> extends MathMLProps<eventTarget> {\n  /** The `columnspan` MathML attribute. */\n  columnspan?: Trackable<number | undefined>\n  /** The `rowspan` MathML attribute. */\n  rowspan?: Trackable<number | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtd#columnalign */\n  columnalign?: Trackable<'center' | 'left' | 'right' | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtd#rowalign */\n  rowalign?: Trackable<'axis' | 'baseline' | 'bottom' | 'center' | 'top' | undefined>\n}\n\n/**\n * Props accepted by `<mtext>` MathML elements.\n */\nexport interface MTextMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {}\n\n/**\n * Props accepted by `<mtr>` MathML elements.\n */\nexport interface MTrMathMLProps<eventTarget extends EventTarget> extends MathMLProps<eventTarget> {\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtr#columnalign */\n  columnalign?: Trackable<'center' | 'left' | 'right' | undefined>\n  /** Non-standard attribute See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtr#rowalign */\n  rowalign?: Trackable<'axis' | 'baseline' | 'bottom' | 'center' | 'top' | undefined>\n}\n\n/**\n * Props accepted by `<munder>` MathML elements.\n */\nexport interface MUnderMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** The `accentunder` MathML attribute. */\n  accentunder?: Trackable<boolean | undefined>\n}\n\n/**\n * Props accepted by `<munderover>` MathML elements.\n */\nexport interface MUnderoverMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {\n  /** The `accent` MathML attribute. */\n  accent?: Trackable<boolean | undefined>\n  /** The `accentunder` MathML attribute. */\n  accentunder?: Trackable<boolean | undefined>\n}\n\n/**\n * Props accepted by `<semantics>` MathML elements.\n */\nexport interface SemanticsMathMLProps<eventTarget extends EventTarget>\n  extends MathMLProps<eventTarget> {}\n"
  },
  {
    "path": "packages/component/src/lib/error-event.ts",
    "content": "/**\n * Error event shape emitted by the component runtime.\n */\nexport type ComponentErrorEvent = ErrorEvent & {\n  readonly error: unknown\n}\n\n/**\n * Creates a normalized component error event from any thrown value.\n *\n * @param error Error-like value to expose on the event.\n * @returns An `error` event carrying the original value.\n */\nexport function createComponentErrorEvent(error: unknown): ComponentErrorEvent {\n  return new ErrorEvent('error', { error }) as ComponentErrorEvent\n}\n\n/**\n * Reads the `.error` payload from a dispatched component error event.\n *\n * @param event Event to inspect.\n * @returns The original error value, if present.\n */\nexport function getComponentError(event: Event): unknown {\n  return (event as { error?: unknown }).error\n}\n"
  },
  {
    "path": "packages/component/src/lib/event-listeners.ts",
    "content": "/**\n * Event type with `currentTarget` narrowed to the dispatched target.\n */\nexport type Dispatched<event extends Event, target extends EventTarget> = Omit<\n  event,\n  'currentTarget'\n> & {\n  currentTarget: target\n}\n\n/**\n * Narrows non-event values to `never` and preserves dispatched event typing otherwise.\n */\nexport type EnsureEvent<event, target extends EventTarget> = event extends Event\n  ? Dispatched<event, target>\n  : never\n\n/**\n * Union of event type names supported by the given target.\n */\nexport type EventType<target extends EventTarget> = target extends { __eventMap?: infer eventMap }\n  ? keyof eventMap\n  : keyof EventMap<target>\n\ntype NavigationTarget = Window extends { navigation: infer navigation } ? navigation : never\ntype NavigationTargetEvent = NavigationTarget extends {\n  onnavigate: ((event: infer navigateEvent) => unknown) | null\n}\n  ? navigateEvent\n  : Event\ntype NavigationTargetEventMap = {\n  navigate: NavigationTargetEvent\n}\n\n/**\n * Listener function type for a specific target and event name.\n */\nexport type ListenerFor<target extends EventTarget, type extends EventType<target>> = (\n  event: EnsureEvent<EventMap<target>[type], target>,\n  signal: AbortSignal,\n) => void\n\n/**\n * Partial map of event listeners keyed by event type.\n */\nexport type EventListeners<target extends EventTarget> = Partial<{\n  [k in EventType<target>]: ListenerFor<target, k>\n}>\n\n// prettier-ignore\n/**\n * Event map resolved for the given DOM or custom event target.\n */\nexport type EventMap<target extends EventTarget> = (\n  // TypedEventTarget\n  target extends { __eventMap?: infer eventMap } ? eventMap :\n\n  // elements\n  target extends HTMLElement ? HTMLElementEventMap :\n  target extends SVGSVGElement ? SVGSVGElementEventMap :\n  target extends SVGElement ? SVGElementEventMap :\n  target extends Element ? ElementEventMap :\n  target extends Window ? WindowEventMap :\n  target extends Document ? DocumentEventMap :\n\n  // everything else\n  target extends AbortSignal ? AbortSignalEventMap :\n  target extends Animation ? AnimationEventMap :\n  target extends AudioDecoder ? AudioDecoderEventMap :\n  target extends AudioEncoder ? AudioEncoderEventMap :\n  target extends AudioNode ? GlobalEventHandlersEventMap :\n  target extends BaseAudioContext ? BaseAudioContextEventMap :\n  target extends BroadcastChannel ? BroadcastChannelEventMap :\n  target extends Clipboard ? GlobalEventHandlersEventMap :\n  target extends EventSource ? EventSourceEventMap :\n  target extends FileReader ? FileReaderEventMap :\n  target extends FontFaceSet ? FontFaceSetEventMap :\n  target extends IDBDatabase ? IDBDatabaseEventMap :\n  target extends IDBTransaction ? IDBTransactionEventMap :\n  target extends MIDIAccess ? MIDIAccessEventMap :\n  target extends MIDIPort ? MIDIPortEventMap :\n  target extends MediaDevices ? MediaDevicesEventMap :\n  target extends MediaKeySession ? MediaKeySessionEventMap :\n  target extends MediaQueryList ? MediaQueryListEventMap :\n  target extends MediaRecorder ? MediaRecorderEventMap :\n  target extends MediaSource ? MediaSourceEventMap :\n  target extends MediaStream ? MediaStreamEventMap :\n  target extends MediaStreamTrack ? MediaStreamTrackEventMap :\n  target extends MessagePort ? MessagePortEventMap :\n  target extends NavigationTarget ? NavigationTargetEventMap :\n  target extends Node ? GlobalEventHandlersEventMap :\n  target extends Notification ? NotificationEventMap :\n  target extends OffscreenCanvas ? OffscreenCanvasEventMap :\n  target extends PaymentRequest ? PaymentRequestEventMap :\n  target extends PaymentResponse ? PaymentResponseEventMap :\n  target extends Performance ? PerformanceEventMap :\n  target extends PermissionStatus ? PermissionStatusEventMap :\n  target extends PictureInPictureWindow ? PictureInPictureWindowEventMap :\n  target extends RTCDTMFSender ? RTCDTMFSenderEventMap :\n  target extends RTCDataChannel ? RTCDataChannelEventMap :\n  target extends RTCDtlsTransport ? RTCDtlsTransportEventMap :\n  target extends RTCIceTransport ? RTCIceTransportEventMap :\n  target extends RTCPeerConnection ? RTCPeerConnectionEventMap :\n  target extends RTCSctpTransport ? RTCSctpTransportEventMap :\n  target extends RemotePlayback ? RemotePlaybackEventMap :\n  target extends ScreenOrientation ? ScreenOrientationEventMap :\n  target extends ServiceWorkerContainer ? ServiceWorkerContainerEventMap :\n  target extends ServiceWorkerRegistration ? ServiceWorkerRegistrationEventMap :\n  target extends ServiceWorker ? AbstractWorkerEventMap :\n  target extends SharedWorker ? AbstractWorkerEventMap :\n  target extends SourceBuffer ? SourceBufferEventMap :\n  target extends SourceBufferList ? SourceBufferListEventMap :\n  target extends SpeechSynthesis ? SpeechSynthesisEventMap :\n  target extends SpeechSynthesisUtterance ? SpeechSynthesisUtteranceEventMap :\n  target extends TextTrack ? TextTrackEventMap :\n  target extends TextTrackCue ? TextTrackCueEventMap :\n  target extends TextTrackList ? TextTrackListEventMap :\n  target extends VideoDecoder ? VideoDecoderEventMap :\n  target extends VideoEncoder ? VideoEncoderEventMap :\n  target extends VisualViewport ? VisualViewportEventMap :\n  target extends WakeLockSentinel ? WakeLockSentinelEventMap :\n  target extends WebSocket ? WebSocketEventMap :\n  target extends Window ? (WindowEventMap & GlobalEventHandlersEventMap) :\n  target extends Worker ? AbstractWorkerEventMap :\n  target extends XMLHttpRequestEventTarget ? XMLHttpRequestEventTargetEventMap :\n  // default\n  GlobalEventHandlersEventMap & Record<string, Event>\n)\n\n/**\n * Adds typed event listeners and reentry abort signals to a target.\n *\n * @param target Event target to attach listeners to.\n * @param signal Lifetime signal used to remove all listeners.\n * @param listeners Listener map keyed by event type.\n */\nexport function addEventListeners<target extends EventTarget>(\n  target: target,\n  signal: AbortSignal,\n  listeners: EventListeners<target>,\n) {\n  type AnyEvent = EnsureEvent<EventMap<target>[EventType<target>], target>\n  type Listener = (event: AnyEvent, signal?: AbortSignal) => void\n\n  for (let type in listeners) {\n    let listener = listeners[type as EventType<target>] as Listener\n    if (!listener) continue\n    let reentry: AbortController | null = null\n\n    signal.addEventListener('abort', () => {\n      reentry?.abort()\n    })\n\n    target.addEventListener(\n      type,\n      (event) => {\n        reentry?.abort()\n        let dispatchedEvent = event as AnyEvent\n\n        if (listener.length < 2) {\n          reentry = null\n          listener(dispatchedEvent)\n        } else {\n          reentry = new AbortController()\n          listener(dispatchedEvent, reentry.signal)\n        }\n      },\n      { signal },\n    )\n  }\n}\n"
  },
  {
    "path": "packages/component/src/lib/frame.ts",
    "content": "import { jsx } from './jsx.ts'\nimport { Frame, createFrameHandle, type FrameContent } from './component.ts'\nimport { createComponentErrorEvent, getComponentError } from './error-event.ts'\nimport { invariant } from './invariant.ts'\nimport type { RemixElement, RemixNode } from './jsx.ts'\nimport type { FrameHandle } from './component.ts'\nimport type { Scheduler, VirtualRoot } from './vdom.ts'\nimport { createRangeRoot, createRoot } from './vdom.ts'\nimport { diffNodes } from './diff-dom.ts'\nimport type { StyleManager } from './style/index.ts'\n\ntype FrameRoot = [Comment, Comment] | Element | Document | DocumentFragment\n\ntype FrameData = {\n  status: 'pending' | 'resolved'\n  name?: string\n  src: string\n}\n\ntype HydrationData = {\n  moduleUrl: string\n  exportName: string\n  props: Record<string, unknown>\n}\n\ntype RmxData = {\n  h?: Record<string, HydrationData>\n  f?: Record<string, FrameData>\n}\n\nexport type VirtualRootMarker = Comment & {\n  $rmx: ReturnType<typeof createRangeRoot>\n}\n\ntype FrameMarkerData = FrameData & {\n  id: string\n}\n\ntype PendingClientEntries = Map<Comment, [Comment, RemixElement]>\n\n/**\n * Loads a client entry module for hydration.\n */\nexport type LoadModule = (moduleUrl: string, exportName: string) => Promise<Function> | Function\n\n/**\n * Resolves frame content for the given frame source.\n */\nexport type ResolveFrame = (\n  src: string,\n  signal?: AbortSignal,\n  target?: string,\n) => Promise<FrameContent> | FrameContent\n\ntype InternalFrameContent = FrameContent | DocumentFragment\n\ntype FrameTemplateListener = (fragment: DocumentFragment) => void\n\nlet bufferedFrameTemplates = new Map<string, DocumentFragment[]>()\nlet frameTemplateListeners = new Map<string, Set<FrameTemplateListener>>()\n\nfunction syncElementAttributes(target: Element, source: Element) {\n  for (let attribute of Array.from(target.attributes)) {\n    if (!source.hasAttribute(attribute.name)) {\n      target.removeAttribute(attribute.name)\n    }\n  }\n\n  for (let attribute of Array.from(source.attributes)) {\n    if (target.getAttribute(attribute.name) !== attribute.value) {\n      target.setAttribute(attribute.name, attribute.value)\n    }\n  }\n}\n\nexport type FrameRuntime = {\n  topFrame?: FrameHandle\n  errorTarget: EventTarget\n  loadModule: LoadModule\n  resolveFrame: ResolveFrame\n  pendingClientEntries: PendingClientEntries\n  scheduler: Scheduler\n  styleManager: StyleManager\n  data: RmxData\n  moduleCache: Map<string, Function>\n  moduleLoads: Map<string, Promise<Function | undefined>>\n  frameInstances: WeakMap<Comment, Frame>\n  namedFrames: Map<string, FrameHandle>\n}\n\nexport type FrameContext = {\n  topFrame?: FrameHandle\n  errorTarget: EventTarget\n  loadModule: LoadModule\n  resolveFrame: ResolveFrame\n  pendingClientEntries: PendingClientEntries\n  scheduler: Scheduler\n  frame: FrameHandle\n  styleManager: StyleManager\n  data: RmxData\n  moduleCache: Map<string, Function>\n  moduleLoads: Map<string, Promise<Function | undefined>>\n  frameInstances: WeakMap<Comment, Frame>\n  namedFrames: Map<string, FrameHandle>\n  regionTailRef?: ChildNode | null\n  regionParent?: ParentNode | null\n}\n\ntype FrameInit = {\n  name?: string\n  topFrame?: FrameHandle\n  src: string\n  errorTarget: EventTarget\n  loadModule: LoadModule\n  resolveFrame: ResolveFrame\n  pendingClientEntries: PendingClientEntries\n  scheduler: Scheduler\n  styleManager: StyleManager\n  marker?: FrameMarkerData\n  data: RmxData\n  moduleCache: Map<string, Function>\n  moduleLoads: Map<string, Promise<Function | undefined>>\n  frameInstances: WeakMap<Comment, Frame>\n  namedFrames: Map<string, FrameHandle>\n}\n\nexport type Frame = {\n  render: (content: InternalFrameContent, options?: RenderOptions) => Promise<void>\n  ready: () => Promise<void>\n  flush: () => void\n  dispose: () => void\n  handle: FrameHandle\n}\n\ntype RenderOptions = {\n  initialHydrationTracker?: InitialHydrationTracker\n  signal?: AbortSignal\n}\n\nexport function createFrame(root: FrameRoot, init: FrameInit): Frame {\n  let container = createContainer(root)\n  let observers: MutationObserver[] = []\n  let subscriptions: Array<() => void> = []\n  let contentRoot: VirtualRoot | undefined\n  let reloadController: AbortController | undefined\n\n  // Merge any rmx-data found in the current document once at startup.\n  mergeRmxDataFromDocument(init.data, container.doc)\n\n  let runtime = createFrameRuntime(init)\n\n  let frame = createFrameHandle({\n    src: init.src,\n    $runtime: runtime,\n    reload: async () => {\n      reloadController?.abort()\n      let controller = new AbortController()\n      reloadController = controller\n      frame.dispatchEvent(new Event('reloadStart'))\n      try {\n        let content = await init.resolveFrame(frame.src, controller.signal, frameName)\n        if (reloadController !== controller || controller.signal.aborted) return controller.signal\n        await render(content, { signal: controller.signal })\n        return controller.signal\n      } catch (error) {\n        if (reloadController !== controller || controller.signal.aborted) return controller.signal\n        init.errorTarget.dispatchEvent(createComponentErrorEvent(error))\n        throw error\n      } finally {\n        if (reloadController === controller) {\n          frame.dispatchEvent(new Event('reloadComplete'))\n        }\n      }\n    },\n    replace: async (content: FrameContent) => {\n      await render(content)\n    },\n  })\n  runtime.topFrame = runtime.topFrame ?? init.topFrame ?? frame\n\n  let frameName = init.marker?.name ?? init.name\n  if (frameName) {\n    init.namedFrames.set(frameName, frame)\n  }\n\n  let context: FrameContext = {\n    topFrame: runtime.topFrame,\n    errorTarget: init.errorTarget,\n    loadModule: init.loadModule,\n    resolveFrame: init.resolveFrame,\n    pendingClientEntries: init.pendingClientEntries,\n    scheduler: init.scheduler,\n    frame,\n    styleManager: init.styleManager,\n    data: init.data,\n    moduleCache: init.moduleCache,\n    moduleLoads: init.moduleLoads,\n    frameInstances: init.frameInstances,\n    namedFrames: init.namedFrames,\n    regionTailRef: container.regionTailRef,\n    regionParent: container.regionParent,\n  }\n\n  async function render(content: InternalFrameContent, options?: RenderOptions): Promise<void> {\n    if (options?.signal?.aborted) return\n\n    if (content instanceof ReadableStream) {\n      await renderFrameStream(content, container.doc, async (html) => {\n        if (options?.signal?.aborted) return\n        await render(html, options)\n      })\n      return\n    }\n\n    if (isRemixNodeFrameContent(content)) {\n      if (!contentRoot) {\n        let currentNodes = getContentNodes()\n        removeVirtualRoots(currentNodes)\n        disposeSubFrames(currentNodes, context)\n        clearFrameContent()\n        contentRoot = createFrameContentRoot()\n      }\n\n      if (options?.signal?.aborted) return\n      contentRoot.render(content)\n      return\n    }\n\n    if (contentRoot) {\n      contentRoot.dispose()\n      contentRoot = undefined\n    }\n\n    let isFullDocumentReload =\n      container.root instanceof Document &&\n      typeof content === 'string' &&\n      isFullDocumentHtml(content)\n\n    if (isFullDocumentReload && typeof content === 'string') {\n      // Full-document reload should tear down existing hydrated roots and subframes\n      // before diffing fresh HTML, otherwise stale component instances can survive\n      // on detached DOM nodes.\n      let previousBodyNodes = Array.from(container.doc.body.childNodes)\n      removeVirtualRoots(previousBodyNodes)\n      disposeSubFrames(previousBodyNodes, context)\n      let parsed = new DOMParser().parseFromString(content, 'text/html')\n      mergeRmxDataFromDocument(context.data, parsed)\n\n      syncElementAttributes(container.doc.documentElement, parsed.documentElement)\n      syncElementAttributes(container.doc.head, parsed.head)\n      syncElementAttributes(container.doc.body, parsed.body)\n\n      diffNodes(Array.from(container.doc.head.childNodes), Array.from(parsed.head.childNodes), {\n        ...context,\n        regionParent: container.doc.head,\n        regionTailRef: null,\n      })\n      diffNodes(Array.from(container.doc.body.childNodes), Array.from(parsed.body.childNodes), {\n        ...context,\n        regionParent: container.doc.body,\n        regionTailRef: null,\n      })\n\n      let bodyContainer = createElementContainer(container.doc.body)\n      if (options?.signal?.aborted) return\n      scheduleHydrationInContainer(bodyContainer, context, options?.initialHydrationTracker)\n      createSubFrames(bodyContainer.childNodes, context)\n      return\n    }\n\n    let fragment =\n      typeof content === 'string' ? createFragmentFromString(container.doc, content) : content\n    moveServerStylesToHead(container.doc, fragment)\n    mergeRmxDataFromFragment(context.data, fragment)\n\n    let nextContainer = createContainer(fragment)\n\n    if (options?.signal?.aborted) return\n\n    diffNodes(container.childNodes, Array.from(nextContainer.childNodes), {\n      ...context,\n      regionTailRef: container.regionTailRef,\n      regionParent: container.regionParent,\n    })\n\n    if (options?.signal?.aborted) return\n    scheduleHydrationInContainer(container, context, options?.initialHydrationTracker)\n    createSubFrames(container.childNodes, context)\n  }\n\n  function createFrameContentRoot(): VirtualRoot {\n    let virtualRoot: VirtualRoot\n    if (container.root instanceof Document) {\n      virtualRoot = createRoot(container.doc.body, {\n        scheduler: context.scheduler,\n        frame,\n        styleManager: context.styleManager,\n      })\n    } else {\n      invariant(Array.isArray(root), 'Expected comment-bounded frame root')\n      virtualRoot = createRangeRoot(root, {\n        scheduler: context.scheduler,\n        frame,\n        styleManager: context.styleManager,\n      })\n    }\n\n    virtualRoot.addEventListener('error', (event: Event) => {\n      if (context.errorTarget === virtualRoot) return\n      context.errorTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event)))\n    })\n\n    return virtualRoot\n  }\n\n  function getContentNodes(): Node[] {\n    return container.root instanceof Document\n      ? Array.from(container.doc.body.childNodes)\n      : container.childNodes\n  }\n\n  function clearFrameContent() {\n    for (let node of getContentNodes()) {\n      node.parentNode?.removeChild(node)\n    }\n  }\n\n  async function hydrateInitial(): Promise<void> {\n    let initialHydrationTracker = createInitialHydrationTracker()\n\n    createSubFrames(container.childNodes, context)\n    scheduleHydrationInContainer(container, context, initialHydrationTracker)\n\n    if (init.marker?.status === 'pending') {\n      let markerId = init.marker.id\n      let early = consumeFrameTemplate(markerId) ?? getEarlyFrameContent(markerId)\n      if (early) {\n        moveServerStylesToHead(container.doc, early)\n        mergeRmxDataFromFragment(context.data, early)\n        await render(early, { initialHydrationTracker })\n      } else {\n        let observer = setupTemplateObserver()\n        let unsubscribe = subscribeFrameTemplate(markerId, async (fragment) => {\n          unsubscribe()\n          moveServerStylesToHead(container.doc, fragment)\n          mergeRmxDataFromFragment(context.data, fragment)\n          await render(fragment)\n          observer.disconnect()\n        })\n        subscriptions.push(unsubscribe)\n        let buffered = consumeFrameTemplate(markerId)\n        if (buffered) {\n          unsubscribe()\n          moveServerStylesToHead(container.doc, buffered)\n          mergeRmxDataFromFragment(context.data, buffered)\n          await render(buffered)\n          observer.disconnect()\n        }\n        observers.push(observer)\n      }\n    }\n\n    initialHydrationTracker.finalize()\n    await initialHydrationTracker.ready()\n  }\n\n  function dispose(): void {\n    reloadController?.abort()\n    reloadController = undefined\n    contentRoot?.dispose()\n    contentRoot = undefined\n\n    // Disconnect any MutationObservers waiting for templates.\n    for (let observer of observers) {\n      observer.disconnect()\n    }\n    observers.length = 0\n    for (let unsubscribe of subscriptions) {\n      unsubscribe()\n    }\n    subscriptions.length = 0\n\n    // Remove hydrated virtual roots in this frame's region.\n    removeVirtualRoots(container.childNodes)\n\n    // Dispose sub-frames recursively.\n    disposeSubFrames(container.childNodes, context)\n\n    if (frameName) {\n      if (init.namedFrames.get(frameName) === frame) {\n        init.namedFrames.delete(frameName)\n      }\n    }\n  }\n\n  let readyPromise = hydrateInitial()\n\n  return {\n    render,\n    ready: () => readyPromise,\n    flush: () => context.scheduler.dequeue(),\n    dispose,\n    handle: frame,\n  }\n}\n\nexport function createFrameRuntime(init: {\n  topFrame?: FrameHandle\n  errorTarget: EventTarget\n  loadModule: LoadModule\n  resolveFrame: ResolveFrame\n  pendingClientEntries: PendingClientEntries\n  scheduler: Scheduler\n  styleManager: StyleManager\n  data: RmxData\n  moduleCache: Map<string, Function>\n  moduleLoads: Map<string, Promise<Function | undefined>>\n  frameInstances: WeakMap<Comment, Frame>\n  namedFrames: Map<string, FrameHandle>\n}): FrameRuntime {\n  return {\n    topFrame: init.topFrame,\n    errorTarget: init.errorTarget,\n    loadModule: init.loadModule,\n    resolveFrame: init.resolveFrame,\n    pendingClientEntries: init.pendingClientEntries,\n    scheduler: init.scheduler,\n    styleManager: init.styleManager,\n    data: init.data,\n    moduleCache: init.moduleCache,\n    moduleLoads: init.moduleLoads,\n    frameInstances: init.frameInstances,\n    namedFrames: init.namedFrames,\n  }\n}\n\ntype InitialHydrationTracker = {\n  track: () => () => void\n  finalize: () => void\n  ready: () => Promise<void>\n}\n\nfunction createInitialHydrationTracker(): InitialHydrationTracker {\n  let pending = 0\n  let finalized = false\n\n  let resolveReady: (() => void) | undefined\n  let readyPromise = new Promise<void>((resolve) => {\n    resolveReady = resolve\n  })\n\n  function maybeResolve() {\n    if (finalized && pending === 0) {\n      resolveReady?.()\n      resolveReady = undefined\n    }\n  }\n\n  return {\n    track() {\n      pending++\n      let completed = false\n      return () => {\n        if (completed) return\n        completed = true\n        pending--\n        maybeResolve()\n      }\n    },\n    finalize() {\n      finalized = true\n      maybeResolve()\n    },\n    ready() {\n      return readyPromise\n    },\n  }\n}\n\nfunction mergeRmxDataFromDocument(into: RmxData, doc: Document): void {\n  let scripts = Array.from(doc.querySelectorAll('script#rmx-data'))\n  for (let script of scripts) {\n    if (!(script instanceof HTMLScriptElement)) continue\n    mergeRmxData(into, parseRmxDataScript(script))\n    script.remove()\n  }\n}\n\nfunction mergeRmxDataFromFragment(into: RmxData, fragment: DocumentFragment): void {\n  let scripts = Array.from(fragment.querySelectorAll('script#rmx-data'))\n  for (let script of scripts) {\n    if (!(script instanceof HTMLScriptElement)) continue\n    mergeRmxData(into, parseRmxDataScript(script))\n    script.remove()\n  }\n}\n\nfunction moveServerStylesToHead(doc: Document, fragment: DocumentFragment): void {\n  let target = doc.head\n  if (!target) return\n\n  let styles = Array.from(fragment.querySelectorAll('style[data-rmx-styles]'))\n  for (let style of styles) {\n    if (style instanceof HTMLStyleElement) {\n      target.appendChild(style)\n    }\n  }\n\n  let heads = Array.from(fragment.querySelectorAll('head'))\n  for (let head of heads) {\n    if (!head.childNodes.length) {\n      head.remove()\n    }\n  }\n}\n\nfunction parseRmxDataScript(script: HTMLScriptElement): RmxData {\n  try {\n    return JSON.parse(script.textContent || '{}')\n  } catch {\n    console.error('[createFrame] Failed to parse rmx-data script')\n    return {}\n  }\n}\n\nfunction mergeRmxData(into: RmxData, from: RmxData): void {\n  if (from.h) {\n    if (!into.h) into.h = {}\n    copyOwnRmxEntries(into.h, from.h)\n  }\n\n  if (from.f) {\n    if (!into.f) into.f = {}\n    copyOwnRmxEntries(into.f, from.f)\n  }\n}\n\nfunction copyOwnRmxEntries<T>(target: Record<string, T>, source: Record<string, T>): void {\n  for (let key of Object.keys(source)) {\n    if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue\n    if (!Object.hasOwn(source, key)) continue\n    target[key] = source[key]!\n  }\n}\n\nfunction scheduleHydrationInContainer(\n  container: FrameContainer,\n  context: FrameContext,\n  initialHydrationTracker?: InitialHydrationTracker,\n): void {\n  let hydrationMarkers = findHydrationMarkers(container)\n  if (hydrationMarkers.length === 0) return\n\n  let hydrationData = context.data.h\n  if (!hydrationData) return\n\n  for (let marker of hydrationMarkers) {\n    let entry = hydrationData[marker.id]\n    if (!entry) continue\n    scheduleHydrationMarker(marker, entry, context, initialHydrationTracker)\n  }\n}\n\nfunction scheduleHydrationMarker(\n  marker: HydrationMarker,\n  entry: HydrationData,\n  context: FrameContext,\n  initialHydrationTracker?: InitialHydrationTracker,\n): void {\n  let done = initialHydrationTracker?.track()\n  let key = `${entry.moduleUrl}#${entry.exportName}`\n\n  let hydrateWithComponent = (component: Function) => {\n    if (!isHydrationMarkerLive(marker, context)) return\n    let vElement = createElement(component, entry.props)\n    context.pendingClientEntries.set(marker.start, [marker.end, vElement])\n    hydrateRegion(vElement, marker.start, marker.end, context)\n  }\n\n  let cached = context.moduleCache.get(key)\n  if (cached) {\n    hydrateWithComponent(cached)\n    done?.()\n    return\n  }\n\n  getOrStartModuleLoad(key, entry, marker.id, context)\n    .then((component) => {\n      if (component) {\n        hydrateWithComponent(component)\n      }\n    })\n    .finally(() => {\n      done?.()\n    })\n}\n\nfunction getOrStartModuleLoad(\n  key: string,\n  entry: HydrationData,\n  markerId: string,\n  context: FrameContext,\n): Promise<Function | undefined> {\n  let inFlight = context.moduleLoads.get(key)\n  if (inFlight) return inFlight\n\n  let loadPromise = (async () => {\n    try {\n      let mod = await context.loadModule(entry.moduleUrl, entry.exportName)\n      if (typeof mod !== 'function') {\n        throw new Error(`Export \"${entry.exportName}\" from \"${entry.moduleUrl}\" is not a function`)\n      }\n      context.moduleCache.set(key, mod)\n      return mod\n    } catch (error) {\n      console.error(`[createFrame] Failed to load module for ${markerId}:`, error)\n      return undefined\n    } finally {\n      context.moduleLoads.delete(key)\n    }\n  })()\n\n  context.moduleLoads.set(key, loadPromise)\n  return loadPromise\n}\n\nfunction createElement(component: Function, props: Record<string, unknown>): RemixElement {\n  let revivedProps = reviveSerializedValue(props)\n  return jsx(component, revivedProps as any)\n}\n\nfunction reviveSerializedValue(value: unknown): unknown {\n  if (value === null || value === undefined) return value\n  if (typeof value !== 'object') return value\n\n  if (Array.isArray(value)) {\n    return value.map((item) => reviveSerializedValue(item))\n  }\n\n  let record = value as Record<string, unknown>\n\n  if (record.$rmxFrame === true) {\n    let props = reviveSerializedObject(record.props)\n    let key = reviveSerializedValue(record.key)\n    return jsx(Frame as any, props as any, key as any)\n  }\n\n  if (record.$rmx === true && typeof record.type === 'string') {\n    let props = reviveSerializedObject(record.props)\n    let key = reviveSerializedValue(record.key)\n    return jsx(record.type as any, props as any, key as any)\n  }\n\n  let revived: Record<string, unknown> = {}\n  for (let key in record) {\n    revived[key] = reviveSerializedValue(record[key])\n  }\n  return revived\n}\n\nfunction reviveSerializedObject(value: unknown): Record<string, unknown> {\n  if (!value || typeof value !== 'object' || Array.isArray(value)) return {}\n  let revived = reviveSerializedValue(value)\n  if (!revived || typeof revived !== 'object' || Array.isArray(revived)) return {}\n  return revived as Record<string, unknown>\n}\n\nfunction hydrateRegion(\n  vElement: RemixElement,\n  start: Comment,\n  end: Comment,\n  context: FrameContext,\n): void {\n  context.pendingClientEntries.delete(start)\n\n  // The same marker can be discovered by overlapping hydration passes\n  // (for example, document root + nested frame root). Reuse the existing\n  // virtual root instead of redefining the marker property.\n  if (isHydratedVirtualRootMarker(start)) {\n    start.$rmx.render(vElement)\n    return\n  }\n\n  let root = createRangeRoot([start, end], {\n    scheduler: context.scheduler,\n    frame: context.frame,\n    styleManager: context.styleManager,\n  })\n  root.addEventListener('error', (event) => {\n    if (context.errorTarget === root) return\n    context.errorTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event)))\n  })\n\n  Object.defineProperty(start, '$rmx', { value: root, enumerable: false })\n  root.render(vElement)\n}\n\nfunction createSubFrames(nodes: Node[], context: FrameContext) {\n  for (let i = 0; i < nodes.length; i++) {\n    let node = nodes[i]\n\n    if (isFrameStart(node)) {\n      let end = findEndMarker(node, isFrameStart, isFrameEnd)\n\n      if (!context.frameInstances.has(node)) {\n        let id = getFrameId(node)\n        let marker = context.data.f?.[id]\n        if (marker) {\n          let frameMarker: FrameMarkerData = { ...marker, id }\n          let subFrame = createFrame([node, end], {\n            src: frameMarker.src,\n            marker: frameMarker,\n            topFrame: context.topFrame,\n            errorTarget: context.errorTarget,\n            loadModule: context.loadModule,\n            resolveFrame: context.resolveFrame,\n            pendingClientEntries: context.pendingClientEntries,\n            scheduler: context.scheduler,\n            styleManager: context.styleManager,\n            data: context.data,\n            moduleCache: context.moduleCache,\n            moduleLoads: context.moduleLoads,\n            frameInstances: context.frameInstances,\n            namedFrames: context.namedFrames,\n          })\n          context.frameInstances.set(node, subFrame)\n        }\n      }\n\n      i = nodes.indexOf(end)\n      continue\n    }\n\n    if (node.childNodes && node.childNodes.length > 0) {\n      createSubFrames(Array.from(node.childNodes), context)\n    }\n  }\n}\n\nfunction isHydrationMarkerLive(marker: HydrationMarker, context: FrameContext): boolean {\n  if (!marker.start.isConnected || !marker.end.isConnected) return false\n  if (marker.start.parentNode !== marker.end.parentNode) return false\n\n  let startText = marker.start.data.trim()\n  if (startText !== `rmx:h:${marker.id}`) return false\n  if (marker.end.data.trim() !== '/rmx:h') return false\n\n  let parent = marker.start.parentNode\n  if (!parent) return false\n\n  if (context.regionTailRef) {\n    let startPosition = marker.start.compareDocumentPosition(context.regionTailRef)\n    let endPosition = marker.end.compareDocumentPosition(context.regionTailRef)\n    let tailFollowsStart = (startPosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0\n    let tailFollowsEnd = (endPosition & Node.DOCUMENT_POSITION_FOLLOWING) !== 0\n    if (!tailFollowsStart || !tailFollowsEnd) return false\n  }\n\n  return true\n}\n\nfunction removeVirtualRoots(nodes: Node[]): void {\n  for (let i = 0; i < nodes.length; i++) {\n    let node = nodes[i]\n\n    if (isHydratedVirtualRootMarker(node)) {\n      node.$rmx.dispose()\n      let end = findEndMarker(node, isHydrationStart, isHydrationEnd)\n      i = nodes.indexOf(end)\n      continue\n    }\n\n    if (node.childNodes && node.childNodes.length > 0) {\n      removeVirtualRoots(Array.from(node.childNodes))\n    }\n  }\n}\n\nfunction disposeSubFrames(nodes: Node[], context: FrameContext): void {\n  for (let i = 0; i < nodes.length; i++) {\n    let node = nodes[i]\n\n    if (isFrameStart(node)) {\n      let end = findEndMarker(node, isFrameStart, isFrameEnd)\n      let subFrame = context.frameInstances.get(node)\n      if (subFrame) {\n        subFrame.dispose()\n        context.frameInstances.delete(node)\n      }\n      i = nodes.indexOf(end)\n      continue\n    }\n\n    if (node.childNodes && node.childNodes.length > 0) {\n      disposeSubFrames(Array.from(node.childNodes), context)\n    }\n  }\n}\n\nfunction getEarlyFrameContent(id: string): DocumentFragment | null {\n  let template = document.querySelector(`template#${id}`)\n  if (template instanceof HTMLTemplateElement) {\n    let fragment = template.content\n    template.remove()\n    return fragment\n  }\n  return null\n}\n\nfunction setupTemplateObserver(): MutationObserver {\n  let root = document.body ?? document.documentElement ?? document\n  let observer = new MutationObserver((mutations) => {\n    for (let mutation of mutations) {\n      for (let node of mutation.addedNodes) {\n        collectAndPublishTemplates(node)\n      }\n    }\n  })\n\n  observer.observe(root, { childList: true, subtree: true })\n  return observer\n}\n\nfunction collectAndPublishTemplates(node: Node): void {\n  if (node instanceof HTMLTemplateElement) {\n    publishFrameTemplateElement(node)\n    return\n  }\n\n  if (!(node instanceof Element)) return\n  let templates = Array.from(node.querySelectorAll('template'))\n  for (let template of templates) {\n    if (!(template instanceof HTMLTemplateElement)) continue\n    publishFrameTemplateElement(template)\n  }\n}\n\nfunction publishFrameTemplateElement(template: HTMLTemplateElement): void {\n  if (!template.id) return\n  template.remove()\n  publishFrameTemplate(template.id, template.content)\n}\n\nexport function publishFrameTemplate(id: string, fragment: DocumentFragment): void {\n  let listeners = frameTemplateListeners.get(id)\n  if (!listeners || listeners.size === 0) {\n    let queue = bufferedFrameTemplates.get(id)\n    if (!queue) {\n      queue = []\n      bufferedFrameTemplates.set(id, queue)\n    }\n    queue.push(fragment)\n    return\n  }\n\n  for (let listener of listeners) {\n    listener(fragment.cloneNode(true) as DocumentFragment)\n  }\n}\n\nexport function consumeFrameTemplate(id: string): DocumentFragment | null {\n  let queue = bufferedFrameTemplates.get(id)\n  if (!queue || queue.length === 0) return null\n\n  let fragment = queue.shift() ?? null\n  if (queue.length === 0) {\n    bufferedFrameTemplates.delete(id)\n  }\n\n  return fragment\n}\n\nfunction subscribeFrameTemplate(id: string, listener: FrameTemplateListener): () => void {\n  let listeners = frameTemplateListeners.get(id)\n  if (!listeners) {\n    listeners = new Set()\n    frameTemplateListeners.set(id, listeners)\n  }\n  listeners.add(listener)\n  return () => {\n    let current = frameTemplateListeners.get(id)\n    if (!current) return\n    current.delete(listener)\n    if (current.size === 0) {\n      frameTemplateListeners.delete(id)\n    }\n  }\n}\n\ntype StreamTemplateParseResult = {\n  html: string\n  remainder: string\n}\n\nconst COMPLETE_TEMPLATE_WITH_ID_PATTERN =\n  /<template\\b[^>]*\\bid=(?:\"([^\"]+)\"|'([^']+)')[^>]*>[\\s\\S]*?<\\/template>/gi\n\nfunction extractTemplatesFromBuffer(\n  doc: Document,\n  buffer: string,\n  onTemplate: (id: string, fragment: DocumentFragment) => void,\n): StreamTemplateParseResult {\n  let html = ''\n  let cursor = 0\n  let hadMatch = false\n\n  COMPLETE_TEMPLATE_WITH_ID_PATTERN.lastIndex = 0\n  let match = COMPLETE_TEMPLATE_WITH_ID_PATTERN.exec(buffer)\n\n  while (match) {\n    hadMatch = true\n    let index = match.index\n    let fullMatch = match[0]\n    let id = match[1] ?? match[2]\n    let matchEnd = index + fullMatch.length\n\n    html += buffer.slice(cursor, index)\n\n    if (id) {\n      let parsed = createFragmentFromString(doc, fullMatch)\n      let template = parsed.querySelector('template')\n      if (template instanceof HTMLTemplateElement && template.id) {\n        onTemplate(template.id, template.content)\n      }\n    }\n\n    cursor = matchEnd\n    match = COMPLETE_TEMPLATE_WITH_ID_PATTERN.exec(buffer)\n  }\n\n  let tail = buffer.slice(cursor)\n  if (tail === '') return { html, remainder: '' }\n\n  let tailStart = tail.toLowerCase().lastIndexOf('<template')\n  if (tailStart === -1) {\n    return { html: html + tail, remainder: '' }\n  }\n\n  if (!hadMatch) {\n    return {\n      html: buffer.slice(0, tailStart),\n      remainder: buffer.slice(tailStart),\n    }\n  }\n\n  return {\n    html: html + tail.slice(0, tailStart),\n    remainder: tail.slice(tailStart),\n  }\n}\n\nasync function renderFrameStream(\n  stream: ReadableStream<Uint8Array>,\n  doc: Document,\n  applyHtml: (html: string) => Promise<void>,\n): Promise<void> {\n  let reader = stream.getReader()\n  let decoder = new TextDecoder()\n  let buffer = ''\n  let html = ''\n  let appliedLength = 0\n  let appliedOnce = false\n\n  try {\n    while (true) {\n      let { done, value } = await reader.read()\n      if (done) break\n\n      buffer += decoder.decode(value, { stream: true })\n      let parsed = extractTemplatesFromBuffer(doc, buffer, publishFrameTemplate)\n      buffer = parsed.remainder\n\n      if (parsed.html !== '') {\n        html += parsed.html\n        let htmlMarkers = collectHtmlMarkerSummary(html)\n        if (!hasBalancedMarkerSummary(htmlMarkers)) {\n          continue\n        }\n        await applyHtml(html)\n        appliedLength = html.length\n        appliedOnce = true\n      }\n    }\n\n    buffer += decoder.decode()\n    let parsed = extractTemplatesFromBuffer(doc, buffer, publishFrameTemplate)\n    html += parsed.html\n    buffer = parsed.remainder\n    if (buffer !== '') {\n      html += buffer\n      buffer = ''\n    }\n\n    if (html !== '' && html.length > appliedLength) {\n      await applyHtml(html)\n      appliedOnce = true\n    }\n\n    // A frame stream can legitimately resolve to empty content. Ensure the\n    // existing frame region is cleared instead of treated as a no-op.\n    if (html === '' && !appliedOnce) {\n      await applyHtml('')\n    }\n  } finally {\n    reader.releaseLock()\n  }\n}\n\ntype FrameContainer = {\n  doc: Document\n  root: ParentNode\n  childNodes: Node[]\n  regionTailRef?: ChildNode | null\n  regionParent?: ParentNode | null\n}\n\nfunction createContainer(root: FrameRoot): FrameContainer {\n  return Array.isArray(root) ? createCommentContainer(root) : createElementContainer(root)\n}\n\nfunction createElementContainer(root: Document | Element | DocumentFragment): FrameContainer {\n  let doc = root instanceof Document ? root : (root.ownerDocument ?? document)\n  return {\n    doc,\n    root,\n    get childNodes() {\n      return Array.from(root.childNodes)\n    },\n  }\n}\n\nfunction createCommentContainer([start, end]: [Comment, Comment]): FrameContainer {\n  let parent = end.parentNode\n  invariant(parent, 'Invalid comment container')\n  invariant(start.parentNode === parent, 'Boundaries must share parent')\n  let doc = parent.ownerDocument ?? document\n\n  let getChildNodesBetween = (): Node[] => {\n    let nodes: Node[] = []\n    let node = start.nextSibling\n    while (node && node !== end) {\n      nodes.push(node)\n      node = node.nextSibling\n    }\n    return nodes\n  }\n\n  return {\n    doc,\n    root: parent,\n    get childNodes() {\n      return getChildNodesBetween()\n    },\n    regionTailRef: end,\n    regionParent: parent,\n  }\n}\n\nfunction createFragmentFromString(doc: Document, content: string): DocumentFragment {\n  let template = doc.createElement('template')\n  template.innerHTML = content.trim()\n  return template.content\n}\n\nfunction isRemixNodeFrameContent(content: InternalFrameContent): content is RemixNode {\n  return !(\n    content instanceof ReadableStream ||\n    content instanceof DocumentFragment ||\n    typeof content === 'string'\n  )\n}\n\nfunction isFullDocumentHtml(content: string): boolean {\n  let trimmed = content.trimStart()\n  return /^<!doctype html\\b/i.test(trimmed) || /^<html[\\s>]/i.test(trimmed)\n}\n\ntype HydrationMarker = {\n  id: string\n  start: Comment\n  end: Comment\n}\n\nfunction findHydrationMarkers(container: FrameContainer): HydrationMarker[] {\n  let results: HydrationMarker[] = []\n\n  forEachComment(container, (comment) => {\n    let trimmed = comment.data.trim()\n    if (!trimmed.startsWith('rmx:h:')) return\n\n    let id = trimmed.slice('rmx:h:'.length)\n    let end = findEndMarker(comment, isHydrationStart, isHydrationEnd)\n    results.push({ id, start: comment, end })\n  })\n\n  return results\n}\n\nfunction forEachComment(container: FrameContainer, cb: (comment: Comment) => void): void {\n  walkCommentsInNodes(container.childNodes, cb)\n}\n\nfunction walkCommentsInNodes(nodes: Node[], cb: (comment: Comment) => void): void {\n  for (let i = 0; i < nodes.length; i++) {\n    let node = nodes[i]\n\n    // Frame ownership boundary: hydration markers inside nested frame regions\n    // are discovered and hydrated by the nested frame instance only.\n    if (isFrameStart(node)) {\n      let end = findEndMarker(node, isFrameStart, isFrameEnd)\n      i = nodes.indexOf(end)\n      continue\n    }\n\n    if (node.nodeType === Node.COMMENT_NODE) cb(node as Comment)\n    if (node.childNodes && node.childNodes.length > 0) {\n      walkCommentsInNodes(Array.from(node.childNodes), cb)\n    }\n  }\n}\n\nfunction isHydrationStart(node: Comment): boolean {\n  return node.data.trim().startsWith('rmx:h:')\n}\n\nfunction isHydrationEnd(node: Comment): boolean {\n  return node.data.trim() === '/rmx:h'\n}\n\nfunction isHydratedVirtualRootMarker(node: Node): node is VirtualRootMarker {\n  return node instanceof Comment && '$rmx' in node\n}\n\nfunction isFrameStart(node: Node): node is Comment {\n  return node instanceof Comment && node.data.trim().startsWith('rmx:f:')\n}\n\nfunction isFrameEnd(node: Comment): boolean {\n  return node.data.trim() === '/rmx:f'\n}\n\nfunction getFrameId(start: Comment): string {\n  let trimmed = start.data.trim()\n  invariant(trimmed.startsWith('rmx:f:'), 'Invalid frame start marker')\n  return trimmed.slice('rmx:f:'.length)\n}\n\nfunction findEndMarker(\n  start: Comment,\n  isStart: (node: Comment) => boolean,\n  isEnd: (node: Comment) => boolean,\n): Comment {\n  let node: Node | null = start.nextSibling\n  let depth = 1\n\n  while (node) {\n    if (node.nodeType === Node.COMMENT_NODE) {\n      let comment = node as Comment\n      if (isStart(comment)) depth++\n      else if (isEnd(comment)) {\n        depth--\n        if (depth === 0) return comment\n      }\n    }\n    node = node.nextSibling\n  }\n\n  throw new Error('End marker not found')\n}\n\nfunction collectHtmlMarkerSummary(html: string): Record<string, number> {\n  return {\n    frameStarts: html.match(/<!--\\s*rmx:f:/g)?.length ?? 0,\n    frameEnds: html.match(/<!--\\s*\\/rmx:f\\s*-->/g)?.length ?? 0,\n    hydrationStarts: html.match(/<!--\\s*rmx:h:/g)?.length ?? 0,\n    hydrationEnds: html.match(/<!--\\s*\\/rmx:h\\s*-->/g)?.length ?? 0,\n  }\n}\n\nfunction hasBalancedMarkerSummary(summary: Record<string, number>): boolean {\n  return (\n    summary.frameStarts === summary.frameEnds && summary.hydrationStarts === summary.hydrationEnds\n  )\n}\n"
  },
  {
    "path": "packages/component/src/lib/invariant.ts",
    "content": "/**\n * Use this when framework code is incorrect, indicating an internal bug rather\n * than application code error.\n *\n * @param assertion The value to check for truthiness\n * @param message Optional message to include in the error\n */\nexport function invariant(assertion: any, message?: string): asserts assertion {\n  let prefix = 'Framework invariant'\n  if (assertion) return\n  throw new Error(message ? `${prefix}: ${message}` : prefix)\n}\n\n/**\n * Use this when application logic is incorrect, indicating a developer error.\n *\n * Using ID-based warnings with external documentation links allows us to:\n * - Update warning messages without releasing new versions\n * - Avoid bloating the library with warning messages or complicating builds\n *   with prod/dev build/export shenanigans\n * - Provide detailed troubleshooting guides and examples\n *\n * `id` is first so we can easily grep the codebase for ensure calls\n *\n * @param id The error ID for the documentation link\n * @param assertion The value to check for truthiness\n */\nexport function ensure(id: number, assertion: boolean): asserts assertion {\n  if (assertion) return\n  throw new Error(`REMIX_${id}: https://rmx.as/w/${id}`)\n}\n"
  },
  {
    "path": "packages/component/src/lib/jsx.ts",
    "content": "import type * as dom from './dom.d.ts'\nimport type { Component, Handle, RenderFn } from './component.ts'\n\n/**\n * Any valid element type accepted by JSX or {@link import('./create-element.ts').createElement}.\n * - `string` for host elements (e.g., 'div')\n * - `Function` for user components\n */\nexport type ElementType = string | Function\n\n/**\n * Generic bag of props passed to elements/components.\n * Consumers should define specific prop types on their components; this is the\n * renderer's normalized shape used throughout reconciler/SSR code.\n */\nexport type ElementProps = Record<string, any>\n\n/**\n * A virtual element produced by JSX or {@link import('./create-element.ts').createElement}\n * describing UI. Carries a `$rmx` brand used to distinguish it from plain objects at runtime.\n */\nexport interface RemixElement {\n  /** Host tag or component function for the element. */\n  type: ElementType\n  /** Normalized props for the element. */\n  props: ElementProps\n  /** Optional reconciliation key. */\n  key?: any\n  /** Internal brand used to distinguish Remix elements at runtime. */\n  $rmx: true\n}\n\n/**\n * Any single value Remix can render. Booleans render as empty text.\n */\nexport type Renderable = RemixElement | string | number | bigint | boolean | null | undefined\n\n/**\n * Anything that Remix can render, including nested arrays of renderable values.\n * This mirrors how JSX spreads children into arrays (e.g. when using `map`)\n * and how our reconciler flattens children at runtime.\n *\n * Particularly useful for `props.children`.\n *\n * ```tsx\n * function MyComponent({ children }: { children: RemixNode }) {}\n * ```\n */\nexport type RemixNode = Renderable | RemixNode[]\n\ntype MixItem<mix> = mix extends ReadonlyArray<infer descriptor> ? descriptor : mix\n\ntype NormalizeMixProp<props> = props extends { mix?: infer mix }\n  ? Omit<props, 'mix'> & {\n      mix?: Array<MixItem<Exclude<mix, undefined>>>\n    }\n  : props\n\ntype ExpandMixProp<props> = props extends { mix?: infer mix }\n  ? Omit<props, 'mix'> & {\n      mix?: MixItem<Exclude<mix, undefined>> | ReadonlyArray<MixItem<Exclude<mix, undefined>>>\n    }\n  : props\n\n/**\n * Get the props for a specific element type.\n *\n * @example\n * interface MyButtonProps extends Props<\"button\"> {\n *   size: \"sm\" | \"md\" | \"lg\"\n * }\n */\nexport type Props<T extends keyof JSX.IntrinsicElements> = NormalizeMixProp<\n  JSX.IntrinsicElements[T]\n>\n\n/**\n * Creates a Remix virtual element.\n *\n * @param type Host tag or component function.\n * @param props Element props.\n * @param key Optional reconciliation key.\n * @returns A Remix virtual element.\n */\nexport function jsx(type: string, props: ElementProps, key?: string): RemixElement\n/**\n * Creates a Remix virtual element from a component function.\n *\n * @param type Component function.\n * @param props Element props.\n * @param key Optional reconciliation key.\n * @returns A Remix virtual element.\n */\nexport function jsx(type: Function, props: ElementProps, key?: string): RemixElement\nexport function jsx(type: any, props: any, key?: any): RemixElement {\n  return { type, props: normalizeElementProps(props), key, $rmx: true }\n}\n\nexport { jsx as jsxDEV, jsx as jsxs }\n\ndeclare global {\n  namespace JSX {\n    export interface IntrinsicAttributes {\n      key?: any\n    }\n\n    type Element = RemixElement\n\n    type ElementType =\n      // host elements\n      | keyof IntrinsicElements\n      // Factory component\n      | ((handle: Handle<any>, setup: any) => RenderFn<any>)\n\n    type ElementChildrenAttribute = {\n      children: any\n    }\n\n    export interface ElementAttributesProperty {\n      props: any\n    }\n\n    type LibraryManagedAttributes<component, props> = component extends (\n      handle: Handle<any>,\n      setup: infer S,\n    ) => RenderFn<infer R>\n      ? // It's a ComponentFactory - combine setup + props\n        (unknown extends S ? {} : undefined extends S ? { setup?: S } : { setup: S }) &\n          ExpandMixProp<R>\n      : // Otherwise use props as-is (simple function component)\n        ExpandMixProp<props>\n\n    export interface IntrinsicSVGElements {\n      svg: dom.SVGProps<SVGSVGElement>\n      animate: dom.SVGProps<SVGAnimateElement>\n      circle: dom.SVGProps<SVGCircleElement>\n      animateMotion: dom.SVGProps<SVGAnimateMotionElement>\n      animateTransform: dom.SVGProps<SVGAnimateTransformElement>\n      clipPath: dom.SVGProps<SVGClipPathElement>\n      defs: dom.SVGProps<SVGDefsElement>\n      desc: dom.SVGProps<SVGDescElement>\n      ellipse: dom.SVGProps<SVGEllipseElement>\n      feBlend: dom.SVGProps<SVGFEBlendElement>\n      feColorMatrix: dom.SVGProps<SVGFEColorMatrixElement>\n      feComponentTransfer: dom.SVGProps<SVGFEComponentTransferElement>\n      feComposite: dom.SVGProps<SVGFECompositeElement>\n      feConvolveMatrix: dom.SVGProps<SVGFEConvolveMatrixElement>\n      feDiffuseLighting: dom.SVGProps<SVGFEDiffuseLightingElement>\n      feDisplacementMap: dom.SVGProps<SVGFEDisplacementMapElement>\n      feDistantLight: dom.SVGProps<SVGFEDistantLightElement>\n      feDropShadow: dom.SVGProps<SVGFEDropShadowElement>\n      feFlood: dom.SVGProps<SVGFEFloodElement>\n      feFuncA: dom.SVGProps<SVGFEFuncAElement>\n      feFuncB: dom.SVGProps<SVGFEFuncBElement>\n      feFuncG: dom.SVGProps<SVGFEFuncGElement>\n      feFuncR: dom.SVGProps<SVGFEFuncRElement>\n      feGaussianBlur: dom.SVGProps<SVGFEGaussianBlurElement>\n      feImage: dom.SVGProps<SVGFEImageElement>\n      feMerge: dom.SVGProps<SVGFEMergeElement>\n      feMergeNode: dom.SVGProps<SVGFEMergeNodeElement>\n      feMorphology: dom.SVGProps<SVGFEMorphologyElement>\n      feOffset: dom.SVGProps<SVGFEOffsetElement>\n      fePointLight: dom.SVGProps<SVGFEPointLightElement>\n      feSpecularLighting: dom.SVGProps<SVGFESpecularLightingElement>\n      feSpotLight: dom.SVGProps<SVGFESpotLightElement>\n      feTile: dom.SVGProps<SVGFETileElement>\n      feTurbulence: dom.SVGProps<SVGFETurbulenceElement>\n      filter: dom.SVGProps<SVGFilterElement>\n      foreignObject: dom.SVGProps<SVGForeignObjectElement>\n      g: dom.SVGProps<SVGGElement>\n      image: dom.SVGProps<SVGImageElement>\n      line: dom.SVGProps<SVGLineElement>\n      linearGradient: dom.SVGProps<SVGLinearGradientElement>\n      marker: dom.SVGProps<SVGMarkerElement>\n      mask: dom.SVGProps<SVGMaskElement>\n      metadata: dom.SVGProps<SVGMetadataElement>\n      mpath: dom.SVGProps<SVGMPathElement>\n      path: dom.SVGProps<SVGPathElement>\n      pattern: dom.SVGProps<SVGPatternElement>\n      polygon: dom.SVGProps<SVGPolygonElement>\n      polyline: dom.SVGProps<SVGPolylineElement>\n      radialGradient: dom.SVGProps<SVGRadialGradientElement>\n      rect: dom.SVGProps<SVGRectElement>\n      set: dom.SVGProps<SVGSetElement>\n      stop: dom.SVGProps<SVGStopElement>\n      switch: dom.SVGProps<SVGSwitchElement>\n      symbol: dom.SVGProps<SVGSymbolElement>\n      text: dom.SVGProps<SVGTextElement>\n      textPath: dom.SVGProps<SVGTextPathElement>\n      tspan: dom.SVGProps<SVGTSpanElement>\n      use: dom.SVGProps<SVGUseElement>\n      view: dom.SVGProps<SVGViewElement>\n    }\n\n    export interface IntrinsicMathMLElements {\n      annotation: dom.AnnotationMathMLProps<MathMLElement>\n      'annotation-xml': dom.AnnotationXmlMathMLProps<MathMLElement>\n      /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/maction */\n      maction: dom.MActionMathMLProps<MathMLElement>\n      math: dom.MathMathMLProps<MathMLElement>\n      /** This feature is non-standard. See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/menclose  */\n      menclose: dom.MEncloseMathMLProps<MathMLElement>\n      merror: dom.MErrorMathMLProps<MathMLElement>\n      /** @deprecated See https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mfenced */\n      mfenced: dom.MFencedMathMLProps<MathMLElement>\n      mfrac: dom.MFracMathMLProps<MathMLElement>\n      mi: dom.MiMathMLProps<MathMLElement>\n      mmultiscripts: dom.MmultiScriptsMathMLProps<MathMLElement>\n      mn: dom.MNMathMLProps<MathMLElement>\n      mo: dom.MOMathMLProps<MathMLElement>\n      mover: dom.MOverMathMLProps<MathMLElement>\n      mpadded: dom.MPaddedMathMLProps<MathMLElement>\n      mphantom: dom.MPhantomMathMLProps<MathMLElement>\n      mprescripts: dom.MPrescriptsMathMLProps<MathMLElement>\n      mroot: dom.MRootMathMLProps<MathMLElement>\n      mrow: dom.MRowMathMLProps<MathMLElement>\n      ms: dom.MSMathMLProps<MathMLElement>\n      mspace: dom.MSpaceMathMLProps<MathMLElement>\n      msqrt: dom.MSqrtMathMLProps<MathMLElement>\n      mstyle: dom.MStyleMathMLProps<MathMLElement>\n      msub: dom.MSubMathMLProps<MathMLElement>\n      msubsup: dom.MSubsupMathMLProps<MathMLElement>\n      msup: dom.MSupMathMLProps<MathMLElement>\n      mtable: dom.MTableMathMLProps<MathMLElement>\n      mtd: dom.MTdMathMLProps<MathMLElement>\n      mtext: dom.MTextMathMLProps<MathMLElement>\n      mtr: dom.MTrMathMLProps<MathMLElement>\n      munder: dom.MUnderMathMLProps<MathMLElement>\n      munderover: dom.MUnderMathMLProps<MathMLElement>\n      semantics: dom.SemanticsMathMLProps<MathMLElement>\n    }\n\n    export interface IntrinsicHTMLElements {\n      a: dom.AccessibleAnchorHTMLProps<HTMLAnchorElement>\n      abbr: dom.HTMLProps<HTMLElement>\n      address: dom.HTMLProps<HTMLElement>\n      area: dom.AccessibleAreaHTMLProps<HTMLAreaElement>\n      article: dom.ArticleHTMLProps<HTMLElement>\n      aside: dom.AsideHTMLProps<HTMLElement>\n      audio: dom.AudioHTMLProps<HTMLAudioElement>\n      b: dom.HTMLProps<HTMLElement>\n      base: dom.BaseHTMLProps<HTMLBaseElement>\n      bdi: dom.HTMLProps<HTMLElement>\n      bdo: dom.HTMLProps<HTMLElement>\n      big: dom.HTMLProps<HTMLElement>\n      blockquote: dom.BlockquoteHTMLProps<HTMLQuoteElement>\n      body: dom.HTMLProps<HTMLBodyElement>\n      br: dom.BrHTMLProps<HTMLBRElement>\n      button: dom.ButtonHTMLProps<HTMLButtonElement>\n      canvas: dom.CanvasHTMLProps<HTMLCanvasElement>\n      caption: dom.CaptionHTMLProps<HTMLTableCaptionElement>\n      cite: dom.HTMLProps<HTMLElement>\n      code: dom.HTMLProps<HTMLElement>\n      col: dom.ColHTMLProps<HTMLTableColElement>\n      colgroup: dom.ColgroupHTMLProps<HTMLTableColElement>\n      data: dom.DataHTMLProps<HTMLDataElement>\n      datalist: dom.DataListHTMLProps<HTMLDataListElement>\n      dd: dom.DdHTMLProps<HTMLElement>\n      del: dom.DelHTMLProps<HTMLModElement>\n      details: dom.DetailsHTMLProps<HTMLDetailsElement>\n      dfn: dom.HTMLProps<HTMLElement>\n      dialog: dom.DialogHTMLProps<HTMLDialogElement>\n      div: dom.HTMLProps<HTMLDivElement>\n      dl: dom.DlHTMLProps<HTMLDListElement>\n      dt: dom.DtHTMLProps<HTMLElement>\n      em: dom.HTMLProps<HTMLElement>\n      embed: dom.EmbedHTMLProps<HTMLEmbedElement>\n      fieldset: dom.FieldsetHTMLProps<HTMLFieldSetElement>\n      figcaption: dom.FigcaptionHTMLProps<HTMLElement>\n      figure: dom.HTMLProps<HTMLElement>\n      footer: dom.FooterHTMLProps<HTMLElement>\n      form: dom.FormHTMLProps<HTMLFormElement>\n      h1: dom.HeadingHTMLProps<HTMLHeadingElement>\n      h2: dom.HeadingHTMLProps<HTMLHeadingElement>\n      h3: dom.HeadingHTMLProps<HTMLHeadingElement>\n      h4: dom.HeadingHTMLProps<HTMLHeadingElement>\n      h5: dom.HeadingHTMLProps<HTMLHeadingElement>\n      h6: dom.HeadingHTMLProps<HTMLHeadingElement>\n      head: dom.HeadHTMLProps<HTMLHeadElement>\n      header: dom.HeaderHTMLProps<HTMLElement>\n      hgroup: dom.HTMLProps<HTMLElement>\n      hr: dom.HrHTMLProps<HTMLHRElement>\n      html: dom.HtmlHTMLProps<HTMLHtmlElement>\n      i: dom.HTMLProps<HTMLElement>\n      iframe: dom.IframeHTMLProps<HTMLIFrameElement>\n      img: dom.AccessibleImgHTMLProps<HTMLImageElement>\n      input: dom.AccessibleInputHTMLProps<HTMLInputElement>\n      ins: dom.InsHTMLProps<HTMLModElement>\n      kbd: dom.HTMLProps<HTMLElement>\n      keygen: dom.KeygenHTMLProps<HTMLUnknownElement>\n      label: dom.LabelHTMLProps<HTMLLabelElement>\n      legend: dom.LegendHTMLProps<HTMLLegendElement>\n      li: dom.LiHTMLProps<HTMLLIElement>\n      link: dom.LinkHTMLProps<HTMLLinkElement>\n      main: dom.MainHTMLProps<HTMLElement>\n      map: dom.MapHTMLProps<HTMLMapElement>\n      mark: dom.HTMLProps<HTMLElement>\n      marquee: dom.MarqueeHTMLProps<HTMLMarqueeElement>\n      menu: dom.MenuHTMLProps<HTMLMenuElement>\n      menuitem: dom.HTMLProps<HTMLUnknownElement>\n      meta: dom.MetaHTMLProps<HTMLMetaElement>\n      meter: dom.MeterHTMLProps<HTMLMeterElement>\n      nav: dom.NavHTMLProps<HTMLElement>\n      noscript: dom.NoScriptHTMLProps<HTMLElement>\n      object: dom.ObjectHTMLProps<HTMLObjectElement>\n      ol: dom.OlHTMLProps<HTMLOListElement>\n      optgroup: dom.OptgroupHTMLProps<HTMLOptGroupElement>\n      option: dom.OptionHTMLProps<HTMLOptionElement>\n      output: dom.OutputHTMLProps<HTMLOutputElement>\n      p: dom.HTMLProps<HTMLParagraphElement>\n      param: dom.ParamHTMLProps<HTMLParamElement>\n      picture: dom.PictureHTMLProps<HTMLPictureElement>\n      pre: dom.HTMLProps<HTMLPreElement>\n      progress: dom.ProgressHTMLProps<HTMLProgressElement>\n      q: dom.QuoteHTMLProps<HTMLQuoteElement>\n      rp: dom.HTMLProps<HTMLElement>\n      rt: dom.HTMLProps<HTMLElement>\n      ruby: dom.HTMLProps<HTMLElement>\n      s: dom.HTMLProps<HTMLElement>\n      samp: dom.HTMLProps<HTMLElement>\n      script: dom.ScriptHTMLProps<HTMLScriptElement>\n      search: dom.SearchHTMLProps<HTMLElement>\n      section: dom.HTMLProps<HTMLElement>\n      select: dom.AccessibleSelectHTMLProps<HTMLSelectElement>\n      slot: dom.SlotHTMLProps<HTMLSlotElement>\n      small: dom.HTMLProps<HTMLElement>\n      source: dom.SourceHTMLProps<HTMLSourceElement>\n      span: dom.HTMLProps<HTMLSpanElement>\n      strong: dom.HTMLProps<HTMLElement>\n      style: dom.StyleHTMLProps<HTMLStyleElement>\n      sub: dom.HTMLProps<HTMLElement>\n      summary: dom.HTMLProps<HTMLElement>\n      sup: dom.HTMLProps<HTMLElement>\n      table: dom.TableHTMLProps<HTMLTableElement>\n      tbody: dom.HTMLProps<HTMLTableSectionElement>\n      td: dom.TdHTMLProps<HTMLTableCellElement>\n      template: dom.TemplateHTMLProps<HTMLTemplateElement>\n      textarea: dom.TextareaHTMLProps<HTMLTextAreaElement>\n      tfoot: dom.HTMLProps<HTMLTableSectionElement>\n      th: dom.ThHTMLProps<HTMLTableCellElement>\n      thead: dom.HTMLProps<HTMLTableSectionElement>\n      time: dom.TimeHTMLProps<HTMLTimeElement>\n      title: dom.TitleHTMLProps<HTMLTitleElement>\n      tr: dom.HTMLProps<HTMLTableRowElement>\n      track: dom.TrackHTMLProps<HTMLTrackElement>\n      u: dom.UlHTMLProps<HTMLElement>\n      ul: dom.HTMLProps<HTMLUListElement>\n      var: dom.HTMLProps<HTMLElement>\n      video: dom.VideoHTMLProps<HTMLVideoElement>\n      wbr: dom.WbrHTMLProps<HTMLElement>\n    }\n\n    export interface IntrinsicElements\n      extends IntrinsicSVGElements,\n        IntrinsicMathMLElements,\n        IntrinsicHTMLElements {}\n  }\n}\n\nfunction normalizeElementProps(props: ElementProps | null | undefined): ElementProps {\n  if (!props) return {}\n  if (!('mix' in props)) return props\n\n  let { mix, ...rest } = props\n  let normalizedMix = normalizeMixValue(mix)\n  return normalizedMix === undefined ? rest : { ...rest, mix: normalizedMix }\n}\n\nfunction normalizeMixValue(mix: unknown): unknown[] | undefined {\n  if (mix == null) return undefined\n  if (Array.isArray(mix)) {\n    return mix.length === 0 ? undefined : [...mix]\n  }\n  return [mix]\n}\n"
  },
  {
    "path": "packages/component/src/lib/mixin.ts",
    "content": "import type { FrameHandle } from './component.ts'\nimport type { ElementProps, RemixElement } from './jsx.ts'\nimport type { Scheduler } from './scheduler.ts'\nimport type { SchedulerPhaseEvent } from './scheduler.ts'\nimport { TypedEventTarget } from './typed-event-target.ts'\nimport { invariant } from './invariant.ts'\n\ntype RebindNode<value, baseNode, boundNode> = value extends (\n  ...args: infer fnArgs\n) => infer fnResult\n  ? (...args: RebindTuple<fnArgs, baseNode, boundNode>) => RebindNode<fnResult, baseNode, boundNode>\n  : [value] extends [baseNode]\n    ? [baseNode] extends [value]\n      ? boundNode\n      : value\n    : value\n\ntype RebindTuple<args extends unknown[], baseNode, boundNode> = {\n  [index in keyof args]: RebindNode<args[index], baseNode, boundNode>\n}\n\nexport type MixinProps<\n  node extends EventTarget = Element,\n  props extends ElementProps = ElementProps,\n> = props & {\n  mix?: MixValue<node, props>\n}\n\nexport type MixinElement<\n  node extends EventTarget = Element,\n  props extends ElementProps = ElementProps,\n> = ((\n  handle: { update(): Promise<AbortSignal> },\n  setup: unknown,\n) => (props: MixinProps<node, props>) => RemixElement) & {\n  __rmxMixinElementType: string\n}\n\nexport type MixinInsertEvent<node extends EventTarget = Element> = Event & {\n  node: node\n  parent: ParentNode\n  key?: string\n}\n\nexport type MixinReclaimedEvent<node extends EventTarget = Element> = Event & {\n  node: node\n  parent: ParentNode\n  key?: string\n}\n\nexport type MixinUpdateEvent<node extends EventTarget = Element> = Event & {\n  node: node\n}\n\nexport type MixinBeforeRemoveEvent = Event & {\n  persistNode(teardown: (signal: AbortSignal) => void | Promise<void>): void\n}\n\ntype MixinHandleEventMap<node extends EventTarget = Element> = {\n  beforeRemove: MixinBeforeRemoveEvent\n  reclaimed: MixinReclaimedEvent<node>\n  remove: Event\n  insert: MixinInsertEvent<node>\n  beforeUpdate: MixinUpdateEvent<node>\n  commit: MixinUpdateEvent<node>\n}\n\n/**\n * Runtime handle passed to mixin setup functions.\n */\nexport type MixinHandle<\n  node extends EventTarget = Element,\n  props extends ElementProps = ElementProps,\n> = TypedEventTarget<MixinHandleEventMap<node>> & {\n  id: string\n  frame: FrameHandle\n  element: MixinElement<node, props>\n  signal: AbortSignal\n  update(): Promise<AbortSignal>\n  queueTask(task: (node: node, signal: AbortSignal) => void): void\n}\n\ntype MixinRuntimeType<\n  args extends unknown[] = [],\n  node extends EventTarget = Element,\n  props extends ElementProps = ElementProps,\n> = (\n  handle: MixinHandle<node, props>,\n  type: string,\n) =>\n  | ((\n      ...args: [...args, currentProps: props]\n    ) => void | null | RemixElement | MixinElement<node, props>)\n  | void\n\n/**\n * Public mixin setup function signature.\n */\nexport type MixinType<\n  node extends EventTarget = Element,\n  args extends unknown[] = [],\n  props extends ElementProps = ElementProps,\n> = (\n  handle: MixinHandle<node, props>,\n  type: string,\n) =>\n  | ((\n      ...args: [...args, currentProps: props]\n    ) => void | null | RemixElement | MixinElement<node, props>)\n  | void\n\n/**\n * Serializable descriptor stored in the `mix` prop.\n */\nexport type MixinDescriptor<\n  node extends EventTarget = Element,\n  args extends unknown[] = [],\n  props extends ElementProps = ElementProps,\n> = {\n  type: MixinRuntimeType<args, node, props>\n  args: args\n  readonly __node?: (node: node) => void\n}\n\n/**\n * Accepted value shape for the `mix` prop.\n */\nexport type MixValue<\n  node extends EventTarget = Element,\n  props extends ElementProps = ElementProps,\n> = MixinDescriptor<node, any, props> | ReadonlyArray<MixinDescriptor<node, any, props>>\n\ntype AnyMixinType = MixinRuntimeType<unknown[], Element, ElementProps>\ntype AnyMixinDescriptor = MixinDescriptor<Element, unknown[], ElementProps>\ntype AnyMixinRunner = (\n  ...args: [...unknown[], currentProps: ElementProps]\n) => void | null | RemixElement | MixinElement<Element, ElementProps>\ntype AnyMixinRunnerResult = ReturnType<AnyMixinRunner>\ntype AnyMixinSetupResult = ReturnType<AnyMixinType> | AnyMixinRunnerResult\ntype AnyMixinHandle = MixinHandle<Element, ElementProps>\ntype ScopedAnyMixinHandle = AnyMixinHandle & {\n  queueCommitTask(task: () => void): void\n  setActiveScope(scope?: symbol): void\n  dispatchScopedEvent(scope: symbol, event: Event): void\n  releaseScope(scope: symbol): void\n}\n\ntype RunnerEntry = {\n  type: AnyMixinType\n  runner: AnyMixinRunner\n  scope: symbol\n}\n\ntype MixinHandleFactoryOptions = {\n  id: string\n  hostType: string\n  frame: FrameHandle\n  scheduler: Scheduler\n  getSignal: () => AbortSignal\n  getBinding: () => MixinRuntimeBinding | undefined\n}\n\nexport type MixinRuntimeBinding = {\n  node: Element\n  parent: ParentNode\n  key?: string\n  target: unknown\n  frame: FrameHandle\n  scheduler: Scheduler\n  enqueueUpdate(done: (signal: AbortSignal) => void): void\n}\n\ntype ResolveMixedPropsInput = {\n  hostType: string\n  frame: FrameHandle\n  scheduler: Scheduler\n  props: ElementProps\n  state?: MixinRuntimeState\n}\n\ntype ResolveMixedPropsOutput = {\n  props: ElementProps\n  state: MixinRuntimeState\n}\n\nexport type MixinRuntimeState = {\n  id: string\n  controller?: AbortController\n  aborted: boolean\n  handle?: AnyMixinHandle\n  runners: RunnerEntry[]\n  binding?: MixinRuntimeBinding\n  removePrepared?: boolean\n  pendingRemoval?: {\n    signal: AbortSignal\n    cancel: (reason?: unknown) => void\n    done: Promise<void>\n  }\n}\n\nlet mixinHandleId = 0\n\n/**\n * Creates a typed mixin factory that can be passed through the `mix` prop.\n *\n * @param type Mixin setup function.\n * @returns A function that captures mixin arguments and returns a descriptor.\n */\nexport function createMixin<\n  node extends EventTarget = Element,\n  args extends unknown[] = [],\n  props extends ElementProps = ElementProps,\n>(type: MixinType<node, args, props>) {\n  return <boundNode extends node = node>(\n    ...args: RebindTuple<args, node, boundNode>\n  ): MixinDescriptor<boundNode, RebindTuple<args, node, boundNode>, props> => ({\n    type: type as unknown as MixinRuntimeType<RebindTuple<args, node, boundNode>, boundNode, props>,\n    args: args as RebindTuple<args, node, boundNode>,\n  })\n}\n\nexport function resolveMixedProps(input: ResolveMixedPropsInput): ResolveMixedPropsOutput {\n  let state = input.state ?? createMixinRuntimeState()\n  let handle = state.handle as ScopedAnyMixinHandle | undefined\n  if (!handle) {\n    handle = createMixinHandle({\n      id: state.id,\n      hostType: input.hostType,\n      frame: input.frame,\n      scheduler: input.scheduler,\n      getSignal: () => getMixinRuntimeSignal(state),\n      getBinding: () => state.binding,\n    }) as ScopedAnyMixinHandle\n    state.handle = handle\n  }\n  let hostType = input.hostType\n  let descriptors = resolveMixDescriptors(input.props)\n  let composedProps = withoutMix(input.props)\n  let maxDescriptors = 1024\n\n  for (let index = 0; index < descriptors.length && index < maxDescriptors; index++) {\n    let descriptor = descriptors[index]\n    let entry = state.runners[index]\n    if (!entry || entry.type !== descriptor.type) {\n      if (entry) {\n        queueMixinRemove(handle, entry.scope)\n      }\n      let scope = Symbol('mixin-scope')\n      handle.setActiveScope(scope)\n      entry = {\n        scope,\n        type: descriptor.type as AnyMixinType,\n        runner: normalizeMixinRunner(\n          descriptor.type(handle, hostType) as AnyMixinSetupResult,\n          handle,\n        ),\n      }\n      handle.setActiveScope(undefined)\n      state.runners[index] = entry\n      let binding = state.binding\n      if (binding?.node) {\n        queueMixinInsert(handle, entry.scope, binding.node, binding.parent, binding.key)\n      }\n    }\n\n    handle.setActiveScope(entry.scope)\n    let result = entry.runner(...descriptor.args, composedProps)\n    handle.setActiveScope(undefined)\n    if (!result) continue\n    if (isMixinElement(result)) continue\n\n    if (!isRemixElement(result)) {\n      console.error(new Error('mixins must return a remix element'))\n      continue\n    }\n\n    let resultType =\n      typeof result.type === 'string'\n        ? result.type\n        : isMixinElement(result.type)\n          ? result.type.__rmxMixinElementType\n          : null\n    if (resultType !== hostType) {\n      console.error(new Error('mixins must return an element with the same host type'))\n      continue\n    }\n\n    if (result.type !== resultType) {\n      result = { ...result, type: resultType }\n    }\n\n    let nestedDescriptors = resolveMixDescriptors(result.props)\n    for (let nested of nestedDescriptors) descriptors.push(nested)\n    composedProps = composeMixinProps(composedProps, withoutMix(result.props))\n  }\n\n  for (let index = descriptors.length; index < state.runners.length; index++) {\n    let entry = state.runners[index]\n    if (entry) {\n      handle.dispatchScopedEvent(entry.scope, new Event('remove'))\n      handle.releaseScope(entry.scope)\n    }\n  }\n\n  if (state.runners.length > descriptors.length) {\n    state.runners.length = descriptors.length\n  }\n\n  let nextMix = input.props.mix\n  return {\n    state,\n    props: {\n      ...composedProps,\n      ...(nextMix === undefined ? {} : { mix: nextMix }),\n    },\n  }\n}\n\nexport function teardownMixins(state?: MixinRuntimeState) {\n  if (!state) return\n  state.binding = undefined\n  prepareMixinRemoval(state)\n  cancelPendingMixinRemoval(state)\n  let handle = state.handle as ScopedAnyMixinHandle | undefined\n  if (handle) {\n    handle.queueCommitTask(() => finalizeMixinTeardown(state))\n    return\n  }\n  finalizeMixinTeardown(state)\n}\n\nexport function bindMixinRuntime(\n  state: MixinRuntimeState | undefined,\n  binding?: MixinRuntimeBinding,\n  options?: { dispatchReclaimed?: boolean },\n) {\n  if (!state) return\n  let previousNode = state.binding?.node\n  let nextBinding = binding\n  state.binding = nextBinding\n  if (!nextBinding?.node || previousNode === nextBinding.node) return\n  let nextNode = nextBinding.node\n  let handle = state.handle as ScopedAnyMixinHandle | undefined\n  if (!handle) return\n  for (let entry of state.runners) {\n    if (options?.dispatchReclaimed) {\n      queueMixinReclaimed(handle, entry.scope, nextNode, nextBinding.parent, nextBinding.key)\n    } else {\n      queueMixinInsert(handle, entry.scope, nextNode, nextBinding.parent, nextBinding.key)\n    }\n  }\n}\n\nexport function prepareMixinRemoval(state?: MixinRuntimeState) {\n  if (!state || state.removePrepared) return state?.pendingRemoval?.done\n  state.removePrepared = true\n\n  let pendingRemoval: MixinRuntimeState['pendingRemoval']\n  let persistTeardowns: Array<(signal: AbortSignal) => void | Promise<void>> = []\n  let registerPersistNode = (teardown: (signal: AbortSignal) => void | Promise<void>) => {\n    persistTeardowns.push(teardown)\n  }\n\n  let handle = state.handle as ScopedAnyMixinHandle | undefined\n  if (!handle) return\n  for (let entry of state.runners) {\n    dispatchMixinBeforeRemove(handle, entry.scope, registerPersistNode)\n  }\n\n  if (persistTeardowns.length > 0) {\n    let controller = new AbortController()\n    let done = Promise.allSettled(\n      persistTeardowns.map((teardown) => Promise.resolve().then(() => teardown(controller.signal))),\n    ).then(() => {})\n    pendingRemoval = {\n      signal: controller.signal,\n      cancel(reason) {\n        controller.abort(reason)\n      },\n      done,\n    }\n  }\n\n  state.pendingRemoval = pendingRemoval\n  return pendingRemoval?.done\n}\n\nexport function cancelPendingMixinRemoval(\n  state?: MixinRuntimeState,\n  reason: unknown = new DOMException('', 'AbortError'),\n) {\n  if (!state?.pendingRemoval) return\n  state.pendingRemoval.cancel(reason)\n  state.pendingRemoval = undefined\n  state.removePrepared = false\n}\n\nfunction createMixinRuntimeState(): MixinRuntimeState {\n  return {\n    id: `m${++mixinHandleId}`,\n    aborted: false,\n    runners: [],\n  }\n}\n\nfunction createMixinHandle(options: {\n  id: string\n  hostType: string\n  frame: FrameHandle\n  scheduler: Scheduler\n  getSignal: () => AbortSignal\n  getBinding: () => MixinRuntimeBinding | undefined\n}): AnyMixinHandle {\n  return new MixinHandleImpl(options)\n}\n\nclass MixinHandleImpl\n  extends TypedEventTarget<MixinHandleEventMap<Element>>\n  implements ScopedAnyMixinHandle\n{\n  id: string\n  frame: FrameHandle\n  element: MixinElement<Element, ElementProps>\n  #options: MixinHandleFactoryOptions\n  #phaseListenerCounts: Record<'beforeUpdate' | 'commit', number> = {\n    beforeUpdate: 0,\n    commit: 0,\n  }\n  #activeScope?: symbol\n  #scopeTargets = new Map<symbol, TypedEventTarget<MixinHandleEventMap<Element>>>()\n  #scopePhaseCounts = new Map<symbol, Record<'beforeUpdate' | 'commit', number>>()\n  #onSchedulerBeforeUpdate = (event: Event) => {\n    this.#dispatchSchedulerPhaseToHandle('beforeUpdate', event as SchedulerPhaseEvent)\n  }\n  #onSchedulerCommit = (event: Event) => {\n    this.#dispatchSchedulerPhaseToHandle('commit', event as SchedulerPhaseEvent)\n  }\n\n  constructor(options: MixinHandleFactoryOptions) {\n    super()\n    this.#options = options\n    this.id = options.id\n    this.frame = options.frame\n\n    let element = ((_: { update(): Promise<AbortSignal> }, __: unknown) =>\n      (props: ElementProps) => ({\n        $rmx: true as const,\n        type: options.hostType,\n        key: null,\n        props,\n      })) as unknown as MixinElement<Element, ElementProps>\n    element.__rmxMixinElementType = options.hostType\n    this.element = element\n  }\n\n  get signal() {\n    return this.#options.getSignal()\n  }\n\n  addEventListener(\n    type: string,\n    listener: EventListenerOrEventListenerObject | null,\n    options?: AddEventListenerOptions | boolean,\n  ): void {\n    let target = this.#getActiveScopeTarget()\n    target.addEventListener(\n      type as keyof MixinHandleEventMap<Element>,\n      listener as EventListener,\n      options,\n    )\n    if (!listener || !isSchedulerPhaseType(type)) return\n    let scope = this.#activeScope\n    invariant(scope)\n    let scopePhaseCounts = this.#scopePhaseCounts.get(scope)\n    invariant(scopePhaseCounts)\n    scopePhaseCounts[type] += 1\n    this.#phaseListenerCounts[type] += 1\n    if (this.#phaseListenerCounts[type] !== 1) return\n    if (type === 'beforeUpdate') {\n      this.#options.scheduler.addEventListener('beforeUpdate', this.#onSchedulerBeforeUpdate)\n    } else {\n      this.#options.scheduler.addEventListener('commit', this.#onSchedulerCommit)\n    }\n  }\n\n  removeEventListener(\n    type: string,\n    listener: EventListenerOrEventListenerObject | null,\n    options?: EventListenerOptions | boolean,\n  ): void {\n    let target = this.#getActiveScopeTarget()\n    target.removeEventListener(\n      type as keyof MixinHandleEventMap<Element>,\n      listener as EventListener,\n      typeof options === 'boolean' ? { capture: options } : options,\n    )\n    if (!listener || !isSchedulerPhaseType(type)) return\n    let scope = this.#activeScope\n    invariant(scope)\n    let scopePhaseCounts = this.#scopePhaseCounts.get(scope)\n    invariant(scopePhaseCounts)\n    scopePhaseCounts[type] = Math.max(0, scopePhaseCounts[type] - 1)\n    this.#phaseListenerCounts[type] = Math.max(0, this.#phaseListenerCounts[type] - 1)\n    if (this.#phaseListenerCounts[type] !== 0) return\n    if (type === 'beforeUpdate') {\n      this.#options.scheduler.removeEventListener('beforeUpdate', this.#onSchedulerBeforeUpdate)\n    } else {\n      this.#options.scheduler.removeEventListener('commit', this.#onSchedulerCommit)\n    }\n  }\n\n  update(): Promise<AbortSignal> {\n    return new Promise((resolve) => {\n      let signal = this.#options.getSignal()\n      if (signal.aborted) {\n        resolve(signal)\n        return\n      }\n      let binding = this.#options.getBinding()\n      if (!binding) {\n        resolve(signal)\n        return\n      }\n      binding.enqueueUpdate(resolve)\n    })\n  }\n\n  queueTask(task: (node: Element, signal: AbortSignal) => void): void {\n    this.#options.scheduler.enqueueTasks([\n      () => {\n        let binding = this.#options.getBinding()\n        invariant(binding)\n        task(binding.node, this.#options.getSignal())\n      },\n    ])\n  }\n\n  queueCommitTask(task: () => void): void {\n    this.#options.scheduler.enqueueCommitPhase([task])\n  }\n\n  setActiveScope(scope?: symbol): void {\n    this.#activeScope = scope\n    if (!scope) return\n    if (this.#scopeTargets.has(scope)) return\n    this.#scopeTargets.set(scope, new TypedEventTarget<MixinHandleEventMap<Element>>())\n    this.#scopePhaseCounts.set(scope, { beforeUpdate: 0, commit: 0 })\n  }\n\n  dispatchScopedEvent(scope: symbol, event: Event): void {\n    let previousScope = this.#activeScope\n    this.#activeScope = scope\n    this.#scopeTargets.get(scope)?.dispatchEvent(event)\n    this.#activeScope = previousScope\n  }\n\n  releaseScope(scope: symbol): void {\n    let scopePhaseCounts = this.#scopePhaseCounts.get(scope)\n    if (scopePhaseCounts) {\n      this.#decrementGlobalPhaseCount('beforeUpdate', scopePhaseCounts.beforeUpdate)\n      this.#decrementGlobalPhaseCount('commit', scopePhaseCounts.commit)\n    }\n    this.#scopePhaseCounts.delete(scope)\n    this.#scopeTargets.delete(scope)\n    if (this.#activeScope === scope) {\n      this.#activeScope = undefined\n    }\n  }\n\n  #dispatchSchedulerPhaseToHandle(type: 'beforeUpdate' | 'commit', event: SchedulerPhaseEvent) {\n    let binding = this.#options.getBinding()\n    if (!binding) return\n    if (!isBindingInUpdateScope(binding, event.parents)) return\n    for (let [, target] of this.#scopeTargets) {\n      let updateEvent = new Event(type) as MixinUpdateEvent<Element>\n      updateEvent.node = binding.node\n      target.dispatchEvent(updateEvent)\n    }\n  }\n\n  #getActiveScopeTarget(): TypedEventTarget<MixinHandleEventMap<Element>> {\n    let scope = this.#activeScope\n    invariant(scope)\n    let target = this.#scopeTargets.get(scope)\n    invariant(target)\n    return target\n  }\n\n  #decrementGlobalPhaseCount(type: 'beforeUpdate' | 'commit', amount: number) {\n    if (amount <= 0) return\n    this.#phaseListenerCounts[type] = Math.max(0, this.#phaseListenerCounts[type] - amount)\n    if (this.#phaseListenerCounts[type] !== 0) return\n    if (type === 'beforeUpdate') {\n      this.#options.scheduler.removeEventListener('beforeUpdate', this.#onSchedulerBeforeUpdate)\n    } else {\n      this.#options.scheduler.removeEventListener('commit', this.#onSchedulerCommit)\n    }\n  }\n}\n\nexport function getMixinRuntimeSignal(state: MixinRuntimeState): AbortSignal {\n  let controller = state.controller\n  if (!controller) {\n    controller = new AbortController()\n    if (state.aborted) {\n      controller.abort()\n    }\n    state.controller = controller\n  }\n  return controller.signal\n}\n\nexport function dispatchMixinBeforeUpdate(state?: MixinRuntimeState) {\n  dispatchMixinUpdateEvent(state, 'beforeUpdate')\n}\n\nexport function dispatchMixinCommit(state?: MixinRuntimeState) {\n  dispatchMixinUpdateEvent(state, 'commit')\n}\n\nfunction dispatchMixinInsert(\n  handle: ScopedAnyMixinHandle,\n  scope: symbol,\n  node: Element,\n  parent: ParentNode,\n  key?: string,\n) {\n  let event = new Event('insert') as MixinInsertEvent<Element>\n  event.node = node\n  event.parent = parent\n  event.key = key\n  handle.dispatchScopedEvent(scope, event)\n}\n\nfunction dispatchMixinReclaimed(\n  handle: ScopedAnyMixinHandle,\n  scope: symbol,\n  node: Element,\n  parent: ParentNode,\n  key?: string,\n) {\n  let event = new Event('reclaimed') as MixinReclaimedEvent<Element>\n  event.node = node\n  event.parent = parent\n  event.key = key\n  handle.dispatchScopedEvent(scope, event)\n}\n\nfunction dispatchMixinBeforeRemove(\n  handle: ScopedAnyMixinHandle,\n  scope: symbol,\n  persistNode: (teardown: (signal: AbortSignal) => void | Promise<void>) => void,\n) {\n  let event = new Event('beforeRemove') as MixinBeforeRemoveEvent\n  event.persistNode = persistNode\n  handle.dispatchScopedEvent(scope, event)\n}\n\nfunction queueMixinInsert(\n  handle: ScopedAnyMixinHandle,\n  scope: symbol,\n  node: Element,\n  parent: ParentNode,\n  key?: string,\n) {\n  handle.queueCommitTask(() => {\n    dispatchMixinInsert(handle, scope, node, parent, key)\n  })\n}\n\nfunction queueMixinReclaimed(\n  handle: ScopedAnyMixinHandle,\n  scope: symbol,\n  node: Element,\n  parent: ParentNode,\n  key?: string,\n) {\n  handle.queueCommitTask(() => {\n    dispatchMixinReclaimed(handle, scope, node, parent, key)\n  })\n}\n\nfunction queueMixinRemove(handle: ScopedAnyMixinHandle, scope: symbol) {\n  handle.queueCommitTask(() => {\n    handle.dispatchScopedEvent(scope, new Event('remove'))\n    handle.releaseScope(scope)\n  })\n}\n\nfunction dispatchMixinRemoveEvent(state?: MixinRuntimeState) {\n  let runners = state?.runners\n  if (!runners?.length) return\n  let handle = state?.handle as ScopedAnyMixinHandle | undefined\n  if (!handle) return\n  for (let entry of runners) {\n    handle.dispatchScopedEvent(entry.scope, new Event('remove'))\n  }\n}\n\nfunction finalizeMixinTeardown(state: MixinRuntimeState) {\n  dispatchMixinRemoveEvent(state)\n  let handle = state.handle as ScopedAnyMixinHandle | undefined\n  if (handle) {\n    for (let entry of state.runners) {\n      handle.releaseScope(entry.scope)\n    }\n  }\n  state.runners.length = 0\n  state.aborted = true\n  state.controller?.abort()\n  state.pendingRemoval = undefined\n  state.removePrepared = true\n  state.handle = undefined\n}\n\nfunction dispatchMixinUpdateEvent(\n  state: MixinRuntimeState | undefined,\n  type: 'beforeUpdate' | 'commit',\n) {\n  let node = state?.binding?.node\n  if (!node) return\n  let runners = state?.runners\n  if (!runners?.length) return\n  let handle = state?.handle as ScopedAnyMixinHandle | undefined\n  if (!handle) return\n  for (let entry of runners) {\n    let event = new Event(type) as MixinUpdateEvent<Element>\n    event.node = node\n    handle.dispatchScopedEvent(entry.scope, event)\n  }\n}\n\nfunction isSchedulerPhaseType(type: string): type is 'beforeUpdate' | 'commit' {\n  return type === 'beforeUpdate' || type === 'commit'\n}\n\nfunction isBindingInUpdateScope(binding: MixinRuntimeBinding, parents: ParentNode[]): boolean {\n  if (parents.length === 0) return false\n  let node = binding.node as Node\n  for (let parent of parents) {\n    let parentNode = parent as Node\n    if (parentNode === node) return true\n    if (parentNode.contains(node)) return true\n  }\n  return false\n}\n\nfunction resolveMixDescriptors(props: ElementProps): AnyMixinDescriptor[] {\n  let mix = props.mix\n  if (mix == null) return []\n  if (Array.isArray(mix)) {\n    if (mix.length === 0) return []\n    return [...mix] as AnyMixinDescriptor[]\n  }\n  return [mix] as AnyMixinDescriptor[]\n}\n\nfunction withoutMix(props: ElementProps): ElementProps {\n  if (!('mix' in props)) return props\n  let output = { ...props }\n  delete output.mix\n  return output\n}\n\nfunction composeMixinProps(previous: ElementProps, next: ElementProps): ElementProps {\n  return { ...previous, ...next }\n}\n\nfunction isRemixElement(value: unknown): value is RemixElement {\n  if (!value || typeof value !== 'object') return false\n  return (value as { $rmx?: unknown }).$rmx === true\n}\n\nfunction isMixinElement(value: unknown): value is MixinElement<Element, ElementProps> {\n  if (typeof value !== 'function') return false\n  return '__rmxMixinElementType' in value\n}\n\nfunction normalizeMixinRunner(result: AnyMixinSetupResult, handle: AnyMixinHandle): AnyMixinRunner {\n  if (typeof result === 'function' && !isMixinElement(result)) {\n    return result as AnyMixinRunner\n  }\n  if (result === undefined) {\n    return () => handle.element\n  }\n  return () => result as AnyMixinRunnerResult\n}\n"
  },
  {
    "path": "packages/component/src/lib/mixins/animate-layout-mixin.test.tsx",
    "content": "import { afterEach, beforeEach, describe, expect, it } from 'vitest'\nimport { createRoot } from '../vdom.ts'\nimport { animateLayout } from './animate-layout-mixin.tsx'\nimport { invariant } from '../invariant.ts'\n\ninterface MockAnimation {\n  keyframes: Keyframe[]\n  options: KeyframeAnimationOptions\n  playState: AnimationPlayState\n  finished: Promise<Animation>\n  cancel: () => void\n}\n\nlet originalAnimate: typeof Element.prototype.animate\nlet originalRaf: typeof globalThis.requestAnimationFrame\nlet mockAnimations: MockAnimation[] = []\n\nfunction createMockAnimation(\n  keyframes: Keyframe[],\n  options: KeyframeAnimationOptions,\n): MockAnimation {\n  let resolveFinished!: () => void\n  let finished = new Promise<Animation>((_resolve) => {\n    resolveFinished = () => _resolve({} as Animation)\n  })\n  return {\n    keyframes,\n    options,\n    playState: 'running',\n    finished,\n    cancel() {\n      this.playState = 'idle'\n      resolveFinished()\n    },\n  }\n}\n\nfunction mockBoundingRect(\n  el: Element,\n  rect: { left: number; top: number; right: number; bottom: number },\n) {\n  el.getBoundingClientRect = () =>\n    ({\n      left: rect.left,\n      top: rect.top,\n      right: rect.right,\n      bottom: rect.bottom,\n      width: rect.right - rect.left,\n      height: rect.bottom - rect.top,\n      x: rect.left,\n      y: rect.top,\n      toJSON() {\n        return this\n      },\n    }) as DOMRect\n}\n\nfunction mockBoundingRectSequence(\n  el: Element,\n  rects: Array<{ left: number; top: number; right: number; bottom: number }>,\n) {\n  let index = 0\n  el.getBoundingClientRect = () => {\n    let next = rects[Math.min(index, rects.length - 1)]\n    index++\n    return {\n      left: next.left,\n      top: next.top,\n      right: next.right,\n      bottom: next.bottom,\n      width: next.right - next.left,\n      height: next.bottom - next.top,\n      x: next.left,\n      y: next.top,\n      toJSON() {\n        return this\n      },\n    } as DOMRect\n  }\n}\n\ndescribe('animateLayout mixin', () => {\n  beforeEach(() => {\n    mockAnimations = []\n    originalAnimate = Element.prototype.animate\n    originalRaf = globalThis.requestAnimationFrame\n    globalThis.requestAnimationFrame = (callback: FrameRequestCallback) => {\n      queueMicrotask(() => callback(performance.now() + 1000))\n      return 0\n    }\n    Element.prototype.animate = function (keyframes, options) {\n      let animation = createMockAnimation(\n        keyframes as Keyframe[],\n        options as KeyframeAnimationOptions,\n      ) as unknown as Animation\n      mockAnimations.push(animation as unknown as MockAnimation)\n      return animation\n    }\n  })\n\n  afterEach(() => {\n    Element.prototype.animate = originalAnimate\n    globalThis.requestAnimationFrame = originalRaf\n    document.body.innerHTML = ''\n  })\n\n  it('animates when layout geometry changes', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(<div data-tick=\"0\" mix={[animateLayout({ duration: 350, easing: 'linear' })]} />)\n    root.flush()\n    let node = container.querySelector('div')\n    invariant(node)\n\n    mockBoundingRect(node, { left: 0, top: 0, right: 100, bottom: 100 })\n    root.render(<div data-tick=\"1\" mix={[animateLayout({ duration: 350, easing: 'linear' })]} />)\n    root.flush()\n    mockAnimations = []\n\n    mockBoundingRectSequence(node, [\n      { left: 0, top: 0, right: 100, bottom: 100 },\n      { left: 40, top: 10, right: 140, bottom: 110 },\n    ])\n    root.render(<div data-tick=\"2\" mix={[animateLayout({ duration: 350, easing: 'linear' })]} />)\n    root.flush()\n\n    expect(mockAnimations).toHaveLength(1)\n    let animation = mockAnimations[0]\n    expect(animation.options.duration).toBe(350)\n    expect(animation.options.easing).toBe('linear')\n  })\n\n  it('does not animate when geometry does not change', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(<div data-tick=\"0\" mix={[animateLayout()]} />)\n    root.flush()\n    let node = container.querySelector('div')\n    invariant(node)\n\n    mockBoundingRect(node, { left: 5, top: 5, right: 105, bottom: 105 })\n    root.render(<div data-tick=\"1\" mix={[animateLayout()]} />)\n    root.flush()\n    mockAnimations = []\n\n    mockBoundingRectSequence(node, [\n      { left: 5, top: 5, right: 105, bottom: 105 },\n      { left: 5, top: 5, right: 105, bottom: 105 },\n    ])\n    root.render(<div data-tick=\"2\" mix={[animateLayout()]} />)\n    root.flush()\n\n    expect(mockAnimations).toHaveLength(0)\n  })\n\n  it('cancels an in-flight layout animation when interrupted', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(<div data-tick=\"0\" mix={[animateLayout()]} />)\n    root.flush()\n    let node = container.querySelector('div')\n    invariant(node)\n\n    mockBoundingRect(node, { left: 0, top: 0, right: 100, bottom: 100 })\n    root.render(<div data-tick=\"1\" mix={[animateLayout()]} />)\n    root.flush()\n    mockAnimations = []\n\n    mockBoundingRectSequence(node, [\n      { left: 0, top: 0, right: 100, bottom: 100 },\n      { left: 30, top: 0, right: 130, bottom: 100 },\n    ])\n    root.render(<div data-tick=\"2\" mix={[animateLayout()]} />)\n    root.flush()\n    let firstAnimation = mockAnimations[0]\n    expect(firstAnimation.playState).toBe('running')\n\n    mockBoundingRectSequence(node, [\n      { left: 30, top: 0, right: 130, bottom: 100 },\n      { left: 60, top: 0, right: 160, bottom: 100 },\n    ])\n    root.render(<div data-tick=\"3\" mix={[animateLayout()]} />)\n    root.flush()\n\n    expect(firstAnimation.playState).toBe('idle')\n    expect(mockAnimations).toHaveLength(2)\n  })\n\n  it('cancels active animation on remove', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(<div data-tick=\"0\" mix={[animateLayout()]} />)\n    root.flush()\n    let node = container.querySelector('div')\n    invariant(node)\n\n    mockBoundingRect(node, { left: 0, top: 0, right: 100, bottom: 100 })\n    root.render(<div data-tick=\"1\" mix={[animateLayout()]} />)\n    root.flush()\n    mockAnimations = []\n\n    mockBoundingRectSequence(node, [\n      { left: 0, top: 0, right: 100, bottom: 100 },\n      { left: 30, top: 0, right: 130, bottom: 100 },\n    ])\n    root.render(<div data-tick=\"2\" mix={[animateLayout()]} />)\n    root.flush()\n    let active = mockAnimations[0]\n    expect(active.playState).toBe('running')\n\n    root.render(null)\n    root.flush()\n\n    expect(active.playState).toBe('idle')\n  })\n})\n"
  },
  {
    "path": "packages/component/src/lib/mixins/animate-layout-mixin.tsx",
    "content": "import { createMixin } from '../mixin.ts'\nimport type { ElementProps } from '../jsx.ts'\nimport type { MixinDescriptor } from '../mixin.ts'\nimport type { LayoutAnimationConfig } from '../dom.ts'\n\ntype LayoutConfig = true | false | null | undefined | LayoutAnimationConfig\n\ntype Axis = { min: number; max: number }\ntype Box = { x: Axis; y: Axis }\ntype AxisDelta = { translate: number; scale: number; origin: number; originPoint: number }\ntype Delta = { x: AxisDelta; y: AxisDelta }\n\nconst DEFAULT_DURATION = 200\nconst DEFAULT_EASING = 'ease-out'\nconst SCALE_PRECISION = 0.0001\nconst TRANSLATE_PRECISION = 0.01\n\nfunction createAxisDelta(): AxisDelta {\n  return { translate: 0, scale: 1, origin: 0.5, originPoint: 0 }\n}\n\nfunction createDelta(): Delta {\n  return { x: createAxisDelta(), y: createAxisDelta() }\n}\n\nfunction mix(from: number, to: number, progress: number): number {\n  return from + (to - from) * progress\n}\n\nfunction isNear(value: number, target: number, threshold: number): boolean {\n  return Math.abs(value - target) <= threshold\n}\n\nfunction calcLength(axis: Axis): number {\n  return axis.max - axis.min\n}\n\nfunction calcAxisDelta(delta: AxisDelta, source: Axis, target: Axis, origin: number = 0.5): void {\n  delta.origin = origin\n  delta.originPoint = mix(source.min, source.max, origin)\n\n  let sourceLength = calcLength(source)\n  let targetLength = calcLength(target)\n  delta.scale = sourceLength !== 0 ? targetLength / sourceLength : 1\n\n  let targetOriginPoint = mix(target.min, target.max, origin)\n  delta.translate = targetOriginPoint - delta.originPoint\n\n  if (isNear(delta.scale, 1, SCALE_PRECISION) || Number.isNaN(delta.scale)) {\n    delta.scale = 1\n  }\n  if (isNear(delta.translate, 0, TRANSLATE_PRECISION) || Number.isNaN(delta.translate)) {\n    delta.translate = 0\n  }\n}\n\nfunction calcBoxDelta(delta: Delta, source: Box, target: Box): void {\n  calcAxisDelta(delta.x, source.x, target.x, 0.5)\n  calcAxisDelta(delta.y, source.y, target.y, 0.5)\n}\n\nfunction mixAxisDelta(output: AxisDelta, delta: AxisDelta, progress: number): void {\n  output.translate = mix(delta.translate, 0, progress)\n  output.scale = mix(delta.scale, 1, progress)\n  output.origin = delta.origin\n  output.originPoint = delta.originPoint\n}\n\nfunction mixDelta(output: Delta, delta: Delta, progress: number): void {\n  mixAxisDelta(output.x, delta.x, progress)\n  mixAxisDelta(output.y, delta.y, progress)\n}\n\nfunction copyAxisDeltaInto(target: AxisDelta, source: AxisDelta): void {\n  target.translate = source.translate\n  target.scale = source.scale\n  target.origin = source.origin\n  target.originPoint = source.originPoint\n}\n\nfunction copyDeltaInto(target: Delta, source: Delta): void {\n  copyAxisDeltaInto(target.x, source.x)\n  copyAxisDeltaInto(target.y, source.y)\n}\n\nfunction isDeltaZero(delta: Delta): boolean {\n  return (\n    isNear(delta.x.translate, 0, TRANSLATE_PRECISION) &&\n    isNear(delta.y.translate, 0, TRANSLATE_PRECISION) &&\n    isNear(delta.x.scale, 1, SCALE_PRECISION) &&\n    isNear(delta.y.scale, 1, SCALE_PRECISION)\n  )\n}\n\nfunction buildProjectionTransform(delta: Delta): string {\n  let transform = ''\n  if (delta.x.translate || delta.y.translate) {\n    transform = `translate3d(${delta.x.translate}px, ${delta.y.translate}px, 0)`\n  }\n  if (delta.x.scale !== 1 || delta.y.scale !== 1) {\n    transform += transform ? ' ' : ''\n    transform += `scale(${delta.x.scale}, ${delta.y.scale})`\n  }\n  return transform || 'none'\n}\n\nfunction buildTransformOrigin(delta: Delta): string {\n  return `${delta.x.origin * 100}% ${delta.y.origin * 100}%`\n}\n\nfunction rectToBox(rect: DOMRect): Box {\n  return {\n    x: { min: rect.left, max: rect.right },\n    y: { min: rect.top, max: rect.bottom },\n  }\n}\n\nfunction measureNaturalBox(node: HTMLElement): Box {\n  let prevTransform = node.style.transform\n  let prevOrigin = node.style.transformOrigin\n  node.style.transform = 'none'\n  node.style.transformOrigin = ''\n  let rect = node.getBoundingClientRect()\n  node.style.transform = prevTransform\n  node.style.transformOrigin = prevOrigin\n  return rectToBox(rect)\n}\n\nfunction resolveLayoutConfig(config: LayoutConfig): LayoutAnimationConfig | null {\n  if (!config) return null\n  if (config === true) return {}\n  return config\n}\n\nlet animateLayoutMixin = createMixin<Element, [config?: LayoutConfig], ElementProps>((handle) => {\n  let snapshot: Box | null = null\n  let currentConfig: LayoutConfig = true\n  let currentDelta: Delta | null = null\n  let animationProgress = 0\n  let animation: Animation | null = null\n\n  let scheduleProgressTracking = (duration: number, active: Animation) => {\n    let start = performance.now()\n    let tick = () => {\n      if (animation !== active) return\n      animationProgress = Math.min(1, (performance.now() - start) / duration)\n      if (animationProgress < 1) {\n        requestAnimationFrame(tick)\n      }\n    }\n    requestAnimationFrame(tick)\n  }\n\n  let clearProjectionStyles = (node: HTMLElement) => {\n    node.style.transform = ''\n    node.style.transformOrigin = ''\n  }\n\n  let resetAnimation = () => {\n    animation = null\n    currentDelta = null\n    animationProgress = 0\n  }\n\n  handle.addEventListener('beforeUpdate', (event) => {\n    let layoutConfig = resolveLayoutConfig(currentConfig)\n    if (!layoutConfig) return\n    snapshot = measureNaturalBox(event.node as HTMLElement)\n  })\n\n  handle.addEventListener('commit', (event) => {\n    let layoutConfig = resolveLayoutConfig(currentConfig)\n    let htmlNode = event.node as HTMLElement\n    let latest = measureNaturalBox(htmlNode)\n\n    if (!layoutConfig) {\n      animation?.cancel()\n      clearProjectionStyles(htmlNode)\n      resetAnimation()\n      snapshot = latest\n      return\n    }\n\n    if (!snapshot) {\n      snapshot = latest\n      return\n    }\n\n    let targetDelta = createDelta()\n    calcBoxDelta(targetDelta, latest, snapshot)\n\n    if (isDeltaZero(targetDelta)) {\n      snapshot = latest\n      return\n    }\n\n    if (animation && animation.playState === 'running') {\n      animation.cancel()\n      if (currentDelta && animationProgress > 0 && animationProgress < 1) {\n        let visual = createDelta()\n        mixDelta(visual, currentDelta, animationProgress)\n        targetDelta.x.translate += visual.x.translate\n        targetDelta.y.translate += visual.y.translate\n        targetDelta.x.scale *= visual.x.scale\n        targetDelta.y.scale *= visual.y.scale\n      }\n    }\n\n    if (!currentDelta) currentDelta = createDelta()\n    copyDeltaInto(currentDelta, targetDelta)\n    animationProgress = 0\n\n    let invert = buildProjectionTransform(targetDelta)\n    let origin = buildTransformOrigin(targetDelta)\n    htmlNode.style.transform = invert\n    htmlNode.style.transformOrigin = origin\n\n    let duration = layoutConfig.duration ?? DEFAULT_DURATION\n    let easing = layoutConfig.easing ?? DEFAULT_EASING\n    let active = htmlNode.animate(\n      [\n        { transform: invert, transformOrigin: origin },\n        { transform: 'none', transformOrigin: origin },\n      ],\n      { duration, easing, fill: 'forwards' },\n    )\n    animation = active\n    scheduleProgressTracking(duration, active)\n    active.finished\n      .then(() => {\n        if (animation !== active) return\n        clearProjectionStyles(htmlNode)\n        resetAnimation()\n        snapshot = rectToBox(htmlNode.getBoundingClientRect())\n      })\n      .catch(() => {})\n  })\n\n  handle.addEventListener('remove', () => {\n    animation?.cancel()\n    resetAnimation()\n    snapshot = null\n  })\n\n  return (config = true) => {\n    currentConfig = config\n    return handle.element\n  }\n})\n\n/**\n * Animates layout changes for an element using FLIP-style transforms.\n *\n * @param config Layout animation configuration.\n * @returns A mixin descriptor for the target element.\n */\nexport function animateLayout<target extends EventTarget = Element>(\n  config: LayoutConfig = true,\n): MixinDescriptor<target, [LayoutConfig?], ElementProps> {\n  return animateLayoutMixin(config) as unknown as MixinDescriptor<\n    target,\n    [LayoutConfig?],\n    ElementProps\n  >\n}\n"
  },
  {
    "path": "packages/component/src/lib/mixins/animate-mixins.test.tsx",
    "content": "import { describe, expect, it, vi } from 'vitest'\nimport { createRoot } from '../vdom.ts'\nimport { animateEntrance, animateExit } from './animate-mixins.tsx'\nimport { invariant } from '../invariant.ts'\n\ndescribe('animate entrance/exit mixins', () => {\n  it('reclaims persisted nodes by type/key and reuses the same DOM element', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <div key=\"item\" id=\"reclaim-target\" mix={[animateExit({ opacity: 0, duration: 150 })]} />,\n    )\n    root.flush()\n\n    let first = container.querySelector('#reclaim-target')\n    invariant(first)\n\n    root.render(null)\n    root.flush()\n    expect(container.querySelector('#reclaim-target')).toBe(first)\n\n    root.render(\n      <div key=\"item\" id=\"reclaim-target\" mix={[animateExit({ opacity: 0, duration: 150 })]} />,\n    )\n    root.flush()\n\n    let second = container.querySelector('#reclaim-target')\n    expect(second).toBe(first)\n  })\n\n  it('retargets reclaim to natural styles instead of reversing exit animation', async () => {\n    let reverse = vi.fn()\n    let commitStyles = vi.fn()\n    let cancel = vi.fn()\n    let animateSpy = vi.spyOn(HTMLElement.prototype, 'animate').mockImplementation(\n      () =>\n        ({\n          playState: 'running',\n          reverse,\n          commitStyles,\n          cancel,\n          finished: new Promise(() => {}),\n        }) as unknown as Animation,\n    )\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <div key=\"item\" id=\"reverse-target\" mix={[animateExit({ opacity: 0, duration: 150 })]} />,\n    )\n    root.flush()\n\n    root.render(null)\n    root.flush()\n    await Promise.resolve()\n    expect(animateSpy).toHaveBeenCalledTimes(1)\n\n    root.render(\n      <div key=\"item\" id=\"reverse-target\" mix={[animateExit({ opacity: 0, duration: 150 })]} />,\n    )\n    root.flush()\n    await Promise.resolve()\n\n    expect(reverse).toHaveBeenCalledTimes(0)\n    expect(commitStyles).toHaveBeenCalledTimes(1)\n    expect(cancel).toHaveBeenCalledTimes(1)\n    expect(animateSpy).toHaveBeenCalledTimes(2)\n    animateSpy.mockRestore()\n  })\n\n  it('does not reverse on initial insert when entrance and exit mixins are both present', () => {\n    let reverse = vi.fn()\n    let animateSpy = vi.spyOn(HTMLElement.prototype, 'animate').mockImplementation(\n      () =>\n        ({\n          playState: 'running',\n          reverse,\n          finished: new Promise(() => {}),\n        }) as unknown as Animation,\n    )\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(\n      <div\n        key=\"both\"\n        id=\"both-mixins-target\"\n        mix={[\n          animateEntrance({ opacity: 0, duration: 150 }),\n          animateExit({ opacity: 0, duration: 150 }),\n        ]}\n      />,\n    )\n    root.flush()\n\n    expect(animateSpy).toHaveBeenCalledTimes(1)\n    expect(reverse).toHaveBeenCalledTimes(0)\n    animateSpy.mockRestore()\n  })\n\n  it('keeps persist behavior after reclaim interruption completes', async () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    let mix = [\n      animateEntrance({ opacity: 0, duration: 40 }),\n      animateExit({ opacity: 0, duration: 40 }),\n    ]\n\n    root.render(<div key=\"item\" id=\"persist-after-interrupt\" mix={mix} />)\n    root.flush()\n\n    root.render(null)\n    root.flush()\n    expect(container.querySelector('#persist-after-interrupt')).not.toBe(null)\n\n    root.render(<div key=\"item\" id=\"persist-after-interrupt\" mix={mix} />)\n    root.flush()\n    await new Promise((resolve) => setTimeout(resolve, 80))\n\n    root.render(null)\n    root.flush()\n    expect(container.querySelector('#persist-after-interrupt')).not.toBe(null)\n  })\n\n  it('skips first entrance when initial is false, but animates on reclaimed add', async () => {\n    let animateSpy = vi.spyOn(HTMLElement.prototype, 'animate').mockImplementation(\n      () =>\n        ({\n          playState: 'running',\n          reverse: vi.fn(),\n          commitStyles: vi.fn(),\n          cancel: vi.fn(),\n          finished: new Promise(() => {}),\n        }) as unknown as Animation,\n    )\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    let mix = [\n      animateEntrance({ initial: false, opacity: 0, duration: 100 }),\n      animateExit({ opacity: 0, duration: 100 }),\n    ]\n\n    root.render(<div key=\"initial-false\" id=\"initial-false-target\" mix={mix} />)\n    root.flush()\n    expect(animateSpy).toHaveBeenCalledTimes(0)\n\n    root.render(null)\n    root.flush()\n    await Promise.resolve()\n    expect(animateSpy).toHaveBeenCalledTimes(1)\n\n    root.render(<div key=\"initial-false\" id=\"initial-false-target\" mix={mix} />)\n    root.flush()\n    await Promise.resolve()\n    expect(animateSpy).toHaveBeenCalledTimes(2)\n\n    animateSpy.mockRestore()\n  })\n})\n"
  },
  {
    "path": "packages/component/src/lib/mixins/animate-mixins.tsx",
    "content": "import { createMixin } from '../mixin.ts'\nimport type { ElementProps } from '../jsx.ts'\nimport type { MixinDescriptor } from '../mixin.ts'\nimport { invariant } from '../invariant.ts'\n\ntype AnimateTiming = {\n  duration: number\n  easing?: string\n  delay?: number\n  composite?: CompositeOperation\n  initial?: boolean\n}\n\ntype AnimateStyleProps = {\n  [property: string]: unknown\n}\n\nexport type AnimateMixinConfig = AnimateTiming & AnimateStyleProps\n\ntype AnimationConfig = true | false | null | undefined | AnimateMixinConfig\n\nconst DEFAULT_ENTER: AnimateMixinConfig = {\n  opacity: 0,\n  duration: 150,\n  easing: 'ease-out',\n}\n\nconst DEFAULT_EXIT: AnimateMixinConfig = {\n  opacity: 0,\n  duration: 150,\n  easing: 'ease-in',\n}\n\ntype AnimationState = {\n  animation: Animation\n  properties: string[]\n}\n\nlet animatingNodes = new WeakMap<Element, AnimationState>()\nlet initialEntranceSeenByParent = new WeakMap<ParentNode, Set<string>>()\n\nfunction extractStyleProps(config: AnimateMixinConfig): Keyframe {\n  let result: Keyframe = {}\n  for (let key in config) {\n    if (\n      key === 'duration' ||\n      key === 'easing' ||\n      key === 'delay' ||\n      key === 'composite' ||\n      key === 'initial'\n    ) {\n      continue\n    }\n    let value = config[key]\n    if (value === undefined) continue\n    if (typeof value !== 'string' && typeof value !== 'number') continue\n    result[key as keyof Keyframe] = value\n  }\n  return result\n}\n\nfunction buildEnterKeyframes(config: AnimateMixinConfig): Keyframe[] {\n  let keyframe = extractStyleProps(config)\n  return [keyframe, {}]\n}\n\nfunction buildExitKeyframes(config: AnimateMixinConfig): Keyframe[] {\n  let keyframe = extractStyleProps(config)\n  return [{}, keyframe]\n}\n\nfunction resolveEnterConfig(config: AnimationConfig): AnimateMixinConfig | null {\n  if (!config) return null\n  if (config === true) return DEFAULT_ENTER\n  return config\n}\n\nfunction resolveExitConfig(config: AnimationConfig): AnimateMixinConfig | null {\n  if (!config) return null\n  if (config === true) return DEFAULT_EXIT\n  return config\n}\n\nfunction createAnimationOptions(\n  config: AnimateMixinConfig,\n  fill: FillMode,\n): KeyframeAnimationOptions {\n  return {\n    duration: config.duration,\n    delay: config.delay,\n    easing: config.easing,\n    composite: config.composite,\n    fill,\n  }\n}\n\nfunction collectAnimatedProperties(keyframes: Keyframe[]): string[] {\n  let properties = new Set<string>()\n  for (let keyframe of keyframes) {\n    for (let key in keyframe) {\n      if (key === 'offset' || key === 'easing' || key === 'composite') continue\n      properties.add(key)\n    }\n  }\n  return [...properties]\n}\n\nfunction toCssPropertyName(property: string): string {\n  return property.includes('-')\n    ? property\n    : property.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)\n}\n\nfunction readInlineStyle(style: CSSStyleDeclaration, property: string): string {\n  return style.getPropertyValue(toCssPropertyName(property))\n}\n\nfunction writeInlineStyle(style: CSSStyleDeclaration, property: string, value: string): void {\n  let cssProperty = toCssPropertyName(property)\n  if (value === '') {\n    style.removeProperty(cssProperty)\n    return\n  }\n  style.setProperty(cssProperty, value)\n}\n\nfunction trackAnimation(node: Element, animation: Animation, keyframes: Keyframe[]) {\n  let properties = collectAnimatedProperties(keyframes)\n  animatingNodes.set(node, { animation, properties })\n  animation.finished\n    .catch(() => {})\n    .finally(() => {\n      let current = animatingNodes.get(node)\n      if (current?.animation !== animation) return\n      animatingNodes.delete(node)\n    })\n}\n\nfunction waitForAnimationOrAbort(animation: Animation, signal: AbortSignal): Promise<void> {\n  if (signal.aborted) return Promise.resolve()\n  return new Promise((resolve) => {\n    let settled = false\n    let settle = () => {\n      if (settled) return\n      settled = true\n      signal.removeEventListener('abort', settle)\n      resolve()\n    }\n    signal.addEventListener('abort', settle, { once: true })\n    void animation.finished.catch(() => {}).finally(settle)\n  })\n}\n\nfunction shouldSkipInitialEntrance(\n  event: { key?: string; parent: ParentNode },\n  config: AnimateMixinConfig,\n): boolean {\n  if (config.initial !== false) return false\n  if (event.key == null) return false\n  let seenForParent = initialEntranceSeenByParent.get(event.parent)\n  if (!seenForParent) {\n    seenForParent = new Set<string>()\n    initialEntranceSeenByParent.set(event.parent, seenForParent)\n  }\n  if (seenForParent.has(event.key)) return false\n  seenForParent.add(event.key)\n  return true\n}\n\nlet animateEntranceMixin = createMixin<Element, [config: AnimationConfig], ElementProps>(\n  (handle) => {\n    let currentConfig: AnimationConfig = true\n\n    handle.addEventListener('insert', (event) => {\n      let node = event.node\n      let current = animatingNodes.get(node)\n      if (current && current.animation.playState === 'running') {\n        return\n      }\n\n      let config = resolveEnterConfig(currentConfig)\n      if (!config) return\n      if (shouldSkipInitialEntrance(event, config)) return\n      let keyframes = buildEnterKeyframes(config)\n      let options = createAnimationOptions(config, 'backwards')\n      let animation = (node as HTMLElement).animate(keyframes, options)\n      trackAnimation(node, animation, keyframes)\n    })\n\n    return (config) => {\n      currentConfig = config\n      return handle.element\n    }\n  },\n)\n\nlet animateExitMixin = createMixin<Element, [config: AnimationConfig], ElementProps>((handle) => {\n  let currentConfig: AnimationConfig = true\n  let node: Element | null = null\n\n  handle.addEventListener('insert', (event) => {\n    node = event.node\n  })\n\n  handle.addEventListener('reclaimed', (event) => {\n    node = event.node\n    let current = animatingNodes.get(event.node)\n    if (current && current.animation.playState === 'running') {\n      // WAAPI can throw InvalidStateError here if the target is transiently non-rendered\n      // during reclaim; we still have computed-style fallback below for retargeting.\n      try {\n        current.animation.commitStyles()\n      } catch {}\n      current.animation.cancel()\n\n      let style = (event.node as HTMLElement).style\n      let computed = getComputedStyle(event.node as Element)\n      let from: Keyframe = {}\n      for (let property of current.properties) {\n        let cssProperty = toCssPropertyName(property)\n        let value = readInlineStyle(style, property) || computed.getPropertyValue(cssProperty)\n        if (value !== '') {\n          from[property as keyof Keyframe] = value\n        }\n        writeInlineStyle(style, property, '')\n      }\n\n      let enterConfig = resolveEnterConfig(currentConfig) ?? DEFAULT_ENTER\n      let keyframes: Keyframe[] = [from, {}]\n      let options = createAnimationOptions(enterConfig, 'none')\n      let animation = (event.node as HTMLElement).animate(keyframes, options)\n      trackAnimation(event.node, animation, keyframes)\n    }\n  })\n\n  handle.addEventListener('beforeRemove', (event) => {\n    let config = resolveExitConfig(currentConfig)\n    if (!config) return\n    event.persistNode(async (signal) => {\n      invariant(node)\n      let current = animatingNodes.get(node)\n      if (current && current.animation.playState === 'running') {\n        current.animation.reverse()\n        await waitForAnimationOrAbort(current.animation, signal)\n        return\n      }\n\n      let keyframes = buildExitKeyframes(config)\n      let options = createAnimationOptions(config, 'forwards')\n      let animation = (node as HTMLElement).animate(keyframes, options)\n      trackAnimation(node, animation, keyframes)\n      await waitForAnimationOrAbort(animation, signal)\n    })\n  })\n\n  return (config) => {\n    currentConfig = config\n    return handle.element\n  }\n})\n\n/**\n * Animates an element when it is inserted into the DOM.\n *\n * @param config Entrance animation configuration.\n * @returns A mixin descriptor for the target element.\n */\nexport function animateEntrance<target extends EventTarget = Element>(\n  config: AnimationConfig = true,\n): MixinDescriptor<target, [AnimationConfig], ElementProps> {\n  return animateEntranceMixin(config) as unknown as MixinDescriptor<\n    target,\n    [AnimationConfig],\n    ElementProps\n  >\n}\n\n/**\n * Animates an element when it is removed from the DOM.\n *\n * @param config Exit animation configuration.\n * @returns A mixin descriptor for the target element.\n */\nexport function animateExit<target extends EventTarget = Element>(\n  config: AnimationConfig = true,\n): MixinDescriptor<target, [AnimationConfig], ElementProps> {\n  return animateExitMixin(config) as unknown as MixinDescriptor<\n    target,\n    [AnimationConfig],\n    ElementProps\n  >\n}\n"
  },
  {
    "path": "packages/component/src/lib/mixins/css-mixin.test.tsx",
    "content": "import { describe, expect, it } from 'vitest'\nimport { createRoot } from '../vdom.ts'\nimport { invariant } from '../invariant.ts'\nimport { css } from './css-mixin.tsx'\n\ndescribe('css mixin', () => {\n  it('concatenates generated classes with existing className', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(\n      <div\n        className=\"base\"\n        mix={[\n          css({ color: 'red' }),\n          css({\n            backgroundColor: 'blue',\n          }),\n        ]}\n      />,\n    )\n    root.flush()\n\n    let div = container.querySelector('div')\n    invariant(div)\n    let classNames = div.className.split(/\\s+/).filter(Boolean)\n    let generated = classNames.filter((name) => name.startsWith('rmxc-'))\n    expect(classNames).toContain('base')\n    expect(generated.length).toBe(2)\n    expect(new Set(generated).size).toBe(2)\n  })\n\n  it('coexists with existing class/className props', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(\n      <div\n        class=\"from-class\"\n        className=\"from-classname\"\n        mix={[\n          css({\n            borderColor: 'black',\n            borderStyle: 'solid',\n            borderWidth: 1,\n          }),\n        ]}\n      />,\n    )\n    root.flush()\n\n    let div = container.querySelector('div')\n    invariant(div)\n    let classNames = div.className.split(/\\s+/).filter(Boolean)\n    expect(classNames).toContain('from-class')\n    expect(classNames).toContain('from-classname')\n    expect(classNames.some((name) => name.startsWith('rmxc-'))).toBe(true)\n  })\n\n  it('supports keyframes and nested media rules', () => {\n    let container = document.createElement('div')\n    document.body.appendChild(container)\n    let root = createRoot(container)\n\n    root.render(\n      <div\n        mix={[\n          css({\n            animationName: 'fade-in',\n            animationDuration: '1s',\n            '@keyframes fade-in': {\n              from: {\n                opacity: 0,\n                transform: 'translateY(8px)',\n                '&:hover': { color: 'red' } as any,\n              },\n              to: {\n                opacity: 1,\n              },\n              skipped: null as any,\n            },\n            '@media (min-width: 1px)': {\n              color: 'rgb(1, 2, 3)',\n              '&:hover': {\n                color: 'rgb(4, 5, 6)',\n              },\n            },\n          }),\n        ]}\n      >\n        Animated\n      </div>,\n    )\n    root.flush()\n\n    let div = container.querySelector('div')\n    invariant(div)\n    expect(div.className).toMatch(/rmxc-/)\n    let cssTexts = readAdoptedCssTexts()\n    expect(cssTexts.some((text) => text.includes('@keyframes fade-in'))).toBe(true)\n    expect(cssTexts.some((text) => text.includes('@media (min-width: 1px)'))).toBe(true)\n  })\n\n  it('skips undefined conditional selectors and at-rules', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <div\n        mix={[\n          css({\n            color: 'rgb(10, 20, 30)',\n            '@supports (display: grid)': undefined,\n            '&[data-active=\"true\"]': undefined,\n            '&:focus': {\n              outlineWidth: 2,\n              outlineStyle: 'solid',\n            },\n          }),\n        ]}\n      >\n        Conditional rules\n      </div>,\n    )\n    root.flush()\n\n    let div = container.querySelector('div')\n    invariant(div)\n    expect(div.className).toMatch(/rmxc-/)\n  })\n\n  it('handles empty keyframe steps and at-rule bodies', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <div\n        mix={[\n          css({\n            animationName: 'empty-frames',\n            animationDuration: '1s',\n            '@keyframes empty-frames': {\n              from: {\n                '&:hover': { color: 'red' } as any,\n              },\n              to: {},\n            },\n            '@media (min-width: 1px)': {},\n          }),\n        ]}\n      >\n        Empty blocks\n      </div>,\n    )\n    root.flush()\n\n    let div = container.querySelector('div')\n    invariant(div)\n    expect(div.className).toMatch(/rmxc-/)\n\n    let cssTexts = readAdoptedCssTexts()\n    expect(cssTexts.some((text) => text.includes('@keyframes empty-frames'))).toBe(true)\n  })\n})\n\nfunction readAdoptedCssTexts(): string[] {\n  let texts: string[] = []\n  for (let sheet of document.adoptedStyleSheets) {\n    let rules = Array.from(sheet.cssRules).map((rule) => rule.cssText)\n    texts.push(rules.join('\\n'))\n  }\n  return texts\n}\n"
  },
  {
    "path": "packages/component/src/lib/mixins/css-mixin.tsx",
    "content": "// @jsxRuntime classic\n// @jsx jsx\nimport { createMixin } from '../mixin.ts'\nimport { jsx } from '../jsx.ts'\nimport type { ElementProps } from '../jsx.ts'\nimport { invariant } from '../invariant.ts'\nimport { processStyleClass } from '../style/index.ts'\nimport type { CSSProps } from '../style/lib/style.ts'\n\ntype StyleEntry = { selector: string; css: string }\ntype StyleCache = Map<string, StyleEntry>\ntype StyleManagerLike = {\n  insert(className: string, rule: string): void\n  remove(className: string): void\n}\n\nlet clientStyleCache: StyleCache = new Map()\n\n/**\n * Applies generated class names for CSS object styles.\n */\nexport let css = createMixin<Element, [styles: CSSProps], ElementProps>((handle) => {\n  let activeSelector = ''\n  let currentStyles: CSSProps = {}\n\n  handle.addEventListener('remove', () => {\n    if (!activeSelector) return\n    let runtime = handle.frame.$runtime as {\n      styleCache?: StyleCache\n      styleManager?: StyleManagerLike\n    }\n    invariant(runtime, 'css mixin requires frame runtime')\n    let styleTarget = resolveStyleTarget(runtime)\n    styleTarget.styleManager?.remove(activeSelector)\n    activeSelector = ''\n  })\n\n  return (styles, props) => {\n    currentStyles = styles\n    let runtime = handle.frame.$runtime as {\n      styleCache?: StyleCache\n      styleManager?: StyleManagerLike\n    }\n    invariant(runtime, 'css mixin requires frame runtime')\n    let styleTarget = resolveStyleTarget(runtime)\n    let { selector, css: cssText } = processStyleClass(currentStyles, styleTarget.styleCache)\n\n    if (styleTarget.styleManager) {\n      if (activeSelector && activeSelector !== selector) {\n        styleTarget.styleManager.remove(activeSelector)\n      }\n      if (selector && activeSelector !== selector) {\n        styleTarget.styleManager.insert(selector, cssText)\n      }\n      activeSelector = selector\n    }\n\n    if (!selector) {\n      return handle.element\n    }\n\n    return (\n      <handle.element\n        {...props}\n        className={props.className ? `${props.className} ${selector}` : selector}\n      />\n    )\n  }\n})\n\nfunction resolveStyleTarget(runtime: {\n  styleCache?: StyleCache\n  styleManager?: StyleManagerLike\n}): { styleCache: StyleCache; styleManager?: StyleManagerLike } {\n  return {\n    styleCache: runtime.styleCache ?? clientStyleCache,\n    styleManager: runtime.styleManager,\n  }\n}\n"
  },
  {
    "path": "packages/component/src/lib/mixins/keys-mixin.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../vdom.ts'\nimport { invariant } from '../invariant.ts'\nimport { on } from './on-mixin.tsx'\nimport { keysEvents } from './keys-mixin.tsx'\n\ndescribe('keysEvents mixin', () => {\n  it('dispatches keydown:Space events and prevents default', () => {\n    let calls = 0\n    let keydownResult = true\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <div\n        tabIndex={0}\n        mix={[\n          keysEvents(),\n          on(keysEvents.space, () => {\n            calls++\n          }),\n        ]}\n      />,\n    )\n    root.flush()\n\n    let div = container.querySelector('div')\n    invariant(div)\n    keydownResult = div.dispatchEvent(\n      new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true }),\n    )\n    root.flush()\n\n    expect(calls).toBe(1)\n    expect(keydownResult).toBe(false)\n  })\n\n  it('dispatches keydown:ArrowUp events', () => {\n    let calls = 0\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <div\n        mix={[\n          keysEvents(),\n          on(keysEvents.arrowUp, () => {\n            calls++\n          }),\n        ]}\n      />,\n    )\n    root.flush()\n\n    let div = container.querySelector('div')\n    invariant(div)\n    div.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }))\n    root.flush()\n\n    expect(calls).toBe(1)\n  })\n})\n"
  },
  {
    "path": "packages/component/src/lib/mixins/keys-mixin.tsx",
    "content": "// @jsxRuntime classic\n// @jsx jsx\nimport { on } from './on-mixin.tsx'\nimport { createMixin } from '../mixin.ts'\nimport { jsx } from '../jsx.ts'\n\nexport let escapeEventType = 'keydown:Escape' as const\nexport let enterEventType = 'keydown:Enter' as const\nexport let spaceEventType = 'keydown: ' as const\nexport let backspaceEventType = 'keydown:Backspace' as const\nexport let deleteEventType = 'keydown:Delete' as const\nexport let arrowLeftEventType = 'keydown:ArrowLeft' as const\nexport let arrowRightEventType = 'keydown:ArrowRight' as const\nexport let arrowUpEventType = 'keydown:ArrowUp' as const\nexport let arrowDownEventType = 'keydown:ArrowDown' as const\nexport let homeEventType = 'keydown:Home' as const\nexport let endEventType = 'keydown:End' as const\nexport let pageUpEventType = 'keydown:PageUp' as const\nexport let pageDownEventType = 'keydown:PageDown' as const\n\ndeclare global {\n  interface HTMLElementEventMap {\n    [escapeEventType]: KeyboardEvent\n    [enterEventType]: KeyboardEvent\n    [spaceEventType]: KeyboardEvent\n    [backspaceEventType]: KeyboardEvent\n    [deleteEventType]: KeyboardEvent\n    [arrowLeftEventType]: KeyboardEvent\n    [arrowRightEventType]: KeyboardEvent\n    [arrowUpEventType]: KeyboardEvent\n    [arrowDownEventType]: KeyboardEvent\n    [homeEventType]: KeyboardEvent\n    [endEventType]: KeyboardEvent\n    [pageUpEventType]: KeyboardEvent\n    [pageDownEventType]: KeyboardEvent\n  }\n}\n\nlet keyToEventType: Record<string, string> = {\n  Escape: escapeEventType,\n  Enter: enterEventType,\n  ' ': spaceEventType,\n  Backspace: backspaceEventType,\n  Delete: deleteEventType,\n  ArrowLeft: arrowLeftEventType,\n  ArrowRight: arrowRightEventType,\n  ArrowUp: arrowUpEventType,\n  ArrowDown: arrowDownEventType,\n  Home: homeEventType,\n  End: endEventType,\n  PageUp: pageUpEventType,\n  PageDown: pageDownEventType,\n}\n\nlet baseKeysEvents = createMixin<HTMLElement>((handle) => (props) => (\n  <handle.element\n    {...props}\n    mix={[\n      on('keydown', (event) => {\n        let type = keyToEventType[event.key]\n        if (!type) return\n        event.preventDefault()\n        event.currentTarget.dispatchEvent(\n          new KeyboardEvent(type, {\n            key: event.key,\n          }),\n        )\n      }),\n    ]}\n  />\n))\n\ntype KeysEventsMixin = typeof baseKeysEvents & {\n  readonly escape: typeof escapeEventType\n  readonly enter: typeof enterEventType\n  readonly space: typeof spaceEventType\n  readonly backspace: typeof backspaceEventType\n  readonly del: typeof deleteEventType\n  readonly arrowLeft: typeof arrowLeftEventType\n  readonly arrowRight: typeof arrowRightEventType\n  readonly arrowUp: typeof arrowUpEventType\n  readonly arrowDown: typeof arrowDownEventType\n  readonly home: typeof homeEventType\n  readonly end: typeof endEventType\n  readonly pageUp: typeof pageUpEventType\n  readonly pageDown: typeof pageDownEventType\n}\n\n/**\n * Normalizes common keyboard keys into custom key-specific DOM events.\n */\nexport let keysEvents: KeysEventsMixin = Object.assign(baseKeysEvents, {\n  escape: escapeEventType,\n  enter: enterEventType,\n  space: spaceEventType,\n  backspace: backspaceEventType,\n  del: deleteEventType,\n  arrowLeft: arrowLeftEventType,\n  arrowRight: arrowRightEventType,\n  arrowUp: arrowUpEventType,\n  arrowDown: arrowDownEventType,\n  home: homeEventType,\n  end: endEventType,\n  pageUp: pageUpEventType,\n  pageDown: pageDownEventType,\n})\n"
  },
  {
    "path": "packages/component/src/lib/mixins/link-mixin.test.tsx",
    "content": "import { afterEach, describe, expect, it, vi } from 'vitest'\n\nimport { createRoot } from '../vdom.ts'\nimport { invariant } from '../invariant.ts'\nimport type { RemixNode } from '../jsx.ts'\nimport { link } from './link-mixin.tsx'\n\nfunction render(node: RemixNode) {\n  let container = document.createElement('div')\n  document.body.append(container)\n  let root = createRoot(container)\n  root.render(node)\n  root.flush()\n  return { container, root }\n}\n\ndescribe('link mixin', () => {\n  afterEach(() => {\n    document.body.innerHTML = ''\n    vi.unstubAllGlobals()\n  })\n\n  it('adds href and runtime attributes to anchors', () => {\n    let { container } = render(\n      <a mix={link('/login', { src: '/partials/login', target: 'auth', resetScroll: false })}>\n        Login\n      </a>,\n    )\n\n    let anchor = container.querySelector('a')\n    invariant(anchor)\n    expect(anchor.getAttribute('href')).toBe('/login')\n    expect(anchor.getAttribute('rmx-target')).toBe('auth')\n    expect(anchor.getAttribute('rmx-src')).toBe('/partials/login')\n    expect(anchor.getAttribute('rmx-reset-scroll')).toBe('false')\n    expect(anchor.getAttribute('role')).toBeNull()\n  })\n\n  it('adds link semantics to buttons', () => {\n    let { container } = render(<button mix={link('/login')}>Login</button>)\n\n    let button = container.querySelector('button')\n    invariant(button)\n    expect(button.getAttribute('role')).toBe('link')\n    expect(button.getAttribute('type')).toBe('button')\n    expect(button.getAttribute('tabindex')).toBeNull()\n  })\n\n  it('adds link semantics and tabIndex to generic elements', () => {\n    let { container } = render(<li mix={link('/login')}>Login</li>)\n\n    let item = container.querySelector('li')\n    invariant(item)\n    expect(item.getAttribute('role')).toBe('link')\n    expect(item.getAttribute('tabindex')).toBe('0')\n  })\n\n  it('omits runtime attributes on anchors when options are not provided', () => {\n    let { container } = render(<a mix={link('/docs')}>Docs</a>)\n\n    let anchor = container.querySelector('a')\n    invariant(anchor)\n    expect(anchor.getAttribute('href')).toBe('/docs')\n    expect(anchor.getAttribute('rmx-target')).toBeNull()\n    expect(anchor.getAttribute('rmx-src')).toBeNull()\n  })\n\n  it('navigates on plain click for non-anchor hosts', () => {\n    let navigateMock = vi.fn(() => ({ finished: Promise.resolve() }))\n    vi.stubGlobal('navigation', { navigate: navigateMock })\n\n    let { container } = render(<button mix={link('/login', { target: 'auth' })}>Login</button>)\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))\n\n    expect(navigateMock).toHaveBeenCalledWith('/login', {\n      state: { target: 'auth', src: '/login', resetScroll: true, $rmx: true },\n      history: undefined,\n    })\n  })\n\n  it('passes history options through for non-anchor navigation', () => {\n    let navigateMock = vi.fn(() => ({ finished: Promise.resolve() }))\n    vi.stubGlobal('navigation', { navigate: navigateMock })\n\n    let { container } = render(<button mix={link('/login', { history: 'replace' })}>Login</button>)\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))\n\n    expect(navigateMock).toHaveBeenCalledWith('/login', {\n      state: { target: undefined, src: '/login', resetScroll: true, $rmx: true },\n      history: 'replace',\n    })\n  })\n\n  it('passes resetScroll=false through for non-anchor navigation', () => {\n    let navigateMock = vi.fn(() => ({ finished: Promise.resolve() }))\n    vi.stubGlobal('navigation', { navigate: navigateMock })\n\n    let { container } = render(<button mix={link('/login', { resetScroll: false })}>Login</button>)\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))\n\n    expect(navigateMock).toHaveBeenCalledWith('/login', {\n      state: { target: undefined, src: '/login', resetScroll: false, $rmx: true },\n      history: undefined,\n    })\n  })\n\n  it('activates link buttons on Enter and suppresses keyboard clicks from Enter and Space', () => {\n    let navigateMock = vi.fn(() => ({ finished: Promise.resolve() }))\n    vi.stubGlobal('navigation', { navigate: navigateMock })\n\n    let { container } = render(<button mix={link('/login')}>Login</button>)\n\n    let button = container.querySelector('button')\n    invariant(button)\n\n    button.dispatchEvent(\n      new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),\n    )\n    expect(navigateMock).toHaveBeenCalledTimes(1)\n\n    button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, detail: 0 }))\n    expect(navigateMock).toHaveBeenCalledTimes(1)\n\n    button.dispatchEvent(\n      new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true }),\n    )\n    button.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', bubbles: true, cancelable: true }))\n    button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, detail: 0 }))\n    expect(navigateMock).toHaveBeenCalledTimes(1)\n  })\n\n  it('activates generic link elements on Enter', () => {\n    let navigateMock = vi.fn(() => ({ finished: Promise.resolve() }))\n    vi.stubGlobal('navigation', { navigate: navigateMock })\n\n    let { container } = render(<li mix={link('/login')}>Login</li>)\n\n    let item = container.querySelector('li')\n    invariant(item)\n    item.dispatchEvent(\n      new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),\n    )\n\n    expect(navigateMock).toHaveBeenCalledTimes(1)\n    expect(navigateMock).toHaveBeenCalledWith('/login', {\n      state: { target: undefined, src: '/login', resetScroll: true, $rmx: true },\n      history: undefined,\n    })\n  })\n\n  it('opens a new tab for meta-click, ctrl-click, and middle-click on non-anchor hosts', () => {\n    let navigateMock = vi.fn(() => ({ finished: Promise.resolve() }))\n    let openMock = vi.fn()\n    vi.stubGlobal('navigation', { navigate: navigateMock })\n    vi.stubGlobal('open', openMock)\n\n    let { container } = render(<button mix={link('/login')}>Login</button>)\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.dispatchEvent(\n      new MouseEvent('click', { bubbles: true, cancelable: true, metaKey: true }),\n    )\n    button.dispatchEvent(\n      new MouseEvent('click', { bubbles: true, cancelable: true, ctrlKey: true }),\n    )\n    button.dispatchEvent(new MouseEvent('auxclick', { bubbles: true, cancelable: true, button: 1 }))\n\n    expect(openMock).toHaveBeenCalledTimes(3)\n    expect(openMock).toHaveBeenNthCalledWith(1, '/login', '_blank')\n    expect(openMock).toHaveBeenNthCalledWith(2, '/login', '_blank')\n    expect(openMock).toHaveBeenNthCalledWith(3, '/login', '_blank')\n    expect(navigateMock).not.toHaveBeenCalled()\n  })\n\n  it('does not navigate disabled or aria-disabled link hosts', () => {\n    let navigateMock = vi.fn(() => ({ finished: Promise.resolve() }))\n    vi.stubGlobal('navigation', { navigate: navigateMock })\n\n    let { container } = render(\n      <div>\n        <button disabled={true} mix={link('/login')}>\n          Login\n        </button>\n        <li aria-disabled=\"true\" mix={link('/help')}>\n          Help\n        </li>\n      </div>,\n    )\n\n    let button = container.querySelector('button')\n    let item = container.querySelector('li')\n    invariant(button)\n    invariant(item)\n\n    button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))\n    item.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))\n    item.dispatchEvent(\n      new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),\n    )\n\n    expect(item.getAttribute('aria-disabled')).toBe('true')\n    expect(navigateMock).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "packages/component/src/lib/mixins/link-mixin.tsx",
    "content": "// @jsxRuntime classic\n// @jsx jsx\nimport { createMixin } from '../mixin.ts'\nimport { jsx } from '../jsx.ts'\nimport { navigate } from '../navigation.ts'\nimport { on } from './on-mixin.tsx'\nimport type { ElementProps } from '../jsx.ts'\n\nimport type { NavigationOptions } from '../navigation.ts'\n\ntype LinkCurrentProps = ElementProps & {\n  disabled?: boolean\n  role?: string\n  tabIndex?: number\n  tabindex?: number\n  type?: string\n  contentEditable?: boolean | string\n  contenteditable?: boolean | string\n  'aria-disabled'?: boolean | 'true' | 'false'\n}\n\nlet nativeLinkHostTypes = new Set(['a', 'area'])\n\n/**\n * Adds client-side navigation behavior to anchor-like elements.\n */\nexport let link = createMixin<\n  HTMLElement,\n  [href: string, options?: NavigationOptions],\n  LinkCurrentProps\n>((handle, hostType) => {\n  let suppressKeyboardClick = false\n\n  return (href, options, props: LinkCurrentProps) => {\n    if (nativeLinkHostTypes.has(hostType)) {\n      return (\n        <handle.element\n          {...props}\n          href={href}\n          {...(options?.target == null ? {} : { 'rmx-target': options.target })}\n          {...(options?.src == null ? {} : { 'rmx-src': options.src })}\n          {...(options?.resetScroll === false ? { 'rmx-reset-scroll': 'false' } : {})}\n        />\n      )\n    }\n\n    let nextProps = { ...props }\n    if (nextProps.role == null) {\n      nextProps.role = 'link'\n    }\n    if (nextProps.disabled === true && nextProps['aria-disabled'] == null) {\n      nextProps['aria-disabled'] = 'true'\n    }\n    if (hostType === 'button' && nextProps.type == null) {\n      nextProps.type = 'button'\n    }\n    if (\n      hostType !== 'button' &&\n      nextProps.tabIndex == null &&\n      nextProps.tabindex == null &&\n      nextProps.contentEditable == null &&\n      nextProps.contenteditable == null\n    ) {\n      nextProps.tabIndex = 0\n    }\n\n    return (\n      <handle.element\n        {...nextProps}\n        mix={[\n          on('click', (event) => {\n            if (event.detail === 0 && suppressKeyboardClick) {\n              suppressKeyboardClick = false\n              event.preventDefault()\n              return\n            }\n\n            suppressKeyboardClick = false\n            if (isDisabledElement(event.currentTarget)) {\n              event.preventDefault()\n              return\n            }\n            if (event.button !== 0) return\n\n            event.preventDefault()\n            if (event.metaKey || event.ctrlKey) {\n              globalThis.open(href, '_blank')\n              return\n            }\n\n            void navigate(href, options)\n          }),\n          on('auxclick', (event) => {\n            suppressKeyboardClick = false\n            if (isDisabledElement(event.currentTarget)) {\n              event.preventDefault()\n              return\n            }\n            if (event.button !== 1) return\n\n            event.preventDefault()\n            globalThis.open(href, '_blank')\n          }),\n          on('keydown', (event) => {\n            if (event.key === 'Enter') {\n              if (event.repeat) return\n              if (isDisabledElement(event.currentTarget)) {\n                event.preventDefault()\n                return\n              }\n\n              suppressKeyboardClick = hostType === 'button'\n              event.preventDefault()\n              void navigate(href, options)\n              return\n            }\n\n            if (hostType === 'button' && event.key === ' ') {\n              suppressKeyboardClick = true\n              event.preventDefault()\n            }\n          }),\n          on('keyup', (event) => {\n            if (hostType === 'button' && event.key === ' ') {\n              event.preventDefault()\n            }\n          }),\n        ]}\n      />\n    )\n  }\n})\n\nfunction isDisabledElement(node: Element) {\n  return (\n    ('disabled' in node && node.disabled === true) || node.getAttribute('aria-disabled') === 'true'\n  )\n}\n"
  },
  {
    "path": "packages/component/src/lib/mixins/on-mixin.test.tsx",
    "content": "import { describe, it, expect, vi } from 'vitest'\nimport { createRoot } from '../vdom.ts'\nimport { on } from './on-mixin.tsx'\nimport { invariant } from '../invariant.ts'\nimport type { Assert, Equal } from '../../test/utils.ts'\nimport type { Dispatched } from './on-mixin.tsx'\n\ndescribe('on mixin', () => {\n  it('updates listeners in place without rebinding when capture is unchanged', () => {\n    let calls: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          on('click', () => {\n            calls.push('first')\n          }),\n        ]}\n      >\n        click\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    let addSpy = vi.spyOn(button, 'addEventListener')\n    let removeSpy = vi.spyOn(button, 'removeEventListener')\n\n    root.render(\n      <button\n        mix={[\n          on('click', () => {\n            calls.push('second')\n          }),\n        ]}\n      >\n        click\n      </button>,\n    )\n    root.flush()\n    button.click()\n    root.flush()\n\n    expect(calls).toEqual(['second'])\n    expect(addSpy).toHaveBeenCalledTimes(0)\n    expect(removeSpy).toHaveBeenCalledTimes(0)\n  })\n\n  it('rebinds when capture option changes', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(<button mix={[on('click', () => {}, false)]}>click</button>)\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    let addSpy = vi.spyOn(button, 'addEventListener')\n    let removeSpy = vi.spyOn(button, 'removeEventListener')\n\n    root.render(<button mix={[on('click', () => {}, true)]}>click</button>)\n    root.flush()\n\n    expect(addSpy).toHaveBeenCalledTimes(1)\n    expect(removeSpy).toHaveBeenCalledTimes(1)\n  })\n\n  it('passes abort signal as the second handler argument', () => {\n    let receivedSignal = AbortSignal.abort()\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          on('click', (_event, signal) => {\n            receivedSignal = signal\n          }),\n        ]}\n      >\n        click\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.click()\n    root.flush()\n\n    expect(receivedSignal).toBeInstanceOf(AbortSignal)\n    expect(receivedSignal.aborted).toBe(false)\n  })\n\n  it('supports multiple event types on the same element', () => {\n    let calls: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          on('click', () => {\n            calls.push('click')\n          }),\n          on('focus', () => {\n            calls.push('focus')\n          }),\n        ]}\n      >\n        click\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.dispatchEvent(new FocusEvent('focus', { bubbles: true }))\n    button.click()\n    root.flush()\n\n    expect(calls).toEqual(['focus', 'click'])\n  })\n\n  it('removes listeners when on() mixin is removed', () => {\n    let calls = 0\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          on('click', () => {\n            calls++\n          }),\n        ]}\n      >\n        click\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.click()\n    root.flush()\n    expect(calls).toBe(1)\n\n    root.render(<button>click</button>)\n    root.flush()\n    button.click()\n    root.flush()\n    expect(calls).toBe(1)\n  })\n\n  it('aborts previous handler signal on reentry', async () => {\n    let signals: AbortSignal[] = []\n    let pendingResolvers: Array<() => void> = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          on('click', async (_event, signal) => {\n            signals.push(signal)\n            await new Promise<void>((resolve) => {\n              pendingResolvers.push(resolve)\n            })\n          }),\n        ]}\n      >\n        click\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.click()\n    button.click()\n    root.flush()\n\n    expect(signals).toHaveLength(2)\n    expect(signals[0]!.aborted).toBe(true)\n    expect(signals[1]!.aborted).toBe(false)\n\n    for (let resolve of pendingResolvers) resolve()\n    await Promise.resolve()\n  })\n})\n\nlet infersNodeType = (\n  <button\n    mix={[\n      on('pointerdown', (event, signal) => {\n        type inferredEvent = Assert<\n          Equal<typeof event, Dispatched<PointerEvent, HTMLButtonElement>>\n        >\n        type inferredTarget = Assert<Equal<typeof event.currentTarget, HTMLButtonElement>>\n        type inferredSignal = Assert<Equal<typeof signal, AbortSignal>>\n      }),\n    ]}\n  />\n)\n"
  },
  {
    "path": "packages/component/src/lib/mixins/on-mixin.tsx",
    "content": "import { createMixin } from '../mixin.ts'\nimport type { ElementProps } from '../jsx.ts'\nimport type { MixinDescriptor } from '../mixin.ts'\nimport type {\n  EventType as AddEventType,\n  ListenerFor as AddEventListenerFor,\n} from '../event-listeners.ts'\n\nexport type { Dispatched } from '../event-listeners.ts'\n\ntype SignaledListener<event extends Event> = (\n  event: event,\n  signal: AbortSignal,\n) => void | Promise<void>\n\ntype EventType<target extends Element> = Extract<AddEventType<target>, string>\ntype ListenerFor<target extends Element, type extends EventType<target>> = SignaledListener<\n  Parameters<AddEventListenerFor<target, type>>[0]\n>\n\nlet onMixin = createMixin<\n  Element,\n  [type: string, handler: SignaledListener<Event>, captureBoolean?: boolean],\n  ElementProps\n>((handle) => {\n  let currentHandler: SignaledListener<Event> = () => {}\n  let currentType = ''\n  let currentCapture = false\n  let currentNode: Element | null = null\n  let reentry: AbortController | null = null\n\n  let stableHandler = (event: Event) => {\n    reentry?.abort(new DOMException('', 'EventReentry'))\n    reentry = new AbortController()\n    void currentHandler(event, reentry.signal)\n  }\n\n  handle.addEventListener('insert', (event) => {\n    currentNode = event.node\n    currentNode.addEventListener(currentType, stableHandler, currentCapture)\n  })\n\n  handle.addEventListener('remove', () => {\n    currentNode?.removeEventListener(currentType, stableHandler, currentCapture)\n    currentNode = null\n    reentry?.abort(new DOMException('', 'AbortError'))\n  })\n\n  return (type, handler, captureBoolean = false) => {\n    let previousType = currentType\n    let previousCapture = currentCapture\n    let needsRebind = currentType !== type || currentCapture !== captureBoolean\n    currentType = type\n    currentHandler = handler\n    currentCapture = captureBoolean\n\n    if (needsRebind && currentNode) {\n      currentNode.removeEventListener(previousType, stableHandler, previousCapture)\n      currentNode.addEventListener(type, stableHandler, captureBoolean)\n    }\n\n    return handle.element\n  }\n})\n\n/**\n * Attaches a typed DOM event handler through the mixin system.\n *\n * @param type Event type to listen for.\n * @param handler Event handler.\n * @param captureBoolean Whether to listen during capture.\n * @returns A mixin descriptor for the target element.\n */\nexport function on<\n  target extends Element = Element,\n  type extends EventType<target> = EventType<target>,\n>(\n  type: type,\n  handler: ListenerFor<target, type>,\n  captureBoolean?: boolean,\n): MixinDescriptor<target, [type, ListenerFor<target, type>, boolean?], ElementProps> {\n  // Keep this typed wrapper so JSX host context can infer event/currentTarget\n  // from `type`, rather than exposing the raw `string` + `Event` runtime signature.\n  return onMixin(\n    type as string,\n    handler as unknown as SignaledListener<Event>,\n    captureBoolean,\n  ) as unknown as MixinDescriptor<target, [type, ListenerFor<target, type>, boolean?], ElementProps>\n}\n"
  },
  {
    "path": "packages/component/src/lib/mixins/press-mixin.test.tsx",
    "content": "import { describe, it, expect, vi } from 'vitest'\nimport { createRoot, on, pressEvents } from '../../index.ts'\n\ndescribe('press mixin', () => {\n  it('dispatches down, up, and press for Enter key', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.down, () => {\n            events.push('down')\n          }),\n          on(pressEvents.up, () => {\n            events.push('up')\n          }),\n          on(pressEvents.press, () => {\n            events.push('press')\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n    button.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))\n    button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }))\n    root.flush()\n\n    expect(events).toEqual(['down', 'up', 'press'])\n  })\n\n  it('suppresses up and press when long press is prevented', () => {\n    vi.useFakeTimers()\n    try {\n      let events: string[] = []\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      root.render(\n        <button\n          mix={[\n            pressEvents(),\n            on(pressEvents.long, (event) => {\n              events.push('long')\n              event.preventDefault()\n            }),\n            on(pressEvents.up, () => {\n              events.push('up')\n            }),\n            on(pressEvents.press, () => {\n              events.push('press')\n            }),\n          ]}\n        >\n          Hold me\n        </button>,\n      )\n      root.flush()\n\n      let button = container.querySelector('button') as HTMLButtonElement\n      button.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))\n      vi.advanceTimersByTime(501)\n      button.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', bubbles: true }))\n      root.flush()\n\n      expect(events).toEqual(['long'])\n    } finally {\n      vi.useRealTimers()\n    }\n  })\n\n  it('dispatches cancel when pointer ends outside target', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.cancel, () => {\n            events.push('cancel')\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n    dispatchPointer(button, 'pointerdown', { isPrimary: true })\n    dispatchPointer(button.ownerDocument, 'pointerup')\n    root.flush()\n\n    expect(events).toEqual(['cancel'])\n  })\n\n  it('dispatches down, up, and press for primary pointer interactions', () => {\n    let events: string[] = []\n    let points: Array<{ type: string; x: number; y: number }> = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.down, (event) => {\n            events.push('down')\n            points.push({ type: 'down', x: event.clientX, y: event.clientY })\n          }),\n          on(pressEvents.up, (event) => {\n            events.push('up')\n            points.push({ type: 'up', x: event.clientX, y: event.clientY })\n          }),\n          on(pressEvents.press, (event) => {\n            events.push('press')\n            points.push({ type: 'press', x: event.clientX, y: event.clientY })\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n\n    dispatchPointer(button, 'pointerdown', { clientX: 11, clientY: 22, isPrimary: true })\n    dispatchPointer(button, 'pointerup', { clientX: 33, clientY: 44, isPrimary: true })\n    root.flush()\n\n    expect(events).toEqual(['down', 'up', 'press'])\n    expect(points).toEqual([\n      { type: 'down', x: 11, y: 22 },\n      { type: 'up', x: 33, y: 44 },\n      { type: 'press', x: 33, y: 44 },\n    ])\n  })\n\n  it('ignores non-primary pointerdown events', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.down, () => {\n            events.push('down')\n          }),\n          on(pressEvents.up, () => {\n            events.push('up')\n          }),\n          on(pressEvents.press, () => {\n            events.push('press')\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n\n    dispatchPointer(button, 'pointerdown', { isPrimary: false })\n    dispatchPointer(button, 'pointerup', { isPrimary: false })\n    root.flush()\n\n    expect(events).toEqual([])\n  })\n\n  it('clears long-press timer on pointerleave while pressed', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.long, () => {\n            events.push('long')\n          }),\n          on(pressEvents.up, () => {\n            events.push('up')\n          }),\n          on(pressEvents.press, () => {\n            events.push('press')\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n\n    dispatchPointer(button, 'pointerdown')\n    dispatchPointer(button, 'pointerleave')\n    dispatchPointer(button, 'pointerup')\n    root.flush()\n\n    expect(events).toEqual(['up', 'press'])\n  })\n\n  it('suppresses pointer up/press after prevented long press', () => {\n    vi.useFakeTimers()\n    try {\n      let events: string[] = []\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      root.render(\n        <button\n          mix={[\n            pressEvents(),\n            on(pressEvents.long, (event) => {\n              events.push('long')\n              event.preventDefault()\n            }),\n            on(pressEvents.up, () => {\n              events.push('up')\n            }),\n            on(pressEvents.press, () => {\n              events.push('press')\n            }),\n          ]}\n        >\n          Press me\n        </button>,\n      )\n      root.flush()\n\n      let button = container.querySelector('button') as HTMLButtonElement\n\n      dispatchPointer(button, 'pointerdown')\n      vi.advanceTimersByTime(501)\n      dispatchPointer(button, 'pointerup')\n      root.flush()\n\n      expect(events).toEqual(['long'])\n    } finally {\n      vi.useRealTimers()\n    }\n  })\n\n  it('dispatches cancel and suppresses up/press when Escape cancels keyboard press', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.down, () => {\n            events.push('down')\n          }),\n          on(pressEvents.up, () => {\n            events.push('up')\n          }),\n          on(pressEvents.press, () => {\n            events.push('press')\n          }),\n          on(pressEvents.cancel, () => {\n            events.push('cancel')\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n\n    button.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))\n    button.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))\n    button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }))\n    root.flush()\n\n    expect(events).toEqual(['down', 'cancel'])\n  })\n\n  it('ignores keyup when no keyboard press is active', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.up, () => {\n            events.push('up')\n          }),\n          on(pressEvents.press, () => {\n            events.push('press')\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n\n    button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }))\n    root.flush()\n\n    expect(events).toEqual([])\n  })\n\n  it('ignores duplicate keydown while a key press is already active', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.down, () => {\n            events.push('down')\n          }),\n          on(pressEvents.up, () => {\n            events.push('up')\n          }),\n          on(pressEvents.press, () => {\n            events.push('press')\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n\n    button.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))\n    button.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))\n    button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }))\n    root.flush()\n\n    expect(events).toEqual(['down', 'up', 'press'])\n  })\n\n  it('ignores document pointerup when no pointer press is active', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.cancel, () => {\n            events.push('cancel')\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n    dispatchPointer(button.ownerDocument, 'pointerup')\n    root.flush()\n\n    expect(events).toEqual([])\n  })\n\n  it('cleans up listeners when root is removed', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          pressEvents(),\n          on(pressEvents.press, () => {\n            events.push('press')\n          }),\n          on(pressEvents.cancel, () => {\n            events.push('cancel')\n          }),\n        ]}\n      >\n        Press me\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button') as HTMLButtonElement\n\n    dispatchPointer(button, 'pointerdown')\n    root.render(null)\n    root.flush()\n\n    dispatchPointer(button, 'pointerup')\n    dispatchPointer(button.ownerDocument, 'pointerup')\n\n    expect(events).toEqual([])\n  })\n})\n\nfunction dispatchPointer(\n  target: EventTarget,\n  type: 'pointerdown' | 'pointerup' | 'pointerleave',\n  init: { clientX?: number; clientY?: number; isPrimary?: boolean } = {},\n) {\n  let event = new Event(type, { bubbles: true, cancelable: true }) as PointerEvent\n  ;(event as { clientX: number }).clientX = init.clientX ?? 0\n  ;(event as { clientY: number }).clientY = init.clientY ?? 0\n  ;(event as { isPrimary: boolean }).isPrimary = init.isPrimary ?? true\n  target.dispatchEvent(event)\n}\n"
  },
  {
    "path": "packages/component/src/lib/mixins/press-mixin.tsx",
    "content": "import { createMixin } from '../mixin.ts'\n\nexport let pressEventType = 'rmx:press' as const\nexport let pressDownEventType = 'rmx:press-down' as const\nexport let pressUpEventType = 'rmx:press-up' as const\nexport let longPressEventType = 'rmx:long-press' as const\nexport let pressCancelEventType = 'rmx:press-cancel' as const\n\ndeclare global {\n  interface HTMLElementEventMap {\n    [pressEventType]: PressEvent\n    [pressDownEventType]: PressEvent\n    [pressUpEventType]: PressEvent\n    [longPressEventType]: PressEvent\n    [pressCancelEventType]: PressEvent\n  }\n}\n\n/**\n * Event emitted by the {@link pressEvents} mixin for pointer and keyboard presses.\n */\nexport class PressEvent extends Event {\n  /**\n   * The horizontal pointer coordinate for the press event.\n   */\n  clientX: number\n\n  /**\n   * The vertical pointer coordinate for the press event.\n   */\n  clientY: number\n\n  constructor(\n    type:\n      | typeof pressEventType\n      | typeof pressDownEventType\n      | typeof pressUpEventType\n      | typeof longPressEventType\n      | typeof pressCancelEventType,\n    init: { clientX?: number; clientY?: number } = {},\n  ) {\n    super(type, { bubbles: true, cancelable: true })\n    this.clientX = init.clientX ?? 0\n    this.clientY = init.clientY ?? 0\n  }\n}\n\nlet basePressEvents = createMixin<HTMLElement>((handle) => {\n  let target: HTMLElement | null = null\n  let doc: Document | null = null\n  let isPointerDown = false\n  let isKeyboardDown = false\n  let longPressTimer = 0\n  let suppressNextUp = false\n\n  let clearLongTimer = () => {\n    if (longPressTimer) {\n      clearTimeout(longPressTimer)\n      longPressTimer = 0\n    }\n  }\n\n  let startLongTimer = () => {\n    if (!target) return\n    clearLongTimer()\n    longPressTimer = window.setTimeout(() => {\n      if (!target) return\n      suppressNextUp = !target.dispatchEvent(new PressEvent(longPressEventType))\n    }, 500)\n  }\n\n  let onPointerDown = (event: PointerEvent) => {\n    if (!target) return\n    if (event.isPrimary === false) return\n    if (isPointerDown) return\n    isPointerDown = true\n    target.dispatchEvent(\n      new PressEvent(pressDownEventType, {\n        clientX: event.clientX,\n        clientY: event.clientY,\n      }),\n    )\n    startLongTimer()\n  }\n\n  let onPointerUp = (event: PointerEvent) => {\n    if (!target) return\n    if (!isPointerDown) return\n    isPointerDown = false\n    clearLongTimer()\n    if (suppressNextUp) {\n      suppressNextUp = false\n      return\n    }\n\n    target.dispatchEvent(\n      new PressEvent(pressUpEventType, {\n        clientX: event.clientX,\n        clientY: event.clientY,\n      }),\n    )\n    target.dispatchEvent(\n      new PressEvent(pressEventType, {\n        clientX: event.clientX,\n        clientY: event.clientY,\n      }),\n    )\n  }\n\n  let onPointerLeave = () => {\n    if (!isPointerDown) return\n    clearLongTimer()\n  }\n\n  let onKeyDown = (event: KeyboardEvent) => {\n    if (!target) return\n    let key = event.key\n    if (key == 'Escape' && (isKeyboardDown || isPointerDown)) {\n      clearLongTimer()\n      suppressNextUp = true\n      target.dispatchEvent(new PressEvent(pressCancelEventType))\n      return\n    }\n\n    if (!(key === 'Enter' || key === ' ')) return\n    if (event.repeat) return\n    if (isKeyboardDown) return\n    isKeyboardDown = true\n\n    target.dispatchEvent(new PressEvent(pressDownEventType))\n    startLongTimer()\n  }\n\n  let onKeyUp = (event: KeyboardEvent) => {\n    if (!target) return\n    let key = event.key\n    if (!(key === 'Enter' || key === ' ')) return\n    if (!isKeyboardDown) return\n    isKeyboardDown = false\n\n    clearLongTimer()\n    if (suppressNextUp) {\n      suppressNextUp = false\n      return\n    }\n\n    target.dispatchEvent(new PressEvent(pressUpEventType))\n    target.dispatchEvent(new PressEvent(pressEventType))\n  }\n\n  let onDocumentPointerUp = () => {\n    if (!target) return\n    if (!isPointerDown) return\n    isPointerDown = false\n    target.dispatchEvent(new PressEvent(pressCancelEventType))\n  }\n\n  handle.addEventListener('insert', (event) => {\n    target = event.node\n    doc = target.ownerDocument\n    target.addEventListener('pointerdown', onPointerDown)\n    target.addEventListener('pointerup', onPointerUp)\n    target.addEventListener('pointerleave', onPointerLeave)\n    target.addEventListener('keydown', onKeyDown)\n    target.addEventListener('keyup', onKeyUp)\n    doc.addEventListener('pointerup', onDocumentPointerUp)\n  })\n\n  handle.addEventListener('remove', () => {\n    clearLongTimer()\n    if (target) {\n      target.removeEventListener('pointerdown', onPointerDown)\n      target.removeEventListener('pointerup', onPointerUp)\n      target.removeEventListener('pointerleave', onPointerLeave)\n      target.removeEventListener('keydown', onKeyDown)\n      target.removeEventListener('keyup', onKeyUp)\n    }\n    if (doc) {\n      doc.removeEventListener('pointerup', onDocumentPointerUp)\n    }\n    target = null\n    doc = null\n    isPointerDown = false\n    isKeyboardDown = false\n    suppressNextUp = false\n  })\n})\n\ntype PressEventsMixin = typeof basePressEvents & {\n  readonly press: typeof pressEventType\n  readonly down: typeof pressDownEventType\n  readonly up: typeof pressUpEventType\n  readonly long: typeof longPressEventType\n  readonly cancel: typeof pressCancelEventType\n}\n\n/**\n * Normalizes pointer and keyboard input into press lifecycle events.\n */\nexport let pressEvents: PressEventsMixin = Object.assign(basePressEvents, {\n  press: pressEventType,\n  down: pressDownEventType,\n  up: pressUpEventType,\n  long: longPressEventType,\n  cancel: pressCancelEventType,\n})\n"
  },
  {
    "path": "packages/component/src/lib/mixins/ref-mixin.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport type { Handle } from '../component.ts'\nimport { createRoot } from '../vdom.ts'\nimport { invariant } from '../invariant.ts'\nimport { ref } from './ref-mixin.tsx'\nimport type { Assert, Equal } from '../../test/utils.ts'\n\ndescribe('ref mixin', () => {\n  it('passes the bound node and handle signal to the callback', () => {\n    let receivedNode: Element | null = null\n    let state: { signal?: AbortSignal } = {}\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          ref((node, signal) => {\n            receivedNode = node\n            state.signal = signal\n          }),\n        ]}\n      >\n        click\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    expect(receivedNode).toBe(button)\n    let signal =\n      state.signal ??\n      (() => {\n        throw new Error('expected ref callback to receive signal')\n      })()\n    expect(signal).toBeInstanceOf(AbortSignal)\n    expect(signal.aborted).toBe(false)\n  })\n\n  it('aborts the signal when the host node is removed', () => {\n    let state: { signal?: AbortSignal } = {}\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <div\n        mix={[\n          ref((_node, signal) => {\n            state.signal = signal\n          }),\n        ]}\n      />,\n    )\n    root.flush()\n    let signal =\n      state.signal ??\n      (() => {\n        throw new Error('expected ref callback to receive signal')\n      })()\n    expect(signal.aborted).toBe(false)\n\n    root.render(null)\n    root.flush()\n    expect(signal.aborted).toBe(true)\n  })\n\n  it('allows handle.update() during the insert callback', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    function Toggle(handle: Handle) {\n      let ready = false\n\n      return () => (\n        <button\n          mix={[\n            ref(() => {\n              if (ready) return\n              ready = true\n              handle.update()\n            }),\n          ]}\n        >\n          {ready ? 'ready' : 'loading'}\n        </button>\n      )\n    }\n\n    root.render(<Toggle />)\n    root.flush()\n\n    expect(container.textContent).toBe('ready')\n  })\n\n  it('dispatches insert before queued component tasks', () => {\n    let events: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    function Example(handle: Handle) {\n      return () => {\n        handle.queueTask(() => {\n          events.push('task')\n        })\n\n        return (\n          <div\n            mix={[\n              ref(() => {\n                events.push('insert')\n              }),\n            ]}\n          />\n        )\n      }\n    }\n\n    root.render(<Example />)\n    root.flush()\n\n    expect(events).toEqual(['insert', 'task'])\n  })\n})\n\nlet infersNodeType = (\n  <button\n    mix={[\n      ref((node, signal) => {\n        type inferredNode = Assert<Equal<typeof node, HTMLButtonElement>>\n        type inferredSignal = Assert<Equal<typeof signal, AbortSignal>>\n      }),\n    ]}\n  />\n)\n"
  },
  {
    "path": "packages/component/src/lib/mixins/ref-mixin.tsx",
    "content": "import { createMixin } from '../mixin.ts'\nimport type { ElementProps } from '../jsx.ts'\n\n/**\n * Callback invoked with the bound node and a lifetime signal.\n */\nexport type RefCallback<node extends EventTarget> = (node: node, signal: AbortSignal) => void\n\n/**\n * Calls a callback when an element is inserted and aborts it when removed.\n */\nexport let ref = createMixin<Element, [callback: RefCallback<Element>], ElementProps>((handle) => {\n  let controller: AbortController | undefined\n\n  handle.addEventListener('insert', (event) => {\n    controller = new AbortController()\n    callback(event.node, controller.signal)\n  })\n\n  handle.addEventListener('remove', () => {\n    controller?.abort(new DOMException('', 'AbortError'))\n    controller = undefined\n  })\n\n  let callback: RefCallback<Element> = () => {}\n  return (nextCallback) => {\n    callback = nextCallback\n    return handle.element\n  }\n})\n"
  },
  {
    "path": "packages/component/src/lib/navigation.ts",
    "content": "import { getTopFrame, getNamedFrame } from './run.ts'\n\ntype NavigationState = {\n  target: string | undefined\n  src: string\n  resetScroll: boolean\n  $rmx: true\n}\n\ntype SourceElementNavigateEvent = NavigateEvent & {\n  sourceElement?: Element | null\n}\n\n/**\n * Options for client-side frame-aware navigation.\n */\nexport type NavigationOptions = {\n  src?: string\n  target?: string\n  history?: 'push' | 'replace'\n  resetScroll?: boolean\n}\n\n/**\n * Performs a Navigation API transition understood by Remix frame runtime state.\n *\n * @param href Destination URL.\n * @param options Navigation options.\n */\nexport async function navigate(href: string, options?: NavigationOptions) {\n  let state = {\n    target: options?.target,\n    src: options?.src ?? href,\n    resetScroll: options?.resetScroll !== false,\n    $rmx: true,\n  } satisfies NavigationState\n  let transition = window.navigation.navigate(href, { state, history: options?.history })\n  await transition.finished\n}\n\n/**\n * Starts listening for Navigation API transitions and routes them through frame reloads.\n *\n * @param signal Abort signal used to remove the listener.\n */\nexport function startNavigationListener(signal: AbortSignal) {\n  let navigation = window.navigation\n\n  navigation.updateCurrentEntry({\n    state: { target: undefined, src: window.location.href, resetScroll: true, $rmx: true },\n  })\n\n  navigation.addEventListener(\n    'navigate',\n    (event) => {\n      if (!event.canIntercept) return\n\n      let state = getRuntimeNavigationState(event)\n      if (!state) return\n\n      let topFrame = getTopFrame()\n      let namedFrame = state.target ? getNamedFrame(state.target) : undefined\n      let frame = namedFrame ?? topFrame\n\n      event.intercept({\n        async handler() {\n          if (event.navigationType !== 'traverse') {\n            navigation.updateCurrentEntry({ state })\n          }\n\n          frame.src = frame === topFrame ? event.destination.url : state.src\n          await frame.reload()\n\n          let isNewEntry = event.navigationType === 'push' || event.navigationType === 'replace'\n          if (state.resetScroll && isNewEntry) {\n            window.scrollTo(0, 0)\n          }\n        },\n      })\n    },\n    { signal },\n  )\n}\n\nfunction isRuntimeNavigation(info: unknown): info is NavigationState {\n  return typeof info === 'object' && info != null && '$rmx' in info\n}\n\nfunction getRuntimeNavigationState(event: NavigateEvent): NavigationState | undefined {\n  if (event.navigationType === 'traverse') {\n    return getTraverseNavigationState(event)\n  }\n\n  let sourceState = getSourceElementNavigationState(event)\n  if (sourceState) return sourceState\n\n  let destinationState = event.destination.getState()\n  if (isRuntimeNavigation(destinationState)) return destinationState\n}\n\nfunction getTraverseNavigationState(event: NavigateEvent): NavigationState | undefined {\n  let destinationState = event.destination.getState()\n  if (isRuntimeNavigation(destinationState)) {\n    return destinationState\n  }\n\n  // Safari returns `null` for destination.getState(), even though its in the\n  // navigation.entries(), so we do its job for it and look it up.\n  let navigation = window.navigation\n  let matchingEntry = navigation.entries().find((entry) => entry.key === event.destination.key)\n  if (matchingEntry) {\n    let state = matchingEntry.getState()\n    if (isRuntimeNavigation(state)) {\n      return state\n    }\n  }\n\n  return undefined\n}\n\nfunction getSourceElementNavigationState(event: NavigateEvent): NavigationState | undefined {\n  let sourceEvent = event as SourceElementNavigateEvent\n  let sourceElement = sourceEvent.sourceElement\n  if (!(sourceElement instanceof Element)) return\n  let linkElement = sourceElement.closest('a, area')\n  if (!(linkElement instanceof Element)) return\n  if (linkElement.hasAttribute('rmx-document')) return\n  if (linkElement.hasAttribute('download')) return\n\n  return {\n    target: linkElement.getAttribute('rmx-target') ?? undefined,\n    src: linkElement.getAttribute('rmx-src') ?? event.destination.url,\n    resetScroll: linkElement.getAttribute('rmx-reset-scroll') !== 'false',\n    $rmx: true,\n  } satisfies NavigationState\n}\n"
  },
  {
    "path": "packages/component/src/lib/reconcile.ts",
    "content": "import type { Component, ComponentHandle, FrameContent, FrameHandle } from './component.ts'\nimport { createComponent, Frame } from './component.ts'\nimport type { Frame as FrameInstance, FrameRuntime } from './frame.ts'\nimport { createFrame } from './frame.ts'\nimport { createRangeRoot } from './vdom.ts'\nimport type {\n  ComponentNode,\n  CommittedComponentNode,\n  CommittedHostNode,\n  CommittedTextNode,\n  FragmentNode,\n  HostNode,\n  TextNode,\n  VNode,\n  VNodeType,\n} from './vnode.ts'\nimport {\n  isCommittedComponentNode,\n  isComponentNode,\n  isCommittedHostNode,\n  isCommittedTextNode,\n  isFragmentNode,\n  isHostNode,\n  isTextNode,\n  findContextFromAncestry,\n} from './vnode.ts'\nimport { invariant } from './invariant.ts'\nimport { diffHostProps } from './diff-props.ts'\nimport type { StyleManager } from './style/index.ts'\nimport type { ElementProps } from './jsx.ts'\nimport { skipComments, logHydrationMismatch } from './client-entries.ts'\nimport type { Scheduler } from './scheduler.ts'\nimport { toVNode } from './to-vnode.ts'\nimport {\n  bindMixinRuntime,\n  cancelPendingMixinRemoval,\n  dispatchMixinBeforeUpdate,\n  dispatchMixinCommit,\n  getMixinRuntimeSignal,\n  prepareMixinRemoval,\n  resolveMixedProps,\n  teardownMixins,\n  type MixinRuntimeBinding,\n  type MixinRuntimeState,\n} from './mixin.ts'\n\nconst SVG_NS = 'http://www.w3.org/2000/svg'\n\n// Internal diffing flags (modeled after Preact)\nconst INSERT_VNODE = 1 << 0\nconst MATCHED = 1 << 1\n\nlet idCounter = 0\nlet persistedRemovalToken = 0\nlet persistedMixinNodes = new Set<CommittedHostNode>()\nlet activeSchedulerUpdateParents: ParentNode[] | undefined\n\n// Compute SVG context for a node based on its parent and type.\n// Returns true if the node is within an SVG subtree, false otherwise.\nfunction getSvgContext(vParent: VNode, nodeType: VNodeType): boolean {\n  // Only host elements (strings) can affect SVG context\n  if (typeof nodeType === 'string') {\n    // svg element creates SVG context\n    if (nodeType === 'svg') return true\n    // foreignObject switches back to HTML context\n    if (nodeType === 'foreignObject') return false\n  }\n  // Otherwise inherit from parent\n  return vParent._svg ?? false\n}\n\nfunction getHostProps(node: HostNode | CommittedHostNode): ElementProps {\n  return node._mixedProps ?? node.props\n}\n\nfunction markNodePersistedByMixins(node: CommittedHostNode, domParent: ParentNode, token: number) {\n  node._persistedByMixins = true\n  node._persistedParentByMixins = domParent\n  node._persistedRemovalToken = token\n  persistedMixinNodes.add(node)\n  bindMixinRuntime(node._mixState as MixinRuntimeState | undefined, undefined)\n}\n\nfunction unmarkNodePersistedByMixins(node: CommittedHostNode) {\n  node._persistedByMixins = false\n  node._persistedParentByMixins = undefined\n  node._persistedRemovalToken = undefined\n  persistedMixinNodes.delete(node)\n}\n\nfunction findMatchingPersistedMixinNode(\n  type: string,\n  key: string | undefined,\n  domParent: ParentNode,\n): CommittedHostNode | null {\n  if (key == null) return null\n  for (let node of persistedMixinNodes) {\n    if (node._persistedParentByMixins !== domParent) continue\n    if (node.type !== type) continue\n    if (node.key !== key) continue\n    return node\n  }\n  return null\n}\n\ntype ControlledReflectionState = {\n  disposed: boolean\n  listenersAttached: boolean\n  pendingRestoreVersion: number\n  managesValue: boolean\n  managesChecked: boolean\n  hasControlledValue: boolean\n  controlledValue: unknown\n  hasControlledChecked: boolean\n  controlledChecked: unknown\n  onInput: () => void\n  onChange: () => void\n}\n\nfunction ensureControlledReflection(\n  node: CommittedHostNode,\n  scheduler: Scheduler,\n): ControlledReflectionState {\n  let existing = node._controlledState as ControlledReflectionState | undefined\n  if (existing) return existing\n\n  let state: ControlledReflectionState = {\n    disposed: false,\n    listenersAttached: false,\n    pendingRestoreVersion: 0,\n    managesValue: false,\n    managesChecked: false,\n    hasControlledValue: false,\n    controlledValue: undefined,\n    hasControlledChecked: false,\n    controlledChecked: undefined,\n    onInput: () => {\n      scheduleControlledRestore(node, state)\n    },\n    onChange: () => {\n      scheduleControlledRestore(node, state)\n    },\n  }\n\n  node._controlledState = state\n  scheduler.enqueueTasks([\n    () => {\n      if (state.disposed) return\n      node._dom.addEventListener('input', state.onInput)\n      node._dom.addEventListener('change', state.onChange)\n      state.listenersAttached = true\n    },\n  ])\n  return state\n}\n\nfunction syncControlledReflection(node: CommittedHostNode, props: ElementProps): void {\n  let state = node._controlledState as ControlledReflectionState | undefined\n  if (!state || state.disposed) return\n\n  state.managesValue = canManageValue(node.type, node._dom)\n  state.managesChecked = canReflectProperty(node._dom, 'checked')\n  state.hasControlledValue = state.managesValue && hasControlledValueProp(props)\n  state.controlledValue = props.value\n  state.hasControlledChecked = state.managesChecked && hasControlledCheckedProp(props)\n  state.controlledChecked = props.checked\n  state.pendingRestoreVersion++\n}\n\nfunction shouldTrackControlledReflection(props: ElementProps): boolean {\n  return hasControlledValueProp(props) || hasControlledCheckedProp(props)\n}\n\nfunction scheduleControlledRestore(\n  node: CommittedHostNode,\n  state: ControlledReflectionState,\n): void {\n  if (state.disposed) return\n  let version = ++state.pendingRestoreVersion\n  queueMicrotask(() => {\n    if (state.disposed) return\n    if (state.pendingRestoreVersion !== version) return\n    restoreControlledReflections(node, state)\n  })\n}\n\nfunction restoreControlledReflections(\n  node: CommittedHostNode,\n  state: ControlledReflectionState,\n): void {\n  let element = node._dom\n  if (state.hasControlledValue && readDomProp(element, 'value') !== state.controlledValue) {\n    setPropertyReflection(element, 'value', state.controlledValue)\n  }\n  if (state.hasControlledChecked && readDomProp(element, 'checked') !== state.controlledChecked) {\n    setPropertyReflection(element, 'checked', state.controlledChecked)\n  }\n}\n\nfunction teardownControlledReflection(node: CommittedHostNode): void {\n  let state = node._controlledState as ControlledReflectionState | undefined\n  if (!state) return\n  state.disposed = true\n  state.pendingRestoreVersion++\n  if (state.listenersAttached) {\n    node._dom.removeEventListener('input', state.onInput)\n    node._dom.removeEventListener('change', state.onChange)\n    state.listenersAttached = false\n  }\n}\n\nfunction canManageValue(type: string, element: Element): boolean {\n  if (type === 'progress') return false\n  return canReflectProperty(element, 'value')\n}\n\nfunction hasControlledValueProp(props: ElementProps): boolean {\n  return 'value' in props && props.value !== undefined\n}\n\nfunction hasControlledCheckedProp(props: ElementProps): boolean {\n  return 'checked' in props && props.checked !== undefined\n}\n\nfunction canReflectProperty(\n  element: Element,\n  key: string,\n): element is Element & Record<string, unknown> {\n  return key in element && !key.includes('-')\n}\n\nfunction readDomProp(element: Element, key: string): unknown {\n  if (!canReflectProperty(element, key)) return undefined\n  return element[key]\n}\n\nfunction setPropertyReflection(element: Element, key: string, value: unknown): void {\n  if (!canReflectProperty(element, key)) return\n  element[key] = value == null ? '' : value\n}\n\nfunction resolveNodeMixProps(\n  node: HostNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  state?: MixinRuntimeState,\n): ElementProps {\n  let mix = node.props.mix\n  if (state == null && (mix == null || (Array.isArray(mix) && mix.length === 0))) {\n    node._mixState = undefined\n    node._mixedProps = node.props\n    return node.props\n  }\n\n  let resolved = resolveMixedProps({\n    hostType: node.type,\n    frame,\n    scheduler,\n    props: node.props,\n    state,\n  })\n  node._mixState = resolved.state\n  node._mixedProps = resolved.props\n  return resolved.props\n}\n\nfunction enqueueMixinBindingUpdate(\n  this: MixinRuntimeBinding,\n  done: (signal: AbortSignal) => void,\n): void {\n  let node = this.target as CommittedHostNode\n  let state = node._mixState as MixinRuntimeState | undefined\n  this.scheduler.enqueueWork([\n    () => {\n      if (state?.aborted) {\n        done(getMixinRuntimeSignal(state))\n        return\n      }\n\n      dispatchMixinBeforeUpdate(state)\n      let prevProps = getHostProps(node)\n      let nextProps = resolveNodeMixProps(node, this.frame, this.scheduler, state)\n      diffHostProps(prevProps, nextProps, this.node)\n\n      dispatchMixinCommit(state)\n      done(state ? getMixinRuntimeSignal(state) : AbortSignal.abort())\n    },\n  ])\n}\n\nfunction bindNodeMixRuntime(\n  node: CommittedHostNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  reclaimed: boolean = false,\n  parent?: ParentNode,\n) {\n  let state = node._mixState as MixinRuntimeState | undefined\n  bindMixinRuntime(\n    state,\n    {\n      node: node._dom,\n      parent: parent ?? (node._dom.parentNode as ParentNode),\n      key: node.key,\n      target: node,\n      frame,\n      scheduler,\n      enqueueUpdate: enqueueMixinBindingUpdate,\n    },\n    { dispatchReclaimed: reclaimed },\n  )\n}\n\nfunction isHeadHostNode(node: HostNode): boolean {\n  return node.type.toLowerCase() === 'head'\n}\n\nfunction getDocumentHead(domParent: ParentNode): HTMLHeadElement | null {\n  if (domParent instanceof Document) {\n    return domParent.head\n  }\n  if (domParent instanceof Node) {\n    return domParent.ownerDocument?.head ?? null\n  }\n  return null\n}\n\nexport function diffVNodes(\n  curr: VNode | null,\n  next: VNode,\n  domParent: ParentNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  vParent: VNode,\n  rootTarget: EventTarget,\n  anchor?: Node,\n  rootCursor?: Node | null,\n): Node | null | undefined {\n  next._parent = vParent // set parent for initial render context lookups\n  next._svg = getSvgContext(vParent, next.type)\n\n  // new\n  if (curr === null) {\n    return insert(\n      next,\n      domParent,\n      frame,\n      scheduler,\n      styles,\n      vParent,\n      rootTarget,\n      anchor,\n      rootCursor,\n    )\n  }\n\n  if (curr.type !== next.type) {\n    replace(curr, next, domParent, frame, scheduler, styles, vParent, rootTarget, anchor)\n    return rootCursor\n  }\n\n  if (isCommittedTextNode(curr) && isTextNode(next)) {\n    diffText(curr, next, vParent)\n    return rootCursor\n  }\n\n  if (isCommittedHostNode(curr) && isHostNode(next)) {\n    diffHost(curr, next, frame, scheduler, styles, vParent, rootTarget)\n    return rootCursor\n  }\n\n  if (isCommittedComponentNode(curr) && isComponentNode(next)) {\n    diffComponent(curr, next, frame, scheduler, styles, domParent, vParent, rootTarget)\n    return rootCursor\n  }\n\n  if (isFragmentNode(curr) && isFragmentNode(next)) {\n    diffChildren(\n      curr._children,\n      next._children,\n      domParent,\n      frame,\n      scheduler,\n      styles,\n      vParent,\n      rootTarget,\n      undefined,\n      anchor,\n    )\n    return rootCursor\n  }\n\n  if (curr.type === Frame && next.type === Frame) {\n    diffFrame(curr, next, domParent, frame, scheduler, styles, vParent, rootTarget, anchor)\n    return rootCursor\n  }\n\n  invariant(false, 'Unexpected diff case')\n}\n\nfunction replace(\n  curr: VNode,\n  next: VNode,\n  domParent: ParentNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  vParent: VNode,\n  rootTarget: EventTarget,\n  anchor?: Node,\n) {\n  let currAnchor = findFirstDomAnchor(curr)\n  if (currAnchor && currAnchor.parentNode === domParent) {\n    let replacementAnchor = document.createComment('rmx:replace')\n    domParent.insertBefore(replacementAnchor, currAnchor)\n    try {\n      remove(curr, domParent, scheduler, styles)\n      insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)\n    } finally {\n      replacementAnchor.parentNode?.removeChild(replacementAnchor)\n    }\n    return\n  }\n\n  let replacementAnchor = findNextSiblingDomAnchor(curr, vParent) ?? anchor\n  remove(curr, domParent, scheduler, styles)\n  insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)\n}\n\nfunction diffHost(\n  curr: CommittedHostNode,\n  next: HostNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  vParent: VNode,\n  rootTarget: EventTarget,\n) {\n  let mixState = curr._mixState as MixinRuntimeState | undefined\n  let currProps = getHostProps(curr)\n  let nextProps = resolveNodeMixProps(next, frame, scheduler, mixState)\n  if (shouldDispatchInlineMixinLifecycle(curr._dom)) {\n    dispatchMixinBeforeUpdate(next._mixState as MixinRuntimeState | undefined)\n  }\n\n  // Handle innerHTML prop BEFORE diffChildren to avoid clearing children\n  if (nextProps.innerHTML != null) {\n    // innerHTML is set, update it if changed\n    if (currProps.innerHTML !== nextProps.innerHTML) {\n      curr._dom.innerHTML = nextProps.innerHTML as string\n    }\n  } else if (currProps.innerHTML != null) {\n    // innerHTML was removed, clear it before adding children\n    curr._dom.innerHTML = ''\n  }\n\n  diffChildren(\n    curr._children,\n    next._children,\n    curr._dom,\n    frame,\n    scheduler,\n    styles,\n    next,\n    rootTarget,\n  )\n  diffHostProps(currProps, nextProps, curr._dom)\n\n  next._dom = curr._dom\n  next._parent = vParent\n  next._controller = curr._controller\n  next._controlledState = curr._controlledState\n\n  if (next._controlledState || shouldTrackControlledReflection(nextProps)) {\n    ensureControlledReflection(next as CommittedHostNode, scheduler)\n    syncControlledReflection(next as CommittedHostNode, nextProps)\n  }\n\n  bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)\n  if (shouldDispatchInlineMixinLifecycle(curr._dom)) {\n    scheduler.enqueueCommitPhase([\n      () => dispatchMixinCommit(next._mixState as MixinRuntimeState | undefined),\n    ])\n  }\n\n  return\n}\n\nfunction setupHostNode(node: HostNode, dom: Element, scheduler: Scheduler): void {\n  node._dom = dom\n  let props = getHostProps(node)\n  let committedNode = node as CommittedHostNode\n\n  if (shouldTrackControlledReflection(props)) {\n    ensureControlledReflection(committedNode, scheduler)\n    syncControlledReflection(committedNode, props)\n  }\n}\n\nfunction diffText(curr: CommittedTextNode, next: TextNode, vParent: VNode) {\n  if (curr._text !== next._text) {\n    curr._dom.textContent = next._text\n  }\n  next._dom = curr._dom\n  next._parent = vParent\n}\n\nfunction insert(\n  node: VNode,\n  domParent: ParentNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  vParent: VNode,\n  rootTarget: EventTarget,\n  anchor?: Node,\n  cursor?: Node | null,\n): Node | null | undefined {\n  node._parent = vParent // set parent for initial render context lookups\n  node._svg = getSvgContext(vParent, node.type)\n\n  // Stop hydration if cursor has reached the anchor (end boundary)\n  // Check BEFORE skipComments to prevent escaping range root markers\n  if (cursor && anchor && cursor === anchor) {\n    cursor = null\n  }\n\n  cursor =\n    node.type === Frame\n      ? skipCommentsExceptFrameStart(cursor ?? null)\n      : skipComments(cursor ?? null)\n\n  // Also check after skipComments in case we skipped past the anchor\n  if (cursor && anchor && cursor === anchor) {\n    cursor = null\n  }\n\n  let doInsert = anchor\n    ? (dom: Node) => domParent.insertBefore(dom, anchor)\n    : (dom: Node) => domParent.appendChild(dom)\n\n  if (isTextNode(node)) {\n    if (cursor instanceof Text) {\n      node._parent = vParent\n      // Handle text node consolidation: server renders adjacent text as single node\n      // e.g., <span>Hello {world}</span> → server: \"Hello world\", client: [\"Hello \", \"world\"]\n      if (cursor.data !== node._text) {\n        if (cursor.data.startsWith(node._text) && node._text.length < cursor.data.length) {\n          // Consolidation case: split the text node at the boundary\n          // cursor becomes the first part (node._text), remainder is returned for next vnode\n          let remainder = cursor.splitText(node._text.length)\n          node._dom = cursor\n          return remainder\n        }\n        // Genuine mismatch - correct it\n        logHydrationMismatch('text mismatch', cursor.data, node._text)\n        cursor.data = node._text\n      }\n      node._dom = cursor\n      return cursor.nextSibling\n    }\n    let dom = document.createTextNode(node._text)\n    node._dom = dom\n    node._parent = vParent\n    doInsert(dom)\n    return cursor\n  }\n\n  if (isHostNode(node)) {\n    let hostProps = resolveNodeMixProps(node, frame, scheduler)\n\n    if (isHeadHostNode(node)) {\n      let targetHead = getDocumentHead(domParent)\n      if (targetHead) {\n        let childCursor = cursor\n        if (cursor instanceof Element && cursor.tagName.toLowerCase() === 'head') {\n          childCursor = cursor.firstChild\n          let nextCursor = cursor.nextSibling\n          if (cursor !== targetHead) {\n            while (cursor.firstChild) {\n              targetHead.appendChild(cursor.firstChild)\n            }\n            cursor.remove()\n          }\n          cursor = nextCursor\n        }\n\n        // Render explicit <head> children directly into document.head.\n        diffChildren(\n          null,\n          node._children,\n          targetHead,\n          frame,\n          scheduler,\n          styles,\n          node,\n          rootTarget,\n          childCursor,\n        )\n        diffHostProps({}, hostProps, targetHead)\n        setupHostNode(node, targetHead, scheduler)\n        bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)\n        return cursor\n      }\n    }\n\n    // Check for matching mixin-persisted node that can be reclaimed\n    let persistedNode = findMatchingPersistedMixinNode(node.type, node.key, domParent)\n    if (persistedNode) {\n      reclaimPersistedMixinNode(persistedNode, node, frame, scheduler, styles, vParent, rootTarget)\n      return cursor\n    }\n\n    if (cursor instanceof Element) {\n      // SVG elements have case-sensitive tag names (e.g. linearGradient, clipPath)\n      // HTML elements are case-insensitive, so we lowercase for comparison\n      let cursorTag = node._svg ? cursor.tagName : cursor.tagName.toLowerCase()\n      if (cursorTag === node.type) {\n        let nextCursor = cursor.nextSibling\n        diffHostProps({}, hostProps, cursor)\n\n        // Handle innerHTML prop\n        if (hostProps.innerHTML != null) {\n          cursor.innerHTML = hostProps.innerHTML as string\n        } else {\n          let childCursor = cursor.firstChild\n          // Ignore excess nodes - browser extensions may inject content\n          diffChildren(\n            null,\n            node._children,\n            cursor,\n            frame,\n            scheduler,\n            styles,\n            node,\n            rootTarget,\n            childCursor,\n          )\n        }\n\n        setupHostNode(node, cursor, scheduler)\n        bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)\n        return nextCursor\n      } else {\n        // Type mismatch - try single-advance retry to handle browser extension injections\n        // at the start of containers. Skip this node and try the next sibling once.\n        let nextSibling = skipComments(cursor.nextSibling)\n        if (nextSibling instanceof Element) {\n          let nextTag = node._svg ? nextSibling.tagName : nextSibling.tagName.toLowerCase()\n          if (nextTag === node.type) {\n            let nextCursor = nextSibling.nextSibling\n            // Found a match after skipping - adopt it and leave skipped node in place\n            diffHostProps({}, hostProps, nextSibling)\n\n            if (hostProps.innerHTML != null) {\n              nextSibling.innerHTML = hostProps.innerHTML as string\n            } else {\n              let childCursor = nextSibling.firstChild\n              diffChildren(\n                null,\n                node._children,\n                nextSibling,\n                frame,\n                scheduler,\n                styles,\n                node,\n                rootTarget,\n                childCursor,\n              )\n            }\n\n            setupHostNode(node, nextSibling, scheduler)\n            bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)\n            return nextCursor\n          }\n        }\n        // Retry failed - log mismatch and create new element (don't remove mismatched nodes)\n        logHydrationMismatch('tag', cursorTag, node.type)\n        cursor = undefined // stop hydration for this tree\n      }\n    }\n    let dom = node._svg\n      ? document.createElementNS(SVG_NS, node.type)\n      : document.createElement(node.type)\n    diffHostProps({}, hostProps, dom)\n\n    // Handle innerHTML prop\n    if (hostProps.innerHTML != null) {\n      dom.innerHTML = hostProps.innerHTML as string\n    } else {\n      diffChildren(null, node._children, dom, frame, scheduler, styles, node, rootTarget)\n    }\n\n    setupHostNode(node, dom, scheduler)\n    bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)\n    doInsert(dom)\n    return cursor\n  }\n\n  if (isFragmentNode(node)) {\n    // Insert fragment children in order before the same anchor\n    for (let child of node._children) {\n      cursor = insert(\n        child,\n        domParent,\n        frame,\n        scheduler,\n        styles,\n        vParent,\n        rootTarget,\n        anchor,\n        cursor,\n      )\n    }\n    return cursor\n  }\n\n  if (isComponentNode(node)) {\n    return diffComponent(\n      null,\n      node,\n      frame,\n      scheduler,\n      styles,\n      domParent,\n      vParent,\n      rootTarget,\n      anchor,\n      cursor,\n    )\n  }\n\n  if (node.type === Frame) {\n    return insertFrame(\n      node,\n      domParent,\n      frame,\n      scheduler,\n      styles,\n      vParent,\n      rootTarget,\n      anchor,\n      cursor,\n    )\n  }\n\n  invariant(false, 'Unexpected node type')\n}\n\nfunction diffFrame(\n  curr: VNode,\n  next: VNode,\n  domParent: ParentNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  vParent: VNode,\n  rootTarget: EventTarget,\n  anchor?: Node,\n): void {\n  let currSrc = getFrameSrc(curr)\n  let nextSrc = getFrameSrc(next)\n  let currName = getFrameName(curr)\n  let nextName = getFrameName(next)\n\n  if (currName !== nextName) {\n    let replaceAnchor = curr._rangeEnd?.nextSibling ?? anchor\n    remove(curr, domParent, scheduler, styles)\n    insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replaceAnchor)\n    return\n  }\n\n  // If the frame hasn't resolved yet, preserve existing cancel/remount behavior\n  // so pending streams from the old src cannot take over the new src.\n  if (currSrc !== nextSrc && !curr._frameResolved) {\n    let replaceAnchor = curr._rangeEnd?.nextSibling ?? anchor\n    remove(curr, domParent, scheduler, styles)\n    insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replaceAnchor)\n    return\n  }\n\n  next._rangeStart = curr._rangeStart\n  next._rangeEnd = curr._rangeEnd\n  next._frameInstance = curr._frameInstance\n  next._frameFallbackRoot = curr._frameFallbackRoot\n  next._frameResolveToken = curr._frameResolveToken\n  next._frameResolved = curr._frameResolved\n  next._parent = vParent\n\n  if (currSrc !== nextSrc) {\n    let frameInstance = next._frameInstance as FrameInstance | undefined\n    if (frameInstance) {\n      frameInstance.handle.src = nextSrc\n    }\n\n    let runtime = getFrameRuntime(frame)\n    if (runtime) {\n      resolveClientFrame(next, runtime, rootTarget)\n    }\n  }\n\n  if (!next._frameResolved && next._frameFallbackRoot) {\n    next._frameFallbackRoot.render(next.props?.fallback ?? null)\n  }\n}\n\nfunction insertFrame(\n  node: VNode,\n  domParent: ParentNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  vParent: VNode,\n  rootTarget: EventTarget,\n  anchor?: Node,\n  cursor?: Node | null,\n): Node | null | undefined {\n  let runtime = getFrameRuntime(frame)\n  if (!runtime || (runtime as { canResolveFrames?: boolean }).canResolveFrames === false) {\n    throw new Error(\n      'Cannot render <Frame /> without frame runtime. Use run() or pass frameInit to createRoot/createRangeRoot.',\n    )\n  }\n\n  // Hydration path: adopt server-rendered frame markers and reuse the existing\n  // frame instance created during createSubFrames().\n  if (isFrameStartComment(cursor)) {\n    let start = cursor\n    let end = findFrameEndComment(start)\n    if (end) {\n      node._rangeStart = start\n      node._rangeEnd = end\n      node._parent = vParent\n      node._frameResolveToken = 0\n      node._frameResolveController = undefined\n      node._frameFallbackRoot = undefined\n      node._frameResolved = true\n\n      let frameId = getFrameIdFromComment(start)\n      let marker = frameId ? runtime.data.f?.[frameId] : undefined\n      let src = marker?.src ?? getFrameSrc(node)\n      let instance = runtime.frameInstances.get(start)\n      if (!instance) {\n        instance = createFrame([start, end], {\n          name: getFrameName(node),\n          src,\n          marker: frameId && marker ? { ...marker, id: frameId } : undefined,\n          errorTarget: runtime.errorTarget,\n          loadModule: runtime.loadModule,\n          resolveFrame: runtime.resolveFrame,\n          pendingClientEntries: runtime.pendingClientEntries,\n          scheduler: runtime.scheduler,\n          styleManager: runtime.styleManager,\n          data: runtime.data,\n          moduleCache: runtime.moduleCache,\n          moduleLoads: runtime.moduleLoads,\n          frameInstances: runtime.frameInstances,\n          namedFrames: runtime.namedFrames,\n        })\n        runtime.frameInstances.set(start, instance)\n      }\n\n      node._frameInstance = instance\n\n      return end.nextSibling\n    }\n  }\n\n  let start = document.createComment(` rmx:f:${randomFrameId()} `)\n  let end = document.createComment(' /rmx:f ')\n  let doInsert = anchor\n    ? (dom: Node) => domParent.insertBefore(dom, anchor)\n    : (dom: Node) => domParent.appendChild(dom)\n\n  doInsert(start)\n  doInsert(end)\n\n  node._rangeStart = start\n  node._rangeEnd = end\n  node._parent = vParent\n\n  let fallbackRoot = createRangeRoot([start, end], {\n    frame,\n    styleManager: styles,\n  })\n  fallbackRoot.render(node.props?.fallback ?? null)\n  node._frameFallbackRoot = fallbackRoot\n  node._frameResolved = false\n  node._frameResolveToken = 0\n\n  let instance = createFrame([start, end], {\n    name: getFrameName(node),\n    src: getFrameSrc(node),\n    errorTarget: runtime.errorTarget,\n    loadModule: runtime.loadModule,\n    resolveFrame: runtime.resolveFrame,\n    pendingClientEntries: runtime.pendingClientEntries,\n    scheduler: runtime.scheduler,\n    styleManager: runtime.styleManager,\n    data: runtime.data,\n    moduleCache: runtime.moduleCache,\n    moduleLoads: runtime.moduleLoads,\n    frameInstances: runtime.frameInstances,\n    namedFrames: runtime.namedFrames,\n  })\n  node._frameInstance = instance\n  runtime.frameInstances.set(start, instance)\n\n  resolveClientFrame(node, runtime, rootTarget)\n\n  return cursor\n}\n\nfunction resolveClientFrame(node: VNode, runtime: FrameRuntime, rootTarget: EventTarget): void {\n  let frameSrc = getFrameSrc(node)\n  let instance = node._frameInstance as FrameInstance | undefined\n  if (!instance) return\n\n  let token = (node._frameResolveToken ?? 0) + 1\n  node._frameResolveToken = token\n  node._frameResolveController?.abort()\n  let resolveController = new AbortController()\n  node._frameResolveController = resolveController\n\n  Promise.resolve(runtime.resolveFrame(frameSrc, resolveController.signal))\n    .then(async (content) => {\n      if (node._frameResolveToken !== token || resolveController.signal.aborted) return\n      node._frameFallbackRoot?.dispose()\n      node._frameFallbackRoot = undefined\n      let nextContent = asAbortableFrameContent(content, resolveController.signal)\n      await instance.render(nextContent, { signal: resolveController.signal })\n      if (node._frameResolveToken !== token || resolveController.signal.aborted) return\n      node._frameResolved = true\n    })\n    .catch(() => {})\n    .finally(() => {\n      if (node._frameResolveController === resolveController) {\n        node._frameResolveController = undefined\n      }\n    })\n}\n\nfunction disposeFrameResources(node: VNode): void {\n  node._frameResolveToken = (node._frameResolveToken ?? 0) + 1\n  node._frameResolveController?.abort()\n  node._frameResolveController = undefined\n  node._frameFallbackRoot?.dispose()\n  node._frameFallbackRoot = undefined\n\n  let frameInstance = node._frameInstance as FrameInstance | undefined\n  if (frameInstance) {\n    frameInstance.dispose()\n    node._frameInstance = undefined\n  }\n}\n\nfunction asAbortableFrameContent(content: FrameContent, signal: AbortSignal): FrameContent {\n  if (!(content instanceof ReadableStream)) return content\n  return createAbortableReadableStream(content, signal)\n}\n\nfunction createAbortableReadableStream(\n  source: ReadableStream<Uint8Array>,\n  signal: AbortSignal,\n): ReadableStream<Uint8Array> {\n  let reader = source.getReader()\n  let aborted = false\n\n  let onAbort = () => {\n    aborted = true\n    void reader.cancel(signal.reason)\n  }\n\n  if (signal.aborted) onAbort()\n  else signal.addEventListener('abort', onAbort, { once: true })\n\n  return new ReadableStream<Uint8Array>({\n    async pull(controller) {\n      if (aborted) {\n        controller.close()\n        return\n      }\n\n      let removeAbortReadListener: undefined | (() => void)\n      let abortRead = new Promise<ReadableStreamReadResult<Uint8Array>>((resolve) => {\n        if (signal.aborted) {\n          resolve({ done: true, value: undefined as unknown as Uint8Array })\n          return\n        }\n        let onAbortRead = () => {\n          resolve({ done: true, value: undefined as unknown as Uint8Array })\n        }\n        removeAbortReadListener = () => signal.removeEventListener('abort', onAbortRead)\n        signal.addEventListener('abort', onAbortRead, { once: true })\n      })\n\n      let { done, value } = await Promise.race([reader.read(), abortRead])\n      removeAbortReadListener?.()\n\n      if (done) {\n        controller.close()\n        return\n      }\n      controller.enqueue(value)\n    },\n    cancel(reason) {\n      signal.removeEventListener('abort', onAbort)\n      return reader.cancel(reason)\n    },\n  })\n}\n\nfunction removeFrameDomRange(node: VNode, domParent: ParentNode): void {\n  let start = node._rangeStart\n  let end = node._rangeEnd\n  if (!(start instanceof Comment) || !(end instanceof Comment)) return\n\n  let cursor: Node | null = start\n  while (cursor) {\n    let nextSibling: Node | null = cursor.nextSibling\n    if (cursor.parentNode === domParent) {\n      domParent.removeChild(cursor)\n    }\n    if (cursor === end) break\n    cursor = nextSibling\n  }\n\n  node._rangeStart = undefined\n  node._rangeEnd = undefined\n}\n\nfunction getFrameRuntime(frame: FrameHandle): FrameRuntime | undefined {\n  return frame.$runtime as FrameRuntime | undefined\n}\n\nfunction getFrameSrc(node: VNode): string {\n  let src = node.props?.src\n  invariant(typeof src === 'string' && src.length > 0, '<Frame /> requires a src prop')\n  return src\n}\n\nfunction getFrameName(node: VNode): string | undefined {\n  let name = node.props?.name\n  return typeof name === 'string' && name.length > 0 ? name : undefined\n}\n\nfunction randomFrameId(): string {\n  return `f${crypto.randomUUID().slice(0, 8)}`\n}\n\nfunction skipCommentsExceptFrameStart(cursor: Node | null): Node | null {\n  while (cursor && cursor.nodeType === Node.COMMENT_NODE) {\n    if (isFrameStartComment(cursor)) return cursor\n    cursor = cursor.nextSibling\n  }\n  return cursor\n}\n\nfunction isFrameStartComment(node: Node | null | undefined): node is Comment {\n  return node instanceof Comment && node.data.trim().startsWith('rmx:f:')\n}\n\nfunction isFrameEndComment(node: Node | null | undefined): node is Comment {\n  return node instanceof Comment && node.data.trim() === '/rmx:f'\n}\n\nfunction getFrameIdFromComment(comment: Comment): string | undefined {\n  let text = comment.data.trim()\n  if (!text.startsWith('rmx:f:')) return undefined\n  return text.slice('rmx:f:'.length)\n}\n\nfunction findFrameEndComment(start: Comment): Comment | null {\n  let depth = 1\n  let node: Node | null = start.nextSibling\n\n  while (node) {\n    if (isFrameStartComment(node)) depth++\n    else if (isFrameEndComment(node)) {\n      depth--\n      if (depth === 0) return node\n    }\n    node = node.nextSibling\n  }\n\n  return null\n}\n\nexport function renderComponent(\n  handle: ComponentHandle,\n  currContent: VNode | null,\n  next: ComponentNode,\n  domParent: ParentNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  rootTarget: EventTarget,\n  vParent?: VNode,\n  anchor?: Node,\n  cursor?: Node | null,\n): Node | null | undefined {\n  let [element, tasks] = handle.render(next.props)\n  let content = toVNode(element)\n  let newCursor = diffVNodes(\n    currContent,\n    content,\n    domParent,\n    frame,\n    scheduler,\n    styles,\n    next,\n    rootTarget,\n    anchor,\n    cursor,\n  )\n  next._content = content\n  next._handle = handle\n  next._parent = vParent\n\n  let committed = next as CommittedComponentNode\n  handle.setScheduleUpdate(() => {\n    scheduler.enqueue(committed, domParent)\n  })\n\n  scheduler.enqueueTasks(tasks)\n\n  return newCursor\n}\n\nfunction diffComponent(\n  curr: CommittedComponentNode | null,\n  next: ComponentNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  domParent: ParentNode,\n  vParent: VNode,\n  rootTarget: EventTarget,\n  anchor?: Node,\n  cursor?: Node | null,\n): Node | null | undefined {\n  if (curr === null) {\n    let componentId = vParent._pendingHydrationComponentId\n    if (componentId) {\n      vParent._pendingHydrationComponentId = undefined\n    } else {\n      componentId = `c${++idCounter}`\n    }\n    next._handle = createComponent({\n      id: componentId,\n      frame,\n      type: next.type,\n      getContext: (type: Component) => findContextFromAncestry(vParent, type),\n      getFrameByName(name: string) {\n        let runtime = getFrameRuntime(frame)\n        return runtime?.namedFrames.get(name)\n      },\n      getTopFrame() {\n        let runtime = getFrameRuntime(frame)\n        return runtime?.topFrame\n      },\n    })\n\n    return renderComponent(\n      next._handle,\n      null,\n      next,\n      domParent,\n      frame,\n      scheduler,\n      styles,\n      rootTarget,\n      vParent,\n      anchor,\n      cursor,\n    )\n  }\n  next._handle = curr._handle\n  let { _content, _handle } = curr\n  return renderComponent(\n    _handle,\n    _content,\n    next,\n    domParent,\n    frame,\n    scheduler,\n    styles,\n    rootTarget,\n    vParent,\n    anchor,\n    cursor,\n  )\n}\n\n// Cleanup without DOM removal - used for descendants when parent DOM node is removed\nfunction cleanupDescendants(node: VNode, scheduler: Scheduler, styles: StyleManager): void {\n  if (isCommittedTextNode(node)) {\n    return\n  }\n\n  if (isCommittedHostNode(node)) {\n    for (let child of node._children) {\n      cleanupDescendants(child, scheduler, styles)\n    }\n\n    teardownMixins(node._mixState as MixinRuntimeState | undefined)\n    teardownControlledReflection(node)\n    if (node._controller) node._controller.abort()\n    return\n  }\n\n  if (isFragmentNode(node)) {\n    for (let child of node._children) {\n      cleanupDescendants(child, scheduler, styles)\n    }\n    return\n  }\n\n  if (isCommittedComponentNode(node)) {\n    cleanupDescendants(node._content, scheduler, styles)\n    let tasks = node._handle.remove()\n    scheduler.enqueueTasks(tasks)\n    return\n  }\n\n  if (node.type === Frame) {\n    disposeFrameResources(node)\n    return\n  }\n}\n\nexport function remove(\n  node: VNode,\n  domParent: ParentNode,\n  scheduler: Scheduler,\n  styles: StyleManager,\n) {\n  if (isCommittedTextNode(node)) {\n    node._dom.parentNode?.removeChild(node._dom)\n    return\n  }\n\n  if (isCommittedHostNode(node)) {\n    if (node._persistedByMixins) return\n\n    let persistedRemoval = prepareMixinRemoval(node._mixState as MixinRuntimeState | undefined)\n    if (persistedRemoval) {\n      let token = ++persistedRemovalToken\n      markNodePersistedByMixins(node, domParent, token)\n      void persistedRemoval\n        .catch(() => {})\n        .finally(() => {\n          if (!node._persistedByMixins) return\n          if (node._persistedRemovalToken !== token) return\n          unmarkNodePersistedByMixins(node)\n          performHostNodeRemoval(node, domParent, scheduler, styles)\n        })\n      return\n    }\n    performHostNodeRemoval(node, domParent, scheduler, styles)\n    return\n  }\n\n  if (isFragmentNode(node)) {\n    for (let child of node._children) {\n      remove(child, domParent, scheduler, styles)\n    }\n    return\n  }\n\n  if (isCommittedComponentNode(node)) {\n    remove(node._content, domParent, scheduler, styles)\n    let tasks = node._handle.remove()\n    scheduler.enqueueTasks(tasks)\n    return\n  }\n\n  if (node.type === Frame) {\n    disposeFrameResources(node)\n    removeFrameDomRange(node, domParent)\n    return\n  }\n}\n\n// Actually remove a host node from DOM and clean up\nfunction performHostNodeRemoval(\n  node: CommittedHostNode,\n  domParent: ParentNode,\n  scheduler: Scheduler,\n  styles: StyleManager,\n) {\n  if (isHeadHostNode(node)) {\n    for (let child of node._children) {\n      remove(child, node._dom, scheduler, styles)\n    }\n  } else {\n    // Clean up all descendants first (before removing DOM subtree)\n    for (let child of node._children) {\n      cleanupDescendants(child, scheduler, styles)\n    }\n  }\n\n  teardownMixins(node._mixState as MixinRuntimeState | undefined)\n  teardownControlledReflection(node)\n  // Never remove the real document.head node when reconciling a <head> vnode.\n  if (!isHeadHostNode(node)) {\n    node._dom.parentNode?.removeChild(node._dom)\n  }\n  if (node._controller) node._controller.abort()\n}\n\nfunction diffChildren(\n  curr: VNode[] | null,\n  next: VNode[],\n  domParent: ParentNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  vParent: VNode,\n  rootTarget: EventTarget,\n  cursor?: Node | null,\n  anchor?: Node,\n) {\n  let nextLength = next.length\n\n  // Warn when duplicate keys are present among siblings. Duplicate keys are\n  // still processed (last one wins), but they make keyed diffing ambiguous.\n  let hasKeys = false\n  let seenKeys: Set<string> | undefined\n  let duplicateKeys: Set<string> | undefined\n  for (let i = 0; i < nextLength; i++) {\n    let node = next[i]\n    if (node && node.key != null) {\n      hasKeys = true\n      if (!seenKeys) {\n        seenKeys = new Set([node.key])\n        continue\n      }\n      if (seenKeys.has(node.key)) {\n        if (!duplicateKeys) {\n          duplicateKeys = new Set()\n        }\n        duplicateKeys.add(node.key)\n      } else {\n        seenKeys.add(node.key)\n      }\n    }\n  }\n\n  if (duplicateKeys?.size) {\n    let quotedKeys = Array.from(duplicateKeys, (key) => `\"${key}\"`)\n    console.warn(\n      `Duplicate keys detected in siblings: ${quotedKeys.join(', ')}. Keys should be unique.`,\n    )\n  }\n\n  // Initial mount / hydration: delegate to insert() for each child so that\n  // hydration cursors and creation logic remain centralized there.\n  if (curr === null) {\n    for (let node of next) {\n      cursor = insert(\n        node,\n        domParent,\n        frame,\n        scheduler,\n        styles,\n        vParent,\n        rootTarget,\n        anchor,\n        cursor,\n      )\n    }\n    vParent._children = next\n    return cursor\n  }\n\n  let currLength = curr.length\n\n  // Detect if any keys are present in the new children. If not, we can fall\n  // back to the simpler index-based diff which is cheaper and matches\n  // pre-existing behavior.\n  if (!hasKeys) {\n    for (let i = 0; i < nextLength; i++) {\n      let currentNode = i < currLength ? curr[i] : null\n      diffVNodes(\n        currentNode,\n        next[i],\n        domParent,\n        frame,\n        scheduler,\n        styles,\n        vParent,\n        rootTarget,\n        anchor,\n        cursor,\n      )\n    }\n\n    if (currLength > nextLength) {\n      for (let i = nextLength; i < currLength; i++) {\n        let node = curr[i]\n        if (node) remove(node, domParent, scheduler, styles)\n      }\n    }\n\n    vParent._children = next\n    return\n  }\n\n  // --- O(n + m) keyed diff with Map-based lookup ------------------------------\n\n  let oldChildren = curr\n  let oldChildrenLength = currLength\n  let remainingOldChildren = oldChildrenLength\n\n  // Build key → index map for O(1) lookup: O(m)\n  let oldKeyMap = new Map<string, number>()\n  for (let i = 0; i < oldChildrenLength; i++) {\n    let c = oldChildren[i]\n    if (c) {\n      c._flags = 0\n      if (c.key != null) {\n        oldKeyMap.set(c.key, i)\n      }\n    }\n  }\n\n  let skew = 0\n  let newChildren: VNode[] = new Array(nextLength)\n\n  // First pass: match new children to old ones using Map lookup: O(n)\n  for (let i = 0; i < nextLength; i++) {\n    let childVNode = next[i]\n    if (!childVNode) {\n      newChildren[i] = childVNode\n      continue\n    }\n\n    newChildren[i] = childVNode\n    childVNode._parent = vParent\n\n    let skewedIndex = i + skew\n    let matchingIndex = -1\n\n    let key = childVNode.key\n    let type = childVNode.type\n\n    if (key != null) {\n      // O(1) Map lookup for keyed children\n      let mapIndex = oldKeyMap.get(key)\n      if (mapIndex !== undefined) {\n        let candidate = oldChildren[mapIndex]\n        let candidateFlags = candidate?._flags ?? 0\n        if (candidate && (candidateFlags & MATCHED) === 0 && candidate.type === type) {\n          matchingIndex = mapIndex\n        }\n      }\n    } else {\n      // Non-keyed children use positional identity only - no searching\n      let searchVNode = oldChildren[skewedIndex]\n      let searchFlags = searchVNode?._flags ?? 0\n      let available = searchVNode != null && (searchFlags & MATCHED) === 0\n      if (available && searchVNode.key == null && type === searchVNode.type) {\n        matchingIndex = skewedIndex\n      }\n    }\n\n    childVNode._index = matchingIndex\n\n    let matchedOldVNode: VNode | null = null\n    if (matchingIndex !== -1) {\n      matchedOldVNode = oldChildren[matchingIndex]\n      remainingOldChildren--\n      if (matchedOldVNode) {\n        matchedOldVNode._flags = (matchedOldVNode._flags ?? 0) | MATCHED\n      }\n    }\n\n    // Determine whether this is a mount vs move and mark INSERT_VNODE\n    let oldDom = matchedOldVNode && findFirstDomAnchor(matchedOldVNode)\n    let isMounting = !matchedOldVNode || !oldDom\n    if (isMounting) {\n      if (matchingIndex === -1) {\n        // Adjust skew similar to Preact when lengths differ\n        if (nextLength > oldChildrenLength) {\n          skew--\n        } else if (nextLength < oldChildrenLength) {\n          skew++\n        }\n      }\n\n      childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE\n    } else if (matchingIndex !== i + skew) {\n      if (matchingIndex === i + skew - 1) {\n        skew--\n      } else if (matchingIndex === i + skew + 1) {\n        skew++\n      } else {\n        if (matchingIndex! > i + skew) skew--\n        else skew++\n        childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE\n      }\n    }\n  }\n\n  // Unmount any old children that weren't matched\n  if (remainingOldChildren) {\n    for (let i = 0; i < oldChildrenLength; i++) {\n      let oldVNode = oldChildren[i]\n      if (oldVNode && ((oldVNode._flags ?? 0) & MATCHED) === 0) {\n        remove(oldVNode, domParent, scheduler, styles)\n      }\n    }\n  }\n\n  // Second pass: diff matched pairs and place/move DOM nodes in the correct\n  // order, similar to Preact's diffChildren + insert.\n  vParent._children = newChildren\n\n  let lastPlaced: Node | null = null\n\n  for (let i = 0; i < nextLength; i++) {\n    let childVNode = newChildren[i]\n    if (!childVNode) continue\n\n    let idx = childVNode._index ?? -1\n    let oldVNode = idx >= 0 ? oldChildren[idx] : null\n\n    diffVNodes(\n      oldVNode,\n      childVNode,\n      domParent,\n      frame,\n      scheduler,\n      styles,\n      vParent,\n      rootTarget,\n      anchor,\n      cursor,\n    )\n\n    let shouldPlace = (childVNode._flags ?? 0) & INSERT_VNODE\n    let firstDom = findFirstDomAnchor(childVNode)\n    let lastDom = firstDom ? findLastDomAnchor(childVNode) : null\n    if (shouldPlace && firstDom && lastDom && firstDom.parentNode === domParent) {\n      let target: Node | null\n      if (lastPlaced === null) {\n        if (vParent._rangeStart && vParent._rangeStart.parentNode === domParent) {\n          target = vParent._rangeStart.nextSibling\n        } else {\n          target = domParent.firstChild\n        }\n      } else {\n        target = lastPlaced.nextSibling\n      }\n\n      if (target === null && anchor) target = anchor\n\n      // If target lies within the range we're moving, skip the move.\n      if (target && domRangeContainsNode(firstDom, lastDom, target)) {\n        // no-op\n      } else if (firstDom !== target) {\n        moveDomRange(domParent, firstDom, lastDom, target)\n      }\n    }\n\n    if (lastDom) lastPlaced = lastDom\n\n    // Clear internal flags for next diff\n    childVNode._flags = 0\n    childVNode._index = undefined\n  }\n\n  return\n}\n\nexport function findFirstDomAnchor(node: VNode | null | undefined): Node | null {\n  if (!node) return null\n  if (isCommittedTextNode(node)) return node._dom\n  if (isCommittedHostNode(node)) return node._dom\n  if (isCommittedComponentNode(node)) return findFirstDomAnchor(node._content)\n  if (node.type === Frame) return node._rangeStart ?? null\n  if (isFragmentNode(node)) {\n    for (let child of node._children) {\n      let dom = findFirstDomAnchor(child)\n      if (dom) return dom\n    }\n  }\n  return null\n}\n\nexport function findLastDomAnchor(node: VNode | null | undefined): Node | null {\n  if (!node) return null\n  if (isCommittedTextNode(node)) return node._dom\n  if (isCommittedHostNode(node)) return node._dom\n  if (isCommittedComponentNode(node)) return findLastDomAnchor(node._content)\n  if (node.type === Frame) return node._rangeEnd ?? null\n  if (isFragmentNode(node)) {\n    for (let i = node._children.length - 1; i >= 0; i--) {\n      let dom = findLastDomAnchor(node._children[i])\n      if (dom) return dom\n    }\n  }\n  return null\n}\n\nfunction domRangeContainsNode(first: Node, last: Node, node: Node): boolean {\n  let current: Node | null = first\n  while (current) {\n    if (current === node) return true\n    if (current === last) break\n    current = current.nextSibling\n  }\n  return false\n}\n\nfunction moveDomRange(domParent: ParentNode, first: Node, last: Node, before: Node | null): void {\n  let current: Node | null = first\n  while (current) {\n    let next: Node | null = current === last ? null : current.nextSibling\n    domParent.insertBefore(current, before)\n    if (current === last) break\n    current = next\n  }\n}\n\nexport function setActiveSchedulerUpdateParents(parents: ParentNode[] | undefined) {\n  activeSchedulerUpdateParents = parents\n}\n\nfunction shouldDispatchInlineMixinLifecycle(node: Node): boolean {\n  let parents = activeSchedulerUpdateParents\n  if (!parents?.length) return true\n  for (let parent of parents) {\n    let parentNode = parent as Node\n    if (parentNode === node) return false\n    if (parentNode.contains(node)) return false\n  }\n  return true\n}\n\nexport function findNextSiblingDomAnchor(curr: VNode, vParent?: VNode): Node | null {\n  if (!vParent || !Array.isArray(vParent._children)) return null\n  let children = vParent._children\n  let idx = children.indexOf(curr)\n  if (idx === -1) return null\n  for (let i = idx + 1; i < children.length; i++) {\n    let dom = findFirstDomAnchor(children[i])\n    if (dom) return dom\n  }\n  return null\n}\n\nfunction reclaimPersistedMixinNode(\n  persistedNode: CommittedHostNode,\n  newNode: HostNode,\n  frame: FrameHandle,\n  scheduler: Scheduler,\n  styles: StyleManager,\n  vParent: VNode,\n  rootTarget: EventTarget,\n): void {\n  cancelPendingMixinRemoval(persistedNode._mixState as MixinRuntimeState | undefined)\n  unmarkNodePersistedByMixins(persistedNode)\n\n  newNode._dom = persistedNode._dom\n  newNode._parent = vParent\n  newNode._controller = persistedNode._controller\n  newNode._mixState = persistedNode._mixState\n  newNode._controlledState = persistedNode._controlledState\n\n  let prevProps = getHostProps(persistedNode)\n  let nextProps = resolveNodeMixProps(\n    newNode,\n    frame,\n    scheduler,\n    newNode._mixState as MixinRuntimeState | undefined,\n  )\n  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {\n    dispatchMixinBeforeUpdate(newNode._mixState as MixinRuntimeState | undefined)\n  }\n  diffHostProps(prevProps, nextProps, persistedNode._dom)\n  ensureControlledReflection(newNode as CommittedHostNode, scheduler)\n  syncControlledReflection(newNode as CommittedHostNode, nextProps)\n\n  diffChildren(\n    persistedNode._children,\n    newNode._children,\n    persistedNode._dom,\n    frame,\n    scheduler,\n    styles,\n    newNode,\n    rootTarget,\n  )\n\n  bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)\n  if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {\n    scheduler.enqueueCommitPhase([\n      () => dispatchMixinCommit(newNode._mixState as MixinRuntimeState | undefined),\n    ])\n  }\n}\n"
  },
  {
    "path": "packages/component/src/lib/run.ts",
    "content": "import { createFrame, type Frame } from './frame.ts'\nimport { createScheduler } from './vdom.ts'\nimport { defaultStyleManager } from './diff-props.ts'\nimport type { FrameHandle } from './component.ts'\nimport { createComponentErrorEvent } from './error-event.ts'\nimport type { ComponentErrorEvent } from './error-event.ts'\nimport type { LoadModule, ResolveFrame } from './frame.ts'\nimport { startNavigationListener } from './navigation.ts'\nimport { TypedEventTarget } from './typed-event-target.ts'\n\n/**\n * Options for starting the client runtime with {@link run}.\n */\nexport type RunInit = {\n  loadModule: LoadModule\n  resolveFrame?: ResolveFrame\n}\n\n/**\n * Events emitted by the application runtime.\n */\nexport type AppRuntimeEventMap = {\n  error: ComponentErrorEvent\n}\n\n/**\n * Client runtime returned by {@link run}.\n */\nexport type AppRuntime = TypedEventTarget<AppRuntimeEventMap> & {\n  ready(): Promise<void>\n  flush(): void\n  dispose(): void\n}\n\nlet topFrame: Frame\n/**\n * Returns the top-level frame handle for the running application.\n *\n * @returns The top-level frame handle.\n */\nexport function getTopFrame(): FrameHandle {\n  if (!topFrame) throw new Error('app runtime not initialized')\n  return topFrame.handle\n}\n\nlet namedFrames = new Map<string, FrameHandle>()\n/**\n * Returns a named frame handle, falling back to the top frame when not found.\n *\n * @param name Name of the frame to look up.\n * @returns The matching frame handle or the top frame.\n */\nexport function getNamedFrame(name: string): FrameHandle {\n  return namedFrames.get(name) ?? getTopFrame()\n}\n\n/**\n * Starts the client-side Remix component runtime for the current document.\n *\n * @param init Runtime hooks for loading modules and resolving frames.\n * @returns The running application runtime.\n */\nexport function run(init: RunInit): AppRuntime {\n  let styleManager = defaultStyleManager\n  let errorTarget = new TypedEventTarget<AppRuntimeEventMap>()\n  let scheduler = createScheduler(document, errorTarget, styleManager)\n\n  let resolveFrame: ResolveFrame = init.resolveFrame ?? (() => '<p>resolve frame unimplemented</p>')\n\n  topFrame = createFrame(document, {\n    src: document.location.href,\n    errorTarget,\n    loadModule: init.loadModule,\n    resolveFrame,\n    pendingClientEntries: new Map(),\n    scheduler,\n    styleManager,\n    data: {},\n    moduleCache: new Map(),\n    moduleLoads: new Map(),\n    frameInstances: new WeakMap(),\n    namedFrames,\n  })\n\n  let appController = new AbortController()\n  startNavigationListener(appController.signal)\n  let readyPromise = topFrame.ready().catch((error) => {\n    errorTarget.dispatchEvent(createComponentErrorEvent(error))\n    throw error\n  })\n\n  return Object.assign(errorTarget, {\n    ready: () => readyPromise,\n    flush: () => topFrame.flush(),\n    dispose: () => {\n      appController.abort()\n      topFrame.dispose()\n    },\n  })\n}\n"
  },
  {
    "path": "packages/component/src/lib/scheduler.ts",
    "content": "import { createDocumentState } from './document-state.ts'\nimport { createComponentErrorEvent } from './error-event.ts'\nimport type { CommittedComponentNode, VNode } from './vnode.ts'\nimport { isCommittedComponentNode } from './vnode.ts'\nimport {\n  findNextSiblingDomAnchor,\n  renderComponent,\n  setActiveSchedulerUpdateParents,\n} from './reconcile.ts'\nimport { defaultStyleManager } from './diff-props.ts'\nimport type { StyleManager } from './style/index.ts'\n\ntype EmptyFn = () => void\ntype SchedulerPhaseType = 'beforeUpdate' | 'commit'\ntype SchedulerPhaseListener = EventListenerOrEventListenerObject | null\n\n/**\n * Scheduler API used by the reconciler and frame runtime.\n */\nexport type Scheduler = ReturnType<typeof createScheduler>\n\n// Protect against infinite cascading updates (e.g. handle.update() during render)\nconst MAX_CASCADING_UPDATES = 50\n\nexport type SchedulerPhaseEvent = Event & {\n  parents: ParentNode[]\n}\n\n/**\n * Creates the DOM update scheduler used by the component runtime.\n *\n * @param doc Document associated with the rendered tree.\n * @param rootTarget Event target that receives runtime errors.\n * @param styles Style manager used during reconciliation.\n * @returns A scheduler instance.\n */\nexport function createScheduler(\n  doc: Document,\n  rootTarget: EventTarget,\n  styles: StyleManager = defaultStyleManager,\n) {\n  let documentState = createDocumentState(doc)\n  let scheduled = new Map<CommittedComponentNode, ParentNode>()\n  let workTasks: EmptyFn[] = []\n  let commitPhaseTasks: EmptyFn[] = []\n  let postCommitTasks: EmptyFn[] = []\n  let flushScheduled = false\n  let flushing = false\n  let cascadingUpdateCount = 0\n  let resetScheduled = false\n  let phaseEvents = new EventTarget()\n  let scheduler: {\n    enqueue(vnode: CommittedComponentNode, domParent: ParentNode): void\n    enqueueWork(newTasks: EmptyFn[]): void\n    enqueueCommitPhase(newTasks: EmptyFn[]): void\n    enqueueTasks(newTasks: EmptyFn[]): void\n    addEventListener(\n      type: SchedulerPhaseType,\n      listener: SchedulerPhaseListener,\n      options?: AddEventListenerOptions | boolean,\n    ): void\n    removeEventListener(\n      type: SchedulerPhaseType,\n      listener: SchedulerPhaseListener,\n      options?: EventListenerOptions | boolean,\n    ): void\n    dequeue(): void\n  }\n\n  function dispatchError(error: unknown) {\n    console.error(error)\n    rootTarget.dispatchEvent(createComponentErrorEvent(error))\n  }\n\n  function scheduleCounterReset() {\n    if (resetScheduled) return\n    resetScheduled = true\n    // Reset when control returns to the event loop while still allowing\n    // microtask-driven flushes in the same turn to count as cascading.\n    setTimeout(() => {\n      cascadingUpdateCount = 0\n      resetScheduled = false\n    }, 0)\n  }\n\n  function flush() {\n    if (flushing) return\n    flushing = true\n    try {\n      while (true) {\n        flushScheduled = false\n\n        let batch = new Map(scheduled)\n        scheduled.clear()\n\n        let hasWork =\n          batch.size > 0 ||\n          workTasks.length > 0 ||\n          commitPhaseTasks.length > 0 ||\n          postCommitTasks.length > 0\n        if (!hasWork) return\n\n        cascadingUpdateCount++\n        scheduleCounterReset()\n\n        if (cascadingUpdateCount > MAX_CASCADING_UPDATES) {\n          let error = new Error('handle.update() infinite loop detected')\n          dispatchError(error)\n          return\n        }\n\n        documentState.capture()\n\n        let updateParents = batch.size > 0 ? Array.from(new Set(batch.values())) : []\n        setActiveSchedulerUpdateParents(updateParents)\n        dispatchPhaseEvent('beforeUpdate', updateParents)\n\n        if (batch.size > 0) {\n          let vnodes = Array.from(batch)\n          let noScheduledAncestor = new Set<VNode>()\n\n          for (let [vnode, domParent] of vnodes) {\n            if (ancestorIsScheduled(vnode, batch, noScheduledAncestor)) continue\n            let handle = vnode._handle\n            let curr = vnode._content\n            let vParent = vnode._parent!\n            // Calculate anchor at render time from current vdom position (never stale).\n            // Needed for fragment self-updates that add children - without this, new children\n            // would be appended after siblings. The keyed diff has placement logic, but unkeyed\n            // diff relies on anchor for correct positioning.\n            let anchor = findNextSiblingDomAnchor(vnode, vParent) || undefined\n            try {\n              renderComponent(\n                handle,\n                curr,\n                vnode,\n                domParent,\n                handle.frame,\n                scheduler,\n                styles,\n                rootTarget,\n                vParent,\n                anchor,\n              )\n            } catch (error) {\n              dispatchError(error)\n            }\n          }\n        }\n\n        flushTaskQueue(workTasks)\n        setActiveSchedulerUpdateParents(undefined)\n\n        // Restore selection before commit-phase lifecycle work so mixins see\n        // the final DOM state but still run before commit listeners and user tasks.\n        documentState.restore()\n\n        flushTaskQueue(commitPhaseTasks)\n        dispatchPhaseEvent('commit', updateParents)\n        flushTaskQueue(postCommitTasks)\n      }\n    } finally {\n      setActiveSchedulerUpdateParents(undefined)\n      flushing = false\n    }\n  }\n\n  function dispatchPhaseEvent(type: SchedulerPhaseType, parents: ParentNode[]) {\n    let event = new Event(type) as SchedulerPhaseEvent\n    event.parents = parents\n    phaseEvents.dispatchEvent(event)\n  }\n\n  function flushTaskQueue(queue: EmptyFn[]) {\n    while (queue.length > 0) {\n      let task = queue.shift()\n      if (!task) continue\n      try {\n        task()\n      } catch (error) {\n        dispatchError(error)\n      }\n    }\n  }\n\n  function scheduleFlush() {\n    if (flushScheduled || flushing) return\n    flushScheduled = true\n    queueMicrotask(flush)\n  }\n\n  function ancestorIsScheduled(\n    vnode: VNode,\n    batch: Map<CommittedComponentNode, ParentNode>,\n    safe: Set<VNode>,\n  ): boolean {\n    let path: VNode[] = []\n    let current = vnode._parent\n\n    while (current) {\n      // Already verified this node has no scheduled ancestor above it\n      if (safe.has(current)) {\n        for (let node of path) safe.add(node)\n        return false\n      }\n\n      path.push(current)\n\n      if (isCommittedComponentNode(current) && batch.has(current)) {\n        return true\n      }\n\n      current = current._parent\n    }\n\n    // Reached root - mark entire path as safe for future lookups\n    for (let node of path) safe.add(node)\n    return false\n  }\n\n  scheduler = {\n    enqueue(vnode: CommittedComponentNode, domParent: ParentNode): void {\n      scheduled.set(vnode, domParent)\n      scheduleFlush()\n    },\n\n    enqueueWork(newTasks: EmptyFn[]): void {\n      workTasks.push(...newTasks)\n      scheduleFlush()\n    },\n\n    enqueueCommitPhase(newTasks: EmptyFn[]): void {\n      commitPhaseTasks.push(...newTasks)\n      scheduleFlush()\n    },\n\n    enqueueTasks(newTasks: EmptyFn[]): void {\n      postCommitTasks.push(...newTasks)\n      scheduleFlush()\n    },\n\n    addEventListener(type, listener, options) {\n      phaseEvents.addEventListener(type, listener, options)\n    },\n\n    removeEventListener(type, listener, options) {\n      phaseEvents.removeEventListener(type, listener, options)\n    },\n\n    dequeue() {\n      flush()\n    },\n  }\n\n  return scheduler\n}\n"
  },
  {
    "path": "packages/component/src/lib/spring.ts",
    "content": "/**\n * Spring physics based on SwiftUI's spring math.\n *\n * Returns a decorated iterator that can be:\n * - Iterated to get position values (0→1)\n * - Spread for WAAPI: { ...spring() }\n * - Stringified for CSS: `transform ${spring()}`\n *\n * Spring Parameter Conversion (SwiftUI formulas, mass = 1):\n *   stiffness = (2π ÷ duration)²\n *   damping = 1 - 4π × bounce ÷ duration  (bounce ≥ 0)\n *   damping = 4π ÷ (duration + 4π × bounce)  (bounce < 0)\n */\n\nexport type SpringPreset = 'smooth' | 'snappy' | 'bouncy'\n\n/**\n * Options for generating a spring easing iterator.\n */\nexport interface SpringOptions {\n  /** Perceptual duration in milliseconds used to derive spring stiffness. */\n  duration?: number // perceptual duration in ms (default: 300) - affects stiffness\n  /** Spring bounce amount from overdamped (`< 0`) to bouncy (`> 0`). */\n  bounce?: number // -1 to ~0.95: negative = overdamped, 0 = critical, positive = bouncy\n  /** Initial velocity in units per second. */\n  velocity?: number // initial velocity in units per second\n}\n\n/**\n * Iterator returned by {@link spring}, decorated for CSS and WAAPI use.\n */\nexport interface SpringIterator extends IterableIterator<number> {\n  /** Time when spring settles to rest (milliseconds) */\n  duration: number\n  /** CSS linear() easing function */\n  easing: string\n  /** Returns \"duration ms linear(...)\" for CSS transitions */\n  toString(): string\n}\n\nlet presets: Record<SpringPreset, { duration: number; bounce: number }> = {\n  smooth: { duration: 400, bounce: -0.3 },\n  snappy: { duration: 200, bounce: 0 },\n  bouncy: { duration: 400, bounce: 0.3 },\n}\n\n// Rest thresholds for determining when spring has settled\nlet restSpeed = 0.01\nlet restDelta = 0.005\nlet maxSettlingTime = 20_000\nlet frameMs = 1000 / 60 // ~16.67ms per frame\n\n/**\n * Create a spring iterator for animations.\n *\n * @example\n * let s = spring('bouncy')\n *\n * // As CSS transition\n * element.style.transition = `transform ${s}`\n *\n * // Spread for WAAPI\n * element.animate(keyframes, { ...spring() })\n *\n * // Iterate for JS animation\n * for (let position of spring()) {\n *   element.style.transform = `translateX(${position * 100}px)`\n * }\n */\n/**\n * Creates a spring iterator from a named preset.\n *\n * @param preset Preset spring profile to start from.\n * @param overrides Optional preset overrides.\n * @returns A spring iterator.\n */\nexport function spring(\n  preset: SpringPreset,\n  overrides?: Omit<SpringOptions, 'bounce'>,\n): SpringIterator\n/**\n * Creates a spring iterator from explicit spring options.\n *\n * @param options Spring parameters.\n * @returns A spring iterator.\n */\nexport function spring(options?: SpringOptions): SpringIterator\nexport function spring(\n  presetOrOptions?: SpringPreset | SpringOptions,\n  overrides?: Omit<SpringOptions, 'bounce'>,\n): SpringIterator {\n  let options = resolveOptions(presetOrOptions, overrides)\n  let { position, settlingTime, easing } = computeSpring(options)\n\n  let duration = Math.round(settlingTime)\n\n  function* generator(): Generator<number, void, undefined> {\n    let t = 0\n    while (t < settlingTime) {\n      yield position(t)\n      t += frameMs\n    }\n    yield 1\n  }\n\n  let iter = generator()\n\n  // Decorate iterator with spring properties (enumerable for spread)\n  Object.defineProperties(iter, {\n    duration: { value: duration, enumerable: true },\n    easing: { value: easing, enumerable: true },\n    toString: {\n      value() {\n        return `${duration}ms ${easing}`\n      },\n    },\n  })\n\n  return iter as unknown as SpringIterator\n}\n\n// Transition helper for CSS transition property\nspring.transition = function transition(\n  property: string | string[],\n  presetOrOptions?: SpringPreset | SpringOptions,\n  overrides?: Omit<SpringOptions, 'bounce'>,\n): string {\n  let s =\n    typeof presetOrOptions === 'string'\n      ? spring(presetOrOptions, overrides)\n      : spring(presetOrOptions)\n\n  let properties = Array.isArray(property) ? property : [property]\n  return properties.map((p) => `${p} ${s}`).join(', ')\n}\n\n// Access preset defaults\nspring.presets = presets\n\nfunction resolveOptions(\n  presetOrOptions?: SpringPreset | SpringOptions,\n  overrides?: Omit<SpringOptions, 'bounce'>,\n): SpringOptions {\n  if (typeof presetOrOptions === 'string') {\n    let preset = presets[presetOrOptions]\n    return {\n      duration: overrides?.duration ?? preset.duration,\n      bounce: preset.bounce,\n      velocity: overrides?.velocity,\n    }\n  }\n  if (presetOrOptions) {\n    return presetOrOptions\n  }\n  // Default to 'snappy' preset\n  return presets.snappy\n}\n\ninterface ComputedSpring {\n  position: (t: number) => number\n  settlingTime: number\n  easing: string\n}\n\n// Core spring computation\nfunction computeSpring(options: SpringOptions): ComputedSpring {\n  let { duration: durationMs = 300, bounce = 0, velocity = 0 } = options\n\n  // Convert duration to seconds for physics calculations\n  let durationSec = durationMs / 1000\n\n  // Natural frequency: ω₀ = √(stiffness) = 2π / duration\n  let omega0 = (2 * Math.PI) / durationSec\n\n  // Clamp bounce to valid range\n  bounce = Math.max(-1, Math.min(0.95, bounce))\n\n  // Damping ratio (ζ):\n  // bounce >= 0: ζ = 1 - bounce (linear, 0→1 maps to critical→underdamped)\n  // bounce < 0: ζ = 1 / (1 + bounce) (stronger overdamping as bounce→-1)\n  let zeta = bounce >= 0 ? 1 - bounce : 1 / (1 + bounce)\n\n  // Convert to per-millisecond for time calculations\n  let omega0Ms = omega0 / 1000\n  let velocityMs = -velocity / 1000 // negated for spring equation convention\n\n  // Position function based on damping regime\n  let position: (t: number) => number\n\n  if (zeta < 1) {\n    // Underdamped (bouncy) - oscillates around target\n    let omegaD = omega0Ms * Math.sqrt(1 - zeta * zeta)\n    position = (t: number) => {\n      let envelope = Math.exp(-zeta * omega0Ms * t)\n      return (\n        1 -\n        envelope *\n          (((velocityMs + zeta * omega0Ms) / omegaD) * Math.sin(omegaD * t) + Math.cos(omegaD * t))\n      )\n    }\n  } else if (zeta > 1) {\n    // Overdamped (smooth) - no oscillation, two decay rates\n    let sqrtTerm = Math.sqrt(zeta * zeta - 1)\n    let s1 = omega0Ms * (-zeta + sqrtTerm) // slower decay\n    let s2 = omega0Ms * (-zeta - sqrtTerm) // faster decay\n    let A = (s2 + velocityMs) / (s2 - s1)\n    let B = 1 - A\n    position = (t: number) => 1 - A * Math.exp(s1 * t) - B * Math.exp(s2 * t)\n  } else {\n    // Critically damped - fastest approach without oscillation\n    position = (t: number) => 1 - Math.exp(-omega0Ms * t) * (1 + (velocityMs + omega0Ms) * t)\n  }\n\n  // Velocity via numerical differentiation (units per second)\n  let velocitySampleMs = 0.5\n  function velocityAt(t: number): number {\n    if (t < velocitySampleMs) {\n      return ((position(velocitySampleMs) - position(0)) / velocitySampleMs) * 1000\n    }\n    return ((position(t) - position(t - velocitySampleMs)) / velocitySampleMs) * 1000\n  }\n\n  // Find settling time\n  let settlingTime = maxSettlingTime\n  let step = 50\n\n  for (let t = 0; t < maxSettlingTime; t += step) {\n    let pos = position(t)\n    let vel = Math.abs(velocityAt(t))\n    let displacement = Math.abs(1 - pos)\n\n    if (vel <= restSpeed && displacement <= restDelta) {\n      settlingTime = t\n      break\n    }\n  }\n\n  // Generate CSS easing\n  let easing = generateEasing(position, settlingTime)\n\n  return { position, settlingTime, easing }\n}\n\n// Generate CSS linear() easing with adaptive sampling\nfunction generateEasing(position: (t: number) => number, settlingTime: number): string {\n  let points = adaptiveSample(position, settlingTime)\n\n  return `linear(${points\n    .map((p, i) => {\n      let isLast = i === points.length - 1\n      let value = isLast ? 1 : Math.round(p.value * 10000) / 10000\n      if (i === 0 || isLast) {\n        return value === 1 ? '1' : value.toString()\n      }\n      let percent = Math.round((p.t / settlingTime) * 1000) / 10\n      return `${value} ${percent}%`\n    })\n    .join(', ')})`\n}\n\n// Adaptive sampling - more points where curvature is high, fewer where linear\nfunction adaptiveSample(\n  resolve: (t: number) => number,\n  duration: number,\n  tolerance: number = 0.002,\n  minSegment: number = 8,\n): Array<{ t: number; value: number }> {\n  let points: Array<{ t: number; value: number }> = []\n\n  function addPoint(t: number, value: number) {\n    if (points.length === 0 || points[points.length - 1].t < t) {\n      points.push({ t, value })\n    }\n  }\n\n  function subdivide(t0: number, v0: number, t1: number, v1: number, depth: number = 0) {\n    if (depth > 12) {\n      addPoint(t0, v0)\n      return\n    }\n\n    let tMid = (t0 + t1) / 2\n    let vMid = resolve(tMid)\n    let vLinear = (v0 + v1) / 2\n    let error = Math.abs(vMid - vLinear)\n\n    if (error > tolerance && t1 - t0 > minSegment) {\n      subdivide(t0, v0, tMid, vMid, depth + 1)\n      subdivide(tMid, vMid, t1, v1, depth + 1)\n    } else {\n      addPoint(t0, v0)\n    }\n  }\n\n  subdivide(0, resolve(0), duration, resolve(duration))\n  addPoint(duration, resolve(duration))\n\n  return points\n}\n"
  },
  {
    "path": "packages/component/src/lib/stream.ts",
    "content": "import type { ComponentHandle, FrameHandle, Key, RemixNode } from './component.ts'\nimport type { ElementType, ElementProps, RemixElement } from './jsx.ts'\nimport { Fragment, createComponent, createFrameHandle, Frame } from './component.ts'\nimport { isEntry, type EntryComponent } from './client-entries.ts'\nimport { normalizeSvgAttribute } from './svg-attributes.ts'\n\ninterface VNode {\n  type: ElementType\n  props: ElementProps\n  key?: Key\n  _handle?: ComponentHandle\n  _parent?: VNode\n}\n\nexport function createVNode(type: ElementType, props: ElementProps, key?: Key): VNode {\n  return { type, props, key }\n}\n\n/**\n * Options for server-side rendering to a byte stream.\n */\nexport interface RenderToStreamOptions {\n  /** Source URL to associate with the current frame render. */\n  frameSrc?: string | URL\n  /** Source URL for the top-level frame in nested frame renders. */\n  topFrameSrc?: string | URL\n  /** Error hook invoked when rendering work throws. */\n  onError?: (error: unknown) => void\n  /** Callback used to resolve nested frame content during streaming SSR. */\n  resolveFrame?: (\n    src: string,\n    target?: string,\n    context?: ResolveFrameContext,\n  ) => Promise<string | ReadableStream<Uint8Array>> | string | ReadableStream<Uint8Array>\n}\n\n/**\n * Context passed to `resolveFrame` during server rendering.\n */\nexport interface ResolveFrameContext {\n  /** Source URL for the frame currently being resolved. */\n  currentFrameSrc: string\n  /** Source URL for the top-level frame in the current render. */\n  topFrameSrc: string\n}\n\ninterface HydrationData {\n  moduleUrl: string\n  exportName: string\n  props: Record<string, unknown>\n}\n\ninterface FrameData {\n  status: 'pending' | 'resolved'\n  name?: string\n  src: string\n}\n\ninterface RenderContext {\n  insideSvg: boolean\n  onError: (error: unknown) => void\n  parentVNode?: VNode\n  styleCache: Map<string, { selector: string; css: string }>\n  resolveFrame: (\n    src: string,\n    target?: string,\n    context?: ResolveFrameContext,\n  ) => Promise<string | ReadableStream<Uint8Array>> | string | ReadableStream<Uint8Array>\n  pendingFrames: Array<{ frameId: string; promise: Promise<ResolvedFrameHtml> }>\n  hydrationData: Map<string, HydrationData>\n  frameData: Map<string, FrameData>\n  blockingFrameTails: ReadableStream<Uint8Array>[]\n  serverIdScope: string\n  serverIdCounter: number\n}\n\ninterface ResolvedFrameHtml {\n  html: string\n  tail?: ReadableStream<Uint8Array>\n}\n\ninterface SsrFrameState {\n  frame: FrameHandle\n  topFrame: FrameHandle\n}\n\ntype Segment =\n  | { kind: 'static'; html: string }\n  | { kind: 'composite'; parts: Segment[] }\n  | {\n      kind: 'frame'\n      frameId: string\n      content: Segment | null\n      pending?: Promise<void>\n    }\n\nconst SELF_CLOSING_TAGS = new Set([\n  'area',\n  'base',\n  'br',\n  'col',\n  'embed',\n  'hr',\n  'img',\n  'input',\n  'link',\n  'meta',\n  'param',\n  'source',\n  'track',\n  'wbr',\n])\n\nconst NUMERIC_CSS_PROPS = new Set([\n  'z-index',\n  'opacity',\n  'flex-grow',\n  'flex-shrink',\n  'flex-order',\n  'grid-area',\n  'grid-row',\n  'grid-column',\n  'font-weight',\n  'line-height',\n  'order',\n  'orphans',\n  'widows',\n  'zoom',\n  'columns',\n  'column-count',\n])\n\nconst FRAMEWORK_PROPS = new Set(['children', 'innerHTML', 'on', 'key', 'mix'])\nconst SSR_MIXIN_SIGNAL = createSsrThrowingSignal()\n\nfunction createSsrSignalError() {\n  return new Error('handle.signal is not available during SSR.')\n}\n\nfunction createSsrThrowingSignal(): AbortSignal {\n  let error = createSsrSignalError()\n  let throwAccess = () => {\n    throw error\n  }\n  return new Proxy({} as AbortSignal, {\n    get: throwAccess,\n    set: throwAccess,\n    has: throwAccess,\n    ownKeys: throwAccess,\n    getOwnPropertyDescriptor: throwAccess,\n    defineProperty: throwAccess,\n    getPrototypeOf: throwAccess,\n  })\n}\n\n/**\n * Renders a node tree to a streaming HTML response body.\n *\n * @param node Node tree to render.\n * @param options Stream rendering options.\n * @returns A readable byte stream of HTML.\n */\nexport function renderToStream(\n  node: RemixNode,\n  options?: RenderToStreamOptions,\n): ReadableStream<Uint8Array> {\n  let encoder = new TextEncoder()\n  let onError = options?.onError ?? ((error) => console.error(error))\n  let currentFrameSrc = normalizeFrameSrc(options?.frameSrc ?? options?.topFrameSrc)\n  let topFrameSrc = normalizeFrameSrc(options?.topFrameSrc ?? currentFrameSrc)\n  let rootFrameState = createSsrFrameState(currentFrameSrc, topFrameSrc)\n\n  let context: RenderContext = {\n    insideSvg: false,\n    onError,\n    resolveFrame: options?.resolveFrame ?? defaultResolveFrame,\n    styleCache: new Map(),\n    pendingFrames: [],\n    hydrationData: new Map(),\n    frameData: new Map(),\n    blockingFrameTails: [],\n    serverIdScope: crypto.randomUUID().slice(0, 8),\n    serverIdCounter: 0,\n  }\n\n  return new ReadableStream({\n    async start(controller) {\n      try {\n        let root = buildSegment(node, context, rootFrameState)\n        await resolveBlocking(root)\n        let html = serializeSegment(root)\n        let finalHtml = finalizeHtml(html, context)\n        let bytes = encoder.encode(finalHtml)\n        controller.enqueue(bytes)\n\n        // If we have any tails from blocking frame streams, stream them now.\n        // These contain nested non-blocking frame templates (or other follow-up chunks)\n        // that must come after the initial document chunk.\n        let tailPromise =\n          context.blockingFrameTails.length > 0\n            ? streamByteStreams(context.blockingFrameTails, controller, context.onError)\n            : Promise.resolve()\n\n        // If we have pending non-blocking frames, stream them as they resolve\n        let pendingPromise =\n          context.pendingFrames.length > 0\n            ? streamPendingFrames(context, controller, encoder)\n            : Promise.resolve()\n\n        await Promise.all([tailPromise, pendingPromise])\n\n        controller.close()\n      } catch (error) {\n        onError(error)\n        controller.error(error)\n      }\n    },\n  })\n}\n\nfunction defaultResolveFrame(): never {\n  throw new Error('No resolveFrame provided')\n}\n\nfunction normalizeFrameSrc(value?: string | URL): string {\n  return value == null ? '' : String(value)\n}\n\nfunction createSsrFrameState(frameSrc: string, topFrameSrc = frameSrc): SsrFrameState {\n  let topFrame = createFrameHandle({ src: topFrameSrc })\n  let frame = frameSrc === topFrameSrc ? topFrame : createFrameHandle({ src: frameSrc })\n  return { frame, topFrame }\n}\n\nfunction getResolveFrameContext(frameState: SsrFrameState): ResolveFrameContext {\n  return {\n    currentFrameSrc: frameState.frame.src,\n    topFrameSrc: frameState.topFrame.src,\n  }\n}\n\nfunction randomId(prefix: string): string {\n  return prefix + crypto.randomUUID().slice(0, 8)\n}\n\nfunction createServerComponentId(context: RenderContext): string {\n  context.serverIdCounter++\n  return `s${context.serverIdScope}-${context.serverIdCounter}`\n}\n\nasync function splitFirstChunk(\n  stream: ReadableStream<Uint8Array>,\n): Promise<{ first: Uint8Array; tail: ReadableStream<Uint8Array> }> {\n  let reader = stream.getReader()\n\n  let { value, done } = await reader.read()\n  if (done || !value) {\n    reader.releaseLock()\n    return {\n      first: new Uint8Array(),\n      tail: new ReadableStream({\n        start(controller) {\n          controller.close()\n        },\n      }),\n    }\n  }\n\n  let released = false\n  function release() {\n    if (released) return\n    released = true\n    try {\n      reader.releaseLock()\n    } catch {\n      // ignore\n    }\n  }\n\n  let tail = new ReadableStream<Uint8Array>({\n    async pull(controller) {\n      let next = await reader.read()\n      if (next.done) {\n        controller.close()\n        release()\n        return\n      }\n      controller.enqueue(next.value)\n    },\n    cancel(reason) {\n      release()\n      return reader.cancel(reason)\n    },\n  })\n\n  return { first: value, tail }\n}\n\nasync function resolveFrameHtml(\n  input: string | ReadableStream<Uint8Array>,\n): Promise<ResolvedFrameHtml> {\n  if (typeof input === 'string') return { html: input }\n\n  let decoder = new TextDecoder()\n  let { first, tail } = await splitFirstChunk(input)\n  return { html: decoder.decode(first), tail }\n}\n\nfunction isRemixElement(node: unknown): node is RemixElement {\n  return typeof node === 'object' && node !== null && '$rmx' in node\n}\n\nfunction staticSeg(html: string): Segment {\n  return { kind: 'static', html }\n}\n\nfunction compositeSeg(parts: Segment[]): Segment {\n  return { kind: 'composite', parts }\n}\n\nfunction buildSegment(node: RemixNode, context: RenderContext, frameState: SsrFrameState): Segment {\n  if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {\n    return staticSeg(escapeTextContent(String(node)))\n  }\n\n  if (node === null || node === undefined || typeof node === 'boolean') {\n    return staticSeg('')\n  }\n\n  if (Array.isArray(node)) {\n    return compositeSeg(node.map((child) => buildSegment(child, context, frameState)))\n  }\n\n  if (isRemixElement(node)) {\n    let type = node.type\n    let props = node.props\n\n    if (type === Fragment) {\n      let children = props.children\n      return children != null ? buildSegment(children, context, frameState) : staticSeg('')\n    }\n\n    if (typeof type === 'string') {\n      let tag = type\n\n      if (tag === 'html') {\n        return buildElementSegment(tag, props, context, frameState)\n      }\n\n      if (tag === 'head') {\n        return buildHeadElementSegment(tag, props, context, frameState)\n      }\n\n      return buildElementSegment(tag, props, context, frameState)\n    }\n\n    if (typeof type === 'function') {\n      if (type === Frame) {\n        return buildFrameSegment(props, context, frameState)\n      }\n      if (isEntry(type)) {\n        return buildEntrySegment(type, props, context, frameState)\n      }\n      return buildComponentSegment(\n        type,\n        props,\n        context,\n        createServerComponentId(context),\n        frameState,\n      )\n    }\n  }\n\n  return staticSeg('')\n}\n\nfunction buildFrameSegment(props: any, context: RenderContext, frameState: SsrFrameState): Segment {\n  let frameId = randomId('f')\n\n  // Store frame data in context for aggregation\n  context.frameData.set(frameId, {\n    status: props.fallback ? 'pending' : 'resolved',\n    name: props.name,\n    src: props.src,\n  })\n\n  let seg: Segment = {\n    kind: 'frame',\n    frameId,\n    content: null,\n  }\n\n  let resolveFrameContext = getResolveFrameContext(frameState)\n  let nonBlocking = !!props.fallback\n  if (nonBlocking) {\n    seg.content = buildSegment(props.fallback, context, frameState)\n    let framePromise = Promise.resolve(\n      context.resolveFrame(props.src, props.name, resolveFrameContext),\n    ).then(async (resolved) => resolveFrameHtml(resolved))\n    context.pendingFrames.push({ frameId, promise: framePromise })\n  } else {\n    seg.pending = Promise.resolve(\n      context.resolveFrame(props.src, props.name, resolveFrameContext),\n    ).then(async (resolved) => {\n      let { html, tail } = await resolveFrameHtml(resolved)\n      seg.content = staticSeg(html)\n      if (tail) {\n        context.blockingFrameTails.push(tail)\n      }\n    })\n  }\n\n  return seg\n}\n\nfunction buildElementSegment(\n  tag: string,\n  props: any,\n  context: RenderContext,\n  frameState: SsrFrameState,\n): Segment {\n  let mixedProps = resolveSsrMixedProps(tag, props, context, frameState)\n  let processedProps = processStyleProps(mixedProps)\n  // Determine namespace context for the current element and its children\n  let currentIsSvg = context.insideSvg || tag === 'svg'\n  let attrs = renderAttributes(processedProps, currentIsSvg)\n\n  if (SELF_CLOSING_TAGS.has(tag)) {\n    return staticSeg(`<${tag}${attrs} />`)\n  }\n\n  if (props.innerHTML) {\n    return staticSeg(`<${tag}${attrs}>${props.innerHTML}</${tag}>`)\n  }\n\n  let open = staticSeg(`<${tag}${attrs}>`)\n  // Adjust svg context for children: foreignObject switches back to HTML\n  let previousInsideSvg = context.insideSvg\n  context.insideSvg = tag === 'foreignObject' ? false : currentIsSvg\n  let children =\n    props.children != null ? buildSegment(props.children, context, frameState) : staticSeg('')\n  context.insideSvg = previousInsideSvg\n  let close = staticSeg(`</${tag}>`)\n  return compositeSeg([open, children, close])\n}\n\nfunction buildHeadElementSegment(\n  tag: string,\n  props: any,\n  context: RenderContext,\n  frameState: SsrFrameState,\n): Segment {\n  let processedProps = processStyleProps(props)\n  let attrs = renderAttributes(processedProps, false)\n\n  let open = staticSeg(`<${tag}${attrs}>`)\n  let children =\n    props.children != null ? buildSegment(props.children, context, frameState) : staticSeg('')\n  let close = staticSeg(`</${tag}>`)\n\n  return compositeSeg([open, children, close])\n}\n\nfunction renderAttributes(props: any, isSvg: boolean): string {\n  let attrs = ''\n\n  for (let key in props) {\n    if (FRAMEWORK_PROPS.has(key)) continue\n\n    let value = props[key]\n    if (value === undefined || value === null || value === false) continue\n\n    let attrName = transformAttributeName(key, isSvg)\n\n    if (value === true) {\n      attrs += ` ${attrName}`\n    } else {\n      attrs += ` ${attrName}=\"${escapeHtml(String(value))}\"`\n    }\n  }\n\n  return attrs\n}\n\nfunction resolveSsrMixedProps(\n  hostType: string,\n  initialProps: ElementProps,\n  context: RenderContext,\n  frameState: SsrFrameState,\n): ElementProps {\n  let descriptors = resolveSsrMixDescriptors(initialProps)\n  if (descriptors.length === 0) return initialProps\n\n  let composedProps = withoutSsrMix(initialProps)\n  let maxDescriptors = 1024\n\n  for (let index = 0; index < descriptors.length && index < maxDescriptors; index++) {\n    let descriptor = descriptors[index]\n    let runner = resolveSsrMixinRunner(hostType, descriptor, context, frameState)\n    if (!runner) continue\n\n    let result: unknown\n    try {\n      result = runner(...descriptor.args, composedProps)\n    } catch (error) {\n      console.error(error)\n      continue\n    }\n\n    if (!result) continue\n    if (isSsrMixinElement(result)) continue\n\n    if (!isRemixElement(result)) {\n      console.error(new Error('mixins must return a remix element'))\n      continue\n    }\n    let remixResult = result\n\n    let resultType =\n      typeof remixResult.type === 'string'\n        ? remixResult.type\n        : isSsrMixinElement(remixResult.type)\n          ? remixResult.type.__rmxMixinElementType\n          : null\n\n    if (resultType !== hostType) {\n      console.error(new Error('mixins must return an element with the same host type'))\n      continue\n    }\n\n    if (remixResult.type !== resultType) {\n      remixResult = { ...remixResult, type: resultType }\n    }\n\n    let nextProps = remixResult.props as ElementProps\n    let nestedDescriptors = resolveSsrMixDescriptors(nextProps)\n    for (let nested of nestedDescriptors) descriptors.push(nested)\n    composedProps = { ...composedProps, ...withoutSsrMix(nextProps) }\n  }\n\n  let nextMix = initialProps.mix\n  return {\n    ...composedProps,\n    ...(nextMix === undefined ? {} : { mix: nextMix }),\n  }\n}\n\nfunction resolveSsrMixinRunner(\n  hostType: string,\n  descriptor: { type?: unknown; args?: unknown[] },\n  context: RenderContext,\n  frameState: SsrFrameState,\n): ((...args: unknown[]) => unknown) | null {\n  if (typeof descriptor.type !== 'function') return null\n  try {\n    let handle = createSsrMixinHandle(hostType, context, frameState)\n    let runner = descriptor.type(handle, hostType)\n    if (typeof runner !== 'function') return null\n    return runner\n  } catch (error) {\n    console.error(error)\n    return null\n  }\n}\n\nfunction createSsrMixinHandle(hostType: string, context: RenderContext, frameState: SsrFrameState) {\n  let signal = SSR_MIXIN_SIGNAL\n  let element = ((_: { update(): Promise<AbortSignal> }, __: unknown) => (props: ElementProps) => ({\n    $rmx: true as const,\n    type: hostType,\n    key: null,\n    props,\n  })) as ((\n    handle: { update(): Promise<AbortSignal> },\n    setup: unknown,\n  ) => (props: ElementProps) => RemixElement) & {\n    __rmxMixinElementType: string\n  }\n  element.__rmxMixinElementType = hostType\n\n  return {\n    id: 'ssr-mixin',\n    frame: createFrameHandle({\n      src: frameState.frame.src,\n      $runtime: {\n        styleCache: context.styleCache,\n      },\n    }),\n    element,\n    signal,\n    update: () => {\n      throw new Error('handle.update() is not available during SSR.')\n    },\n    queueTask: () => {},\n    on: () => {},\n    addEventListener: () => {},\n    removeEventListener: () => {},\n    dispatchEvent: () => true,\n  }\n}\n\nfunction resolveSsrMixDescriptors(props: ElementProps): Array<{ type: any; args: unknown[] }> {\n  let mix = props.mix\n  if (mix == null) return []\n  if (Array.isArray(mix)) {\n    if (mix.length === 0) return []\n    return [...mix] as Array<{ type: any; args: unknown[] }>\n  }\n  return [mix] as Array<{ type: any; args: unknown[] }>\n}\n\nfunction withoutSsrMix(props: ElementProps): ElementProps {\n  if (!('mix' in props)) return props\n  let output = { ...props }\n  delete output.mix\n  return output\n}\n\nfunction isSsrMixinElement(\n  value: unknown,\n): value is ((...args: unknown[]) => unknown) & { __rmxMixinElementType: string } {\n  if (typeof value !== 'function') return false\n  return '__rmxMixinElementType' in value\n}\n\nfunction buildComponentSegment(\n  type: Function,\n  props: any,\n  context: RenderContext,\n  componentId: string,\n  frameState: SsrFrameState,\n): Segment {\n  let vnode = createVNode(type, props)\n  if (context.parentVNode) {\n    vnode._parent = context.parentVNode\n  }\n\n  let handle = createComponent({\n    id: componentId,\n    type: type,\n    frame: frameState.frame,\n    getContext(providerType) {\n      let current = vnode._parent\n      while (current) {\n        if (current.type === providerType) {\n          let providerHandle = current._handle\n          // TODO: need better vnode types to avoid defensive checks\n          if (providerHandle) {\n            return providerHandle.getContextValue()\n          }\n        }\n        current = current._parent\n      }\n      return undefined\n    },\n    getFrameByName() {\n      return undefined\n    },\n    getTopFrame() {\n      return frameState.topFrame\n    },\n  })\n\n  vnode._handle = handle\n  let [renderedNode] = handle.render(props)\n  let childContext = { ...context, parentVNode: vnode }\n\n  return buildSegment(renderedNode, childContext, frameState)\n}\n\nfunction createHydrationPropsReplacer(context: RenderContext, frameState: SsrFrameState) {\n  function unwrapNode(node: RemixNode): unknown {\n    if (node === null || node === undefined || typeof node === 'boolean') return node\n    if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {\n      return node\n    }\n    if (Array.isArray(node)) {\n      return node.map((child) => unwrapNode(child))\n    }\n    if (isRemixElement(node)) {\n      return unwrapElement(node)\n    }\n    return node\n  }\n\n  function unwrapElement(element: RemixElement): unknown {\n    let type = element.type\n    let props = element.props\n\n    // Preserve Frame semantics through serialized props by emitting\n    // a dedicated descriptor that can be revived on the client.\n    if (type === Frame) {\n      return {\n        $rmxFrame: true,\n        props: transformProps(props),\n        key: element.key,\n      }\n    }\n\n    // If it's a DOM tag, return a serializable shape with transformed props\n    if (typeof type === 'string') {\n      return { $rmx: true, type, props: transformProps(props) }\n    }\n\n    // Component function: render synchronously, then unwrap its result\n    if (typeof type === 'function') {\n      let vnode = createVNode(type, props)\n      if (context.parentVNode) {\n        vnode._parent = context.parentVNode\n      }\n\n      let handle = createComponent({\n        id: 'SERIALIZED',\n        type: type,\n        frame: frameState.frame,\n        getContext(providerType) {\n          let current = vnode._parent\n          while (current) {\n            if (current.type === providerType) {\n              let providerHandle = current._handle\n              if (providerHandle) {\n                return providerHandle.getContextValue()\n              }\n            }\n            current = current._parent\n          }\n          return undefined\n        },\n        getFrameByName() {\n          return undefined\n        },\n        getTopFrame() {\n          return frameState.topFrame\n        },\n      })\n\n      vnode._handle = handle\n      let [renderedNode] = handle.render(props)\n      return unwrapNode(renderedNode)\n    }\n\n    return null\n  }\n\n  function transformProps(input: ElementProps): Record<string, unknown> {\n    let out: Record<string, unknown> = {}\n    for (let key in input) {\n      let value = input[key]\n      if (key === 'children') {\n        out[key] = unwrapNode(value)\n      } else {\n        if (isRemixElement(value)) {\n          out[key] = unwrapNode(value)\n        } else if (Array.isArray(value)) {\n          out[key] = value.map((v) => unwrapNode(v))\n        } else {\n          out[key] = value\n        }\n      }\n    }\n    return out\n  }\n\n  return function replacer(_key: string, value: unknown) {\n    if (isRemixElement(value)) {\n      return unwrapElement(value)\n    }\n    if (Array.isArray(value)) {\n      return value.map((v) => unwrapNode(v))\n    }\n    return value\n  }\n}\n\nfunction buildEntrySegment(\n  type: EntryComponent,\n  props: any,\n  context: RenderContext,\n  frameState: SsrFrameState,\n): Segment {\n  let instanceId = randomId('h')\n  let rendered = buildComponentSegment(type, props, context, instanceId, frameState)\n\n  // Store hydration data in context for aggregation\n  let replacer = createHydrationPropsReplacer(context, frameState)\n  context.hydrationData.set(instanceId, {\n    moduleUrl: type.$moduleUrl,\n    exportName: type.$exportName,\n    props: JSON.parse(JSON.stringify(props, replacer)),\n  })\n\n  let start = staticSeg(`<!-- rmx:h:${instanceId} -->`)\n  let end = staticSeg('<!-- /rmx:h -->')\n  return compositeSeg([start, rendered, end])\n}\n\n// Resolve all blocking frame content once\nasync function resolveBlocking(segment: Segment): Promise<void> {\n  if (segment.kind === 'frame') {\n    if (segment.pending) {\n      await segment.pending\n      segment.pending = undefined\n    }\n    if (segment.content) await resolveBlocking(segment.content)\n    return\n  }\n  if (segment.kind === 'composite') {\n    for (let part of segment.parts) {\n      await resolveBlocking(part)\n    }\n  }\n}\n\n// Serialize the segment tree to HTML\nfunction serializeSegment(seg: Segment): string {\n  if (seg.kind === 'static') return seg.html\n  if (seg.kind === 'composite') return seg.parts.map(serializeSegment).join('')\n  // frame\n  let inner = seg.content ? serializeSegment(seg.content) : ''\n  let start = `<!-- rmx:f:${seg.frameId} -->`\n  let end = `<!-- /rmx:f -->`\n  return start + inner + end\n}\n\nfunction escapeHtml(str: string): string {\n  return str\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#39;')\n}\n\nfunction escapeTextContent(str: string): string {\n  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\nfunction escapeTemplateContent(html: string): string {\n  return html.replace(/<\\/template/gi, '<\\\\/template')\n}\n\nfunction transformAttributeName(name: string, isSvg: boolean): string {\n  // aria-/data- pass through\n  if (name.startsWith('aria-') || name.startsWith('data-')) return name\n\n  // HTML mappings\n  if (name === 'className') return 'class'\n  if (!isSvg) {\n    if (name === 'htmlFor') return 'for'\n    if (name === 'tabIndex') return 'tabindex'\n    if (name === 'acceptCharset') return 'accept-charset'\n    if (name === 'httpEquiv') return 'http-equiv'\n    return name.toLowerCase()\n  }\n\n  return normalizeSvgAttribute(name).attr\n}\n\nfunction finalizeHtml(html: string, context: RenderContext): string {\n  let hasHtmlRoot = html.trimStart().toLowerCase().startsWith('<html')\n\n  let css = collectAllStyles(context)\n  if (css) {\n    let headContent = `<style data-rmx-styles>${css}</style>`\n    if (hasHtmlRoot) {\n      // For HTML root, inject into existing head or create one\n      let headCloseIndex = html.indexOf('</head>')\n      if (headCloseIndex !== -1) {\n        // Inject before existing </head>\n        html = html.slice(0, headCloseIndex) + headContent + html.slice(headCloseIndex)\n      } else {\n        // No existing head, inject after <html>\n        let htmlOpenMatch = html.match(/<html[^>]*>/)\n        if (htmlOpenMatch) {\n          let insertIndex = htmlOpenMatch.index! + htmlOpenMatch[0].length\n          html =\n            html.slice(0, insertIndex) + `<head>${headContent}</head>` + html.slice(insertIndex)\n        }\n      }\n    } else {\n      // No HTML root, prepend head\n      html = `<head>${headContent}</head>${html}`\n    }\n  }\n\n  // Append aggregated hydration/frame data script at the end\n  let rmxData = buildRmxDataScript(context)\n  if (rmxData) {\n    if (hasHtmlRoot) {\n      // Insert before </body> if present, otherwise before </html>\n      let bodyCloseIndex = html.indexOf('</body>')\n      if (bodyCloseIndex !== -1) {\n        html = html.slice(0, bodyCloseIndex) + rmxData + html.slice(bodyCloseIndex)\n      } else {\n        let htmlCloseIndex = html.indexOf('</html>')\n        if (htmlCloseIndex !== -1) {\n          html = html.slice(0, htmlCloseIndex) + rmxData + html.slice(htmlCloseIndex)\n        } else {\n          html += rmxData\n        }\n      }\n    } else {\n      html += rmxData\n    }\n  }\n\n  return html\n}\n\nfunction processStyleProps(props: any): any {\n  let processedProps = { ...props }\n  let classAttr = typeof props.class === 'string' ? props.class : ''\n  let className = typeof props.className === 'string' ? props.className : ''\n  let mergedClassName = [classAttr, className].filter(Boolean).join(' ')\n\n  if (mergedClassName) {\n    processedProps.className = mergedClassName\n    delete processedProps.class\n  }\n\n  if (typeof props.style === 'object') {\n    processedProps.style = serializeStyleObject(props.style)\n  }\n\n  return processedProps\n}\n\nfunction collectAllStyles(context: RenderContext): string {\n  if (context.styleCache.size === 0) return ''\n\n  let allCss = ''\n  for (let { css } of context.styleCache.values()) {\n    allCss += css + '\\n'\n  }\n  return `@layer rmx { ${allCss.trim()} }`\n}\n\nfunction buildRmxDataScript(context: RenderContext): string {\n  if (context.hydrationData.size === 0 && context.frameData.size === 0) {\n    return ''\n  }\n\n  let data: {\n    h?: Record<string, HydrationData>\n    f?: Record<string, FrameData>\n  } = {}\n\n  if (context.hydrationData.size > 0) {\n    data.h = Object.fromEntries(context.hydrationData)\n  }\n\n  if (context.frameData.size > 0) {\n    data.f = Object.fromEntries(context.frameData)\n  }\n\n  let serializedData = escapeScriptJson(JSON.stringify(data))\n  return `<script type=\"application/json\" id=\"rmx-data\">${serializedData}</script>`\n}\n\nfunction escapeScriptJson(json: string): string {\n  // Avoid prematurely closing the script tag when serialized data contains \"</script>\".\n  return json.replace(/</g, '\\\\u003c')\n}\n\nfunction serializeStyleObject(style: Record<string, any>): string {\n  let parts: string[] = []\n\n  for (let [key, value] of Object.entries(style)) {\n    if (value == null) continue\n    if (typeof value === 'boolean') continue\n    if (typeof value === 'number' && !Number.isFinite(value)) continue\n\n    // Convert camelCase to kebab-case\n    let cssKey = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)\n\n    // Add px to numeric values where appropriate\n    let shouldAppendPx =\n      typeof value === 'number' &&\n      value !== 0 &&\n      !NUMERIC_CSS_PROPS.has(cssKey) &&\n      !cssKey.startsWith('--')\n\n    let cssValue = shouldAppendPx\n      ? `${value}px`\n      : Array.isArray(value)\n        ? value.join(', ')\n        : String(value)\n\n    parts.push(`${cssKey}: ${cssValue};`)\n  }\n\n  return parts.join(' ')\n}\n\n// Frame styles work end-to-end when frame handlers use their own `renderToStream`:\n// the handler's `finalizeHtml` emits `<style data-rmx-styles>` in its HTML, and on the client,\n// the `adoptServerStyleTag` MutationObserver (stylesheet.ts) picks it up anywhere in the\n// document and adopts the CSS into an adopted stylesheet.\nasync function streamPendingFrames(\n  context: RenderContext,\n  controller: ReadableStreamDefaultController,\n  encoder: TextEncoder,\n): Promise<void> {\n  let processedFrames = new Set<string>()\n\n  while (true) {\n    let batch = context.pendingFrames.filter(({ frameId }) => !processedFrames.has(frameId))\n    if (batch.length === 0) break\n\n    await Promise.all(\n      batch.map(async ({ frameId, promise }) => {\n        processedFrames.add(frameId)\n        try {\n          let { html, tail } = await promise\n\n          // Stream as a template element (first chunk only)\n          let templateHtml = `<template id=\"${frameId}\">${escapeTemplateContent(html)}</template>`\n          controller.enqueue(encoder.encode(templateHtml))\n\n          // Forward any additional chunks from a stream-valued resolveFrame result.\n          if (tail) {\n            await streamByteStreams([tail], controller, context.onError)\n          }\n        } catch (error) {\n          context.onError(error)\n        }\n      }),\n    )\n  }\n}\n\nasync function streamByteStreams(\n  streams: ReadableStream<Uint8Array>[],\n  controller: ReadableStreamDefaultController,\n  onError: (error: unknown) => void,\n): Promise<void> {\n  await Promise.all(\n    streams.map(async (stream) => {\n      let reader = stream.getReader()\n      try {\n        while (true) {\n          let { done, value } = await reader.read()\n          if (done) break\n          controller.enqueue(value)\n        }\n      } catch (error) {\n        onError(error)\n      } finally {\n        reader.releaseLock()\n      }\n    }),\n  )\n}\n\nasync function drain(stream: ReadableStream<Uint8Array>): Promise<string> {\n  let reader = stream.getReader()\n  let decoder = new TextDecoder()\n  let html = ''\n\n  while (true) {\n    let { done, value } = await reader.read()\n    if (done) break\n    html += decoder.decode(value)\n  }\n\n  return html\n}\n\n/**\n * Renders a node tree to a complete HTML string.\n *\n * @param node Node tree to render.\n * @returns Rendered HTML.\n */\nexport async function renderToString(node: RemixNode): Promise<string> {\n  return drain(\n    renderToStream(node, {\n      onError(error) {\n        throw error\n      },\n    }),\n  )\n}\n"
  },
  {
    "path": "packages/component/src/lib/style/index.ts",
    "content": "import { createStyleManager } from './lib/stylesheet.ts'\n\nexport type {\n  CSSProps as EnhancedStyleProperties,\n  DOMStyleProperties as StyleProperties,\n} from './lib/style.ts'\nexport { processStyleClass, normalizeCssValue } from './lib/style.ts'\nexport { createStyleManager }\n\nexport type StyleManager = ReturnType<typeof createStyleManager>\n"
  },
  {
    "path": "packages/component/src/lib/style/lib/style.ts",
    "content": "export type DOMStyleProperties = {\n  [key in keyof Omit<\n    CSSStyleDeclaration,\n    'item' | 'setProperty' | 'removeProperty' | 'getPropertyValue' | 'getPropertyPriority'\n  >]?: string | number | null | undefined\n}\nexport type AllStyleProperties = {\n  [key: string]: string | number | null | undefined\n}\nexport interface StyleProps extends AllStyleProperties, DOMStyleProperties {\n  cssText?: string | null\n}\n\n// allow nesting\nexport interface CSSProps extends DOMStyleProperties {\n  [key: string]: CSSProps | string | number | null | undefined\n}\n\ntype StyleObject = Record<string, unknown>\n\n// Convert camelCase CSS properties to kebab-case\nfunction camelToKebab(str: string): string {\n  return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)\n}\n\n// Properties that should remain unitless (numeric values without px)\nconst NUMERIC_CSS_PROPS = new Set([\n  'aspect-ratio',\n  'z-index',\n  'opacity',\n  'flex-grow',\n  'flex-shrink',\n  'flex-order',\n  'grid-area',\n  'grid-row',\n  'grid-column',\n  'font-weight',\n  'line-height',\n  'order',\n  'orphans',\n  'widows',\n  'zoom',\n  'columns',\n  'column-count',\n])\n\n// Normalize numeric CSS values: append 'px' for properties that need units\n// In Standards Mode, browsers drop numeric values without units when multiple properties\n// are present in insertRule(). We must normalize them ourselves.\nexport function normalizeCssValue(key: string, value: unknown): string {\n  if (value == null) return String(value)\n  if (typeof value === 'number' && value !== 0) {\n    let cssKey = camelToKebab(key)\n    if (!NUMERIC_CSS_PROPS.has(cssKey) && !cssKey.startsWith('--')) {\n      return `${value}px`\n    }\n  }\n  return String(value)\n}\n\n// Check if a style property is a nested selector or media query\nfunction isComplexSelector(key: string): boolean {\n  return (\n    key.startsWith('&') ||\n    key.startsWith('@') ||\n    key.startsWith(':') ||\n    key.startsWith('[') ||\n    key.startsWith('.')\n  )\n}\n\n// Detect @keyframes (including vendor-prefixed variants)\nfunction isKeyframesAtRule(key: string): boolean {\n  if (!key.startsWith('@')) return false\n  let lower = key.toLowerCase()\n  return (\n    lower.startsWith('@keyframes') ||\n    lower.startsWith('@-webkit-keyframes') ||\n    lower.startsWith('@-moz-keyframes') ||\n    lower.startsWith('@-o-keyframes')\n  )\n}\n\n// Generate a hash for style objects to create unique class names\nfunction hashStyle(obj: any): string {\n  // Sort keys to ensure consistent hashing, but include values in the string\n  let sortedEntries = Object.entries(obj).sort(([a], [b]) => a.localeCompare(b))\n  let str = JSON.stringify(sortedEntries)\n  let hash = 0\n  for (let i = 0; i < str.length; i++) {\n    let char = str.charCodeAt(i)\n    hash = (hash << 5) - hash + char\n    hash = hash & hash // Convert to 32-bit integer\n  }\n  return Math.abs(hash).toString(36)\n}\n\n// Convert style object to CSS text\nfunction styleToCss(styles: StyleObject, selector: string = ''): string {\n  let baseDeclarations: string[] = []\n  let nestedBlocks: string[] = []\n  let atRules: string[] = []\n  let preludeAtRules: string[] = []\n\n  for (let [key, value] of Object.entries(styles)) {\n    if (isComplexSelector(key)) {\n      if (key.startsWith('@')) {\n        // Allow at-rules to be conditionally disabled.\n        // e.g. { '@media (min-width: 600px)': condition ? undefined : { ... } }\n        let record = toRecord(value)\n        if (!record) continue\n\n        // Some at-rules (e.g., @media) scope declarations to the selector.\n        // Others (e.g., @function) must NOT include the selector in their body.\n        if (key.startsWith('@function')) {\n          let body = atRuleBodyToCss(record)\n          if (body.trim().length > 0) {\n            preludeAtRules.push(`${key} {\\n${indent(body, 2)}\\n}`)\n          } else {\n            preludeAtRules.push(`${key} {\\n}`)\n          }\n        } else if (isKeyframesAtRule(key)) {\n          // Keyframes definitions must not be wrapped with the element selector.\n          // Emit them before the class rule so animations can be referenced.\n          let body = keyframesBodyToCss(record)\n          if (body.trim().length > 0) {\n            preludeAtRules.push(`${key} {\\n${indent(body, 2)}\\n}`)\n          } else {\n            preludeAtRules.push(`${key} {\\n}`)\n          }\n        } else {\n          // Default: keep at-rules nested with the element selector\n          let inner = styleToCss(record, selector)\n          if (inner.trim().length > 0) {\n            atRules.push(`${key} {\\n${indent(inner, 2)}\\n}`)\n          } else {\n            // Empty at-rule body with selector block\n            atRules.push(`${key} {\\n  ${selector} {\\n  }\\n}`)\n          }\n        }\n        continue\n      }\n\n      // For nested selectors, keep them wholesale inside the base block\n      // Allow nested selectors to be conditionally disabled.\n      // e.g. { '&:hover': condition ? undefined : { ... } }\n      let record = toRecord(value)\n      if (!record) continue\n\n      let nestedContent = ''\n      for (let [prop, propValue] of Object.entries(record)) {\n        if (propValue != null) {\n          let normalizedValue = normalizeCssValue(prop, propValue)\n          nestedContent += `    ${camelToKebab(prop)}: ${normalizedValue};\\n`\n        }\n      }\n      if (nestedContent) {\n        // Preserve key verbatim (e.g., '&[aria-selected], &[rmx-focus]')\n        nestedBlocks.push(`  ${key} {\\n${nestedContent}  }`)\n      }\n    } else {\n      // Base declaration\n      if (value != null) {\n        let normalizedValue = normalizeCssValue(key, value)\n        baseDeclarations.push(`  ${camelToKebab(key)}: ${normalizedValue};`)\n      }\n    }\n  }\n\n  let css = ''\n  if (preludeAtRules.length > 0) {\n    css += preludeAtRules.join('\\n')\n  }\n  if (selector && (baseDeclarations.length > 0 || nestedBlocks.length > 0)) {\n    css += (css ? '\\n' : '') + `${selector} {\\n`\n    if (baseDeclarations.length > 0) {\n      css += baseDeclarations.join('\\n') + '\\n'\n    }\n    if (nestedBlocks.length > 0) {\n      css += nestedBlocks.join('\\n') + '\\n'\n    }\n    css += '}'\n  }\n\n  if (atRules.length > 0) {\n    css += (css ? '\\n' : '') + atRules.join('\\n')\n  }\n\n  return css\n}\n\nfunction indent(text: string, spaces: number): string {\n  let pad = ' '.repeat(spaces)\n  return text\n    .split('\\n')\n    .map((line) => (line.length ? pad + line : line))\n    .join('\\n')\n}\n\n// Narrow unknown values to plain record objects\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> | null {\n  return isRecord(value) ? value : null\n}\n\n// Build the body of a @keyframes rule (without wrapping selector)\nfunction keyframesBodyToCss(frames: StyleObject): string {\n  let blocks: string[] = []\n\n  for (let [frameSelector, frameValue] of Object.entries(frames)) {\n    if (!isRecord(frameValue)) {\n      // Skip non-object frame definitions\n      continue\n    }\n\n    let declarations: string[] = []\n    for (let [prop, propValue] of Object.entries(frameValue)) {\n      if (propValue == null) continue\n      // Ignore nested selectors/at-rules inside keyframe steps\n      if (isComplexSelector(prop)) continue\n      let normalizedValue = normalizeCssValue(prop, propValue)\n      declarations.push(`  ${camelToKebab(prop)}: ${normalizedValue};`)\n    }\n\n    if (declarations.length > 0) {\n      blocks.push(`${frameSelector} {\\n${declarations.join('\\n')}\\n}`)\n    } else {\n      blocks.push(`${frameSelector} {\\n}`)\n    }\n  }\n\n  return blocks.join('\\n')\n}\n\n// Build the body for at-rules that should not include a selector wrapper (e.g., @function)\nfunction atRuleBodyToCss(styles: StyleObject): string {\n  let declarations: string[] = []\n  let nested: string[] = []\n\n  for (let [key, value] of Object.entries(styles)) {\n    if (isComplexSelector(key)) {\n      if (key.startsWith('@')) {\n        // Nested at-rules inside definition blocks; render their bodies recursively without selectors\n        let record = toRecord(value)\n        if (!record) continue\n        let inner = atRuleBodyToCss(record)\n        if (inner.trim().length > 0) {\n          nested.push(`${key} {\\n${indent(inner, 2)}\\n}`)\n        } else {\n          nested.push(`${key} {\\n}`)\n        }\n      } else {\n        // Ignore nested selectors (&, :, ., [) inside definition-style at-rules\n        // They are not meaningful within e.g. @function bodies\n        continue\n      }\n    } else {\n      if (value != null) {\n        let normalizedValue = normalizeCssValue(key, value)\n        declarations.push(`  ${camelToKebab(key)}: ${normalizedValue};`)\n      }\n    }\n  }\n\n  let body = ''\n  if (declarations.length > 0) {\n    body += declarations.join('\\n')\n  }\n  if (nested.length > 0) {\n    body += (body ? '\\n' : '') + nested.join('\\n')\n  }\n  return body\n}\n\nexport function processStyleClass(\n  styleObj: CSSProps,\n  styleCache: Map<string, { selector: string; css: string }>,\n): {\n  selector: string\n  css: string\n} {\n  // Check if the object is empty\n  if (Object.keys(styleObj).length === 0) {\n    return { selector: '', css: '' }\n  }\n\n  // Generate a hash for the style object\n  let hash = hashStyle(styleObj)\n  let selector = `rmxc-${hash}`\n\n  // Check cache first\n  let cached = styleCache.get(hash)\n  if (cached) {\n    return cached\n  }\n\n  let css = styleToCss(styleObj, `.${selector}`)\n  let result = { selector, css }\n\n  // Store in cache\n  styleCache.set(hash, result)\n\n  return result\n}\n\n// Clear style cache (useful for testing)\nexport function clearStyleCache(styleCache: Map<string, { selector: string; css: string }>): void {\n  styleCache.clear()\n}\n"
  },
  {
    "path": "packages/component/src/lib/style/lib/stylesheet.ts",
    "content": "// .rmx-23ace { color: red; }\n// <ul>{item.map(() => <li className=\"rmxc-123\">)}\n\ntype RuleEntry = { count: number; index: number }\ntype ActiveManager = { layer: string; ruleMap: Map<string, RuleEntry> }\n\ntype ServerStyleState = {\n  sheet: CSSStyleSheet\n  text: string\n  refCount: number\n  observer: MutationObserver | null\n  processed: WeakSet<HTMLStyleElement>\n  adoptedTexts: Set<string>\n  selectorsByLayer: Map<string, Set<string>>\n}\n\nlet serverStyleState: ServerStyleState | null = null\nlet activeManagers = new Set<ActiveManager>()\n\nfunction isHtmlStyleElement(node: unknown): node is HTMLStyleElement {\n  return typeof node === 'object' && node !== null && node instanceof HTMLStyleElement\n}\n\nfunction getLayerName(rule: CSSRule): string | null {\n  if (typeof (globalThis as any).CSSLayerBlockRule === 'undefined') return null\n  if (!(rule instanceof (globalThis as any).CSSLayerBlockRule)) return null\n  // CSSLayerBlockRule.name exists in modern browsers, but it's not always in TS DOM libs\n  return ((rule as any).name as string | undefined) ?? null\n}\n\nfunction isCssStyleRule(rule: CSSRule): rule is CSSStyleRule {\n  if (typeof (globalThis as any).CSSStyleRule === 'undefined') return false\n  return rule instanceof (globalThis as any).CSSStyleRule\n}\n\nfunction walkRulesForSelectors(\n  rules: CSSRuleList,\n  layerName: string | null,\n  addSelector: (layerName: string, selector: string) => void,\n) {\n  for (let i = 0; i < rules.length; i++) {\n    let rule = rules[i]\n    if (!rule) continue\n\n    let nextLayerName = getLayerName(rule) ?? layerName\n\n    if (isCssStyleRule(rule)) {\n      if (!nextLayerName) continue\n      // Extract class-based css mixin selectors (.rmxc-*).\n      let classMatches = rule.selectorText.matchAll(/\\.((?:rmxc-[a-z0-9]+))/g)\n      for (let match of classMatches) {\n        let selector = match[1]\n        if (selector) addSelector(nextLayerName, selector)\n      }\n      continue\n    }\n\n    // Recurse through grouping rules (@media, @supports, @layer, etc.)\n    let childRules = (rule as any).cssRules as CSSRuleList | undefined\n    if (childRules) {\n      walkRulesForSelectors(childRules, nextLayerName, addSelector)\n    }\n  }\n}\n\nfunction seedManagersWithServerSelectors(layerName: string, selectors: Set<string>) {\n  for (let mgr of activeManagers) {\n    if (mgr.layer !== layerName) continue\n    for (let selector of selectors) {\n      if (!mgr.ruleMap.has(selector)) {\n        // Track as existing (count: 1, index: -1 since it's in the shared server stylesheet)\n        mgr.ruleMap.set(selector, { count: 1, index: -1 })\n      }\n    }\n  }\n}\n\nfunction ensureServerStyleState(): ServerStyleState {\n  if (serverStyleState) return serverStyleState\n\n  let sheet = new CSSStyleSheet()\n  document.adoptedStyleSheets.push(sheet)\n\n  serverStyleState = {\n    sheet,\n    text: '',\n    refCount: 0,\n    observer: null,\n    processed: new WeakSet(),\n    adoptedTexts: new Set(),\n    selectorsByLayer: new Map(),\n  }\n\n  adoptAllServerStyleTags()\n  startServerStyleObserver()\n\n  return serverStyleState\n}\n\nfunction adoptAllServerStyleTags() {\n  if (!serverStyleState) return\n\n  let styles = document.querySelectorAll('style[data-rmx-styles]')\n  for (let i = 0; i < styles.length; i++) {\n    let el = styles[i]\n    if (isHtmlStyleElement(el)) adoptServerStyleTag(el)\n  }\n}\n\nfunction startServerStyleObserver() {\n  if (!serverStyleState) return\n  if (serverStyleState.observer) return\n\n  // Adopt streamed chunks that include their own <style data-rmx-styles> tags.\n  // We watch the whole document so templates inserted into <body> are covered too.\n  let root = document.documentElement\n  if (!root) return\n\n  serverStyleState.observer = new MutationObserver((mutations) => {\n    for (let mutation of mutations) {\n      for (let node of mutation.addedNodes) {\n        if (!node) continue\n        if (isHtmlStyleElement(node)) {\n          if (node.matches('style[data-rmx-styles]')) adoptServerStyleTag(node)\n          continue\n        }\n        if (node instanceof Element) {\n          let nested = node.querySelectorAll?.('style[data-rmx-styles]') ?? []\n          for (let i = 0; i < nested.length; i++) {\n            let el = nested[i]\n            if (isHtmlStyleElement(el)) adoptServerStyleTag(el)\n          }\n        }\n      }\n    }\n  })\n\n  serverStyleState.observer.observe(root, { childList: true, subtree: true })\n}\n\nfunction adoptServerStyleTag(styleEl: HTMLStyleElement) {\n  if (!serverStyleState) return\n  if (serverStyleState.processed.has(styleEl)) return\n  serverStyleState.processed.add(styleEl)\n\n  let addedSelectorsByLayer = new Map<string, Set<string>>()\n  function addSelector(layerName: string, selector: string) {\n    let layerSet = serverStyleState!.selectorsByLayer.get(layerName)\n    if (!layerSet) {\n      layerSet = new Set()\n      serverStyleState!.selectorsByLayer.set(layerName, layerSet)\n    }\n    if (layerSet.has(selector)) return\n    layerSet.add(selector)\n\n    let addedSet = addedSelectorsByLayer.get(layerName)\n    if (!addedSet) {\n      addedSet = new Set()\n      addedSelectorsByLayer.set(layerName, addedSet)\n    }\n    addedSet.add(selector)\n  }\n\n  try {\n    if (styleEl.sheet) {\n      walkRulesForSelectors(styleEl.sheet.cssRules, null, addSelector)\n    }\n  } catch {\n    // If CSSOM access fails, we still adopt the CSS text below.\n  }\n\n  let adopted = false\n  let cssText = styleEl.textContent?.trim() ?? ''\n  if (cssText.length === 0) {\n    adopted = true\n  } else if (serverStyleState.adoptedTexts.has(cssText)) {\n    // Duplicate chunk - safe to remove the tag\n    adopted = true\n  } else {\n    try {\n      if (typeof (serverStyleState.sheet as any).replaceSync === 'function') {\n        serverStyleState.text += (serverStyleState.text ? '\\n' : '') + cssText\n        ;(serverStyleState.sheet as any).replaceSync(serverStyleState.text)\n        serverStyleState.adoptedTexts.add(cssText)\n        adopted = true\n      } else if (styleEl.sheet) {\n        let rules = styleEl.sheet.cssRules\n        for (let i = 0; i < rules.length; i++) {\n          let rule = rules[i]\n          serverStyleState.sheet.insertRule(rule.cssText, serverStyleState.sheet.cssRules.length)\n        }\n        serverStyleState.adoptedTexts.add(cssText)\n        adopted = true\n      }\n    } catch {\n      // If adoption fails (e.g. invalid CSS), keep the <style> tag in the DOM so styles\n      // still apply. We'll still seed selectors so the client won't duplicate rules.\n    }\n  }\n\n  // Remove the server-rendered <style> tag now that we've adopted its content.\n  if (adopted) {\n    styleEl.remove()\n  }\n\n  // Ensure existing managers treat these as pre-existing rules.\n  for (let [layerName, selectors] of addedSelectorsByLayer) {\n    seedManagersWithServerSelectors(layerName, selectors)\n  }\n}\n\nfunction teardownServerStyleStateIfUnused() {\n  if (!serverStyleState) return\n  if (serverStyleState.refCount > 0) return\n\n  if (serverStyleState.observer) {\n    serverStyleState.observer.disconnect()\n  }\n\n  document.adoptedStyleSheets = Array.from(document.adoptedStyleSheets).filter(\n    (s) => s !== serverStyleState!.sheet,\n  )\n\n  serverStyleState = null\n}\n\nexport function createStyleManager(layer: string = 'rmx') {\n  let server = ensureServerStyleState()\n  server.refCount++\n  adoptAllServerStyleTags()\n\n  let stylesheet: CSSStyleSheet | null = null\n  function getStylesheet(): CSSStyleSheet {\n    if (!stylesheet) {\n      stylesheet = new CSSStyleSheet()\n      document.adoptedStyleSheets.push(stylesheet)\n    }\n    return stylesheet\n  }\n\n  // Track usage count and rule index per className\n  // Using an object to track both count and index together\n  let ruleMap = new Map<string, RuleEntry>()\n\n  // Seed from any already-adopted server selectors for this layer\n  let serverSelectors = server.selectorsByLayer.get(layer)\n  if (serverSelectors) {\n    for (let selector of serverSelectors) {\n      ruleMap.set(selector, { count: 1, index: -1 })\n    }\n  }\n\n  let manager: ActiveManager = { layer, ruleMap }\n  activeManagers.add(manager)\n\n  function has(className: string) {\n    let entry = ruleMap.get(className)\n    return entry !== undefined && entry.count > 0\n  }\n\n  function insert(className: string, rule: string) {\n    let entry = ruleMap.get(className)\n\n    if (entry) {\n      // Already exists, just increment count\n      entry.count++\n      return\n    }\n\n    // New rule - insert and track\n    let sheet = getStylesheet()\n    let index = sheet.cssRules.length\n    try {\n      sheet.insertRule(`@layer ${layer} { ${rule} }`, index)\n      ruleMap.set(className, { count: 1, index })\n    } catch (error) {\n      // If insertion fails (e.g., invalid CSS), don't track it\n      // The browser will have thrown, so we can't proceed\n      throw error\n    }\n  }\n\n  function remove(className: string) {\n    let entry = ruleMap.get(className)\n    if (!entry) return\n\n    // Decrement count\n    entry.count--\n\n    if (entry.count > 0) {\n      // Still in use, keep the rule\n      return\n    }\n\n    // Count reached zero, remove the rule\n    let indexToDelete = entry.index\n\n    // Remove from tracking\n    ruleMap.delete(className)\n\n    // Server-rendered rules (index: -1) live in the shared server stylesheet, nothing to delete\n    if (indexToDelete < 0) return\n\n    // If we somehow don't have a sheet, there's nothing to delete\n    if (!stylesheet) return\n\n    // TODO: just search and remove, stop re-indexing\n    stylesheet.deleteRule(indexToDelete)\n\n    // Update indices for all rules that came after the deleted one\n    // They all shift down by 1\n    for (let [name, data] of ruleMap.entries()) {\n      if (data.index > indexToDelete) {\n        data.index--\n      }\n    }\n  }\n\n  function dispose() {\n    if (stylesheet) {\n      // Remove stylesheet from document\n      document.adoptedStyleSheets = Array.from(document.adoptedStyleSheets).filter(\n        (s) => s !== stylesheet,\n      )\n    }\n    // Clear internal state\n    ruleMap.clear()\n    activeManagers.delete(manager)\n    server.refCount--\n    teardownServerStyleStateIfUnused()\n  }\n\n  return { insert, remove, has, dispose }\n}\n"
  },
  {
    "path": "packages/component/src/lib/svg-attributes.ts",
    "content": "const XLINK_NS = 'http://www.w3.org/1999/xlink'\nconst XML_NS = 'http://www.w3.org/XML/1998/namespace'\n\nconst CANONICAL_CAMEL_SVG_ATTRS = new Set([\n  'accentHeight',\n  'attributeName',\n  'attributeType',\n  'baseFrequency',\n  'baseProfile',\n  'calcMode',\n  'viewBox',\n  'preserveAspectRatio',\n  'externalResourcesRequired',\n  'filterRes',\n  'gradientUnits',\n  'gradientTransform',\n  'glyphRef',\n  'kernelMatrix',\n  'kernelUnitLength',\n  'keyPoints',\n  'keySplines',\n  'keyTimes',\n  'lengthAdjust',\n  'limitingConeAngle',\n  'markerHeight',\n  'patternUnits',\n  'patternContentUnits',\n  'patternTransform',\n  'markerWidth',\n  'numOctaves',\n  'pathLength',\n  'pointsAtX',\n  'pointsAtY',\n  'pointsAtZ',\n  'preserveAlpha',\n  'clipPathUnits',\n  'maskUnits',\n  'maskContentUnits',\n  'filterUnits',\n  'primitiveUnits',\n  'refX',\n  'refY',\n  'requiredExtensions',\n  'requiredFeatures',\n  'specularConstant',\n  'specularExponent',\n  'spreadMethod',\n  'startOffset',\n  'stdDeviation',\n  'stitchTiles',\n  'surfaceScale',\n  'systemLanguage',\n  'tableValues',\n  'targetX',\n  'targetY',\n  'textLength',\n  'viewTarget',\n  'xChannelSelector',\n  'yChannelSelector',\n  'zoomAndPan',\n  'edgeMode',\n  'diffuseConstant',\n  'markerUnits',\n])\n\nconst SVG_ATTR_ALIASES = new Map<string, string>()\nfor (let attr of CANONICAL_CAMEL_SVG_ATTRS) {\n  SVG_ATTR_ALIASES.set(camelToKebab(attr), attr)\n}\n\nconst NAMESPACED_SVG_ALIASES = new Map([\n  ['xlinkHref', { ns: XLINK_NS, attr: 'xlink:href' }],\n  ['xlink:href', { ns: XLINK_NS, attr: 'xlink:href' }],\n  ['xlink-href', { ns: XLINK_NS, attr: 'xlink:href' }],\n  ['xlinkActuate', { ns: XLINK_NS, attr: 'xlink:actuate' }],\n  ['xlink:actuate', { ns: XLINK_NS, attr: 'xlink:actuate' }],\n  ['xlink-actuate', { ns: XLINK_NS, attr: 'xlink:actuate' }],\n  ['xlinkArcrole', { ns: XLINK_NS, attr: 'xlink:arcrole' }],\n  ['xlink:arcrole', { ns: XLINK_NS, attr: 'xlink:arcrole' }],\n  ['xlink-arcrole', { ns: XLINK_NS, attr: 'xlink:arcrole' }],\n  ['xlinkRole', { ns: XLINK_NS, attr: 'xlink:role' }],\n  ['xlink:role', { ns: XLINK_NS, attr: 'xlink:role' }],\n  ['xlink-role', { ns: XLINK_NS, attr: 'xlink:role' }],\n  ['xlinkShow', { ns: XLINK_NS, attr: 'xlink:show' }],\n  ['xlink:show', { ns: XLINK_NS, attr: 'xlink:show' }],\n  ['xlink-show', { ns: XLINK_NS, attr: 'xlink:show' }],\n  ['xlinkTitle', { ns: XLINK_NS, attr: 'xlink:title' }],\n  ['xlink:title', { ns: XLINK_NS, attr: 'xlink:title' }],\n  ['xlink-title', { ns: XLINK_NS, attr: 'xlink:title' }],\n  ['xlinkType', { ns: XLINK_NS, attr: 'xlink:type' }],\n  ['xlink:type', { ns: XLINK_NS, attr: 'xlink:type' }],\n  ['xlink-type', { ns: XLINK_NS, attr: 'xlink:type' }],\n  ['xmlBase', { ns: XML_NS, attr: 'xml:base' }],\n  ['xml:base', { ns: XML_NS, attr: 'xml:base' }],\n  ['xml-base', { ns: XML_NS, attr: 'xml:base' }],\n  ['xmlLang', { ns: XML_NS, attr: 'xml:lang' }],\n  ['xml:lang', { ns: XML_NS, attr: 'xml:lang' }],\n  ['xml-lang', { ns: XML_NS, attr: 'xml:lang' }],\n  ['xmlSpace', { ns: XML_NS, attr: 'xml:space' }],\n  ['xml:space', { ns: XML_NS, attr: 'xml:space' }],\n  ['xml-space', { ns: XML_NS, attr: 'xml:space' }],\n  ['xmlnsXlink', { attr: 'xmlns:xlink' }],\n  ['xmlns:xlink', { attr: 'xmlns:xlink' }],\n  ['xmlns-xlink', { attr: 'xmlns:xlink' }],\n])\n\nexport function normalizeSvgAttributeName(name: string): string {\n  let alias = SVG_ATTR_ALIASES.get(name)\n  if (alias) return alias\n\n  if (CANONICAL_CAMEL_SVG_ATTRS.has(name)) return name\n\n  return camelToKebab(name)\n}\n\nexport function normalizeSvgAttribute(name: string): {\n  ns?: string\n  attr: string\n} {\n  let namespaced = NAMESPACED_SVG_ALIASES.get(name)\n  if (namespaced) {\n    return namespaced\n  }\n\n  return { attr: normalizeSvgAttributeName(name) }\n}\n\nfunction camelToKebab(input: string): string {\n  return input\n    .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n    .replace(/_/g, '-')\n    .toLowerCase()\n}\n"
  },
  {
    "path": "packages/component/src/lib/to-vnode.ts",
    "content": "import { Fragment } from './component.ts'\nimport { invariant } from './invariant.ts'\nimport type { RemixElement, RemixNode } from './jsx.ts'\nimport { isRemixElement, TEXT_NODE, type VNode } from './vnode.ts'\n\nfunction flatMapChildrenToVNodes(node: RemixElement): VNode[] {\n  return 'children' in node.props\n    ? Array.isArray(node.props.children)\n      ? node.props.children.flat(Infinity).map(toVNode)\n      : [toVNode(node.props.children)]\n    : []\n}\n\nfunction flattenRemixNodeArray(nodes: RemixNode[], out: RemixNode[] = []): RemixNode[] {\n  for (let child of nodes) {\n    if (Array.isArray(child)) {\n      flattenRemixNodeArray(child, out)\n    } else {\n      out.push(child)\n    }\n  }\n  return out\n}\n\nexport function toVNode(node: RemixNode): VNode {\n  if (node === null || node === undefined || typeof node === 'boolean') {\n    return { type: TEXT_NODE, _text: '' }\n  }\n\n  if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {\n    return { type: TEXT_NODE, _text: String(node) }\n  }\n\n  if (Array.isArray(node)) {\n    let flatChildren = flattenRemixNodeArray(node)\n    return { type: Fragment, _children: flatChildren.map(toVNode) }\n  }\n\n  if (node.type === Fragment) {\n    return { type: Fragment, key: node.key, _children: flatMapChildrenToVNodes(node) }\n  }\n\n  if (isRemixElement(node)) {\n    // When innerHTML is set, ignore children\n    let children = node.props.innerHTML != null ? [] : flatMapChildrenToVNodes(node)\n    return { type: node.type, key: node.key, props: node.props, _children: children }\n  }\n\n  invariant(false, 'Unexpected RemixNode')\n}\n"
  },
  {
    "path": "packages/component/src/lib/tween.ts",
    "content": "/**\n * Attempt to find the t value for a given x on a cubic bezier curve.\n * Uses Newton-Raphson iteration for fast convergence.\n * @param x1 First control point x coordinate\n * @param x2 Second control point x coordinate\n * @param targetX The x value to solve for\n * @returns The t parameter value\n */\nfunction solveCubicBezierX(x1: number, x2: number, targetX: number): number {\n  // Initial guess\n  let t = targetX\n\n  // Newton-Raphson iteration (usually converges in 4-8 iterations)\n  for (let i = 0; i < 8; i++) {\n    let currentX = cubicBezier(t, x1, x2)\n    let slope = cubicBezierDerivative(t, x1, x2)\n\n    if (Math.abs(slope) < 1e-6) break\n\n    let error = currentX - targetX\n    if (Math.abs(error) < 1e-6) break\n\n    t -= error / slope\n  }\n\n  return Math.max(0, Math.min(1, t))\n}\n\n/**\n * Compute the value of a cubic bezier at parameter t.\n * For CSS-style beziers, start is (0,0) and end is (1,1),\n * so we only need the two middle control point coordinates.\n * @param t The parameter value (0 to 1)\n * @param p1 First control point coordinate\n * @param p2 Second control point coordinate\n * @returns The bezier value at t\n */\nfunction cubicBezier(t: number, p1: number, p2: number): number {\n  // B(t) = 3(1-t)²t·p1 + 3(1-t)t²·p2 + t³\n  let oneMinusT = 1 - t\n  return 3 * oneMinusT * oneMinusT * t * p1 + 3 * oneMinusT * t * t * p2 + t * t * t\n}\n\n/**\n * Derivative of the cubic bezier function.\n * @param t The parameter value (0 to 1)\n * @param p1 First control point coordinate\n * @param p2 Second control point coordinate\n * @returns The derivative of the bezier at t\n */\nfunction cubicBezierDerivative(t: number, p1: number, p2: number): number {\n  // B'(t) = 3(1-t)²·p1 + 6(1-t)t·(p2-p1) + 3t²·(1-p2)\n  let oneMinusT = 1 - t\n  return 3 * oneMinusT * oneMinusT * p1 + 6 * oneMinusT * t * (p2 - p1) + 3 * t * t * (1 - p2)\n}\n\n/**\n * Cubic-bezier control points used by {@link tween}.\n */\nexport interface BezierCurve {\n  /** First control point x coordinate. */\n  x1: number\n  /** First control point y coordinate. */\n  y1: number\n  /** Second control point x coordinate. */\n  x2: number\n  /** Second control point y coordinate. */\n  y2: number\n}\n\n// Common easing presets\n/**\n * Common cubic-bezier presets for {@link tween}.\n */\nexport const easings = {\n  linear: { x1: 0, y1: 0, x2: 1, y2: 1 },\n  ease: { x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 },\n  easeIn: { x1: 0.42, y1: 0, x2: 1, y2: 1 },\n  easeOut: { x1: 0, y1: 0, x2: 0.58, y2: 1 },\n  easeInOut: { x1: 0.42, y1: 0, x2: 0.58, y2: 1 },\n} as const\n\n/**\n * Options for generating tweened values over time.\n */\nexport interface TweenOptions {\n  /** Starting value for the tween. */\n  from: number\n  /** Ending value for the tween. */\n  to: number\n  /** Total tween duration in milliseconds. */\n  duration: number\n  /** Cubic-bezier curve used to shape the interpolation. */\n  curve: BezierCurve\n}\n\n/**\n * Generator that tweens a value over time using a cubic bezier curve.\n * Yields the current value on each frame. Use the iterator's `done` property\n * to check if the animation is complete.\n * @param options The tween configuration\n * @yields The current tweened value\n * @returns The final value\n */\nexport function* tween(options: TweenOptions): Generator<number, number, number> {\n  let { from, to, duration, curve } = options\n  let { x1, y1, x2, y2 } = curve\n\n  let startTime: number | null = null\n  let value = from\n\n  while (true) {\n    // Yield current value and receive the next timestamp\n    let timestamp: number = yield value\n\n    if (startTime === null) {\n      startTime = timestamp\n    }\n\n    let elapsed = timestamp - startTime\n    let linearProgress = Math.min(elapsed / duration, 1)\n\n    // Map linear progress through the bezier curve\n    // x-axis = time, y-axis = value\n    let t = solveCubicBezierX(x1, x2, linearProgress)\n    let easedProgress = cubicBezier(t, y1, y2)\n\n    value = from + (to - from) * easedProgress\n\n    if (linearProgress >= 1) {\n      return to\n    }\n  }\n}\n"
  },
  {
    "path": "packages/component/src/lib/typed-event-target.ts",
    "content": "/**\n * An `EventTarget` subclass with typed event maps.\n */\nexport class TypedEventTarget<eventMap> extends EventTarget {\n  /**\n   * Phantom property that carries the event map type on instances.\n   */\n  declare readonly __eventMap?: eventMap\n}\n\n/**\n * Interface surface for {@link TypedEventTarget} with typed listener overloads.\n */\nexport interface TypedEventTarget<eventMap> {\n  /**\n   * Adds a listener for a typed event name from the event map.\n   *\n   * @param type Event name to listen for.\n   * @param listener Listener to invoke when the event fires.\n   * @param options Listener registration options.\n   */\n  addEventListener<type extends Extract<keyof eventMap, string>>(\n    type: type,\n    listener: TypedEventListener<eventMap>[type],\n    options?: AddEventListenerOptions,\n  ): void\n  /**\n   * Adds a listener using the standard untyped `EventTarget` signature.\n   *\n   * @param type Event name to listen for.\n   * @param listener Listener to invoke when the event fires.\n   * @param options Listener registration options.\n   */\n  addEventListener(\n    type: string,\n    listener: EventListenerOrEventListenerObject | null,\n    options?: boolean | AddEventListenerOptions,\n  ): void\n  /**\n   * Removes a listener for a typed event name from the event map.\n   *\n   * @param type Event name to stop listening for.\n   * @param listener Previously registered listener.\n   * @param options Listener removal options.\n   */\n  removeEventListener<type extends Extract<keyof eventMap, string>>(\n    type: type,\n    listener: TypedEventListener<eventMap>[type],\n    options?: EventListenerOptions,\n  ): void\n  /**\n   * Removes a listener using the standard untyped `EventTarget` signature.\n   *\n   * @param type Event name to stop listening for.\n   * @param listener Previously registered listener.\n   * @param options Listener removal options.\n   */\n  removeEventListener(\n    type: string,\n    listener: EventListenerOrEventListenerObject | null,\n    options?: EventListenerOptions,\n  ): void\n}\n\ntype TypedEventListener<eventMap> = {\n  [key in keyof eventMap]: (event: eventMap[key]) => void\n}\n"
  },
  {
    "path": "packages/component/src/lib/vdom.ts",
    "content": "import type { FrameContent, FrameHandle } from './component.ts'\nimport { createFrameHandle } from './component.ts'\nimport { invariant } from './invariant.ts'\nimport type { RemixNode } from './jsx.ts'\nimport {\n  createComponentErrorEvent,\n  getComponentError,\n  type ComponentErrorEvent,\n} from './error-event.ts'\nimport { createScheduler, type Scheduler } from './scheduler.ts'\nimport { diffVNodes, remove as removeVNode } from './reconcile.ts'\nimport { toVNode } from './to-vnode.ts'\nimport { TypedEventTarget } from './typed-event-target.ts'\nimport { ROOT_VNODE, type VNode } from './vnode.ts'\nimport { resetStyleState, defaultStyleManager } from './diff-props.ts'\nimport type { StyleManager } from './style/index.ts'\n\n/**\n * Events emitted by virtual roots.\n */\nexport type VirtualRootEventMap = {\n  error: ComponentErrorEvent\n}\n\n/**\n * Root controller returned by {@link createRoot} and {@link createRangeRoot}.\n */\nexport type VirtualRoot = TypedEventTarget<VirtualRootEventMap> & {\n  render: (element: RemixNode) => void\n  dispose: () => void\n  flush: () => void\n}\n\n/**\n * Options for creating a virtual DOM root with {@link createRoot} or {@link createRangeRoot}.\n */\nexport type VirtualRootOptions = {\n  frame?: FrameHandle\n  scheduler?: Scheduler\n  styleManager?: StyleManager\n  frameInit?: {\n    src?: string\n    resolveFrame: (\n      src: string,\n      signal?: AbortSignal,\n      target?: string,\n    ) => Promise<FrameContent> | FrameContent\n    loadModule?: (moduleUrl: string, exportName: string) => Promise<Function> | Function\n  }\n}\n\nexport { createScheduler, type Scheduler }\nexport { diffVNodes, toVNode }\nexport { resetStyleState }\n\nfunction getHydrationComponentIdFromRangeStart(start: Node): string | undefined {\n  if (!(start instanceof Comment)) return undefined\n  let marker = start.data.trim()\n  if (!marker.startsWith('rmx:h:')) return undefined\n  let id = marker.slice('rmx:h:'.length)\n  return id.length > 0 ? id : undefined\n}\n\n/**\n * Creates a virtual root bounded by two DOM nodes.\n *\n * @param boundaries Start and end marker nodes that define the render region.\n * @param options Root configuration.\n * @returns A virtual root controller.\n */\nexport function createRangeRoot(\n  boundaries: [Node, Node],\n  options: VirtualRootOptions = {},\n): VirtualRoot {\n  let [start, end] = boundaries\n  let vroot: VNode | null = null\n  let styles = options.styleManager ?? defaultStyleManager\n\n  let container = end.parentNode\n  invariant(container, 'Expected parent node')\n  invariant(start.parentNode === container, 'Boundaries must share parent')\n  let parent = container\n\n  let hydrationCursor = start.nextSibling\n\n  let eventTarget = new TypedEventTarget<VirtualRootEventMap>()\n  let scheduler =\n    options.scheduler ?? createScheduler(parent.ownerDocument ?? document, eventTarget, styles)\n  let frameStub =\n    options.frame ??\n    createRootFrameHandle({\n      src: options.frameInit?.src,\n      resolveFrame: options.frameInit?.resolveFrame,\n      loadModule: options.frameInit?.loadModule,\n      errorTarget: eventTarget,\n      scheduler,\n      styleManager: styles,\n    })\n\n  let isErrorForwardingAttached = false\n  function forwardDomError(event: Event) {\n    eventTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event)))\n  }\n  function attachDomErrorForwarding() {\n    if (isErrorForwardingAttached) return\n    parent.addEventListener('error', forwardDomError)\n    isErrorForwardingAttached = true\n  }\n  function detachDomErrorForwarding() {\n    if (!isErrorForwardingAttached) return\n    parent.removeEventListener('error', forwardDomError)\n    isErrorForwardingAttached = false\n  }\n  attachDomErrorForwarding()\n\n  return Object.assign(eventTarget, {\n    render(element: RemixNode) {\n      attachDomErrorForwarding()\n\n      let vnode = toVNode(element)\n      let vParent: VNode = {\n        type: ROOT_VNODE,\n        _svg: false,\n        _rangeStart: start,\n        _rangeEnd: end,\n        _pendingHydrationComponentId: getHydrationComponentIdFromRangeStart(start),\n      }\n      scheduler.enqueueWork([\n        () => {\n          diffVNodes(\n            vroot,\n            vnode,\n            parent,\n            frameStub,\n            scheduler,\n            styles,\n            vParent,\n            eventTarget,\n            end,\n            hydrationCursor,\n          )\n          vroot = vnode\n          hydrationCursor = null\n        },\n      ])\n      scheduler.dequeue()\n    },\n\n    dispose() {\n      detachDomErrorForwarding()\n\n      if (!vroot) return\n      let current = vroot\n      vroot = null\n      scheduler.enqueueWork([() => removeVNode(current, parent, scheduler, styles)])\n      scheduler.dequeue()\n    },\n\n    flush() {\n      scheduler.dequeue()\n    },\n  })\n}\n\n/**\n * Creates a virtual root for a host container element.\n *\n * @param container Host element to render into.\n * @param options Root configuration.\n * @returns A virtual root controller.\n */\nexport function createRoot(container: HTMLElement, options: VirtualRootOptions = {}): VirtualRoot {\n  let vroot: VNode | null = null\n  let styles = options.styleManager ?? defaultStyleManager\n  let hydrationCursor = container.innerHTML.trim() !== '' ? container.firstChild : undefined\n\n  let eventTarget = new TypedEventTarget<VirtualRootEventMap>()\n  let scheduler =\n    options.scheduler ?? createScheduler(container.ownerDocument ?? document, eventTarget, styles)\n  let frameStub =\n    options.frame ??\n    createRootFrameHandle({\n      src: options.frameInit?.src,\n      resolveFrame: options.frameInit?.resolveFrame,\n      loadModule: options.frameInit?.loadModule,\n      errorTarget: eventTarget,\n      scheduler,\n      styleManager: styles,\n    })\n\n  let isErrorForwardingAttached = false\n  function forwardDomError(event: Event) {\n    eventTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event)))\n  }\n  function attachDomErrorForwarding() {\n    if (isErrorForwardingAttached) return\n    container.addEventListener('error', forwardDomError)\n    isErrorForwardingAttached = true\n  }\n  function detachDomErrorForwarding() {\n    if (!isErrorForwardingAttached) return\n    container.removeEventListener('error', forwardDomError)\n    isErrorForwardingAttached = false\n  }\n  attachDomErrorForwarding()\n\n  return Object.assign(eventTarget, {\n    render(element: RemixNode) {\n      attachDomErrorForwarding()\n\n      let vnode = toVNode(element)\n      let vParent: VNode = { type: ROOT_VNODE, _svg: false }\n      scheduler.enqueueWork([\n        () => {\n          diffVNodes(\n            vroot,\n            vnode,\n            container,\n            frameStub,\n            scheduler,\n            styles,\n            vParent,\n            eventTarget,\n            undefined,\n            hydrationCursor,\n          )\n          vroot = vnode\n          hydrationCursor = undefined\n        },\n      ])\n      scheduler.dequeue()\n    },\n\n    dispose() {\n      detachDomErrorForwarding()\n\n      if (!vroot) return\n      let current = vroot\n      vroot = null\n      scheduler.enqueueWork([() => removeVNode(current, container, scheduler, styles)])\n      scheduler.dequeue()\n    },\n\n    flush() {\n      scheduler.dequeue()\n    },\n  })\n}\n\nfunction createRootFrameHandle(init: {\n  src?: string\n  resolveFrame?: (\n    src: string,\n    signal?: AbortSignal,\n    target?: string,\n  ) => Promise<FrameContent> | FrameContent\n  loadModule?: (moduleUrl: string, exportName: string) => Promise<Function> | Function\n  errorTarget: EventTarget\n  scheduler: Scheduler\n  styleManager: StyleManager\n}): FrameHandle {\n  let resolveFrame =\n    init.resolveFrame ??\n    (() => {\n      throw new Error(\n        'Cannot render <Frame /> without frame runtime. Use run() or pass frameInit to createRoot/createRangeRoot.',\n      )\n    })\n\n  let frame = createFrameHandle({\n    src: init.src ?? '/',\n    $runtime: {\n      canResolveFrames: !!init.resolveFrame,\n      topFrame: undefined,\n      loadModule:\n        init.loadModule ??\n        (() => {\n          throw new Error('loadModule is required to hydrate client entries inside <Frame />')\n        }),\n      resolveFrame,\n      errorTarget: init.errorTarget,\n      pendingClientEntries: new Map(),\n      scheduler: init.scheduler,\n      styleManager: init.styleManager,\n      data: {},\n      moduleCache: new Map(),\n      moduleLoads: new Map(),\n      frameInstances: new WeakMap(),\n      namedFrames: new Map(),\n    },\n  })\n  let runtime = frame.$runtime as { topFrame?: FrameHandle } | undefined\n  if (runtime) runtime.topFrame = frame\n  return frame\n}\n"
  },
  {
    "path": "packages/component/src/lib/vnode.ts",
    "content": "import type { ComponentHandle, Component } from './component.ts'\nimport { Fragment, Frame } from './component.ts'\nimport type { ElementProps, RemixElement, RemixNode } from './jsx.ts'\n\nexport const TEXT_NODE = Symbol('TEXT_NODE')\nexport const ROOT_VNODE = Symbol('ROOT_VNODE')\n\nexport type VNodeType =\n  | typeof ROOT_VNODE\n  | string // host element\n  | Function // component\n  | typeof TEXT_NODE\n  | typeof Fragment\n  | typeof Frame\n\nexport type VNode<T extends VNodeType = VNodeType> = {\n  type: T\n  props?: ElementProps\n  _mixedProps?: ElementProps\n  key?: string\n\n  // _prefixes assigned during reconciliation\n  _parent?: VNode\n  _children?: VNode[]\n  _dom?: unknown\n  _controller?: AbortController\n  _mixState?: unknown\n  _controlledState?: unknown\n  _svg?: boolean\n  // Range roots render between comment boundary markers\n  _rangeStart?: Node\n  _rangeEnd?: Node\n  _pendingHydrationComponentId?: string\n  _frameInstance?: unknown\n  _frameFallbackRoot?: { render: (element: RemixNode) => void; dispose: () => void }\n  _frameResolveToken?: number\n  _frameResolveController?: AbortController\n  _frameResolved?: boolean\n\n  // Internal diffing fields\n  _index?: number\n  _flags?: number\n\n  // TEXT_NODE\n  _text?: string\n\n  // Component\n  _handle?: ComponentHandle\n  _id?: string\n  _content?: VNode\n\n  // Mixin-persisted node removal state\n  _persistedByMixins?: boolean\n  _persistedParentByMixins?: ParentNode\n  _persistedRemovalToken?: number\n}\n\nexport type FragmentNode = VNode & {\n  type: typeof Fragment\n  _children: VNode[]\n}\n\nexport type TextNode = VNode & {\n  type: typeof TEXT_NODE\n  _text: string\n}\n\nexport type CommittedTextNode = TextNode & {\n  _dom: Text\n}\n\nexport type HostNode = VNode & {\n  type: string\n  props: ElementProps\n  _children: VNode[]\n}\n\nexport type CommittedHostNode = HostNode & {\n  _dom: Element\n  _controller?: AbortController\n}\n\nexport type ComponentNode = VNode & {\n  type: Function\n  props: ElementProps\n  _handle: ComponentHandle\n}\n\nexport type CommittedComponentNode = VNode & {\n  type: Function\n  props: ElementProps\n  _content: VNode\n  _handle: ComponentHandle\n}\n\nexport function isFragmentNode(node: VNode): node is FragmentNode {\n  return node.type === Fragment\n}\n\nexport function isTextNode(node: VNode): node is TextNode {\n  return node.type === TEXT_NODE\n}\n\nexport function isCommittedTextNode(node: VNode): node is CommittedTextNode {\n  return isTextNode(node) && node._dom instanceof Text\n}\n\nexport function isHostNode(node: VNode): node is HostNode {\n  return typeof node.type === 'string'\n}\n\nexport function isCommittedHostNode(node: VNode): node is CommittedHostNode {\n  return isHostNode(node) && node._dom instanceof Element\n}\n\nexport function isComponentNode(node: VNode): node is ComponentNode {\n  return typeof node.type === 'function' && node.type !== Frame\n}\n\nexport function isCommittedComponentNode(node: VNode): node is CommittedComponentNode {\n  return isComponentNode(node) && node._content !== undefined\n}\n\nexport function isRemixElement(node: RemixNode): node is RemixElement {\n  return typeof node === 'object' && node !== null && '$rmx' in node\n}\n\nexport function findContextFromAncestry(node: VNode, type: Component): unknown {\n  let current: VNode | undefined = node\n  while (current) {\n    if (current.type === type && isComponentNode(current)) {\n      return current._handle.getContextValue()\n    }\n    current = current._parent\n  }\n  return undefined\n}\n"
  },
  {
    "path": "packages/component/src/server.ts",
    "content": "export { renderToString, renderToStream } from './lib/stream.ts'\nexport type { RenderToStreamOptions, ResolveFrameContext } from './lib/stream.ts'\n"
  },
  {
    "path": "packages/component/src/test/client-entry.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport type { Handle } from '../lib/component.ts'\nimport { clientEntry, isEntry } from '../lib/client-entries.ts'\n\ndescribe('clientEntry', () => {\n  describe('types', () => {\n    it('keeps original types', () => {\n      function Input(handle: Handle, props: { defaultValue?: string }) {\n        let value = props.defaultValue ?? ''\n        return ({ label }: { label: string }) => (\n          <label>\n            {label}: <input type=\"text\" value={value} />\n          </label>\n        )\n      }\n\n      let HydratedInput = clientEntry('/js/test.js#Input', Input)\n\n      // @ts-expect-error - should require default render prop\n      let el = <Input />\n      // @ts-expect-error - should require default render prop\n      let el2 = <HydratedInput />\n\n      expect(true).toBe(true)\n    })\n\n    it('only allows serializable props', () => {\n      function Input(handle: Handle, props: { defaultValue?: string; func: () => void }) {\n        let value = props.defaultValue ?? ''\n        return ({ label }: { label: string }) => (\n          <label>\n            {label}: <input type=\"text\" value={value} />\n          </label>\n        )\n      }\n\n      // @ts-expect-error - should disallow non-serializable function prop\n      let HydratedInput = clientEntry('/js/test.js#Input', Input)\n\n      function Input2(handle: Handle, props: { defaultValue?: string }) {\n        let value = props.defaultValue ?? ''\n        return ({ label }: { label: string; func: () => void }) => (\n          <label>\n            {label}: <input type=\"text\" value={value} />\n          </label>\n        )\n      }\n\n      // @ts-expect-error - should disallow non-serializable function prop\n      let HydratedInput2 = clientEntry('/js/test.js#Input', Input2)\n\n      expect(true).toBe(true)\n    })\n  })\n\n  describe('basic functionality', () => {\n    it('marks a component as an entry', () => {\n      function TestComponent(handle: Handle, props: { count: number }) {\n        return () => <div>Count: {props.count}</div>\n      }\n\n      let EntryComponent = clientEntry('/js/test.js#TestComponent', TestComponent)\n\n      expect(EntryComponent.$entry).toBe(true)\n      expect(EntryComponent.$moduleUrl).toBe('/js/test.js')\n      expect(EntryComponent.$exportName).toBe('TestComponent')\n    })\n\n    it('parses module URL and export name from href', () => {\n      function MyComponent() {\n        return () => <div>Hello</div>\n      }\n\n      let EntryComponent = clientEntry('/js/components.js#MyComponent', MyComponent)\n\n      expect(EntryComponent.$moduleUrl).toBe('/js/components.js')\n      expect(EntryComponent.$exportName).toBe('MyComponent')\n    })\n\n    it('uses component name as fallback when no export name provided', () => {\n      function NamedComponent() {\n        return () => <div>Hello</div>\n      }\n\n      let EntryComponent = clientEntry('/js/components.js', NamedComponent)\n\n      expect(EntryComponent.$moduleUrl).toBe('/js/components.js')\n      expect(EntryComponent.$exportName).toBe('NamedComponent')\n    })\n\n    it('preserves the original component functionality', () => {\n      function TestComponent(handle: Handle, props: { initialCount: number }) {\n        let count = props.initialCount\n\n        return (props: { label: string }) => (\n          <button>\n            {props.label}: {count}\n          </button>\n        )\n      }\n\n      let EntryComponent = clientEntry('/js/test.js#TestComponent', TestComponent)\n\n      // The entry component should still be callable\n      expect(typeof EntryComponent).toBe('function')\n\n      // Mock Handle for testing\n      let mockHandle = {} as Handle\n\n      // Should work the same as the original component\n      let renderFn = EntryComponent(mockHandle, { initialCount: 5 })\n      expect(typeof renderFn).toBe('function')\n\n      if (typeof renderFn === 'function') {\n        let element = renderFn({ label: 'Count' })\n        expect(element).toEqual({\n          $rmx: true,\n          type: 'button',\n          props: {\n            children: ['Count', ': ', 5],\n          },\n          key: undefined,\n        })\n      }\n    })\n  })\n\n  describe('error handling', () => {\n    it('throws error when no module URL provided', () => {\n      function TestComponent() {\n        return () => <div>Test</div>\n      }\n\n      expect(() => {\n        clientEntry('', TestComponent)\n      }).toThrow('clientEntry() requires a module URL')\n    })\n\n    it('throws error when no export name and component is anonymous', () => {\n      let anonymousComponent = function () {\n        return () => <div>Test</div>\n      }\n\n      // Force the function name to be empty to simulate truly anonymous function\n      Object.defineProperty(anonymousComponent, 'name', { value: '' })\n\n      expect(() => {\n        clientEntry('/js/test.js', anonymousComponent)\n      }).toThrow('clientEntry() requires either an export name in the href')\n    })\n\n    it('throws error when no export name and component name is empty', () => {\n      function TestComponent() {\n        return () => <div>Test</div>\n      }\n\n      // Simulate unnamed function\n      Object.defineProperty(TestComponent, 'name', { value: '' })\n\n      expect(() => {\n        clientEntry('/js/test.js', TestComponent)\n      }).toThrow('clientEntry() requires either an export name in the href')\n    })\n  })\n\n  describe('type constraints', () => {\n    it('accepts components with serializable props', () => {\n      // This should compile without errors\n      function ValidComponent(\n        handle: Handle,\n        props: {\n          str: string\n          num: number\n          bool: boolean\n          obj: { nested: string }\n          arr: number[]\n          element: JSX.Element\n        },\n      ) {\n        return () => <div>Valid</div>\n      }\n\n      let EntryComponent = clientEntry('/js/valid.js#ValidComponent', ValidComponent)\n      expect(EntryComponent.$entry).toBe(true)\n    })\n\n    // Type-level rejection: non-serializable props should be disallowed\n    it('rejects components with non-serializable props', () => {\n      function InvalidComponent(handle: Handle, props: { func: () => void }) {\n        return () => <div>Invalid</div>\n      }\n\n      // @ts-expect-error - non-serializable function prop should be rejected\n      let HydratedInvalid = clientEntry('/js/invalid.js#InvalidComponent', InvalidComponent)\n      expect(true).toBe(true)\n    })\n\n    it('accepts primitive setup types', () => {\n      // Setup can be a primitive like number, string, boolean\n      function Counter(handle: Handle, setup: number) {\n        let count = setup ?? 0\n        return () => <div>Count: {count}</div>\n      }\n\n      let EntryCounter = clientEntry('/js/counter.js#Counter', Counter)\n      expect(EntryCounter.$entry).toBe(true)\n    })\n\n    it('accepts null and undefined setup types', () => {\n      function NullSetup(handle: Handle, setup: null) {\n        return () => <div>Null setup</div>\n      }\n\n      function UndefinedSetup(handle: Handle, setup: undefined) {\n        return () => <div>Undefined setup</div>\n      }\n\n      let EntryNull = clientEntry('/js/null.js#NullSetup', NullSetup)\n      let EntryUndefined = clientEntry('/js/undefined.js#UndefinedSetup', UndefinedSetup)\n\n      expect(EntryNull.$entry).toBe(true)\n      expect(EntryUndefined.$entry).toBe(true)\n    })\n\n    it('accepts array setup types', () => {\n      function ArraySetup(handle: Handle, setup: string[]) {\n        return () => <div>{setup.join(', ')}</div>\n      }\n\n      let EntryArray = clientEntry('/js/array.js#ArraySetup', ArraySetup)\n      expect(EntryArray.$entry).toBe(true)\n    })\n  })\n\n  describe('isEntry type guard', () => {\n    it('returns true for entry components', () => {\n      function TestComponent() {\n        return () => <div>Test</div>\n      }\n\n      let EntryComponent = clientEntry('/js/test.js#TestComponent', TestComponent)\n      expect(isEntry(EntryComponent)).toBe(true)\n    })\n\n    it('returns false for regular components', () => {\n      function RegularComponent() {\n        return () => <div>Regular</div>\n      }\n\n      expect(isEntry(RegularComponent)).toBe(false)\n    })\n\n    it('returns false for non-function values', () => {\n      expect(isEntry(null)).toBe(false)\n      expect(isEntry(undefined)).toBe(false)\n      expect(isEntry('string')).toBe(false)\n      expect(isEntry(123)).toBe(false)\n      expect(isEntry({})).toBe(false)\n    })\n\n    it('returns false for functions without entry metadata', () => {\n      function normalFunction() {}\n      expect(isEntry(normalFunction)).toBe(false)\n    })\n  })\n\n  describe('complex components', () => {\n    it('handles stateful components with setup and render phases', () => {\n      function Counter(handle: Handle, setupProps: { initialCount: number }) {\n        let count = setupProps.initialCount\n\n        return (renderProps: { label: string }) => (\n          <button type=\"button\">\n            {renderProps.label} {count}\n          </button>\n        )\n      }\n\n      let EntryCounter = clientEntry('/js/counter.js#Counter', Counter)\n\n      expect(EntryCounter.$entry).toBe(true)\n      expect(EntryCounter.$moduleUrl).toBe('/js/counter.js')\n      expect(EntryCounter.$exportName).toBe('Counter')\n    })\n\n    it('handles simple components that return JSX directly', () => {\n      function SimpleComponent(handle: Handle, props: { message: string }) {\n        return () => <div>{props.message}</div>\n      }\n\n      let EntrySimple = clientEntry('/js/simple.js#SimpleComponent', SimpleComponent)\n\n      expect(EntrySimple.$entry).toBe(true)\n      expect(EntrySimple.$moduleUrl).toBe('/js/simple.js')\n      expect(EntrySimple.$exportName).toBe('SimpleComponent')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/create-element.test.ts",
    "content": "import { describe, it, expect } from 'vitest'\n\nimport { createElement } from '../lib/create-element.ts'\nimport { createMixin } from '../index.ts'\n\ndescribe('createElement', () => {\n  it('creates an element', () => {\n    let element = createElement('div', {}, 'Hello, world!')\n    expect(element.type).toBe('div')\n    expect(element.props.children).toEqual(['Hello, world!'])\n  })\n\n  it('normalizes mix to an array or undefined', () => {\n    let passthrough = createMixin((_handle) => {})\n    let descriptor = passthrough()\n\n    let withSingle = createElement('div', { mix: descriptor })\n    let withArray = createElement('div', { mix: [descriptor] })\n    let withEmptyArray = createElement('div', { mix: [] })\n\n    expect(withSingle.props.mix).toEqual([descriptor])\n    expect(withArray.props.mix).toEqual([descriptor])\n    expect(withEmptyArray.props.mix).toBeUndefined()\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/diff-dom.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { invariant } from '../lib/invariant.ts'\nimport { diffNodes } from '../lib/diff-dom.ts'\n\nfunction diffDom(container: HTMLElement, next: string) {\n  let template = document.createElement('template')\n  template.innerHTML = next\n\n  diffNodes(Array.from(container.childNodes), Array.from(template.content.childNodes), {\n    pendingClientEntries: new Map(),\n  } as any)\n}\n\ndescribe('diffNodes', () => {\n  describe('basic diffing', () => {\n    it('diffs text nodes', () => {\n      let container = document.createElement('div')\n      container.innerHTML = 'Hello, world!'\n      let text = container.firstChild\n      invariant(text)\n\n      diffDom(container, 'Goodbye, world!')\n\n      expect(container.innerHTML).toBe('Goodbye, world!')\n      expect(container.firstChild).toBe(text)\n    })\n\n    it('diffs element nodes', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<div>Hello, world!</div>'\n      let div = container.firstChild\n      invariant(div)\n\n      diffDom(container, '<div>Goodbye, world!</div>')\n\n      expect(container.innerHTML).toBe('<div>Goodbye, world!</div>')\n      expect(container.firstChild).toBe(div)\n    })\n\n    it('diffs element nodes with attributes', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<div id=\"hello\">Hello, world!</div>'\n      let div = container.firstChild\n      invariant(div)\n\n      diffDom(container, '<div id=\"goodbye\">Goodbye, world!</div>')\n\n      expect(container.innerHTML).toBe('<div id=\"goodbye\">Goodbye, world!</div>')\n      expect(container.firstChild).toBe(div)\n    })\n\n    it('diffs children', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<div><span>Hello, world!</span></div>'\n      let div = container.firstChild\n      invariant(div)\n      let span = container.querySelector('span')\n      invariant(span)\n\n      diffDom(container, '<div><span>Goodbye, world!</span></div>')\n\n      expect(container.innerHTML).toBe('<div><span>Goodbye, world!</span></div>')\n      expect(container.firstChild).toBe(div)\n      expect(container.querySelector('span')).toBe(span)\n    })\n\n    it('replaces children elements', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<div><span>Hello, world!</span></div>'\n      let div = container.firstChild\n      invariant(div)\n\n      diffDom(container, '<div><p>Goodbye, world!</p></div>')\n\n      expect(container.innerHTML).toBe('<div><p>Goodbye, world!</p></div>')\n      expect(container.firstChild).toBe(div)\n    })\n  })\n\n  describe('comments', () => {\n    it('retains comments', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<!-- start --><div>hello</div><!-- end -->'\n      let comment = container.firstChild\n      invariant(comment)\n\n      diffDom(container, '<!-- start --><div>goodbye</div><!-- end -->')\n\n      expect(container.innerHTML).toBe('<!-- start --><div>goodbye</div><!-- end -->')\n      expect(container.firstChild).toBe(comment)\n    })\n\n    it('diffs comment data', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<!-- a --><div>hello</div><!-- z -->'\n      let first = container.firstChild\n      let last = container.lastChild\n      invariant(first && last)\n\n      diffDom(container, '<!-- b --><div>hello</div><!-- y -->')\n\n      expect(container.innerHTML).toBe('<!-- b --><div>hello</div><!-- y -->')\n      expect(container.firstChild).toBe(first)\n      expect(container.lastChild).toBe(last)\n    })\n\n    it('updates hydration marker ids while fast-forwarding boundaries', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<!-- rmx:h:old --><button>Old</button><!-- /rmx:h -->'\n\n      diffDom(container, '<!-- rmx:h:new --><button>Old</button><!-- /rmx:h -->')\n\n      let start = container.firstChild\n      invariant(start && start.nodeType === Node.COMMENT_NODE)\n      expect((start as Comment).data.trim()).toBe('rmx:h:new')\n    })\n  })\n\n  describe('keyed diffs', () => {\n    it('retains keyed elements via data-key', () => {\n      let container = document.createElement('div')\n      container.innerHTML =\n        '<ul><li data-key=\"a\">A</li><li data-key=\"b\">B</li><li data-key=\"c\">C</li></ul>'\n      let list = container.querySelector('ul')\n      invariant(list)\n\n      let a = list.children.item(0)\n      let b = list.children.item(1)\n      let c = list.children.item(2)\n      invariant(a && b && c)\n\n      diffDom(\n        container,\n        '<ul><li data-key=\"b\">B</li><li data-key=\"a\">A</li><li data-key=\"c\">C</li></ul>',\n      )\n\n      let updatedList = container.querySelector('ul')\n      invariant(updatedList)\n      expect(updatedList.children.item(0)).toBe(b)\n      expect(updatedList.children.item(1)).toBe(a)\n      expect(updatedList.children.item(2)).toBe(c)\n      expect(updatedList.innerHTML).toBe(\n        '<li data-key=\"b\">B</li><li data-key=\"a\">A</li><li data-key=\"c\">C</li>',\n      )\n    })\n  })\n\n  describe('live browser state', () => {\n    it('preserves current details open state when incoming html removes open', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<details open><summary>Toggle</summary><p>Body</p></details>'\n      let details = container.querySelector('details')\n      invariant(details)\n\n      diffDom(container, '<details><summary>Toggle</summary><p>Body</p></details>')\n\n      expect(details.open).toBe(true)\n      expect(details.hasAttribute('open')).toBe(true)\n    })\n\n    it('preserves current dialog open state when incoming html removes open', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<dialog open>Hello</dialog>'\n      let dialog = container.querySelector('dialog')\n      invariant(dialog)\n\n      diffDom(container, '<dialog>Hello</dialog>')\n\n      expect(dialog.open).toBe(true)\n      expect(dialog.hasAttribute('open')).toBe(true)\n    })\n\n    it('preserves current input checked state when incoming html removes checked', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<input type=\"checkbox\" checked>'\n      let input = container.querySelector('input')\n      invariant(input)\n\n      diffDom(container, '<input type=\"checkbox\">')\n\n      expect(input.checked).toBe(true)\n      expect(input.hasAttribute('checked')).toBe(true)\n    })\n\n    it('preserves current input value when incoming html changes value', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<input value=\"server\">'\n      let input = container.querySelector('input')\n      invariant(input)\n      input.value = 'user'\n\n      diffDom(container, '<input value=\"server-next\">')\n\n      expect(input.value).toBe('user')\n      expect(input.getAttribute('value')).toBe('server')\n    })\n\n    it('preserves current textarea value when incoming html changes its text', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<textarea>server</textarea>'\n      let textarea = container.querySelector('textarea')\n      invariant(textarea)\n      textarea.value = 'user'\n\n      diffDom(container, '<textarea>server-next</textarea>')\n\n      expect(textarea.value).toBe('user')\n      expect(textarea.textContent).toBe('server')\n    })\n\n    it('preserves current select value when incoming html changes selected options', () => {\n      let container = document.createElement('div')\n      container.innerHTML =\n        '<select><option value=\"a\">A</option><option value=\"b\">B</option></select>'\n      let select = container.querySelector('select')\n      invariant(select)\n      let first = select.options.item(0)\n      let second = select.options.item(1)\n      invariant(first && second)\n      select.value = 'b'\n\n      diffDom(\n        container,\n        '<select><option value=\"a\" selected>A</option><option value=\"b\">B</option></select>',\n      )\n\n      expect(select.value).toBe('b')\n      expect(first.selected).toBe(false)\n      expect(second.selected).toBe(true)\n      expect(first.hasAttribute('selected')).toBe(false)\n    })\n\n    it('preserves current popover visibility when incoming html removes popover', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<div popover=\"auto\">Hello</div>'\n      let popover = container.querySelector('div')\n      invariant(popover)\n      document.body.appendChild(container)\n\n      try {\n        expect(typeof popover.showPopover).toBe('function')\n        popover.showPopover()\n\n        diffDom(container, '<div>Hello</div>')\n\n        expect(popover.matches(':popover-open')).toBe(true)\n        expect(popover.getAttribute('popover')).toBe('auto')\n      } finally {\n        container.remove()\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/document-state.test.ts",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { createDocumentState } from '../lib/document-state.ts'\n\ndescribe('document-state', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.removeChild(container)\n  })\n\n  describe('capture and restore after DOM moves', () => {\n    it('restores focus and selection after moving a text input', () => {\n      let input = document.createElement('input')\n      input.type = 'text'\n      input.value = 'Hello World'\n      container.appendChild(input)\n      input.focus()\n      input.setSelectionRange(6, 11)\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Move the node (simulating what happens during DOM updates)\n      // This causes focus/selection to be lost\n      container.removeChild(input)\n      container.appendChild(input)\n      expect(document.activeElement).toBe(document.body)\n\n      state.restore()\n\n      expect(document.activeElement).toBe(input)\n      expect(input.selectionStart).toBe(6)\n      expect(input.selectionEnd).toBe(11)\n    })\n\n    it('restores focus and selection after reordering nodes', () => {\n      let input1 = document.createElement('input')\n      input1.type = 'text'\n      input1.value = 'First'\n      container.appendChild(input1)\n\n      let input2 = document.createElement('input')\n      input2.type = 'text'\n      input2.value = 'Second'\n      container.appendChild(input2)\n\n      let input3 = document.createElement('input')\n      input3.type = 'text'\n      input3.value = 'Third'\n      container.appendChild(input3)\n\n      // Focus the middle input with selection\n      input2.focus()\n      input2.setSelectionRange(0, 6)\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Reorder: move input2 to the end (simulating keyed list reordering)\n      container.removeChild(input2)\n      container.appendChild(input2)\n\n      state.restore()\n\n      expect(document.activeElement).toBe(input2)\n      expect(input2.selectionStart).toBe(0)\n      expect(input2.selectionEnd).toBe(6)\n    })\n\n    it('restores focus and selection after moving textarea', () => {\n      let textarea = document.createElement('textarea')\n      textarea.value = 'Hello\\nWorld\\nTest'\n      container.appendChild(textarea)\n      textarea.focus()\n      textarea.setSelectionRange(6, 11)\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Move the textarea\n      container.removeChild(textarea)\n      container.appendChild(textarea)\n\n      state.restore()\n\n      expect(document.activeElement).toBe(textarea)\n      expect(textarea.selectionStart).toBe(6)\n      expect(textarea.selectionEnd).toBe(11)\n    })\n\n    it('restores focus and selection for different input types after move', () => {\n      let types = ['text', 'search', 'tel', 'url', 'password'] as const\n      for (let type of types) {\n        let input = document.createElement('input')\n        input.type = type\n        input.value = 'test value'\n        container.appendChild(input)\n        input.focus()\n        input.setSelectionRange(0, 4)\n\n        let state = createDocumentState()\n        state.capture()\n\n        // Move the input\n        container.removeChild(input)\n        container.appendChild(input)\n\n        state.restore()\n\n        expect(document.activeElement).toBe(input)\n        expect(input.selectionStart).toBe(0)\n        expect(input.selectionEnd).toBe(4)\n\n        container.removeChild(input)\n      }\n    })\n\n    it('restores focus without selection when element had no selection', () => {\n      let input = document.createElement('input')\n      input.type = 'text'\n      input.value = 'Hello World'\n      container.appendChild(input)\n      input.focus()\n      // No selection set\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Move the input\n      container.removeChild(input)\n      container.appendChild(input)\n\n      state.restore()\n\n      expect(document.activeElement).toBe(input)\n    })\n\n    it('restores focus after moving element (scroll preservation is handled internally)', () => {\n      // The scroll preservation mechanism is tested implicitly through focus restoration\n      // The actual scroll preservation happens internally during restore() to prevent\n      // focus() from changing scroll positions\n      let scrollable = document.createElement('div')\n      scrollable.style.width = '100px'\n      scrollable.style.height = '100px'\n      scrollable.style.overflow = 'auto'\n      let inner = document.createElement('div')\n      inner.style.width = '200px'\n      inner.style.height = '200px'\n      scrollable.appendChild(inner)\n      container.appendChild(scrollable)\n\n      let input = document.createElement('input')\n      input.type = 'text'\n      scrollable.appendChild(input)\n      input.focus()\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Move the input (which would normally lose focus)\n      scrollable.removeChild(input)\n      scrollable.appendChild(input)\n\n      state.restore()\n\n      // Focus should be restored to the moved element\n      expect(document.activeElement).toBe(input)\n    })\n\n    it('restores focus for non-selectable element after move', () => {\n      let div = document.createElement('div')\n      div.tabIndex = -1\n      container.appendChild(div)\n      div.focus()\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Move the div\n      container.removeChild(div)\n      container.appendChild(div)\n\n      state.restore()\n\n      expect(document.activeElement).toBe(div)\n    })\n\n    it('restores focus for contentEditable element after move', () => {\n      let div = document.createElement('div')\n      div.contentEditable = 'true'\n      div.textContent = 'Hello World'\n      container.appendChild(div)\n      div.focus()\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Move the div\n      container.removeChild(div)\n      container.appendChild(div)\n\n      state.restore()\n\n      expect(document.activeElement).toBe(div)\n    })\n\n    it('does not restore if element is removed from document', () => {\n      let input = document.createElement('input')\n      input.type = 'text'\n      input.value = 'Hello World'\n      container.appendChild(input)\n      input.focus()\n      input.setSelectionRange(0, 5)\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Remove element from document (not just moved)\n      container.removeChild(input)\n\n      // Should not throw\n      state.restore()\n\n      // Element should not be focused since it's not in document\n      expect(document.activeElement).not.toBe(input)\n    })\n\n    it('handles selection end beyond value length after move', () => {\n      let input = document.createElement('input')\n      input.type = 'text'\n      input.value = 'Hello'\n      container.appendChild(input)\n      input.focus()\n      input.setSelectionRange(0, 10) // Beyond length\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Move the input\n      container.removeChild(input)\n      container.appendChild(input)\n\n      state.restore()\n\n      expect(document.activeElement).toBe(input)\n      // Should clamp to value length\n      expect(input.selectionEnd).toBeLessThanOrEqual(input.value.length)\n    })\n\n    it('restores focus when element is moved to different parent', () => {\n      let parent1 = document.createElement('div')\n      let parent2 = document.createElement('div')\n      container.appendChild(parent1)\n      container.appendChild(parent2)\n\n      let input = document.createElement('input')\n      input.type = 'text'\n      input.value = 'Hello World'\n      parent1.appendChild(input)\n      input.focus()\n      input.setSelectionRange(0, 5)\n\n      let state = createDocumentState()\n      state.capture()\n\n      // Move to different parent\n      parent1.removeChild(input)\n      parent2.appendChild(input)\n\n      state.restore()\n\n      expect(document.activeElement).toBe(input)\n      expect(input.selectionStart).toBe(0)\n      expect(input.selectionEnd).toBe(5)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/event-listeners.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { addEventListeners, TypedEventTarget, createRoot } from '../index.ts'\nimport type { Dispatched } from '../lib/event-listeners.ts'\nimport type { Assert, Equal } from './utils.ts'\nimport type { Handle } from '../lib/component.ts'\n\ndescribe('addEventListeners', () => {\n  it('adds listeners to an event target', () => {\n    let controller = new AbortController()\n    let clickCount = 0\n\n    addEventListeners(document, controller.signal, {\n      click: () => {\n        clickCount++\n      },\n    })\n\n    document.dispatchEvent(new MouseEvent('click'))\n    expect(clickCount).toBe(1)\n\n    document.dispatchEvent(new MouseEvent('click'))\n    expect(clickCount).toBe(2)\n  })\n\n  it('removes listeners when signal aborts', () => {\n    let controller = new AbortController()\n    let clickCount = 0\n\n    addEventListeners(document, controller.signal, {\n      click: () => {\n        clickCount++\n      },\n    })\n\n    document.dispatchEvent(new MouseEvent('click'))\n    expect(clickCount).toBe(1)\n\n    controller.abort()\n    document.dispatchEvent(new MouseEvent('click'))\n    expect(clickCount).toBe(1)\n  })\n\n  it('works with component handle signal for auto cleanup', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    let clickCount = 0\n\n    function App(handle: Handle) {\n      addEventListeners(document, handle.signal, {\n        click: () => {\n          clickCount++\n        },\n      })\n      return () => <div>App</div>\n    }\n\n    root.render(<App />)\n    root.flush()\n\n    document.dispatchEvent(new MouseEvent('click'))\n    expect(clickCount).toBe(1)\n\n    root.render(null)\n    root.flush()\n\n    document.dispatchEvent(new MouseEvent('click'))\n    expect(clickCount).toBe(1)\n  })\n\n  it('does not pass a re-entry signal to one-argument listeners', () => {\n    let controller = new AbortController()\n    let receivedSignal: AbortSignal | undefined\n\n    addEventListeners(document, controller.signal, {\n      click(event) {\n        void event\n        receivedSignal = arguments[1] as AbortSignal | undefined\n      },\n    })\n\n    document.dispatchEvent(new MouseEvent('click'))\n    expect(receivedSignal).toBeUndefined()\n  })\n\n  it('aborts re-entry signal for two-argument listeners', () => {\n    let controller = new AbortController()\n    let signals: AbortSignal[] = []\n\n    addEventListeners(document, controller.signal, {\n      click(_event, signal) {\n        signals.push(signal)\n      },\n    })\n\n    document.dispatchEvent(new MouseEvent('click'))\n    expect(signals).toHaveLength(1)\n    expect(signals[0]?.aborted).toBe(false)\n\n    document.dispatchEvent(new MouseEvent('click'))\n    expect(signals).toHaveLength(2)\n    expect(signals[0]?.aborted).toBe(true)\n    expect(signals[1]?.aborted).toBe(false)\n\n    controller.abort()\n    expect(signals[1]?.aborted).toBe(true)\n  })\n\n  describe('types', () => {\n    it('provides literal event and target types for document', () => {\n      function App(handle: Handle) {\n        addEventListeners(document, handle.signal, {\n          keydown: (event) => {\n            type test = Assert<Equal<typeof event, Dispatched<KeyboardEvent, Document>>>\n          },\n        })\n        return () => <div>App</div>\n      }\n    })\n\n    it('provides abort signal as required second listener argument', () => {\n      function App(handle: Handle) {\n        addEventListeners(document, handle.signal, {\n          keydown: (event, signal) => {\n            type eventTest = Assert<Equal<typeof event, Dispatched<KeyboardEvent, Document>>>\n            type signalTest = Assert<Equal<typeof signal, AbortSignal>>\n          },\n        })\n        return () => <div>App</div>\n      }\n    })\n\n    it('infers events from TypedEventTarget event map', () => {\n      type PingEventMap = {\n        ping: CustomEvent<{ value: number }>\n      }\n\n      class PingTarget extends TypedEventTarget<PingEventMap> {}\n\n      let target = new PingTarget()\n      let controller = new AbortController()\n\n      addEventListeners(target, controller.signal, {\n        ping: (event) => {\n          type test = Assert<\n            Equal<typeof event, Dispatched<CustomEvent<{ value: number }>, PingTarget>>\n          >\n          void event.detail.value\n        },\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/frame.test.tsx",
    "content": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'\nimport type { Handle } from '../lib/component.ts'\nimport { Frame } from '../lib/component.ts'\nimport { clientEntry } from '../lib/client-entries.ts'\nimport { getTopFrame, run } from '../lib/run.ts'\nimport { createRangeRoot, createRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport { renderToStream } from '../lib/stream.ts'\nimport { css, on } from '../index.ts'\nimport { drain, readChunks, withResolvers } from './utils.ts'\n\nfunction getCommentMarkerId(html: string, prefix: 'rmx:f:' | 'rmx:h:'): string {\n  let re = prefix === 'rmx:f:' ? /<!--\\s*rmx:f:([^ ]+)\\s*-->/ : /<!--\\s*rmx:h:([^ ]+)\\s*-->/\n  let match = html.match(re)\n  invariant(match, `Expected comment marker \"${prefix}\"`)\n  return match[1]!\n}\n\nfunction streamFromChunks(chunks: Array<string | Promise<string>>): ReadableStream<Uint8Array> {\n  let encoder = new TextEncoder()\n  return new ReadableStream<Uint8Array>({\n    async start(controller) {\n      for (let chunk of chunks) {\n        let value = typeof chunk === 'string' ? chunk : await chunk\n        controller.enqueue(encoder.encode(value))\n      }\n      controller.close()\n    },\n  })\n}\n\ndescribe('run', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n    for (let node of Array.from(document.head.childNodes)) {\n      document.head.removeChild(node)\n    }\n  })\n\n  it('hydrates a single component', async () => {\n    let Counter = clientEntry(\n      '/js/counter.js#Counter',\n      function Counter(handle: Handle, setup: number) {\n        let count = setup\n        return () => (\n          <button\n            mix={[\n              on('click', () => {\n                count++\n                handle.update()\n              }),\n            ]}\n          >\n            Count: {count}\n          </button>\n        )\n      },\n    )\n\n    let stream = renderToStream(<Counter setup={5} />)\n    let html = await drain(stream)\n\n    document.body.innerHTML = html\n\n    let loadModule = vi.fn().mockResolvedValue(Counter)\n\n    let frame = run({ loadModule })\n    await frame.ready()\n\n    expect(loadModule).toHaveBeenCalledWith('/js/counter.js', 'Counter')\n\n    let button = document.querySelector('button')\n    expect(button?.textContent).toBe('Count: 5')\n\n    button?.click()\n    frame.flush()\n\n    expect(button?.textContent).toBe('Count: 6')\n\n    frame.dispose()\n  })\n\n  it('forwards hydrated client entry root error events to app listeners', async () => {\n    let error = new Error('hydrated client entry root error')\n    let Broken = clientEntry('/js/broken.js#Broken', function Broken() {\n      return () => <button>Trigger</button>\n    })\n\n    let html = await drain(renderToStream(<Broken />))\n    document.body.innerHTML = html\n\n    let app = run({ loadModule: vi.fn().mockResolvedValue(Broken) })\n    let forwarded: unknown\n    app.addEventListener('error', (event) => {\n      forwarded = (event as ErrorEvent).error\n    })\n\n    await app.ready()\n\n    let marker = Array.from(document.body.childNodes).find(\n      (node): node is Comment & { $rmx: EventTarget } => node instanceof Comment && '$rmx' in node,\n    )\n    invariant(marker, 'Expected hydrated client entry marker')\n    marker.$rmx.dispatchEvent(new ErrorEvent('error', { error }))\n\n    expect(forwarded).toBe(error)\n\n    app.dispose()\n  })\n\n  it('dispatches ready() rejections to app error listeners', async () => {\n    document.body.innerHTML = '<!-- rmx:h:broken --><button>Broken</button>'\n\n    let app = run({ loadModule: vi.fn() })\n    let forwarded: unknown\n    app.addEventListener('error', (event) => {\n      forwarded = event.error\n    })\n\n    let readyError = await app.ready().catch((error) => error)\n\n    expect(readyError).toBeInstanceOf(Error)\n    expect((readyError as Error).message).toBe('End marker not found')\n    expect(forwarded).toBe(readyError)\n\n    app.dispose()\n  })\n\n  it('hydrates multiple components', async () => {\n    let Button = clientEntry('/js/button.js#Button', function Button(handle: Handle) {\n      let clicked = false\n      return ({ text }: { text: string }) => (\n        <button\n          mix={[\n            on('click', () => {\n              clicked = true\n              handle.update()\n            }),\n          ]}\n        >\n          {clicked ? `${text} clicked!` : text}\n        </button>\n      )\n    })\n\n    let stream = renderToStream(\n      <div>\n        <Button text=\"First\" />\n        <Button text=\"Second\" />\n      </div>,\n    )\n    let html = await drain(stream)\n\n    document.body.innerHTML = html\n\n    let loadModule = vi.fn().mockResolvedValue(Button)\n\n    let frame = run({ loadModule })\n    await frame.ready()\n\n    // Module is cached by moduleUrl+exportName\n    expect(loadModule).toHaveBeenCalledTimes(1)\n\n    let buttons = document.querySelectorAll('button')\n    expect(buttons).toHaveLength(2)\n    expect(buttons[0]?.textContent).toBe('First')\n    expect(buttons[1]?.textContent).toBe('Second')\n\n    buttons[0]?.click()\n    frame.flush()\n\n    expect(buttons[0]?.textContent).toBe('First clicked!')\n    expect(buttons[1]?.textContent).toBe('Second')\n\n    frame.dispose()\n  })\n\n  it('removes orphaned hydration end markers after full-document reloads of adjacent client entries', async () => {\n    let FragmentEntry = clientEntry(\n      '/js/fragment-entry.js#FragmentEntry',\n      function FragmentEntry() {\n        return () => <div />\n      },\n    )\n\n    async function renderInitialBody() {\n      return await drain(\n        renderToStream(\n          <div>\n            <FragmentEntry />\n            <FragmentEntry />\n          </div>,\n        ),\n      )\n    }\n\n    async function renderReloadDocument() {\n      return await drain(\n        renderToStream(\n          <html>\n            <body>\n              <div>\n                <FragmentEntry />\n              </div>\n            </body>\n          </html>,\n        ),\n      )\n    }\n\n    document.body.innerHTML = await renderInitialBody()\n\n    let app = run({\n      loadModule: vi.fn().mockResolvedValue(FragmentEntry),\n      async resolveFrame(src: string) {\n        if (src === '/b') return await renderReloadDocument()\n        throw new Error(`Unexpected frame src: ${src}`)\n      },\n    })\n\n    await app.ready()\n\n    let topFrame = getTopFrame()\n\n    topFrame.src = '/b'\n    await topFrame.reload()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    let bodyHtml = document.body.innerHTML\n    let hydrationStarts = bodyHtml.match(/<!--\\s*rmx:h:/g)?.length ?? 0\n    let hydrationEnds = bodyHtml.match(/<!--\\s*\\/rmx:h\\s*-->/g)?.length ?? 0\n\n    expect(hydrationStarts).toBe(hydrationEnds)\n\n    app.dispose()\n  })\n\n  it('hydrates ready modules before slower modules while ready() stays pending', async () => {\n    let Fast = clientEntry('/js/fast.js#Fast', function Fast(handle: Handle) {\n      let clicked = false\n      return () => (\n        <button\n          id=\"fast\"\n          mix={[\n            on('click', () => {\n              clicked = true\n              handle.update()\n            }),\n          ]}\n        >\n          {clicked ? 'Fast!' : 'Fast'}\n        </button>\n      )\n    })\n\n    let Slow = clientEntry('/js/slow.js#Slow', function Slow(handle: Handle) {\n      let clicked = false\n      return () => (\n        <button\n          id=\"slow\"\n          mix={[\n            on('click', () => {\n              clicked = true\n              handle.update()\n            }),\n          ]}\n        >\n          {clicked ? 'Slow!' : 'Slow'}\n        </button>\n      )\n    })\n\n    let html = await drain(\n      renderToStream(\n        <div>\n          <Fast />\n          <Slow />\n        </div>,\n      ),\n    )\n    document.body.innerHTML = html\n\n    let [slowModulePromise, resolveSlowModule] = withResolvers<Function>()\n    let loadModule = vi.fn().mockImplementation((moduleUrl: string, exportName: string) => {\n      if (moduleUrl === '/js/fast.js' && exportName === 'Fast') return Fast\n      if (moduleUrl === '/js/slow.js' && exportName === 'Slow') return slowModulePromise\n      throw new Error(`Unexpected module request: ${moduleUrl}#${exportName}`)\n    })\n\n    let app = run({ loadModule })\n\n    let readySettled = false\n    let readyPromise = app.ready().then(() => {\n      readySettled = true\n    })\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    let fastButton = document.getElementById('fast')\n    let slowButton = document.getElementById('slow')\n    invariant(fastButton instanceof HTMLButtonElement)\n    invariant(slowButton instanceof HTMLButtonElement)\n\n    fastButton.click()\n    app.flush()\n    expect(fastButton.textContent).toBe('Fast!')\n\n    slowButton.click()\n    app.flush()\n    expect(slowButton.textContent).toBe('Slow')\n\n    expect(readySettled).toBe(false)\n\n    resolveSlowModule(Slow)\n    await readyPromise\n\n    slowButton.click()\n    app.flush()\n    expect(slowButton.textContent).toBe('Slow!')\n\n    app.dispose()\n  })\n\n  it('handles complex props', async () => {\n    let Card = clientEntry('/js/card.js#Card', function Card() {\n      return (props: { title: string; count: number; enabled: boolean; items: string[] }) => (\n        <div>\n          <h2>{props.title}</h2>\n          <p>Count: {props.count}</p>\n          <p>Enabled: {String(props.enabled)}</p>\n          <ul>\n            {props.items.map((item, i) => (\n              <li key={i}>{item}</li>\n            ))}\n          </ul>\n        </div>\n      )\n    })\n\n    let stream = renderToStream(\n      <Card title=\"Test\" count={42} enabled={true} items={['one', 'two', 'three']} />,\n    )\n    let html = await drain(stream)\n\n    document.body.innerHTML = html\n\n    let loadModule = vi.fn().mockResolvedValue(Card)\n\n    let frame = run({ loadModule })\n    await frame.ready()\n\n    expect(loadModule).toHaveBeenCalledWith('/js/card.js', 'Card')\n    expect(document.querySelector('h2')?.textContent).toBe('Test')\n    expect(document.querySelector('p')?.textContent).toBe('Count: 42')\n    expect(document.querySelectorAll('li')).toHaveLength(3)\n\n    frame.dispose()\n  })\n\n  it('ready() does not wait for hydration markers from later frame templates', async () => {\n    let Initial = clientEntry('/js/initial.js#Initial', function Initial(handle: Handle) {\n      let clicked = false\n      return () => (\n        <button\n          id=\"initial\"\n          mix={[\n            on('click', () => {\n              clicked = true\n              handle.update()\n            }),\n          ]}\n        >\n          {clicked ? 'Initial!' : 'Initial'}\n        </button>\n      )\n    })\n\n    let Late = clientEntry('/js/late.js#Late', function Late(handle: Handle) {\n      let clicked = false\n      return () => (\n        <button\n          id=\"late\"\n          mix={[\n            on('click', () => {\n              clicked = true\n              handle.update()\n            }),\n          ]}\n        >\n          {clicked ? 'Late!' : 'Late'}\n        </button>\n      )\n    })\n\n    let pageStream = renderToStream(\n      <div>\n        <Initial />\n        <Frame src=\"/late-frame\" fallback={<span id=\"frame-fallback\">Loading…</span>} />\n      </div>,\n      { resolveFrame: () => new Promise<string>(() => {}) },\n    )\n    let pageChunks = readChunks(pageStream)\n    let first = await pageChunks.next()\n    invariant(!first.done)\n    document.body.innerHTML = first.value\n\n    let frameId = getCommentMarkerId(first.value, 'rmx:f:')\n    let [lateModulePromise, resolveLateModule] = withResolvers<Function>()\n\n    let loadModule = vi.fn().mockImplementation((moduleUrl: string, exportName: string) => {\n      if (moduleUrl === '/js/initial.js' && exportName === 'Initial') return Initial\n      if (moduleUrl === '/js/late.js' && exportName === 'Late') return lateModulePromise\n      throw new Error(`Unexpected module request: ${moduleUrl}#${exportName}`)\n    })\n\n    let app = run({ loadModule })\n    await app.ready()\n\n    // Only initial adopted-document markers block ready().\n    expect(loadModule).toHaveBeenCalledTimes(1)\n\n    let template = document.createElement('template')\n    template.id = frameId\n    template.innerHTML = await drain(renderToStream(<Late />))\n    document.body.appendChild(template)\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Late template markers hydrate after ready() and are not part of initial barrier.\n    expect(loadModule).toHaveBeenCalledTimes(2)\n\n    let lateButton = document.getElementById('late')\n    invariant(lateButton instanceof HTMLButtonElement)\n    lateButton.click()\n    app.flush()\n    expect(lateButton.textContent).toBe('Late')\n\n    resolveLateModule(Late)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    lateButton.click()\n    app.flush()\n    expect(lateButton.textContent).toBe('Late!')\n\n    app.dispose()\n  })\n\n  it('does nothing when no rmx-data script exists', async () => {\n    document.body.innerHTML = '<div>No hydration here</div>'\n\n    let loadModule = vi.fn()\n\n    let frame = run({ loadModule })\n    await frame.ready()\n\n    expect(loadModule).not.toHaveBeenCalled()\n\n    frame.dispose()\n  })\n\n  it('does nothing when rmx-data has no hydration data', async () => {\n    document.body.innerHTML = `\n      <div>Static content</div>\n      <script type=\"application/json\" id=\"rmx-data\">{}</script>\n    `\n\n    let loadModule = vi.fn()\n\n    let frame = run({ loadModule })\n    await frame.ready()\n\n    expect(loadModule).not.toHaveBeenCalled()\n\n    frame.dispose()\n  })\n\n  it('adopts existing DOM nodes during hydration', async () => {\n    let Counter = clientEntry('/js/counter.js#Counter', function Counter() {\n      return () => (\n        <div>\n          <span>Static text</span>\n        </div>\n      )\n    })\n\n    let stream = renderToStream(<Counter />)\n    let html = await drain(stream)\n\n    document.body.innerHTML = html\n\n    let existingSpan = document.querySelector('span')\n    expect(existingSpan).toBeTruthy()\n\n    let loadModule = vi.fn().mockResolvedValue(Counter)\n\n    let frame = run({ loadModule })\n    await frame.ready()\n\n    let spanAfterHydration = document.querySelector('span')\n    expect(spanAfterHydration).toBe(existingSpan)\n\n    frame.dispose()\n  })\n\n  it('replaces pending frame regions when streamed templates arrive', async () => {\n    let stream = renderToStream(\n      <div>\n        <h1>Title</h1>\n        <Frame src=\"/x\" fallback={<nav>Loading...</nav>} />\n        <p>Main</p>\n      </div>,\n      { resolveFrame: () => '<nav>Loaded</nav>' },\n    )\n\n    let chunks = readChunks(stream)\n    let first = await chunks.next()\n    invariant(!first.done)\n    document.body.innerHTML = first.value\n\n    let h1 = document.querySelector('h1')\n    let p = document.querySelector('p')\n    let nav = document.querySelector('nav')\n    invariant(h1 && p && nav)\n    expect(nav.textContent).toBe('Loading...')\n\n    let frame = run({ loadModule: vi.fn() })\n    await frame.ready()\n\n    let second = await chunks.next()\n    invariant(!second.done)\n    document.body.insertAdjacentHTML('beforeend', second.value)\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.querySelector('h1')).toBe(h1)\n    expect(document.querySelector('p')).toBe(p)\n    expect(document.querySelector('nav')).toBe(nav)\n    expect(nav.textContent).toBe('Loaded')\n\n    frame.dispose()\n  })\n\n  it('merges hydration data across multiple rmx-data scripts', async () => {\n    function A(handle: Handle) {\n      let clicked = false\n      return () => (\n        <button\n          id=\"a\"\n          mix={[\n            on('click', () => {\n              clicked = true\n              handle.update()\n            }),\n          ]}\n        >\n          {clicked ? 'A!' : 'A'}\n        </button>\n      )\n    }\n\n    function B(handle: Handle) {\n      let clicked = false\n      return () => (\n        <button\n          id=\"b\"\n          mix={[\n            on('click', () => {\n              clicked = true\n              handle.update()\n            }),\n          ]}\n        >\n          {clicked ? 'B!' : 'B'}\n        </button>\n      )\n    }\n\n    document.body.innerHTML = `\n      <!-- rmx:h:h1 --><button id=\"a\">A</button><!-- /rmx:h -->\n      <!-- rmx:h:h2 --><button id=\"b\">B</button><!-- /rmx:h -->\n      <script type=\"application/json\" id=\"rmx-data\">\n        {\"h\":{\"h1\":{\"moduleUrl\":\"/a.js\",\"exportName\":\"A\",\"props\":{}}}}\n      </script>\n      <script type=\"application/json\" id=\"rmx-data\">\n        {\"h\":{\"h2\":{\"moduleUrl\":\"/b.js\",\"exportName\":\"B\",\"props\":{}}}}\n      </script>\n    `\n\n    let loadModule = vi.fn().mockImplementation((moduleUrl: string, exportName: string) => {\n      if (moduleUrl === '/a.js' && exportName === 'A') return A\n      if (moduleUrl === '/b.js' && exportName === 'B') return B\n      throw new Error(`Unexpected module request: ${moduleUrl}#${exportName}`)\n    })\n\n    let frame = run({ loadModule })\n    await frame.ready()\n\n    expect(loadModule).toHaveBeenCalledTimes(2)\n\n    let a = document.getElementById('a')\n    let b = document.getElementById('b')\n    invariant(a instanceof HTMLButtonElement)\n    invariant(b instanceof HTMLButtonElement)\n\n    expect(a.textContent).toBe('A')\n    expect(b.textContent).toBe('B')\n\n    a.click()\n    b.click()\n    frame.flush()\n\n    expect(a.textContent).toBe('A!')\n    expect(b.textContent).toBe('B!')\n\n    frame.dispose()\n  })\n\n  it('ignores prototype-polluting keys when merging rmx-data scripts', async () => {\n    function A() {\n      return () => <button id=\"a\">A</button>\n    }\n\n    document.body.innerHTML = `\n      <!-- rmx:h:h1 --><button id=\"a\">A</button><!-- /rmx:h -->\n      <!-- rmx:h:h2 --><button id=\"b\">B</button><!-- /rmx:h -->\n      <script type=\"application/json\" id=\"rmx-data\">\n        {\"h\":{\"__proto__\":{\"h2\":{\"moduleUrl\":\"/evil.js\",\"exportName\":\"Evil\",\"props\":{}}}}}\n      </script>\n      <script type=\"application/json\" id=\"rmx-data\">\n        {\"h\":{\"h1\":{\"moduleUrl\":\"/a.js\",\"exportName\":\"A\",\"props\":{}}}}\n      </script>\n    `\n\n    let loadModule = vi.fn().mockImplementation((moduleUrl: string, exportName: string) => {\n      if (moduleUrl === '/a.js' && exportName === 'A') return A\n      throw new Error(`Unexpected module request: ${moduleUrl}#${exportName}`)\n    })\n\n    let frame = run({ loadModule })\n    await frame.ready()\n\n    expect(loadModule).toHaveBeenCalledTimes(1)\n    expect(loadModule).toHaveBeenCalledWith('/a.js', 'A')\n\n    frame.dispose()\n  })\n\n  it('reloads a frame region and preserves static DOM nodes', async () => {\n    let renderCount = 0\n\n    let reload: undefined | (() => Promise<AbortSignal>)\n\n    let ReloadButton = clientEntry('/assets/reload.js#Reload', function Reload(handle: Handle) {\n      reload = () => handle.frame.reload()\n      return () => <button>Reload</button>\n    })\n\n    async function renderTimeFragment() {\n      renderCount++\n      let stream = renderToStream(\n        <section>\n          <h2>Activity</h2>\n          <p>Server: {renderCount}</p>\n          <ul>\n            <li>First</li>\n            <li>Second</li>\n          </ul>\n          <ReloadButton />\n        </section>,\n        {\n          onError(error) {\n            console.error(error)\n          },\n        },\n      )\n      return await drain(stream)\n    }\n\n    let stream = renderToStream(\n      <main>\n        <Frame src=\"/time\" fallback={<div>Loading…</div>} />\n      </main>,\n      { resolveFrame: renderTimeFragment },\n    )\n\n    let html = await drain(stream)\n    document.body.innerHTML = html\n\n    // Ensure template exists so the pending frame can render immediately.\n    let frameId = getCommentMarkerId(html, 'rmx:f:')\n    expect(document.querySelector(`template#${frameId}`)).toBeTruthy()\n\n    let clientFrame = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/reload.js' && exportName === 'Reload') return ReloadButton\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame: renderTimeFragment,\n    })\n\n    await clientFrame.ready()\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.querySelector('p')?.textContent).toBe('Server: 1')\n    invariant(reload)\n\n    // Capture references to every element before reload.\n    let section = document.querySelector('section')\n    let heading = document.querySelector('h2')\n    let paragraph = document.querySelector('p')\n    let list = document.querySelector('ul')\n    let items = document.querySelectorAll('li')\n    let button = document.querySelector('button')\n    invariant(section && heading && paragraph && list && button)\n    invariant(items.length === 2)\n\n    await reload()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Dynamic text updated.\n    expect(document.querySelector('p')?.textContent).toBe('Server: 2')\n\n    // Static elements are the exact same DOM nodes — not replaced.\n    expect(document.querySelector('section')).toBe(section)\n    expect(document.querySelector('h2')).toBe(heading)\n    expect(document.querySelector('p')).toBe(paragraph)\n    expect(document.querySelector('ul')).toBe(list)\n    expect(document.querySelectorAll('li')[0]).toBe(items[0])\n    expect(document.querySelectorAll('li')[1]).toBe(items[1])\n    expect(document.querySelector('button')).toBe(button)\n\n    // Static text preserved.\n    expect(heading.textContent).toBe('Activity')\n    expect(items[0].textContent).toBe('First')\n    expect(items[1].textContent).toBe('Second')\n\n    clientFrame.dispose()\n  })\n\n  it('clears frame content when reload resolves to an empty stream', async () => {\n    let reload: undefined | (() => Promise<AbortSignal>)\n\n    let ReloadButton = clientEntry(\n      '/assets/reload-empty.js#ReloadEmpty',\n      function ReloadEmpty(handle: Handle) {\n        reload = () => handle.frame.reload()\n        return () => <button id=\"reload-empty\">Reload empty</button>\n      },\n    )\n\n    async function renderInitial(): Promise<string> {\n      return await drain(\n        renderToStream(\n          <section id=\"frame-content\">\n            <p id=\"frame-value\">Initial content</p>\n            <ReloadButton />\n          </section>,\n        ),\n      )\n    }\n\n    let html = await drain(\n      renderToStream(\n        <main>\n          <Frame src=\"/reload-empty\" fallback={<div>Loading…</div>} />\n        </main>,\n        { resolveFrame: renderInitial },\n      ),\n    )\n    document.body.innerHTML = html\n\n    let clientFrame = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/reload-empty.js' && exportName === 'ReloadEmpty') {\n          return ReloadButton\n        }\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame(src: string) {\n        if (src !== '/reload-empty') throw new Error(`Unexpected frame src: ${src}`)\n        return new ReadableStream<Uint8Array>({\n          start(controller) {\n            controller.close()\n          },\n        })\n      },\n    })\n\n    await clientFrame.ready()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(document.getElementById('frame-value')?.textContent).toBe('Initial content')\n\n    invariant(reload)\n    await reload()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.getElementById('frame-content')).toBeNull()\n    expect(document.getElementById('frame-value')).toBeNull()\n\n    clientFrame.dispose()\n  })\n\n  it('looks up named adjacent frames from handle.frames.get(name)', async () => {\n    let summaryRenderCount = 0\n    let reloadSummary: undefined | (() => Promise<void>)\n\n    let RowAction = clientEntry(\n      '/assets/row-action.js#RowAction',\n      function RowAction(handle: Handle) {\n        reloadSummary = async () => {\n          expect(handle.frames.get('missing-frame')).toBeUndefined()\n          await handle.frames.get('cart-summary')?.reload()\n        }\n        return () => <button id=\"row-action\">Update</button>\n      },\n    )\n\n    async function resolveFrame(src: string) {\n      if (src === '/summary') {\n        summaryRenderCount++\n        let stream = renderToStream(<p id=\"summary\">Summary: {summaryRenderCount}</p>)\n        return await drain(stream)\n      }\n      if (src === '/row') {\n        let stream = renderToStream(<RowAction />)\n        return await drain(stream)\n      }\n      return '<p>Unexpected frame</p>'\n    }\n\n    let stream = renderToStream(\n      <main>\n        <Frame name=\"cart-summary\" src=\"/summary\" fallback={<div>Loading summary…</div>} />\n        <Frame src=\"/row\" fallback={<div>Loading row…</div>} />\n      </main>,\n      { resolveFrame },\n    )\n\n    let html = await drain(stream)\n    document.body.innerHTML = html\n\n    let clientFrame = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/row-action.js' && exportName === 'RowAction') return RowAction\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame,\n    })\n\n    await clientFrame.ready()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.querySelector('#summary')?.textContent).toBe('Summary: 1')\n\n    invariant(reloadSummary)\n    await reloadSummary()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.querySelector('#summary')?.textContent).toBe('Summary: 2')\n\n    clientFrame.dispose()\n  })\n\n  it('exposes the root frame as handle.frames.top', async () => {\n    let assertTopFrame: undefined | (() => void)\n\n    let ReloadTop = clientEntry(\n      '/assets/reload-top.js#ReloadTop',\n      function ReloadTop(handle: Handle) {\n        assertTopFrame = () => {\n          expect(handle.frames.top).not.toBe(handle.frame)\n        }\n        return () => <button id=\"reload-top\">Check top frame</button>\n      },\n    )\n\n    async function renderInner() {\n      let stream = renderToStream(<ReloadTop />)\n      return await drain(stream)\n    }\n    document.body.innerHTML = await drain(\n      renderToStream(\n        <main>\n          <Frame src=\"/inner\" />\n        </main>,\n        {\n          resolveFrame(src: string) {\n            if (src === '/inner') return renderInner()\n            throw new Error(`Unexpected page frame src: ${src}`)\n          },\n        },\n      ),\n    )\n\n    let app = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/reload-top.js' && exportName === 'ReloadTop') {\n          return ReloadTop\n        }\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      async resolveFrame(src: string) {\n        if (src === '/inner') {\n          return await renderInner()\n        }\n        throw new Error(`Unexpected frame src: ${src}`)\n      },\n    })\n\n    await app.ready()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    invariant(assertTopFrame)\n    assertTopFrame()\n    app.dispose()\n  })\n\n  it('dispatches reloadStart and reloadComplete events for handle.frame and handle.frames.get(name)', async () => {\n    let summaryReloadStartEvents = 0\n    let rowReloadStartEvents = 0\n    let summaryReloadCompleteEvents = 0\n    let rowReloadCompleteEvents = 0\n    let triggerReloads: undefined | (() => Promise<void>)\n    let summaryRenderCount = 0\n\n    let RowAction = clientEntry(\n      '/assets/reload-events.js#ReloadEvents',\n      function ReloadEvents(handle: Handle) {\n        triggerReloads = async () => {\n          let summaryFrame = handle.frames.get('cart-summary')\n          invariant(summaryFrame)\n          summaryFrame.addEventListener(\n            'reloadStart',\n            () => {\n              summaryReloadStartEvents++\n            },\n            { once: true },\n          )\n          summaryFrame.addEventListener(\n            'reloadComplete',\n            () => {\n              summaryReloadCompleteEvents++\n            },\n            { once: true },\n          )\n          handle.frame.addEventListener(\n            'reloadStart',\n            () => {\n              rowReloadStartEvents++\n            },\n            { once: true },\n          )\n          handle.frame.addEventListener(\n            'reloadComplete',\n            () => {\n              rowReloadCompleteEvents++\n            },\n            { once: true },\n          )\n          await Promise.all([summaryFrame.reload(), handle.frame.reload()])\n        }\n\n        return () => <button id=\"reload-events\">Reload events</button>\n      },\n    )\n\n    async function resolveFrame(src: string) {\n      if (src === '/summary') {\n        summaryRenderCount++\n        return await drain(renderToStream(<p id=\"summary-events\">Summary: {summaryRenderCount}</p>))\n      }\n      if (src === '/row') {\n        return await drain(renderToStream(<RowAction />))\n      }\n      throw new Error(`Unexpected frame src: ${src}`)\n    }\n\n    let html = await drain(\n      renderToStream(\n        <main>\n          <Frame name=\"cart-summary\" src=\"/summary\" fallback={<div>Loading summary…</div>} />\n          <Frame src=\"/row\" fallback={<div>Loading row…</div>} />\n        </main>,\n        { resolveFrame },\n      ),\n    )\n    document.body.innerHTML = html\n\n    let app = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/reload-events.js' && exportName === 'ReloadEvents') {\n          return RowAction\n        }\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame,\n    })\n\n    await app.ready()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.getElementById('summary-events')?.textContent).toBe('Summary: 1')\n\n    invariant(triggerReloads)\n    await triggerReloads()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(summaryReloadStartEvents).toBe(1)\n    expect(rowReloadStartEvents).toBe(1)\n    expect(summaryReloadCompleteEvents).toBe(1)\n    expect(rowReloadCompleteEvents).toBe(1)\n    expect(document.getElementById('summary-events')?.textContent).toBe('Summary: 2')\n\n    app.dispose()\n  })\n\n  it('reloads a frame region when the response uses css mixins', async () => {\n    let renderCount = 0\n\n    let reload: undefined | (() => Promise<AbortSignal>)\n\n    let ReloadButton = clientEntry(\n      '/assets/reload-css.js#ReloadCss',\n      function ReloadCss(handle: Handle) {\n        reload = () => handle.frame.reload()\n        return () => <button mix={[css({ color: '#fff' })]}>Reload</button>\n      },\n    )\n\n    async function renderTimeFragmentWithCss() {\n      renderCount++\n      let stream = renderToStream(\n        <section mix={[css({ padding: 8 })]}>\n          <p mix={[css({ margin: 0 })]}>Server: {renderCount}</p>\n          <ReloadButton />\n        </section>,\n        {\n          onError(error) {\n            console.error(error)\n          },\n        },\n      )\n      return await drain(stream)\n    }\n\n    let stream = renderToStream(\n      <main mix={[css({ color: '#0bf' })]}>\n        <Frame src=\"/time-css\" fallback={<div>Loading…</div>} />\n      </main>,\n      { resolveFrame: renderTimeFragmentWithCss },\n    )\n\n    let html = await drain(stream)\n    document.body.innerHTML = html\n\n    let frameId = getCommentMarkerId(html, 'rmx:f:')\n    expect(document.querySelector(`template#${frameId}`)).toBeTruthy()\n\n    let clientFrame = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/reload-css.js' && exportName === 'ReloadCss') return ReloadButton\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame: renderTimeFragmentWithCss,\n    })\n\n    await clientFrame.ready()\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.querySelector('p')?.textContent).toBe('Server: 1')\n    invariant(reload)\n\n    let section = document.querySelector('section')\n    let paragraph = document.querySelector('p')\n    let button = document.querySelector('button')\n    invariant(section && paragraph && button)\n\n    await reload()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(document.querySelector('p')?.textContent).toBe('Server: 2')\n\n    // Regression guard: reload should preserve node identity even with css-prop styles.\n    expect(document.querySelector('section')).toBe(section)\n    expect(document.querySelector('p')).toBe(paragraph)\n    expect(document.querySelector('button')).toBe(button)\n\n    clientFrame.dispose()\n  })\n\n  it('dispatches reload rejections to app error listeners', async () => {\n    let reload: undefined | (() => Promise<AbortSignal>)\n    let reloadError = new TypeError('Failed to fetch')\n    let renderCount = 0\n\n    let ReloadButton = clientEntry(\n      '/assets/reload-error.js#ReloadError',\n      function ReloadError(handle: Handle) {\n        reload = () => handle.frame.reload()\n        return () => <button id=\"reload-error\">Reload error</button>\n      },\n    )\n\n    async function resolveFrame(src: string) {\n      if (src !== '/reload-error') throw new Error(`Unexpected frame src: ${src}`)\n      renderCount++\n      if (renderCount === 1) {\n        return await drain(\n          renderToStream(\n            <section>\n              <p id=\"reload-error-value\">Initial</p>\n              <ReloadButton />\n            </section>,\n          ),\n        )\n      }\n      throw reloadError\n    }\n\n    let html = await drain(\n      renderToStream(\n        <main>\n          <Frame src=\"/reload-error\" fallback={<div>Loading…</div>} />\n        </main>,\n        { resolveFrame },\n      ),\n    )\n    document.body.innerHTML = html\n\n    let app = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/reload-error.js' && exportName === 'ReloadError') {\n          return ReloadButton\n        }\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame,\n    })\n    let forwarded: unknown\n    app.addEventListener('error', (event) => {\n      forwarded = event.error\n    })\n\n    await app.ready()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    invariant(reload)\n\n    let caught = await reload().catch((error) => error)\n\n    expect(caught).toBe(reloadError)\n    expect(forwarded).toBe(reloadError)\n    expect(document.getElementById('reload-error-value')?.textContent).toBe('Initial')\n\n    app.dispose()\n  })\n\n  it('aborts stale frame reloads when reload is re-entered', async () => {\n    let reload: undefined | (() => Promise<AbortSignal>)\n    let callCount = 0\n    let firstSignal: AbortSignal | undefined\n    let secondSignal: AbortSignal | undefined\n    let [firstReloadContent, resolveFirstReloadContent] = withResolvers<string>()\n    let [secondReloadContent, resolveSecondReloadContent] = withResolvers<string>()\n\n    let ReloadButton = clientEntry(\n      '/assets/reload-abort.js#ReloadAbort',\n      function ReloadAbort(handle: Handle) {\n        reload = () => handle.frame.reload()\n        return () => <button id=\"reload-abort\">Reload abort</button>\n      },\n    )\n\n    async function renderInitial() {\n      return await drain(\n        renderToStream(\n          <section>\n            <p id=\"reload-value\">Initial</p>\n            <ReloadButton />\n          </section>,\n        ),\n      )\n    }\n\n    let html = await drain(\n      renderToStream(\n        <main>\n          <Frame src=\"/reload-abort\" fallback={<div>Loading…</div>} />\n        </main>,\n        { resolveFrame: renderInitial },\n      ),\n    )\n    document.body.innerHTML = html\n\n    let clientFrame = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/reload-abort.js' && exportName === 'ReloadAbort') {\n          return ReloadButton\n        }\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame(src: string, signal?: AbortSignal) {\n        if (src !== '/reload-abort') throw new Error(`Unexpected frame src: ${src}`)\n        callCount++\n        if (callCount === 1) {\n          firstSignal = signal\n          return firstReloadContent\n        }\n        if (callCount === 2) {\n          secondSignal = signal\n          return secondReloadContent\n        }\n        throw new Error(`Unexpected reload call count: ${callCount}`)\n      },\n    })\n\n    await clientFrame.ready()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    invariant(reload)\n\n    let firstReloadPromise = reload()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(firstSignal?.aborted).toBe(false)\n\n    let secondReloadPromise = reload()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(firstSignal?.aborted).toBe(true)\n    expect(secondSignal?.aborted).toBe(false)\n\n    resolveFirstReloadContent('<section><p id=\"reload-value\">Stale</p></section>')\n    let firstReturnedSignal = await firstReloadPromise\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(firstReturnedSignal).toBe(firstSignal)\n    expect(firstReturnedSignal.aborted).toBe(true)\n\n    // First reload should be ignored because it was superseded.\n    expect(document.getElementById('reload-value')?.textContent).toBe('Initial')\n\n    resolveSecondReloadContent('<section><p id=\"reload-value\">Fresh</p></section>')\n    let secondReturnedSignal = await secondReloadPromise\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(secondReturnedSignal).toBe(secondSignal)\n    expect(secondReturnedSignal.aborted).toBe(false)\n\n    expect(document.getElementById('reload-value')?.textContent).toBe('Fresh')\n    clientFrame.dispose()\n  })\n\n  it('keeps head-like elements inside the frame when a frame reloads', async () => {\n    let renderCount = 0\n    let reload: undefined | (() => Promise<AbortSignal>)\n\n    let ReloadButton = clientEntry(\n      '/assets/reload-head.js#ReloadHead',\n      function ReloadHead(handle: Handle) {\n        reload = () => handle.frame.reload()\n        return () => <button id=\"reload-head\">Reload head</button>\n      },\n    )\n\n    async function renderHeadFragment() {\n      renderCount++\n      let stream = renderToStream(\n        <>\n          <title>Frame title {renderCount}</title>\n          <meta name=\"frame-description\" content={`frame-${renderCount}`} />\n          <script type=\"application/ld+json\">{`{\"count\":${renderCount}}`}</script>\n          <script type=\"text/javascript\">{`window.__frameRegular = ${renderCount}`}</script>\n          <section>\n            <p>Frame body {renderCount}</p>\n            <ReloadButton />\n          </section>\n        </>,\n      )\n\n      return await drain(stream)\n    }\n\n    let stream = renderToStream(\n      <main>\n        <Frame src=\"/head-frame\" fallback={<div>Loading…</div>} />\n      </main>,\n      { resolveFrame: renderHeadFragment },\n    )\n\n    let html = await drain(stream)\n    document.body.innerHTML = html\n\n    let frameId = getCommentMarkerId(html, 'rmx:f:')\n    expect(document.querySelector(`template#${frameId}`)).toBeTruthy()\n\n    let clientFrame = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/reload-head.js' && exportName === 'ReloadHead')\n          return ReloadButton\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame: renderHeadFragment,\n    })\n\n    await clientFrame.ready()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    let main = document.querySelector('main')\n    invariant(main)\n    expect(main.querySelector('title')?.textContent).toBe('Frame title 1')\n    expect(main.querySelector('meta[name=\"frame-description\"]')?.getAttribute('content')).toBe(\n      'frame-1',\n    )\n    expect(main.querySelector('script[type=\"application/ld+json\"]')?.textContent).toBe(\n      '{\"count\":1}',\n    )\n    expect(main.querySelector('script[type=\"text/javascript\"]')?.textContent).toBe(\n      'window.__frameRegular = 1',\n    )\n    expect(document.head.querySelector('title')).toBeNull()\n    expect(document.head.querySelector('meta[name=\"frame-description\"]')).toBeNull()\n    expect(document.head.querySelector('script[type=\"application/ld+json\"]')).toBeNull()\n\n    invariant(reload)\n    await reload()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    let titles = main.querySelectorAll('title')\n    expect(titles).toHaveLength(1)\n    expect(titles[0]?.textContent).toBe('Frame title 2')\n\n    let metas = main.querySelectorAll('meta[name=\"frame-description\"]')\n    expect(metas).toHaveLength(1)\n    expect(metas[0]?.getAttribute('content')).toBe('frame-2')\n\n    let ldJsonScripts = main.querySelectorAll('script[type=\"application/ld+json\"]')\n    expect(ldJsonScripts).toHaveLength(1)\n    expect(ldJsonScripts[0]?.textContent).toBe('{\"count\":2}')\n    expect(main.querySelector('script[type=\"text/javascript\"]')?.textContent).toBe(\n      'window.__frameRegular = 2',\n    )\n    expect(document.querySelector('p')?.textContent).toBe('Frame body 2')\n\n    clientFrame.dispose()\n  })\n\n  it('hydrates client entries in blocking frame content without redefining virtual roots', async () => {\n    let Counter = clientEntry('/js/counter.js#Counter', function Counter() {\n      return () => (\n        <button id=\"counter\" type=\"button\">\n          Count\n        </button>\n      )\n    })\n\n    async function renderInner(): Promise<string> {\n      return await drain(renderToStream(<Counter />))\n    }\n\n    let stream = renderToStream(\n      <main>\n        <Frame src=\"/inner\" />\n      </main>,\n      { resolveFrame: renderInner },\n    )\n\n    let html = await drain(stream)\n    document.body.innerHTML = html\n\n    let [modulePromise, resolveModule] = withResolvers<Function>()\n    let loadModule = vi.fn().mockImplementation(async () => modulePromise)\n    let clientFrame = run({ loadModule })\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    resolveModule(Counter)\n\n    await expect(clientFrame.ready()).resolves.toBeUndefined()\n\n    let button = document.getElementById('counter') as HTMLButtonElement | null\n    invariant(button)\n    expect(button.textContent).toContain('Count')\n    expect(loadModule).toHaveBeenCalled()\n\n    clientFrame.dispose()\n  })\n\n  it('hydrates components without waiting for pending frames', async () => {\n    let Counter = clientEntry(\n      '/js/counter.js#Counter',\n      function Counter(handle: Handle, setup: number) {\n        let count = setup\n        return () => (\n          <button\n            id=\"counter\"\n            mix={[\n              on('click', () => {\n                count++\n                handle.update()\n              }),\n            ]}\n          >\n            Count: {count}\n          </button>\n        )\n      },\n    )\n\n    let [framePromise, resolveFramePromise] = withResolvers<string>()\n\n    let stream = renderToStream(\n      <div>\n        <Counter setup={0} />\n        <Frame src=\"/slow\" fallback={<span id=\"frame\">Loading…</span>} />\n      </div>,\n      { resolveFrame: () => framePromise },\n    )\n\n    // Get first chunk only (fallback + counter HTML).\n    let chunks = readChunks(stream)\n    let first = await chunks.next()\n    invariant(!first.done)\n    document.body.innerHTML = first.value\n\n    // Frame shows fallback.\n    expect(document.getElementById('frame')!.textContent).toBe('Loading…')\n\n    let clientFrame = run({\n      loadModule: vi.fn().mockResolvedValue(Counter),\n    })\n    await clientFrame.ready()\n\n    // Counter is hydrated and interactive BEFORE frame resolves.\n    let button = document.getElementById('counter') as HTMLButtonElement\n    expect(button.textContent).toBe('Count: 0')\n    button.click()\n    clientFrame.flush()\n    expect(button.textContent).toBe('Count: 1')\n\n    // Frame still shows fallback.\n    expect(document.getElementById('frame')!.textContent).toBe('Loading…')\n\n    // Now resolve the frame and inject the template.\n    resolveFramePromise('<span id=\"frame\">Loaded!</span>')\n    let second = await chunks.next()\n    invariant(!second.done)\n    document.body.insertAdjacentHTML('beforeend', second.value)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Frame is now rendered.\n    expect(document.getElementById('frame')!.textContent).toBe('Loaded!')\n\n    // Counter still works.\n    button.click()\n    clientFrame.flush()\n    expect(button.textContent).toBe('Count: 2')\n\n    clientFrame.dispose()\n  })\n\n  it('pending frames resolve independently as their templates arrive', async () => {\n    let [fastPromise, resolveFast] = withResolvers<string>()\n    let [slowPromise, resolveSlow] = withResolvers<string>()\n\n    let stream = renderToStream(\n      <div>\n        <Frame src=\"/fast\" fallback={<span id=\"fast\">Loading fast…</span>} />\n        <Frame src=\"/slow\" fallback={<span id=\"slow\">Loading slow…</span>} />\n      </div>,\n      {\n        resolveFrame(src: string) {\n          if (src === '/fast') return fastPromise\n          if (src === '/slow') return slowPromise\n          throw new Error(`Unexpected frame src: ${src}`)\n        },\n      },\n    )\n\n    // Get the first chunk (both fallbacks).\n    let chunks = readChunks(stream)\n    let first = await chunks.next()\n    invariant(!first.done)\n    document.body.innerHTML = first.value\n\n    expect(document.getElementById('fast')!.textContent).toBe('Loading fast…')\n    expect(document.getElementById('slow')!.textContent).toBe('Loading slow…')\n\n    let clientFrame = run({ loadModule: vi.fn() })\n    await clientFrame.ready()\n\n    // Resolve the fast frame first.\n    resolveFast('<span id=\"fast\">Fast loaded</span>')\n    let second = await chunks.next()\n    invariant(!second.done)\n    document.body.insertAdjacentHTML('beforeend', second.value)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Fast frame is rendered; slow frame still shows fallback.\n    expect(document.getElementById('fast')!.textContent).toBe('Fast loaded')\n    expect(document.getElementById('slow')!.textContent).toBe('Loading slow…')\n\n    // Now resolve the slow frame.\n    resolveSlow('<span id=\"slow\">Slow loaded</span>')\n    let third = await chunks.next()\n    invariant(!third.done)\n    document.body.insertAdjacentHTML('beforeend', third.value)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Both frames are now rendered.\n    expect(document.getElementById('fast')!.textContent).toBe('Fast loaded')\n    expect(document.getElementById('slow')!.textContent).toBe('Slow loaded')\n\n    clientFrame.dispose()\n  })\n\n  it('pending frames resolve while modules are still loading', async () => {\n    // Uses manual HTML because renderToStream + readChunks with a deferred\n    // loadModule has a timing issue in the Chromium test environment where\n    // the hydration markers aren't found by the tree walker.\n    let [modulePromise, resolveModule] = withResolvers<Function>()\n    let moduleLoaded = false\n\n    function Counter() {\n      return () => <button id=\"counter\">Counter</button>\n    }\n\n    document.body.innerHTML =\n      '<div>' +\n      '<!-- rmx:h:h1 --><button id=\"counter\">Counter</button><!-- /rmx:h -->' +\n      '<!-- rmx:f:f1 --><span id=\"frame\">Loading…</span><!-- /rmx:f -->' +\n      '</div>' +\n      '<script type=\"application/json\" id=\"rmx-data\">' +\n      '{\"h\":{\"h1\":{\"moduleUrl\":\"/counter.js\",\"exportName\":\"Counter\",\"props\":{}}},' +\n      '\"f\":{\"f1\":{\"status\":\"pending\",\"src\":\"/slow\"}}}' +\n      '</script>'\n\n    let loadModuleFn = vi.fn().mockImplementation(async () => {\n      let mod = await modulePromise\n      moduleLoaded = true\n      return mod\n    })\n\n    let clientFrame = run({ loadModule: loadModuleFn })\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // loadModule must have been called (hydration marker was found).\n    expect(loadModuleFn).toHaveBeenCalled()\n    expect(moduleLoaded).toBe(false)\n\n    // Frame still shows fallback (template hasn't arrived).\n    expect(document.getElementById('frame')!.textContent).toBe('Loading…')\n\n    // Simulate frame template arriving via MutationObserver.\n    let template = document.createElement('template')\n    template.id = 'f1'\n    template.innerHTML = '<span id=\"frame\">Loaded!</span>'\n    document.body.appendChild(template)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Frame rendered even though module hasn't loaded yet.\n    expect(document.getElementById('frame')!.textContent).toBe('Loaded!')\n    expect(moduleLoaded).toBe(false)\n\n    // Now resolve the module and let hydration complete.\n    resolveModule(Counter)\n    await clientFrame.ready()\n\n    expect(moduleLoaded).toBe(true)\n\n    clientFrame.dispose()\n  })\n\n  it('hydrates a component inside a nested frame', async () => {\n    let Counter = clientEntry(\n      '/js/counter.js#Counter',\n      function Counter(handle: Handle, setup: number) {\n        let count = setup\n        return () => (\n          <button\n            id=\"nested-counter\"\n            mix={[\n              on('click', () => {\n                count++\n                handle.update()\n              }),\n            ]}\n          >\n            Count: {count}\n          </button>\n        )\n      },\n    )\n\n    // Use renderToStream to produce proper HTML for each level.\n    let neverResolve = () => new Promise<string>(() => {})\n\n    // Render inner frame content (hydrated Counter).\n    let innerContent = await drain(renderToStream(<Counter setup={10} />))\n\n    // Render outer frame content (pending inner frame with fallback).\n    let outerStream = renderToStream(\n      <div>\n        <Frame src=\"/inner\" fallback={<span id=\"inner\">Loading inner…</span>} />\n      </div>,\n      { resolveFrame: neverResolve },\n    )\n    let outerChunks = readChunks(outerStream)\n    let outerFirst = await outerChunks.next()\n    invariant(!outerFirst.done)\n    let outerContent = outerFirst.value\n    let innerFrameId = getCommentMarkerId(outerContent, 'rmx:f:')\n\n    // Render initial page (pending outer frame with fallback).\n    let pageStream = renderToStream(\n      <div>\n        <Frame src=\"/outer\" fallback={<span id=\"outer\">Loading outer…</span>} />\n      </div>,\n      { resolveFrame: neverResolve },\n    )\n    let pageChunks = readChunks(pageStream)\n    let pageFirst = await pageChunks.next()\n    invariant(!pageFirst.done)\n    document.body.innerHTML = pageFirst.value\n    let outerFrameId = getCommentMarkerId(pageFirst.value, 'rmx:f:')\n\n    let clientFrame = run({\n      loadModule: vi.fn().mockResolvedValue(Counter),\n    })\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Outer frame still shows fallback.\n    expect(document.getElementById('outer')!.textContent).toBe('Loading outer…')\n\n    // Outer frame template arrives.\n    let outerTemplate = document.createElement('template')\n    outerTemplate.id = outerFrameId\n    outerTemplate.innerHTML = outerContent\n    document.body.appendChild(outerTemplate)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Outer frame rendered, inner frame shows fallback.\n    expect(document.getElementById('inner')!.textContent).toBe('Loading inner…')\n\n    // Inner frame template arrives.\n    let innerTemplate = document.createElement('template')\n    innerTemplate.id = innerFrameId\n    innerTemplate.innerHTML = innerContent\n    document.body.appendChild(innerTemplate)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Counter inside the nested frame is hydrated and interactive.\n    let button = document.getElementById('nested-counter') as HTMLButtonElement\n    expect(button.textContent).toBe('Count: 10')\n    button.click()\n    clientFrame.flush()\n    expect(button.textContent).toBe('Count: 11')\n\n    clientFrame.dispose()\n  })\n\n  it('deeply nested frames resolve independently at each level', async () => {\n    // Page has outer frame → outer has middle frame → middle has inner frame.\n    // Each level resolves independently via MutationObserver.\n    let neverResolve = () => new Promise<string>(() => {})\n\n    // Render inner content (leaf — no sub-frames).\n    let innerContent = await drain(renderToStream(<p id=\"inner-content\">Inner loaded</p>))\n\n    // Render middle content (pending inner frame with fallback).\n    let middleStream = renderToStream(\n      <div>\n        <p id=\"middle-content\">Middle loaded</p>\n        <Frame src=\"/inner\" fallback={<span id=\"inner\">Loading inner…</span>} />\n      </div>,\n      { resolveFrame: neverResolve },\n    )\n    let middleChunks = readChunks(middleStream)\n    let middleFirst = await middleChunks.next()\n    invariant(!middleFirst.done)\n    let middleContent = middleFirst.value\n    let innerFrameId = getCommentMarkerId(middleContent, 'rmx:f:')\n\n    // Render outer content (pending middle frame with fallback).\n    let outerStream = renderToStream(\n      <div>\n        <p id=\"outer-content\">Outer loaded</p>\n        <Frame src=\"/middle\" fallback={<span id=\"middle\">Loading middle…</span>} />\n      </div>,\n      { resolveFrame: neverResolve },\n    )\n    let outerChunks = readChunks(outerStream)\n    let outerFirst = await outerChunks.next()\n    invariant(!outerFirst.done)\n    let outerContent = outerFirst.value\n    let middleFrameId = getCommentMarkerId(outerContent, 'rmx:f:')\n\n    // Render initial page (pending outer frame with fallback).\n    let pageStream = renderToStream(\n      <div>\n        <h1 id=\"title\">Page</h1>\n        <Frame src=\"/outer\" fallback={<span id=\"outer\">Loading outer…</span>} />\n      </div>,\n      { resolveFrame: neverResolve },\n    )\n    let pageChunks = readChunks(pageStream)\n    let pageFirst = await pageChunks.next()\n    invariant(!pageFirst.done)\n    document.body.innerHTML = pageFirst.value\n    let outerFrameId = getCommentMarkerId(pageFirst.value, 'rmx:f:')\n\n    let clientFrame = run({ loadModule: vi.fn() })\n    await clientFrame.ready()\n\n    // Page content is visible, outer frame shows fallback.\n    expect(document.getElementById('title')!.textContent).toBe('Page')\n    expect(document.getElementById('outer')!.textContent).toBe('Loading outer…')\n\n    // Outer frame template arrives — contains a middle frame.\n    let outerTemplate = document.createElement('template')\n    outerTemplate.id = outerFrameId\n    outerTemplate.innerHTML = outerContent\n    document.body.appendChild(outerTemplate)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Outer content rendered, middle shows fallback.\n    expect(document.getElementById('outer-content')!.textContent).toBe('Outer loaded')\n    expect(document.getElementById('middle')!.textContent).toBe('Loading middle…')\n\n    // Middle frame template arrives — contains an inner frame.\n    let middleTemplate = document.createElement('template')\n    middleTemplate.id = middleFrameId\n    middleTemplate.innerHTML = middleContent\n    document.body.appendChild(middleTemplate)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Middle content rendered, inner shows fallback.\n    expect(document.getElementById('middle-content')!.textContent).toBe('Middle loaded')\n    expect(document.getElementById('inner')!.textContent).toBe('Loading inner…')\n\n    // Inner frame template arrives.\n    let innerTemplate = document.createElement('template')\n    innerTemplate.id = innerFrameId\n    innerTemplate.innerHTML = innerContent\n    document.body.appendChild(innerTemplate)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // All three levels rendered.\n    expect(document.getElementById('outer-content')!.textContent).toBe('Outer loaded')\n    expect(document.getElementById('middle-content')!.textContent).toBe('Middle loaded')\n    expect(document.getElementById('inner-content')!.textContent).toBe('Inner loaded')\n\n    // Page content preserved throughout.\n    expect(document.getElementById('title')!.textContent).toBe('Page')\n\n    clientFrame.dispose()\n  })\n\n  it('reloads a frame that is nested inside another frame', async () => {\n    let reloadInner: undefined | (() => Promise<AbortSignal>)\n    let renderCount = 0\n\n    let ReloadButton = clientEntry(\n      '/js/reload.js#ReloadButton',\n      function ReloadButton(handle: Handle) {\n        reloadInner = () => handle.frame.reload()\n        return () => <button id=\"reload-btn\">Reload</button>\n      },\n    )\n\n    async function renderInner() {\n      renderCount++\n      return await drain(\n        renderToStream(\n          <div>\n            <p id=\"inner-text\">Render {renderCount}</p>\n            <ReloadButton />\n          </div>,\n        ),\n      )\n    }\n\n    // Render outer content with a pending inner frame.\n    let outerStream = renderToStream(\n      <div>\n        <p id=\"outer-text\">Outer</p>\n        <Frame src=\"/inner\" fallback={<span id=\"inner-fallback\">Loading…</span>} />\n      </div>,\n      { resolveFrame: () => new Promise<string>(() => {}) },\n    )\n    let outerChunks = readChunks(outerStream)\n    let outerFirst = await outerChunks.next()\n    invariant(!outerFirst.done)\n    let outerContent = outerFirst.value\n    let innerFrameId = getCommentMarkerId(outerContent, 'rmx:f:')\n\n    // Render page with a blocking outer frame that resolves to the outer content.\n    let pageStream = renderToStream(\n      <div>\n        <Frame src=\"/outer\" />\n      </div>,\n      { resolveFrame: () => outerContent },\n    )\n    let pageHtml = await drain(pageStream)\n    document.body.innerHTML = pageHtml\n\n    let clientFrame = run({\n      loadModule: vi.fn().mockResolvedValue(ReloadButton),\n      resolveFrame: renderInner,\n    })\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Outer is resolved, inner shows fallback.\n    expect(document.getElementById('outer-text')!.textContent).toBe('Outer')\n    expect(document.getElementById('inner-fallback')!.textContent).toBe('Loading…')\n\n    // Inner frame template arrives.\n    let innerTemplate = document.createElement('template')\n    innerTemplate.id = innerFrameId\n    innerTemplate.innerHTML = await renderInner()\n    document.body.appendChild(innerTemplate)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    // Inner frame rendered with hydrated ReloadButton.\n    expect(document.getElementById('inner-text')!.textContent).toBe('Render 1')\n    invariant(reloadInner)\n\n    // Reload the inner frame — only the inner frame should update.\n    await reloadInner()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.getElementById('inner-text')!.textContent).toBe('Render 2')\n    expect(document.getElementById('outer-text')!.textContent).toBe('Outer')\n\n    clientFrame.dispose()\n  })\n\n  it('renders a client-created Frame with createRoot frameInit', async () => {\n    let rootContainer = document.createElement('div')\n    document.body.appendChild(rootContainer)\n\n    let root = createRoot(rootContainer, {\n      frameInit: {\n        resolveFrame: async () => '<p id=\"resolved-frame\">Resolved frame</p>',\n      },\n    })\n\n    root.render(<Frame src=\"/client-frame\" fallback={<p id=\"fallback-frame\">Loading…</p>} />)\n    root.flush()\n\n    expect(rootContainer.querySelector('#fallback-frame')?.textContent).toBe('Loading…')\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(rootContainer.querySelector('#resolved-frame')?.textContent).toBe('Resolved frame')\n    root.dispose()\n  })\n\n  it('renders a client-created Frame with createRangeRoot frameInit', async () => {\n    let host = document.createElement('div')\n    document.body.appendChild(host)\n    let start = document.createComment('start')\n    let end = document.createComment('end')\n    host.append(start, end)\n\n    let root = createRangeRoot([start, end], {\n      frameInit: {\n        src: '/range-root',\n        resolveFrame: async () => '<p id=\"resolved-range-frame\">Resolved range frame</p>',\n        loadModule: async () =>\n          function Module() {\n            return () => null\n          },\n      },\n    })\n\n    root.render(\n      <Frame src=\"/client-range-frame\" fallback={<p id=\"fallback-range-frame\">Loading…</p>} />,\n    )\n    root.flush()\n\n    expect(host.querySelector('#fallback-range-frame')?.textContent).toBe('Loading…')\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(host.querySelector('#resolved-range-frame')?.textContent).toBe('Resolved range frame')\n    root.dispose()\n  })\n\n  it('dispatches a clear error for createRoot Frame without frameInit', () => {\n    let rootContainer = document.createElement('div')\n    document.body.appendChild(rootContainer)\n\n    let root = createRoot(rootContainer)\n    let error: unknown\n    root.addEventListener('error', (event) => {\n      error = (event as ErrorEvent).error\n    })\n\n    root.render(<Frame src=\"/missing-runtime\" fallback={<p>Loading…</p>} />)\n    root.flush()\n\n    expect(error).toBeInstanceOf(Error)\n    expect((error as Error).message).toContain('Cannot render <Frame /> without frame runtime')\n  })\n\n  it('dispatches a clear error for createRangeRoot Frame without frameInit', () => {\n    let host = document.createElement('div')\n    let start = document.createComment('start')\n    let end = document.createComment('end')\n    host.append(start, end)\n\n    let root = createRangeRoot([start, end])\n    let error: unknown\n    root.addEventListener('error', (event) => {\n      error = (event as ErrorEvent).error\n    })\n\n    root.render(<Frame src=\"/missing-range-runtime\" fallback={<p>Loading…</p>} />)\n    root.flush()\n\n    expect(error).toBeInstanceOf(Error)\n    expect((error as Error).message).toContain('Cannot render <Frame /> without frame runtime')\n  })\n\n  it('throws from the root runtime resolveFrame fallback without frameInit', () => {\n    let rootContainer = document.createElement('div')\n    document.body.appendChild(rootContainer)\n\n    let runtime: { resolveFrame(src: string): unknown } | undefined\n    function CaptureRuntime(handle: Handle) {\n      runtime = handle.frame.$runtime as { resolveFrame(src: string): unknown }\n      return () => null\n    }\n\n    let root = createRoot(rootContainer)\n    root.render(<CaptureRuntime />)\n    root.flush()\n\n    expect(runtime).toBeDefined()\n    expect(() => runtime!.resolveFrame('/missing-runtime')).toThrow(\n      'Cannot render <Frame /> without frame runtime',\n    )\n  })\n\n  it('logs a clear error when hydrating client entries without loadModule', async () => {\n    let Counter = clientEntry(\n      '/js/counter.js#Counter',\n      function Counter(handle: Handle, setup: number) {\n        let count = setup\n        return () => <button>{count}</button>\n      },\n    )\n\n    let html = await drain(renderToStream(<Counter setup={2} />))\n    let container = document.createElement('div')\n    let consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n    try {\n      let root = createRoot(container, {\n        frameInit: {\n          resolveFrame: async () => html,\n        },\n      })\n\n      root.render(<Frame src=\"/counter-frame\" fallback={<p>Loading…</p>} />)\n      root.flush()\n      await new Promise((resolve) => setTimeout(resolve, 0))\n\n      expect(consoleError).toHaveBeenCalled()\n      expect(\n        consoleError.mock.calls.some((call) => String(call[0]).includes('Failed to load module')),\n      ).toBe(true)\n      expect(\n        consoleError.mock.calls.some((call) =>\n          call.some((value) =>\n            String(value).includes(\n              'loadModule is required to hydrate client entries inside <Frame />',\n            ),\n          ),\n        ),\n      ).toBe(true)\n    } finally {\n      consoleError.mockRestore()\n    }\n  })\n\n  it('logs a clear error when loadModule resolves to a non-function export', async () => {\n    let Counter = clientEntry(\n      '/js/counter.js#Counter',\n      function Counter(handle: Handle, setup: number) {\n        let count = setup\n        return () => <button>{count}</button>\n      },\n    )\n\n    let html = await drain(renderToStream(<Counter setup={3} />))\n    let container = document.createElement('div')\n    let consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n    try {\n      let root = createRoot(container, {\n        frameInit: {\n          resolveFrame: async () => html,\n          loadModule: async () => ({ not: 'a function' }) as any,\n        },\n      })\n\n      root.render(<Frame src=\"/bad-export-frame\" fallback={<p>Loading…</p>} />)\n      root.flush()\n      await new Promise((resolve) => setTimeout(resolve, 0))\n\n      expect(consoleError).toHaveBeenCalled()\n      expect(\n        consoleError.mock.calls.some((call) =>\n          call.some((value) => String(value).includes('is not a function')),\n        ),\n      ).toBe(true)\n    } finally {\n      consoleError.mockRestore()\n    }\n  })\n\n  it('reloads client-created Frame in place when src changes', async () => {\n    let rootContainer = document.createElement('div')\n    document.body.appendChild(rootContainer)\n\n    let [nextFramePromise, resolveNextFrame] = withResolvers<string>()\n\n    let root = createRoot(rootContainer, {\n      frameInit: {\n        resolveFrame: async (src) => {\n          if (src === '/a') return '<p id=\"frame-a\">A</p>'\n          return await nextFramePromise\n        },\n      },\n    })\n\n    root.render(<Frame src=\"/a\" fallback={<p id=\"fallback-a\">Loading A…</p>} />)\n    root.flush()\n    expect(rootContainer.querySelector('#fallback-a')?.textContent).toBe('Loading A…')\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(rootContainer.querySelector('#frame-a')?.textContent).toBe('A')\n\n    let frameA = rootContainer.querySelector('#frame-a')\n    invariant(frameA instanceof HTMLParagraphElement)\n\n    root.render(<Frame src=\"/b\" fallback={<p id=\"fallback-b\">Loading B…</p>} />)\n    root.flush()\n\n    // src updates should behave like reloads: existing content remains mounted\n    // while the new source resolves.\n    expect(rootContainer.querySelector('#fallback-b')).toBeNull()\n    expect(rootContainer.querySelector('#frame-a')).toBe(frameA)\n\n    resolveNextFrame('<p id=\"frame-b\">B</p>')\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(rootContainer.querySelector('#frame-b')?.textContent).toBe('B')\n    root.dispose()\n  })\n\n  it('renders a client-created Frame after run() from a hydrated entry component', async () => {\n    let mounted = false\n    let showFrame: undefined | (() => void)\n\n    let PostRunFrame = clientEntry(\n      '/js/post-run.js#PostRunFrame',\n      function PostRunFrame(handle: Handle) {\n        showFrame = () => {\n          mounted = true\n          handle.update()\n        }\n\n        return () => (\n          <section>\n            {mounted ? (\n              <Frame\n                src=\"/post-run-frame\"\n                fallback={<p id=\"post-run-fallback\">Loading post-run…</p>}\n              />\n            ) : (\n              <p id=\"before-post-run\">Before frame</p>\n            )}\n          </section>\n        )\n      },\n    )\n\n    let pageHtml = await drain(renderToStream(<PostRunFrame />))\n    document.body.innerHTML = pageHtml\n\n    let app = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/js/post-run.js' && exportName === 'PostRunFrame') return PostRunFrame\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame: async () => '<p id=\"post-run-loaded\">Post-run loaded</p>',\n    })\n\n    await app.ready()\n    invariant(showFrame)\n    showFrame()\n    app.flush()\n\n    expect(document.getElementById('post-run-fallback')?.textContent).toBe('Loading post-run…')\n\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.getElementById('post-run-loaded')?.textContent).toBe('Post-run loaded')\n    app.dispose()\n  })\n\n  it('does not duplicate initially-mounted Frame hydration in a client entry', async () => {\n    let MountedFrame = clientEntry(\n      '/js/mounted-frame.js#MountedFrame',\n      function MountedFrame(handle: Handle) {\n        let showFrame = true\n        return () =>\n          showFrame ? (\n            <section>\n              <Frame src=\"/outer\" fallback={<p id=\"outer-fallback\">Loading outer…</p>} />\n            </section>\n          ) : null\n      },\n    )\n\n    let outerStream = renderToStream(\n      <div id=\"outer-root\">\n        <Frame src=\"/nested\" fallback={<span id=\"nested-fallback\">Loading nested…</span>} />\n      </div>,\n      { resolveFrame: () => new Promise<string>(() => {}) },\n    )\n    let outerChunks = readChunks(outerStream)\n    let outerFirst = await outerChunks.next()\n    invariant(!outerFirst.done)\n    let outerInitialHtml = outerFirst.value\n    let nestedFrameId = getCommentMarkerId(outerInitialHtml, 'rmx:f:')\n\n    let [outerPromise, resolveOuter] = withResolvers<string>()\n    let pageStream = renderToStream(<MountedFrame />, {\n      resolveFrame(src: string) {\n        if (src === '/outer') return outerPromise\n        throw new Error(`Unexpected src during page render: ${src}`)\n      },\n    })\n    let pageChunks = readChunks(pageStream)\n    let pageFirst = await pageChunks.next()\n    invariant(!pageFirst.done)\n    document.body.innerHTML = pageFirst.value\n\n    expect(document.querySelectorAll('#outer-fallback')).toHaveLength(1)\n\n    let clientResolveFrame = vi\n      .fn()\n      .mockImplementation(\n        async (src: string) => `<p data-client-resolve=\"${src}\">client resolve ${src}</p>`,\n      )\n\n    let app = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/js/mounted-frame.js' && exportName === 'MountedFrame') {\n          return MountedFrame\n        }\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame: clientResolveFrame,\n    })\n\n    await app.ready()\n    expect(clientResolveFrame).not.toHaveBeenCalled()\n\n    resolveOuter(outerInitialHtml)\n    let pageSecond = await pageChunks.next()\n    invariant(!pageSecond.done)\n    document.body.insertAdjacentHTML('beforeend', pageSecond.value)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(clientResolveFrame).not.toHaveBeenCalled()\n    expect(document.querySelectorAll('#outer-root')).toHaveLength(1)\n    expect(document.querySelectorAll('#nested-fallback')).toHaveLength(1)\n\n    let nestedTemplate = document.createElement('template')\n    nestedTemplate.id = nestedFrameId\n    nestedTemplate.innerHTML = '<span id=\"nested-loaded\">Nested loaded</span>'\n    document.body.appendChild(nestedTemplate)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(clientResolveFrame).not.toHaveBeenCalled()\n    expect(document.querySelectorAll('#nested-loaded')).toHaveLength(1)\n    expect(document.querySelectorAll('#nested-fallback')).toHaveLength(0)\n\n    app.dispose()\n  })\n\n  it('renders Frame semantics from entry children during initial hydration', async () => {\n    let Card = clientEntry('/js/card.js#Card', function Card(handle: Handle) {\n      return (props: { children: any }) => <section>{props.children}</section>\n    })\n\n    let [framePromise, resolveFramePromise] = withResolvers<string>()\n    let pageStream = renderToStream(\n      <Card>\n        <Frame src=\"/child-frame\" fallback={<span id=\"child-frame\">Loading child frame…</span>} />\n      </Card>,\n      { resolveFrame: () => framePromise },\n    )\n    let chunks = readChunks(pageStream)\n    let first = await chunks.next()\n    invariant(!first.done)\n    document.body.innerHTML = first.value\n\n    expect(document.getElementById('child-frame')?.textContent).toBe('Loading child frame…')\n\n    let app = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/js/card.js' && exportName === 'Card') return Card\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n    })\n\n    await app.ready()\n    expect(document.getElementById('child-frame')?.textContent).toBe('Loading child frame…')\n\n    resolveFramePromise('<span id=\"child-frame\">Loaded child frame</span>')\n    let second = await chunks.next()\n    invariant(!second.done)\n    document.body.insertAdjacentHTML('beforeend', second.value)\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.getElementById('child-frame')?.textContent).toBe('Loaded child frame')\n    app.dispose()\n  })\n\n  it('does not dispose managed stylesheets when removing a client-created Frame', async () => {\n    let rootContainer = document.createElement('div')\n    document.body.appendChild(rootContainer)\n\n    function Shell(handle: Handle) {\n      let mounted = true\n\n      return () => (\n        <main mix={[css({ color: '#0bf' })]}>\n          <button\n            id=\"toggle-frame\"\n            type=\"button\"\n            mix={[\n              on('click', () => {\n                mounted = !mounted\n                handle.update()\n              }),\n            ]}\n          >\n            Toggle\n          </button>\n          {mounted ? (\n            <Frame\n              src=\"/style-frame\"\n              fallback={<div mix={[css({ color: '#f0b' })]}>Loading style frame…</div>}\n            />\n          ) : null}\n        </main>\n      )\n    }\n\n    let root = createRoot(rootContainer, {\n      frameInit: {\n        resolveFrame: async () => '<section class=\"frame-loaded\">Frame loaded</section>',\n      },\n    })\n\n    root.render(<Shell />)\n    root.flush()\n\n    let before = document.adoptedStyleSheets.length\n    let button = rootContainer.querySelector('#toggle-frame')\n    invariant(button instanceof HTMLButtonElement)\n\n    button.click()\n    root.flush()\n\n    let after = document.adoptedStyleSheets.length\n    expect(after).toBeGreaterThan(0)\n    expect(after).toBeGreaterThanOrEqual(before)\n\n    root.dispose()\n  })\n\n  it('streams client resolveFrame templates and updates nested placeholders incrementally', async () => {\n    let reload: undefined | (() => Promise<AbortSignal>)\n\n    let ReloadButton = clientEntry(\n      '/assets/reload-stream.js#ReloadStream',\n      function ReloadStream(handle: Handle) {\n        reload = () => handle.frame.reload()\n        return () => (\n          <button id=\"reload-stream\" type=\"button\">\n            Reload streamed\n          </button>\n        )\n      },\n    )\n\n    async function renderInitial(): Promise<string> {\n      return await drain(\n        renderToStream(\n          <section>\n            <p id=\"outer\">Initial outer</p>\n            <ReloadButton />\n          </section>,\n        ),\n      )\n    }\n\n    let [nestedResolvePromise, resolveNested] = withResolvers<string>()\n    let streamedReload = renderToStream(\n      <section>\n        <p id=\"outer\">Reloaded outer</p>\n        <Frame src=\"/nested\" fallback={<span id=\"nested\">Loading nested…</span>} />\n        <ReloadButton />\n      </section>,\n      {\n        resolveFrame(src: string) {\n          if (src === '/nested') return nestedResolvePromise\n          throw new Error(`Unexpected nested src: ${src}`)\n        },\n      },\n    )\n\n    let streamedChunks = readChunks(streamedReload)\n    let firstChunk = await streamedChunks.next()\n    invariant(!firstChunk.done)\n    resolveNested('<span id=\"nested\">Nested loaded</span>')\n    let secondChunk = await streamedChunks.next()\n    invariant(!secondChunk.done)\n\n    let [secondChunkPromise, releaseSecondChunk] = withResolvers<string>()\n    let serverHtml = await drain(\n      renderToStream(\n        <main>\n          <Frame src=\"/reload-streamed\" fallback={<div id=\"frame-fallback\">Loading…</div>} />\n        </main>,\n        {\n          resolveFrame: renderInitial,\n        },\n      ),\n    )\n    document.body.innerHTML = serverHtml\n\n    let app = run({\n      loadModule(moduleUrl, exportName) {\n        if (moduleUrl === '/assets/reload-stream.js' && exportName === 'ReloadStream') {\n          return ReloadButton\n        }\n        throw new Error(`Unexpected module: ${moduleUrl}#${exportName}`)\n      },\n      resolveFrame(src: string) {\n        if (src === '/reload-streamed') {\n          return streamFromChunks([firstChunk.value, secondChunkPromise])\n        }\n        throw new Error(`Unexpected frame src: ${src}`)\n      },\n    })\n\n    await app.ready()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    invariant(reload)\n    let reloadPromise = reload()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.getElementById('outer')?.textContent).toBe('Reloaded outer')\n    expect(document.getElementById('nested')?.textContent).toBe('Loading nested…')\n\n    releaseSecondChunk(secondChunk.value)\n    await reloadPromise\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(document.getElementById('nested')?.textContent).toBe('Nested loaded')\n  })\n\n  it('cancels stale client frame streams when src changes', async () => {\n    let rootContainer = document.createElement('div')\n    document.body.appendChild(rootContainer)\n\n    let [slowChunkPromise, resolveSlowChunk] = withResolvers<string>()\n\n    function Shell(handle: Handle) {\n      let src = '/slow'\n      return () => (\n        <main>\n          <button\n            id=\"switch-src\"\n            type=\"button\"\n            mix={[\n              on('click', () => {\n                src = '/fast'\n                handle.update()\n              }),\n            ]}\n          >\n            Switch\n          </button>\n          <Frame src={src} fallback={<p id=\"fallback\">Loading…</p>} />\n        </main>\n      )\n    }\n\n    let root = createRoot(rootContainer, {\n      frameInit: {\n        resolveFrame(src: string) {\n          if (src === '/slow') {\n            return streamFromChunks([slowChunkPromise])\n          }\n          if (src === '/fast') {\n            return '<p id=\"result\">Fast result</p>'\n          }\n          throw new Error(`Unexpected src: ${src}`)\n        },\n      },\n    })\n\n    root.render(<Shell />)\n    root.flush()\n\n    expect(rootContainer.querySelector('#fallback')?.textContent).toBe('Loading…')\n\n    let switchButton = rootContainer.querySelector('#switch-src')\n    invariant(switchButton instanceof HTMLButtonElement)\n    switchButton.click()\n    root.flush()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(rootContainer.querySelector('#result')?.textContent).toBe('Fast result')\n\n    resolveSlowChunk('<p id=\"result\">Slow result</p>')\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(rootContainer.querySelector('#result')?.textContent).toBe('Fast result')\n    root.dispose()\n  })\n\n  it('renders RemixNode results from client resolveFrame', async () => {\n    let rootContainer = document.createElement('div')\n    document.body.appendChild(rootContainer)\n\n    let root = createRoot(rootContainer, {\n      frameInit: {\n        resolveFrame(src: string) {\n          if (src === '/html') {\n            return '<div id=\"stale\">Stale content</div>'\n          }\n\n          if (src === '/error') {\n            return (\n              <section id=\"frame-error\">\n                <h2>Frame Error</h2>\n                <p>Retry the page.</p>\n              </section>\n            )\n          }\n\n          throw new Error(`Unexpected src: ${src}`)\n        },\n      },\n    })\n\n    root.render(<Frame src=\"/html\" fallback={<p id=\"fallback\">Loading…</p>} />)\n    root.flush()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(rootContainer.querySelector('#stale')?.textContent).toBe('Stale content')\n\n    root.render(<Frame src=\"/error\" fallback={<p id=\"fallback\">Loading…</p>} />)\n    root.flush()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n\n    expect(rootContainer.querySelector('#frame-error h2')?.textContent).toBe('Frame Error')\n    expect(rootContainer.querySelector('#frame-error p')?.textContent).toBe('Retry the page.')\n    expect(rootContainer.querySelector('#stale')).toBeNull()\n\n    root.dispose()\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/hydration.attributes.test.tsx",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { link } from '../index.ts'\nimport { createRoot } from '../lib/vdom.ts'\nimport { renderToString } from '../lib/stream.ts'\nimport { invariant } from '../lib/invariant.ts'\n\ndescribe('hydration', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n    for (let node of Array.from(document.head.childNodes)) {\n      document.head.removeChild(node)\n    }\n  })\n\n  describe('special case props to HTML attributes', () => {\n    it('hydrates className as class attribute', async () => {\n      let html = await renderToString(<div className=\"my-class\">Hello</div>)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      expect(existingDiv.getAttribute('class')).toBe('my-class')\n\n      let root = createRoot(container)\n      root.render(<div className=\"my-class\">Hello</div>)\n      root.flush()\n\n      // Same DOM node should be adopted\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.getAttribute('class')).toBe('my-class')\n    })\n\n    it('hydrates htmlFor as for attribute', async () => {\n      let html = await renderToString(\n        <div>\n          <label htmlFor=\"my-input\">Label</label>\n          <input id=\"my-input\" />\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingLabel = container.querySelector('label')\n      invariant(existingLabel)\n      expect(existingLabel.getAttribute('for')).toBe('my-input')\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <label htmlFor=\"my-input\">Label</label>\n          <input id=\"my-input\" />\n        </div>,\n      )\n      root.flush()\n\n      expect(container.querySelector('label')).toBe(existingLabel)\n      expect(existingLabel.getAttribute('for')).toBe('my-input')\n    })\n\n    it('hydrates tabIndex as tabindex attribute', async () => {\n      let html = await renderToString(<button tabIndex={0}>Click</button>)\n      container.innerHTML = html\n\n      let existingButton = container.querySelector('button')\n      invariant(existingButton)\n\n      let root = createRoot(container)\n      root.render(<button tabIndex={0}>Click</button>)\n      root.flush()\n\n      expect(container.querySelector('button')).toBe(existingButton)\n      expect(existingButton.getAttribute('tabindex')).toBe('0')\n    })\n\n    it('hydrates role and tabIndex added by link mixins', async () => {\n      let html = await renderToString(<li mix={link('/docs')}>Docs</li>)\n      container.innerHTML = html\n\n      let existingItem = container.querySelector('li')\n      invariant(existingItem)\n\n      let root = createRoot(container)\n      root.render(<li mix={link('/docs')}>Docs</li>)\n      root.flush()\n\n      expect(container.querySelector('li')).toBe(existingItem)\n      expect(existingItem.getAttribute('role')).toBe('link')\n      expect(existingItem.getAttribute('tabindex')).toBe('0')\n    })\n\n    it('hydrates acceptCharset as accept-charset attribute', async () => {\n      let html = await renderToString(<form acceptCharset=\"UTF-8\" />)\n      container.innerHTML = html\n\n      let existingForm = container.querySelector('form')\n      invariant(existingForm)\n\n      let root = createRoot(container)\n      root.render(<form acceptCharset=\"UTF-8\" />)\n      root.flush()\n\n      expect(container.querySelector('form')).toBe(existingForm)\n      expect(existingForm.getAttribute('accept-charset')).toBe('UTF-8')\n    })\n\n    it('hydrates httpEquiv as http-equiv attribute', async () => {\n      let html = await renderToString(<meta httpEquiv=\"refresh\" content=\"5\" />)\n      container.innerHTML = html\n\n      let existingMeta = container.querySelector('meta')\n      invariant(existingMeta)\n\n      let root = createRoot(container)\n      root.render(<meta httpEquiv=\"refresh\" content=\"5\" />)\n      root.flush()\n\n      expect(container.querySelector('meta')).toBe(existingMeta)\n      expect(document.head.querySelector('meta')).toBeNull()\n      expect(existingMeta.getAttribute('http-equiv')).toBe('refresh')\n    })\n\n    it('hydrates aria-* attributes unchanged', async () => {\n      let html = await renderToString(\n        <button aria-label=\"Close\" aria-expanded=\"false\">\n          X\n        </button>,\n      )\n      container.innerHTML = html\n\n      let existingButton = container.querySelector('button')\n      invariant(existingButton)\n\n      let root = createRoot(container)\n      root.render(\n        <button aria-label=\"Close\" aria-expanded=\"false\">\n          X\n        </button>,\n      )\n      root.flush()\n\n      expect(container.querySelector('button')).toBe(existingButton)\n      expect(existingButton.getAttribute('aria-label')).toBe('Close')\n      expect(existingButton.getAttribute('aria-expanded')).toBe('false')\n    })\n\n    it('hydrates data-* attributes unchanged', async () => {\n      let html = await renderToString(<div data-testid=\"my-div\" data-value=\"42\" />)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<div data-testid=\"my-div\" data-value=\"42\" />)\n      root.flush()\n\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.getAttribute('data-testid')).toBe('my-div')\n      expect(existingDiv.getAttribute('data-value')).toBe('42')\n    })\n\n    it('hydrates SVG xlinkHref as xlink:href', async () => {\n      let html = await renderToString(\n        <svg>\n          <use xlinkHref=\"#icon-star\" />\n        </svg>,\n      )\n      container.innerHTML = html\n\n      let existingUse = container.querySelector('use')\n      invariant(existingUse)\n\n      let root = createRoot(container)\n      root.render(\n        <svg>\n          <use xlinkHref=\"#icon-star\" />\n        </svg>,\n      )\n      root.flush()\n\n      expect(container.querySelector('use')).toBe(existingUse)\n      expect(existingUse.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#icon-star')\n    })\n\n    it('hydrates SVG viewBox with preserved case', async () => {\n      let html = await renderToString(<svg viewBox=\"0 0 24 24\" />)\n      container.innerHTML = html\n\n      let existingSvg = container.querySelector('svg')\n      invariant(existingSvg)\n\n      let root = createRoot(container)\n      root.render(<svg viewBox=\"0 0 24 24\" />)\n      root.flush()\n\n      expect(container.querySelector('svg')).toBe(existingSvg)\n      expect(existingSvg.getAttribute('viewBox')).toBe('0 0 24 24')\n    })\n\n    it('hydrates SVG preserveAspectRatio with preserved case', async () => {\n      let html = await renderToString(<svg preserveAspectRatio=\"xMidYMid meet\" />)\n      container.innerHTML = html\n\n      let existingSvg = container.querySelector('svg')\n      invariant(existingSvg)\n\n      let root = createRoot(container)\n      root.render(<svg preserveAspectRatio=\"xMidYMid meet\" />)\n      root.flush()\n\n      expect(container.querySelector('svg')).toBe(existingSvg)\n      expect(existingSvg.getAttribute('preserveAspectRatio')).toBe('xMidYMid meet')\n    })\n\n    it('hydrates SVG filterUnits with canonical case and semantics', async () => {\n      let html = await renderToString(\n        <svg>\n          <defs>\n            <filter id=\"f\" filterUnits=\"userSpaceOnUse\" />\n          </defs>\n        </svg>,\n      )\n      container.innerHTML = html\n\n      let existingFilter = container.querySelector('#f')\n      invariant(existingFilter instanceof SVGFilterElement)\n\n      let root = createRoot(container)\n      root.render(\n        <svg>\n          <defs>\n            <filter id=\"f\" filterUnits=\"userSpaceOnUse\" />\n          </defs>\n        </svg>,\n      )\n      root.flush()\n\n      expect(container.querySelector('#f')).toBe(existingFilter)\n      expect(existingFilter.getAttribute('filterUnits')).toBe('userSpaceOnUse')\n      expect(existingFilter.getAttribute('filter-units')).toBe(null)\n      expect(existingFilter.filterUnits.baseVal).toBe(1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/hydration.boolean-attrs.test.tsx",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { renderToString } from '../lib/stream.ts'\nimport { invariant } from '../lib/invariant.ts'\n\ndescribe('hydration', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n  })\n\n  describe('boolean attributes', () => {\n    it('hydrates disabled attribute', async () => {\n      let html = await renderToString(<button disabled>Click</button>)\n      container.innerHTML = html\n\n      let existingButton = container.querySelector('button')\n      invariant(existingButton)\n      expect(existingButton.disabled).toBe(true)\n\n      let root = createRoot(container)\n      root.render(<button disabled>Click</button>)\n      root.flush()\n\n      expect(container.querySelector('button')).toBe(existingButton)\n      expect(existingButton.disabled).toBe(true)\n    })\n\n    it('hydrates readonly attribute', async () => {\n      let html = await renderToString(<input readOnly />)\n      container.innerHTML = html\n\n      let existingInput = container.querySelector('input')\n      invariant(existingInput)\n\n      let root = createRoot(container)\n      root.render(<input readOnly />)\n      root.flush()\n\n      expect(container.querySelector('input')).toBe(existingInput)\n      expect(existingInput.readOnly).toBe(true)\n    })\n\n    it('hydrates required attribute', async () => {\n      let html = await renderToString(<input required />)\n      container.innerHTML = html\n\n      let existingInput = container.querySelector('input')\n      invariant(existingInput)\n\n      let root = createRoot(container)\n      root.render(<input required />)\n      root.flush()\n\n      expect(container.querySelector('input')).toBe(existingInput)\n      expect(existingInput.required).toBe(true)\n    })\n\n    it('hydrates multiple attribute on select', async () => {\n      let html = await renderToString(\n        <select multiple>\n          <option>A</option>\n          <option>B</option>\n        </select>,\n      )\n      container.innerHTML = html\n\n      let existingSelect = container.querySelector('select')\n      invariant(existingSelect)\n\n      let root = createRoot(container)\n      root.render(\n        <select multiple>\n          <option>A</option>\n          <option>B</option>\n        </select>,\n      )\n      root.flush()\n\n      expect(container.querySelector('select')).toBe(existingSelect)\n      expect(existingSelect.multiple).toBe(true)\n    })\n\n    it('hydrates hidden attribute', async () => {\n      let html = await renderToString(<div hidden>Hidden content</div>)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<div hidden>Hidden content</div>)\n      root.flush()\n\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.hidden).toBe(true)\n    })\n\n    it('hydrates boolean attribute with true value', async () => {\n      let html = await renderToString(<button disabled={true}>Click</button>)\n      container.innerHTML = html\n\n      let existingButton = container.querySelector('button')\n      invariant(existingButton)\n\n      let root = createRoot(container)\n      root.render(<button disabled={true}>Click</button>)\n      root.flush()\n\n      expect(container.querySelector('button')).toBe(existingButton)\n      expect(existingButton.disabled).toBe(true)\n    })\n\n    it('hydrates boolean attribute with false value', async () => {\n      let html = await renderToString(<button disabled={false}>Click</button>)\n      container.innerHTML = html\n\n      let existingButton = container.querySelector('button')\n      invariant(existingButton)\n\n      let root = createRoot(container)\n      root.render(<button disabled={false}>Click</button>)\n      root.flush()\n\n      expect(container.querySelector('button')).toBe(existingButton)\n      expect(existingButton.disabled).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/hydration.components.test.tsx",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport type { Handle } from '../lib/component.ts'\nimport { createRoot } from '../lib/vdom.ts'\nimport { renderToString } from '../lib/stream.ts'\nimport { clientEntry } from '../lib/client-entries.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport { on, ref } from '../index.ts'\n\ndescribe('hydration', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n    for (let node of Array.from(document.head.childNodes)) {\n      document.head.removeChild(node)\n    }\n  })\n\n  describe('component edge cases', () => {\n    it('hydrates component that returns null', async () => {\n      function NullComponent() {\n        return () => null\n      }\n\n      let html = await renderToString(\n        <div>\n          <NullComponent />\n          <span>After</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingSpan = container.querySelector('span')\n      invariant(existingSpan)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <NullComponent />\n          <span>After</span>\n        </div>,\n      )\n      root.flush()\n\n      expect(container.querySelector('span')).toBe(existingSpan)\n      expect(existingSpan.textContent).toBe('After')\n    })\n\n    it('hydrates component that returns fragment', async () => {\n      function FragmentComponent() {\n        return () => (\n          <>\n            <span>First</span>\n            <span>Second</span>\n          </>\n        )\n      }\n\n      let html = await renderToString(\n        <div>\n          <FragmentComponent />\n        </div>,\n      )\n      container.innerHTML = html\n\n      let spans = container.querySelectorAll('span')\n      expect(spans).toHaveLength(2)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <FragmentComponent />\n        </div>,\n      )\n      root.flush()\n\n      let hydratedSpans = container.querySelectorAll('span')\n      expect(hydratedSpans[0]).toBe(spans[0])\n      expect(hydratedSpans[1]).toBe(spans[1])\n    })\n\n    it('hydrates nested hydration boundaries', async () => {\n      let Outer = clientEntry('/outer.js#Outer', function Outer(handle: Handle) {\n        return (props: { children: any }) => <div className=\"outer\">{props.children}</div>\n      })\n\n      let Inner = clientEntry('/inner.js#Inner', function Inner(handle: Handle) {\n        return () => <span className=\"inner\">Inner content</span>\n      })\n\n      let html = await renderToString(\n        <Outer>\n          <Inner />\n        </Outer>,\n      )\n      container.innerHTML = html\n\n      // Should have hydration comment markers\n      expect(html).toContain('<!-- rmx:h:')\n      expect(html).toContain('<!-- /rmx:h -->')\n\n      let existingOuter = container.querySelector('.outer')\n      let existingInner = container.querySelector('.inner')\n      invariant(existingOuter && existingInner)\n\n      // For this test, we use createRoot which should handle the comment markers\n      let root = createRoot(container)\n      root.render(\n        <Outer>\n          <Inner />\n        </Outer>,\n      )\n      root.flush()\n\n      // Both should be adopted\n      expect(container.querySelector('.outer')).toBe(existingOuter)\n      expect(container.querySelector('.inner')).toBe(existingInner)\n    })\n\n    it('hydrates component with state preservation', async () => {\n      function Counter(handle: Handle, setup: number) {\n        let count = setup\n        return () => (\n          <button\n            mix={[\n              on('click', () => {\n                count++\n                handle.update()\n              }),\n            ]}\n          >\n            Count: {count}\n          </button>\n        )\n      }\n\n      let html = await renderToString(<Counter setup={5} />)\n      container.innerHTML = html\n\n      let existingButton = container.querySelector('button')\n      invariant(existingButton)\n      expect(existingButton.textContent).toBe('Count: 5')\n\n      let root = createRoot(container)\n      root.render(<Counter setup={5} />)\n      root.flush()\n\n      // Button should be adopted\n      expect(container.querySelector('button')).toBe(existingButton)\n\n      // Clicking should work\n      existingButton.click()\n      root.flush()\n\n      expect(existingButton.textContent).toBe('Count: 6')\n    })\n  })\n\n  describe('additional scenarios', () => {\n    it('hydrates context across component boundaries', async () => {\n      function Provider(handle: Handle<{ value: string }>) {\n        handle.context.set({ value: 'from context' })\n        return (props: { children: any }) => <div className=\"provider\">{props.children}</div>\n      }\n\n      function Consumer(handle: Handle) {\n        let ctx = handle.context.get(Provider)\n        return () => <span className=\"consumer\">{ctx?.value ?? 'no context'}</span>\n      }\n\n      let html = await renderToString(\n        <Provider>\n          <Consumer />\n        </Provider>,\n      )\n      container.innerHTML = html\n\n      let existingProvider = container.querySelector('.provider')\n      let existingConsumer = container.querySelector('.consumer')\n      invariant(existingProvider && existingConsumer)\n      expect(existingConsumer.textContent).toBe('from context')\n\n      let root = createRoot(container)\n      root.render(\n        <Provider>\n          <Consumer />\n        </Provider>,\n      )\n      root.flush()\n\n      expect(container.querySelector('.provider')).toBe(existingProvider)\n      expect(container.querySelector('.consumer')).toBe(existingConsumer)\n      expect(existingConsumer.textContent).toBe('from context')\n    })\n\n    it('hydrates SVG elements with case-sensitive tags', async () => {\n      let html = await renderToString(\n        <svg>\n          <defs>\n            <linearGradient id=\"grad1\">\n              <stop offset=\"0%\" stopColor=\"red\" />\n              <stop offset=\"100%\" stopColor=\"blue\" />\n            </linearGradient>\n          </defs>\n          <rect fill=\"url(#grad1)\" width=\"100\" height=\"100\" />\n        </svg>,\n      )\n      container.innerHTML = html\n\n      let existingSvg = container.querySelector('svg')\n      let existingGradient = container.querySelector('linearGradient')\n      let existingRect = container.querySelector('rect')\n      invariant(existingSvg && existingGradient && existingRect)\n\n      let root = createRoot(container)\n      root.render(\n        <svg>\n          <defs>\n            <linearGradient id=\"grad1\">\n              <stop offset=\"0%\" stopColor=\"red\" />\n              <stop offset=\"100%\" stopColor=\"blue\" />\n            </linearGradient>\n          </defs>\n          <rect fill=\"url(#grad1)\" width=\"100\" height=\"100\" />\n        </svg>,\n      )\n      root.flush()\n\n      expect(container.querySelector('svg')).toBe(existingSvg)\n      expect(container.querySelector('linearGradient')).toBe(existingGradient)\n      expect(container.querySelector('rect')).toBe(existingRect)\n    })\n\n    it('hydrates innerHTML prop', async () => {\n      let html = await renderToString(<div innerHTML=\"<span>Raw HTML</span>\" />)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      expect(existingDiv.innerHTML).toBe('<span>Raw HTML</span>')\n\n      let root = createRoot(container)\n      root.render(<div innerHTML=\"<span>Raw HTML</span>\" />)\n      root.flush()\n\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.innerHTML).toBe('<span>Raw HTML</span>')\n    })\n\n    it('hydrates style prop as object', async () => {\n      let html = await renderToString(\n        <div style={{ color: 'red', backgroundColor: 'blue', padding: '10px' }}>Styled</div>,\n      )\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(\n        <div style={{ color: 'red', backgroundColor: 'blue', padding: '10px' }}>Styled</div>,\n      )\n      root.flush()\n\n      expect(container.querySelector('div')).toBe(existingDiv)\n      // Style should be applied\n      expect(existingDiv.style.color).toBe('red')\n      expect(existingDiv.style.backgroundColor).toBe('blue')\n    })\n\n    it('calls ref callback after hydration', async () => {\n      let connectedNode: HTMLDivElement | null = null\n\n      function WithConnect() {\n        return () => (\n          <div\n            mix={[\n              ref((node) => {\n                connectedNode = node as HTMLDivElement\n              }),\n            ]}\n          >\n            Connected\n          </div>\n        )\n      }\n\n      let html = await renderToString(<WithConnect />)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<WithConnect />)\n      root.flush()\n\n      // Ref should be called with the adopted node\n      expect(connectedNode).toBe(existingDiv)\n    })\n\n    it('attaches event handlers during hydration', async () => {\n      let clicked = false\n\n      function Clickable() {\n        return () => (\n          <button\n            mix={[\n              on('click', () => {\n                clicked = true\n              }),\n            ]}\n          >\n            Click me\n          </button>\n        )\n      }\n\n      let html = await renderToString(<Clickable />)\n      container.innerHTML = html\n\n      let existingButton = container.querySelector('button')\n      invariant(existingButton)\n\n      let root = createRoot(container)\n      root.render(<Clickable />)\n      root.flush()\n\n      // Button should be adopted\n      expect(container.querySelector('button')).toBe(existingButton)\n\n      // Event should work\n      existingButton.click()\n      expect(clicked).toBe(true)\n    })\n\n    it('hydrates keyed elements', async () => {\n      let items = [\n        { id: 'a', text: 'Item A' },\n        { id: 'b', text: 'Item B' },\n        { id: 'c', text: 'Item C' },\n      ]\n\n      let html = await renderToString(\n        <ul>\n          {items.map((item) => (\n            <li key={item.id}>{item.text}</li>\n          ))}\n        </ul>,\n      )\n      container.innerHTML = html\n\n      let existingItems = container.querySelectorAll('li')\n      expect(existingItems).toHaveLength(3)\n\n      let root = createRoot(container)\n      root.render(\n        <ul>\n          {items.map((item) => (\n            <li key={item.id}>{item.text}</li>\n          ))}\n        </ul>,\n      )\n      root.flush()\n\n      let hydratedItems = container.querySelectorAll('li')\n      expect(hydratedItems[0]).toBe(existingItems[0])\n      expect(hydratedItems[1]).toBe(existingItems[1])\n      expect(hydratedItems[2]).toBe(existingItems[2])\n    })\n\n    it('hydrates deeply nested elements', async () => {\n      let html = await renderToString(\n        <div className=\"level-1\">\n          <div className=\"level-2\">\n            <div className=\"level-3\">\n              <div className=\"level-4\">\n                <span>Deep content</span>\n              </div>\n            </div>\n          </div>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let level1 = container.querySelector('.level-1')\n      let level2 = container.querySelector('.level-2')\n      let level3 = container.querySelector('.level-3')\n      let level4 = container.querySelector('.level-4')\n      let span = container.querySelector('span')\n      invariant(level1 && level2 && level3 && level4 && span)\n\n      let root = createRoot(container)\n      root.render(\n        <div className=\"level-1\">\n          <div className=\"level-2\">\n            <div className=\"level-3\">\n              <div className=\"level-4\">\n                <span>Deep content</span>\n              </div>\n            </div>\n          </div>\n        </div>,\n      )\n      root.flush()\n\n      // All levels should be adopted\n      expect(container.querySelector('.level-1')).toBe(level1)\n      expect(container.querySelector('.level-2')).toBe(level2)\n      expect(container.querySelector('.level-3')).toBe(level3)\n      expect(container.querySelector('.level-4')).toBe(level4)\n      expect(container.querySelector('span')).toBe(span)\n    })\n\n    it('hydrates head-like elements in place', () => {\n      container.innerHTML =\n        '<title>Hydrated title</title>' +\n        '<meta name=\"description\" content=\"Hydrated description\" />' +\n        '<script type=\"application/ld+json\">{\"@type\":\"Thing\",\"name\":\"Hydrated\"}</script>' +\n        '<div id=\"content\">Body content</div>'\n\n      let existingTitle = container.querySelector('title')\n      let existingMeta = container.querySelector('meta[name=\"description\"]')\n      let existingLdJson = container.querySelector('script[type=\"application/ld+json\"]')\n      let existingContent = container.querySelector('#content')\n      invariant(existingTitle && existingMeta && existingLdJson && existingContent)\n\n      let root = createRoot(container)\n      root.render(\n        <>\n          <title>Hydrated title</title>\n          <meta name=\"description\" content=\"Hydrated description\" />\n          <script type=\"application/ld+json\">{'{\"@type\":\"Thing\",\"name\":\"Hydrated\"}'}</script>\n          <div id=\"content\">Body content</div>\n        </>,\n      )\n      root.flush()\n\n      expect(container.querySelector('title')).toBe(existingTitle)\n      expect(container.querySelector('meta[name=\"description\"]')).toBe(existingMeta)\n      expect(container.querySelector('script[type=\"application/ld+json\"]')).toBe(existingLdJson)\n      expect(container.querySelector('#content')).toBe(existingContent)\n      expect(document.head.querySelector('title')).toBeNull()\n      expect(document.head.querySelector('meta[name=\"description\"]')).toBeNull()\n      expect(document.head.querySelector('script[type=\"application/ld+json\"]')).toBeNull()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/hydration.css.test.tsx",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { createRoot, resetStyleState } from '../lib/vdom.ts'\nimport { renderToString } from '../lib/stream.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport { css } from '../index.ts'\n\ndescribe('hydration', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n  })\n\n  describe('css mixin hydration', () => {\n    afterEach(() => {\n      // Reset the global style manager state between tests\n      resetStyleState()\n    })\n\n    it('hydrates element with css mixin and adopts server style', async () => {\n      let html = await renderToString(<div mix={[css({ color: 'red' })]}>Hello</div>)\n\n      // Inject server styles into document.head and append body content\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      let originalClass = existingDiv.className\n      expect(originalClass).toMatch(/rmxc-/)\n\n      let root = createRoot(container)\n      root.render(<div mix={[css({ color: 'red' })]}>Hello</div>)\n      root.flush()\n\n      // Element should be adopted (same DOM node)\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.className).toBe(originalClass)\n      // Style should apply\n      expect(getComputedStyle(existingDiv).color).toBe('rgb(255, 0, 0)')\n    })\n\n    it('updates css mixin after hydration', async () => {\n      let html = await renderToString(<div mix={[css({ color: 'red' })]}>Hello</div>)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<div mix={[css({ color: 'red' })]}>Hello</div>)\n      root.flush()\n\n      expect(getComputedStyle(existingDiv).color).toBe('rgb(255, 0, 0)')\n\n      // Update to different css mixin\n      root.render(<div mix={[css({ color: 'blue' })]}>Hello</div>)\n      root.flush()\n\n      expect(getComputedStyle(existingDiv).color).toBe('rgb(0, 0, 255)')\n      expect(existingDiv.className).toMatch(/rmxc-/)\n    })\n\n    it('hydrates css mixin combined with className', async () => {\n      let html = await renderToString(\n        <div className=\"my-class\" mix={[css({ color: 'green' })]}>\n          Hello\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      expect(existingDiv.className).toContain('my-class')\n      expect(existingDiv.className).toMatch(/rmxc-/)\n\n      let root = createRoot(container)\n      root.render(\n        <div className=\"my-class\" mix={[css({ color: 'green' })]}>\n          Hello\n        </div>,\n      )\n      root.flush()\n\n      // Element should be adopted\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.className).toContain('my-class')\n      expect(existingDiv.className).toMatch(/rmxc-/)\n      expect(getComputedStyle(existingDiv).color).toBe('rgb(0, 128, 0)')\n    })\n\n    it('multiple elements with same css mixin share style during hydration', async () => {\n      let html = await renderToString(\n        <div>\n          <span mix={[css({ color: 'purple' })]}>First</span>\n          <span mix={[css({ color: 'purple' })]}>Second</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let spans = container.querySelectorAll('span')\n      expect(spans).toHaveLength(2)\n\n      let firstClassName = spans[0].className\n      let secondClassName = spans[1].className\n      expect(firstClassName).toBe(secondClassName)\n      expect(firstClassName).toMatch(/rmxc-/)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <span mix={[css({ color: 'purple' })]}>First</span>\n          <span mix={[css({ color: 'purple' })]}>Second</span>\n        </div>,\n      )\n      root.flush()\n\n      // Both spans should be adopted\n      let hydratedSpans = container.querySelectorAll('span')\n      expect(hydratedSpans[0]).toBe(spans[0])\n      expect(hydratedSpans[1]).toBe(spans[1])\n\n      expect(hydratedSpans[0].className).toBe(firstClassName)\n      expect(hydratedSpans[1].className).toBe(firstClassName)\n\n      // Style should apply to both\n      expect(getComputedStyle(hydratedSpans[0]).color).toBe('rgb(128, 0, 128)')\n      expect(getComputedStyle(hydratedSpans[1]).color).toBe('rgb(128, 0, 128)')\n    })\n\n    it('handles element unmount with css mixin after hydration', async () => {\n      let html = await renderToString(\n        <div>\n          <span mix={[css({ color: 'orange' })]}>Will unmount</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <span mix={[css({ color: 'orange' })]}>Will unmount</span>\n        </div>,\n      )\n      root.flush()\n\n      expect(container.querySelector('span')).not.toBe(null)\n\n      // Remove the span\n      root.render(<div />)\n      root.flush()\n\n      // Span should be gone\n      expect(container.querySelector('span')).toBe(null)\n    })\n\n    it('adds css mixin during hydration when server had none', async () => {\n      let html = await renderToString(<div>Hello</div>)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      expect(existingDiv.className).toBe('')\n\n      // Client adds css mixin\n      let root = createRoot(container)\n      root.render(<div mix={[css({ color: 'cyan' })]}>Hello</div>)\n      root.flush()\n\n      // Element should be adopted and css applied\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.className).toMatch(/rmxc-/)\n      expect(getComputedStyle(existingDiv).color).toBe('rgb(0, 255, 255)')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/hydration.extra-nodes.test.tsx",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { renderToString } from '../lib/stream.ts'\nimport { invariant } from '../lib/invariant.ts'\n\ndescribe('hydration', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n  })\n\n  describe('extra DOM nodes (browser extension injection)', () => {\n    it('ignores extra nodes at the end of container', async () => {\n      let html = await renderToString(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      // Simulate browser extension injecting content at the end\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      let existingSpan = container.querySelector('span')\n      invariant(existingSpan)\n\n      let injected = document.createElement('aside')\n      injected.id = 'ext-injected'\n      injected.textContent = 'extension content'\n      existingDiv.appendChild(injected)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      root.flush()\n\n      // Our content should be adopted\n      expect(container.querySelector('span')).toBe(existingSpan)\n      // Injected content should still be there\n      expect(existingDiv.querySelector('#ext-injected')).toBe(injected)\n    })\n\n    it('skips injected node at start and adopts our content', async () => {\n      let html = await renderToString(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      let existingSpan = container.querySelector('span')\n      invariant(existingSpan)\n\n      // Simulate browser extension injecting content at the START\n      let injected = document.createElement('aside')\n      injected.id = 'ext-start'\n      injected.textContent = 'extension content'\n      existingDiv.insertBefore(injected, existingSpan)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      root.flush()\n\n      // Our span should be adopted (cursor advanced past injected aside)\n      expect(container.querySelector('span')).toBe(existingSpan)\n      // Injected content should still be there\n      expect(existingDiv.querySelector('#ext-start')).toBe(injected)\n    })\n\n    it('handles injected nodes at both start and end', async () => {\n      let html = await renderToString(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      let existingSpan = container.querySelector('span')\n      invariant(existingSpan)\n\n      // Inject at start\n      let injectedStart = document.createElement('aside')\n      injectedStart.id = 'ext-start'\n      injectedStart.textContent = 'start extension'\n      existingDiv.insertBefore(injectedStart, existingSpan)\n\n      // Inject at end\n      let injectedEnd = document.createElement('aside')\n      injectedEnd.id = 'ext-end'\n      injectedEnd.textContent = 'end extension'\n      existingDiv.appendChild(injectedEnd)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      root.flush()\n\n      // Our span should be adopted\n      expect(container.querySelector('span')).toBe(existingSpan)\n      // Both injected elements should remain\n      expect(existingDiv.querySelector('#ext-start')).toBe(injectedStart)\n      expect(existingDiv.querySelector('#ext-end')).toBe(injectedEnd)\n    })\n\n    it('extra nodes survive through subsequent updates', async () => {\n      let html = await renderToString(\n        <div>\n          <span>Content 1</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      // Inject at end\n      let injected = document.createElement('aside')\n      injected.id = 'extension'\n      injected.textContent = 'extension content'\n      existingDiv.appendChild(injected)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <span>Content 1</span>\n        </div>,\n      )\n      root.flush()\n\n      expect(existingDiv.querySelector('#extension')).toBe(injected)\n\n      // Update our content\n      root.render(\n        <div>\n          <span>Content 2</span>\n        </div>,\n      )\n      root.flush()\n\n      // Injected content should still be there after update\n      expect(existingDiv.querySelector('#extension')).toBe(injected)\n      expect(existingDiv.querySelector('span')?.textContent).toBe('Content 2')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/hydration.forms.test.tsx",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { renderToString } from '../lib/stream.ts'\nimport { invariant } from '../lib/invariant.ts'\n\ndescribe('hydration', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n  })\n\n  describe('form elements', () => {\n    it('hydrates input with value attribute', async () => {\n      let html = await renderToString(<input type=\"text\" value=\"server value\" readOnly />)\n      container.innerHTML = html\n\n      let existingInput = container.querySelector('input')\n      invariant(existingInput)\n\n      let root = createRoot(container)\n      root.render(<input type=\"text\" value=\"server value\" readOnly />)\n      root.flush()\n\n      expect(container.querySelector('input')).toBe(existingInput)\n      expect(existingInput.value).toBe('server value')\n    })\n\n    it('hydrates input with defaultValue', async () => {\n      let html = await renderToString(<input type=\"text\" defaultValue=\"default\" />)\n      container.innerHTML = html\n\n      let existingInput = container.querySelector('input')\n      invariant(existingInput)\n\n      let root = createRoot(container)\n      root.render(<input type=\"text\" defaultValue=\"default\" />)\n      root.flush()\n\n      expect(container.querySelector('input')).toBe(existingInput)\n      expect(existingInput.value).toBe('default')\n    })\n\n    it('hydrates checkbox with checked attribute', async () => {\n      let html = await renderToString(<input type=\"checkbox\" checked readOnly />)\n      container.innerHTML = html\n\n      let existingInput = container.querySelector('input')\n      invariant(existingInput)\n\n      let root = createRoot(container)\n      root.render(<input type=\"checkbox\" checked readOnly />)\n      root.flush()\n\n      expect(container.querySelector('input')).toBe(existingInput)\n      expect(existingInput.checked).toBe(true)\n    })\n\n    it('hydrates checkbox with defaultChecked', async () => {\n      let html = await renderToString(<input type=\"checkbox\" defaultChecked />)\n      container.innerHTML = html\n\n      let existingInput = container.querySelector('input')\n      invariant(existingInput)\n\n      let root = createRoot(container)\n      root.render(<input type=\"checkbox\" defaultChecked />)\n      root.flush()\n\n      expect(container.querySelector('input')).toBe(existingInput)\n      expect(existingInput.checked).toBe(true)\n    })\n\n    it('hydrates radio with checked attribute', async () => {\n      let html = await renderToString(\n        <div>\n          <input type=\"radio\" name=\"choice\" value=\"a\" checked readOnly />\n          <input type=\"radio\" name=\"choice\" value=\"b\" readOnly />\n        </div>,\n      )\n      container.innerHTML = html\n\n      let inputs = container.querySelectorAll('input')\n      expect(inputs[0].checked).toBe(true)\n      expect(inputs[1].checked).toBe(false)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <input type=\"radio\" name=\"choice\" value=\"a\" checked readOnly />\n          <input type=\"radio\" name=\"choice\" value=\"b\" readOnly />\n        </div>,\n      )\n      root.flush()\n\n      let hydratedInputs = container.querySelectorAll('input')\n      expect(hydratedInputs[0]).toBe(inputs[0])\n      expect(hydratedInputs[1]).toBe(inputs[1])\n      expect(hydratedInputs[0].checked).toBe(true)\n      expect(hydratedInputs[1].checked).toBe(false)\n    })\n\n    it('hydrates textarea with value', async () => {\n      let html = await renderToString(<textarea value=\"textarea content\" readOnly />)\n      container.innerHTML = html\n\n      let existingTextarea = container.querySelector('textarea')\n      invariant(existingTextarea)\n\n      let root = createRoot(container)\n      root.render(<textarea value=\"textarea content\" readOnly />)\n      root.flush()\n\n      expect(container.querySelector('textarea')).toBe(existingTextarea)\n      expect(existingTextarea.value).toBe('textarea content')\n    })\n\n    it('hydrates select with selected option', async () => {\n      let html = await renderToString(\n        <select value=\"b\">\n          <option value=\"a\">A</option>\n          <option value=\"b\">B</option>\n          <option value=\"c\">C</option>\n        </select>,\n      )\n      container.innerHTML = html\n\n      let existingSelect = container.querySelector('select')\n      invariant(existingSelect)\n\n      let root = createRoot(container)\n      root.render(\n        <select value=\"b\">\n          <option value=\"a\">A</option>\n          <option value=\"b\">B</option>\n          <option value=\"c\">C</option>\n        </select>,\n      )\n      root.flush()\n\n      expect(container.querySelector('select')).toBe(existingSelect)\n      expect(existingSelect.value).toBe('b')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/hydration.mismatch.test.tsx",
    "content": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { renderToString } from '../lib/stream.ts'\nimport { invariant } from '../lib/invariant.ts'\n\ndescribe('hydration', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n  })\n\n  describe('attribute mismatch handling', () => {\n    it('adopts element and patches mismatched attributes', async () => {\n      let html = await renderToString(<div className=\"server-class\" data-value=\"server\" />)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<div className=\"client-class\" data-value=\"client\" />)\n      root.flush()\n\n      // Same DOM node should be adopted (not recreated)\n      expect(container.querySelector('div')).toBe(existingDiv)\n      // Attributes should be patched to client values\n      expect(existingDiv.getAttribute('class')).toBe('client-class')\n      expect(existingDiv.getAttribute('data-value')).toBe('client')\n    })\n\n    it('adds missing attributes during hydration', async () => {\n      let html = await renderToString(<div className=\"existing\" />)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<div className=\"existing\" data-new=\"added\" title=\"hello\" />)\n      root.flush()\n\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.getAttribute('data-new')).toBe('added')\n      expect(existingDiv.getAttribute('title')).toBe('hello')\n    })\n\n    it('leaves extra attributes alone during hydration', async () => {\n      let html = await renderToString(<div className=\"keep\" data-extra=\"yes\" title=\"extra\" />)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      expect(existingDiv.getAttribute('data-extra')).toBe('yes')\n      expect(existingDiv.getAttribute('title')).toBe('extra')\n\n      let root = createRoot(container)\n      root.render(<div className=\"keep\" />)\n      root.flush()\n\n      // Element should be adopted\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.getAttribute('class')).toBe('keep')\n      // Extra attributes left alone during hydration (not tracked, so not removed)\n      expect(existingDiv.hasAttribute('data-extra')).toBe(true)\n      expect(existingDiv.hasAttribute('title')).toBe(true)\n    })\n\n    it('preserves DOM node identity when only attributes differ', async () => {\n      let html = await renderToString(\n        <div id=\"test\" className=\"old\" data-value=\"old\">\n          <span>Child</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('#test')\n      let existingSpan = container.querySelector('span')\n      invariant(existingDiv && existingSpan)\n\n      let root = createRoot(container)\n      root.render(\n        <div id=\"test\" className=\"new\" data-value=\"new\">\n          <span>Child</span>\n        </div>,\n      )\n      root.flush()\n\n      // Both parent and child should be the same DOM nodes\n      expect(container.querySelector('#test')).toBe(existingDiv)\n      expect(container.querySelector('span')).toBe(existingSpan)\n    })\n  })\n\n  describe('type mismatch handling', () => {\n    it('advances cursor once on type mismatch to find our element', async () => {\n      let html = await renderToString(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      let existingSpan = container.querySelector('span')\n      invariant(existingSpan)\n\n      // Inject different element type at start\n      let injected = document.createElement('div')\n      injected.className = 'injected'\n      existingDiv.insertBefore(injected, existingSpan)\n\n      // Suppress console.error for expected hydration mismatch log\n      let errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      root.flush()\n\n      errorSpy.mockRestore()\n\n      // Our span should be adopted after advancing past injected div\n      expect(container.querySelector('span')).toBe(existingSpan)\n    })\n\n    it('recreates element if retry also fails', async () => {\n      let html = await renderToString(\n        <div>\n          <span>Original</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      let existingSpan = container.querySelector('span')\n      invariant(existingSpan)\n\n      // Replace span with completely different structure\n      existingDiv.innerHTML = '<div>Wrong</div><p>Also wrong</p>'\n\n      let errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <span>Original</span>\n        </div>,\n      )\n      root.flush()\n\n      errorSpy.mockRestore()\n\n      // Should have created a new span since no match found\n      let newSpan = container.querySelector('span')\n      expect(newSpan).not.toBe(existingSpan)\n      expect(newSpan?.textContent).toBe('Original')\n    })\n\n    it('leaves skipped nodes in place', async () => {\n      let html = await renderToString(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n      let existingSpan = container.querySelector('span')\n      invariant(existingSpan)\n\n      // Inject element that will be skipped\n      let skipped = document.createElement('aside')\n      skipped.id = 'skipped'\n      skipped.textContent = 'Extension content'\n      existingDiv.insertBefore(skipped, existingSpan)\n\n      let errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <span>Our content</span>\n        </div>,\n      )\n      root.flush()\n\n      errorSpy.mockRestore()\n\n      // Skipped element should still be in the DOM\n      expect(existingDiv.querySelector('#skipped')).toBe(skipped)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/hydration.text.test.tsx",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { renderToString } from '../lib/stream.ts'\nimport { invariant } from '../lib/invariant.ts'\n\ndescribe('hydration', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n  })\n\n  describe('text node handling', () => {\n    it('adopts single server text node when client has multiple text children', async () => {\n      // Server renders \"Hello world\" as single text node\n      let html = await renderToString(<span>Hello world</span>)\n      container.innerHTML = html\n\n      let existingSpan = container.querySelector('span')\n      invariant(existingSpan)\n      let originalTextNode = existingSpan.firstChild\n      invariant(originalTextNode instanceof Text)\n\n      // Client has two text children: [\"Hello \", \"world\"]\n      let root = createRoot(container)\n      root.render(\n        <span>\n          {'Hello '}\n          {'world'}\n        </span>,\n      )\n      root.flush()\n\n      // Span should be adopted\n      expect(container.querySelector('span')).toBe(existingSpan)\n      // Text content should match (even if internal structure differs)\n      expect(existingSpan.textContent).toBe('Hello world')\n    })\n\n    it('subsequent update patches consolidated text content', async () => {\n      let html = await renderToString(<span>Hello world</span>)\n      container.innerHTML = html\n\n      let existingSpan = container.querySelector('span')\n      invariant(existingSpan)\n\n      let name = 'world'\n      function render() {\n        root.render(\n          <span>\n            {'Hello '}\n            {name}\n          </span>,\n        )\n        root.flush()\n      }\n\n      let root = createRoot(container)\n      render()\n\n      expect(existingSpan.textContent).toBe('Hello world')\n\n      // Update the dynamic part\n      name = 'Ryan'\n      render()\n\n      expect(existingSpan.textContent).toBe('Hello Ryan')\n    })\n\n    it('handles null children as empty text', async () => {\n      let html = await renderToString(<div>{null}</div>)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<div>{null}</div>)\n      root.flush()\n\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.textContent).toBe('')\n    })\n\n    it('handles undefined children as empty text', async () => {\n      let html = await renderToString(<div>{undefined}</div>)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<div>{undefined}</div>)\n      root.flush()\n\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.textContent).toBe('')\n    })\n\n    it('handles false children as empty text', async () => {\n      let html = await renderToString(<div>{false}</div>)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<div>{false}</div>)\n      root.flush()\n\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.textContent).toBe('')\n    })\n\n    it('handles true children as empty text', async () => {\n      let html = await renderToString(<div>{true}</div>)\n      container.innerHTML = html\n\n      let existingDiv = container.querySelector('div')\n      invariant(existingDiv)\n\n      let root = createRoot(container)\n      root.render(<div>{true}</div>)\n      root.flush()\n\n      expect(container.querySelector('div')).toBe(existingDiv)\n      expect(existingDiv.textContent).toBe('')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/hydration.void-elements.test.tsx",
    "content": "import { describe, it, expect, beforeEach, afterEach } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { renderToString } from '../lib/stream.ts'\nimport { invariant } from '../lib/invariant.ts'\n\ndescribe('hydration', () => {\n  let container: HTMLDivElement\n\n  beforeEach(() => {\n    container = document.createElement('div')\n    document.body.appendChild(container)\n  })\n\n  afterEach(() => {\n    document.body.innerHTML = ''\n    for (let node of Array.from(document.head.childNodes)) {\n      document.head.removeChild(node)\n    }\n  })\n\n  describe('self-closing/void elements', () => {\n    it('hydrates input element', async () => {\n      let html = await renderToString(<input type=\"text\" placeholder=\"Enter text\" />)\n      container.innerHTML = html\n\n      let existingInput = container.querySelector('input')\n      invariant(existingInput)\n\n      let root = createRoot(container)\n      root.render(<input type=\"text\" placeholder=\"Enter text\" />)\n      root.flush()\n\n      expect(container.querySelector('input')).toBe(existingInput)\n      expect(existingInput.placeholder).toBe('Enter text')\n    })\n\n    it('hydrates br element', async () => {\n      let html = await renderToString(\n        <div>\n          Line 1<br />\n          Line 2\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingBr = container.querySelector('br')\n      invariant(existingBr)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          Line 1<br />\n          Line 2\n        </div>,\n      )\n      root.flush()\n\n      expect(container.querySelector('br')).toBe(existingBr)\n    })\n\n    it('hydrates img element', async () => {\n      let html = await renderToString(<img src=\"/image.png\" alt=\"Test image\" />)\n      container.innerHTML = html\n\n      let existingImg = container.querySelector('img')\n      invariant(existingImg)\n\n      let root = createRoot(container)\n      root.render(<img src=\"/image.png\" alt=\"Test image\" />)\n      root.flush()\n\n      expect(container.querySelector('img')).toBe(existingImg)\n      expect(existingImg.getAttribute('src')).toBe('/image.png')\n      expect(existingImg.getAttribute('alt')).toBe('Test image')\n    })\n\n    it('hydrates hr element', async () => {\n      let html = await renderToString(\n        <div>\n          <p>Above</p>\n          <hr />\n          <p>Below</p>\n        </div>,\n      )\n      container.innerHTML = html\n\n      let existingHr = container.querySelector('hr')\n      invariant(existingHr)\n\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <p>Above</p>\n          <hr />\n          <p>Below</p>\n        </div>,\n      )\n      root.flush()\n\n      expect(container.querySelector('hr')).toBe(existingHr)\n    })\n\n    it('hydrates meta element', async () => {\n      let html = await renderToString(<meta name=\"description\" content=\"Test page\" />)\n      container.innerHTML = html\n\n      let existingMeta = container.querySelector('meta')\n      invariant(existingMeta)\n\n      let root = createRoot(container)\n      root.render(<meta name=\"description\" content=\"Test page\" />)\n      root.flush()\n\n      expect(container.querySelector('meta')).toBe(existingMeta)\n      expect(document.head.querySelector('meta')).toBeNull()\n      expect(existingMeta.getAttribute('name')).toBe('description')\n      expect(existingMeta.getAttribute('content')).toBe('Test page')\n    })\n\n    it('hydrates link element', async () => {\n      let html = await renderToString(<link rel=\"stylesheet\" href=\"/styles.css\" />)\n      container.innerHTML = html\n\n      let existingLink = container.querySelector('link')\n      invariant(existingLink)\n\n      let root = createRoot(container)\n      root.render(<link rel=\"stylesheet\" href=\"/styles.css\" />)\n      root.flush()\n\n      expect(container.querySelector('link')).toBe(existingLink)\n      expect(document.head.querySelector('link')).toBeNull()\n      expect(existingLink.getAttribute('rel')).toBe('stylesheet')\n      expect(existingLink.getAttribute('href')).toBe('/styles.css')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/jsx.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport type { Assert, Equal } from './utils'\nimport type { Handle } from '../lib/component'\nimport { animateLayout, createMixin, on, ref } from '../index.ts'\nimport type { Dispatched, MixinHandle, Props } from '../index.ts'\n\ntype MixItem<mix> = mix extends ReadonlyArray<infer descriptor> ? descriptor : mix\ntype NormalizedMix<mix> = Array<MixItem<Exclude<mix, undefined>>> | undefined\n\ndescribe('jsx', () => {\n  it('creates an element', () => {\n    let element = <div>Hello, world!</div>\n    expect(element.type).toBe('div')\n    expect(element.props.children).toEqual('Hello, world!')\n  })\n\n  it('warns when the wrong type of a prop is used', () => {\n    let element = <a target=\"_blank\">Hello, world!</a>\n\n    // @ts-expect-error - wrong type\n    let badElement = <a target={123}>Hello, world!</a>\n  })\n\n  describe('intrinsic elements', () => {\n    it('uses literal types for element props', () => {\n      let good = <button type=\"button\">Click me</button>\n      // @ts-expect-error - wrong type\n      let bad = <button type=\"lol\">Click me</button>\n    })\n\n    it('infers the event target type from the element type', () => {\n      let element = (\n        <button\n          mix={[\n            on('pointerdown', (event) => {\n              type dispatchedEvent = Assert<\n                Equal<typeof event, Dispatched<PointerEvent, HTMLButtonElement>>\n              >\n              type eventTarget = Assert<Equal<typeof event.currentTarget, HTMLButtonElement>>\n            }),\n          ]}\n        >\n          Click me\n        </button>\n      )\n    })\n  })\n\n  describe('library managed attributes', () => {\n    it('infers component setup and props', () => {\n      function Counter(handle: Handle, setup: number) {\n        let count = setup\n\n        return (props: { label: string }) => {\n          // no `props.setup`\n          type componentProps = Assert<Equal<typeof props, { label: string }>>\n          return (\n            <button\n              mix={[\n                on('click', () => {\n                  count++\n                  handle.update()\n                }),\n              ]}\n            >\n              {props.label} {count}\n            </button>\n          )\n        }\n      }\n\n      let good = <Counter setup={10} label=\"Count\" />\n      // @ts-expect-error - wrong type\n      let bad = <Counter setup={{ initial: 10 }} label={10} />\n    })\n\n    it('infers component setup and props with context', () => {\n      function Counter(handle: Handle<number>, setup: number) {\n        let count = setup\n\n        return (props: { label: string }) => {\n          handle.context.set(count)\n          // no `props.setup`\n          type componentProps = Assert<Equal<typeof props, { label: string }>>\n          return (\n            <button\n              mix={[\n                on('click', () => {\n                  count++\n                  handle.update()\n                }),\n              ]}\n            >\n              {props.label} {count}\n            </button>\n          )\n        }\n      }\n\n      let good = <Counter setup={10} label=\"Count\" />\n    })\n\n    it('accepts single or array mix values for component JSX while render props see arrays', () => {\n      let passthrough = createMixin((_handle) => {})\n\n      function Button() {\n        return (props: Props<'button'>) => {\n          type normalizedMix = Assert<\n            Equal<typeof props.mix, NormalizedMix<JSX.IntrinsicElements['button']['mix']>>\n          >\n          return <button {...props} />\n        }\n      }\n\n      let descriptor = passthrough()\n      let withSingle = <Button mix={descriptor} />\n      let withArray = <Button mix={[descriptor]} />\n      let withoutMix = <Button />\n\n      expect(withSingle.props.mix).toEqual([descriptor])\n      expect(withArray.props.mix).toEqual([descriptor])\n      expect(withoutMix.props.mix).toBeUndefined()\n    })\n  })\n\n  describe('mixins', () => {\n    it('infers mixin usage from scoped callback annotations without top-level generics', () => {\n      let buttonOnly = createMixin(\n        (handle: MixinHandle<HTMLButtonElement, Props<'button'>>) => (props: Props<'button'>) => {\n          type inferredButtonProps = Assert<Equal<typeof props, Props<'button'>>>\n          return <handle.element {...props} />\n        },\n      )\n\n      let good = <button mix={[buttonOnly()]} />\n      // @ts-expect-error button-scoped mixin should not apply to div\n      let bad = <div mix={[buttonOnly()]} />\n    })\n\n    it('allows optional explicit narrowing for specific element kinds', () => {\n      let inputOnly = createMixin<HTMLInputElement>((_handle) => {})\n\n      let good = <input mix={[inputOnly()]} />\n      // @ts-expect-error input-only mixin should not apply to button\n      let bad = <button mix={[inputOnly()]} />\n    })\n\n    it('infers insert event node type from createMixin node generic', () => {\n      let inputOnly = createMixin<HTMLInputElement>((handle) => {\n        handle.addEventListener('insert', (event) => {\n          type inferredInsertNode = Assert<Equal<typeof event.node, HTMLInputElement>>\n        })\n      })\n\n      let good = <input mix={[inputOnly()]} />\n    })\n\n    it('infers on mixin event/currentTarget types from host context', () => {\n      let direct = (\n        <button\n          mix={[\n            on('pointerdown', (event, signal) => {\n              type inferredEvent = Assert<\n                Equal<typeof event, Dispatched<PointerEvent, HTMLButtonElement>>\n              >\n              type inferredTarget = Assert<Equal<typeof event.currentTarget, HTMLButtonElement>>\n              type inferredSignal = Assert<Equal<typeof signal, AbortSignal>>\n            }),\n          ]}\n        />\n      )\n\n      let withOnMixin = createMixin<HTMLElement>((handle) => (props: Props<'div'>) => (\n        <handle.element\n          {...props}\n          mix={[\n            on('pointerdown', (event, signal) => {\n              type inferredEvent = Assert<\n                Equal<typeof event, Dispatched<PointerEvent, HTMLElement>>\n              >\n              type inferredTarget = Assert<Equal<typeof event.currentTarget, HTMLElement>>\n              type inferredSignal = Assert<Equal<typeof signal, AbortSignal>>\n            }),\n          ]}\n        />\n      ))\n\n      let applied = <div mix={[withOnMixin()]} />\n    })\n\n    it('infers ref mixin node type from host context', () => {\n      let element = (\n        <button\n          mix={[\n            ref((node, signal) => {\n              type inferredNode = Assert<Equal<typeof node, HTMLButtonElement>>\n              type inferredSignal = Assert<Equal<typeof signal, AbortSignal>>\n            }),\n          ]}\n        />\n      )\n    })\n\n    it('accepts animateLayout mixin usage', () => {\n      let element = (\n        <div mix={[animateLayout(), animateLayout({ duration: 300, easing: 'linear' })]} />\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/navigation.test.ts",
    "content": "import { afterEach, describe, expect, it, vi } from 'vitest'\n\nvi.mock('../lib/run.ts', () => ({\n  getTopFrame() {\n    return {\n      src: '',\n      reload: async () => {},\n    }\n  },\n  getNamedFrame() {\n    return {\n      src: '',\n      reload: async () => {},\n    }\n  },\n}))\n\nimport { navigate, startNavigationListener } from '../lib/navigation.ts'\n\ndescribe('navigate', () => {\n  afterEach(() => {\n    document.body.innerHTML = ''\n    vi.unstubAllGlobals()\n  })\n\n  it('passes runtime state via navigate history state', async () => {\n    let navigateMock = vi.fn(() => ({ finished: Promise.resolve() }))\n    vi.stubGlobal('navigation', { navigate: navigateMock })\n\n    await navigate('/login', {\n      src: '/partials/login',\n      target: 'auth',\n      history: 'replace',\n    })\n\n    expect(navigateMock).toHaveBeenCalledWith('/login', {\n      state: { target: 'auth', src: '/partials/login', resetScroll: true, $rmx: true },\n      history: 'replace',\n    })\n  })\n\n  it('passes resetScroll=false when requested', async () => {\n    let navigateMock = vi.fn(() => ({ finished: Promise.resolve() }))\n    vi.stubGlobal('navigation', { navigate: navigateMock })\n\n    await navigate('/login', {\n      resetScroll: false,\n    })\n\n    expect(navigateMock).toHaveBeenCalledWith('/login', {\n      state: { target: undefined, src: '/login', resetScroll: false, $rmx: true },\n      history: undefined,\n    })\n  })\n\n  it('does not intercept anchors marked for document navigation', () => {\n    let navigation = Object.assign(new EventTarget(), {\n      navigate: vi.fn(() => ({ finished: Promise.resolve() })),\n      updateCurrentEntry: vi.fn(),\n    })\n    vi.stubGlobal('navigation', navigation)\n\n    let controller = new AbortController()\n    startNavigationListener(controller.signal)\n\n    let anchor = document.createElement('a')\n    anchor.href = '/login'\n    anchor.setAttribute('rmx-document', '')\n    document.body.append(anchor)\n    anchor.addEventListener('click', (event) => event.preventDefault())\n\n    let clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true })\n    anchor.dispatchEvent(clickEvent)\n\n    expect(navigation.navigate).not.toHaveBeenCalled()\n    expect(clickEvent.defaultPrevented).toBe(true)\n\n    anchor.remove()\n    controller.abort()\n  })\n\n  it('does not intercept anchors marked for download', () => {\n    let navigation = Object.assign(new EventTarget(), {\n      navigate: vi.fn(() => ({ finished: Promise.resolve() })),\n      updateCurrentEntry: vi.fn(),\n    })\n    vi.stubGlobal('navigation', navigation)\n\n    let controller = new AbortController()\n    startNavigationListener(controller.signal)\n\n    let anchor = document.createElement('a')\n    anchor.href = '/report.csv'\n    anchor.setAttribute('download', '')\n    document.body.append(anchor)\n    anchor.addEventListener('click', (event) => event.preventDefault())\n\n    let clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true })\n    anchor.dispatchEvent(clickEvent)\n\n    expect(navigation.navigate).not.toHaveBeenCalled()\n    expect(clickEvent.defaultPrevented).toBe(true)\n\n    anchor.remove()\n    controller.abort()\n  })\n\n  it('intercepts anchors when sourceElement is a nested svg node', () => {\n    let navigateListener: EventListener | undefined\n    let navigation = {\n      navigate: vi.fn(() => ({ finished: Promise.resolve() })),\n      updateCurrentEntry: vi.fn(),\n      addEventListener(type: string, listener: EventListener) {\n        if (type === 'navigate') {\n          navigateListener = listener\n        }\n      },\n    }\n    vi.stubGlobal('navigation', navigation)\n\n    let controller = new AbortController()\n    startNavigationListener(controller.signal)\n\n    let anchor = document.createElement('a')\n    anchor.href = '/logo'\n    let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')\n    let path = document.createElementNS('http://www.w3.org/2000/svg', 'path')\n    svg.append(path)\n    anchor.append(svg)\n\n    let intercept = vi.fn()\n    let event = Object.assign(new Event('navigate'), {\n      canIntercept: true,\n      navigationType: 'push',\n      sourceElement: path,\n      destination: {\n        url: 'https://example.com/logo',\n        key: 'next',\n        getState: () => undefined,\n      },\n      intercept,\n    })\n\n    navigateListener?.(event)\n\n    expect(intercept).toHaveBeenCalledTimes(1)\n\n    controller.abort()\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/spring.test.ts",
    "content": "import { describe, it, expect } from 'vitest'\n\nimport { spring } from '../lib/spring.ts'\n\ndescribe('spring', () => {\n  describe('interface', () => {\n    it('returns an iterator', () => {\n      let s = spring()\n      expect(typeof s.next).toBe('function')\n\n      let result = s.next()\n      expect(result).toHaveProperty('value')\n      expect(result).toHaveProperty('done')\n    })\n\n    it('has duration property', () => {\n      let s = spring()\n      expect(typeof s.duration).toBe('number')\n      expect(s.duration).toBeGreaterThan(0)\n      expect(s.duration).toBeLessThan(20000)\n    })\n\n    it('has easing property', () => {\n      let s = spring()\n      expect(typeof s.easing).toBe('string')\n      expect(s.easing).toMatch(/^linear\\(/)\n      expect(s.easing).toMatch(/\\)$/)\n    })\n\n    it('has toString that returns CSS value', () => {\n      let s = spring()\n      let str = s.toString()\n      expect(str).toMatch(/^\\d+ms linear\\(/)\n      expect(str).toMatch(/\\)$/)\n    })\n\n    it('can be spread for WAAPI', () => {\n      let s = spring()\n      let spread = { ...s }\n      expect(spread).toHaveProperty('duration')\n      expect(spread).toHaveProperty('easing')\n      expect(typeof spread.duration).toBe('number')\n      expect(typeof spread.easing).toBe('string')\n    })\n\n    it('works in template literals', () => {\n      let s = spring()\n      let css = `transform ${s}`\n      expect(css).toMatch(/^transform \\d+ms linear\\(/)\n    })\n  })\n\n  describe('presets', () => {\n    it('accepts bouncy preset', () => {\n      let s = spring('bouncy')\n      expect(s.duration).toBeGreaterThan(0)\n    })\n\n    it('accepts snappy preset', () => {\n      let s = spring('snappy')\n      expect(s.duration).toBeGreaterThan(0)\n    })\n\n    it('accepts smooth preset', () => {\n      let s = spring('smooth')\n      expect(s.duration).toBeGreaterThan(0)\n    })\n\n    it('defaults to snappy when no args', () => {\n      let defaultSpring = spring()\n      let snappySpring = spring('snappy')\n      expect(defaultSpring.duration).toBe(snappySpring.duration)\n    })\n\n    it('allows duration override on presets', () => {\n      let s = spring('bouncy', { duration: 1000 })\n      // Should be longer than default bouncy\n      expect(s.duration).toBeGreaterThan(spring('bouncy').duration)\n    })\n\n    it('exposes preset defaults via spring.presets', () => {\n      expect(spring.presets).toHaveProperty('smooth')\n      expect(spring.presets).toHaveProperty('snappy')\n      expect(spring.presets).toHaveProperty('bouncy')\n      expect(spring.presets.bouncy).toEqual({ duration: 400, bounce: 0.3 })\n    })\n  })\n\n  describe('custom options', () => {\n    it('accepts custom duration', () => {\n      let short = spring({ duration: 100 })\n      let long = spring({ duration: 500 })\n      expect(long.duration).toBeGreaterThan(short.duration)\n    })\n\n    it('accepts custom bounce', () => {\n      let s = spring({ bounce: 0.5 })\n      expect(s.duration).toBeGreaterThan(0)\n    })\n\n    it('accepts custom velocity', () => {\n      let s = spring({ velocity: 5 })\n      expect(s.duration).toBeGreaterThan(0)\n    })\n  })\n\n  describe('physics invariants', () => {\n    it('starts near 0', () => {\n      let s = spring()\n      let first = s.next().value\n      expect(first).toBeCloseTo(0, 1)\n    })\n\n    it('ends at 1 when iteration completes', () => {\n      let s = spring()\n      let last = 0\n      for (let value of s) {\n        last = value\n      }\n      expect(last).toBe(1)\n    })\n\n    it('underdamped (bounce > 0) overshoots target', () => {\n      let s = spring({ bounce: 0.5 })\n      let maxValue = 0\n      for (let value of s) {\n        maxValue = Math.max(maxValue, value)\n      }\n      expect(maxValue).toBeGreaterThan(1)\n    })\n\n    it('critically damped (bounce = 0) never overshoots', () => {\n      let s = spring({ bounce: 0 })\n      for (let value of s) {\n        expect(value).toBeLessThanOrEqual(1.001) // tiny tolerance for float precision\n      }\n    })\n\n    it('overdamped (bounce < 0) never overshoots', () => {\n      let s = spring({ bounce: -0.5 })\n      for (let value of s) {\n        expect(value).toBeLessThanOrEqual(1.001)\n      }\n    })\n\n    it('higher bounce means longer settling time', () => {\n      let low = spring({ duration: 300, bounce: 0.1 })\n      let high = spring({ duration: 300, bounce: 0.7 })\n      expect(high.duration).toBeGreaterThan(low.duration)\n    })\n\n    it('positive velocity causes faster initial movement', () => {\n      let noVelocity = spring({ duration: 300, bounce: 0 })\n      let withVelocity = spring({ duration: 300, bounce: 0, velocity: 10 })\n\n      // Get position at early time (second frame)\n      noVelocity.next()\n      withVelocity.next()\n      let posNoVel = noVelocity.next().value\n      let posWithVel = withVelocity.next().value\n\n      expect(posWithVel).toBeGreaterThan(posNoVel)\n    })\n  })\n\n  describe('spring.transition helper', () => {\n    it('formats single property', () => {\n      let result = spring.transition('transform', 'bouncy')\n      expect(result).toMatch(/^transform \\d+ms linear\\(/)\n    })\n\n    it('formats multiple properties', () => {\n      let result = spring.transition(['transform', 'opacity'], 'snappy')\n      expect(result).toMatch(/^transform \\d+ms linear\\(.+\\), opacity \\d+ms linear\\(/)\n    })\n\n    it('accepts custom options', () => {\n      let result = spring.transition('width', { duration: 500, bounce: 0.2 })\n      expect(result).toMatch(/^width \\d+ms linear\\(/)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/stream.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport type { Handle, RemixNode } from '../lib/component.ts'\nimport { createMixin, css, on } from '../index.ts'\n\nimport { renderToStream, renderToString } from '../lib/stream.ts'\nimport { clientEntry } from '../lib/client-entries.ts'\nimport { drain, readChunks, withResolvers } from './utils.ts'\nimport { Frame } from '../lib/component.ts'\nimport { invariant } from '../lib/invariant.ts'\n\nconst rmxDataScriptSelector = 'script[type=\"application/json\"]#rmx-data'\n\ndescribe('stream', () => {\n  function getLatestRmxDataScript(root: ParentNode): HTMLScriptElement {\n    let scripts = root.querySelectorAll(rmxDataScriptSelector)\n    let script = scripts.item(scripts.length - 1) as HTMLScriptElement | null\n    invariant(script)\n    return script\n  }\n\n  function parseRmxDataFromHtml(html: string): any {\n    let shelf = document.createElement('template')\n    shelf.innerHTML = html\n    let script = getLatestRmxDataScript(shelf.content)\n    return JSON.parse(script.textContent || '{}')\n  }\n\n  function getSingleEntry(obj: Record<string, any>): [string, any] {\n    let entries = Object.entries(obj)\n    expect(entries.length).toBe(1)\n    return entries[0]!\n  }\n\n  function getCommentMarkerId(html: string, prefix: 'rmx:f:' | 'rmx:h:'): string {\n    let re = prefix === 'rmx:f:' ? /<!--\\s*rmx:f:([^ ]+)\\s*-->/ : /<!--\\s*rmx:h:([^ ]+)\\s*-->/\n    let match = html.match(re)\n    expect(match).not.toBeNull()\n    return match![1]!\n  }\n\n  describe('basic nodes', () => {\n    it('should render to a stream', () => {\n      let stream = renderToStream(<div>Hello, world!</div>)\n      expect(stream).toBeDefined()\n    })\n\n    it('streams basic HTML', async () => {\n      let stream = renderToStream(<div>Hello, world!</div>)\n      let html = await drain(stream)\n      expect(html).toBe('<div>Hello, world!</div>')\n    })\n\n    it('renders string nodes', async () => {\n      let stream = renderToStream('Hello, world!')\n      let html = await drain(stream)\n      expect(html).toBe('Hello, world!')\n    })\n\n    it('escapes text node content', async () => {\n      let stream = renderToStream('<img src=x onerror=\"alert(1)\">&</img>')\n      let html = await drain(stream)\n      expect(html).toBe('&lt;img src=x onerror=\"alert(1)\"&gt;&amp;&lt;/img&gt;')\n    })\n\n    it('escapes text children in elements', async () => {\n      let stream = renderToStream(<div>{'<script>alert(1)</script>'}</div>)\n      let html = await drain(stream)\n      expect(html).toBe('<div>&lt;script&gt;alert(1)&lt;/script&gt;</div>')\n    })\n\n    it('renders number nodes', async () => {\n      let stream = renderToStream(42)\n      let html = await drain(stream)\n      expect(html).toBe('42')\n    })\n\n    it('renders 0', async () => {\n      let stream = renderToStream(<span>0</span>)\n      let html = await drain(stream)\n      expect(html).toBe('<span>0</span>')\n    })\n\n    it('renders bigint nodes', async () => {\n      let stream = renderToStream(BigInt(9007199254740991))\n      let html = await drain(stream)\n      expect(html).toBe('9007199254740991')\n    })\n\n    it('renders boolean nodes', async () => {\n      let stream = renderToStream(true)\n      let html = await drain(stream)\n      expect(html).toBe('')\n    })\n\n    it('renders null nodes', async () => {\n      let stream = renderToStream(null)\n      let html = await drain(stream)\n      expect(html).toBe('')\n    })\n\n    it('renders undefined nodes', async () => {\n      let stream = renderToStream(undefined)\n      let html = await drain(stream)\n      expect(html).toBe('')\n    })\n\n    it('renders array of nodes', async () => {\n      let stream = renderToStream([<div>One</div>, <span>Two</span>])\n      let html = await drain(stream)\n      expect(html).toBe('<div>One</div><span>Two</span>')\n    })\n\n    it('renders mixed array of nodes', async () => {\n      let stream = renderToStream([<div>One</div>, 'text', 42, null, undefined])\n      let html = await drain(stream)\n      expect(html).toBe('<div>One</div>text42')\n    })\n\n    it('renders fragments', async () => {\n      let stream = renderToStream(\n        <>\n          <h1>Title</h1>\n          <p>Paragraph</p>\n          <div>Content</div>\n        </>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe('<h1>Title</h1><p>Paragraph</p><div>Content</div>')\n    })\n  })\n\n  describe('component nodes', () => {\n    it('renders component nodes', async () => {\n      function Greeting(handle: Handle) {\n        return ({ name }: { name: string }) => <div>Hello, {name}!</div>\n      }\n      let stream = renderToStream(<Greeting name=\"World\" />)\n      let html = await drain(stream)\n      expect(html).toBe('<div>Hello, World!</div>')\n    })\n\n    it('renders 0', async () => {\n      function Test(handle: Handle) {\n        let n = 0\n        return () => <span>{n}</span>\n      }\n      let stream = renderToStream(<Test />)\n      let html = await drain(stream)\n      expect(html).toBe('<span>0</span>')\n    })\n\n    it('renders stateful component nodes', async () => {\n      function Stateful(handle: Handle) {\n        return () => <div>Stateful</div>\n      }\n      let stream = renderToStream(<Stateful />)\n      let html = await drain(stream)\n      expect(html).toBe('<div>Stateful</div>')\n    })\n\n    it('provides and reads context', async () => {\n      type ThemeContext = { color: string; size: number }\n\n      function ThemeProvider(handle: Handle<ThemeContext>) {\n        handle.context.set({ color: 'blue', size: 16 })\n        return ({ children }: { children: any }) => children\n      }\n\n      function ThemedText(handle: Handle) {\n        let theme = handle.context.get(ThemeProvider)\n        return () => <p style={`color: ${theme.color}; font-size: ${theme.size}px`}>Themed!</p>\n      }\n\n      function App(handle: Handle) {\n        return () => (\n          <ThemeProvider>\n            <div>\n              <ThemedText />\n            </div>\n          </ThemeProvider>\n        )\n      }\n\n      let stream = renderToStream(<App />)\n      let html = await drain(stream)\n      expect(html).toBe('<div><p style=\"color: blue; font-size: 16px\">Themed!</p></div>')\n    })\n\n    it('provides and reads nested context', async () => {\n      type ThemeContext = { color: string }\n      type UserContext = { name: string }\n\n      function ThemeProvider(handle: Handle<ThemeContext>) {\n        handle.context.set({ color: 'red' })\n        return ({ children }: { children: any }) => children\n      }\n\n      function UserProvider(handle: Handle<UserContext>) {\n        handle.context.set({ name: 'John' })\n        return ({ children }: { children: any }) => children\n      }\n\n      function Display(handle: Handle) {\n        let theme = handle.context.get(ThemeProvider)\n        let user = handle.context.get(UserProvider)\n        return () => <p style={`color: ${theme.color}`}>Hello, {user.name}!</p>\n      }\n\n      function App(handle: Handle) {\n        return () => (\n          <ThemeProvider>\n            <UserProvider>\n              <div>\n                <Display />\n              </div>\n            </UserProvider>\n          </ThemeProvider>\n        )\n      }\n\n      let stream = renderToStream(<App />)\n      let html = await drain(stream)\n      expect(html).toBe('<div><p style=\"color: red\">Hello, John!</p></div>')\n    })\n\n    it('provides context to multiple consumers', async () => {\n      type CountContext = { count: number }\n\n      function CountProvider(handle: Handle<CountContext>) {\n        handle.context.set({ count: 42 })\n        return ({ children }: { children: any }) => children\n      }\n\n      function CountDisplay(handle: Handle) {\n        let { count } = handle.context.get(CountProvider)\n        return () => <span>Count: {count}</span>\n      }\n\n      function DoubleDisplay(handle: Handle) {\n        let { count } = handle.context.get(CountProvider)\n        return () => <span>Double: {count * 2}</span>\n      }\n\n      function App(handle: Handle) {\n        return () => (\n          <CountProvider>\n            <div>\n              <CountDisplay />\n              <br />\n              <DoubleDisplay />\n            </div>\n          </CountProvider>\n        )\n      }\n\n      let stream = renderToStream(<App />)\n      let html = await drain(stream)\n      expect(html).toBe('<div><span>Count: 42</span><br /><span>Double: 84</span></div>')\n    })\n\n    it('exposes the current and top frame src during SSR', async () => {\n      let seen:\n        | {\n            frameSrc: string\n            topFrameSrc: string\n            sameFrame: boolean\n          }\n        | undefined\n\n      function Inspect(handle: Handle) {\n        seen = {\n          frameSrc: handle.frame.src,\n          topFrameSrc: handle.frames.top.src,\n          sameFrame: handle.frame === handle.frames.top,\n        }\n\n        return () => <div>{handle.frames.top.src}</div>\n      }\n\n      let html = await drain(\n        renderToStream(<Inspect />, { frameSrc: 'https://example.com/dashboard' }),\n      )\n\n      expect(seen).toEqual({\n        frameSrc: 'https://example.com/dashboard',\n        topFrameSrc: 'https://example.com/dashboard',\n        sameFrame: true,\n      })\n      expect(html).toBe('<div>https://example.com/dashboard</div>')\n    })\n  })\n\n  describe('special props', () => {\n    it('renders innerHTML on elements', async () => {\n      let htmlContent = '<strong>Bold text</strong> and <em>italic text</em>'\n      let stream = renderToStream(\n        <div>\n          <h1>Title</h1>\n          <div innerHTML={htmlContent} />\n          <p>After innerHTML</p>\n        </div>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<div><h1>Title</h1><div><strong>Bold text</strong> and <em>italic text</em></div><p>After innerHTML</p></div>',\n      )\n    })\n\n    it('changes className to class', async () => {\n      let stream = renderToStream(<div className=\"test-class\">Content</div>)\n      let html = await drain(stream)\n      expect(html).toBe('<div class=\"test-class\">Content</div>')\n    })\n\n    it('changes htmlFor to for', async () => {\n      let stream = renderToStream(\n        <>\n          <label htmlFor=\"test-input\">Label</label>\n          <input id=\"test-input\" />\n        </>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe('<label for=\"test-input\">Label</label><input id=\"test-input\" />')\n    })\n\n    it('changes acceptCharset to accept-charset', async () => {\n      let stream = renderToStream(\n        <form acceptCharset=\"UTF-8\">\n          <input type=\"submit\" />\n        </form>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe('<form accept-charset=\"UTF-8\"><input type=\"submit\" /></form>')\n    })\n\n    it('changes httpEquiv to http-equiv', async () => {\n      let stream = renderToStream(\n        <head>\n          <meta httpEquiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n        </head>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /></head>',\n      )\n    })\n\n    it('handles namespaced xlinkHref to xlink:href', async () => {\n      let stream = renderToStream(\n        <svg>\n          <use xlinkHref=\"#icon-star\" />\n        </svg>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe('<svg><use xlink:href=\"#icon-star\"></use></svg>')\n    })\n\n    it(\"lowercases camelCase attributes that don't need special handling\", async () => {\n      let stream = renderToStream(\n        <input autoComplete=\"off\" autoFocus={true} readOnly={true} tabIndex={-1} maxLength={10} />,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<input autocomplete=\"off\" autofocus readonly tabindex=\"-1\" maxlength=\"10\" />',\n      )\n    })\n\n    it('handles table attributes colSpan and rowSpan', async () => {\n      let stream = renderToStream(\n        <table>\n          <tr>\n            <td colSpan={2} rowSpan={3}>\n              Cell\n            </td>\n          </tr>\n        </table>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe('<table><tr><td colspan=\"2\" rowspan=\"3\">Cell</td></tr></table>')\n    })\n\n    it('filters framework-specific props', async () => {\n      let stream = renderToStream(\n        <div>\n          <button key=\"btn-1\" mix={[on('click', () => {})]} type=\"button\">\n            Click me\n          </button>\n          <ul>\n            <li key=\"item-1\">First</li>\n            <li key=\"item-2\">Second</li>\n          </ul>\n        </div>,\n      )\n      let html = await drain(stream)\n\n      // Framework props should not appear in HTML\n      expect(html).not.toContain('key=')\n      expect(html).not.toContain('mix=')\n\n      // But regular HTML attributes should be preserved\n      expect(html).toContain('type=\"button\"')\n      expect(html).toContain('<li>First</li>')\n      expect(html).toContain('<li>Second</li>')\n    })\n\n    it('resolves mixins for host prop composition', async () => {\n      let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (\n        <handle.element {...props} title={title} />\n      ))\n      let appendTitle = createMixin((handle) => (suffix: string, props: { title?: string }) => (\n        <handle.element {...props} title={`${props.title ?? ''}${suffix}`} />\n      ))\n\n      let stream = renderToStream(<div mix={[withTitle('hello'), appendTitle('-world')]} />)\n      let html = await drain(stream)\n\n      expect(html).toBe('<div title=\"hello-world\"></div>')\n      expect(html).not.toContain('mix=')\n    })\n\n    it('supports nested mix descriptors via handle.element', async () => {\n      let withData = createMixin(\n        (handle) => (value: string, props: { ['data-mixed']?: string }) => (\n          <handle.element {...props} data-mixed={value} />\n        ),\n      )\n      let withNested = createMixin(\n        (handle) => (value: string, props: { ['data-mixed']?: string }) => (\n          <handle.element {...props} mix={[withData(value)]} />\n        ),\n      )\n\n      let stream = renderToStream(<div mix={[withNested('nested')]} />)\n      let html = await drain(stream)\n\n      expect(html).toBe('<div data-mixed=\"nested\"></div>')\n      expect(html).not.toContain('mix=')\n    })\n\n    it('ignores lifecycle-only mixin side effects during SSR', async () => {\n      let updateError: unknown\n      let lifecycleOnly = createMixin((handle) => {\n        handle.addEventListener('insert', () => {\n          throw new Error('should not run in SSR')\n        })\n        handle.queueTask(() => {\n          throw new Error('should not run in SSR')\n        })\n        try {\n          void handle.update()\n        } catch (error) {\n          updateError = error\n        }\n\n        return (props: { title?: string }) => <handle.element {...props} title=\"ok\" />\n      })\n\n      let stream = renderToStream(<div mix={[lifecycleOnly()]} />)\n      let html = await drain(stream)\n\n      expect(html).toBe('<div title=\"ok\"></div>')\n      expect(updateError).toBeInstanceOf(Error)\n      expect((updateError as Error).message).toBe('handle.update() is not available during SSR.')\n    })\n\n    it('serializes css mixin styles into style tags and class names', async () => {\n      let stream = renderToStream(\n        <div\n          className=\"base\"\n          mix={[\n            css({ color: 'red' }),\n            css({\n              backgroundColor: 'black',\n            }),\n          ]}\n        />,\n      )\n      let html = await drain(stream)\n\n      expect(html).toContain('<style data-rmx-styles>')\n      expect(html).toMatch(/class=\"base rmxc-[a-z0-9]+ rmxc-[a-z0-9]+\"/)\n      expect(html).toContain('.rmxc-')\n    })\n  })\n\n  describe('svg', () => {\n    it('renders SVG with preserved viewBox and kebab-cased attributes', async () => {\n      let stream = renderToStream(\n        <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"icon\">\n          <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"m4.5 12.75 6 6 9-13.5\" />\n        </svg>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<svg viewBox=\"0 0 24 24\" fill=\"none\" class=\"icon\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4.5 12.75 6 6 9-13.5\"></path></svg>',\n      )\n    })\n\n    it('renders foreignObject subtree as HTML (className -> class)', async () => {\n      let stream = renderToStream(\n        <svg>\n          <foreignObject>\n            <div id=\"x\" className=\"a\">\n              Hello\n            </div>\n          </foreignObject>\n        </svg>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<svg><foreignObject><div id=\"x\" class=\"a\">Hello</div></foreignObject></svg>',\n      )\n    })\n\n    it('renders xmlLang and xmlSpace as xml:lang and xml:space', async () => {\n      let stream = renderToStream(\n        <svg>\n          <text xmlLang=\"en\" xmlSpace=\"preserve\">\n            Hi\n          </text>\n        </svg>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe('<svg><text xml:lang=\"en\" xml:space=\"preserve\">Hi</text></svg>')\n    })\n\n    it('renders canonical camelCase unit attributes for SVG filters and gradients', async () => {\n      let stream = renderToStream(\n        <svg>\n          <defs>\n            <filter id=\"f\" filterUnits=\"userSpaceOnUse\" primitiveUnits=\"objectBoundingBox\">\n              <feGaussianBlur stdDeviation=\"2.5\" />\n            </filter>\n            <linearGradient id=\"g\" gradientUnits=\"userSpaceOnUse\" />\n          </defs>\n        </svg>,\n      )\n      let html = await drain(stream)\n      expect(html).toContain('filterUnits=\"userSpaceOnUse\"')\n      expect(html).toContain('primitiveUnits=\"objectBoundingBox\"')\n      expect(html).toContain('gradientUnits=\"userSpaceOnUse\"')\n      expect(html).not.toContain('filter-units=')\n      expect(html).not.toContain('primitive-units=')\n      expect(html).not.toContain('gradient-units=')\n      expect(html).toContain('stdDeviation=\"2.5\"')\n      expect(html).not.toContain('std-deviation=')\n    })\n  })\n\n  describe('styles', () => {\n    it('handles css mixin with style objects', async () => {\n      let stream = renderToStream(\n        <div mix={[css({ color: 'red', fontSize: '16px' })]}>Styled text</div>,\n      )\n      let html = await drain(stream)\n\n      // Should have a style tag in head\n      expect(html).toContain('<style data-rmx-styles>')\n      expect(html).toContain('.rmxc-')\n      expect(html).toContain('color: red')\n      expect(html).toContain('font-size: 16px')\n\n      expect(html).toMatch(/<div class=\"rmxc-[a-z0-9]+\"/)\n    })\n\n    it('handles string style prop', async () => {\n      let stream = renderToStream(<div style=\"color: blue; font-weight: bold;\">String styled</div>)\n      let html = await drain(stream)\n\n      // String styles should be passed through as-is\n      expect(html).toBe('<div style=\"color: blue; font-weight: bold;\">String styled</div>')\n    })\n\n    it('converts style object to inline string', async () => {\n      let stream = renderToStream(\n        <div style={{ color: 'green', marginTop: 10, padding: '5px' }}>Object styled</div>,\n      )\n      let html = await drain(stream)\n\n      // Style objects should be serialized to inline styles\n      expect(html).toBe(\n        '<div style=\"color: green; margin-top: 10px; padding: 5px;\">Object styled</div>',\n      )\n    })\n\n    it('combines css mixin with existing className', async () => {\n      let stream = renderToStream(\n        <div className=\"existing-class\" mix={[css({ background: 'yellow' })]}>\n          Combined classes\n        </div>,\n      )\n      let html = await drain(stream)\n\n      expect(html).toMatch(/<div class=\"existing-class rmxc-[a-z0-9]+\"/)\n      expect(html).toContain('background: yellow')\n    })\n\n    it('combines css mixin with existing class attribute', async () => {\n      let stream = renderToStream(\n        <div class=\"existing-class\" mix={[css({ background: 'yellow' })]}>\n          Combined classes\n        </div>,\n      )\n      let html = await drain(stream)\n\n      expect(html).toMatch(/<div class=\"existing-class rmxc-[a-z0-9]+\"/)\n      expect(html).toContain('background: yellow')\n    })\n\n    it('deduplicates styles across multiple elements', async () => {\n      let stream = renderToStream(\n        <div>\n          <span mix={[css({ color: 'red', fontSize: '14px' })]}>First</span>\n          <span mix={[css({ color: 'red', fontSize: '14px' })]}>Second</span>\n          <span mix={[css({ color: 'blue' })]}>Third</span>\n        </div>,\n      )\n      let html = await drain(stream)\n\n      // Should only have one instance of the red/14px style\n      let redStyleMatches = html.match(/color: red/g)\n      expect(redStyleMatches?.length).toBe(1)\n\n      // Should have the blue style too\n      expect(html).toContain('color: blue')\n\n      // Both red spans should have same className (same css hash)\n      let spanMatches = html.match(/<span class=\"rmxc-[a-z0-9]+\">/g)\n      expect(spanMatches?.length).toBe(3)\n      // First two spans have same style, third has different\n      expect(spanMatches?.[0]).toBe(spanMatches?.[1])\n      expect(spanMatches?.[0]).not.toBe(spanMatches?.[2])\n    })\n\n    it('places styles in head when html root exists', async () => {\n      let stream = renderToStream(\n        <html>\n          <body>\n            <div mix={[css({ color: 'purple' })]}>Content</div>\n          </body>\n        </html>,\n      )\n      let html = await drain(stream)\n\n      // Style should be in the head section\n      expect(html).toContain('<html><head><style data-rmx-styles>')\n      expect(html).toContain('color: purple')\n      expect(html).toContain('</style></head><body>')\n    })\n\n    it('places styles in head when no html root', async () => {\n      let stream = renderToStream(<div mix={[css({ color: 'orange' })]}>No HTML root</div>)\n      let html = await drain(stream)\n\n      // Style should be in a head element\n      expect(html).toMatch(/^<head><style data-rmx-styles>/)\n      expect(html).toContain('color: orange')\n      expect(html).toMatch(/<\\/style><\\/head><div class=\"rmxc-[a-z0-9]+\">No HTML root<\\/div>$/)\n    })\n\n    it('handles css mixin in components', async () => {\n      function StyledButton(handle: Handle) {\n        return ({ label }: { label: string }) => (\n          <button mix={[css({ background: 'blue', color: 'white', padding: '10px' })]}>\n            {label}\n          </button>\n        )\n      }\n\n      let stream = renderToStream(\n        <div>\n          <StyledButton label=\"Click me\" />\n          <StyledButton label=\"And me\" />\n        </div>,\n      )\n      let html = await drain(stream)\n\n      // Should have the style only once\n      let bgMatches = html.match(/background: blue/g)\n      expect(bgMatches?.length).toBe(1)\n\n      // Both buttons should have the same generated class\n      let buttonMatches = html.match(/<button class=\"rmxc-[a-z0-9]+\"/g)\n      expect(buttonMatches?.length).toBe(2)\n      expect(buttonMatches?.[0]).toBe(buttonMatches?.[1])\n    })\n\n    it('handles empty css mixin', async () => {\n      let stream = renderToStream(<div mix={[css({})]}>Empty css mixin</div>)\n      let html = await drain(stream)\n\n      // Should not add any class or styles\n      expect(html).toBe('<div>Empty css mixin</div>')\n      expect(html).not.toContain('<style')\n      expect(html).not.toContain('class=')\n    })\n\n    it('handles pseudo selectors in css mixin', async () => {\n      let stream = renderToStream(\n        <button\n          mix={[\n            css({\n              color: 'black',\n              ':hover': {\n                color: 'red',\n              },\n              ':focus': {\n                outline: '2px solid blue',\n              },\n            }),\n          ]}\n        >\n          Hover me\n        </button>,\n      )\n      let html = await drain(stream)\n\n      // Should generate hover and focus styles\n      expect(html).toContain(':hover')\n      expect(html).toContain('color: red')\n      expect(html).toContain(':focus')\n      expect(html).toContain('outline: 2px solid blue')\n    })\n\n    it('handles media queries in css mixin', async () => {\n      let stream = renderToStream(\n        <div\n          mix={[\n            css({\n              fontSize: '14px',\n              '@media (min-width: 768px)': {\n                fontSize: '16px',\n              },\n            }),\n          ]}\n        >\n          Responsive text\n        </div>,\n      )\n      let html = await drain(stream)\n\n      // Should generate media query\n      expect(html).toContain('@media (min-width: 768px)')\n      expect(html).toContain('font-size: 16px')\n    })\n\n    it('merges styles with existing head content', async () => {\n      let stream = renderToStream(\n        <html>\n          <head>\n            <title>Page Title</title>\n            <meta charSet=\"utf-8\" />\n          </head>\n          <body>\n            <div mix={[css({ fontWeight: 'bold' })]}>Bold text</div>\n          </body>\n        </html>,\n      )\n      let html = await drain(stream)\n\n      // Styles should be injected into existing head, preserving other content\n      expect(html).toContain('<head>')\n      expect(html).toContain('<style data-rmx-styles>')\n      expect(html).toContain('font-weight: bold')\n      expect(html).toContain('<title>Page Title</title>')\n      expect(html).toContain('<meta charset=\"utf-8\" />')\n      expect(html).toContain('</head>')\n\n      // Verify styles are in the head\n      let headMatch = html.match(/<head>(.*?)<\\/head>/s)\n      expect(headMatch).toBeTruthy()\n      let headContent = headMatch![1]\n      expect(headContent).toContain('<style data-rmx-styles>')\n      expect(headContent).toContain('<title>Page Title</title>')\n      expect(headContent).toContain('<meta charset=\"utf-8\" />')\n    })\n  })\n\n  describe('error handling', () => {\n    it('calls onError for errors', async () => {\n      let capturedError: unknown\n\n      function BadComponent() {\n        throw new Error('Render error!')\n        return () => null\n      }\n\n      let stream = renderToStream(<BadComponent />, {\n        onError: (error) => {\n          capturedError = error\n        },\n      })\n\n      await expect(drain(stream)).rejects.toThrow('Render error!')\n      expect(capturedError instanceof Error && capturedError.message).toBe('Render error!')\n    })\n\n    it('renderToString throws errors that can be caught with try/catch', async () => {\n      function BadComponent() {\n        throw new Error('Component error!')\n        return () => null\n      }\n\n      let caughtError: unknown\n      try {\n        await renderToString(<BadComponent />)\n      } catch (error) {\n        caughtError = error\n      }\n\n      expect(caughtError).toBeInstanceOf(Error)\n      expect((caughtError as Error).message).toBe('Component error!')\n    })\n  })\n\n  describe('doctype', () => {\n    it('handles whitespace before html element', async () => {\n      let stream = renderToStream([\n        '\\n  ',\n        <html>\n          <body>Content</body>\n        </html>,\n      ])\n      let html = await drain(stream)\n      expect(html).toBe('\\n  <html><body>Content</body></html>')\n    })\n  })\n\n  describe('head-like elements outside explicit head', () => {\n    it('renders title elements in place', async () => {\n      let stream = renderToStream(\n        <html>\n          <body>\n            <title>Page Title</title>\n            <div>Content</div>\n          </body>\n        </html>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe('<html><body><title>Page Title</title><div>Content</div></body></html>')\n    })\n\n    it('renders meta elements in place', async () => {\n      let stream = renderToStream(\n        <div>\n          <meta name=\"description\" content=\"Test page\" />\n          <h1>Hello</h1>\n        </div>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe('<div><meta name=\"description\" content=\"Test page\" /><h1>Hello</h1></div>')\n    })\n\n    it('renders link elements in place', async () => {\n      let stream = renderToStream(\n        <div>\n          <link rel=\"stylesheet\" href=\"/styles.css\" />\n          <p>Content</p>\n        </div>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe('<div><link rel=\"stylesheet\" href=\"/styles.css\" /><p>Content</p></div>')\n    })\n\n    it('renders multiple head-like elements in place', async () => {\n      let stream = renderToStream(\n        <div>\n          <title>My App</title>\n          <meta charSet=\"utf-8\" />\n          <p>Hello</p>\n          <link rel=\"icon\" href=\"/favicon.ico\" />\n          <meta name=\"viewport\" content=\"width=device-width\" />\n        </div>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<div><title>My App</title><meta charset=\"utf-8\" /><p>Hello</p><link rel=\"icon\" href=\"/favicon.ico\" /><meta name=\"viewport\" content=\"width=device-width\" /></div>',\n      )\n    })\n\n    it('renders head-like elements from components in place', async () => {\n      function SEO(handle: Handle) {\n        return () => (\n          <>\n            <title>Component Title</title>\n            <meta name=\"description\" content=\"Component Description\" />\n          </>\n        )\n      }\n\n      let stream = renderToStream(\n        <div>\n          <SEO />\n          <main>Content</main>\n        </div>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<div><title>Component Title</title><meta name=\"description\" content=\"Component Description\" /><main>Content</main></div>',\n      )\n    })\n\n    it('keeps bare head-like elements out of an explicit head tag', async () => {\n      let stream = renderToStream(\n        <html>\n          <head>\n            <meta charSet=\"utf-8\" />\n          </head>\n          <body>\n            <title>Body Title</title>\n            <link rel=\"stylesheet\" href=\"/app.css\" />\n            <div>Content</div>\n          </body>\n        </html>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<html><head><meta charset=\"utf-8\" /></head><body><title>Body Title</title><link rel=\"stylesheet\" href=\"/app.css\" /><div>Content</div></body></html>',\n      )\n    })\n\n    it('renders structured data scripts in place', async () => {\n      let structuredData = {\n        '@context': 'https://schema.org',\n        '@type': 'Product',\n        name: 'Test Product',\n      }\n\n      let stream = renderToStream(\n        <div>\n          <script type=\"application/ld+json\" innerHTML={JSON.stringify(structuredData)} />\n          <h1>Product Page</h1>\n        </div>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<div><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"Product\",\"name\":\"Test Product\"}</script><h1>Product Page</h1></div>',\n      )\n    })\n\n    it('does NOT hoist regular script tags', async () => {\n      let stream = renderToStream(\n        <div>\n          <h1>Page Title</h1>\n          <script innerHTML=\"console.log('Hello World')\" />\n          <p>Some content</p>\n        </div>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        \"<div><h1>Page Title</h1><script>console.log('Hello World')</script><p>Some content</p></div>\",\n      )\n    })\n\n    it('renders ld+json scripts in place when mixed with regular scripts', async () => {\n      let stream = renderToStream(\n        <div>\n          <script type=\"text/javascript\" innerHTML=\"console.log('Regular script')\" />\n          <script\n            type=\"application/ld+json\"\n            innerHTML='{\"@context\":\"https://schema.org\",\"@type\":\"WebPage\"}'\n          />\n          <script innerHTML=\"console.log('Another regular script')\" />\n          <h1>Mixed Scripts Page</h1>\n        </div>,\n      )\n      let html = await drain(stream)\n      expect(html).toBe(\n        '<div><script type=\"text/javascript\">console.log(\\'Regular script\\')</script><script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebPage\"}</script><script>console.log(\\'Another regular script\\')</script><h1>Mixed Scripts Page</h1></div>',\n      )\n    })\n  })\n\n  describe('hydration', () => {\n    it('renders hydrated component with hydration script', async () => {\n      // Create a simple hydrated component\n      let Counter = clientEntry('/js/counter.js#Counter', function Counter(handle: Handle) {\n        return ({ initialCount }: { initialCount: number }) => <div>Count: {initialCount}</div>\n      })\n\n      // Render the component\n      let stream = renderToStream(<Counter initialCount={42} />)\n      let html = await drain(stream)\n\n      // Verify the output contains both the rendered HTML and markers with ID\n      let hydrationId = getCommentMarkerId(html, 'rmx:h:')\n      expect(html).toContain(`<!-- rmx:h:${hydrationId} -->`)\n      expect(html).toContain('<div>Count: 42</div>')\n      expect(html).toContain('<!-- /rmx:h -->')\n\n      // Check for aggregated data script at the end\n      expect(html).toContain('<script type=\"application/json\" id=\"rmx-data\">')\n\n      // Parse the aggregated data\n      let data = parseRmxDataFromHtml(html)\n\n      expect(data.h).toBeDefined()\n      expect(data.h[hydrationId]).toEqual({\n        moduleUrl: '/js/counter.js',\n        exportName: 'Counter',\n        props: { initialCount: 42 },\n      })\n\n      // Ordering: start marker < content < end marker\n      let startIdx = html.indexOf(`<!-- rmx:h:${hydrationId} -->`)\n      let contentIdx = html.indexOf('<div>Count: 42</div>')\n      let endIdx = html.indexOf('<!-- /rmx:h -->')\n      expect(startIdx).toBeGreaterThanOrEqual(0)\n      expect(contentIdx).toBeGreaterThan(startIdx)\n      expect(endIdx).toBeGreaterThan(contentIdx)\n    })\n\n    it('escapes rmx-data payloads that contain script end tags', async () => {\n      let Counter = clientEntry('/js/counter.js#Counter', function Counter(handle: Handle) {\n        return ({ label }: { label: string }) => <div>{label}</div>\n      })\n\n      let scriptBreakingLabel = '</script><script>alert(\"xss\")</script>'\n      let stream = renderToStream(<Counter label={scriptBreakingLabel} />)\n      let html = await drain(stream)\n      let shelf = document.createElement('template')\n      shelf.innerHTML = html\n      let script = getLatestRmxDataScript(shelf.content)\n\n      // Ensure we do not emit a literal closing tag inside rmx-data payload.\n      expect(script.textContent || '').not.toContain('</script>')\n\n      // Ensure the data still parses and round-trips to the original value.\n      let data = parseRmxDataFromHtml(html)\n      let [hydrationId, entry] = getSingleEntry(data.h)\n      expect(hydrationId).toBeDefined()\n      expect(entry.props.label).toBe(scriptBreakingLabel)\n    })\n\n    it('renders multiple hydrated components with unique instance IDs', async () => {\n      // Create hydrated components\n      let Button = clientEntry('/js/button.js#Button', function Button(handle: Handle) {\n        return ({ text }: { text: string }) => <button>{text}</button>\n      })\n\n      // Render multiple hydrated components\n      let stream = renderToStream(\n        <div>\n          <Button text=\"First\" />\n          <Button text=\"Second\" />\n        </div>,\n      )\n      let html = await drain(stream)\n\n      // Verify both buttons are rendered inside hydration markers with unique IDs\n      expect(html).toContain('<!-- rmx:h:')\n      expect(html).toContain('<button>First</button>')\n      expect(html).toContain('<button>Second</button>')\n\n      let data = parseRmxDataFromHtml(html)\n      expect(Object.keys(data.h).length).toBe(2)\n      for (let entry of Object.values<any>(data.h)) {\n        expect(entry.moduleUrl).toBe('/js/button.js')\n        expect(entry.exportName).toBe('Button')\n      }\n    })\n\n    it('renders hydrated component with complex props', async () => {\n      let Card = clientEntry('/js/card.js#Card', function Card(handle: Handle) {\n        return (props: {\n          title: string\n          count: number\n          enabled: boolean\n          items: string[]\n          nested: { value: number }\n        }) => (\n          <div>\n            <h2>{props.title}</h2>\n            <p>Count: {props.count}</p>\n            <section>Enabled: {String(props.enabled)}</section>\n            <ul>\n              {props.items.map((item, i) => (\n                <li key={i}>{item}</li>\n              ))}\n            </ul>\n            <main>Nested value: {props.nested.value}</main>\n          </div>\n        )\n      })\n\n      let stream = renderToStream(\n        <Card\n          title=\"Test Card\"\n          count={10}\n          enabled={true}\n          items={['one', 'two', 'three']}\n          nested={{ value: 99 }}\n        />,\n      )\n      let html = await drain(stream)\n\n      // Verify the rendered output inside hydration markers\n      let shelf = document.createElement('template')\n      shelf.innerHTML = html\n      let content = shelf.content\n      invariant(content.firstChild instanceof Comment)\n      expect(content.firstChild.data.trim().startsWith('rmx:h:')).toBe(true)\n      expect(shelf.content.querySelector('h2')?.textContent).toBe('Test Card')\n      expect(shelf.content.querySelector('p')?.textContent).toBe('Count: 10')\n      expect(shelf.content.querySelector('section')?.textContent).toBe('Enabled: true')\n      let items = shelf.content.querySelectorAll('li')\n      expect(items).toHaveLength(3)\n      expect(items[0].textContent).toBe('one')\n      expect(items[1].textContent).toBe('two')\n      expect(items[2].textContent).toBe('three')\n      expect(shelf.content.querySelector('main')?.textContent).toBe('Nested value: 99')\n\n      // Check aggregated data script\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      let [, entry] = getSingleEntry(data.h)\n      expect(entry.props).toEqual({\n        title: 'Test Card',\n        count: 10,\n        enabled: true,\n        items: ['one', 'two', 'three'],\n        nested: { value: 99 },\n      })\n    })\n\n    it('serializes virtual host elements', async () => {\n      let Card = clientEntry('/js/card.js#Card', function Card(handle: Handle) {\n        return (props: { children: RemixNode }) => (\n          <div>\n            <h1>Test Card</h1>\n            {props.children}\n          </div>\n        )\n      })\n\n      let stream = renderToStream(\n        <Card>\n          <p>Hello, world!</p>\n        </Card>,\n      )\n      let html = await drain(stream)\n      let shelf = document.createElement('template')\n      shelf.innerHTML = html\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n\n      let [, entry] = getSingleEntry(data.h)\n      expect(entry.props.children).toEqual({\n        $rmx: true,\n        type: 'p',\n        props: {\n          children: 'Hello, world!',\n        },\n      })\n    })\n\n    it('serializes virtual component elements', async () => {\n      let Card = clientEntry('/js/card.js#Card', function Card(handle: Handle) {\n        return (props: { children: RemixNode }) => (\n          <div>\n            <h1>Test Card</h1>\n            {props.children}\n          </div>\n        )\n      })\n\n      function UnwrappedChild(handle: Handle) {\n        return () => (\n          <p>\n            <DeepChild />\n          </p>\n        )\n      }\n\n      function DeepChild(handle: Handle) {\n        return () => <span>Hello, world!</span>\n      }\n\n      let stream = renderToStream(\n        <Card>\n          <UnwrappedChild />\n        </Card>,\n      )\n      let html = await drain(stream)\n      let shelf = document.createElement('template')\n      shelf.innerHTML = html\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      let [, entry] = getSingleEntry(data.h)\n      expect(entry).toEqual({\n        exportName: 'Card',\n        moduleUrl: '/js/card.js',\n        props: {\n          children: {\n            $rmx: true,\n            props: {\n              children: {\n                $rmx: true,\n                props: {\n                  children: 'Hello, world!',\n                },\n                type: 'span',\n              },\n            },\n            type: 'p',\n          },\n        },\n      })\n    })\n\n    it('serializes Frame elements in entry props as frame descriptors', async () => {\n      let Card = clientEntry('/js/card.js#Card', function Card(handle: Handle) {\n        return (props: { children: RemixNode }) => <div>{props.children}</div>\n      })\n\n      let stream = renderToStream(\n        <Card>\n          <Frame src=\"/child-frame\" fallback={<p>Loading child frame…</p>} />\n        </Card>,\n        {\n          resolveFrame: () => '<p>Loaded child frame</p>',\n        },\n      )\n      let html = await drain(stream)\n      let shelf = document.createElement('template')\n      shelf.innerHTML = html\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      let [, entry] = getSingleEntry(data.h)\n\n      expect(entry.props.children).toEqual({\n        $rmxFrame: true,\n        props: {\n          src: '/child-frame',\n          fallback: {\n            $rmx: true,\n            type: 'p',\n            props: {\n              children: 'Loading child frame…',\n            },\n          },\n        },\n      })\n    })\n\n    it('nests hydrated components', async () => {\n      let Card = clientEntry('/card.js#Card', function Card(handle: Handle) {\n        return ({ children }: { children: RemixNode }) => <div>{children}</div>\n      })\n\n      let Button = clientEntry('/button.js#Button', function Button(handle: Handle) {\n        return () => <button />\n      })\n\n      let stream = renderToStream(\n        <Card>\n          <Button />\n        </Card>,\n      )\n\n      let html = await drain(stream)\n      let shelf = document.createElement('template')\n      shelf.innerHTML = html\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      let entries = Object.values<any>(data.h)\n      expect(entries.length).toBe(2)\n      expect(\n        entries.some((entry) => entry.moduleUrl === '/card.js' && entry.exportName === 'Card'),\n      ).toBe(true)\n      expect(\n        entries.some((entry) => entry.moduleUrl === '/button.js' && entry.exportName === 'Button'),\n      ).toBe(true)\n    })\n  })\n\n  describe('frames', () => {\n    it('adds frame scripts for non-blocking frames', async () => {\n      // Test non-blocking frame (with fallback)\n      let stream = renderToStream(<Frame src=\"/x\" fallback={<div>Loading...</div>} />, {\n        resolveFrame: () => '<div>Resolved</div>',\n      })\n      let result = await drain(stream)\n\n      let frameId = getCommentMarkerId(result, 'rmx:f:')\n      // Should render fallback content with frame markers\n      expect(result).toContain(`<!-- rmx:f:${frameId} -->`)\n      expect(result).toContain('<div>Loading...</div>')\n      expect(result).toContain('<!-- /rmx:f -->')\n\n      // Should have aggregated frame metadata script\n      let data = parseRmxDataFromHtml(result)\n      expect(data.f[frameId]).toEqual({\n        status: 'pending',\n        src: '/x',\n      })\n    })\n\n    it('adds frame scripts for blocking frames', async () => {\n      let stream = renderToStream(<Frame src=\"/x\" />, {\n        resolveFrame: () => '<div>Resolved</div>',\n      })\n      let result = await drain(stream)\n\n      let frameId = getCommentMarkerId(result, 'rmx:f:')\n      // Should have frame markers\n      expect(result).toContain(`<!-- rmx:f:${frameId} -->`)\n      expect(result).toContain('<div>Resolved</div>')\n      expect(result).toContain('<!-- /rmx:f -->')\n\n      // Should have aggregated frame metadata script\n      let data = parseRmxDataFromHtml(result)\n      expect(data.f[frameId].status).toBe('resolved')\n    })\n\n    it('renders blocking frames without fallback', async () => {\n      // Test blocking frame (no fallback)\n      let stream = renderToStream(<Frame src=\"/fragments/product\" />, {\n        resolveFrame: async () => '<div>Product</div>',\n      })\n      let chunks = readChunks(stream)\n\n      // Get first chunk\n      let firstChunk = await chunks.next()\n      expect(firstChunk.done).toBe(false)\n      let content = firstChunk.value!\n\n      // First chunk should contain the resolved frame with markers\n      let frameId = getCommentMarkerId(content, 'rmx:f:')\n      expect(content).toContain(`<!-- rmx:f:${frameId} -->`)\n      expect(content).toContain('<div>Product</div>')\n      expect(content).toContain('<!-- /rmx:f -->')\n\n      // Should have aggregated frame metadata script with resolved status\n      let data = parseRmxDataFromHtml(content)\n      expect(data.f[frameId].status).toBe('resolved')\n\n      // Should be done after first chunk\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('assigns IDs for nested non-blocking frames', async () => {\n      let stream = renderToStream(\n        <div>\n          <Frame\n            src=\"/outer\"\n            fallback={\n              <div>\n                Outer loading\n                <Frame src=\"/inner\" fallback={<span>Inner loading</span>} />\n              </div>\n            }\n          />\n        </div>,\n        { resolveFrame: async () => '<div>Resolved</div>' },\n      )\n      let html = await drain(stream)\n\n      // Check for frame markers\n      expect(html).toContain('<!-- rmx:f:')\n\n      // Aggregated data with hierarchical ids and pending statuses\n      let data = parseRmxDataFromHtml(html)\n      expect(Object.keys(data.f).length).toBe(2)\n      for (let entry of Object.values<any>(data.f)) {\n        expect(entry.status).toBe('pending')\n      }\n\n      // Fallbacks rendered\n      expect(html).toContain('Outer loading')\n      expect(html).toContain('Inner loading')\n    })\n\n    it('awaits nested blocking frames before first chunk', async () => {\n      async function resolveFrame(src: string) {\n        if (src === '/outer') {\n          return renderToStream(\n            <div>\n              Outer\n              <Frame src=\"/inner\" />\n            </div>,\n            { resolveFrame },\n          )\n        }\n        if (src === '/inner') {\n          return '<div>Inner</div>'\n        }\n        return '<div></div>'\n      }\n\n      let stream = renderToStream(<Frame src=\"/outer\" />, { resolveFrame })\n\n      let chunks = readChunks(stream)\n      let first = await chunks.next()\n      expect(first.done).toBe(false)\n      let content = first.value!\n\n      // Both outer and inner should be present in first chunk\n      expect(content).toContain('Outer')\n      expect(content).toContain('<div>Inner</div>')\n\n      // Both frames resolved in aggregated data\n      let shelf = document.createElement('template')\n      shelf.innerHTML = content\n      let scripts = shelf.content.querySelectorAll(rmxDataScriptSelector)\n      let script = scripts.item(scripts.length - 1)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      expect(Object.values<any>(data.f).some((v) => v.status === 'resolved')).toBe(true)\n\n      console.log(content)\n      // Stream completes\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('renders nested non-blocking inside blocking with fallback', async () => {\n      async function resolveFrame(src: string) {\n        if (src === '/outer') {\n          return renderToStream(\n            <div>\n              Outer\n              <Frame src=\"/inner\" fallback={<span>Inner loading</span>} />\n            </div>,\n            { resolveFrame },\n          )\n        }\n        if (src === '/inner') return '<div>Inner</div>'\n        return '<div></div>'\n      }\n\n      let stream = renderToStream(<Frame src=\"/outer\" />, { resolveFrame })\n\n      let chunks = readChunks(stream)\n      let first = await chunks.next()\n      expect(first.done).toBe(false)\n      let content = first.value!\n\n      // Outer resolved, inner fallback pending in first chunk\n      expect(content).toContain('Outer')\n      expect(content).toContain('Inner loading')\n\n      // Check aggregated data\n      let shelf = document.createElement('template')\n      shelf.innerHTML = content\n      let scripts = shelf.content.querySelectorAll(rmxDataScriptSelector)\n      let script = scripts.item(scripts.length - 1)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      expect(Object.values<any>(data.f).some((v) => v.status === 'resolved')).toBe(true)\n\n      // Since the inner frame is non-blocking, it should stream later\n      let second = await chunks.next()\n      expect(second.done).toBe(false)\n      expect(second.value).toContain('<template id=\"f')\n\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('streams non-blocking frame content after initial chunk', async () => {\n      let [promise, resolve] = withResolvers<string>()\n\n      let stream = renderToStream(\n        <div>\n          <h1>Page Title</h1>\n          <Frame src=\"/fragments/product\" fallback={<div>Loading product...</div>} />\n          <p>Footer content</p>\n        </div>,\n        {\n          resolveFrame: async (src) => {\n            if (src === '/fragments/product') {\n              return promise\n            }\n            return '<div></div>'\n          },\n        },\n      )\n\n      let chunks = readChunks(stream)\n\n      // First chunk should contain fallback\n      let firstChunk = await chunks.next()\n      expect(firstChunk.done).toBe(false)\n      let content = firstChunk.value!\n\n      expect(content).toContain('<h1>Page Title</h1>')\n      expect(content).toContain('<div>Loading product...</div>')\n      expect(content).toContain('<p>Footer content</p>')\n\n      // Check aggregated data for pending status\n      let shelf = document.createElement('template')\n      shelf.innerHTML = content\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      expect(Object.values<any>(data.f).some((v) => v.status === 'pending')).toBe(true)\n\n      // Resolve the frame content\n      resolve('<div>Async Product Content</div>')\n\n      // Second chunk should contain the resolved content as a template\n      let secondChunk = await chunks.next()\n      expect(secondChunk.done).toBe(false)\n      let template = secondChunk.value\n\n      expect(template).toContain('<template id=\"f')\n      expect(template).toContain('<div>Async Product Content</div>')\n      expect(template).toContain('</template>')\n\n      // Stream should complete\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('escapes frame template content to prevent template breakouts', async () => {\n      let [promise, resolve] = withResolvers<string>()\n      let injectedHtml = '</template><script>alert(\"xss\")</script><template><p>safe</p>'\n\n      let stream = renderToStream(<Frame src=\"/danger\" fallback={<div>Loading...</div>} />, {\n        resolveFrame: async () => promise,\n      })\n\n      let chunks = readChunks(stream)\n      let firstChunk = await chunks.next()\n      expect(firstChunk.done).toBe(false)\n      expect(firstChunk.value).toContain('<div>Loading...</div>')\n\n      resolve(injectedHtml)\n\n      let secondChunk = await chunks.next()\n      expect(secondChunk.done).toBe(false)\n      let templateChunk = secondChunk.value!\n      expect(templateChunk).toContain('<template id=\"f')\n      expect(templateChunk).toContain(\n        '<\\\\/template><script>alert(\"xss\")</script><template><p>safe</p>',\n      )\n      expect(templateChunk).not.toContain('</template><script>alert(\"xss\")</script><template>')\n\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('streams multiple non-blocking frames in order of resolution', async () => {\n      let [frame1Promise, resolveFrame1] = withResolvers<string>()\n      let [frame2Promise, resolveFrame2] = withResolvers<string>()\n\n      let stream = renderToStream(\n        <div>\n          <Frame src=\"/frame1\" fallback={<div>Loading frame 1...</div>} />\n          <Frame src=\"/frame2\" fallback={<div>Loading frame 2...</div>} />\n        </div>,\n        {\n          resolveFrame: async (src) => {\n            if (src === '/frame1') return frame1Promise\n            if (src === '/frame2') return frame2Promise\n            return '<div></div>'\n          },\n        },\n      )\n\n      let chunks = readChunks(stream)\n\n      // First chunk has both fallbacks\n      let firstChunk = await chunks.next()\n      expect(firstChunk.value).toContain('Loading frame 1...')\n      expect(firstChunk.value).toContain('Loading frame 2...')\n\n      // Check aggregated data\n      let shelf = document.createElement('template')\n      shelf.innerHTML = firstChunk.value!\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      expect(Object.keys(data.f).length).toBe(2)\n\n      // Resolve frame 2 first\n      resolveFrame2('<div>Second Frame Content</div>')\n\n      // Should stream frame 2's content\n      let secondChunk = await chunks.next()\n      expect(secondChunk.value).toContain('<template id=\"f')\n      expect(secondChunk.value).toContain('Second Frame Content')\n\n      // Resolve frame 1\n      resolveFrame1('<div>First Frame Content</div>')\n\n      // Should stream frame 1's content\n      let thirdChunk = await chunks.next()\n      expect(thirdChunk.value).toContain('<template id=\"f')\n      expect(thirdChunk.value).toContain('First Frame Content')\n\n      // Stream completes\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('renders descendant frames returned from resolveFrame', async () => {\n      async function resolveFrame(src: string) {\n        if (src === '/parent') {\n          // Parent frame returns content containing a child frame\n          return renderToStream(\n            <section>\n              <h2>Parent Content</h2>\n              <Frame src=\"/child\" fallback={<span>Loading child...</span>} />\n              <p>Parent footer</p>\n            </section>,\n            { resolveFrame },\n          )\n        }\n        if (src === '/child') {\n          // Simulate async loading of child\n          await new Promise((resolve) => setTimeout(resolve, 10))\n          return '<article>Child Content</article>'\n        }\n        return '<div></div>'\n      }\n\n      let stream = renderToStream(\n        <div>\n          <h1>Page</h1>\n          <Frame src=\"/parent\" fallback={<div>Loading parent...</div>} />\n        </div>,\n        { resolveFrame },\n      )\n\n      let chunks = readChunks(stream)\n\n      // First chunk should contain parent fallback\n      let firstChunk = await chunks.next()\n      expect(firstChunk.done).toBe(false)\n      let content = firstChunk.value!\n\n      expect(content).toContain('<h1>Page</h1>')\n      expect(content).toContain('<div>Loading parent...</div>')\n\n      // Check aggregated data for parent pending\n      let shelf = document.createElement('template')\n      shelf.innerHTML = content\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      expect(Object.values<any>(data.f).some((v) => v.status === 'pending')).toBe(true)\n\n      // Second chunk should contain parent's resolved content with child frame\n      let secondChunk = await chunks.next()\n      expect(secondChunk.done).toBe(false)\n      let parentTemplate = secondChunk.value\n\n      expect(parentTemplate).toContain('<template id=\"f')\n      expect(parentTemplate).toContain('<section>')\n      expect(parentTemplate).toContain('<h2>Parent Content</h2>')\n      expect(parentTemplate).toContain('<span>Loading child...</span>')\n      expect(parentTemplate).toContain('<p>Parent footer</p>')\n      expect(parentTemplate).toContain('</section>')\n      expect(parentTemplate).toContain('</template>')\n\n      // Third chunk should contain child's resolved content\n      let thirdChunk = await chunks.next()\n      expect(thirdChunk.done).toBe(false)\n      let childTemplate = thirdChunk.value\n\n      // IDs are local within the returned fragment stream, so the child template id may collide.\n      expect(childTemplate).toContain('<template id=\"f')\n      expect(childTemplate).toContain('<article>Child Content</article>')\n      expect(childTemplate).toContain('</template>')\n\n      // Stream should complete\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('preserves top frame src across nested SSR frame renders', async () => {\n      let seen = new Map<\n        string,\n        {\n          frameSrc: string\n          topFrameSrc: string\n          sameFrame: boolean\n        }\n      >()\n      let resolveFrameContexts: Array<{\n        src: string\n        currentFrameSrc: string\n        topFrameSrc: string\n      }> = []\n\n      function Inspect(handle: Handle) {\n        return ({ label }: { label: string }) => {\n          seen.set(label, {\n            frameSrc: handle.frame.src,\n            topFrameSrc: handle.frames.top.src,\n            sameFrame: handle.frame === handle.frames.top,\n          })\n\n          return <div data-label={label}>{handle.frame.src}</div>\n        }\n      }\n\n      async function resolveFrame(\n        src: string,\n        _target?: string,\n        context?: { currentFrameSrc: string; topFrameSrc: string },\n      ) {\n        invariant(context)\n        resolveFrameContexts.push({ src, ...context })\n\n        if (src === '/settings') {\n          return renderToStream(\n            <section>\n              <Inspect label=\"settings\" />\n              <Frame src=\"/settings/profile\" />\n            </section>,\n            {\n              frameSrc: new URL(src, context.currentFrameSrc),\n              topFrameSrc: context.topFrameSrc,\n              resolveFrame,\n            },\n          )\n        }\n\n        if (src === '/settings/profile') {\n          return renderToStream(<Inspect label=\"profile\" />, {\n            frameSrc: new URL(src, context.currentFrameSrc),\n            topFrameSrc: context.topFrameSrc,\n            resolveFrame,\n          })\n        }\n\n        throw new Error(`Unexpected frame src: ${src}`)\n      }\n\n      let html = await drain(\n        renderToStream(\n          <main>\n            <Inspect label=\"root\" />\n            <Frame src=\"/settings\" />\n          </main>,\n          {\n            frameSrc: 'https://example.com/app',\n            resolveFrame,\n          },\n        ),\n      )\n\n      expect(html).toContain('https://example.com/app')\n      expect(html).toContain('https://example.com/settings')\n      expect(html).toContain('https://example.com/settings/profile')\n\n      expect(resolveFrameContexts).toEqual([\n        {\n          src: '/settings',\n          currentFrameSrc: 'https://example.com/app',\n          topFrameSrc: 'https://example.com/app',\n        },\n        {\n          src: '/settings/profile',\n          currentFrameSrc: 'https://example.com/settings',\n          topFrameSrc: 'https://example.com/app',\n        },\n      ])\n\n      expect(seen.get('root')).toEqual({\n        frameSrc: 'https://example.com/app',\n        topFrameSrc: 'https://example.com/app',\n        sameFrame: true,\n      })\n      expect(seen.get('settings')).toEqual({\n        frameSrc: 'https://example.com/settings',\n        topFrameSrc: 'https://example.com/app',\n        sameFrame: false,\n      })\n      expect(seen.get('profile')).toEqual({\n        frameSrc: 'https://example.com/settings/profile',\n        topFrameSrc: 'https://example.com/app',\n        sameFrame: false,\n      })\n    })\n\n    it('handles blocking descendant frames returned from resolveFrame', async () => {\n      async function resolveFrame(src: string) {\n        if (src === '/parent') {\n          // Parent returns content with a blocking child frame (no fallback)\n          return renderToStream(\n            <main>\n              <h2>Parent Header</h2>\n              <Frame src=\"/child\" />\n              <p>Parent Footer</p>\n            </main>,\n            { resolveFrame },\n          )\n        }\n        if (src === '/child') {\n          return '<div>Blocking Child Content</div>'\n        }\n        return '<div></div>'\n      }\n\n      let stream = renderToStream(<Frame src=\"/parent\" />, { resolveFrame })\n\n      let chunks = readChunks(stream)\n\n      // First chunk should contain everything resolved (all blocking frames await)\n      let firstChunk = await chunks.next()\n      expect(firstChunk.done).toBe(false)\n      let content = firstChunk.value!\n\n      // Parent frame and content\n      expect(content).toContain('<main>')\n      expect(content).toContain('<h2>Parent Header</h2>')\n\n      // Child frame content (nested within parent)\n      expect(content).toContain('<div>Blocking Child Content</div>')\n\n      expect(content).toContain('<p>Parent Footer</p>')\n      expect(content).toContain('</main>')\n\n      // Both frames should have resolved status in aggregated data\n      let shelf = document.createElement('template')\n      shelf.innerHTML = content\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      expect(Object.values<any>(data.f).some((v) => v.status === 'resolved')).toBe(true)\n\n      // Stream should complete\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('handles mixed blocking and non-blocking descendant frames', async () => {\n      async function resolveFrame(src: string) {\n        if (src === '/parent') {\n          // Blocking parent returns content with both blocking and non-blocking children\n          return renderToStream(\n            <div>\n              <Frame src=\"/blocking-child\" />\n              <Frame src=\"/non-blocking-child\" fallback={<span>Loading...</span>} />\n            </div>,\n            { resolveFrame },\n          )\n        }\n        if (src === '/blocking-child') {\n          return '<div>Blocking Child</div>'\n        }\n        if (src === '/non-blocking-child') {\n          // Simulate async\n          await new Promise((resolve) => setTimeout(resolve, 10))\n          return '<div>Non-blocking Child</div>'\n        }\n        return '<div></div>'\n      }\n\n      let stream = renderToStream(<Frame src=\"/parent\" />, { resolveFrame })\n\n      let chunks = readChunks(stream)\n\n      // First chunk has parent and blocking child resolved, non-blocking child fallback\n      let firstChunk = await chunks.next()\n      let content = firstChunk.value!\n      console.log('first:\\n', firstChunk.value)\n      // Parent frame\n      expect(content).toContain('<div>')\n\n      // Blocking child resolved\n      expect(content).toContain('<div>Blocking Child</div>')\n\n      // Non-blocking child shows fallback\n      expect(content).toContain('<span>Loading...</span>')\n\n      expect(content).toContain('</div>')\n\n      // Check aggregated data\n      let shelf = document.createElement('template')\n      shelf.innerHTML = content\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      expect(Object.values<any>(data.f).some((v) => v.status === 'resolved')).toBe(true)\n\n      // Second chunk has non-blocking child resolved\n      let secondChunk = await chunks.next()\n      console.log('\\n\\nsecond:\\n', secondChunk.value)\n      // IDs are local within the returned fragment stream.\n      expect(secondChunk.value).toContain('<template id=\"f')\n      expect(secondChunk.value).toContain('<div>Non-blocking Child</div>')\n      expect(secondChunk.value).toContain('</template>')\n\n      // Stream completes\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('emits comment boundaries around non-blocking frame content', async () => {\n      let stream = renderToStream(<Frame src=\"/x\" fallback={<div>Loading...</div>} />, {\n        resolveFrame: () => '<div>Resolved</div>',\n      })\n      let chunks = readChunks(stream)\n\n      // First chunk contains fallback + markers\n      let first = await chunks.next()\n      expect(first.done).toBe(false)\n      let content = first.value!\n\n      expect(content).toContain('<!-- rmx:f:')\n      expect(content).toContain('<div>Loading...</div>')\n      expect(content).toContain('<!-- /rmx:f -->')\n\n      let frameId = getCommentMarkerId(content, 'rmx:f:')\n      let startIdx = content.indexOf(`<!-- rmx:f:${frameId} -->`)\n      let innerIdx = content.indexOf('<div>Loading...</div>')\n      let endIdx = content.indexOf('<!-- /rmx:f -->')\n\n      expect(startIdx).toBeGreaterThanOrEqual(0)\n      expect(innerIdx).toBeGreaterThan(startIdx)\n      expect(endIdx).toBeGreaterThan(innerIdx)\n\n      // Second chunk should be the template for resolved content (no markers expected here)\n      let second = await chunks.next()\n      if (!second.done) {\n        expect(second.value).toContain('<template id=\"f')\n      }\n    })\n\n    it('emits comment boundaries around blocking frame content', async () => {\n      let stream = renderToStream(<Frame src=\"/product\" />, {\n        resolveFrame: async () => '<section>Resolved</section>',\n      })\n\n      let chunks = readChunks(stream)\n      let first = await chunks.next()\n      expect(first.done).toBe(false)\n      let content = first.value!\n\n      expect(content).toContain('<!-- rmx:f:')\n      expect(content).toContain('<section>Resolved</section>')\n      expect(content).toContain('<!-- /rmx:f -->')\n\n      let frameId = getCommentMarkerId(content, 'rmx:f:')\n      let startIdx = content.indexOf(`<!-- rmx:f:${frameId} -->`)\n      let innerIdx = content.indexOf('<section>Resolved</section>')\n      let endIdx = content.indexOf('<!-- /rmx:f -->')\n\n      expect(startIdx).toBeGreaterThanOrEqual(0)\n      expect(innerIdx).toBeGreaterThan(startIdx)\n      expect(endIdx).toBeGreaterThan(innerIdx)\n\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n\n    it('adds name to json script', async () => {\n      let stream = renderToStream(<Frame src=\"/x\" name=\"test\" />, {\n        resolveFrame: async () => '<div>Resolved</div>',\n      })\n      let chunks = readChunks(stream)\n      let first = await chunks.next()\n      expect(first.done).toBe(false)\n      let content = first.value!\n\n      // Check aggregated data contains name\n      let shelf = document.createElement('template')\n      shelf.innerHTML = content\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      let entry = Object.values<any>(data.f)[0]\n      expect(entry.name).toBe('test')\n    })\n\n    it('delays streaming non-blocking parent until nested blocking child resolves', async () => {\n      let [parentPromise, resolveParent] = withResolvers<ReadableStream<Uint8Array>>()\n      let [childPromise, resolveChild] = withResolvers<string>()\n\n      async function resolveFrame(src: string) {\n        if (src === '/parent') {\n          return parentPromise\n        }\n        if (src === '/child') {\n          return childPromise\n        }\n        return '<div></div>'\n      }\n\n      let stream = renderToStream(<Frame src=\"/parent\" fallback={<div>Loading parent...</div>} />, {\n        resolveFrame,\n      })\n\n      let chunks = readChunks(stream)\n\n      // First chunk should contain parent fallback with pending status\n      let first = await chunks.next()\n      expect(first.done).toBe(false)\n      let firstContent = first.value!\n      expect(firstContent).toContain('<div>Loading parent...</div>')\n\n      // Check aggregated data\n      let shelf = document.createElement('template')\n      shelf.innerHTML = firstContent\n      let script = shelf.content.querySelector(rmxDataScriptSelector)\n      invariant(script)\n      let data = JSON.parse(script.textContent || '{}')\n      expect(Object.values<any>(data.f).some((v) => v.status === 'pending')).toBe(true)\n\n      // Resolve parent to content that includes a blocking child frame (no fallback)\n      resolveParent(\n        renderToStream(\n          <section>\n            <h2>Parent Content</h2>\n            <Frame src=\"/child\" />\n            <p>Parent footer</p>\n          </section>,\n          { resolveFrame },\n        ),\n      )\n\n      // Expect no new chunk yet because the child is blocking\n      let chunkArrived = false\n      let nextChunkPromise = chunks.next().then((result) => {\n        chunkArrived = true\n        return result\n      })\n\n      await Promise.resolve()\n      expect(chunkArrived).toBe(false)\n\n      // Now resolve the blocking child\n      resolveChild('<article>Child Content</article>')\n\n      // Next chunk should now contain the parent's template with the child's resolved content\n      let second = await nextChunkPromise\n      expect(second.done).toBe(false)\n      let parentTemplate = second.value!\n      expect(parentTemplate).toContain('<template id=\"f')\n      expect(parentTemplate).toContain('<h2>Parent Content</h2>')\n      expect(parentTemplate).toContain('<article>Child Content</article>')\n      expect(parentTemplate).toContain('</template>')\n\n      // Stream should complete\n      let done = await chunks.next()\n      expect(done.done).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/style.test.ts",
    "content": "import { describe, it, expect } from 'vitest'\nimport { processStyleClass } from '../lib/style/lib/style.ts'\n\ndescribe('processStyleClass', () => {\n  it('returns class selectors and css text', () => {\n    let cache = new Map<string, { selector: string; css: string }>()\n    let result = processStyleClass({ color: 'red', fontSize: '16px' }, cache)\n\n    expect(result.selector).toMatch(/^rmxc-/)\n    expect(result.css).toContain(`.${result.selector}`)\n    expect(result.css).toContain('color: red')\n    expect(result.css).toContain('font-size: 16px')\n  })\n\n  it('deduplicates identical style objects', () => {\n    let cache = new Map<string, { selector: string; css: string }>()\n    let first = processStyleClass({ color: 'red', '&:hover': { color: 'blue' } }, cache)\n    let second = processStyleClass({ color: 'red', '&:hover': { color: 'blue' } }, cache)\n\n    expect(first.selector).toBe(second.selector)\n    expect(first.css).toBe(second.css)\n  })\n\n  it('returns empty selector/css for empty objects', () => {\n    let cache = new Map<string, { selector: string; css: string }>()\n    let result = processStyleClass({}, cache)\n    expect(result.selector).toBe('')\n    expect(result.css).toBe('')\n  })\n\n  it('keeps nested selectors and media queries', () => {\n    let cache = new Map<string, { selector: string; css: string }>()\n    let result = processStyleClass(\n      {\n        color: 'black',\n        ':hover': { color: 'red' },\n        '@media (min-width: 768px)': {\n          fontSize: '16px',\n        },\n      },\n      cache,\n    )\n\n    expect(result.css).toContain(':hover')\n    expect(result.css).toContain('@media (min-width: 768px)')\n    expect(result.css).toContain('font-size: 16px')\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/stylesheet.test.ts",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createStyleManager } from '../lib/style/lib/stylesheet.ts'\n\ndescribe('createStyleManager', () => {\n  it('inserts a rule once and increments count on repeat', () => {\n    let mgr = createStyleManager('rmx-test')\n    mgr.insert('rmx-a', '.rmx-a { color: red; }')\n    let sheet = document.adoptedStyleSheets[document.adoptedStyleSheets.length - 1]\n    expect(mgr.has('rmx-a')).toBe(true)\n    expect(sheet.cssRules.length).toBe(1)\n\n    // Second insert should increment count, not duplicate rule\n    mgr.insert('rmx-a', '.rmx-a { color: red; }')\n    expect(mgr.has('rmx-a')).toBe(true)\n    expect(sheet.cssRules.length).toBe(1) // Still only one rule\n\n    // Verify layer and content\n    let cssText = sheet.cssRules[0]?.cssText || ''\n    expect(cssText.includes('@layer rmx-test')).toBe(true)\n    expect(cssText.includes('.rmx-a')).toBe(true)\n    expect(cssText.split('.rmx-a').length - 1).toBe(1) // Class appears once\n\n    // cleanup\n    mgr.dispose()\n  })\n\n  it('uses default layer name when not provided', () => {\n    let mgr = createStyleManager()\n    mgr.insert('rmx-a', '.rmx-a { color: red; }')\n    let sheet = document.adoptedStyleSheets[document.adoptedStyleSheets.length - 1]\n    let cssText = sheet.cssRules[0]?.cssText || ''\n    expect(cssText.includes('@layer rmx')).toBe(true)\n    // cleanup\n    mgr.dispose()\n  })\n\n  it('returns false for has() when class was never inserted', () => {\n    let mgr = createStyleManager('rmx-test')\n    expect(mgr.has('rmx-never-inserted')).toBe(false)\n    mgr.dispose()\n  })\n\n  it('safely handles removing a class that was never inserted', () => {\n    let mgr = createStyleManager('rmx-test')\n    let sheet = document.adoptedStyleSheets[document.adoptedStyleSheets.length - 1]\n    let initialLength = sheet.cssRules.length\n\n    mgr.remove('rmx-never-inserted')\n    expect(sheet.cssRules.length).toBe(initialLength)\n    expect(mgr.has('rmx-never-inserted')).toBe(false)\n\n    // cleanup\n    mgr.dispose()\n  })\n\n  it('handles multiple managers with different layers independently', () => {\n    let before = document.adoptedStyleSheets.length\n    let mgr1 = createStyleManager('layer-1')\n    let mgr2 = createStyleManager('layer-2')\n\n    mgr1.insert('rmx-a', '.rmx-a { color: red; }')\n    mgr2.insert('rmx-a', '.rmx-a { color: blue; }')\n\n    // one server sheet, then one client sheet per manager (in insert order)\n    let sheet1 = document.adoptedStyleSheets[before + 1]\n    let sheet2 = document.adoptedStyleSheets[before + 2]\n\n    expect(sheet1.cssRules.length).toBe(1)\n    expect(sheet2.cssRules.length).toBe(1)\n    expect(sheet1.cssRules[0]?.cssText).toContain('@layer layer-1')\n    expect(sheet2.cssRules[0]?.cssText).toContain('@layer layer-2')\n\n    // cleanup\n    mgr1.dispose()\n    mgr2.dispose()\n  })\n\n  it('handles complex CSS rules with nested selectors and at-rules', () => {\n    let mgr = createStyleManager('rmx-test')\n\n    let complexRule = `\n      .rmx-complex {\n        color: red;\n      }\n      .rmx-complex:hover {\n        color: blue;\n      }\n      @media (min-width: 768px) {\n        .rmx-complex {\n          font-size: 16px;\n        }\n      }\n    `\n    mgr.insert('rmx-complex', complexRule)\n\n    let sheet = document.adoptedStyleSheets[document.adoptedStyleSheets.length - 1]\n    expect(sheet.cssRules.length).toBe(1)\n    let cssText = sheet.cssRules[0]?.cssText || ''\n    expect(cssText).toContain('.rmx-complex')\n    expect(cssText).toContain(':hover')\n    expect(cssText).toContain('@media')\n\n    // cleanup\n    mgr.dispose()\n  })\n\n  it('removes only when count reaches zero and reindexes', () => {\n    let mgr = createStyleManager('rmx-test')\n    mgr.insert('rmx-a', '.rmx-a { color: red; }')\n    mgr.insert('rmx-b', '.rmx-b { color: blue; }')\n    mgr.insert('rmx-a', '.rmx-a { color: red; }')\n    let sheet = document.adoptedStyleSheets[document.adoptedStyleSheets.length - 1]\n\n    // decrement rmx-a once (still present)\n    mgr.remove('rmx-a')\n    expect(mgr.has('rmx-a')).toBe(true)\n    expect(sheet.cssRules.length).toBe(2)\n    expect(sheet.cssRules[0]?.cssText + sheet.cssRules[1]?.cssText).toContain('.rmx-a')\n    expect(sheet.cssRules[0]?.cssText + sheet.cssRules[1]?.cssText).toContain('.rmx-b')\n\n    // remove rmx-b fully\n    mgr.remove('rmx-b')\n    expect(mgr.has('rmx-b')).toBe(false)\n    expect(sheet.cssRules.length).toBe(1)\n    expect(sheet.cssRules[0]?.cssText).toContain('.rmx-a')\n\n    // remove rmx-a final time\n    mgr.remove('rmx-a')\n    expect(mgr.has('rmx-a')).toBe(false)\n    expect(sheet.cssRules.length).toBe(0)\n    // cleanup\n    mgr.dispose()\n  })\n\n  it('keeps other rules stable when removing from the middle (reindex correctness)', () => {\n    let mgr = createStyleManager('rmx-test')\n\n    mgr.insert('rmx-a', '.rmx-a { color: red; }')\n    mgr.insert('rmx-b', '.rmx-b { color: blue; }')\n    mgr.insert('rmx-c', '.rmx-c { color: green; }')\n    let sheet = document.adoptedStyleSheets[document.adoptedStyleSheets.length - 1]\n    expect(sheet.cssRules.length).toBe(3)\n\n    // Remove middle\n    mgr.remove('rmx-b')\n    expect(sheet.cssRules.length).toBe(2)\n    let combined = Array.from(sheet.cssRules)\n      .map((r) => (r as any).cssText || '')\n      .join('\\n')\n    expect(combined).toContain('.rmx-a')\n    expect(combined).toContain('.rmx-c')\n\n    // Now remove rmx-c; should target the correct (shifted) index\n    mgr.remove('rmx-c')\n    expect(sheet.cssRules.length).toBe(1)\n    expect((sheet.cssRules[0] as any).cssText || '').toContain('.rmx-a')\n\n    // cleanup\n    mgr.dispose()\n  })\n\n  it('styles apply to the DOM while present and stop applying after full removal', () => {\n    let mgr = createStyleManager('rmx-test')\n\n    let el = document.createElement('div')\n    document.body.appendChild(el)\n\n    mgr.insert('rmx-a', '.rmx-a { color: rgb(255, 0, 0); }')\n    let sheet = document.adoptedStyleSheets[document.adoptedStyleSheets.length - 1]\n    el.className = 'rmx-a'\n    expect(getComputedStyle(el).color).toBe('rgb(255, 0, 0)')\n\n    // Double insert then remove once -> still styled\n    mgr.insert('rmx-a', '.rmx-a { color: rgb(255, 0, 0); }')\n    mgr.remove('rmx-a')\n    expect(getComputedStyle(el).color).toBe('rgb(255, 0, 0)')\n\n    // Final remove -> rule gone, style should not be red (likely 'rgb(0, 0, 0)' or inherited)\n    mgr.remove('rmx-a')\n    expect(getComputedStyle(el).color).not.toBe('rgb(255, 0, 0)')\n\n    // cleanup\n    document.body.removeChild(el)\n    mgr.dispose()\n  })\n\n  it('adopts server-rendered style tag', () => {\n    // Simulate server-rendered style tag\n    let serverStyle = document.createElement('style')\n    serverStyle.setAttribute('data-rmx-styles', '')\n    serverStyle.textContent = '@layer rmx { .rmxc-server1 { color: blue; } }'\n    document.head.appendChild(serverStyle)\n    expect(document.querySelector('style[data-rmx-styles]')).not.toBeNull()\n\n    // Create manager which should detect and adopt the server styles\n    let mgr = createStyleManager('rmx')\n\n    // Tag should be removed after adoption\n    expect(document.querySelector('style[data-rmx-styles]')).toBeNull()\n\n    // Server-rendered selector should be recognized as existing (count: 1)\n    expect(mgr.has('rmxc-server1')).toBe(true)\n\n    // Inserting the same selector should increment count from 1 to 2\n    mgr.insert('rmxc-server1', '.rmxc-server1 { color: blue; }')\n    expect(mgr.has('rmxc-server1')).toBe(true)\n\n    // Ensure the adopted stylesheet content exists in constructed sheets\n    let hasAdoptedRule = Array.from(document.adoptedStyleSheets).some((sheet) => {\n      let text = Array.from(sheet.cssRules)\n        .map((r) => (r as any).cssText || '')\n        .join('\\n')\n      return text.includes('rmxc-server1')\n    })\n    expect(hasAdoptedRule).toBe(true)\n\n    // First remove decrements count from 2 to 1, still exists\n    mgr.remove('rmxc-server1')\n    expect(mgr.has('rmxc-server1')).toBe(true)\n\n    // Second remove decrements count from 1 to 0, no longer tracked\n    // (rule stays in the shared server stylesheet, but ruleMap entry is removed)\n    mgr.remove('rmxc-server1')\n    expect(mgr.has('rmxc-server1')).toBe(false)\n\n    // cleanup\n    mgr.dispose()\n  })\n\n  it('adopts and removes streamed server style tags added after manager creation', async () => {\n    let mgr = createStyleManager('rmx')\n\n    let streamedStyle = document.createElement('style')\n    streamedStyle.setAttribute('data-rmx-styles', '')\n    streamedStyle.textContent = '@layer rmx { .rmxc-stream1 { color: green; } }'\n    document.head.appendChild(streamedStyle)\n\n    // MutationObserver runs on a microtask; wait a tick\n    await new Promise((r) => setTimeout(r, 0))\n\n    expect(document.querySelector('style[data-rmx-styles]')).toBeNull()\n    expect(mgr.has('rmxc-stream1')).toBe(true)\n\n    mgr.dispose()\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/utils.ts",
    "content": "export type Assert<T extends true> = T\n\nexport type Equal<X, Y> =\n  (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false\n\nexport async function drain(stream: ReadableStream<Uint8Array>): Promise<string> {\n  let reader = stream.getReader()\n  let decoder = new TextDecoder()\n  let html = ''\n\n  while (true) {\n    let { done, value } = await reader.read()\n    if (done) break\n    html += decoder.decode(value)\n  }\n\n  return html\n}\n\nexport function readChunks(stream: ReadableStream<Uint8Array>): AsyncGenerator<string, void, void> {\n  let reader = stream.getReader()\n  let decoder = new TextDecoder()\n\n  return (async function* () {\n    while (true) {\n      let { done, value } = await reader.read()\n      if (done) break\n      yield decoder.decode(value)\n    }\n  })()\n}\n\nexport function withResolvers<T = unknown>(): [\n  Promise<T>,\n  (value: T) => void,\n  (error: unknown) => void,\n] {\n  let resolve!: (value: T) => void\n  let reject!: (error: unknown) => void\n  let promise = new Promise<T>((res, rej) => {\n    resolve = res\n    reject = rej\n  })\n  return [promise, resolve, reject]\n}\n"
  },
  {
    "path": "packages/component/src/test/vdom.components.test.tsx",
    "content": "import { describe, it, expect, afterEach } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport type { Handle } from '../lib/component.ts'\n\ndescribe('vnode rendering', () => {\n  afterEach(() => {\n    document.body.innerHTML = ''\n    for (let node of Array.from(document.head.childNodes)) {\n      document.head.removeChild(node)\n    }\n  })\n\n  describe('components', () => {\n    it.todo('warns when render is called after component is removed')\n\n    it('inserts a component', () => {\n      let container = document.createElement('div')\n      function App() {\n        return () => <div>Hello, world!</div>\n      }\n      let { render } = createRoot(container)\n      render(<App />)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n    })\n\n    it('updates a component', () => {\n      let container = document.createElement('div')\n\n      let capturedUpdate = () => {}\n      function App(handle: Handle) {\n        let count = 1\n        capturedUpdate = () => {\n          count++\n          handle.update()\n        }\n        return () => <div>{count}</div>\n      }\n\n      let root = createRoot(container)\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<div>1</div>')\n      let div = container.querySelector('div')\n      invariant(div instanceof HTMLDivElement)\n\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<div>2</div>')\n      expect(container.querySelector('div')).toBe(div)\n\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<div>3</div>')\n      expect(container.querySelector('div')).toBe(div)\n    })\n\n    it('updates a component with a fragment', () => {\n      let container = document.createElement('div')\n\n      let capturedUpdate = () => {}\n      function App(handle: Handle) {\n        let count = 1\n        capturedUpdate = () => {\n          count++\n          handle.update()\n        }\n        return () => (\n          <>\n            {Array.from({ length: count }).map((_, i) => (\n              <span>{i}</span>\n            ))}\n          </>\n        )\n      }\n\n      let root = createRoot(container)\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<span>0</span>')\n      let span = container.querySelector('span')\n      invariant(span)\n\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<span>0</span><span>1</span>')\n      let newSpanTags = container.querySelectorAll('span')\n      expect(newSpanTags.length).toBe(2)\n      expect(newSpanTags[0]).toBe(span)\n\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<span>0</span><span>1</span><span>2</span>')\n    })\n\n    it('renders head-like elements in place on client updates', () => {\n      let container = document.createElement('div')\n      document.body.appendChild(container)\n\n      let rerender = () => {}\n\n      function App(handle: Handle) {\n        let phase = 0\n        rerender = () => {\n          phase++\n          handle.update()\n        }\n\n        return () => {\n          if (phase === 0) {\n            return (\n              <>\n                <title>Page A</title>\n                <meta name=\"description\" content=\"A\" />\n                <script type=\"application/ld+json\">{'{\"name\":\"A\"}'}</script>\n                <script type=\"text/javascript\">window.__regular = \"A\"</script>\n                <div>Phase A</div>\n              </>\n            )\n          }\n\n          if (phase === 1) {\n            return (\n              <>\n                <title>Page B</title>\n                <meta name=\"description\" content=\"B\" />\n                <script type=\"application/ld+json\">{'{\"name\":\"B\"}'}</script>\n                <div>Phase B</div>\n              </>\n            )\n          }\n\n          return <div>Phase C</div>\n        }\n      }\n\n      let root = createRoot(container)\n      root.render(<App />)\n      root.flush()\n\n      expect(container.querySelector('title')?.textContent).toBe('Page A')\n      expect(container.querySelector('meta[name=\"description\"]')?.getAttribute('content')).toBe('A')\n      expect(container.querySelector('script[type=\"application/ld+json\"]')?.textContent).toBe(\n        '{\"name\":\"A\"}',\n      )\n      expect(container.querySelector('script[type=\"text/javascript\"]')).toBeTruthy()\n      expect(document.head.querySelector('title')).toBeNull()\n      expect(document.head.querySelector('meta[name=\"description\"]')).toBeNull()\n      expect(document.head.querySelector('script[type=\"application/ld+json\"]')).toBeNull()\n\n      rerender()\n      root.flush()\n\n      expect(container.querySelector('title')?.textContent).toBe('Page B')\n      expect(container.querySelector('meta[name=\"description\"]')?.getAttribute('content')).toBe('B')\n      expect(container.querySelectorAll('meta[name=\"description\"]')).toHaveLength(1)\n      expect(container.querySelector('script[type=\"application/ld+json\"]')?.textContent).toBe(\n        '{\"name\":\"B\"}',\n      )\n      expect(container.querySelector('script[type=\"text/javascript\"]')).toBeNull()\n      expect(container.querySelector('div')?.textContent).toBe('Phase B')\n      expect(document.head.querySelector('title')).toBeNull()\n      expect(document.head.querySelector('meta[name=\"description\"]')).toBeNull()\n      expect(document.head.querySelector('script[type=\"application/ld+json\"]')).toBeNull()\n\n      rerender()\n      root.flush()\n\n      expect(container.querySelector('title')).toBeNull()\n      expect(container.querySelector('meta[name=\"description\"]')).toBeNull()\n      expect(container.querySelector('script[type=\"application/ld+json\"]')).toBeNull()\n      expect(container.innerHTML).toBe('<div>Phase C</div>')\n    })\n\n    it('dispose cleans up explicit head subtree', () => {\n      let container = document.createElement('div')\n      document.body.appendChild(container)\n\n      let root = createRoot(container)\n      root.render(\n        <>\n          <head>\n            <title>Dispose title</title>\n            <meta name=\"dispose-description\" content=\"dispose\" />\n            <script type=\"application/ld+json\">{'{\"dispose\":true}'}</script>\n          </head>\n          <div>Content</div>\n        </>,\n      )\n      root.flush()\n\n      expect(document.head.querySelector('title')?.textContent).toBe('Dispose title')\n      expect(document.head.querySelector('meta[name=\"dispose-description\"]')).toBeTruthy()\n      expect(document.head.querySelector('script[type=\"application/ld+json\"]')?.textContent).toBe(\n        '{\"dispose\":true}',\n      )\n\n      root.dispose()\n\n      expect(document.head.querySelector('title')).toBeNull()\n      expect(document.head.querySelector('meta[name=\"dispose-description\"]')).toBeNull()\n      expect(document.head.querySelector('script[type=\"application/ld+json\"]')).toBeNull()\n      expect(container.innerHTML).toBe('')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.connect.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport type { Handle } from '../lib/component.ts'\nimport { ref } from '../lib/mixins/ref-mixin.tsx'\n\ndescribe('vnode rendering', () => {\n  describe('ref', () => {\n    it('connects host node lifecycle to component scope', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let capturedNode: Element | null = null\n\n      function App(handle: Handle) {\n        return () => (\n          <div\n            mix={[\n              ref((node: Element, signal: AbortSignal) => {\n                capturedNode = node\n                signal.addEventListener('abort', () => {\n                  capturedNode = null\n                })\n              }),\n            ]}\n          >\n            Hello, world!\n          </div>\n        )\n      }\n\n      root.render(<App />)\n      root.flush()\n      expect(capturedNode).toBeInstanceOf(HTMLDivElement)\n\n      root.render(null)\n      root.flush()\n      expect(capturedNode).toBe(null)\n    })\n  })\n\n  it('calls ref only once', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    let capturedUpdate = () => {}\n    let refCalls = 0\n\n    function App(handle: Handle) {\n      capturedUpdate = () => handle.update()\n      return () => (\n        <div\n          mix={[\n            ref(() => {\n              refCalls++\n            }),\n          ]}\n        >\n          Hello, world!\n        </div>\n      )\n    }\n    root.render(<App />)\n    root.flush()\n    expect(refCalls).toBe(1)\n\n    capturedUpdate()\n    root.flush()\n    expect(refCalls).toBe(1)\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.context.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport type { Handle, RemixNode } from '../lib/component.ts'\n\ndescribe('vnode rendering', () => {\n  describe('context', () => {\n    it('provides and reads context', () => {\n      let container = document.createElement('div')\n\n      function App(handle: Handle<{ value: string }>) {\n        handle.context.set({ value: 'test' })\n        return ({ children }: { children: RemixNode }) => <div>{children}</div>\n      }\n\n      function Child(handle: Handle) {\n        let { value } = handle.context.get(App)\n        return () => <main>Child: {value}</main>\n      }\n\n      let root = createRoot(container)\n      root.render(\n        <App>\n          <Child />\n        </App>,\n      )\n      expect(container.innerHTML).toContain('Child: test')\n    })\n\n    it('provides context on updates', () => {\n      let container = document.createElement('div')\n\n      let capturedUpdate = () => {}\n      function App(handle: Handle<{ value: string }>) {\n        handle.context.set({ value: 'test' })\n        capturedUpdate = () => {\n          handle.context.set({ value: 'test2' })\n          handle.update()\n        }\n        return ({ children }: { children: RemixNode }) => <div>{children}</div>\n      }\n\n      function Child(handle: Handle) {\n        return () => {\n          let { value } = handle.context.get(App)\n          return <main>Child: {value}</main>\n        }\n      }\n\n      let root = createRoot(container)\n      root.render(\n        <App>\n          <Child />\n        </App>,\n      )\n      expect(container.innerHTML).toContain('Child: test')\n\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toContain('Child: test2')\n    })\n\n    it('renders descendants in order of appearance', () => {\n      let container = document.createElement('div')\n\n      let options: string[] = []\n      let renderListbox = () => {}\n\n      function Listbox(handle: Handle<{ registerOption: (option: string) => void }>) {\n        handle.context.set({\n          registerOption: (option: string) => {\n            options.push(option)\n          },\n        })\n\n        renderListbox = handle.update\n\n        return ({ children }: { children: RemixNode }) => {\n          options = []\n          return <div>{children}</div>\n        }\n      }\n\n      function Option(handle: Handle) {\n        let { registerOption } = handle.context.get(Listbox)\n        return ({ value }: { value: string }) => {\n          registerOption(value)\n          return <div>Option</div>\n        }\n      }\n\n      function App(handle: Handle) {\n        return () => (\n          <Listbox>\n            <Option value=\"Option 1\" />\n            <Option value=\"Option 2\" />\n            <Option value=\"Option 3\" />\n          </Listbox>\n        )\n      }\n\n      let root = createRoot(container)\n\n      root.render(<App />)\n      expect(options).toEqual(['Option 1', 'Option 2', 'Option 3'])\n\n      renderListbox()\n      root.flush()\n      expect(options).toEqual(['Option 1', 'Option 2', 'Option 3'])\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.controlled-props.test.tsx",
    "content": "import { describe, expect, it } from 'vitest'\nimport { userEvent } from '@vitest/browser/context'\nimport type { Handle } from '../lib/component.ts'\nimport { createRoot } from '../lib/vdom.ts'\nimport { on } from '../index.ts'\n\ndescribe('vdom controlled props', () => {\n  it('restores controlled value on native input when no update happens', async () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<input value=\"hello\" />)\n    root.flush()\n\n    let input = container.querySelector('input') as HTMLInputElement\n    input.value = 'hello123'\n    input.dispatchEvent(new Event('input', { bubbles: true }))\n    await Promise.resolve()\n    await Promise.resolve()\n    expect(input.value).toBe('hello')\n  })\n\n  it('restores controlled checked on native change when no update happens', async () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<input type=\"checkbox\" checked={true} />)\n    root.flush()\n\n    let input = container.querySelector('input') as HTMLInputElement\n    input.checked = false\n    input.dispatchEvent(new Event('change', { bubbles: true }))\n    await Promise.resolve()\n    await Promise.resolve()\n    expect(input.checked).toBe(true)\n  })\n\n  it('allows controlled value changes when an input event calls handle.update()', () => {\n    function App(handle: Handle) {\n      let value = 'hello'\n      let renderCount = 0\n\n      function rerender() {\n        renderCount++\n        handle.update()\n      }\n\n      return () => (\n        <>\n          <input\n            value={value}\n            mix={[\n              on('input', (event) => {\n                let nextValue = event.currentTarget.value\n                if (/\\d/.test(nextValue)) return\n                value = nextValue\n                rerender()\n              }),\n            ]}\n          />\n          <output>{`${renderCount}:${value}`}</output>\n        </>\n      )\n    }\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<App />)\n    root.flush()\n\n    let input = container.querySelector('input') as HTMLInputElement\n    let output = container.querySelector('output') as HTMLOutputElement\n\n    input.value = 'helloa'\n    input.dispatchEvent(new Event('input', { bubbles: true }))\n    root.flush()\n\n    expect(input.value).toBe('helloa')\n    expect(output.textContent).toBe('1:helloa')\n  })\n\n  it('preserves controlled updates from real typing while rejecting invalid input', async () => {\n    function App(handle: Handle) {\n      let value = 'hello'\n      let renderCount = 0\n\n      function rerender() {\n        renderCount++\n        handle.update()\n      }\n\n      return () => (\n        <>\n          <label htmlFor=\"tracked\">Tracked</label>\n          <input\n            id=\"tracked\"\n            value={value}\n            mix={[\n              on('input', (event) => {\n                let nextValue = event.currentTarget.value\n                if (/\\d/.test(nextValue)) return\n                value = nextValue\n                rerender()\n              }),\n            ]}\n          />\n          <output>{`${renderCount}:${value}`}</output>\n        </>\n      )\n    }\n\n    let container = document.createElement('div')\n    document.body.appendChild(container)\n    let root = createRoot(container)\n    root.render(<App />)\n    root.flush()\n\n    let input = container.querySelector('input') as HTMLInputElement\n    let output = container.querySelector('output') as HTMLOutputElement\n\n    await userEvent.type(input, 'a')\n    root.flush()\n    expect(input.value).toBe('helloa')\n    expect(output.textContent).toBe('1:helloa')\n\n    await userEvent.type(input, '1')\n    await Promise.resolve()\n    await Promise.resolve()\n    expect(input.value).toBe('helloa')\n    expect(output.textContent).toBe('1:helloa')\n\n    await userEvent.type(input, 'b')\n    root.flush()\n    expect(input.value).toBe('helloab')\n    expect(output.textContent).toBe('2:helloab')\n\n    container.remove()\n  })\n\n  it('does not clobber controlled value when input event commits a new value', async () => {\n    function App(handle: Handle) {\n      let value = 'hello'\n      return () => (\n        <input\n          value={value}\n          mix={[\n            on('input', (event) => {\n              value = event.currentTarget.value\n              handle.update()\n            }),\n          ]}\n        />\n      )\n    }\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<App />)\n    root.flush()\n\n    let input = container.querySelector('input') as HTMLInputElement\n    input.value = 'helloa'\n    input.dispatchEvent(new Event('input', { bubbles: true }))\n    await Promise.resolve()\n    await Promise.resolve()\n    expect(input.value).toBe('helloa')\n  })\n\n  it('does not control value/checked when prop value is undefined', async () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(\n      <>\n        <input id=\"text\" value={undefined} />\n        <input id=\"check\" type=\"checkbox\" checked={undefined} />\n      </>,\n    )\n    root.flush()\n\n    let text = container.querySelector('#text') as HTMLInputElement\n    text.value = 'user typed'\n    text.dispatchEvent(new Event('input', { bubbles: true }))\n\n    let check = container.querySelector('#check') as HTMLInputElement\n    check.checked = true\n    check.dispatchEvent(new Event('change', { bubbles: true }))\n\n    await Promise.resolve()\n    await Promise.resolve()\n    expect(text.value).toBe('user typed')\n    expect(check.checked).toBe(true)\n  })\n\n  it('detaches controlled listeners on dispose', async () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<input value=\"hello\" />)\n    root.flush()\n\n    let input = container.querySelector('input') as HTMLInputElement\n    root.dispose()\n    root.flush()\n\n    input.value = 'post-dispose'\n    input.dispatchEvent(new Event('input', { bubbles: true }))\n    input.dispatchEvent(new Event('change', { bubbles: true }))\n    await Promise.resolve()\n    await Promise.resolve()\n    expect(input.value).toBe('post-dispose')\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.dom-order.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport type { Handle } from '../lib/component.ts'\n\ndescribe('vnode rendering', () => {\n  describe('conditional rendering and DOM order', () => {\n    it('maintains DOM order when component switches element types via self-update', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let showB = false\n      let capturedUpdate = () => {}\n\n      function A(handle: Handle) {\n        capturedUpdate = () => handle.update()\n        return () => (showB ? <span>B</span> : <div>A</div>)\n      }\n\n      root.render(\n        <main>\n          <A />\n          <p>C</p>\n        </main>,\n      )\n      expect(container.innerHTML).toBe('<main><div>A</div><p>C</p></main>')\n\n      showB = true\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><span>B</span><p>C</p></main>')\n\n      showB = false\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><div>A</div><p>C</p></main>')\n    })\n\n    it('maintains DOM order when component switches from component to element via self-update', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let showB = false\n      let capturedUpdate = () => {}\n\n      function B() {\n        return () => <span>B</span>\n      }\n\n      function A(handle: Handle) {\n        capturedUpdate = () => handle.update()\n        return () => (showB ? <B /> : <div>A</div>)\n      }\n\n      root.render(\n        <main>\n          <A />\n          <p>C</p>\n        </main>,\n      )\n      expect(container.innerHTML).toBe('<main><div>A</div><p>C</p></main>')\n\n      showB = true\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><span>B</span><p>C</p></main>')\n\n      showB = false\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><div>A</div><p>C</p></main>')\n    })\n\n    it('updates correctly when replaced component self-updates from component to element', () => {\n      // This tests the stale anchor bug: when component A is replaced by B,\n      // the anchor for B is captured from A's DOM. If B then self-updates\n      // to change its content type, the stale anchor must not be used.\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function Loading() {\n        return () => <div>Loading...</div>\n      }\n\n      let loaded = false\n      let capturedUpdate = () => {}\n\n      function PageB(handle: Handle) {\n        capturedUpdate = () => handle.update()\n        return () => (loaded ? <div>Loaded!</div> : <Loading />)\n      }\n\n      function PageA() {\n        return () => <div>Page A</div>\n      }\n\n      let Page: typeof PageA | typeof PageB = PageA\n\n      function App(handle: Handle) {\n        return () => (\n          <main>\n            <nav>Nav</nav>\n            <Page />\n          </main>\n        )\n      }\n\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<main><nav>Nav</nav><div>Page A</div></main>')\n\n      // Switch to PageB (captures anchor from PageA's div)\n      Page = PageB\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<main><nav>Nav</nav><div>Loading...</div></main>')\n\n      // PageB self-updates: Loading -> div (must not use stale PageA anchor)\n      loaded = true\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><nav>Nav</nav><div>Loaded!</div></main>')\n    })\n\n    it('updates correctly when component switches from element to component via self-update', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function Loading() {\n        return () => <span>Loading...</span>\n      }\n\n      let showLoading = false\n      let capturedUpdate = () => {}\n\n      function A(handle: Handle) {\n        capturedUpdate = () => handle.update()\n        return () => (showLoading ? <Loading /> : <div>Content</div>)\n      }\n\n      root.render(\n        <main>\n          <A />\n          <p>Footer</p>\n        </main>,\n      )\n      expect(container.innerHTML).toBe('<main><div>Content</div><p>Footer</p></main>')\n\n      showLoading = true\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><span>Loading...</span><p>Footer</p></main>')\n\n      showLoading = false\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><div>Content</div><p>Footer</p></main>')\n    })\n\n    it('updates correctly with deeply nested component type changes', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function Inner() {\n        return () => <span>Inner</span>\n      }\n\n      function Middle() {\n        return () => <Inner />\n      }\n\n      let useNested = true\n      let capturedUpdate = () => {}\n\n      function Outer(handle: Handle) {\n        capturedUpdate = () => handle.update()\n        return () => (useNested ? <Middle /> : <div>Direct</div>)\n      }\n\n      root.render(\n        <main>\n          <Outer />\n          <footer>Footer</footer>\n        </main>,\n      )\n      expect(container.innerHTML).toBe('<main><span>Inner</span><footer>Footer</footer></main>')\n\n      useNested = false\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><div>Direct</div><footer>Footer</footer></main>')\n\n      useNested = true\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><span>Inner</span><footer>Footer</footer></main>')\n    })\n\n    it('updates correctly when multiple components are replaced and self-update', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function LoadingA() {\n        return () => <span>Loading A...</span>\n      }\n\n      function LoadingB() {\n        return () => <span>Loading B...</span>\n      }\n\n      let loadedA = false\n      let loadedB = false\n      let capturedUpdateA = () => {}\n      let capturedUpdateB = () => {}\n\n      function CompA(handle: Handle) {\n        capturedUpdateA = () => handle.update()\n        return () => (loadedA ? <div>A Done</div> : <LoadingA />)\n      }\n\n      function CompB(handle: Handle) {\n        capturedUpdateB = () => handle.update()\n        return () => (loadedB ? <div>B Done</div> : <LoadingB />)\n      }\n\n      root.render(\n        <main>\n          <CompA />\n          <CompB />\n        </main>,\n      )\n      expect(container.innerHTML).toBe(\n        '<main><span>Loading A...</span><span>Loading B...</span></main>',\n      )\n\n      // Update A first\n      loadedA = true\n      capturedUpdateA()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><div>A Done</div><span>Loading B...</span></main>')\n\n      // Then update B\n      loadedB = true\n      capturedUpdateB()\n      root.flush()\n      expect(container.innerHTML).toBe('<main><div>A Done</div><div>B Done</div></main>')\n    })\n\n    it('maintains DOM order when replaced component self-updates with same element type', () => {\n      // Tests that anchor calculation works for same-type updates (element->element)\n      // after a component replacement. The anchor should be the next sibling, not the\n      // component's own content.\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let count = 0\n      let capturedUpdate = () => {}\n\n      function PageB(handle: Handle) {\n        capturedUpdate = () => handle.update()\n        return () => <div>Count: {count}</div>\n      }\n\n      function PageA() {\n        return () => <div>Page A</div>\n      }\n\n      let Page: typeof PageA | typeof PageB = PageA\n\n      function App(handle: Handle) {\n        return () => (\n          <main>\n            <nav>Nav</nav>\n            <Page />\n            <footer>Footer</footer>\n          </main>\n        )\n      }\n\n      root.render(<App />)\n      expect(container.innerHTML).toBe(\n        '<main><nav>Nav</nav><div>Page A</div><footer>Footer</footer></main>',\n      )\n\n      // Replace PageA with PageB (anchor is captured from PageA's div)\n      Page = PageB\n      root.render(<App />)\n      expect(container.innerHTML).toBe(\n        '<main><nav>Nav</nav><div>Count: 0</div><footer>Footer</footer></main>',\n      )\n\n      // PageB self-updates: same element type (div->div), should maintain position\n      count = 1\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe(\n        '<main><nav>Nav</nav><div>Count: 1</div><footer>Footer</footer></main>',\n      )\n\n      // Another self-update\n      count = 2\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe(\n        '<main><nav>Nav</nav><div>Count: 2</div><footer>Footer</footer></main>',\n      )\n    })\n\n    it('maintains DOM order when fragment component adds children via self-update with siblings', () => {\n      // Critical test: a component renders a fragment, has siblings after it,\n      // and grows the fragment via self-update. Without proper anchor calculation,\n      // new children would be appended after the siblings.\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let items = [0]\n      let capturedUpdate = () => {}\n\n      function List(handle: Handle) {\n        capturedUpdate = () => handle.update()\n        // No keys - uses index-based diff\n        return () => (\n          <>\n            {items.map((i) => (\n              <span>{i}</span>\n            ))}\n          </>\n        )\n      }\n\n      root.render(\n        <main>\n          <List />\n          <footer>Footer</footer>\n        </main>,\n      )\n      expect(container.innerHTML).toBe('<main><span>0</span><footer>Footer</footer></main>')\n\n      // Add more items - new spans must appear BEFORE footer\n      items = [0, 1]\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe(\n        '<main><span>0</span><span>1</span><footer>Footer</footer></main>',\n      )\n\n      // Add even more\n      items = [0, 1, 2]\n      capturedUpdate()\n      root.flush()\n      expect(container.innerHTML).toBe(\n        '<main><span>0</span><span>1</span><span>2</span><footer>Footer</footer></main>',\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.elements-fragments.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\n\ndescribe('vnode rendering', () => {\n  describe('elements', () => {\n    it('renders basic elements', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(<div>Hello, world!</div>)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n    })\n\n    it('renders nested elements', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(\n        <div>\n          Hello, <span>world!</span>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div>Hello, <span>world!</span></div>')\n    })\n\n    it('renders attributes', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(<input id=\"hello\" value=\"world\" />)\n      let input = container.querySelector('input')\n      invariant(input instanceof HTMLInputElement)\n      expect(input.value).toBe('world')\n      expect(container.innerHTML).toBe('<input id=\"hello\">')\n    })\n\n    it('renders 0 as a child', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(<div>{0}</div>)\n      expect(container.innerHTML).toBe('<div>0</div>')\n    })\n\n    it('renders style object via DOM properties; hydration leaves string in place', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(\n        <div\n          style={{\n            marginTop: 12,\n            display: 'block',\n            lineHeight: Number.NaN,\n            '--size': 10,\n          }}\n        >\n          X\n        </div>,\n      )\n      let div = container.querySelector('div')\n      invariant(div instanceof HTMLDivElement)\n      expect(div.style.marginTop).toBe('12px')\n      expect(div.style.display).toBe('block')\n      expect(div.getAttribute('style') || '').toContain('--size: 10')\n      expect(div.style.lineHeight).toBe('')\n\n      let container2 = document.createElement('div')\n      container2.innerHTML = '<div style=\"color: red\">X</div>'\n      let root2 = createRoot(container2)\n      root2.render(<div style={{ color: 'blue' }}>X</div>)\n      let div2 = container2.querySelector('div')\n      invariant(div2 instanceof HTMLDivElement)\n      expect(div2.style.color).toBe('blue')\n    })\n  })\n\n  describe('fragments', () => {\n    it('inserts fragments', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(\n        <>\n          <p>Hello</p>\n          <p>world!</p>\n        </>,\n      )\n      expect(container.innerHTML).toBe('<p>Hello</p><p>world!</p>')\n    })\n\n    it('inserts nested fragments', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(\n        <div>\n          <>\n            <p>Hello</p>\n            <p>world!</p>\n          </>\n          <p>Goodbye</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><p>Hello</p><p>world!</p><p>Goodbye</p></div>')\n    })\n\n    it('inserts new nodes in a parent', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(\n        <div>\n          <p>Hello</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><p>Hello</p></div>')\n\n      let p = container.querySelector('p')\n      invariant(p)\n      render(\n        <div>\n          <p>Hello</p>\n          <p>Goodbye</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><p>Hello</p><p>Goodbye</p></div>')\n      expect(container.querySelector('p')).toBe(p)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.errors.test.tsx",
    "content": "import { describe, it, expect, vi } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { on } from '../index.ts'\nimport type { Handle } from '../lib/component.ts'\n\ndescribe('vdom error handling', () => {\n  describe('root event forwarding', () => {\n    it('forwards bubbling DOM error events to root listeners', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let forwarded: unknown\n\n      root.addEventListener('error', (event) => {\n        forwarded = (event as ErrorEvent).error\n      })\n\n      let expected = new Error('createRoot forwarded error')\n      container.dispatchEvent(new ErrorEvent('error', { bubbles: true, error: expected }))\n\n      expect(forwarded).toBe(expected)\n    })\n\n    it('stops forwarding bubbling DOM error events after dispose', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let forwarded: unknown\n\n      root.addEventListener('error', (event) => {\n        forwarded = (event as ErrorEvent).error\n      })\n\n      root.dispose()\n\n      container.dispatchEvent(\n        new ErrorEvent('error', { bubbles: true, error: new Error('after dispose') }),\n      )\n\n      expect(forwarded).toBeUndefined()\n    })\n\n    it('dispose is a no-op before first render', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      root.dispose()\n      root.flush()\n\n      expect(container.innerHTML).toBe('')\n    })\n  })\n\n  describe('setup errors', () => {\n    it('dispatches error event when setup throws', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let errorHandler = vi.fn()\n      root.addEventListener('error', errorHandler)\n\n      let error = new Error('setup error')\n      function BadComponent() {\n        throw error\n        return () => <div>ok</div>\n      }\n\n      root.render(<BadComponent />)\n\n      expect(errorHandler).toHaveBeenCalledTimes(1)\n      expect((errorHandler.mock.calls[0][0] as ErrorEvent).error).toBe(error)\n    })\n\n    it('dispatches error event when nested component setup throws', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let errorHandler = vi.fn()\n      root.addEventListener('error', errorHandler)\n\n      let error = new Error('nested setup error')\n      function BadChild() {\n        throw error\n        return () => null\n      }\n\n      function Parent() {\n        return () => (\n          <div>\n            <BadChild />\n          </div>\n        )\n      }\n\n      root.render(<Parent />)\n\n      expect(errorHandler).toHaveBeenCalledTimes(1)\n      expect((errorHandler.mock.calls[0][0] as ErrorEvent).error).toBe(error)\n    })\n  })\n\n  describe('render errors', () => {\n    it('dispatches error event when render function throws', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let errorHandler = vi.fn()\n      root.addEventListener('error', errorHandler)\n\n      let error = new Error('render error')\n      function BadComponent() {\n        return () => {\n          throw error\n        }\n      }\n\n      root.render(<BadComponent />)\n\n      expect(errorHandler).toHaveBeenCalledTimes(1)\n      expect((errorHandler.mock.calls[0][0] as ErrorEvent).error).toBe(error)\n    })\n\n    it('dispatches error event when render throws on update', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let errorHandler = vi.fn()\n      root.addEventListener('error', errorHandler)\n\n      let shouldThrow = false\n      let error = new Error('render update error')\n      let update: () => void\n\n      function Component(handle: Handle) {\n        update = () => handle.update()\n        return () => {\n          if (shouldThrow) throw error\n          return <div>ok</div>\n        }\n      }\n\n      root.render(<Component />)\n      expect(container.innerHTML).toBe('<div>ok</div>')\n      expect(errorHandler).not.toHaveBeenCalled()\n\n      shouldThrow = true\n      update!()\n      root.flush()\n\n      expect(errorHandler).toHaveBeenCalledTimes(1)\n      expect((errorHandler.mock.calls[0][0] as ErrorEvent).error).toBe(error)\n    })\n  })\n\n  describe('event handler errors', () => {\n    it('runs sync event handlers attached via on() mixin', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let clicks = 0\n\n      root.render(\n        <button\n          mix={[\n            on('click', () => {\n              clicks++\n            }),\n          ]}\n        >\n          Click\n        </button>,\n      )\n      root.flush()\n\n      let button = container.querySelector('button')!\n      button.click()\n\n      expect(clicks).toBe(1)\n    })\n\n    it('runs async event handlers attached via on() mixin', async () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let calls = 0\n\n      root.render(\n        <button\n          mix={[\n            on('click', async () => {\n              await Promise.resolve()\n              calls++\n            }),\n          ]}\n        >\n          Click\n        </button>,\n      )\n      root.flush()\n\n      let button = container.querySelector('button')!\n      button.click()\n\n      // Let the async handler complete\n      await Promise.resolve()\n      await Promise.resolve()\n\n      expect(calls).toBe(1)\n    })\n  })\n\n  describe('queueTask errors', () => {\n    it('dispatches error event when sync queueTask throws', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let errorHandler = vi.fn()\n      root.addEventListener('error', errorHandler)\n\n      let error = new Error('sync task error')\n\n      function Component(handle: Handle) {\n        handle.queueTask(() => {\n          throw error\n        })\n        return () => <div>ok</div>\n      }\n\n      root.render(<Component />)\n      root.flush()\n\n      expect(errorHandler).toHaveBeenCalledTimes(1)\n      expect((errorHandler.mock.calls[0][0] as ErrorEvent).error).toBe(error)\n    })\n\n    it('dispatches error event when queueTask from update throws', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let errorHandler = vi.fn()\n      root.addEventListener('error', errorHandler)\n\n      let error = new Error('update task error')\n      let update: () => void\n\n      function Component(handle: Handle) {\n        update = () => {\n          handle.queueTask(() => {\n            throw error\n          })\n          handle.update()\n        }\n        return () => <div>ok</div>\n      }\n\n      root.render(<Component />)\n      root.flush()\n      expect(errorHandler).not.toHaveBeenCalled()\n\n      update!()\n      root.flush()\n\n      expect(errorHandler).toHaveBeenCalledTimes(1)\n      expect((errorHandler.mock.calls[0][0] as ErrorEvent).error).toBe(error)\n    })\n  })\n\n  describe('error does not prevent other work', () => {\n    it('continues running tasks after task error', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let errorHandler = vi.fn()\n      root.addEventListener('error', errorHandler)\n\n      let taskRan = false\n\n      function Bad(handle: Handle) {\n        handle.queueTask(() => {\n          throw new Error('bad task')\n        })\n        return () => <div>bad</div>\n      }\n\n      function Good(handle: Handle) {\n        handle.queueTask(() => {\n          taskRan = true\n        })\n        return () => <div>good</div>\n      }\n\n      root.render(\n        <>\n          <Bad />\n          <Good />\n        </>,\n      )\n      root.flush()\n\n      expect(errorHandler).toHaveBeenCalledTimes(1)\n      expect(taskRan).toBe(true)\n    })\n  })\n\n  describe('DOM state after errors', () => {\n    it('leaves DOM empty when initial render throws', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.addEventListener('error', () => {})\n\n      function Bad() {\n        throw new Error('bad')\n        return () => <div>ok</div>\n      }\n\n      root.render(<Bad />)\n\n      expect(container.innerHTML).toBe('')\n    })\n\n    it('preserves previous DOM when update throws', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.addEventListener('error', () => {})\n\n      let shouldThrow = false\n      let update: () => void\n\n      function Component(handle: Handle) {\n        update = () => handle.update()\n        return () => {\n          if (shouldThrow) throw new Error('update error')\n          return <div>ok</div>\n        }\n      }\n\n      root.render(<Component />)\n      expect(container.innerHTML).toBe('<div>ok</div>')\n\n      shouldThrow = true\n      update!()\n      root.flush()\n\n      // Previous DOM is preserved\n      expect(container.innerHTML).toBe('<div>ok</div>')\n    })\n\n    it('preserves DOM when event handler runs', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.addEventListener('error', () => {})\n\n      root.render(\n        <button\n          mix={[\n            on('click', () => {\n              // no-op\n            }),\n          ]}\n        >\n          Click\n        </button>,\n      )\n      root.flush()\n\n      expect(container.innerHTML).toBe('<button>Click</button>')\n\n      let button = container.querySelector('button')!\n      button.click()\n\n      // DOM unchanged after event error\n      expect(container.innerHTML).toBe('<button>Click</button>')\n    })\n  })\n\n  describe('cascading updates protection', () => {\n    it('dispatches error when handle.update() is called during render', async () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let errorHandler = vi.fn()\n      root.addEventListener('error', errorHandler)\n\n      let renderCount = 0\n      let triggerUpdate: () => void\n\n      function InfiniteLoop(handle: Handle) {\n        triggerUpdate = () => {\n          handle.update()\n        }\n        return () => {\n          renderCount++\n          if (renderCount > 1) {\n            handle.update()\n          }\n          return <div>count: {renderCount}</div>\n        }\n      }\n\n      root.render(<InfiniteLoop />)\n      root.flush()\n      expect(container.innerHTML).toBe('<div>count: 1</div>')\n      expect(renderCount).toBe(1)\n\n      triggerUpdate!()\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(errorHandler).toHaveBeenCalled()\n      let error = (errorHandler.mock.calls[0][0] as ErrorEvent).error as Error\n      expect(error.message).toContain('infinite loop detected')\n      expect(renderCount).toBeLessThan(100)\n    })\n\n    it('allows legitimate multiple updates within same event loop turn', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let errorHandler = vi.fn()\n      root.addEventListener('error', errorHandler)\n\n      let count = 0\n      let update: () => void\n\n      function Counter(handle: Handle) {\n        update = () => handle.update()\n        return () => <div>count: {count}</div>\n      }\n\n      root.render(<Counter />)\n      root.flush()\n\n      count++\n      update!()\n      root.flush()\n\n      count++\n      update!()\n      root.flush()\n\n      count++\n      update!()\n      root.flush()\n\n      expect(container.innerHTML).toBe('<div>count: 3</div>')\n      expect(errorHandler).not.toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.events.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport { on } from '../index.ts'\n\ndescribe('vnode rendering', () => {\n  describe('events integration', () => {\n    it('attaches events via on() mixin', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let clicked = false\n      root.render(\n        <button\n          mix={[\n            on('click', () => {\n              clicked = true\n            }),\n          ]}\n        >\n          Click me\n        </button>,\n      )\n\n      expect(container.innerHTML).toBe('<button>Click me</button>')\n      root.flush()\n\n      let button = container.querySelector('button')\n      invariant(button)\n      button.click()\n      expect(clicked).toBe(true)\n    })\n\n    it('updates on() mixin listeners across rerenders', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let clickCount = 0\n      function App() {\n        return () => (\n          <button\n            mix={[\n              on('click', () => {\n                clickCount++\n              }),\n            ]}\n          >\n            Click me\n          </button>\n        )\n      }\n\n      root.render(<App />)\n      root.flush()\n\n      let button = container.querySelector('button')\n      invariant(button)\n      button.click()\n      expect(clickCount).toBe(1)\n\n      root.render(<App />)\n      root.flush()\n\n      button.click()\n      expect(clickCount).toBe(2)\n    })\n\n    it('cleans up mixin listeners when removed', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let clickCount = 0\n      root.render(\n        <button\n          mix={[\n            on('click', () => {\n              clickCount++\n            }),\n          ]}\n        >\n          Click me\n        </button>,\n      )\n      root.flush()\n\n      let button = container.querySelector('button')\n      invariant(button)\n      button.click()\n      expect(clickCount).toBe(1)\n\n      // remove event mixin\n      root.render(<button>Click me</button>)\n      root.flush()\n\n      button.click()\n      expect(clickCount).toBe(1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.insert-remove.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\n\ndescribe('vnode rendering', () => {\n  describe('inserts', () => {\n    it('renders text', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render('Hello, world!')\n      expect(container.innerHTML).toBe('Hello, world!')\n    })\n\n    it('renders number', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(42)\n      expect(container.innerHTML).toBe('42')\n    })\n\n    it('renders 0', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(0)\n      expect(container.innerHTML).toBe('0')\n    })\n\n    it('renders bigint', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(BigInt(9007199254740991))\n      expect(container.innerHTML).toBe('9007199254740991')\n    })\n\n    it('renders true', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(true)\n      expect(container.innerHTML).toBe('')\n    })\n\n    it('renders false', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(false)\n      expect(container.innerHTML).toBe('')\n    })\n\n    it('renders null', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(null)\n      expect(container.innerHTML).toBe('')\n    })\n\n    it('renders undefined', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(undefined)\n      expect(container.innerHTML).toBe('')\n    })\n  })\n\n  describe('removals', () => {\n    it('removes a text node', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(<div>Hello, world!</div>)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      render(<div />)\n      expect(container.innerHTML).toBe('<div></div>')\n    })\n\n    it('removes an element', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(\n        <div>\n          <span>Hello, world!</span>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><span>Hello, world!</span></div>')\n      render(<div />)\n      expect(container.innerHTML).toBe('<div></div>')\n    })\n\n    it('removes attributes', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(<input id=\"hello\" value=\"world\" />)\n      let input = container.querySelector('input')\n      expect(input).toBeInstanceOf(HTMLInputElement)\n      expect((input as HTMLInputElement).value).toBe('world')\n      expect((input as HTMLInputElement).getAttribute('id')).toBe('hello')\n      root.render(<input />)\n      root.flush()\n      expect((input as HTMLInputElement).value).toBe('')\n      expect((input as HTMLInputElement).hasAttribute('id')).toBe(false)\n      expect((input as HTMLInputElement).hasAttribute('value')).toBe(false)\n    })\n\n    it('removes reflected attributes without leaving empty values', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <div id=\"hello\" className=\"world\">\n          content\n        </div>,\n      )\n\n      let div = container.querySelector('div')\n      expect(div).toBeInstanceOf(HTMLDivElement)\n      expect((div as HTMLDivElement).getAttribute('id')).toBe('hello')\n      expect((div as HTMLDivElement).getAttribute('class')).toBe('world')\n\n      root.render(<div>content</div>)\n      root.flush()\n\n      expect((div as HTMLDivElement).hasAttribute('id')).toBe(false)\n      expect((div as HTMLDivElement).hasAttribute('class')).toBe(false)\n    })\n\n    it('removes a fragment', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(\n        <div>\n          <>\n            <p>Hello</p>\n            <p>world!</p>\n          </>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><p>Hello</p><p>world!</p></div>')\n      render(<div />)\n      expect(container.innerHTML).toBe('<div></div>')\n    })\n\n    it('removes a component', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      function App() {\n        return () => <div>Hello, world!</div>\n      }\n      render(\n        <div>\n          <App />\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><div>Hello, world!</div></div>')\n      render(<div></div>)\n      expect(container.innerHTML).toBe('<div></div>')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.keys.test.tsx",
    "content": "import { describe, it, expect, vi } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport { Fragment } from '../lib/component.ts'\n\ndescribe('vnode rendering (keys)', () => {\n  describe('keyed list with non-keyed sibling', () => {\n    it('appends keyed component before non-keyed sibling', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      type CardData = { id: string; title: string }\n\n      function Card() {\n        return ({ card }: { card: CardData }) => <div data-id={card.id}>{card.title}</div>\n      }\n\n      function Column() {\n        return ({ cards, isAddingCard }: { cards: CardData[]; isAddingCard: boolean }) => (\n          <div>\n            {cards.map((card) => (\n              <Card key={card.id} card={card} />\n            ))}\n            {isAddingCard ? <div id=\"form\">Form</div> : <button>Add</button>}\n          </div>\n        )\n      }\n\n      // Initial: 2 cards, button visible\n      let cards: CardData[] = [\n        { id: '1', title: 'Card 1' },\n        { id: '2', title: 'Card 2' },\n      ]\n      root.render(<Column cards={cards} isAddingCard={false} />)\n\n      let col = container.querySelector('div')\n      invariant(col)\n      expect(col.innerHTML).toBe(\n        '<div data-id=\"1\">Card 1</div><div data-id=\"2\">Card 2</div><button>Add</button>',\n      )\n\n      // Click \"Add\" - form appears\n      root.render(<Column cards={cards} isAddingCard={true} />)\n      expect(col.innerHTML).toBe(\n        '<div data-id=\"1\">Card 1</div><div data-id=\"2\">Card 2</div><div id=\"form\">Form</div>',\n      )\n\n      // Add a new card while form is visible\n      cards = [...cards, { id: '3', title: 'Card 3' }]\n      root.render(<Column cards={cards} isAddingCard={true} />)\n\n      // Regression: The new card must appear BEFORE the form.\n      expect(col.innerHTML).toBe(\n        '<div data-id=\"1\">Card 1</div><div data-id=\"2\">Card 2</div><div data-id=\"3\">Card 3</div><div id=\"form\">Form</div>',\n      )\n    })\n  })\n\n  describe('basic keyed list operations', () => {\n    it('handles prepending items with keys', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function List() {\n        return ({ values }: { values: (string | number)[] }) => (\n          <ul>\n            {values.map((value) => (\n              <li key={value} data-id={value}>\n                {value}\n              </li>\n            ))}\n          </ul>\n        )\n      }\n\n      let values: (string | number)[] = ['b', 'c']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('bc')\n\n      values = ['a', ...values]\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('abc')\n\n      let items = Array.from(container.querySelectorAll('li'))\n      expect(items.map((el) => el.getAttribute('data-id'))).toEqual(['a', 'b', 'c'])\n    })\n\n    it('handles appending items with keys', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function List() {\n        return ({ values }: { values: string[] }) => (\n          <ol>\n            {values.map((value) => (\n              <li key={value} data-id={value}>\n                {value}\n              </li>\n            ))}\n          </ol>\n        )\n      }\n\n      let values = ['a', 'b']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('ab')\n\n      let a = container.querySelector('[data-id=\"a\"]')\n      let b = container.querySelector('[data-id=\"b\"]')\n      expect(a).toBeInstanceOf(HTMLLIElement)\n      expect(b).toBeInstanceOf(HTMLLIElement)\n\n      values = [...values, 'c']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('abc')\n\n      let items = Array.from(container.querySelectorAll('li'))\n      expect(items[0]).toBe(a)\n      expect(items[1]).toBe(b)\n      expect(items[2].getAttribute('data-id')).toBe('c')\n    })\n\n    it('handles removing items with keys', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function List() {\n        return ({ values }: { values: string[] }) => (\n          <ul>\n            {values.map((value) => (\n              <li key={value} data-id={value}>\n                {value}\n              </li>\n            ))}\n          </ul>\n        )\n      }\n\n      let values = ['a', 'b', 'c', 'd']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('abcd')\n\n      let a = container.querySelector('[data-id=\"a\"]')\n      let d = container.querySelector('[data-id=\"d\"]')\n      expect(a).toBeInstanceOf(HTMLLIElement)\n      expect(d).toBeInstanceOf(HTMLLIElement)\n\n      values = ['a', 'c', 'd']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('acd')\n\n      let items = Array.from(container.querySelectorAll('li'))\n      expect(items[0]).toBe(a)\n      expect(items[1].getAttribute('data-id')).toBe('c')\n      expect(items[2]).toBe(d)\n      expect(container.querySelector('[data-id=\"b\"]')).toBe(null)\n    })\n\n    it('handles inserting items with keys', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function List() {\n        return ({ values }: { values: string[] }) => (\n          <ul>\n            {values.map((value) => (\n              <li key={value} data-id={value}>\n                {value}\n              </li>\n            ))}\n          </ul>\n        )\n      }\n\n      let values = ['a', 'c']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('ac')\n\n      let a = container.querySelector('[data-id=\"a\"]')\n      let c = container.querySelector('[data-id=\"c\"]')\n      expect(a).toBeInstanceOf(HTMLLIElement)\n      expect(c).toBeInstanceOf(HTMLLIElement)\n\n      values = ['a', 'b', 'c']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('abc')\n\n      let items = Array.from(container.querySelectorAll('li'))\n      expect(items[0]).toBe(a)\n      expect(items[1].getAttribute('data-id')).toBe('b')\n      expect(items[2]).toBe(c)\n    })\n\n    it('handles swapping adjacent items with keys', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function List() {\n        return ({ values }: { values: string[] }) => (\n          <ul>\n            {values.map((value) => (\n              <li key={value} data-id={value}>\n                {value}\n              </li>\n            ))}\n          </ul>\n        )\n      }\n\n      let values = ['a', 'b']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('ab')\n\n      let a = container.querySelector('[data-id=\"a\"]')\n      let b = container.querySelector('[data-id=\"b\"]')\n      expect(a).toBeInstanceOf(HTMLLIElement)\n      expect(b).toBeInstanceOf(HTMLLIElement)\n\n      values = ['b', 'a']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('ba')\n\n      let items = Array.from(container.querySelectorAll('li'))\n      expect(items[0]).toBe(b)\n      expect(items[1]).toBe(a)\n    })\n\n    it('handles reversing list order with keys', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function List() {\n        return ({ values }: { values: string[] }) => (\n          <ul>\n            {values.map((value) => (\n              <li key={value} data-id={value}>\n                {value}\n              </li>\n            ))}\n          </ul>\n        )\n      }\n\n      let values = ['a', 'b', 'c', 'd']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('abcd')\n\n      let nodes = values.map((value) => {\n        let el = container.querySelector(`[data-id=\"${value}\"]`)\n        invariant(el instanceof HTMLLIElement)\n        return el\n      })\n\n      values = [...values].reverse()\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('dcba')\n\n      let items = Array.from(container.querySelectorAll('li'))\n      expect(items[0]).toBe(nodes[3])\n      expect(items[1]).toBe(nodes[2])\n      expect(items[2]).toBe(nodes[1])\n      expect(items[3]).toBe(nodes[0])\n    })\n\n    it('handles complex reordering with keys', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function List() {\n        return ({ values }: { values: string[] }) => (\n          <ul>\n            {values.map((value) => (\n              <li key={value} data-id={value}>\n                {value}\n              </li>\n            ))}\n          </ul>\n        )\n      }\n\n      let values = ['a', 'b', 'c', 'd', 'e', 'f']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('abcdef')\n\n      let nodes = values.map((value) => {\n        let el = container.querySelector(`[data-id=\"${value}\"]`)\n        invariant(el instanceof HTMLLIElement)\n        return el\n      })\n\n      // move e to near the front, and c towards the end\n      values = ['a', 'e', 'b', 'f', 'c', 'd']\n      root.render(<List values={values} />)\n      expect(container.textContent).toBe('aebfcd')\n\n      let items = Array.from(container.querySelectorAll('li'))\n      expect(items[0]).toBe(nodes[0]) // a\n      expect(items[1]).toBe(nodes[4]) // e\n      expect(items[2]).toBe(nodes[1]) // b\n      expect(items[3]).toBe(nodes[5]) // f\n      expect(items[4]).toBe(nodes[2]) // c\n      expect(items[5]).toBe(nodes[3]) // d\n    })\n  })\n\n  describe('key semantics', () => {\n    it('replaces nodes when keys match but type differs', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      root.render(\n        <div>\n          <span key=\"x\" id=\"x\">\n            X\n          </span>\n        </div>,\n      )\n      let first = container.querySelector('#x')\n      invariant(first instanceof HTMLSpanElement)\n      expect(container.innerHTML).toBe('<div><span id=\"x\">X</span></div>')\n\n      root.render(\n        <div>\n          <p key=\"x\" id=\"x\">\n            Y\n          </p>\n        </div>,\n      )\n      let second = container.querySelector('#x')\n      invariant(second instanceof HTMLParagraphElement)\n      expect(container.innerHTML).toBe('<div><p id=\"x\">Y</p></div>')\n      expect(second).not.toBe(first)\n    })\n\n    it('handles mixed keyed and unkeyed children', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function Item() {\n        return ({ label }: { label: string }) => <li>{label}</li>\n      }\n\n      root.render(\n        <ul>\n          <Item key=\"a\" label=\"A\" />\n          <Item label=\"unkeyed-1\" />\n          <Item key=\"b\" label=\"B\" />\n          <Item label=\"unkeyed-2\" />\n        </ul>,\n      )\n\n      expect(container.textContent).toBe('Aunkeyed-1Bunkeyed-2')\n\n      root.render(\n        <ul>\n          {/* swap keyed items and insert another unkeyed between them */}\n          <Item label=\"unkeyed-1\" />\n          <Item key=\"b\" label=\"B\" />\n          <Item label=\"unkeyed-2\" />\n          <Item key=\"a\" label=\"A\" />\n        </ul>,\n      )\n\n      expect(container.textContent).toBe('unkeyed-1Bunkeyed-2A')\n    })\n\n    it('handles duplicate keys (last one wins)', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})\n\n      function List() {\n        return ({ labels }: { labels: string[] }) => (\n          <ul>\n            {labels.map((label, index) => (\n              <li key=\"dup\" data-index={index}>\n                {label}\n              </li>\n            ))}\n          </ul>\n        )\n      }\n\n      try {\n        root.render(<List labels={['first', 'second']} />)\n        expect(container.textContent).toBe('firstsecond')\n\n        root.render(<List labels={['only']} />)\n        expect(container.textContent).toBe('only')\n\n        let items = Array.from(container.querySelectorAll('li'))\n        expect(items.length).toBe(1)\n        expect(items[0].getAttribute('data-index')).toBe('0')\n        expect(warnSpy).toHaveBeenCalledTimes(1)\n        let warning = String(warnSpy.mock.calls[0]?.[0] ?? '')\n        expect(warning).toContain('Duplicate keys detected in siblings')\n        expect(warning).toContain('\"dup\"')\n      } finally {\n        warnSpy.mockRestore()\n      }\n    })\n\n    it('allows any type to be a key', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let objKey = {}\n      let symKey = Symbol('k')\n\n      root.render(\n        <ul>\n          <li key={1}>one</li>\n          <li key=\"two\">two</li>\n          <li key={objKey}>obj</li>\n          <li key={symKey}>sym</li>\n        </ul>,\n      )\n\n      expect(container.textContent).toBe('onetwoobjsym')\n\n      root.render(\n        <ul>\n          <li key={symKey}>sym*</li>\n          <li key={1}>one*</li>\n          <li key={objKey}>obj*</li>\n          <li key=\"two\">two*</li>\n        </ul>,\n      )\n\n      expect(container.textContent).toBe('sym*one*obj*two*')\n    })\n\n    it('reorders keyed fragments correctly (moves entire DOM range)', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      // Each keyed item renders multiple DOM nodes via fragment\n      root.render(\n        <div>\n          {['a', 'b', 'c'].map((id) => (\n            <Fragment key={id}>\n              <span>{id}</span>\n              <button>{id}-btn</button>\n            </Fragment>\n          ))}\n        </div>,\n      )\n\n      expect(container.innerHTML).toBe(\n        '<div><span>a</span><button>a-btn</button><span>b</span><button>b-btn</button><span>c</span><button>c-btn</button></div>',\n      )\n\n      // Reverse order - entire fragment ranges should move together\n      root.render(\n        <div>\n          {['c', 'b', 'a'].map((id) => (\n            <Fragment key={id}>\n              <span>{id}</span>\n              <button>{id}-btn</button>\n            </Fragment>\n          ))}\n        </div>,\n      )\n\n      expect(container.innerHTML).toBe(\n        '<div><span>c</span><button>c-btn</button><span>b</span><button>b-btn</button><span>a</span><button>a-btn</button></div>',\n      )\n\n      // Verify DOM nodes were reused, not recreated\n      let spans = container.querySelectorAll('span')\n      let buttons = container.querySelectorAll('button')\n      expect(spans.length).toBe(3)\n      expect(buttons.length).toBe(3)\n    })\n\n    it('handles keys in fragments without breaking updates', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function Item() {\n        return ({ id, label }: { id: string; label: string }) => (\n          <>\n            <span key={id + '-label'} data-id={id}>\n              {label}\n            </span>\n            <button key={id + '-button'} data-id={id + '-btn'}>\n              click\n            </button>\n          </>\n        )\n      }\n\n      root.render(\n        <div>\n          <Item id=\"a\" label=\"A\" />\n          <Item id=\"b\" label=\"B\" />\n        </div>,\n      )\n\n      expect(container.textContent).toBe('AclickBclick')\n\n      // Swap order of items – inner keys should not cause errors and\n      // both items should still render correctly.\n      root.render(\n        <div>\n          <Item id=\"b\" label=\"B\" />\n          <Item id=\"a\" label=\"A\" />\n        </div>,\n      )\n\n      // Current implementation keeps the DOM order of the two fragment\n      // sections stable for unkeyed components, but updates their\n      // content based on props.\n      expect(\n        container.textContent === 'BclickAclick' || container.textContent === 'AclickBclick',\n      ).toBe(true)\n\n      let labels = Array.from(container.querySelectorAll('span'))\n      let buttons = Array.from(container.querySelectorAll('button'))\n      expect(labels.length).toBe(2)\n      expect(buttons.length).toBe(2)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.mixins.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { createMixin, on, ref } from '../index.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport type { Handle } from '../lib/component.ts'\nimport type { Props } from '../index.ts'\n\ndescribe('vnode mixins', () => {\n  it('composes mixins in order and does not leak mix to the DOM', () => {\n    let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (\n      <handle.element {...props} title={title} />\n    ))\n    let appendTitle = createMixin((handle) => (suffix: string, props: { title?: string }) => (\n      <handle.element {...props} title={`${props.title ?? ''}${suffix}`} />\n    ))\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div mix={[withTitle('hello'), appendTitle('-world')]} />)\n\n    let div = container.querySelector('div')\n    invariant(div)\n    expect(div.getAttribute('title')).toBe('hello-world')\n    expect(div.hasAttribute('mix')).toBe(false)\n  })\n\n  it('supports nested mix descriptors via handle.element', () => {\n    let withData = createMixin((handle) => (value: string, props: { ['data-mixed']?: string }) => (\n      <handle.element {...props} data-mixed={value} />\n    ))\n    let withNested = createMixin(\n      (handle) => (value: string, props: { ['data-mixed']?: string }) => (\n        <handle.element {...props} mix={[withData(value)]} />\n      ),\n    )\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div mix={[withNested('nested')]} />)\n\n    let div = container.querySelector('div')\n    invariant(div)\n    expect(div.getAttribute('data-mixed')).toBe('nested')\n  })\n\n  it('normalizes component mix props so wrapped hosts can compose them', () => {\n    let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (\n      <handle.element {...props} title={title} />\n    ))\n    let appendTitle = createMixin((handle) => (suffix: string, props: { title?: string }) => (\n      <handle.element {...props} title={`${props.title ?? ''}${suffix}`} />\n    ))\n\n    function Button() {\n      return ({ children, mix, ...props }: Props<'button'>) => (\n        <button {...props} mix={[withTitle('base'), ...(mix ?? [])]}>\n          {children}\n        </button>\n      )\n    }\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<Button mix={appendTitle('-override')}>Click</Button>)\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    expect(button.getAttribute('title')).toBe('base-override')\n    expect(button.hasAttribute('mix')).toBe(false)\n  })\n\n  it('shares one handle instance across mixins on the same host node', () => {\n    let handles: unknown[] = []\n    let one = createMixin((handle) => {\n      handles.push(handle)\n    })\n    let two = createMixin((handle) => {\n      handles.push(handle)\n    })\n    let three = createMixin((handle) => {\n      handles.push(handle)\n    })\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div mix={[one(), two(), three()]} />)\n    root.flush()\n\n    expect(handles.length).toBe(3)\n    expect(handles[0]).toBe(handles[1])\n    expect(handles[1]).toBe(handles[2])\n  })\n\n  it('aborts handle.signal when the host node is removed', () => {\n    let signal = AbortSignal.abort()\n    let withSignal = createMixin((handle) => {\n      signal = handle.signal\n    })\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div mix={[withSignal()]} />)\n    root.flush()\n    expect(signal.aborted).toBe(false)\n\n    root.render(null)\n    root.flush()\n    expect(signal.aborted).toBe(true)\n  })\n\n  it('supports setup-only passthrough mixins', () => {\n    let withPassthrough = createMixin((_handle) => {})\n    let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (\n      <handle.element {...props} title={title} />\n    ))\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div mix={[withPassthrough(), withTitle('ok')]} />)\n    root.flush()\n\n    let div = container.querySelector('div')\n    invariant(div)\n    expect(div.getAttribute('title')).toBe('ok')\n  })\n\n  it('does not duplicate on handlers for passthrough mixins', () => {\n    let clicks = 0\n    let passthrough = createMixin((_handle) => {})\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(\n      <button\n        mix={[\n          passthrough(),\n          on('click', () => {\n            clicks++\n          }),\n        ]}\n      />,\n    )\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.dispatchEvent(new MouseEvent('click', { bubbles: true }))\n    root.flush()\n\n    expect(clicks).toBe(1)\n  })\n\n  it('runs remove lifecycle when descriptor type changes and on unmount', () => {\n    let removedA = 0\n    let removedB = 0\n    let persistApiSeenOnRemoveA = false\n    let persistApiSeenOnRemoveB = false\n\n    let a = createMixin((handle) => {\n      handle.addEventListener('remove', (event) => {\n        removedA++\n        persistApiSeenOnRemoveA = 'persistNode' in event\n      })\n      return (props: { id?: string }) => <handle.element {...props} id=\"a\" />\n    })\n\n    let b = createMixin((handle) => {\n      handle.addEventListener('remove', (event) => {\n        removedB++\n        persistApiSeenOnRemoveB = 'persistNode' in event\n      })\n      return (props: { id?: string }) => <handle.element {...props} id=\"b\" />\n    })\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(<div mix={[a()]} />)\n    root.render(<div mix={[b()]} />)\n    root.render(null)\n\n    expect(removedA).toBe(1)\n    expect(removedB).toBe(1)\n    expect(persistApiSeenOnRemoveA).toBe(false)\n    expect(persistApiSeenOnRemoveB).toBe(false)\n  })\n\n  it('exposes persistNode in beforeRemove lifecycle', () => {\n    let beforeRemoveCalls = 0\n    let persistApiSeen = false\n\n    let withBeforeRemove = createMixin((handle) => {\n      handle.addEventListener('beforeRemove', (event) => {\n        beforeRemoveCalls++\n        persistApiSeen = typeof event.persistNode === 'function'\n      })\n      return (props: { id?: string }) => <handle.element {...props} id=\"before-remove\" />\n    })\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div mix={[withBeforeRemove()]} />)\n    root.flush()\n    root.render(null)\n    root.flush()\n\n    expect(beforeRemoveCalls).toBe(1)\n    expect(persistApiSeen).toBe(true)\n  })\n\n  it('runs insert lifecycle with the bound host node', () => {\n    let insertedNode: Element | null = null\n    let insertCount = 0\n\n    let withInsert = createMixin((handle) => {\n      handle.addEventListener('insert', (event) => {\n        insertedNode = event.node\n        insertCount++\n      })\n      return (props: { id?: string }) => <handle.element {...props} id=\"inserted\" />\n    })\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div mix={[withInsert()]} />)\n    root.flush()\n    root.render(<div mix={[withInsert()]} />)\n    root.flush()\n\n    let div = container.querySelector('#inserted')\n    invariant(div)\n    expect(insertedNode).toBe(div)\n    expect(insertCount).toBe(1)\n  })\n\n  it('runs beforeUpdate and commit lifecycle events in update order', () => {\n    let calls: string[] = []\n    let withUpdateLifecycle = createMixin((handle) => {\n      handle.addEventListener('beforeUpdate', (event) => {\n        calls.push(`before:${(event.node as HTMLElement).dataset.step}`)\n      })\n      handle.addEventListener('commit', (event) => {\n        calls.push(`commit:${(event.node as HTMLElement).dataset.step}`)\n      })\n      return (step: string, props: { ['data-step']?: string }) => (\n        <handle.element {...props} data-step={step} />\n      )\n    })\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div mix={[withUpdateLifecycle('0')]} />)\n    root.flush()\n    root.render(<div mix={[withUpdateLifecycle('1')]} />)\n    root.flush()\n\n    expect(calls).toEqual(['before:0', 'commit:1'])\n  })\n\n  it('composes ref callbacks across mixins and base mix', () => {\n    let calls: string[] = []\n\n    let withConnectA = createMixin((handle) => (props: {}) => (\n      <handle.element\n        {...props}\n        mix={[\n          ref((node: Element) => {\n            calls.push('a')\n            if (node instanceof HTMLElement) {\n              node.dataset.a = '1'\n            }\n          }),\n        ]}\n      />\n    ))\n\n    let withConnectB = createMixin((handle) => (props: {}) => (\n      <handle.element\n        {...props}\n        mix={[\n          ref((node: Element) => {\n            calls.push('b')\n            if (node instanceof HTMLElement) {\n              node.dataset.b = '1'\n            }\n          }),\n        ]}\n      />\n    ))\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(\n      <div\n        mix={[\n          withConnectA(),\n          withConnectB(),\n          ref((node: Element) => {\n            calls.push('base')\n            if (node instanceof HTMLElement) {\n              node.dataset.base = '1'\n            }\n          }),\n        ]}\n      />,\n    )\n    root.flush()\n\n    let div = container.querySelector('div')\n    invariant(div)\n    expect(div.dataset.a).toBe('1')\n    expect(div.dataset.b).toBe('1')\n    expect(div.dataset.base).toBe('1')\n    expect(new Set(calls)).toEqual(new Set(['a', 'b', 'base']))\n  })\n\n  it('composes on mixins across nested mixins', () => {\n    let calls: string[] = []\n\n    let withOnA = createMixin<HTMLElement>((handle) => (props: {}) => (\n      <handle.element\n        {...props}\n        mix={[\n          on('click', () => {\n            calls.push('a')\n          }),\n        ]}\n      />\n    ))\n\n    let withOnB = createMixin<HTMLElement>((handle) => (props: {}) => (\n      <handle.element\n        {...props}\n        mix={[\n          on('click', () => {\n            calls.push('b')\n          }),\n        ]}\n      />\n    ))\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(\n      <button\n        mix={[\n          withOnA(),\n          withOnB(),\n          on('click', () => {\n            calls.push('base')\n          }),\n        ]}\n      >\n        click\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.click()\n    root.flush()\n    expect(calls).toEqual(['base', 'a', 'b'])\n  })\n\n  it('supports on mixin helper composition standalone', () => {\n    let calls: string[] = []\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    root.render(\n      <button\n        mix={[\n          on('click', () => {\n            calls.push('first')\n          }),\n          on('click', () => {\n            calls.push('second')\n          }),\n        ]}\n      >\n        click\n      </button>,\n    )\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    button.click()\n    root.flush()\n    expect(calls).toEqual(['first', 'second'])\n  })\n\n  it('updates only host props when mixin calls handle.update', () => {\n    let appRenderCount = 0\n\n    let withCounter = createMixin<HTMLButtonElement>((handle) => {\n      let count = 0\n      return (props: { ['data-count']?: string }) => (\n        <handle.element\n          {...props}\n          data-count={String(count)}\n          mix={[\n            on('click', () => {\n              count++\n              handle.update()\n            }),\n          ]}\n        />\n      )\n    })\n\n    function App(_handle: Handle) {\n      appRenderCount++\n      return () => <button mix={[withCounter()]}>click</button>\n    }\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<App />)\n    root.flush()\n\n    let button = container.querySelector('button')\n    invariant(button)\n    expect(button.getAttribute('data-count')).toBe('0')\n    expect(appRenderCount).toBe(1)\n\n    button.click()\n    root.flush()\n\n    expect(button.getAttribute('data-count')).toBe('1')\n    expect(appRenderCount).toBe(1)\n  })\n\n  it('dispatches reclaimed on persisted reuse without rerunning insert or remove', async () => {\n    let insertCalls = 0\n    let reclaimedCalls = 0\n    let removeCalls = 0\n    let beforeRemoveCalls = 0\n    let resolvePending: (() => void) | null = null\n\n    let withReclaimLifecycle = createMixin((handle) => {\n      handle.addEventListener('insert', () => {\n        insertCalls++\n      })\n      handle.addEventListener('reclaimed', () => {\n        reclaimedCalls++\n      })\n      handle.addEventListener('beforeRemove', (event) => {\n        beforeRemoveCalls++\n        event.persistNode(\n          (signal) =>\n            new Promise<void>((resolve) => {\n              let done = () => resolve()\n              resolvePending = done\n              signal.addEventListener('abort', done, { once: true })\n            }),\n        )\n      })\n      handle.addEventListener('remove', () => {\n        removeCalls++\n      })\n      return (props: { id?: string }) => <handle.element {...props} id=\"reclaimed-target\" />\n    })\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div key=\"reclaimed\" mix={[withReclaimLifecycle()]} />)\n    root.flush()\n    expect(insertCalls).toBe(1)\n    expect(reclaimedCalls).toBe(0)\n    expect(removeCalls).toBe(0)\n\n    root.render(null)\n    root.flush()\n    await Promise.resolve()\n    expect(beforeRemoveCalls).toBe(1)\n    expect(removeCalls).toBe(0)\n\n    root.render(<div key=\"reclaimed\" mix={[withReclaimLifecycle()]} />)\n    root.flush()\n    await Promise.resolve()\n\n    expect(insertCalls).toBe(1)\n    expect(reclaimedCalls).toBe(1)\n    expect(removeCalls).toBe(0)\n\n    if (resolvePending !== null) {\n      ;(resolvePending as () => void)()\n    }\n  })\n\n  it('defers host removal when beforeRemove.persistNode is used', async () => {\n    let releaseRemoval: (() => void) | null = null\n    let beforeRemoveCalls = 0\n    let removeCalls = 0\n    let withDeferredRemove = createMixin((handle) => {\n      handle.addEventListener('beforeRemove', (event) => {\n        beforeRemoveCalls++\n        event.persistNode(\n          () =>\n            new Promise<void>((resolve) => {\n              releaseRemoval = () => resolve()\n            }),\n        )\n      })\n      handle.addEventListener('remove', () => {\n        removeCalls++\n      })\n      return (props: { id?: string }) => <handle.element {...props} id=\"deferred-remove\" />\n    })\n\n    let container = document.createElement('div')\n    let root = createRoot(container)\n    root.render(<div key=\"deferred\" mix={[withDeferredRemove()]} />)\n    root.flush()\n\n    let beforeRemove = container.querySelector('#deferred-remove')\n    invariant(beforeRemove)\n\n    root.render(null)\n    root.flush()\n    await Promise.resolve()\n    expect(beforeRemoveCalls).toBe(1)\n    expect(removeCalls).toBe(0)\n    expect(container.querySelector('#deferred-remove')).toBe(beforeRemove)\n\n    let release =\n      releaseRemoval ??\n      (() => {\n        throw new Error('expected deferred remove callback')\n      })\n    release()\n    await new Promise((resolve) => setTimeout(resolve, 0))\n    expect(removeCalls).toBe(1)\n    expect(container.querySelector('#deferred-remove')).toBe(null)\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.props.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport { css } from '../index.ts'\n\ndescribe('vnode rendering', () => {\n  describe('special attributes', () => {\n    it.todo('className')\n    it.todo('htmlFor')\n    it.todo('acceptCharset')\n    it.todo('httpEquiv')\n    it.todo('xlinkHref')\n    it.todo('xmlLang')\n    it.todo('xmlSpace')\n    it.todo('data-*')\n    it.todo('aria-*')\n  })\n\n  describe('special props', () => {\n    it.todo('style')\n    it.todo('value')\n    it.todo('defaultValue')\n    it.todo('checked')\n    it.todo('defaultChecked')\n    it.todo('disabled')\n  })\n\n  describe('framework props', () => {\n    it.todo('does not render key')\n    it.todo('does not render on')\n    it.todo('does not render mix')\n    it.todo('does not render children')\n    it.todo('does not render tabIndex')\n    it.todo('does not render acceptCharset')\n  })\n\n  describe('innerHTML prop', () => {\n    it('sets innerHTML on element', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(<div innerHTML=\"<span>Hello</span>\" />)\n      expect(container.innerHTML).toBe('<div><span>Hello</span></div>')\n    })\n\n    it('ignores children when innerHTML is set', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <div innerHTML=\"<span>From innerHTML</span>\">\n          <p>Ignored child</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><span>From innerHTML</span></div>')\n    })\n\n    it('updates innerHTML on re-render', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(<div innerHTML=\"<span>First</span>\" />)\n      expect(container.innerHTML).toBe('<div><span>First</span></div>')\n\n      let div = container.querySelector('div')\n      invariant(div)\n\n      root.render(<div innerHTML=\"<span>Second</span>\" />)\n      expect(container.innerHTML).toBe('<div><span>Second</span></div>')\n      expect(container.querySelector('div')).toBe(div)\n    })\n\n    it('clears innerHTML when removed', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(<div innerHTML=\"<span>Hello</span>\" />)\n      expect(container.innerHTML).toBe('<div><span>Hello</span></div>')\n\n      root.render(<div />)\n      expect(container.innerHTML).toBe('<div></div>')\n    })\n\n    it('switches from innerHTML to children', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(<div innerHTML=\"<span>From innerHTML</span>\" />)\n      expect(container.innerHTML).toBe('<div><span>From innerHTML</span></div>')\n\n      root.render(\n        <div>\n          <p>From children</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><p>From children</p></div>')\n    })\n\n    it('switches from children to innerHTML', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <p>From children</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><p>From children</p></div>')\n\n      root.render(<div innerHTML=\"<span>From innerHTML</span>\" />)\n      expect(container.innerHTML).toBe('<div><span>From innerHTML</span></div>')\n    })\n\n    it('switches from text children to innerHTML without throwing', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      let renderError: unknown\n      root.addEventListener('error', (event) => {\n        renderError = (event as ErrorEvent).error\n      })\n\n      root.render(<div>From text child</div>)\n      expect(container.innerHTML).toBe('<div>From text child</div>')\n\n      root.render(<div innerHTML=\"<span>From innerHTML</span>\" />)\n      expect(container.innerHTML).toBe('<div><span>From innerHTML</span></div>')\n      expect(renderError).toBeUndefined()\n    })\n  })\n\n  describe('css mixin', () => {\n    it('adds className-based styles', async () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(<div mix={[css({ color: 'rgb(255, 0, 0)' })]}>Hello</div>)\n      let div = container.querySelector('div')\n      invariant(div instanceof HTMLDivElement)\n      expect(div.className).toMatch(/rmxc-/)\n      document.body.appendChild(container)\n      expect(getComputedStyle(div).color).toBe('rgb(255, 0, 0)')\n    })\n\n    it('composes with className without overriding it', async () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <div mix={[css({ color: 'rgb(255, 0, 0)' })]} className=\"custom-class\">\n          Hello\n        </div>,\n      )\n      let div = container.querySelector('div')\n      invariant(div instanceof HTMLDivElement)\n      expect(div.className).toContain('custom-class')\n      expect(div.className).toMatch(/rmxc-/)\n    })\n\n    it('ignores class when composing css mixin className', async () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <div mix={[css({ color: 'rgb(0, 255, 0)' })]} class=\"another-class\">\n          Hello\n        </div>,\n      )\n      let div = container.querySelector('div')\n      invariant(div instanceof HTMLDivElement)\n      expect(div.className).toMatch(/rmxc-/)\n    })\n\n    it('className updates independently of css mixin output', async () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <div mix={[css({ color: 'rgb(255, 0, 0)' })]} className=\"first\">\n          Hello\n        </div>,\n      )\n      let div = container.querySelector('div')\n      invariant(div instanceof HTMLDivElement)\n      expect(div.className).toContain('first')\n      let generated = div.className.split(/\\s+/).find((token) => token.startsWith('rmxc-'))\n      invariant(generated)\n\n      root.render(\n        <div mix={[css({ color: 'rgb(255, 0, 0)' })]} className=\"second\">\n          Hello\n        </div>,\n      )\n      expect(div.className).toContain('second')\n      expect(div.className).toContain(generated)\n    })\n\n    it('removes nested selector rules when they become undefined', async () => {\n      let container = document.createElement('div')\n      document.body.appendChild(container)\n      let root = createRoot(container)\n\n      root.render(\n        <div\n          mix={[\n            css({\n              // Base styling for the child comes from the parent.\n              '& span': { color: 'rgb(0, 0, 255)' },\n              // More-specific nested selector is conditionally removed.\n              '& span.special': { color: 'rgb(255, 0, 0)' },\n            }),\n          ]}\n        >\n          <span className=\"special\">Test</span>\n        </div>,\n      )\n\n      let child = container.querySelector('span')\n      invariant(child)\n\n      // More-specific nested selector should win.\n      expect(getComputedStyle(child).color).toBe('rgb(255, 0, 0)')\n\n      root.render(\n        <div\n          mix={[\n            css({\n              '& span': { color: 'rgb(0, 0, 255)' },\n              '& span.special': undefined,\n            }),\n          ]}\n        >\n          <span className=\"special\">Test</span>\n        </div>,\n      )\n\n      // Once the more-specific selector becomes undefined, the child should fall back to the base rule.\n      expect(getComputedStyle(child).color).toBe('rgb(0, 0, 255)')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.range-root.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport type { Handle } from '../lib/component.ts'\nimport { createRangeRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport { on } from '../index.ts'\n\ndescribe('createRangeRoot', () => {\n  describe('event forwarding', () => {\n    it('forwards bubbling DOM error events to range root listeners', () => {\n      let host = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      host.append(start, end)\n\n      let root = createRangeRoot([start, end])\n      let forwarded: unknown\n      root.addEventListener('error', (event) => {\n        forwarded = (event as ErrorEvent).error\n      })\n\n      let expected = new Error('createRangeRoot forwarded error')\n      host.dispatchEvent(new ErrorEvent('error', { bubbles: true, error: expected }))\n\n      expect(forwarded).toBe(expected)\n    })\n\n    it('stops forwarding bubbling DOM error events after dispose', () => {\n      let host = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      host.append(start, end)\n\n      let root = createRangeRoot([start, end])\n      let forwarded: unknown\n      root.addEventListener('error', (event) => {\n        forwarded = (event as ErrorEvent).error\n      })\n\n      root.dispose()\n\n      host.dispatchEvent(\n        new ErrorEvent('error', { bubbles: true, error: new Error('after dispose') }),\n      )\n\n      expect(forwarded).toBeUndefined()\n    })\n  })\n\n  describe('basic rendering', () => {\n    it('dispose is a no-op before first render', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.dispose()\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><!--end-->')\n    })\n\n    it('renders content between markers', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render(<div>Hello</div>)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div>Hello</div><!--end-->')\n    })\n\n    it('renders text between markers', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render('Hello, world!')\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start-->Hello, world!<!--end-->')\n    })\n\n    it('renders fragments between markers', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render(\n        <>\n          <p>First</p>\n          <p>Second</p>\n        </>,\n      )\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><p>First</p><p>Second</p><!--end-->')\n    })\n\n    it('renders components between markers', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      function Greeting() {\n        return () => <span>Hello!</span>\n      }\n\n      let root = createRangeRoot([start, end])\n      root.render(<Greeting />)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><span>Hello!</span><!--end-->')\n    })\n  })\n\n  describe('updates', () => {\n    it('updates content between markers', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render(<div>First</div>)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div>First</div><!--end-->')\n\n      root.render(<div>Second</div>)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div>Second</div><!--end-->')\n    })\n\n    it('handles adding children', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render(<div />)\n      root.flush()\n\n      root.render(\n        <div>\n          <span>Added</span>\n        </div>,\n      )\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div><span>Added</span></div><!--end-->')\n    })\n\n    it('handles removing children', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render(\n        <div>\n          <span>To remove</span>\n        </div>,\n      )\n      root.flush()\n\n      root.render(<div />)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div></div><!--end-->')\n    })\n\n    it('replaces all fragment children with different elements', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render(\n        <>\n          <div>First</div>\n          <div>Second</div>\n        </>,\n      )\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div>First</div><div>Second</div><!--end-->')\n\n      // Replace divs with spans\n      root.render(\n        <>\n          <span>A</span>\n          <span>B</span>\n        </>,\n      )\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><span>A</span><span>B</span><!--end-->')\n    })\n\n    it('renders null then content', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render(null)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><!--end-->')\n\n      root.render(<div>Now visible</div>)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div>Now visible</div><!--end-->')\n    })\n\n    it('renders content then null then content', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render(<div>Visible</div>)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div>Visible</div><!--end-->')\n\n      root.render(null)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><!--end-->')\n\n      root.render(<span>Back again</span>)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><span>Back again</span><!--end-->')\n    })\n\n    it('changes fragment child count', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let root = createRangeRoot([start, end])\n      root.render(\n        <>\n          <div>One</div>\n          <div>Two</div>\n          <div>Three</div>\n        </>,\n      )\n      root.flush()\n\n      expect(container.innerHTML).toBe(\n        '<!--start--><div>One</div><div>Two</div><div>Three</div><!--end-->',\n      )\n\n      // Reduce to one child\n      root.render(<div>Only</div>)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div>Only</div><!--end-->')\n\n      // Back to multiple\n      root.render(\n        <>\n          <span>A</span>\n          <span>B</span>\n        </>,\n      )\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><span>A</span><span>B</span><!--end-->')\n    })\n  })\n\n  describe('events', () => {\n    it('attaches event handlers', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      let clicked = false\n      let root = createRangeRoot([start, end])\n      root.render(\n        <button\n          mix={[\n            on('click', () => {\n              clicked = true\n            }),\n          ]}\n        >\n          Click me\n        </button>,\n      )\n      root.flush()\n\n      let button = container.querySelector('button')\n      invariant(button)\n      button.click()\n\n      expect(clicked).toBe(true)\n    })\n  })\n\n  describe('multiple range roots', () => {\n    it('supports multiple ranges in same container', () => {\n      let container = document.createElement('div')\n      container.innerHTML =\n        '<!--a--><div>A</div><!--/a--><p>static</p><!--b--><div>B</div><!--/b-->'\n\n      let startA = container.childNodes[0] as Comment\n      let endA = container.childNodes[2] as Comment\n      let startB = container.childNodes[4] as Comment\n      let endB = container.childNodes[6] as Comment\n\n      let existingDivA = container.querySelector('div')\n      let existingDivB = container.querySelectorAll('div')[1]\n\n      let rootA = createRangeRoot([startA, endA])\n      let rootB = createRangeRoot([startB, endB])\n\n      rootA.render(<div>A updated</div>)\n      rootB.render(<div>B updated</div>)\n      rootA.flush()\n      rootB.flush()\n\n      // Content between markers updated, static content unchanged\n      expect(container.innerHTML).toBe(\n        '<!--a--><div>A updated</div><!--/a--><p>static</p><!--b--><div>B updated</div><!--/b-->',\n      )\n\n      // Original nodes reused\n      expect(container.querySelector('div')).toBe(existingDivA)\n      expect(container.querySelectorAll('div')[1]).toBe(existingDivB)\n    })\n\n    it('ranges are independent', () => {\n      let container = document.createElement('div')\n      let startA = document.createComment('a')\n      let endA = document.createComment('/a')\n      let startB = document.createComment('b')\n      let endB = document.createComment('/b')\n\n      container.appendChild(startA)\n      container.appendChild(endA)\n      container.appendChild(startB)\n      container.appendChild(endB)\n\n      let rootA = createRangeRoot([startA, endA])\n      let rootB = createRangeRoot([startB, endB])\n\n      rootA.render(<span>A</span>)\n      rootA.flush()\n\n      // Only A has content, B is empty\n      expect(container.innerHTML).toBe('<!--a--><span>A</span><!--/a--><!--b--><!--/b-->')\n\n      rootB.render(<span>B</span>)\n      rootB.flush()\n\n      expect(container.innerHTML).toBe(\n        '<!--a--><span>A</span><!--/a--><!--b--><span>B</span><!--/b-->',\n      )\n    })\n  })\n\n  describe('boundary handling', () => {\n    it('throws when start and end markers do not share a parent node', () => {\n      let containerA = document.createElement('div')\n      let containerB = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      containerA.appendChild(start)\n      containerB.appendChild(end)\n\n      expect(() => createRangeRoot([start, end])).toThrow('Boundaries must share parent')\n    })\n\n    it('does not affect content before start marker', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<header>Before</header><!--start--><!--end-->'\n\n      let start = container.childNodes[1] as Comment\n      let end = container.childNodes[2] as Comment\n\n      let root = createRangeRoot([start, end])\n      root.render(<div>Inside</div>)\n      root.flush()\n\n      expect(container.innerHTML).toBe(\n        '<header>Before</header><!--start--><div>Inside</div><!--end-->',\n      )\n    })\n\n    it('does not affect content after end marker', () => {\n      let container = document.createElement('div')\n      // Range has existing content to hydrate\n      container.innerHTML = '<!--start--><div>Old</div><!--end--><footer>After</footer>'\n\n      let start = container.childNodes[0] as Comment\n      let end = container.childNodes[2] as Comment\n\n      let root = createRangeRoot([start, end])\n      root.render(<div>New</div>)\n      root.flush()\n\n      expect(container.innerHTML).toBe('<!--start--><div>New</div><!--end--><footer>After</footer>')\n    })\n\n    it('handles empty ranges with content after end marker', () => {\n      let container = document.createElement('div')\n      // Empty range with content following it\n      container.innerHTML = '<!--start--><!--end--><footer>After</footer>'\n\n      let start = container.firstChild as Comment\n      let end = container.childNodes[1] as Comment\n      let footer = container.querySelector('footer')\n      invariant(footer)\n\n      let root = createRangeRoot([start, end])\n      root.render(<div>New content</div>)\n      root.flush()\n\n      // Content should be inserted inside the range, not adopt the footer\n      expect(container.innerHTML).toBe(\n        '<!--start--><div>New content</div><!--end--><footer>After</footer>',\n      )\n      // Footer should be unchanged\n      expect(container.querySelector('footer')).toBe(footer)\n    })\n\n    it('preserves surrounding content during updates', () => {\n      let container = document.createElement('div')\n      container.innerHTML = '<header>H</header><!--start--><p>Old</p><!--end--><footer>F</footer>'\n\n      let start = container.childNodes[1] as Comment\n      let end = container.childNodes[3] as Comment\n\n      let root = createRangeRoot([start, end])\n      root.render(<p>New</p>)\n      root.flush()\n\n      expect(container.innerHTML).toBe(\n        '<header>H</header><!--start--><p>New</p><!--end--><footer>F</footer>',\n      )\n\n      // Multiple updates shouldn't affect boundaries\n      root.render(<p>Updated again</p>)\n      root.flush()\n\n      expect(container.innerHTML).toBe(\n        '<header>H</header><!--start--><p>Updated again</p><!--end--><footer>F</footer>',\n      )\n    })\n  })\n\n  describe('stateful components', () => {\n    it('maintains component state across renders', () => {\n      let container = document.createElement('div')\n      let start = document.createComment('start')\n      let end = document.createComment('end')\n      container.appendChild(start)\n      container.appendChild(end)\n\n      function Counter(handle: Handle, setup: number) {\n        let count = setup\n        return () => (\n          <button\n            mix={[\n              on('click', () => {\n                count++\n                handle.update()\n              }),\n            ]}\n          >\n            {count}\n          </button>\n        )\n      }\n\n      let root = createRangeRoot([start, end])\n      root.render(<Counter setup={0} />)\n      root.flush()\n\n      let button = container.querySelector('button')\n      invariant(button)\n      expect(button.textContent).toBe('0')\n\n      button.click()\n      root.flush()\n\n      expect(button.textContent).toBe('1')\n\n      button.click()\n      root.flush()\n\n      expect(button.textContent).toBe('2')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.replacements.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport type { Handle } from '../lib/component.ts'\n\ndescribe('vnode rendering', () => {\n  describe('type<-->type updates', () => {\n    it('updates a text node', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render('Hello, world!')\n      expect(container.innerHTML).toBe('Hello, world!')\n      render('Hello, world! 2')\n      expect(container.innerHTML).toBe('Hello, world! 2')\n    })\n\n    it('updates an element', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(<div>Hello, world!</div>)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n\n      let div = container.querySelector('div')\n      render(<div>Hello, world! 2</div>)\n      expect(container.innerHTML).toBe('<div>Hello, world! 2</div>')\n      expect(container.querySelector('div')).toBe(div)\n    })\n\n    it('updates an element with attributes', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(<input id=\"hello\" value=\"world\" />)\n      let input = container.querySelector('input')\n      invariant(input)\n      expect(input.getAttribute('id')).toBe('hello')\n      expect(input.value).toBe('world')\n\n      render(<input id=\"hello\" value=\"world 2\" />)\n      expect(container.querySelector('input')).toBe(input)\n      expect(input.getAttribute('id')).toBe('hello')\n      expect(input.value).toBe('world 2')\n    })\n\n    it('updates a fragment', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n      render(\n        <>\n          <p>Hello</p>\n          <p>world!</p>\n        </>,\n      )\n      let pTags = container.querySelectorAll('p')\n      invariant(pTags.length === 2)\n\n      expect(container.innerHTML).toBe('<p>Hello</p><p>world!</p>')\n      render(\n        <>\n          <p>Goodbye</p>\n          <p>Universe</p>\n        </>,\n      )\n      expect(container.innerHTML).toBe('<p>Goodbye</p><p>Universe</p>')\n      let newPTags = container.querySelectorAll('p')\n      expect(newPTags.length).toBe(2)\n      expect(newPTags[0]).toBe(pTags[0])\n      expect(newPTags[1]).toBe(pTags[1])\n    })\n\n    it('updates a component', () => {\n      let container = document.createElement('div')\n\n      let setupCalls = 0\n      function App(handle: Handle) {\n        let state = ++setupCalls\n        return ({ title }: { title: string }) => (\n          <div>\n            {title} {state}\n          </div>\n        )\n      }\n\n      let root = createRoot(container)\n      root.render(<App title=\"Hello\" />)\n      expect(container.innerHTML).toBe('<div>Hello 1</div>')\n      root.render(<App title=\"Goodbye\" />)\n      expect(container.innerHTML).toBe('<div>Goodbye 1</div>')\n    })\n\n    it('updates a component with a fragment', () => {\n      let container = document.createElement('div')\n\n      let setupCalls = 0\n      function App(handle: Handle) {\n        let state = ++setupCalls\n        return ({ title }: { title: string }) => (\n          <>\n            <span>{title}</span>\n            <span>{state}</span>\n          </>\n        )\n      }\n\n      let root = createRoot(container)\n      root.render(<App title=\"Hello\" />)\n      expect(container.innerHTML).toBe('<span>Hello</span><span>1</span>')\n\n      root.render(<App title=\"Goodbye\" />)\n      expect(container.innerHTML).toBe('<span>Goodbye</span><span>1</span>')\n    })\n  })\n\n  describe('simple replacement', () => {\n    it('replaces element -> text', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(<div>Hello, world!</div>)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      root.render('Goodbye, element!')\n      expect(container.innerHTML).toBe('Goodbye, element!')\n    })\n\n    it('replaces text -> element', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render('Hello, world!')\n      expect(container.innerHTML).toBe('Hello, world!')\n      root.render(<div>Goodbye, world!</div>)\n      expect(container.innerHTML).toBe('<div>Goodbye, world!</div>')\n    })\n\n    it('replaces element -> component', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(<div>Hello, world!</div>)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      function App() {\n        return () => <div>Goodbye, world!</div>\n      }\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<div>Goodbye, world!</div>')\n    })\n\n    it('replaces component -> element', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      function App() {\n        return () => <div>Hello, world!</div>\n      }\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      root.render(<div>Goodbye, world!</div>)\n      expect(container.innerHTML).toBe('<div>Goodbye, world!</div>')\n    })\n\n    it('replaces element -> element', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(<div>Hello, world!</div>)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      root.render(<nav>Goodbye, world!</nav>)\n      expect(container.innerHTML).toBe('<nav>Goodbye, world!</nav>')\n    })\n\n    it('replaces component -> component', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      function App() {\n        return () => <div>Hello, world!</div>\n      }\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      function App2() {\n        return () => <div>Goodbye, world!</div>\n      }\n      root.render(<App2 />)\n      expect(container.innerHTML).toBe('<div>Goodbye, world!</div>')\n    })\n\n    it('replaces component -> fragment', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      function App() {\n        return () => <div>Hello, world!</div>\n      }\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      root.render(\n        <>\n          <p>Goodbye</p>\n          <p>world!</p>\n        </>,\n      )\n      expect(container.innerHTML).toBe('<p>Goodbye</p><p>world!</p>')\n    })\n\n    it('replaces fragment -> component', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <>\n          <div>Hello, world!</div>\n        </>,\n      )\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      function App() {\n        return () => <div>Goodbye, world!</div>\n      }\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<div>Goodbye, world!</div>')\n    })\n\n    it('replaces fragment -> element', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <>\n          <div>Hello, world!</div>\n        </>,\n      )\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      root.render(<div>Goodbye, world!</div>)\n      expect(container.innerHTML).toBe('<div>Goodbye, world!</div>')\n    })\n\n    it('replaces fragment -> text', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <>\n          <div>Hello, world!</div>\n        </>,\n      )\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      root.render('Goodbye, world!')\n      expect(container.innerHTML).toBe('Goodbye, world!')\n    })\n\n    it('replaces text -> component', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render('Hello, world!')\n      expect(container.innerHTML).toBe('Hello, world!')\n      function App() {\n        return () => <div>Goodbye, world!</div>\n      }\n      root.render(<App />)\n      expect(container.innerHTML).toBe('<div>Goodbye, world!</div>')\n    })\n  })\n\n  describe('complex replacements', () => {\n    it('preserves siblings', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <div>div</div>\n          <span>span</span>\n          <p>p</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><div>div</div><span>span</span><p>p</p></div>')\n\n      let div = container.querySelector('div')\n      let p = container.querySelector('p')\n      invariant(div && p)\n      root.render(\n        <div>\n          <div>div</div>\n          <nav>nav</nav>\n          <p>p</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><div>div</div><nav>nav</nav><p>p</p></div>')\n      expect(container.querySelector('div')).toBe(div)\n      expect(container.querySelector('p')).toBe(p)\n    })\n\n    it('replaces null children', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      root.render(\n        <div>\n          <div>div</div>\n          {null}\n          <p>p</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><div>div</div><p>p</p></div>')\n      let div = container.querySelector('div')\n      let p = container.querySelector('p')\n      invariant(div && p)\n\n      root.render(\n        <div>\n          <div>div</div>\n          <span>span</span>\n          <p>p</p>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><div>div</div><span>span</span><p>p</p></div>')\n      expect(container.querySelector('div')).toBe(div)\n      expect(container.querySelector('p')).toBe(p)\n    })\n\n    it('replaces fragment components', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      function Frag() {\n        return () => (\n          <>\n            <span>A</span>\n            <span>B</span>\n          </>\n        )\n      }\n      root.render(\n        <div>\n          <Frag />\n          <main>main</main>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><span>A</span><span>B</span><main>main</main></div>')\n      let main = container.querySelector('main')\n      invariant(main)\n\n      root.render(\n        <div>\n          <div>one</div>\n          <main>main</main>\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><div>one</div><main>main</main></div>')\n      expect(container.querySelector('main')).toBe(main)\n    })\n\n    it('replaces components within elements', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n      function App() {\n        return () => <div>Hello, world!</div>\n      }\n      root.render(\n        <div>\n          <App />\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><div>Hello, world!</div></div>')\n\n      function App2() {\n        return () => <div>Goodbye, world!</div>\n      }\n      root.render(\n        <div>\n          <App2 />\n        </div>,\n      )\n      expect(container.innerHTML).toBe('<div><div>Goodbye, world!</div></div>')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.scheduler.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport type { Handle, RemixNode } from '../lib/component.ts'\n\ndescribe('vnode rendering', () => {\n  describe('scheduling', () => {\n    it('skips descendant updates if ancestor is scheduled', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let capturedParentUpdate = () => {}\n      let appRenderCount = 0\n      function Parent(handle: Handle) {\n        capturedParentUpdate = () => {\n          handle.update()\n        }\n        return ({ children }: { children: RemixNode }) => {\n          appRenderCount++\n          return children\n        }\n      }\n\n      let childRenderCount = 0\n      let capturedChildUpdate = () => {}\n      function Child(handle: Handle) {\n        capturedChildUpdate = () => {\n          handle.update()\n        }\n        return () => {\n          childRenderCount++\n          return <div>Hello, world!</div>\n        }\n      }\n\n      root.render(\n        <Parent>\n          <Child />\n        </Parent>,\n      )\n      expect(container.innerHTML).toBe('<div>Hello, world!</div>')\n      expect(appRenderCount).toBe(1)\n      expect(childRenderCount).toBe(1)\n\n      capturedChildUpdate()\n      capturedParentUpdate()\n      root.flush()\n\n      expect(appRenderCount).toBe(2)\n      expect(childRenderCount).toBe(2)\n\n      // swap order\n      capturedParentUpdate()\n      capturedChildUpdate()\n      root.flush()\n\n      expect(appRenderCount).toBe(3)\n      expect(childRenderCount).toBe(3)\n    })\n\n    it('only runs tasks once', async () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let taskCount = 0\n      let capturedUpdate = () => {}\n      function App(handle: Handle) {\n        handle.queueTask(() => {\n          taskCount++\n        })\n\n        capturedUpdate = () => {\n          handle.queueTask(() => {\n            taskCount++\n          })\n          handle.update()\n        }\n        return () => null\n      }\n\n      root.render(<App />)\n      root.flush()\n      expect(taskCount).toBe(1)\n\n      capturedUpdate()\n      root.flush()\n      expect(taskCount).toBe(2)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.signals.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport type { Handle } from '../lib/component.ts'\n\ndescribe('vnode rendering', () => {\n  describe('signals', () => {\n    it('provides mounted signal on handle.signal', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let capturedSignal: AbortSignal | undefined\n      function App(handle: Handle) {\n        capturedSignal = handle.signal\n        return () => null\n      }\n\n      root.render(<App />)\n      invariant(capturedSignal)\n      expect(capturedSignal).toBeInstanceOf(AbortSignal)\n      expect(capturedSignal.aborted).toBe(false)\n\n      root.render(null)\n      root.flush()\n      expect(capturedSignal.aborted).toBe(true)\n    })\n\n    it('provides render signal to tasks and aborts on re-render', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let signals: AbortSignal[] = []\n      function App(handle: Handle) {\n        handle.queueTask((signal) => {\n          signals.push(signal)\n        })\n        return () => null\n      }\n\n      root.render(<App />)\n      root.flush()\n\n      expect(signals.length).toBe(1)\n      invariant(signals[0])\n      expect(signals[0]).toBeInstanceOf(AbortSignal)\n      expect(signals[0].aborted).toBe(false)\n\n      root.render(<App />)\n      root.flush()\n      expect(signals.length).toBe(1)\n      invariant(signals[0])\n      expect(signals[0].aborted).toBe(true)\n    })\n\n    it('aborts handle.update() signal on next update', async () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let capturedSignal: AbortSignal | undefined\n      let capturedUpdate = () => {}\n      function App(handle: Handle) {\n        capturedUpdate = () => {\n          handle.update().then((signal) => {\n            capturedSignal = signal\n          })\n        }\n        return () => null\n      }\n\n      root.render(<App />)\n      root.flush()\n\n      capturedUpdate()\n      root.flush()\n      await Promise.resolve()\n      invariant(capturedSignal)\n      let firstSignal = capturedSignal\n      expect(firstSignal.aborted).toBe(false)\n\n      capturedUpdate()\n      root.flush()\n      expect(firstSignal.aborted).toBe(true)\n    })\n\n    it('aborts queueTask signal when component is removed', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let capturedSignal: AbortSignal | undefined\n      function App(handle: Handle) {\n        handle.queueTask((signal) => {\n          capturedSignal = signal\n        })\n        return () => null\n      }\n\n      root.render(<App />)\n      root.flush()\n      invariant(capturedSignal)\n      expect(capturedSignal.aborted).toBe(false)\n\n      root.render(null)\n      root.flush()\n      expect(capturedSignal.aborted).toBe(true)\n    })\n\n    it('aborts handle.update() signal when component is removed', async () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let capturedSignal: AbortSignal | undefined\n      let capturedUpdate = () => {}\n      function App(handle: Handle) {\n        capturedUpdate = () => {\n          handle.update().then((signal) => {\n            capturedSignal = signal\n          })\n        }\n        return () => null\n      }\n\n      root.render(<App />)\n      root.flush()\n\n      capturedUpdate()\n      root.flush()\n      await Promise.resolve()\n      invariant(capturedSignal)\n      expect(capturedSignal.aborted).toBe(false)\n\n      root.render(null)\n      root.flush()\n      expect(capturedSignal.aborted).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.svg.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport { invariant } from '../lib/invariant.ts'\nimport { on } from '../index.ts'\nimport type { Handle, RemixNode } from '../lib/component.ts'\n\ndescribe('vnode rendering', () => {\n  describe('svg', () => {\n    it('renders SVG root and children with SVG namespace and attributes', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n\n      render(\n        <svg viewBox=\"0 0 24 24\" fill=\"none\" class=\"icon\">\n          <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"m4.5 12.75 6 6 9-13.5\" />\n        </svg>,\n      )\n\n      let svg = container.querySelector('svg')\n      let path = container.querySelector('path')\n      invariant(svg instanceof SVGSVGElement)\n      invariant(path instanceof SVGPathElement)\n\n      expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg')\n      expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg')\n\n      // Attribute casing: preserve exceptions and kebab-case general SVG attrs\n      expect(svg.getAttribute('viewBox')).toBe('0 0 24 24')\n      expect(svg.getAttribute('class')).toBe('icon')\n      expect(path.getAttribute('stroke-linecap')).toBe('round')\n      expect(path.getAttribute('stroke-linejoin')).toBe('round')\n    })\n\n    it('supports xlinkHref -> xlink:href on SVG elements', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n\n      render(\n        <svg>\n          <use xlinkHref=\"#my-id\" />\n        </svg>,\n      )\n\n      let useEl = container.querySelector('use')\n      invariant(useEl instanceof SVGUseElement)\n\n      expect(useEl.getAttribute('xlink:href')).toBe('#my-id')\n    })\n\n    it('updates and removes namespaced SVG attributes', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      root.render(\n        <svg>\n          <use id=\"u\" xlinkHref=\"#one\" />\n          <text id=\"t\" xmlLang=\"en\">\n            Hi\n          </text>\n        </svg>,\n      )\n\n      let useEl = container.querySelector('#u')\n      invariant(useEl instanceof SVGUseElement)\n      expect(useEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#one')\n\n      let textEl = container.querySelector('#t')\n      invariant(textEl instanceof SVGTextElement)\n      expect(textEl.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe('en')\n\n      root.render(\n        <svg>\n          <use id=\"u\" xlinkHref=\"#two\" />\n          <text id=\"t\" xmlLang=\"fr\">\n            Hi\n          </text>\n        </svg>,\n      )\n\n      let updatedUseEl = container.querySelector('#u')\n      invariant(updatedUseEl instanceof SVGUseElement)\n      expect(updatedUseEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe('#two')\n\n      let updatedTextEl = container.querySelector('#t')\n      invariant(updatedTextEl instanceof SVGTextElement)\n      expect(updatedTextEl.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe(\n        'fr',\n      )\n\n      root.render(\n        <svg>\n          <use id=\"u\" />\n          <text id=\"t\">Hi</text>\n        </svg>,\n      )\n\n      let removedUseEl = container.querySelector('#u')\n      invariant(removedUseEl instanceof SVGUseElement)\n      expect(removedUseEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(null)\n      expect(removedUseEl.getAttribute('xlink:href')).toBe(null)\n\n      let removedTextEl = container.querySelector('#t')\n      invariant(removedTextEl instanceof SVGTextElement)\n      expect(removedTextEl.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')).toBe(\n        null,\n      )\n      expect(removedTextEl.getAttribute('xml:lang')).toBe(null)\n    })\n\n    it('renders HTML subtree inside foreignObject with HTML namespace', () => {\n      let container = document.createElement('div')\n      let { render } = createRoot(container)\n\n      render(\n        <svg>\n          <foreignObject>\n            <div id=\"x\">Hello</div>\n          </foreignObject>\n        </svg>,\n      )\n\n      let div = container.querySelector('#x')\n      invariant(div)\n      expect(div instanceof HTMLDivElement).toBe(true)\n      expect(div.namespaceURI).toBe('http://www.w3.org/1999/xhtml')\n    })\n\n    it('updates and removes SVG attributes', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      root.render(\n        <svg>\n          <path id=\"p\" strokeLinecap=\"round\" />\n        </svg>,\n      )\n      let path = container.querySelector('#p')\n      invariant(path instanceof SVGPathElement)\n\n      // Update value\n      root.render(\n        <svg>\n          <path id=\"p\" strokeLinecap=\"square\" />\n        </svg>,\n      )\n      let updated = container.querySelector('#p')\n      invariant(updated instanceof SVGPathElement)\n      expect(updated).toBe(path)\n      expect(updated.getAttribute('stroke-linecap')).toBe('square')\n\n      // Remove attribute\n      root.render(\n        <svg>\n          <path id=\"p\" />\n        </svg>,\n      )\n      let removed = container.querySelector('#p')\n      invariant(removed instanceof SVGPathElement)\n      expect(removed.hasAttribute('stroke-linecap')).toBe(false)\n    })\n\n    it('uses canonical semantics for critical SVG attributes', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      root.render(\n        <svg>\n          <defs>\n            <filter id=\"f\" filterUnits=\"userSpaceOnUse\" x=\"0\" y=\"0\" width=\"100\" height=\"100\">\n              <feGaussianBlur id=\"blur\" stdDeviation=\"2.5\" />\n            </filter>\n            <linearGradient id=\"g\" gradientUnits=\"userSpaceOnUse\" />\n            <mask id=\"m\" maskUnits=\"userSpaceOnUse\" />\n            <clipPath id=\"c\" clipPathUnits=\"objectBoundingBox\" />\n          </defs>\n        </svg>,\n      )\n\n      let filter = container.querySelector('#f')\n      invariant(filter instanceof SVGFilterElement)\n      expect(filter.getAttribute('filterUnits')).toBe('userSpaceOnUse')\n      expect(filter.getAttribute('filter-units')).toBe(null)\n      expect(filter.filterUnits.baseVal).toBe(1)\n\n      let blur = container.querySelector('#blur')\n      invariant(blur instanceof SVGFEGaussianBlurElement)\n      expect(blur.getAttribute('stdDeviation')).toBe('2.5')\n      expect(blur.getAttribute('std-deviation')).toBe(null)\n\n      let gradient = container.querySelector('#g')\n      invariant(gradient instanceof SVGLinearGradientElement)\n      expect(gradient.getAttribute('gradientUnits')).toBe('userSpaceOnUse')\n      expect(gradient.getAttribute('gradient-units')).toBe(null)\n      expect(gradient.gradientUnits.baseVal).toBe(1)\n\n      let mask = container.querySelector('#m')\n      invariant(mask instanceof SVGMaskElement)\n      expect(mask.getAttribute('maskUnits')).toBe('userSpaceOnUse')\n      expect(mask.getAttribute('mask-units')).toBe(null)\n      expect(mask.maskUnits.baseVal).toBe(1)\n\n      let clipPath = container.querySelector('#c')\n      invariant(clipPath instanceof SVGClipPathElement)\n      expect(clipPath.getAttribute('clipPathUnits')).toBe('objectBoundingBox')\n      expect(clipPath.getAttribute('clip-path-units')).toBe(null)\n      expect(clipPath.clipPathUnits.baseVal).toBe(2)\n    })\n\n    it('attaches events on SVG elements', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      let clicked = false\n      root.render(\n        <svg>\n          <circle\n            id=\"c\"\n            mix={[\n              on('click', () => {\n                clicked = true\n              }),\n            ]}\n          />\n        </svg>,\n      )\n      root.flush()\n\n      let circle = container.querySelector('#c')\n      invariant(circle instanceof SVGCircleElement)\n      circle.dispatchEvent(new MouseEvent('click', { bubbles: true }))\n      expect(clicked).toBe(true)\n    })\n\n    it('propagates SVG namespace through components', () => {\n      let container = document.createElement('div')\n      let root = createRoot(container)\n\n      function SvgGroup() {\n        return ({ href, children }: { href: string; children?: RemixNode }) => (\n          <g href={href}>{children}</g>\n        )\n      }\n\n      root.render(\n        <svg width=\"100\" height=\"100\">\n          <SvgGroup href=\"/test\">\n            <path id=\"p\" />\n          </SvgGroup>\n        </svg>,\n      )\n\n      let svg = container.querySelector('svg')\n      let group = container.querySelector('g')\n      let path = container.querySelector('path')\n\n      invariant(svg instanceof SVGSVGElement)\n      invariant(group instanceof SVGGElement)\n      invariant(path instanceof SVGPathElement)\n\n      // All elements should have SVG namespace\n      expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg')\n      expect(group.namespaceURI).toBe('http://www.w3.org/2000/svg')\n      expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/component/src/test/vdom.tasks.test.tsx",
    "content": "import { describe, it, expect } from 'vitest'\nimport { createRoot } from '../lib/vdom.ts'\nimport type { Handle } from '../lib/component.ts'\n\ndescribe('vnode rendering', () => {\n  it('runs update tasks after updates', () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    let taskRan = false\n    let capturedUpdate = () => {}\n    function App(handle: Handle) {\n      capturedUpdate = () => {\n        handle.queueTask(() => {\n          taskRan = true\n        })\n        handle.update()\n      }\n\n      return () => <div>Hello, world!</div>\n    }\n\n    root.render(<App />)\n    root.flush()\n    expect(taskRan).toBe(false)\n\n    capturedUpdate()\n    expect(taskRan).toBe(false)\n    root.flush()\n    expect(taskRan).toBe(true)\n  })\n\n  it('handle.update() returns a promise that resolves with a signal', async () => {\n    let container = document.createElement('div')\n    let root = createRoot(container)\n\n    let capturedSignal: AbortSignal | undefined\n    let capturedUpdate = () => {}\n    function App(handle: Handle) {\n      capturedUpdate = () => {\n        handle.update().then((signal) => {\n          capturedSignal = signal\n        })\n      }\n\n      return () => <div>Hello, world!</div>\n    }\n\n    root.render(<App />)\n    root.flush()\n    expect(capturedSignal).toBe(undefined)\n\n    capturedUpdate()\n    expect(capturedSignal).toBe(undefined)\n    root.flush()\n    await Promise.resolve()\n    expect(capturedSignal).toBeInstanceOf(AbortSignal)\n    expect(capturedSignal?.aborted).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/component/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"noEmit\": false,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"sourceMap\": true\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"src/**/*.test.tsx\", \"src/lib/test\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/component/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\", \"DOM.AsyncIterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@remix-run/component\"\n  },\n  \"exclude\": [\"bench\", \"demos\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/component/vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  resolve: {\n    conditions: ['development'],\n  },\n  test: {\n    coverage: {\n      provider: 'v8',\n      include: ['src/**/*.{ts,tsx}'],\n      exclude: ['src/**/*.test.{ts,tsx}', 'src/test/**'],\n      thresholds: {\n        lines: 85,\n        functions: 85,\n        branches: 85,\n        statements: 85,\n      },\n    },\n    browser: {\n      enabled: true,\n      provider: 'playwright',\n      instances: [\n        {\n          browser: 'chromium',\n          headless: true,\n        },\n      ],\n      screenshotFailures: false,\n    },\n    include: ['src/**/*.test.{ts,tsx}'],\n  },\n})\n"
  },
  {
    "path": "packages/compression-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/compression-middleware/CHANGELOG.md",
    "content": "# `compression-middleware` CHANGELOG\n\nThis is the changelog for [`compression-middleware`](https://github.com/remix-run/remix/tree/main/packages/compression-middleware). It follows [semantic versioning](https://semver.org/).\n\n## v0.1.3\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0)\n  - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0)\n  - [`response@0.3.2`](https://github.com/remix-run/remix/releases/tag/response@0.3.2)\n\n## v0.1.2\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0)\n\n## v0.1.1\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.1.0 (2025-11-25)\n\nInitial release of this package.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/compression-middleware/README.md) for more details.\n"
  },
  {
    "path": "packages/compression-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/compression-middleware/README.md",
    "content": "# compression-middleware\n\nResponse compression middleware for Remix. It negotiates `br`, `gzip`, and `deflate` from `Accept-Encoding` and applies sensible defaults for when compression is useful.\n\n## Features\n\n- **Encoding Negotiation** - Selects the best supported encoding from `Accept-Encoding`\n- **Compression Guards** - Skips already-compressed responses and range-enabled responses\n- **Size Thresholds** - Configurable minimum response size for compression\n- **MIME Filtering** - Compresses only content types likely to benefit\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { compression } from 'remix/compression-middleware'\n\nlet router = createRouter({\n  middleware: [compression()],\n})\n```\n\nThe middleware will automatically compress responses for compressible MIME types when:\n\n- The client supports compression (`Accept-Encoding` header with a supported encoding)\n- The response is large enough to benefit from compression (≥1024 bytes if `Content-Length` is present, by default)\n- The response hasn't already been compressed\n- The response doesn't advertise range support (`Accept-Ranges: bytes`)\n\n### Threshold\n\n**Default:** `1024` (only enforced if `Content-Length` is present)\n\nSet the minimum response size in bytes to compress:\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { compression } from 'remix/compression-middleware'\n\nlet router = createRouter({\n  middleware: [\n    compression({\n      threshold: 2048, // Only compress responses ≥2KB\n    }),\n  ],\n})\n```\n\n### Encodings\n\n**Default:** `['br', 'gzip', 'deflate']`\n\nCustomize which compression algorithms to support:\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { compression } from 'remix/compression-middleware'\n\nlet router = createRouter({\n  middleware: [\n    compression({\n      encodings: ['br', 'gzip'], // Only use Brotli and Gzip\n    }),\n  ],\n})\n```\n\nThe `encodings` option can also be a function that receives the response:\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { compression } from 'remix/compression-middleware'\n\nlet router = createRouter({\n  middleware: [\n    compression({\n      encodings: (response) => {\n        // Use different encodings for server-sent events\n        let contentType = response.headers.get('Content-Type')\n        return contentType?.startsWith('text/event-stream;')\n          ? ['gzip', 'deflate']\n          : ['br', 'gzip', 'deflate']\n      },\n    }),\n  ],\n})\n```\n\n### Filter Media Type\n\n**Default:** Uses `isCompressibleMimeType()` from [`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime)\n\nYou can customize this behavior with the `filterMediaType` option:\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { compression } from 'remix/compression-middleware'\nimport { isCompressibleMimeType } from 'remix/mime'\n\nlet router = createRouter({\n  middleware: [\n    compression({\n      filterMediaType(mediaType) {\n        // Add a custom media type to the default compressible list\n        return isCompressibleMimeType(mediaType) || mediaType === 'application/vnd.example+data'\n      },\n    }),\n  ],\n})\n```\n\n### Compression Options\n\n**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.\n\nYou can pass options options to the underlying Node.js `zlib` and `brotli` compressors for fine-grained control:\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { compression } from 'remix/compression-middleware'\nimport { zlib } from 'node:zlib'\n\nlet router = createRouter({\n  middleware: [\n    compression({\n      zlib: {\n        level: 6,\n      },\n      brotli: {\n        params: {\n          [zlib.constants.BROTLI_PARAM_QUALITY]: 4,\n        },\n      },\n    }),\n  ],\n})\n```\n\nLike `encodings`, both `zlib` and `brotli` options can also be functions that receive the response:\n\n```ts\nimport zlib from 'node:zlib'\nimport { createRouter } from 'remix/fetch-router'\nimport { compression } from 'remix/compression-middleware'\n\nlet router = createRouter({\n  middleware: [\n    compression({\n      brotli: (response) => {\n        let contentType = response.headers.get('Content-Type')\n        return {\n          params: {\n            [zlib.constants.BROTLI_PARAM_QUALITY]: contentType?.startsWith('text/html;') ? 4 : 11,\n          },\n        }\n      },\n    }),\n  ],\n})\n```\n\n## Related Packages\n\n- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n- [`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) - MIME type utilities\n- [`@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response) - Response helpers\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/compression-middleware/global.d.ts",
    "content": "// See https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/62651\n\ninterface ReadableStream<R = any> {\n  values(options?: { preventCancel?: boolean }): AsyncIterableIterator<R>\n  [Symbol.asyncIterator](): AsyncIterableIterator<R>\n}\n"
  },
  {
    "path": "packages/compression-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/compression-middleware\",\n  \"version\": \"0.1.3\",\n  \"description\": \"Middleware for compressing HTTP responses\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/compression-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/compression-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/mime\": \"workspace:*\",\n    \"@remix-run/response\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\",\n    \"@remix-run/mime\": \"workspace:^\",\n    \"@remix-run/response\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"compression\",\n    \"gzip\",\n    \"brotli\",\n    \"deflate\"\n  ]\n}\n"
  },
  {
    "path": "packages/compression-middleware/src/index.ts",
    "content": "export { compression, type CompressionOptions } from './lib/compression.ts'\n"
  },
  {
    "path": "packages/compression-middleware/src/lib/compression.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { gunzip } from 'node:zlib'\nimport { promisify } from 'node:util'\nimport { describe, it } from 'node:test'\nimport { createRouter } from '@remix-run/fetch-router'\nimport { isCompressibleMimeType } from '@remix-run/mime'\n\nimport { compression } from './compression.ts'\n\nconst gunzipAsync = promisify(gunzip)\n\ndescribe('compression()', () => {\n  it('compresses compressible content types', async () => {\n    let router = createRouter({\n      middleware: [compression()],\n    })\n\n    router.get(\n      '/',\n      () =>\n        new Response('Hello, World!', {\n          headers: { 'Content-Type': 'text/html' },\n        }),\n    )\n\n    let response = await router.fetch('https://remix.run/', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n\n    assert.equal(response.status, 200)\n    assert.equal(response.headers.get('Content-Encoding'), 'gzip')\n\n    let buffer = Buffer.from(await response.arrayBuffer())\n    let decompressed = await gunzipAsync(buffer)\n    assert.equal(decompressed.toString(), 'Hello, World!')\n  })\n\n  it('does not compress non-compressible content types', async () => {\n    let router = createRouter({\n      middleware: [compression()],\n    })\n\n    router.get(\n      '/image.png',\n      () =>\n        new Response('fake image data', {\n          headers: { 'Content-Type': 'image/png' },\n        }),\n    )\n\n    let response = await router.fetch('https://remix.run/image.png', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n\n    assert.equal(response.status, 200)\n    assert.equal(response.headers.get('Content-Encoding'), null)\n    assert.equal(await response.text(), 'fake image data')\n  })\n\n  it('respects threshold option', async () => {\n    let router = createRouter({\n      middleware: [compression({ threshold: 10 })],\n    })\n\n    router.get(\n      '/',\n      () =>\n        new Response('Small', {\n          headers: { 'Content-Type': 'text/plain', 'Content-Length': '5' },\n        }),\n    )\n\n    let response = await router.fetch('https://remix.run/', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n\n    assert.equal(response.headers.get('Content-Encoding'), null)\n    assert.equal(await response.text(), 'Small')\n  })\n\n  it('compresses responses when Content-Length is not set', async () => {\n    let router = createRouter({\n      middleware: [compression({ threshold: 1024 })],\n    })\n\n    router.get(\n      '/',\n      () =>\n        // Small response without Content-Length header\n        new Response('Small', {\n          headers: { 'Content-Type': 'text/plain' },\n        }),\n    )\n\n    let response = await router.fetch('https://remix.run/', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n\n    // Should compress because threshold check requires Content-Length\n    assert.equal(response.headers.get('Content-Encoding'), 'gzip')\n  })\n\n  it('respects custom filterMediaType', async () => {\n    let router = createRouter({\n      middleware: [\n        compression({\n          filterMediaType: (mediaType) =>\n            // Only compress JSON\n            mediaType.includes('json'),\n        }),\n      ],\n    })\n\n    router.get(\n      '/data.json',\n      () =>\n        new Response('{\"data\":\"value\"}', {\n          headers: { 'Content-Type': 'application/json' },\n        }),\n    )\n\n    router.get(\n      '/page.html',\n      () =>\n        new Response('<html>test</html>', {\n          headers: { 'Content-Type': 'text/html' },\n        }),\n    )\n\n    let jsonResponse = await router.fetch('https://remix.run/data.json', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n\n    assert.equal(jsonResponse.headers.get('Content-Encoding'), 'gzip')\n\n    let htmlResponse = await router.fetch('https://remix.run/page.html', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n\n    assert.equal(htmlResponse.headers.get('Content-Encoding'), null)\n    assert.equal(await htmlResponse.text(), '<html>test</html>')\n  })\n\n  it('allows custom filterMediaType to use isCompressibleMimeType', async () => {\n    let router = createRouter({\n      middleware: [\n        compression({\n          filterMediaType: (mediaType) =>\n            // Only compress if it's compressible AND not HTML\n            isCompressibleMimeType(mediaType) && !mediaType.includes('html'),\n        }),\n      ],\n    })\n\n    router.get(\n      '/data.json',\n      () =>\n        new Response('{\"data\":\"value\"}', {\n          headers: { 'Content-Type': 'application/json' },\n        }),\n    )\n\n    router.get(\n      '/page.html',\n      () =>\n        new Response('<html>test</html>', {\n          headers: { 'Content-Type': 'text/html' },\n        }),\n    )\n\n    let jsonResponse = await router.fetch('https://remix.run/data.json', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n\n    assert.equal(jsonResponse.headers.get('Content-Encoding'), 'gzip')\n\n    let htmlResponse = await router.fetch('https://remix.run/page.html', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n\n    // HTML is compressible but our custom filter excludes it\n    assert.equal(htmlResponse.headers.get('Content-Encoding'), null)\n    assert.equal(await htmlResponse.text(), '<html>test</html>')\n  })\n\n  it('supports dynamic encodings based on response', async () => {\n    let router = createRouter({\n      middleware: [\n        compression({\n          encodings: (response) =>\n            response.headers.get('Content-Type') === 'text/event-stream'\n              ? ['gzip', 'deflate']\n              : ['br', 'gzip', 'deflate'],\n        }),\n      ],\n    })\n\n    router.get(\n      '/events',\n      () =>\n        new Response('event: message\\ndata: hello\\n\\n', {\n          headers: { 'Content-Type': 'text/event-stream' },\n        }),\n    )\n\n    router.get(\n      '/data.json',\n      () =>\n        new Response('{\"data\":\"value\"}', {\n          headers: { 'Content-Type': 'application/json' },\n        }),\n    )\n\n    // SSE should use gzip (brotli excluded)\n    let sseResponse = await router.fetch('https://remix.run/events', {\n      headers: { 'Accept-Encoding': 'br, gzip' },\n    })\n    assert.equal(sseResponse.headers.get('Content-Encoding'), 'gzip')\n\n    // JSON should use brotli (brotli included)\n    let jsonResponse = await router.fetch('https://remix.run/data.json', {\n      headers: { 'Accept-Encoding': 'br, gzip' },\n    })\n    assert.equal(jsonResponse.headers.get('Content-Encoding'), 'br')\n  })\n\n  it('allows disabling compression per response via empty encodings array', async () => {\n    let router = createRouter({\n      middleware: [\n        compression({\n          encodings: (response) =>\n            // Don't compress responses with X-No-Compress header\n            response.headers.has('X-No-Compress') ? [] : ['gzip'],\n        }),\n      ],\n    })\n\n    router.get(\n      '/nocompress',\n      () =>\n        new Response('not compressed', {\n          headers: { 'Content-Type': 'text/plain', 'X-No-Compress': 'true' },\n        }),\n    )\n\n    router.get(\n      '/compress',\n      () =>\n        new Response('compressed', {\n          headers: { 'Content-Type': 'text/plain' },\n        }),\n    )\n\n    let noCompressResponse = await router.fetch('https://remix.run/nocompress', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    assert.equal(noCompressResponse.headers.get('Content-Encoding'), null)\n    assert.equal(await noCompressResponse.text(), 'not compressed')\n\n    let compressResponse = await router.fetch('https://remix.run/compress', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    assert.equal(compressResponse.headers.get('Content-Encoding'), 'gzip')\n  })\n})\n"
  },
  {
    "path": "packages/compression-middleware/src/lib/compression.ts",
    "content": "import type { BrotliOptions, ZlibOptions } from 'node:zlib'\nimport type { Middleware } from '@remix-run/fetch-router'\nimport { compressResponse, type CompressResponseOptions } from '@remix-run/response/compress'\nimport { isCompressibleMimeType } from '@remix-run/mime'\n\ntype Encoding = 'br' | 'gzip' | 'deflate'\n\n/**\n * Configuration for automatic response compression.\n */\nexport interface CompressionOptions {\n  /**\n   * Minimum size in bytes to compress (only enforced if Content-Length present).\n   * Default: 1024\n   */\n  threshold?: number\n\n  /**\n   * Optional filter to control which responses get compressed based on media type.\n   * If not provided, uses compressible media types from mime-db.\n   */\n  filterMediaType?: (mediaType: string) => boolean\n\n  /**\n   * Which encodings the server supports for negotiation in order of preference.\n   * Can be static or a function that returns encodings based on the response.\n   *\n   * Default: ['br', 'gzip', 'deflate']\n   */\n  encodings?: Encoding[] | ((response: Response) => Encoding[])\n\n  /**\n   * node:zlib options for gzip/deflate compression.\n   * Can be static or a function that returns options based on the response.\n   *\n   * See: https://nodejs.org/api/zlib.html#class-options\n   */\n  zlib?: ZlibOptions | ((response: Response) => ZlibOptions)\n\n  /**\n   * node:zlib options for Brotli compression.\n   * Can be static or a function that returns options based on the response.\n   *\n   * See: https://nodejs.org/api/zlib.html#class-brotlioptions\n   */\n  brotli?: BrotliOptions | ((response: Response) => BrotliOptions)\n}\n\n/**\n * Creates a middleware handler that automatically compresses responses based on the\n * client's Accept-Encoding header, along with an additional Content-Type filter\n * by default to only apply compression to appropriate media types.\n *\n * @param options Optional compression settings\n * @returns A middleware handler that automatically compresses responses based on the client's Accept-Encoding header\n * @example\n * ```ts\n * let router = createRouter({\n *   middleware: [compression()],\n * })\n * ```\n */\nexport function compression(options?: CompressionOptions): Middleware {\n  return async (context, next) => {\n    let response = await next()\n\n    let contentTypeHeader = response.headers.get('Content-Type')\n    if (!contentTypeHeader) {\n      return response\n    }\n\n    let mediaType = contentTypeHeader.split(';')[0].trim()\n    if (!mediaType) {\n      return response\n    }\n\n    let filterMediaType = options?.filterMediaType ?? isCompressibleMimeType\n    if (!filterMediaType(mediaType)) {\n      return response\n    }\n\n    let compressOptions: CompressResponseOptions = {\n      threshold: options?.threshold,\n      encodings: options?.encodings\n        ? typeof options.encodings === 'function'\n          ? options.encodings(response)\n          : options.encodings\n        : undefined,\n      zlib: options?.zlib\n        ? typeof options.zlib === 'function'\n          ? options.zlib(response)\n          : options.zlib\n        : undefined,\n      brotli: options?.brotli\n        ? typeof options.brotli === 'function'\n          ? options.brotli(response)\n          : options.brotli\n        : undefined,\n    }\n\n    return compressResponse(response, context.request, compressOptions)\n  }\n}\n"
  },
  {
    "path": "packages/compression-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"global.d.ts\", \"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/compression-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/cookie/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/cookie/CHANGELOG.md",
    "content": "# `cookie` CHANGELOG\n\nThis is the changelog for [`cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie). It follows [semantic versioning](https://semver.org/).\n\n## v0.5.1\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.5.0 (2025-11-25)\n\n- Add `Cookie` class. The `createCookie` function now returns an instance of the `Cookie` class.\n\n  ```ts\n  // You can now create cookies using either approach:\n  import { createCookie, Cookie } from '@remix-run/cookie'\n\n  // Factory function\n  let cookie = createCookie('session')\n\n  // Or use the class directly\n  let cookie = new Cookie('session')\n  ```\n\n## v0.4.1 (2025-11-19)\n\n- Force `secure` to be `true` when `partitioned` is `true`\n\n## v0.4.0 (2025-11-18)\n\n- BREAKING CHANGE: Remove `Cookie` class, use `createCookie` instead\n\n  ```tsx\n  // before\n  import { Cookie } from '@remix-run/cookie'\n  let cookie = new Cookie('session')\n\n  // after\n  import { createCookie } from '@remix-run/cookie'\n  let cookie = createCookie('session')\n  ```\n\n- Add `domain`, `expires`, `httpOnly`, `maxAge`, `partitioned`, `path`, `sameSite`, and `secure` properties to `Cookie` objects\n\n## v0.3.0 (2025-11-08)\n\n- BREAKING CHANGE: Rename `cookie.isSigned` to `cookie.signed`\n- Add `createCookie` function to create a new `Cookie` object\n- `CookieOptions` now extends `CookieProperties` so all cookie properties may be set in the `Cookie` constructor\n\n## v0.2.0 (2025-11-04)\n\n- Update `@remix-run/headers` peer dep to v0.15.0\n\n## v0.1.0 (2025-11-04)\n\nThis is the initial release of `@remix-run/cookie`.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/cookie/README.md) for more details.\n"
  },
  {
    "path": "packages/cookie/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/cookie/README.md",
    "content": "# cookie\n\nType-safe cookie parsing and serialization for Remix. It supports secure signing, secret rotation, and complete cookie attribute control.\n\n## Features\n\n- **Secure Cookie Signing:** Built-in cryptographic signing using HMAC-SHA256 to prevent cookie tampering, with support for secret rotation without breaking existing cookies.\n- **Secret Rotation Support:** Seamlessly rotate signing secrets while maintaining backward compatibility with existing cookies.\n- **Web Standards Compliant:** Built on Web Crypto API and standard cookie parsing, making it runtime-agnostic (Node.js, Bun, Deno, Cloudflare Workers).\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```tsx\nimport { createCookie } from 'remix/cookie'\n\nlet sessionCookie = createCookie('session', {\n  httpOnly: true,\n  secrets: ['s3cret1'],\n  secure: true,\n})\n\ncookie.name // \"session\"\ncookie.httpOnly // true\ncookie.secure // true\ncookie.signed // true\n\n// Get the value of the \"session\" cookie from the request's `Cookie` header\nlet value = await sessionCookie.parse(request.headers.get('Cookie'))\n\n// Set the value of the cookie in a Response's `Set-Cookie` header\nlet response = new Response('Hello, world!', {\n  headers: {\n    'Set-Cookie': await sessionCookie.serialize(value),\n  },\n})\n```\n\n### Signing Cookies\n\nThis library supports signing cookies, which is useful for ensuring the integrity of the cookie value and preventing tampering. Signing happens automatically when you provide a `secrets` option to the `Cookie` constructor.\n\nSecret rotation is also supported, so you can easily rotate in new secrets without breaking existing cookies.\n\n```tsx\nimport { Cookie } from 'remix/cookie'\n\n// Start with a single secret\nlet sessionCookie = createCookie('session', {\n  secrets: ['secret1'],\n})\n\nconsole.log(sessionCookie.signed) // true\n\nlet response = new Response('Hello, world!', {\n  headers: {\n    'Set-Cookie': await sessionCookie.serialize(value),\n  },\n})\n```\n\nAll cookies sent in this scenario will be signed with the secret `secret1`. Later, when it's time to rotate secrets, add a new secret to the beginning of the array and all existing cookies will still be able to be parsed.\n\n```tsx\nlet sessionCookie = createCookie('session', {\n  secrets: ['secret2', 'secret1'],\n})\n\n// This works for cookies signed with either secret\nlet value = await sessionCookie.parse(request.headers.get('Cookie'))\n\n// Newly serialized cookies will be signed with the new secret\nlet response = new Response('Hello, world!', {\n  headers: {\n    'Set-Cookie': await sessionCookie.serialize(value),\n  },\n})\n```\n\n### Custom Encoding\n\nBy default, [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) and [`decodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) are used to encode and decode the cookie value. This is suitable for most use cases, but you can provide your own functions to customize the encoding and decoding of the cookie value.\n\n```tsx\nlet sessionCookie = createCookie('session', {\n  encode: (value) => value,\n  decode: (value) => value,\n})\n```\n\nThis can be useful for viewing the value of cookies in a human-readable format in the browser's developer tools. But you should be sure that the cookie value contains only characters that are [valid in a cookie value](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#attributes).\n\n## Related Packages\n\n- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - Type-safe HTTP header manipulation\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Build HTTP routers using the web fetch API\n- [`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\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/cookie/package.json",
    "content": "{\n  \"name\": \"@remix-run/cookie\",\n  \"version\": \"0.5.1\",\n  \"description\": \"A toolkit for working with cookies in JavaScript\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/cookie\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/cookie#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/headers\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"http\",\n    \"cookie\",\n    \"cookies\",\n    \"http-cookies\",\n    \"set-cookie\"\n  ]\n}\n"
  },
  {
    "path": "packages/cookie/src/index.ts",
    "content": "export { type CookieOptions, Cookie, createCookie } from './lib/cookie.ts'\n"
  },
  {
    "path": "packages/cookie/src/lib/cookie-signing.ts",
    "content": "const encoder = new TextEncoder()\n\nexport async function sign(value: string, secret: string): Promise<string> {\n  let data = encoder.encode(value)\n  let key = await createKey(secret, ['sign'])\n  let signature = await crypto.subtle.sign('HMAC', key, data)\n  let hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(/=+$/, '')\n\n  return value + '.' + hash\n}\n\nexport async function unsign(cookie: string, secret: string): Promise<string | false> {\n  let index = cookie.lastIndexOf('.')\n\n  if (index === -1) {\n    return false\n  }\n\n  let value = cookie.slice(0, index)\n  let hash = cookie.slice(index + 1)\n  let data = encoder.encode(value)\n  let key = await createKey(secret, ['verify'])\n\n  try {\n    let signature = byteStringToArray(atob(hash))\n    let valid = await crypto.subtle.verify('HMAC', key, signature, data)\n\n    return valid ? value : false\n  } catch (error: unknown) {\n    // atob will throw a DOMException with name === 'InvalidCharacterError'\n    // if the signature contains a non-base64 character, which should just\n    // be treated as an invalid signature.\n    return false\n  }\n}\n\nfunction createKey(secret: string, usages: CryptoKey['usages']): Promise<CryptoKey> {\n  return crypto.subtle.importKey(\n    'raw',\n    encoder.encode(secret),\n    { name: 'HMAC', hash: 'SHA-256' },\n    false,\n    usages,\n  )\n}\n\nfunction byteStringToArray(byteString: string): Uint8Array<ArrayBuffer> {\n  let array = new Uint8Array(byteString.length)\n\n  for (let i = 0; i < byteString.length; i++) {\n    array[i] = byteString.charCodeAt(i)\n  }\n\n  return array\n}\n"
  },
  {
    "path": "packages/cookie/src/lib/cookie.test.ts",
    "content": "import { SetCookie } from '@remix-run/headers'\n\nimport * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createCookie } from './cookie.ts'\n\nfunction getCookieFromSetCookie(setCookie: string): string {\n  let header = new SetCookie(setCookie)\n  return header.name + '=' + header.value\n}\n\ndescribe('Cookie', () => {\n  it('defaults path to /', async () => {\n    let cookie = createCookie('my-cookie')\n    assert.equal(cookie.path, '/')\n    let setCookie = await cookie.serialize('hello world')\n    assert.ok(setCookie.includes('Path=/'))\n  })\n\n  it('defaults sameSite to Lax', async () => {\n    let cookie = createCookie('my-cookie')\n    assert.equal(cookie.sameSite, 'Lax')\n    let setCookie = await cookie.serialize('hello world')\n    assert.ok(setCookie.includes('SameSite=Lax'))\n  })\n\n  it('defaults secure to true when partitioned is true', async () => {\n    let cookie = createCookie('my-cookie', { partitioned: true })\n    assert.equal(cookie.secure, true)\n  })\n\n  it('parses/serializes empty string values', async () => {\n    let cookie = createCookie('my-cookie')\n    let setCookie = await cookie.serialize('')\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie))\n    assert.equal(value, '')\n  })\n\n  it('parses/serializes unsigned string values', async () => {\n    let cookie = createCookie('my-cookie')\n    let setCookie = await cookie.serialize('hello world')\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie))\n    assert.equal(value, 'hello world')\n  })\n\n  it('parses/serializes signed string values', async () => {\n    let cookie = createCookie('my-cookie', {\n      secrets: ['secret1'],\n    })\n    let setCookie = await cookie.serialize('hello michael')\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie))\n\n    assert.equal(value, 'hello michael')\n  })\n\n  it('parses/serializes string values containing utf8 characters', async () => {\n    let cookie = createCookie('my-cookie')\n    let setCookie = await cookie.serialize('日本語')\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie))\n\n    assert.equal(value, '日本語')\n  })\n\n  it('fails to parses signed string values with invalid signature', async () => {\n    let cookie = createCookie('my-cookie', {\n      secrets: ['secret1'],\n    })\n    let setCookie = await cookie.serialize('hello michael')\n    let cookie2 = createCookie('my-cookie', {\n      secrets: ['secret2'],\n    })\n    let value = await cookie2.parse(getCookieFromSetCookie(setCookie))\n\n    assert.equal(value, null)\n  })\n\n  it('fails to parse signed string values with invalid signature encoding', async () => {\n    let cookie = createCookie('my-cookie', {\n      secrets: ['secret1'],\n    })\n    let setCookie = await cookie.serialize('hello michael')\n    let cookie2 = createCookie('my-cookie', {\n      secrets: ['secret2'],\n    })\n    // use characters that are invalid for base64 encoding\n    let value = await cookie2.parse(getCookieFromSetCookie(setCookie) + '%^&')\n\n    assert.equal(value, null)\n  })\n\n  it('parses/serializes signed object values', async () => {\n    let cookie = createCookie('my-cookie', {\n      secrets: ['secret1'],\n    })\n    let setCookie = await cookie.serialize(JSON.stringify({ hello: 'mjackson' }))\n    let value = JSON.parse((await cookie.parse(getCookieFromSetCookie(setCookie)))!)\n\n    assert.deepEqual(value, { hello: 'mjackson' })\n  })\n\n  it('supports secret rotation', async () => {\n    let cookie = createCookie('my-cookie', {\n      secrets: ['secret1'],\n    })\n    let setCookie = await cookie.serialize('mjackson')\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie))\n\n    assert.deepEqual(value, 'mjackson')\n\n    // A new secret enters the rotation...\n    cookie = createCookie('my-cookie', {\n      secrets: ['secret2', 'secret1'],\n    })\n\n    // cookie should still be able to parse old cookies.\n    let oldValue = await cookie.parse(getCookieFromSetCookie(setCookie))\n    assert.deepEqual(oldValue, value)\n\n    // New Set-Cookie should be different, it uses a different secret.\n    let setCookie2 = await cookie.serialize(value)\n    assert.notEqual(setCookie, setCookie2)\n\n    let newValue = await cookie.parse(getCookieFromSetCookie(setCookie2))\n    assert.deepEqual(oldValue, newValue)\n  })\n\n  it('is not signed by default', async () => {\n    let cookie = createCookie('my-cookie')\n    assert.equal(cookie.signed, false)\n\n    let cookie2 = createCookie('my-cookie2', { secrets: undefined })\n    assert.equal(cookie2.signed, false)\n  })\n\n  it('supports overriding cookie properties in the constructor', async () => {\n    let cookie = createCookie('my-cookie', {\n      domain: 'remix.run',\n      path: '/about',\n      maxAge: 3600,\n      sameSite: 'None',\n      secure: true,\n      httpOnly: true,\n    })\n    let setCookie = await cookie.serialize('hello world')\n    assert.ok(setCookie.includes('Domain=remix.run'))\n    assert.ok(setCookie.includes('Path=/about'))\n    assert.ok(setCookie.includes('Max-Age=3600'))\n    assert.ok(setCookie.includes('SameSite=None'))\n    assert.ok(setCookie.includes('Secure'))\n    assert.ok(setCookie.includes('HttpOnly'))\n  })\n\n  it('supports overriding cookie properties in the serialize method', async () => {\n    let cookie = createCookie('my-cookie')\n    let setCookie = await cookie.serialize('hello world', {\n      domain: 'remix.run',\n      path: '/about',\n      maxAge: 3600,\n      sameSite: 'None',\n      secure: true,\n      httpOnly: true,\n    })\n    assert.ok(setCookie.includes('Domain=remix.run'))\n    assert.ok(setCookie.includes('Path=/about'))\n    assert.ok(setCookie.includes('Max-Age=3600'))\n    assert.ok(setCookie.includes('SameSite=None'))\n    assert.ok(setCookie.includes('Secure'))\n    assert.ok(setCookie.includes('HttpOnly'))\n  })\n\n  it('parses empty values as null', async () => {\n    let cookie = createCookie('my-cookie')\n    let value = await cookie.parse(null)\n\n    assert.equal(value, null)\n  })\n\n  it('parses missing cookies as null', async () => {\n    let cookie = createCookie('my-cookie')\n    let value = await cookie.parse('other-cookie=hello')\n\n    assert.equal(value, null)\n  })\n})\n"
  },
  {
    "path": "packages/cookie/src/lib/cookie.ts",
    "content": "import {\n  Cookie as CookieHeader,\n  SetCookie as SetCookieHeader,\n  type CookieProperties,\n} from '@remix-run/headers'\n\nimport { sign, unsign } from './cookie-signing.ts'\n\n/**\n * Options for creating a cookie.\n */\nexport interface CookieOptions extends CookieProperties {\n  /**\n   * A function that decodes the cookie value. Decodes any URL-encoded sequences into their\n   * original characters.\n   *\n   * See [RFC 6265](https://tools.ietf.org/html/rfc6265#section-4.1.1) for more details.\n   *\n   * @default decodeURIComponent\n   */\n  decode?: (value: string) => string\n  /**\n   * A function that encodes the cookie value. Percent-encodes all characters that are not allowed\n   * in a cookie value.\n   *\n   * See [RFC 6265](https://tools.ietf.org/html/rfc6265#section-4.1.1) for more details.\n   *\n   * @default encodeURIComponent\n   */\n  encode?: (value: string) => string\n  /**\n   * An array of secrets that may be used to sign/unsign the value of a cookie.\n   *\n   * The array makes it easy to rotate secrets. New secrets should be added to\n   * the beginning of the array. `cookie.serialize()` will always use the first\n   * value in the array, but `cookie.parse()` may use any of them so that\n   * cookies that were signed with older secrets still work.\n   */\n  secrets?: string[]\n}\n\ntype SameSiteValue = 'Strict' | 'Lax' | 'None'\ntype Coder = (value: string) => string\n\n/**\n * Represents a HTTP cookie.\n *\n * Supports parsing and serializing the cookie to/from `Cookie` and `Set-Cookie` headers.\n *\n * Also supports cryptographic signing of the cookie value to ensure it's not tampered with, and\n * secret rotation to easily rotate secrets without breaking existing cookies.\n */\nexport class Cookie implements CookieProperties {\n  #name: string\n  #decode: Coder\n  #encode: Coder\n  #secrets: string[]\n  #domain: string | undefined\n  #expires: Date | undefined\n  #httpOnly: boolean | undefined\n  #maxAge: number | undefined\n  #partitioned: boolean | undefined\n  #path: string\n  #sameSite: SameSiteValue\n  #secure: boolean | undefined\n\n  /**\n   * @param name The name of the cookie\n   * @param options Options for the cookie\n   */\n  constructor(name: string, options?: CookieOptions) {\n    let {\n      decode = decodeURIComponent,\n      encode = encodeURIComponent,\n      secrets = [],\n      domain,\n      expires,\n      httpOnly,\n      maxAge,\n      path = '/',\n      partitioned,\n      secure,\n      sameSite = 'Lax',\n    } = options ?? {}\n\n    if (partitioned === true) {\n      // Partitioned cookies must be set with Secure\n      // See https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies\n      secure = true\n    }\n\n    this.#name = name\n    this.#decode = decode\n    this.#encode = encode\n    this.#secrets = secrets\n    this.#domain = domain\n    this.#expires = expires\n    this.#httpOnly = httpOnly\n    this.#maxAge = maxAge\n    this.#partitioned = partitioned\n    this.#path = path\n    this.#sameSite = sameSite\n    this.#secure = secure\n  }\n\n  /**\n   * The domain of the cookie.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#domaindomain-value)\n   */\n  get domain(): string | undefined {\n    return this.#domain\n  }\n\n  /**\n   * The expiration date of the cookie.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#expiresdate)\n   */\n  get expires(): Date | undefined {\n    return this.#expires\n  }\n\n  /**\n   * True if the cookie is HTTP-only.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#httponly)\n   *\n   * @default false\n   */\n  get httpOnly(): boolean {\n    return this.#httpOnly ?? false\n  }\n\n  /**\n   * The maximum age of the cookie in seconds.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#max-agenumber)\n   */\n  get maxAge(): number | undefined {\n    return this.#maxAge\n  }\n\n  /**\n   * The name of the cookie.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#cookie-namecookie-value)\n   */\n  get name(): string {\n    return this.#name\n  }\n\n  /**\n   * Extracts the value of this cookie from a `Cookie` header value.\n   *\n   * @param headerValue The `Cookie` header to parse\n   * @returns The value of this cookie, or `null` if it's not present\n   */\n  async parse(headerValue: string | null): Promise<string | null> {\n    if (!headerValue) return null\n\n    let header = new CookieHeader(headerValue)\n    if (!header.has(this.#name)) return null\n\n    let value = header.get(this.#name)!\n    if (value === '') return ''\n\n    let decoded = await decodeCookieValue(value, this.#secrets, this.#decode)\n    return decoded\n  }\n\n  /**\n   * True if the cookie is partitioned.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#partitioned)\n   *\n   * @default false\n   */\n  get partitioned(): boolean {\n    return this.#partitioned ?? false\n  }\n\n  /**\n   * The path of the cookie.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value)\n   *\n   * @default '/'\n   */\n  get path(): string {\n    return this.#path\n  }\n\n  /**\n   * The `SameSite` attribute of the cookie.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value)\n   *\n   * @default 'Lax'\n   */\n  get sameSite(): SameSiteValue {\n    return this.#sameSite\n  }\n\n  /**\n   * True if the cookie is secure (only sent over HTTPS).\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#secure)\n   *\n   * @default false\n   */\n  get secure(): boolean {\n    return this.#secure ?? false\n  }\n\n  /**\n   * Returns the value to use in a `Set-Cookie` header for this cookie.\n   *\n   * @param value The value to serialize\n   * @param props Additional properties to use when serializing the cookie\n   * @returns The `Set-Cookie` header value for this cookie\n   */\n  async serialize(value: string, props?: CookieProperties): Promise<string> {\n    let header = new SetCookieHeader({\n      name: this.#name,\n      value: value === '' ? '' : await encodeCookieValue(value, this.#secrets, this.#encode),\n      domain: this.#domain,\n      expires: this.#expires,\n      httpOnly: this.#httpOnly,\n      maxAge: this.#maxAge,\n      partitioned: this.#partitioned,\n      path: this.#path,\n      sameSite: this.#sameSite,\n      secure: this.#secure,\n      ...props,\n    })\n\n    return header.toString()\n  }\n\n  /**\n   * True if this cookie uses one or more secrets for verification.\n   */\n  get signed(): boolean {\n    return this.#secrets.length > 0\n  }\n}\n\n/**\n * Creates a new cookie object.\n *\n * @param name The name of the cookie\n * @param options Options for the cookie\n * @returns A new {@link Cookie} object\n */\nexport function createCookie(name: string, options?: CookieOptions): Cookie {\n  return new Cookie(name, options)\n}\n\nasync function decodeCookieValue(\n  value: string,\n  secrets: string[],\n  decode: Coder,\n): Promise<string | null> {\n  if (secrets.length > 0) {\n    for (let secret of secrets) {\n      let unsignedValue = await unsign(value, secret)\n      if (unsignedValue !== false) {\n        return decodeValue(unsignedValue, decode)\n      }\n    }\n\n    return null\n  }\n\n  return decodeValue(value, decode)\n}\n\nfunction decodeValue(value: string, decode: Coder): string | null {\n  try {\n    return decode(myEscape(atob(value)))\n  } catch {\n    return null\n  }\n}\n\n// See: https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.escape.js\nfunction myEscape(value: string): string {\n  let str = value.toString()\n  let result = ''\n  let index = 0\n  let chr, code\n  while (index < str.length) {\n    chr = str.charAt(index++)\n    if (/[\\w*+\\-./@]/.exec(chr)) {\n      result += chr\n    } else {\n      code = chr.charCodeAt(0)\n      if (code < 256) {\n        result += '%' + hex(code, 2)\n      } else {\n        result += '%u' + hex(code, 4).toUpperCase()\n      }\n    }\n  }\n  return result\n}\n\nfunction hex(code: number, length: number): string {\n  let result = code.toString(16)\n  while (result.length < length) result = '0' + result\n  return result\n}\n\nasync function encodeCookieValue(value: string, secrets: string[], encode: Coder): Promise<string> {\n  let encoded = encodeValue(value, encode)\n  if (secrets.length > 0) encoded = await sign(encoded, secrets[0])\n  return encoded\n}\n\nfunction encodeValue(value: string, encode: Coder): string {\n  return btoa(myUnescape(encode(value)))\n}\n\n// See: https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.unescape.js\nfunction myUnescape(value: string): string {\n  let str = value.toString()\n  let result = ''\n  let index = 0\n  let chr, part\n  while (index < str.length) {\n    chr = str.charAt(index++)\n    if (chr === '%') {\n      if (str.charAt(index) === 'u') {\n        part = str.slice(index + 1, index + 5)\n        if (/^[\\da-f]{4}$/i.exec(part)) {\n          result += String.fromCharCode(parseInt(part, 16))\n          index += 5\n          continue\n        }\n      } else {\n        part = str.slice(index, index + 2)\n        if (/^[\\da-f]{2}$/i.exec(part)) {\n          result += String.fromCharCode(parseInt(part, 16))\n          index += 2\n          continue\n        }\n      }\n    }\n    result += chr\n  }\n  return result\n}\n"
  },
  {
    "path": "packages/cookie/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/cookie/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/cop-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/cop-middleware/.changes/minor.initial-release.md",
    "content": "Add the initial release of `@remix-run/cop-middleware`.\n\n- Expose `cop(options)` for browser-focused cross-origin protection using `Sec-Fetch-Site`\n  with `Origin` fallback.\n- Support trusted origins, explicit insecure bypass patterns, and custom deny handlers.\n- Allow apps to layer `cop()` ahead of `session()` and `csrf()` when they want both\n  browser-origin filtering and token-backed CSRF protection.\n"
  },
  {
    "path": "packages/cop-middleware/CHANGELOG.md",
    "content": "# `cop-middleware` CHANGELOG\n\nThis is the changelog for [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware). It follows [semantic versioning](https://semver.org/).\n\n## v0.0.0\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/cop-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/cop-middleware/README.md",
    "content": "# cop-middleware\n\nCross-origin protection middleware for Remix. It mirrors Go's `CrossOriginProtection` by rejecting unsafe cross-origin browser requests without synchronizer tokens.\n\n## Features\n\n- **Browser Provenance Checks** - Uses `Sec-Fetch-Site` when present and falls back to `Origin`\n- **Trusted Origins** - Allow specific cross-origin callers by exact origin\n- **Explicit Escape Hatches** - Support insecure bypass patterns for endpoints like webhooks\n- **No Session State** - Does not require synchronizer tokens or server-side CSRF storage\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { cop } from 'remix/cop-middleware'\n\nlet router = createRouter({\n  middleware: [cop()],\n})\n```\n\n## Behavior\n\nFor unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`), `cop()` follows the same broad model as Go's `CrossOriginProtection`:\n\n- Allow `Sec-Fetch-Site: same-origin`\n- Allow `Sec-Fetch-Site: none`\n- Reject other `Sec-Fetch-Site` values unless the request matches a trusted origin or insecure bypass\n- If `Sec-Fetch-Site` is missing, compare `Origin` to the request host\n- If both `Sec-Fetch-Site` and `Origin` are missing, allow the request\n\nThis 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).\n\n## Caveats\n\n- `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.\n- 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.\n- If `Sec-Fetch-Site` is missing, `cop()` only rejects when `Origin` is present and does not match the request host.\n- 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.\n\n## Using with csrf-middleware\n\nYou can also layer `cop()` in front of `csrf()` when you want both browser provenance checks and session-backed synchronizer tokens.\n\n```ts\nimport { createCookie } from 'remix/cookie'\nimport { createRouter } from 'remix/fetch-router'\nimport { createCookieSessionStorage } from 'remix/session/cookie-storage'\nimport { session } from 'remix/session-middleware'\nimport { cop } from 'remix/cop-middleware'\nimport { csrf } from 'remix/csrf-middleware'\n\nlet sessionCookie = createCookie('__session', { secrets: ['secret1'] })\nlet sessionStorage = createCookieSessionStorage()\n\nlet router = createRouter({\n  middleware: [cop(), session(sessionCookie, sessionStorage), csrf()],\n})\n```\n\nIn 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.\n\n## Trusted Origins\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { cop } from 'remix/cop-middleware'\n\nlet router = createRouter({\n  middleware: [\n    cop({\n      trustedOrigins: ['https://admin.example.com'],\n    }),\n  ],\n})\n```\n\nTrusted origins must be exact origin values in the form `scheme://host[:port]`.\n\n## Insecure Bypass Patterns\n\nBypass patterns intentionally weaken protection for specific endpoints. They support:\n\n- Optional method prefixes, for example `POST /webhooks/{provider}`\n- Exact paths, for example `/healthz`\n- Trailing-slash subtree patterns, for example `/webhooks/`\n- Single-segment wildcards with `{name}`\n- Tail wildcards with `{name...}`\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { cop } from 'remix/cop-middleware'\n\nlet router = createRouter({\n  middleware: [\n    cop({\n      insecureBypassPatterns: ['POST /webhooks/{provider}', '/healthz'],\n    }),\n  ],\n})\n```\n\n## Related Packages\n\n- [`csrf-middleware`](https://github.com/remix-run/remix/tree/main/packages/csrf-middleware) - Session-backed CSRF protection with synchronizer tokens\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/cop-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/cop-middleware\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Middleware for tokenless cross-origin protection in Fetch API servers\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/cop-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/cop-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"csrf\",\n    \"cross-origin\",\n    \"sec-fetch-site\"\n  ]\n}\n"
  },
  {
    "path": "packages/cop-middleware/src/index.ts",
    "content": "export { cop, type CopDenyHandler, type CopFailureReason, type CopOptions } from './lib/cop.ts'\n"
  },
  {
    "path": "packages/cop-middleware/src/lib/cop.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createRouter } from '@remix-run/fetch-router'\n\nimport { cop } from './cop.ts'\n\nfunction createRequest(pathname: string, init?: RequestInit): Request {\n  return new Request(`https://example.com${pathname}`, init)\n}\n\nfunction createTestRouter(middleware: ReturnType<typeof cop>[]): ReturnType<typeof createRouter> {\n  let router = createRouter({ middleware })\n\n  router.get('/', () => new Response('ok'))\n  router.head('/', () => new Response(null, { status: 200 }))\n  router.options('/', () => new Response('ok'))\n  router.post('/', () => new Response('ok'))\n  router.put('/', () => new Response('ok'))\n  router.post('/bypass/', () => new Response('ok'))\n  router.post('/bypass/*path', () => new Response('ok'))\n  router.post('/only/:foo', () => new Response('ok'))\n  router.post('/no-trailing', () => new Response('ok'))\n  router.post('/yes-trailing/', () => new Response('ok'))\n  router.post('/post-only/', () => new Response('ok'))\n  router.post('/put-only/', () => new Response('ok'))\n  router.post('/get-only/', () => new Response('ok'))\n\n  return router\n}\n\ndescribe('cop middleware', () => {\n  it('allows same-origin requests for unsafe methods', async () => {\n    let router = createTestRouter([cop()])\n\n    let response = await router.fetch(\n      createRequest('/', {\n        method: 'POST',\n        headers: {\n          'Sec-Fetch-Site': 'same-origin',\n        },\n      }),\n    )\n\n    assert.equal(response.status, 200)\n  })\n\n  it('allows browser initiated top-level requests with Sec-Fetch-Site none', async () => {\n    let router = createTestRouter([cop()])\n\n    let response = await router.fetch(\n      createRequest('/', {\n        method: 'POST',\n        headers: {\n          'Sec-Fetch-Site': 'none',\n        },\n      }),\n    )\n\n    assert.equal(response.status, 200)\n  })\n\n  it('rejects unsafe cross-site requests from Sec-Fetch-Site', async () => {\n    let router = createTestRouter([cop()])\n\n    let response = await router.fetch(\n      createRequest('/', {\n        method: 'POST',\n        headers: {\n          'Sec-Fetch-Site': 'cross-site',\n        },\n      }),\n    )\n\n    assert.equal(response.status, 403)\n    assert.equal(\n      await response.text(),\n      'Forbidden: cross-origin request detected from Sec-Fetch-Site header',\n    )\n  })\n\n  it('rejects same-site requests to preserve origin-level guarantees', async () => {\n    let router = createTestRouter([cop()])\n\n    let response = await router.fetch(\n      createRequest('/', {\n        method: 'POST',\n        headers: {\n          'Sec-Fetch-Site': 'same-site',\n        },\n      }),\n    )\n\n    assert.equal(response.status, 403)\n  })\n\n  it('falls back to Origin when Sec-Fetch-Site is missing', async () => {\n    let router = createTestRouter([cop()])\n\n    let allowedResponse = await router.fetch(\n      createRequest('/', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://example.com',\n        },\n      }),\n    )\n\n    let deniedResponse = await router.fetch(\n      createRequest('/', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://attacker.example',\n        },\n      }),\n    )\n\n    assert.equal(allowedResponse.status, 200)\n    assert.equal(deniedResponse.status, 403)\n    assert.equal(\n      await deniedResponse.text(),\n      'Forbidden: cross-origin request detected, and/or browser is out of date: Sec-Fetch-Site is missing, and Origin does not match Host',\n    )\n  })\n\n  it('allows requests with no browser provenance headers', async () => {\n    let router = createTestRouter([cop()])\n\n    let response = await router.fetch(\n      createRequest('/', {\n        method: 'POST',\n      }),\n    )\n\n    assert.equal(response.status, 200)\n  })\n\n  it('allows safe methods even for cross-site requests', async () => {\n    let router = createTestRouter([cop()])\n\n    let response = await router.fetch(\n      createRequest('/', {\n        method: 'GET',\n        headers: {\n          'Sec-Fetch-Site': 'cross-site',\n        },\n      }),\n    )\n\n    assert.equal(response.status, 200)\n  })\n\n  it('supports trusted origins', async () => {\n    let router = createTestRouter([\n      cop({\n        trustedOrigins: ['https://trusted.example'],\n      }),\n    ])\n\n    let response = await router.fetch(\n      createRequest('/', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://trusted.example',\n          'Sec-Fetch-Site': 'cross-site',\n        },\n      }),\n    )\n\n    assert.equal(response.status, 200)\n  })\n\n  it('supports insecure bypass patterns', async () => {\n    let router = createTestRouter([\n      cop({\n        insecureBypassPatterns: ['/bypass/', '/only/{foo}', 'POST /post-only/'],\n      }),\n    ])\n\n    let bypassResponse = await router.fetch(\n      createRequest('/bypass/', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://attacker.example',\n          'Sec-Fetch-Site': 'cross-site',\n        },\n      }),\n    )\n\n    let wildcardResponse = await router.fetch(\n      createRequest('/only/123', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://attacker.example',\n        },\n      }),\n    )\n\n    let methodSpecificResponse = await router.fetch(\n      createRequest('/post-only/', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://attacker.example',\n        },\n      }),\n    )\n\n    assert.equal(bypassResponse.status, 200)\n    assert.equal(wildcardResponse.status, 200)\n    assert.equal(methodSpecificResponse.status, 200)\n  })\n\n  it('does not bypass paths that only differ by trailing slash or method', async () => {\n    let router = createTestRouter([\n      cop({\n        insecureBypassPatterns: [\n          '/no-trailing',\n          '/yes-trailing/',\n          'PUT /put-only/',\n          'GET /get-only/',\n        ],\n      }),\n    ])\n\n    let noTrailingResponse = await router.fetch(\n      createRequest('/no-trailing/', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://attacker.example',\n        },\n      }),\n    )\n\n    let yesTrailingResponse = await router.fetch(\n      createRequest('/yes-trailing', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://attacker.example',\n        },\n      }),\n    )\n\n    let putOnlyResponse = await router.fetch(\n      createRequest('/put-only/', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://attacker.example',\n        },\n      }),\n    )\n\n    let getOnlyResponse = await router.fetch(\n      createRequest('/get-only/', {\n        method: 'POST',\n        headers: {\n          Origin: 'https://attacker.example',\n        },\n      }),\n    )\n\n    assert.equal(noTrailingResponse.status, 403)\n    assert.equal(yesTrailingResponse.status, 403)\n    assert.equal(putOnlyResponse.status, 403)\n    assert.equal(getOnlyResponse.status, 403)\n  })\n\n  it('supports custom deny handlers', async () => {\n    let router = createTestRouter([\n      cop({\n        onDeny() {\n          return new Response('custom deny', { status: 418 })\n        },\n      }),\n    ])\n\n    let response = await router.fetch(\n      createRequest('/', {\n        method: 'POST',\n        headers: {\n          'Sec-Fetch-Site': 'cross-site',\n        },\n      }),\n    )\n\n    assert.equal(response.status, 418)\n    assert.equal(await response.text(), 'custom deny')\n  })\n\n  it('validates trusted origins and bypass patterns', async () => {\n    assert.throws(() => cop({ trustedOrigins: ['https://example.com/'] }))\n    assert.throws(() => cop({ trustedOrigins: ['null'] }))\n    assert.throws(() => cop({ insecureBypassPatterns: ['POST foo'] }))\n    assert.throws(() => cop({ insecureBypassPatterns: ['/foo/{...}/bar'] }))\n  })\n})\n"
  },
  {
    "path": "packages/cop-middleware/src/lib/cop.ts",
    "content": "import { RequestMethods } from '@remix-run/fetch-router'\nimport type { Middleware, RequestContext, RequestMethod } from '@remix-run/fetch-router'\n\nlet safeMethods: RequestMethod[] = ['GET', 'HEAD', 'OPTIONS']\n\ntype BypassSegment = { type: 'static'; value: string } | { type: 'wildcard' } | { type: 'rest' }\n\ninterface BypassPattern {\n  method: RequestMethod | null\n  pathname: string\n  segments: BypassSegment[]\n  matchesSubtree: boolean\n}\n\n/**\n * Reason reported when cross-origin protection rejects a request.\n */\nexport type CopFailureReason = 'cross-origin-request' | 'cross-origin-request-from-old-browser'\n\n/**\n * Custom response handler for rejected cross-origin requests.\n */\nexport interface CopDenyHandler {\n  /**\n   * Builds the response returned when a request is denied.\n   */\n  (reason: CopFailureReason, context: RequestContext): Response | Promise<Response>\n}\n\n/**\n * Configuration for the cross-origin protection middleware.\n */\nexport interface CopOptions {\n  /**\n   * Exact origins that should bypass cross-origin rejection.\n   */\n  trustedOrigins?: readonly string[]\n\n  /**\n   * Path patterns that should bypass protection for matching requests.\n   */\n  insecureBypassPatterns?: readonly string[]\n\n  /**\n   * Optional custom response handler for rejected requests.\n   */\n  onDeny?: CopDenyHandler\n}\n\nclass CrossOriginProtection {\n  #trustedOrigins = new Set<string>()\n  #insecureBypassPatterns: BypassPattern[] = []\n  #onDeny?: CopDenyHandler\n\n  constructor(options: CopOptions = {}) {\n    for (let trustedOrigin of options.trustedOrigins ?? []) {\n      this.addTrustedOrigin(trustedOrigin)\n    }\n\n    for (let insecureBypassPattern of options.insecureBypassPatterns ?? []) {\n      this.addInsecureBypassPattern(insecureBypassPattern)\n    }\n\n    this.setDenyHandler(options.onDeny)\n  }\n\n  addTrustedOrigin(origin: string): void {\n    this.#trustedOrigins.add(validateTrustedOrigin(origin))\n  }\n\n  addInsecureBypassPattern(pattern: string): void {\n    this.#insecureBypassPatterns.push(parseBypassPattern(pattern))\n  }\n\n  setDenyHandler(onDeny?: CopDenyHandler): void {\n    this.#onDeny = onDeny\n  }\n\n  check(context: RequestContext): CopFailureReason | null {\n    if (safeMethods.includes(context.method)) {\n      return null\n    }\n\n    let secFetchSite = getHeaderValue(context.headers, 'Sec-Fetch-Site')?.toLowerCase() ?? ''\n    switch (secFetchSite) {\n      case '':\n        break\n      case 'same-origin':\n      case 'none':\n        return null\n      default:\n        return this.#isRequestExempt(context) ? null : 'cross-origin-request'\n    }\n\n    let requestOrigin = getHeaderValue(context.headers, 'Origin')\n    if (requestOrigin == null) {\n      return null\n    }\n\n    let parsedOrigin = parseOrigin(requestOrigin)\n    if (parsedOrigin != null && parsedOrigin.host === context.url.host) {\n      return null\n    }\n\n    return this.#isRequestExempt(context) ? null : 'cross-origin-request-from-old-browser'\n  }\n\n  deny(reason: CopFailureReason, context: RequestContext): Response | Promise<Response> {\n    if (this.#onDeny) {\n      return this.#onDeny(reason, context)\n    }\n\n    return new Response(getDefaultErrorMessage(reason), { status: 403 })\n  }\n\n  #isRequestExempt(context: RequestContext): boolean {\n    for (let pattern of this.#insecureBypassPatterns) {\n      if (matchesBypassPattern(pattern, context)) {\n        return true\n      }\n    }\n\n    let requestOrigin = getHeaderValue(context.headers, 'Origin')\n    if (requestOrigin == null) {\n      return false\n    }\n\n    let normalizedOrigin = normalizeOrigin(requestOrigin)\n    return normalizedOrigin != null && this.#trustedOrigins.has(normalizedOrigin)\n  }\n}\n\n/**\n * Creates middleware that rejects unsafe cross-origin requests.\n *\n * @param options Cross-origin protection options.\n * @returns Middleware that validates request origin headers.\n */\nexport function cop(options: CopOptions = {}): Middleware {\n  let protection = new CrossOriginProtection(options)\n\n  return async (context, next) => {\n    let reason = protection.check(context)\n    if (reason == null) {\n      return next()\n    }\n\n    return protection.deny(reason, context)\n  }\n}\n\nfunction getDefaultErrorMessage(reason: CopFailureReason): string {\n  if (reason === 'cross-origin-request') {\n    return 'Forbidden: cross-origin request detected from Sec-Fetch-Site header'\n  }\n\n  return 'Forbidden: cross-origin request detected, and/or browser is out of date: Sec-Fetch-Site is missing, and Origin does not match Host'\n}\n\nfunction getHeaderValue(headers: Headers, name: string): string | null {\n  let value = headers.get(name)\n  if (value == null) {\n    return null\n  }\n\n  let trimmedValue = value.trim()\n  return trimmedValue === '' ? null : trimmedValue\n}\n\nfunction validateTrustedOrigin(origin: string): string {\n  let trimmedOrigin = origin.trim()\n  if (trimmedOrigin === '') {\n    throw new Error('trusted origin must not be empty')\n  }\n\n  if (trimmedOrigin.endsWith('/')) {\n    throw new Error(`invalid origin ${JSON.stringify(origin)}: trailing slash is not allowed`)\n  }\n\n  let parsedOrigin = parseOrigin(trimmedOrigin)\n  if (parsedOrigin == null) {\n    throw new Error(`invalid origin ${JSON.stringify(origin)}`)\n  }\n\n  if (parsedOrigin.pathname !== '/' || parsedOrigin.search !== '' || parsedOrigin.hash !== '') {\n    throw new Error(\n      `invalid origin ${JSON.stringify(origin)}: path, query, and fragment are not allowed`,\n    )\n  }\n\n  return serializeOrigin(parsedOrigin)\n}\n\nfunction normalizeOrigin(origin: string): string | null {\n  let parsedOrigin = parseOrigin(origin)\n  return parsedOrigin == null ? null : serializeOrigin(parsedOrigin)\n}\n\nfunction parseOrigin(origin: string): URL | null {\n  try {\n    let parsedOrigin = new URL(origin)\n    if (parsedOrigin.host === '') {\n      return null\n    }\n\n    if (parsedOrigin.username !== '' || parsedOrigin.password !== '') {\n      return null\n    }\n\n    return parsedOrigin\n  } catch {\n    return null\n  }\n}\n\nfunction serializeOrigin(origin: URL): string {\n  return `${origin.protocol}//${origin.host}`\n}\n\nfunction parseBypassPattern(pattern: string): BypassPattern {\n  let trimmedPattern = pattern.trim()\n  if (trimmedPattern === '') {\n    throw new Error('bypass pattern must not be empty')\n  }\n\n  let method: RequestMethod | null = null\n  let pathname = trimmedPattern\n  let methodPattern = /^([A-Z]+)\\s+(.+)$/.exec(trimmedPattern)\n\n  if (methodPattern != null && methodPattern[2].startsWith('/')) {\n    let maybeMethod = methodPattern[1] as RequestMethod\n    if (!RequestMethods.includes(maybeMethod)) {\n      throw new Error(`invalid request method in bypass pattern ${JSON.stringify(pattern)}`)\n    }\n\n    method = maybeMethod\n    pathname = methodPattern[2]\n  }\n\n  if (!pathname.startsWith('/')) {\n    throw new Error(`invalid bypass pattern ${JSON.stringify(pattern)}: path must start with \"/\"`)\n  }\n\n  if (pathname.includes('?') || pathname.includes('#')) {\n    throw new Error(\n      `invalid bypass pattern ${JSON.stringify(pattern)}: query strings and fragments are not supported`,\n    )\n  }\n\n  let matchesSubtree = pathname.endsWith('/')\n  let normalizedPathname =\n    pathname.length > 1 && matchesSubtree ? pathname.slice(0, pathname.length - 1) : pathname\n  let rawSegments = normalizedPathname === '/' ? [] : normalizedPathname.slice(1).split('/')\n  let segments = rawSegments.map((segment, index) =>\n    parseBypassSegment(pattern, segment, index === rawSegments.length - 1),\n  )\n\n  return { method, pathname, segments, matchesSubtree }\n}\n\nfunction parseBypassSegment(\n  pattern: string,\n  segment: string,\n  isLastSegment: boolean,\n): BypassSegment {\n  if (segment === '') {\n    throw new Error(\n      `invalid bypass pattern ${JSON.stringify(pattern)}: empty path segments are not allowed`,\n    )\n  }\n\n  if (!segment.startsWith('{') || !segment.endsWith('}')) {\n    return { type: 'static', value: segment }\n  }\n\n  let wildcardName = segment.slice(1, segment.length - 1)\n  if (wildcardName === '') {\n    throw new Error(\n      `invalid bypass pattern ${JSON.stringify(pattern)}: empty wildcards are not allowed`,\n    )\n  }\n\n  if (wildcardName === '$') {\n    throw new Error(\n      `invalid bypass pattern ${JSON.stringify(pattern)}: \"{$}\" is not supported in cop-middleware`,\n    )\n  }\n\n  if (wildcardName.endsWith('...')) {\n    if (!isLastSegment) {\n      throw new Error(\n        `invalid bypass pattern ${JSON.stringify(pattern)}: tail wildcards must be last`,\n      )\n    }\n\n    if (wildcardName.length === 3) {\n      throw new Error(\n        `invalid bypass pattern ${JSON.stringify(pattern)}: tail wildcards require a name`,\n      )\n    }\n\n    return { type: 'rest' }\n  }\n\n  return { type: 'wildcard' }\n}\n\nfunction matchesBypassPattern(pattern: BypassPattern, context: RequestContext): boolean {\n  if (pattern.method != null && pattern.method !== context.method) {\n    return false\n  }\n\n  let pathname = context.url.pathname\n  let hasTrailingSlash = pathname.length > 1 && pathname.endsWith('/')\n  let normalizedPathname =\n    pathname.length > 1 && hasTrailingSlash ? pathname.slice(0, pathname.length - 1) : pathname\n  let pathSegments = normalizedPathname === '/' ? [] : normalizedPathname.slice(1).split('/')\n\n  let segmentIndex = 0\n  while (segmentIndex < pattern.segments.length) {\n    let pathSegment = pathSegments[segmentIndex]\n    let segment = pattern.segments[segmentIndex]\n\n    if (segment.type === 'rest') {\n      return true\n    }\n\n    if (pathSegment == null) {\n      return false\n    }\n\n    if (segment.type === 'static' && segment.value !== pathSegment) {\n      return false\n    }\n\n    segmentIndex++\n  }\n\n  if (pattern.matchesSubtree) {\n    if (pathSegments.length === pattern.segments.length) {\n      return pattern.pathname === '/' || hasTrailingSlash\n    }\n\n    return pathSegments.length > pattern.segments.length\n  }\n\n  return pathSegments.length === pattern.segments.length && !hasTrailingSlash\n}\n"
  },
  {
    "path": "packages/cop-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/cop-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/cors-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/cors-middleware/.changes/minor.initial-release.md",
    "content": "Add the initial release of `@remix-run/cors-middleware`.\n\n- Expose `cors(options)` for standard CORS response headers and preflight handling in Fetch API servers.\n- Support static and dynamic origin policies, credentialed requests, allowed and exposed headers, preflight max-age, and private network preflights.\n- Allow apps to either short-circuit preflight requests or continue them into custom `OPTIONS` handlers.\n"
  },
  {
    "path": "packages/cors-middleware/CHANGELOG.md",
    "content": "# `cors-middleware` CHANGELOG\n\nThis is the changelog for [`cors-middleware`](https://github.com/remix-run/remix/tree/main/packages/cors-middleware). It follows [semantic versioning](https://semver.org/).\n\n## v0.0.0\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/cors-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/cors-middleware/README.md",
    "content": "# cors-middleware\n\nCORS 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.\n\n## Features\n\n- **Preflight Handling** - Automatically handles `OPTIONS` preflight requests\n- **Flexible Origin Rules** - Supports static, regex, list, and function-based origin policies\n- **Credential Support** - Supports credentialed requests with spec-safe origin reflection\n- **Header Controls** - Configure allowed and exposed headers, preflight methods, and max age\n- **Private Network Support** - Optionally allow private network preflight requests\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { cors } from 'remix/cors-middleware'\n\nlet router = createRouter({\n  middleware: [\n    cors({\n      origin: ['https://app.example.com', 'https://admin.example.com'],\n      credentials: true,\n      exposedHeaders: ['X-Request-Id'],\n    }),\n  ],\n})\n\nrouter.get('/api/projects', () => {\n  return Response.json([{ id: 'p1', name: 'Remix' }], {\n    headers: {\n      'X-Request-Id': 'req_123',\n    },\n  })\n})\n```\n\n## Origin Policies\n\n`origin` supports:\n\n- `'*'` to allow all origins\n- `string` for a single exact origin\n- `RegExp` for pattern-based matching\n- `Array<string | RegExp>` for multiple exact and pattern matches\n- `true` to reflect the request origin\n- `(origin, context) => boolean | string` for dynamic policies\n\n### Restrict Origins\n\n```ts\nlet router = createRouter({\n  middleware: [\n    cors({\n      origin: ['https://app.example.com', 'https://admin.example.com'],\n      credentials: true,\n    }),\n  ],\n})\n```\n\n### Dynamic Origin Policies\n\n```ts\nlet router = createRouter({\n  middleware: [\n    cors({\n      origin(origin, context) {\n        if (context.url.pathname.startsWith('/public/')) {\n          return '*'\n        }\n\n        return origin.endsWith('.trusted.example')\n      },\n    }),\n  ],\n})\n```\n\n## Preflight Behavior\n\nBy default, preflight requests are short-circuited with status `204`.\n\n```ts\nlet router = createRouter({\n  middleware: [\n    cors({\n      methods: ['GET', 'POST', 'PATCH'],\n      allowedHeaders: ['Authorization', 'Content-Type'],\n      maxAge: 600,\n    }),\n  ],\n})\n```\n\nUse a function-based `allowedHeaders` policy when the header allowlist depends on the request:\n\n```ts\nlet router = createRouter({\n  middleware: [\n    cors({\n      allowedHeaders(request) {\n        let requestedHeaders = request.headers.get('Access-Control-Request-Headers')\n\n        if (requestedHeaders?.includes('x-admin-token')) {\n          return ['Authorization', 'Content-Type', 'X-Admin-Token']\n        }\n\n        return ['Authorization', 'Content-Type']\n      },\n    }),\n  ],\n})\n```\n\nFunction-based `allowedHeaders` responses vary on `Access-Control-Request-Headers`, so caches do not reuse a preflight response for a different requested-header set.\n\nSet `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`.\n\n## Private Network Preflights\n\n```ts\nlet router = createRouter({\n  middleware: [\n    cors({\n      allowPrivateNetwork: true,\n    }),\n  ],\n})\n```\n\nWhen `allowPrivateNetwork` is enabled, the middleware adds `Access-Control-Allow-Private-Network: true` for preflight requests that ask for private network access.\n\n## Expose Response Headers\n\n```ts\nlet router = createRouter({\n  middleware: [\n    cors({\n      exposedHeaders: ['X-Request-Id', 'X-Trace-Id'],\n    }),\n  ],\n})\n```\n\n## Caveats\n\n- CORS is primarily a browser enforcement mechanism. Disallowed non-preflight requests still reach your handlers unless you add separate request validation.\n- When `credentials: true` is used with `origin: '*'`, the middleware reflects the request origin and adds `Vary: Origin` so the response stays cache-safe.\n- 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.\n- `preflightContinue` and `preflightStatusCode` only affect how preflight `OPTIONS` requests are handled. They do not change actual request authorization.\n\n## Related Packages\n\n- [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) - Browser-origin protection middleware for unsafe cross-origin requests\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - Typed HTTP header utilities\n\n## Related Work\n\n- [MDN: Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\n- [Fetch Standard: CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol)\n- [expressjs/cors](https://github.com/expressjs/cors)\n- [rack-cors](https://github.com/cyu/rack-cors)\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/cors-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/cors-middleware\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Middleware for handling CORS in Fetch API servers\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/cors-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/cors-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/headers\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\",\n    \"@remix-run/headers\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"cors\",\n    \"cross-origin\",\n    \"preflight\"\n  ]\n}\n"
  },
  {
    "path": "packages/cors-middleware/src/index.ts",
    "content": "export {\n  cors,\n  type CorsAllowedHeadersResolver,\n  type CorsAllowedHeadersResolverResult,\n  type CorsOptions,\n  type CorsOrigin,\n  type CorsOriginResolver,\n  type CorsOriginResolverResult,\n} from './lib/cors.ts'\n"
  },
  {
    "path": "packages/cors-middleware/src/lib/cors.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createRouter } from '@remix-run/fetch-router'\nimport { Vary } from '@remix-run/headers'\n\nimport { cors } from './cors.ts'\n\ndescribe('cors middleware', () => {\n  it('adds wildcard CORS response headers by default', async () => {\n    let router = createRouter({\n      middleware: [cors()],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let response = await router.fetch('https://remix.run/', {\n      headers: {\n        Origin: 'https://example.com',\n      },\n    })\n\n    assert.equal(response.status, 200)\n    assert.equal(response.headers.get('Access-Control-Allow-Origin'), '*')\n    assert.equal(response.headers.get('Vary'), null)\n  })\n\n  it('reflects origin and adds Vary when credentials are enabled', async () => {\n    let router = createRouter({\n      middleware: [cors({ credentials: true })],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let response = await router.fetch('https://remix.run/', {\n      headers: {\n        Origin: 'https://example.com',\n      },\n    })\n\n    assert.equal(response.headers.get('Access-Control-Allow-Origin'), 'https://example.com')\n    assert.equal(response.headers.get('Access-Control-Allow-Credentials'), 'true')\n\n    let vary = Vary.from(response.headers.get('Vary'))\n    assert.ok(vary.has('Origin'))\n  })\n\n  it('short-circuits preflight requests', async () => {\n    let router = createRouter({\n      middleware: [cors()],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'OPTIONS',\n      headers: {\n        Origin: 'https://example.com',\n        'Access-Control-Request-Method': 'PATCH',\n        'Access-Control-Request-Headers': 'x-api-key,content-type',\n      },\n    })\n\n    assert.equal(response.status, 204)\n    assert.equal(response.headers.get('Access-Control-Allow-Origin'), '*')\n    assert.equal(\n      response.headers.get('Access-Control-Allow-Methods'),\n      'GET, HEAD, PUT, PATCH, POST, DELETE',\n    )\n    assert.equal(response.headers.get('Access-Control-Allow-Headers'), 'x-api-key,content-type')\n\n    let vary = Vary.from(response.headers.get('Vary'))\n    assert.ok(vary.has('Access-Control-Request-Method'))\n    assert.ok(vary.has('Access-Control-Request-Headers'))\n  })\n\n  it('continues preflight requests when preflightContinue is true', async () => {\n    let router = createRouter({\n      middleware: [cors({ preflightContinue: true })],\n    })\n\n    router.options('/', () => new Response('continued'))\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'OPTIONS',\n      headers: {\n        Origin: 'https://example.com',\n        'Access-Control-Request-Method': 'POST',\n      },\n    })\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'continued')\n    assert.equal(response.headers.get('Access-Control-Allow-Origin'), '*')\n    assert.equal(\n      response.headers.get('Access-Control-Allow-Methods'),\n      'GET, HEAD, PUT, PATCH, POST, DELETE',\n    )\n  })\n\n  it('blocks disallowed origins in preflight requests', async () => {\n    let router = createRouter({\n      middleware: [cors({ origin: 'https://allowed.example' })],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'OPTIONS',\n      headers: {\n        Origin: 'https://blocked.example',\n        'Access-Control-Request-Method': 'POST',\n      },\n    })\n\n    assert.equal(response.status, 403)\n    assert.equal(response.headers.get('Access-Control-Allow-Origin'), null)\n  })\n\n  it('supports function-based dynamic origin checks', async () => {\n    let router = createRouter({\n      middleware: [\n        cors({\n          origin: (origin) => origin.endsWith('.trusted.example'),\n        }),\n      ],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let allowedResponse = await router.fetch('https://remix.run/', {\n      headers: {\n        Origin: 'https://api.trusted.example',\n      },\n    })\n\n    let blockedResponse = await router.fetch('https://remix.run/', {\n      headers: {\n        Origin: 'https://evil.example',\n      },\n    })\n\n    assert.equal(\n      allowedResponse.headers.get('Access-Control-Allow-Origin'),\n      'https://api.trusted.example',\n    )\n    assert.equal(blockedResponse.headers.get('Access-Control-Allow-Origin'), null)\n  })\n\n  it('matches regex origins consistently across repeated requests', async () => {\n    let router = createRouter({\n      middleware: [cors({ origin: /\\.trusted\\.example$/g })],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let firstResponse = await router.fetch('https://remix.run/', {\n      headers: {\n        Origin: 'https://api.trusted.example',\n      },\n    })\n\n    let secondResponse = await router.fetch('https://remix.run/', {\n      headers: {\n        Origin: 'https://api.trusted.example',\n      },\n    })\n\n    assert.equal(\n      firstResponse.headers.get('Access-Control-Allow-Origin'),\n      'https://api.trusted.example',\n    )\n    assert.equal(\n      secondResponse.headers.get('Access-Control-Allow-Origin'),\n      'https://api.trusted.example',\n    )\n  })\n\n  it('supports explicit allowed headers and max-age for preflight requests', async () => {\n    let router = createRouter({\n      middleware: [\n        cors({\n          allowedHeaders: ['Authorization', 'Content-Type'],\n          maxAge: 600,\n        }),\n      ],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'OPTIONS',\n      headers: {\n        Origin: 'https://example.com',\n        'Access-Control-Request-Method': 'POST',\n        'Access-Control-Request-Headers': 'x-custom-header',\n      },\n    })\n\n    assert.equal(response.status, 204)\n    assert.equal(\n      response.headers.get('Access-Control-Allow-Headers'),\n      'Authorization, Content-Type',\n    )\n    assert.equal(response.headers.get('Access-Control-Max-Age'), '600')\n\n    let vary = Vary.from(response.headers.get('Vary'))\n    assert.ok(!vary.has('Access-Control-Request-Headers'))\n  })\n\n  it('adds Vary for function-based allowed header resolvers', async () => {\n    let router = createRouter({\n      middleware: [\n        cors({\n          allowedHeaders: () => ['Authorization'],\n        }),\n      ],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'OPTIONS',\n      headers: {\n        Origin: 'https://example.com',\n        'Access-Control-Request-Method': 'POST',\n        'Access-Control-Request-Headers': 'x-api-key',\n      },\n    })\n\n    assert.equal(response.status, 204)\n    assert.equal(response.headers.get('Access-Control-Allow-Headers'), 'Authorization')\n\n    let vary = Vary.from(response.headers.get('Vary'))\n    assert.ok(vary.has('Access-Control-Request-Headers'))\n  })\n\n  it('falls back to requested headers for function-based allowed header resolvers', async () => {\n    let router = createRouter({\n      middleware: [\n        cors({\n          allowedHeaders: () => undefined,\n        }),\n      ],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'OPTIONS',\n      headers: {\n        Origin: 'https://example.com',\n        'Access-Control-Request-Method': 'POST',\n        'Access-Control-Request-Headers': 'x-api-key,content-type',\n      },\n    })\n\n    assert.equal(response.status, 204)\n    assert.equal(response.headers.get('Access-Control-Allow-Headers'), 'x-api-key,content-type')\n\n    let vary = Vary.from(response.headers.get('Vary'))\n    assert.ok(vary.has('Access-Control-Request-Headers'))\n  })\n\n  it('sets Access-Control-Allow-Private-Network when requested', async () => {\n    let router = createRouter({\n      middleware: [cors({ allowPrivateNetwork: true })],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'OPTIONS',\n      headers: {\n        Origin: 'https://example.com',\n        'Access-Control-Request-Method': 'POST',\n        'Access-Control-Request-Private-Network': 'true',\n      },\n    })\n\n    assert.equal(response.status, 204)\n    assert.equal(response.headers.get('Access-Control-Allow-Private-Network'), 'true')\n  })\n\n  it('sets Access-Control-Expose-Headers for actual requests', async () => {\n    let router = createRouter({\n      middleware: [cors({ exposedHeaders: ['X-Request-Id', 'X-Trace-Id'] })],\n    })\n\n    router.get('/', () => new Response('ok'))\n\n    let response = await router.fetch('https://remix.run/', {\n      headers: {\n        Origin: 'https://example.com',\n      },\n    })\n\n    assert.equal(response.status, 200)\n    assert.equal(response.headers.get('Access-Control-Expose-Headers'), 'X-Request-Id, X-Trace-Id')\n  })\n\n  it('merges CORS Vary values with an existing response Vary header', async () => {\n    let router = createRouter({\n      middleware: [cors({ credentials: true })],\n    })\n\n    router.get(\n      '/',\n      () =>\n        new Response('ok', {\n          headers: {\n            Vary: 'Accept-Encoding',\n          },\n        }),\n    )\n\n    let response = await router.fetch('https://remix.run/', {\n      headers: {\n        Origin: 'https://example.com',\n      },\n    })\n\n    let vary = Vary.from(response.headers.get('Vary'))\n    assert.ok(vary.has('Accept-Encoding'))\n    assert.ok(vary.has('Origin'))\n  })\n})\n"
  },
  {
    "path": "packages/cors-middleware/src/lib/cors.ts",
    "content": "import type { Middleware, RequestContext } from '@remix-run/fetch-router'\nimport { Vary } from '@remix-run/headers'\n\nlet defaultCorsMethods = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']\n\ntype OriginMatcher = string | RegExp | ReadonlyArray<string | RegExp>\n\n/**\n * Return shape for a dynamic CORS origin resolver.\n */\nexport type CorsOriginResolverResult = '*' | string | boolean | null | undefined\n\n/**\n * Resolves the allowed origin for a given request origin.\n */\nexport interface CorsOriginResolver {\n  /**\n   * Resolves the allowed origin for a request with an `Origin` header.\n   */\n  (\n    origin: string,\n    context: RequestContext,\n  ): CorsOriginResolverResult | Promise<CorsOriginResolverResult>\n}\n\n/**\n * Accepted forms for configuring allowed CORS origins.\n */\nexport type CorsOrigin = OriginMatcher | boolean | CorsOriginResolver\n\n/**\n * Return shape for a dynamic allowed-headers resolver.\n */\nexport type CorsAllowedHeadersResolverResult = readonly string[] | null | undefined\n\n/**\n * Resolves the allowed request headers for a preflight request.\n */\nexport interface CorsAllowedHeadersResolver {\n  /**\n   * Resolves the request headers allowed by a preflight request.\n   */\n  (\n    request: Request,\n    context: RequestContext,\n  ): CorsAllowedHeadersResolverResult | Promise<CorsAllowedHeadersResolverResult>\n}\n\n/**\n * Options for CORS middleware.\n */\nexport interface CorsOptions {\n  /**\n   * Allowed origins. Defaults to '*'.\n   *\n   * - `true` reflects the request Origin\n   * - `false` disables CORS headers\n   * - `'*'` allows all origins\n   * - `string`/`RegExp`/array allow matching origins\n   * - `function` allows dynamic origin checks\n   */\n  origin?: CorsOrigin\n\n  /**\n   * Allowed methods for preflight responses.\n   *\n   * @default ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']\n   */\n  methods?: readonly string[]\n\n  /**\n   * Allowed request headers for preflight responses.\n   *\n   * Defaults to reflecting Access-Control-Request-Headers.\n   */\n  allowedHeaders?: readonly string[] | CorsAllowedHeadersResolver\n\n  /**\n   * Exposed response headers for non-preflight requests.\n   */\n  exposedHeaders?: readonly string[]\n\n  /**\n   * Include Access-Control-Allow-Credentials: true.\n   *\n   * @default false\n   */\n  credentials?: boolean\n\n  /**\n   * Access-Control-Max-Age value for preflight responses (seconds).\n   */\n  maxAge?: number\n\n  /**\n   * Continue to downstream handlers for preflight requests.\n   *\n   * @default false\n   */\n  preflightContinue?: boolean\n\n  /**\n   * Status code to use when short-circuiting preflight responses.\n   *\n   * @default 204\n   */\n  preflightStatusCode?: number\n\n  /**\n   * Include Access-Control-Allow-Private-Network: true when requested.\n   *\n   * @default false\n   */\n  allowPrivateNetwork?: boolean\n}\n\ntype ResolvedAllowedHeaders = {\n  headerValue: string | null\n  varyOnRequestHeaders: boolean\n}\n\n/**\n * Middleware that adds CORS headers and handles CORS preflight requests.\n *\n * @param options CORS options\n * @returns CORS middleware\n */\nexport function cors(options: CorsOptions = {}): Middleware {\n  let methods = normalizeMethodList(options.methods ?? defaultCorsMethods)\n  let exposedHeaders = options.exposedHeaders ? normalizeHeaderList(options.exposedHeaders) : ''\n  let allowCredentials = options.credentials ?? false\n  let preflightContinue = options.preflightContinue ?? false\n  let preflightStatusCode = options.preflightStatusCode ?? 204\n\n  return async (context, next) => {\n    let requestOrigin = context.headers.get('Origin')\n    let preflightRequest = isPreflightRequest(context)\n\n    if (requestOrigin == null) {\n      if (preflightRequest && !preflightContinue) {\n        return new Response(null, { status: preflightStatusCode })\n      }\n\n      return next()\n    }\n\n    let allowedOrigin = await resolveAllowedOrigin(requestOrigin, context, options.origin)\n    if (allowedOrigin == null) {\n      if (preflightRequest && !preflightContinue) {\n        return new Response(null, { status: 403 })\n      }\n\n      return next()\n    }\n\n    let corsHeaders = new Headers()\n    let vary = new Vary()\n\n    let allowOriginHeader = allowedOrigin\n    if (allowCredentials && allowedOrigin === '*') {\n      allowOriginHeader = requestOrigin\n    }\n\n    corsHeaders.set('Access-Control-Allow-Origin', allowOriginHeader)\n\n    if (allowOriginHeader !== '*') {\n      vary.add('Origin')\n    }\n\n    if (allowCredentials) {\n      corsHeaders.set('Access-Control-Allow-Credentials', 'true')\n    }\n\n    if (preflightRequest) {\n      corsHeaders.set('Access-Control-Allow-Methods', methods)\n      vary.add('Access-Control-Request-Method')\n\n      let allowedHeaders = await resolveAllowedHeaders(context, options.allowedHeaders)\n      if (allowedHeaders.headerValue != null) {\n        corsHeaders.set('Access-Control-Allow-Headers', allowedHeaders.headerValue)\n      }\n\n      if (allowedHeaders.varyOnRequestHeaders) {\n        vary.add('Access-Control-Request-Headers')\n      }\n\n      if (options.maxAge != null) {\n        let maxAge = Math.max(0, Math.floor(options.maxAge))\n        corsHeaders.set('Access-Control-Max-Age', String(maxAge))\n      }\n\n      if (\n        options.allowPrivateNetwork &&\n        context.headers.get('Access-Control-Request-Private-Network')?.toLowerCase() === 'true'\n      ) {\n        corsHeaders.set('Access-Control-Allow-Private-Network', 'true')\n        vary.add('Access-Control-Request-Private-Network')\n      }\n\n      if (!preflightContinue) {\n        if (vary.size > 0) {\n          corsHeaders.set('Vary', vary.toString())\n        }\n\n        return new Response(null, {\n          status: preflightStatusCode,\n          headers: corsHeaders,\n        })\n      }\n    } else if (exposedHeaders !== '') {\n      corsHeaders.set('Access-Control-Expose-Headers', exposedHeaders)\n    }\n\n    let response = await next()\n\n    return withCorsHeaders(response, corsHeaders, vary)\n  }\n}\n\nfunction isPreflightRequest(context: RequestContext): boolean {\n  return context.method === 'OPTIONS' && context.headers.has('Access-Control-Request-Method')\n}\n\nfunction normalizeMethodList(methods: readonly string[]): string {\n  let normalized: string[] = []\n\n  for (let method of methods) {\n    let value = method.trim().toUpperCase()\n    if (value === '') {\n      continue\n    }\n\n    if (normalized.includes(value)) {\n      continue\n    }\n\n    normalized.push(value)\n  }\n\n  return normalized.join(', ')\n}\n\nfunction normalizeHeaderList(headerNames: readonly string[]): string {\n  let normalized: string[] = []\n\n  for (let headerName of headerNames) {\n    let value = headerName.trim()\n    if (value === '') {\n      continue\n    }\n\n    let duplicate = normalized.some((existing) => existing.toLowerCase() === value.toLowerCase())\n    if (duplicate) {\n      continue\n    }\n\n    normalized.push(value)\n  }\n\n  return normalized.join(', ')\n}\n\nasync function resolveAllowedOrigin(\n  requestOrigin: string,\n  context: RequestContext,\n  configuredOrigin: CorsOrigin | undefined,\n): Promise<string | '*' | null> {\n  let origin = configuredOrigin ?? '*'\n\n  if (typeof origin === 'function') {\n    let result = await origin(requestOrigin, context)\n    return normalizeResolvedOrigin(result, requestOrigin)\n  }\n\n  if (origin === true) {\n    return requestOrigin\n  }\n\n  if (origin === false) {\n    return null\n  }\n\n  if (typeof origin === 'string') {\n    if (origin === '*') {\n      return '*'\n    }\n\n    return origin === requestOrigin ? requestOrigin : null\n  }\n\n  if (origin instanceof RegExp) {\n    return matchesOriginPattern(origin, requestOrigin) ? requestOrigin : null\n  }\n\n  for (let allowed of origin) {\n    if (allowed === '*') {\n      return '*'\n    }\n\n    if (typeof allowed === 'string' && allowed === requestOrigin) {\n      return requestOrigin\n    }\n\n    if (allowed instanceof RegExp && matchesOriginPattern(allowed, requestOrigin)) {\n      return requestOrigin\n    }\n  }\n\n  return null\n}\n\nfunction matchesOriginPattern(pattern: RegExp, requestOrigin: string): boolean {\n  let normalizedPattern = new RegExp(pattern.source, pattern.flags)\n  return normalizedPattern.test(requestOrigin)\n}\n\nfunction normalizeResolvedOrigin(\n  resolved: CorsOriginResolverResult,\n  requestOrigin: string,\n): string | '*' | null {\n  if (resolved == null || resolved === false) {\n    return null\n  }\n\n  if (resolved === true) {\n    return requestOrigin\n  }\n\n  if (resolved === '*') {\n    return '*'\n  }\n\n  return resolved\n}\n\nasync function resolveAllowedHeaders(\n  context: RequestContext,\n  configuredAllowedHeaders: readonly string[] | CorsAllowedHeadersResolver | undefined,\n): Promise<ResolvedAllowedHeaders> {\n  if (Array.isArray(configuredAllowedHeaders)) {\n    let headerValue = normalizeHeaderList(configuredAllowedHeaders)\n\n    return {\n      headerValue: headerValue === '' ? null : headerValue,\n      varyOnRequestHeaders: false,\n    }\n  }\n\n  if (typeof configuredAllowedHeaders === 'function') {\n    let resolved = await configuredAllowedHeaders(context.request, context)\n    if (resolved != null) {\n      let headerValue = normalizeHeaderList(resolved)\n\n      return {\n        headerValue: headerValue === '' ? null : headerValue,\n        varyOnRequestHeaders: true,\n      }\n    }\n\n    let requestedHeaders = context.headers.get('Access-Control-Request-Headers')\n    if (requestedHeaders == null || requestedHeaders.trim() === '') {\n      return {\n        headerValue: null,\n        varyOnRequestHeaders: true,\n      }\n    }\n\n    return {\n      headerValue: requestedHeaders,\n      varyOnRequestHeaders: true,\n    }\n  }\n\n  let requestedHeaders = context.headers.get('Access-Control-Request-Headers')\n  if (requestedHeaders == null || requestedHeaders.trim() === '') {\n    return {\n      headerValue: null,\n      varyOnRequestHeaders: false,\n    }\n  }\n\n  return {\n    headerValue: requestedHeaders,\n    varyOnRequestHeaders: true,\n  }\n}\n\nfunction withCorsHeaders(response: Response, corsHeaders: Headers, vary: Vary): Response {\n  let responseHeaders = new Headers(response.headers)\n\n  for (let [headerName, headerValue] of corsHeaders) {\n    responseHeaders.set(headerName, headerValue)\n  }\n\n  if (vary.size > 0) {\n    let responseVary = Vary.from(responseHeaders.get('Vary'))\n    vary.forEach((headerName) => responseVary.add(headerName))\n    responseHeaders.set('Vary', responseVary.toString())\n  }\n\n  return new Response(response.body, {\n    status: response.status,\n    statusText: response.statusText,\n    headers: responseHeaders,\n  })\n}\n"
  },
  {
    "path": "packages/cors-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/cors-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/csrf-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/csrf-middleware/.changes/minor.initial-release.md",
    "content": "Add the initial release of `@remix-run/csrf-middleware`.\n\n- Expose `csrf(options)` and `getCsrfToken(context)` for session-backed CSRF protection in\n  Remix apps that accept unsafe form submissions.\n- Validate a per-session token together with request origin metadata, with support for token\n  transport in headers, form data, and query params.\n- Allow apps to layer `csrf()` after `cop()` when they need stricter token-backed protection\n  on top of browser-origin filtering.\n"
  },
  {
    "path": "packages/csrf-middleware/CHANGELOG.md",
    "content": "# `csrf-middleware` CHANGELOG\n\nThis is the changelog for [`csrf-middleware`](https://github.com/remix-run/remix/tree/main/packages/csrf-middleware). It follows [semantic versioning](https://semver.org/).\n\n## v0.0.0\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/csrf-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/csrf-middleware/README.md",
    "content": "# csrf-middleware\n\nCSRF protection middleware for Remix. It provides synchronizer-token validation backed by session storage, plus origin checks for unsafe requests.\n\n## Features\n\n- **Session-Backed Tokens** - Creates and persists CSRF tokens in the request session\n- **Flexible Token Extraction** - Reads tokens from headers, form fields, query params, or a custom resolver\n- **Origin Validation** - Validates `Origin`/`Referer` for unsafe methods with customizable policies\n- **Configurable Enforcement** - Control safe methods, token keys, and failure responses\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nThis middleware requires [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) to run before it.\n\n```ts\nimport { createCookie } from 'remix/cookie'\nimport { createRouter } from 'remix/fetch-router'\nimport { createCookieSessionStorage } from 'remix/session/cookie-storage'\nimport { session } from 'remix/session-middleware'\nimport { csrf, getCsrfToken } from 'remix/csrf-middleware'\n\nlet sessionCookie = createCookie('__session', { secrets: ['secret1'] })\nlet sessionStorage = createCookieSessionStorage()\n\nlet router = createRouter({\n  middleware: [session(sessionCookie, sessionStorage), csrf()],\n})\n\nrouter.get('/form', (context) => {\n  let token = getCsrfToken(context)\n\n  return new Response(`\n    <form method=\"post\" action=\"/submit\">\n      <input type=\"hidden\" name=\"_csrf\" value=\"${token}\" />\n      <button type=\"submit\">Submit</button>\n    </form>\n  `)\n})\n```\n\n## Token Sources\n\nBy default, `csrf()` checks token values in this order:\n\n1. Request headers: `x-csrf-token`, `x-xsrf-token`, `csrf-token`\n2. Form field: `_csrf` (requires `formData()` middleware to parse request bodies)\n3. Query param: `_csrf`\n\nYou can override extraction using `value(context)`.\n\nHeaders 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.\n\n## Origin Validation\n\nFor unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`), the middleware validates request origin.\n\n- Default: same-origin validation when `Origin` or `Referer` is present\n- Custom: provide `origin` as string, regex, array, or function\n- Missing origin behavior: controlled by `allowMissingOrigin` (default `true`)\n\n## Caveats\n\n- The synchronizer token is the primary defense in `csrf()`. `Origin` and `Referer` checks are an additional signal, not the only protection.\n- 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.\n- 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.\n- 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()`.\n\n## Why This Exists\n\nModern browsers now provide stronger cross-origin signals like `Sec-Fetch-Site`, and explicit\n`SameSite=Lax` cookies already block many CSRF attacks. We have considered the lighter,\ntokenless model used by Go's `CrossOriginProtection`, and we think it is a good fit when a\ndeployment can make all of the guarantees that model depends on.\n\nRemix cannot assume those guarantees for every app. `csrf()` still exists as the conservative\noption for apps that want synchronizer tokens in addition to origin checks, especially for\nsession-backed HTML form workflows and mixed deployment environments.\n\nIf your deployment can guarantee the prerequisites for the tokenless model, this middleware is\noptional. In that case, [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware)\nmay be a better fit.\n\n## Related Packages\n\n- [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) - Middleware for tokenless cross-origin protection using browser provenance headers\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n- [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) - Session middleware required by `csrf()`\n- [`form-data-middleware`](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) - Needed for form body token extraction\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/csrf-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/csrf-middleware\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Middleware for CSRF protection in Fetch API servers\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/csrf-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/csrf-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/cookie\": \"workspace:*\",\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/form-data-middleware\": \"workspace:*\",\n    \"@remix-run/session\": \"workspace:*\",\n    \"@remix-run/session-middleware\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\",\n    \"@remix-run/session\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"csrf\",\n    \"security\",\n    \"origin\"\n  ]\n}\n"
  },
  {
    "path": "packages/csrf-middleware/src/index.ts",
    "content": "export {\n  csrf,\n  getCsrfToken,\n  type CsrfFailureReason,\n  type CsrfOptions,\n  type CsrfOrigin,\n  type CsrfOriginResolver,\n  type CsrfOriginResolverResult,\n  type CsrfTokenResolver,\n  type CsrfTokenResolverResult,\n} from './lib/csrf.ts'\n"
  },
  {
    "path": "packages/csrf-middleware/src/lib/csrf.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createCookie } from '@remix-run/cookie'\nimport { createRouter } from '@remix-run/fetch-router'\nimport { formData } from '@remix-run/form-data-middleware'\nimport { createCookieSessionStorage } from '@remix-run/session/cookie-storage'\nimport { session } from '@remix-run/session-middleware'\n\nimport { csrf, getCsrfToken } from './csrf.ts'\n\nfunction createRequest(fromResponse?: Response, init?: RequestInit): Request {\n  let headers = new Headers(init?.headers)\n\n  if (fromResponse) {\n    let cookies = fromResponse.headers\n      .getSetCookie()\n      .map((setCookieValue) => setCookieValue.split(';', 1)[0])\n      .join('; ')\n\n    if (cookies !== '') {\n      headers.set('Cookie', cookies)\n    }\n  }\n\n  return new Request('https://remix.run/', {\n    ...init,\n    headers,\n  })\n}\n\ndescribe('csrf middleware', () => {\n  it('creates and stores a token on safe requests', async () => {\n    let cookie = createCookie('__session', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [session(cookie, storage), csrf()],\n    })\n\n    router.get('/', (context) => {\n      let token = getCsrfToken(context)\n      return new Response(token)\n    })\n\n    let response = await router.fetch('https://remix.run/')\n    let token = await response.text()\n\n    assert.equal(response.status, 200)\n    assert.equal(token.length, 64)\n  })\n\n  it('accepts a valid token from request headers', async () => {\n    let cookie = createCookie('__session', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [session(cookie, storage), csrf()],\n    })\n\n    router.get('/', (context) => new Response(getCsrfToken(context)))\n    router.post('/', () => new Response('ok'))\n\n    let tokenResponse = await router.fetch('https://remix.run/')\n    let token = await tokenResponse.text()\n\n    let postRequest = createRequest(tokenResponse, {\n      method: 'POST',\n      headers: {\n        'X-CSRF-Token': token,\n      },\n    })\n\n    let response = await router.fetch(postRequest)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'ok')\n  })\n\n  it('rejects unsafe requests with a missing token', async () => {\n    let cookie = createCookie('__session', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [session(cookie, storage), csrf()],\n    })\n\n    router.get('/', (context) => new Response(getCsrfToken(context)))\n    router.post('/', () => new Response('ok'))\n\n    let tokenResponse = await router.fetch('https://remix.run/')\n    let postRequest = createRequest(tokenResponse, {\n      method: 'POST',\n    })\n\n    let response = await router.fetch(postRequest)\n\n    assert.equal(response.status, 403)\n    assert.equal(await response.text(), 'Forbidden: missing CSRF token')\n  })\n\n  it('rejects unsafe requests with an invalid token', async () => {\n    let cookie = createCookie('__session', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [session(cookie, storage), csrf()],\n    })\n\n    router.get('/', (context) => new Response(getCsrfToken(context)))\n    router.post('/', () => new Response('ok'))\n\n    let tokenResponse = await router.fetch('https://remix.run/')\n\n    let postRequest = createRequest(tokenResponse, {\n      method: 'POST',\n      headers: {\n        'X-CSRF-Token': 'invalid-token',\n      },\n    })\n\n    let response = await router.fetch(postRequest)\n\n    assert.equal(response.status, 403)\n    assert.equal(await response.text(), 'Forbidden: invalid CSRF token')\n  })\n\n  it('validates form field tokens when formData middleware is enabled', async () => {\n    let cookie = createCookie('__session', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [session(cookie, storage), formData(), csrf()],\n    })\n\n    router.get('/', (context) => new Response(getCsrfToken(context)))\n    router.post('/', () => new Response('ok'))\n\n    let tokenResponse = await router.fetch('https://remix.run/')\n    let token = await tokenResponse.text()\n\n    let postRequest = createRequest(tokenResponse, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: `_csrf=${encodeURIComponent(token)}`,\n    })\n\n    let response = await router.fetch(postRequest)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'ok')\n  })\n\n  it('rejects unsafe requests with invalid origin by default', async () => {\n    let cookie = createCookie('__session', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [session(cookie, storage), csrf()],\n    })\n\n    router.get('/', (context) => new Response(getCsrfToken(context)))\n    router.post('/', () => new Response('ok'))\n\n    let tokenResponse = await router.fetch('https://remix.run/')\n    let token = await tokenResponse.text()\n\n    let postRequest = createRequest(tokenResponse, {\n      method: 'POST',\n      headers: {\n        Origin: 'https://evil.example',\n        'X-CSRF-Token': token,\n      },\n    })\n\n    let response = await router.fetch(postRequest)\n\n    assert.equal(response.status, 403)\n    assert.equal(await response.text(), 'Forbidden: invalid CSRF origin')\n  })\n\n  it('supports custom origin matching', async () => {\n    let cookie = createCookie('__session', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [session(cookie, storage), csrf({ origin: ['https://admin.example.com'] })],\n    })\n\n    router.get('/', (context) => new Response(getCsrfToken(context)))\n    router.post('/', () => new Response('ok'))\n\n    let tokenResponse = await router.fetch('https://remix.run/')\n    let token = await tokenResponse.text()\n\n    let postRequest = createRequest(tokenResponse, {\n      method: 'POST',\n      headers: {\n        Origin: 'https://admin.example.com',\n        'X-CSRF-Token': token,\n      },\n    })\n\n    let response = await router.fetch(postRequest)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'ok')\n  })\n\n  it('supports custom token value extractors', async () => {\n    let cookie = createCookie('__session', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [\n        session(cookie, storage),\n        csrf({\n          value(context) {\n            return context.headers.get('X-Custom-CSRF')\n          },\n        }),\n      ],\n    })\n\n    router.get('/', (context) => new Response(getCsrfToken(context)))\n    router.post('/', () => new Response('ok'))\n\n    let tokenResponse = await router.fetch('https://remix.run/')\n    let token = await tokenResponse.text()\n\n    let postRequest = createRequest(tokenResponse, {\n      method: 'POST',\n      headers: {\n        'X-Custom-CSRF': token,\n      },\n    })\n\n    let response = await router.fetch(postRequest)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'ok')\n  })\n\n  it('throws when session middleware is not registered', async () => {\n    let router = createRouter({\n      middleware: [csrf()],\n    })\n\n    router.post('/', () => new Response('ok'))\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/', {\n        method: 'POST',\n      })\n    }, new Error('csrf middleware requires session() middleware to run before it'))\n  })\n})\n"
  },
  {
    "path": "packages/csrf-middleware/src/lib/csrf.ts",
    "content": "import type { Middleware, RequestContext, RequestMethod } from '@remix-run/fetch-router'\nimport { Session } from '@remix-run/session'\n\nlet defaultSafeMethods: RequestMethod[] = ['GET', 'HEAD', 'OPTIONS']\nlet defaultTokenHeaderNames = ['x-csrf-token', 'x-xsrf-token', 'csrf-token']\n\ntype OriginMatcher = string | RegExp | ReadonlyArray<string | RegExp>\n\n/**\n * Return shape for a dynamic CSRF origin resolver.\n */\nexport type CsrfOriginResolverResult = boolean | null | undefined\n\n/**\n * Resolves whether an unsafe cross-origin request should be allowed.\n */\nexport interface CsrfOriginResolver {\n  /**\n   * Resolves whether an unsafe request origin should be trusted.\n   */\n  (\n    origin: string,\n    context: RequestContext,\n  ): CsrfOriginResolverResult | Promise<CsrfOriginResolverResult>\n}\n\n/**\n * Accepted forms for configuring allowed CSRF origins.\n */\nexport type CsrfOrigin = OriginMatcher | CsrfOriginResolver\n\n/**\n * Return shape for a dynamic CSRF token resolver.\n */\nexport type CsrfTokenResolverResult = string | null | undefined\n\n/**\n * Resolves the submitted CSRF token for a request.\n */\nexport interface CsrfTokenResolver {\n  /**\n   * Resolves the submitted CSRF token for the current request.\n   */\n  (context: RequestContext): CsrfTokenResolverResult | Promise<CsrfTokenResolverResult>\n}\n\n/**\n * The reason a CSRF request was rejected.\n */\nexport type CsrfFailureReason = 'invalid-origin' | 'missing-token' | 'invalid-token'\n\n/**\n * Options for the CSRF middleware.\n */\nexport interface CsrfOptions {\n  /**\n   * Session key used to store the server-generated CSRF token.\n   *\n   * @default '_csrf'\n   */\n  tokenKey?: string\n\n  /**\n   * Form field name to read CSRF tokens from.\n   *\n   * @default '_csrf'\n   */\n  fieldName?: string\n\n  /**\n   * Header names checked (in order) for CSRF tokens.\n   *\n   * @default ['x-csrf-token', 'x-xsrf-token', 'csrf-token']\n   */\n  headerNames?: readonly string[]\n\n  /**\n   * Methods that do not require CSRF validation.\n   *\n   * @default ['GET', 'HEAD', 'OPTIONS']\n   */\n  safeMethods?: readonly RequestMethod[]\n\n  /**\n   * Allowed cross-origin origins for unsafe requests.\n   *\n   * When omitted, requests are validated as same-origin.\n   */\n  origin?: CsrfOrigin\n\n  /**\n   * Allow requests without Origin/Referer headers.\n   *\n   * @default true\n   */\n  allowMissingOrigin?: boolean\n\n  /**\n   * Custom function for extracting the submitted token.\n   */\n  value?: CsrfTokenResolver\n\n  /**\n   * Optional custom error response for rejected requests.\n   */\n  onError?: (reason: CsrfFailureReason, context: RequestContext) => Response | Promise<Response>\n}\n\n/**\n * Session-backed CSRF protection middleware.\n *\n * This middleware requires the session middleware to run before it.\n *\n * @param options CSRF options\n * @returns CSRF middleware\n */\nexport function csrf(options: CsrfOptions = {}): Middleware {\n  let safeMethods = options.safeMethods ?? defaultSafeMethods\n  let tokenKey = options.tokenKey ?? '_csrf'\n  let fieldName = options.fieldName ?? '_csrf'\n  let headerNames = options.headerNames ?? defaultTokenHeaderNames\n  let allowMissingOrigin = options.allowMissingOrigin ?? true\n\n  return async (context, next) => {\n    if (!context.has(Session)) {\n      throw new Error('csrf middleware requires session() middleware to run before it')\n    }\n\n    let expectedToken = getCsrfToken(context, tokenKey)\n\n    if (safeMethods.includes(context.method)) {\n      return next()\n    }\n\n    let validOrigin = await validateRequestOrigin(\n      context,\n      options.origin,\n      allowMissingOrigin,\n      context.url.origin,\n    )\n    if (!validOrigin) {\n      return getErrorResponse(options, 'invalid-origin', context)\n    }\n\n    let submittedToken = await resolveSubmittedToken(context, options.value, fieldName, headerNames)\n\n    if (submittedToken == null || submittedToken === '') {\n      return getErrorResponse(options, 'missing-token', context)\n    }\n\n    if (!constantTimeEqual(submittedToken, expectedToken)) {\n      return getErrorResponse(options, 'invalid-token', context)\n    }\n\n    return next()\n  }\n}\n\n/**\n * Gets the CSRF token from the session. Creates one if missing.\n *\n * @param context Request context with a started session\n * @param tokenKey Session key that stores the token\n * @returns The active CSRF token\n */\nexport function getCsrfToken(context: RequestContext, tokenKey = '_csrf'): string {\n  if (!context.has(Session)) {\n    throw new Error('Session is not started. Use session() middleware before csrf().')\n  }\n\n  let session = context.get(Session)\n  let token = session.get(tokenKey)\n  if (typeof token === 'string' && token !== '') {\n    return token\n  }\n\n  let createdToken = createCsrfToken()\n  session.set(tokenKey, createdToken)\n\n  return createdToken\n}\n\nfunction createCsrfToken(): string {\n  let bytes = new Uint8Array(32)\n  crypto.getRandomValues(bytes)\n\n  let token = ''\n  for (let byte of bytes) {\n    token += byte.toString(16).padStart(2, '0')\n  }\n\n  return token\n}\n\nfunction getErrorResponse(\n  options: CsrfOptions,\n  reason: CsrfFailureReason,\n  context: RequestContext,\n): Response | Promise<Response> {\n  if (options.onError) {\n    return options.onError(reason, context)\n  }\n\n  if (reason === 'invalid-origin') {\n    return new Response('Forbidden: invalid CSRF origin', { status: 403 })\n  }\n\n  if (reason === 'missing-token') {\n    return new Response('Forbidden: missing CSRF token', { status: 403 })\n  }\n\n  return new Response('Forbidden: invalid CSRF token', { status: 403 })\n}\n\nasync function resolveSubmittedToken(\n  context: RequestContext,\n  valueResolver: CsrfTokenResolver | undefined,\n  fieldName: string,\n  headerNames: readonly string[],\n): Promise<string | null> {\n  if (valueResolver) {\n    let value = await valueResolver(context)\n    if (value == null) {\n      return null\n    }\n\n    let trimmedValue = value.trim()\n    return trimmedValue === '' ? null : trimmedValue\n  }\n\n  for (let headerName of headerNames) {\n    let headerValue = context.headers.get(headerName)\n    if (headerValue == null) {\n      continue\n    }\n\n    let trimmedHeaderValue = headerValue.trim()\n    if (trimmedHeaderValue !== '') {\n      return trimmedHeaderValue\n    }\n  }\n\n  let formValue = context.has(FormData) ? context.get(FormData).get(fieldName) : undefined\n  if (typeof formValue === 'string') {\n    let trimmedFormValue = formValue.trim()\n    if (trimmedFormValue !== '') {\n      return trimmedFormValue\n    }\n  }\n\n  let queryValue = context.url.searchParams.get(fieldName)\n  if (queryValue == null) {\n    return null\n  }\n\n  let trimmedQueryValue = queryValue.trim()\n  return trimmedQueryValue === '' ? null : trimmedQueryValue\n}\n\nasync function validateRequestOrigin(\n  context: RequestContext,\n  configuredOrigin: CsrfOrigin | undefined,\n  allowMissingOrigin: boolean,\n  defaultOrigin: string,\n): Promise<boolean> {\n  let requestOrigin = getRequestOrigin(context)\n  if (requestOrigin == null) {\n    return allowMissingOrigin\n  }\n\n  if (configuredOrigin == null) {\n    return requestOrigin === defaultOrigin\n  }\n\n  if (typeof configuredOrigin === 'function') {\n    let result = await configuredOrigin(requestOrigin, context)\n    return result === true\n  }\n\n  if (typeof configuredOrigin === 'string') {\n    return configuredOrigin === requestOrigin\n  }\n\n  if (configuredOrigin instanceof RegExp) {\n    return configuredOrigin.test(requestOrigin)\n  }\n\n  for (let allowedOrigin of configuredOrigin) {\n    if (typeof allowedOrigin === 'string' && allowedOrigin === requestOrigin) {\n      return true\n    }\n\n    if (allowedOrigin instanceof RegExp && allowedOrigin.test(requestOrigin)) {\n      return true\n    }\n  }\n\n  return false\n}\n\nfunction getRequestOrigin(context: RequestContext): string | null {\n  let origin = context.headers.get('Origin')\n  if (origin != null && origin.trim() !== '') {\n    return origin\n  }\n\n  let referer = context.headers.get('Referer')\n  if (referer == null || referer.trim() === '') {\n    return null\n  }\n\n  try {\n    return new URL(referer).origin\n  } catch {\n    return null\n  }\n}\n\nfunction constantTimeEqual(left: string, right: string): boolean {\n  let mismatch = left.length === right.length ? 0 : 1\n  let maxLength = Math.max(left.length, right.length)\n\n  for (let index = 0; index < maxLength; index++) {\n    let leftCode = left.charCodeAt(index) || 0\n    let rightCode = right.charCodeAt(index) || 0\n    mismatch |= leftCode ^ rightCode\n  }\n\n  return mismatch === 0\n}\n"
  },
  {
    "path": "packages/csrf-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/csrf-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/data-schema/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/data-schema/.changes/minor.form-data.md",
    "content": "Add `@remix-run/data-schema/form-data` with `object`, `field`, `fields`, `file`, and `files`\nhelpers for validating `FormData` and `URLSearchParams` with `parse()` and `parseSafe()`.\n"
  },
  {
    "path": "packages/data-schema/.changes/patch.remove-unnecessary-as-const-from-enum.md",
    "content": "Remove unnecessary `as const` from `enum_()` examples in docs and tests since the `const` type parameter already preserves literal type inference.\n"
  },
  {
    "path": "packages/data-schema/CHANGELOG.md",
    "content": "# `data-schema` CHANGELOG\n\nThis is the changelog for [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema). It follows [semantic versioning](https://semver.org/).\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release of `@remix-run/data-schema`.\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/data-schema/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/data-schema/README.md",
    "content": "# data-schema\n\nTiny, standards-aligned data validation for Remix and the wider TypeScript ecosystem.\n\n- [Standard Schema](https://standardschema.dev/) v1 compatible\n- Sync-first, minimal API surface\n- Runs anywhere JavaScript runs (browser, Node.js, Bun, Deno, Workers)\n\n## Quick start\n\n```ts\nimport { enum_, literal, number, object, parse, string, variant } from '@remix-run/data-schema'\nimport { email, maxLength, min, minLength } from '@remix-run/data-schema/checks'\nimport * as coerce from '@remix-run/data-schema/coerce'\n\nlet User = object({\n  id: string(),\n  email: string().pipe(email()),\n  username: string().pipe(minLength(3), maxLength(20)),\n  age: coerce.number().pipe(min(13)),\n  role: enum_(['admin', 'member', 'guest']),\n  flags: object({\n    beta: coerce.boolean(),\n  }),\n})\n\nlet Event = variant('type', {\n  created: object({ type: literal('created'), id: string() }),\n  updated: object({ type: literal('updated'), id: string(), version: number() }),\n})\n\nlet user = parse(User, {\n  id: 'u1',\n  email: 'ada@example.com',\n  username: 'ada',\n  age: '37',\n  role: 'admin',\n  flags: { beta: 'true' },\n})\n\nlet event = parse(Event, { type: 'created', id: 'evt_1' })\n```\n\n## Parsing\n\nUse `parse()` when you want a typed value or an exception.\n\n```ts\nimport { object, string, number, parse } from '@remix-run/data-schema'\n\nlet User = object({ name: string(), age: number() })\n\nlet user = parse(User, { name: 'Ada', age: 37 })\n```\n\nUse `parseSafe()` when you prefer explicit branching over exceptions.\n\n```ts\nimport { object, string, number, parseSafe } from '@remix-run/data-schema'\n\nlet User = object({ name: string(), age: number() })\n\nlet result = parseSafe(User, input)\n\nif (!result.success) {\n  // result.issues — array of { message, path? }\n} else {\n  let user = result.value\n}\n```\n\nBoth `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.\n\nFor `FormData` and `URLSearchParams`, use the `remix/data-schema/form-data` helpers to build\nschemas that plug into the same `parse()` / `parseSafe()` flow:\n\n```ts\nimport * as s from 'remix/data-schema'\nimport * as f from 'remix/data-schema/form-data'\nimport * as checks from 'remix/data-schema/checks'\nimport * as coerce from 'remix/data-schema/coerce'\n\nlet Login = f.object({\n  email: f.field(coerce.string().pipe(checks.email())),\n  password: f.field(s.string().pipe(checks.minLength(8))),\n})\n\nlet credentials = s.parse(Login, await request.formData())\nlet filters = s.parse(\n  f.object({\n    query: f.field(s.defaulted(s.string(), '')),\n    tags: f.fields(s.array(s.string())),\n  }),\n  new URL(request.url).searchParams,\n)\n```\n\n`f.object(...)` is the root schema for `FormData` and `URLSearchParams`.\nUse `f.field(...)` for one text value, `f.fields(...)` for repeated text values,\n`f.file(...)` for one uploaded file, and `f.files(...)` for repeated files.\nWhen you want a fallback value, prefer `s.defaulted(s.string(), '')`.\nFile helpers are intended for `FormData`; `URLSearchParams` only supports text values.\n\nYou can also customize built-in validation messages with `errorMap`:\n\n```ts\nimport { object, parseSafe, string } from '@remix-run/data-schema'\nimport { minLength } from '@remix-run/data-schema/checks'\n\nlet User = object({\n  name: string(),\n  username: string().pipe(minLength(3)),\n})\nlet result = parseSafe(User, input, {\n  locale: 'es',\n  errorMap(context) {\n    if (context.code === 'type.string') {\n      return 'Se esperaba texto'\n    }\n\n    if (context.code === 'string.min_length') {\n      return (\n        'Debe tener al menos ' + String((context.values as { min: number }).min) + ' caracteres'\n      )\n    }\n  },\n})\n```\n\n`errorMap` receives `{ code, defaultMessage, path, values, input, locale }`.\nReturn `undefined` to keep the default message.\n\n## Primitives\n\n```ts\nimport { string, number, boolean, bigint, symbol, null_, undefined_ } from '@remix-run/data-schema'\n\nstring() // validates typeof === 'string'\nnumber() // validates finite numbers (rejects NaN, Infinity)\nboolean() // validates typeof === 'boolean'\nbigint() // validates typeof === 'bigint'\nsymbol() // validates typeof === 'symbol'\nnull_() // validates value === null\nundefined_() // validates value === undefined\n```\n\n## Literals, enums, and unions\n\n```ts\nimport { literal, enum_, union } from '@remix-run/data-schema'\n\n// Exact value match\nlet yes = literal('yes')\n\n// One of several allowed values\nlet Status = enum_(['active', 'inactive', 'pending'])\n\n// First schema that matches wins\nlet StringOrNumber = union([string(), number()])\n```\n\n## Objects\n\n```ts\nimport { object, string, number, optional, defaulted } from '@remix-run/data-schema'\n\nlet User = object({\n  name: string(),\n  bio: optional(string()), // accepts undefined\n  role: defaulted(string(), 'user'), // fills in 'user' when undefined\n  age: number(),\n})\n```\n\nUnknown keys are stripped by default. Change this with `unknownKeys`:\n\n```ts\nobject({ name: string() }, { unknownKeys: 'passthrough' }) // keeps unknown keys\nobject({ name: string() }, { unknownKeys: 'error' }) // rejects unknown keys\n```\n\n## Collections\n\n```ts\nimport { array, tuple, record, map, set, string, number, boolean } from '@remix-run/data-schema'\n\narray(number()) // number[]\ntuple([string(), number(), boolean()]) // [string, number, boolean]\nrecord(string(), number()) // Record<string, number>\nmap(string(), number()) // Map<string, number>\nset(number()) // Set<number>\n```\n\n## Modifiers\n\n```ts\nimport { nullable, optional, defaulted, string, number } from '@remix-run/data-schema'\n\nnullable(string()) // string | null\noptional(number()) // number | undefined\ndefaulted(string(), 'n/a') // fills 'n/a' when undefined\n```\n\n## Instance checks\n\n```ts\nimport { instanceof_, object } from '@remix-run/data-schema'\n\nlet Schema = object({\n  created: instanceof_(Date),\n  pattern: instanceof_(RegExp),\n})\n```\n\n## Any\n\nAccept any value without validation. Useful when part of a structure is opaque.\n\n```ts\nimport { any, object, string } from '@remix-run/data-schema'\n\nlet Envelope = object({\n  type: string(),\n  payload: any(),\n})\n```\n\n## Custom rules with `.refine()`\n\nAdd domain-specific validation logic inline. The predicate runs after the schema validates.\n\n```ts\nimport { number, string, object } from '@remix-run/data-schema'\n\nlet Profile = object({\n  username: string().refine((s) => s.length >= 3, 'Too short'),\n  age: number().refine((n) => n >= 18, 'Must be an adult'),\n})\n```\n\n## Validation pipelines with `.pipe()`\n\nCompose reusable `Check` objects for common constraints.\n\n```ts\nimport { object, string, number } from '@remix-run/data-schema'\nimport { minLength, maxLength, email, min, max } from '@remix-run/data-schema/checks'\n\nlet Credentials = object({\n  username: string().pipe(minLength(3), maxLength(20)),\n  email: string().pipe(email()),\n  age: number().pipe(min(13), max(130)),\n})\n```\n\nBuilt-in checks: `minLength`, `maxLength`, `email`, `url`, `min`, `max`.\n\n## Coercing input values\n\nTurn stringly-typed inputs (like form data or query strings) into real types at the schema boundary.\n\n```ts\nimport { object, parse } from '@remix-run/data-schema'\nimport * as coerce from '@remix-run/data-schema/coerce'\n\nlet Query = object({\n  page: coerce.number(),\n  includeArchived: coerce.boolean(),\n  since: coerce.date(),\n  limit: coerce.bigint(),\n  search: coerce.string(),\n})\n\nlet query = parse(Query, {\n  page: '2',\n  includeArchived: 'true',\n  since: '2025-01-01',\n  limit: '100',\n  search: 42,\n})\n```\n\n## Discriminated unions\n\nPick the right schema based on a discriminator property.\n\n```ts\nimport { literal, number, object, string, variant } from '@remix-run/data-schema'\n\nlet Event = variant('type', {\n  created: object({ type: literal('created'), id: string() }),\n  updated: object({ type: literal('updated'), id: string(), version: number() }),\n})\n```\n\n## Recursive schemas\n\nModel trees and self-referencing structures. `lazy()` defers schema resolution to avoid circular references.\n\n```ts\nimport { array, object, string } from '@remix-run/data-schema'\nimport { lazy } from '@remix-run/data-schema/lazy'\nimport type { Schema } from '@remix-run/data-schema'\n\ntype TreeNode = { id: string; children: TreeNode[] }\n\nlet Node: Schema<unknown, TreeNode> = lazy(() => object({ id: string(), children: array(Node) }))\n```\n\n## Aborting early\n\nBy default, validation collects all issues in a single pass. To stop at the first issue, enable `abortEarly`.\n\n```ts\nimport { object, string, number, parseSafe } from '@remix-run/data-schema'\n\nlet result = parseSafe(\n  object({ name: string(), age: number() }),\n  { name: 123, age: 'x' },\n  { abortEarly: true },\n)\n\nif (!result.success) {\n  console.log(result.issues) // only the first issue\n}\n```\n\n## Type inference\n\nExtract input and output types from any Standard Schema-compatible schema.\n\n```ts\nimport { object, string, number } from '@remix-run/data-schema'\nimport type { InferInput, InferOutput } from '@remix-run/data-schema'\n\nlet User = object({ name: string(), age: number() })\n\ntype UserInput = InferInput<typeof User> // unknown\ntype UserOutput = InferOutput<typeof User> // { name: string; age: number }\n```\n\n## Extending data-schema\n\nBuild custom schemas using `createSchema`, `createIssue`, and `fail`. These are the same primitives used internally by every built-in schema.\n\n```ts\nimport { createSchema, createIssue, fail } from '@remix-run/data-schema'\nimport type { Schema } from '@remix-run/data-schema'\n\n// A schema that validates a non-empty trimmed string\nfunction trimmedString(): Schema<unknown, string> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'string') {\n      return fail('Expected string', context.path)\n    }\n\n    let trimmed = value.trim()\n\n    if (trimmed.length === 0) {\n      return fail('Expected non-empty string', context.path)\n    }\n\n    return { value: trimmed }\n  })\n}\n\n// A schema that validates a [lat, lng] coordinate pair\nfunction latLng(): Schema<unknown, [number, number]> {\n  return createSchema(function validate(value, context) {\n    if (!Array.isArray(value) || value.length !== 2) {\n      return fail('Expected [lat, lng] pair', context.path)\n    }\n\n    let issues = []\n    let [lat, lng] = value\n\n    if (typeof lat !== 'number' || lat < -90 || lat > 90) {\n      issues.push(createIssue('Latitude must be between -90 and 90', [...context.path, 0]))\n    }\n\n    if (typeof lng !== 'number' || lng < -180 || lng > 180) {\n      issues.push(createIssue('Longitude must be between -180 and 180', [...context.path, 1]))\n    }\n\n    if (issues.length > 0) {\n      return { issues }\n    }\n\n    return { value: [lat, lng] }\n  })\n}\n```\n\nThe 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.\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/data-schema/package.json",
    "content": "{\n  \"name\": \"@remix-run/data-schema\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Tiny, standards-aligned schema validation\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/data-schema\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/data-schema#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./checks\": \"./src/checks.ts\",\n    \"./coerce\": \"./src/coerce.ts\",\n    \"./form-data\": \"./src/form-data.ts\",\n    \"./lazy\": \"./src/lazy.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./checks\": {\n        \"types\": \"./dist/checks.d.ts\",\n        \"default\": \"./dist/checks.js\"\n      },\n      \"./coerce\": {\n        \"types\": \"./dist/coerce.d.ts\",\n        \"default\": \"./dist/coerce.js\"\n      },\n      \"./form-data\": {\n        \"types\": \"./dist/form-data.d.ts\",\n        \"default\": \"./dist/form-data.js\"\n      },\n      \"./lazy\": {\n        \"types\": \"./dist/lazy.d.ts\",\n        \"default\": \"./dist/lazy.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"validation\",\n    \"schema\",\n    \"standard-schema\",\n    \"validator\",\n    \"remix\"\n  ],\n  \"dependencies\": {\n    \"@standard-schema/spec\": \"^1.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/data-schema/src/checks.ts",
    "content": "export { email, max, maxLength, min, minLength, url } from './lib/checks.ts'\n"
  },
  {
    "path": "packages/data-schema/src/coerce.ts",
    "content": "export {\n  coerceBigint as bigint,\n  coerceBoolean as boolean,\n  coerceDate as date,\n  coerceNumber as number,\n  coerceString as string,\n} from './lib/coerce.ts'\n"
  },
  {
    "path": "packages/data-schema/src/form-data.ts",
    "content": "export { object, field, fields, file, files } from './lib/form-data.ts'\nexport type {\n  FormDataEntrySchema,\n  FormDataFieldOptions,\n  FormDataFieldsOptions,\n  FormDataFileOptions,\n  FormDataFilesOptions,\n  FormDataObjectSchema,\n  FormDataSource,\n  FormDataSchema,\n  ParsedFormData,\n} from './lib/form-data.ts'\n"
  },
  {
    "path": "packages/data-schema/src/index.ts",
    "content": "export {\n  any,\n  array,\n  bigint,\n  boolean,\n  createIssue,\n  createSchema,\n  defaulted,\n  enum_,\n  fail,\n  instanceof_,\n  literal,\n  map,\n  null_,\n  nullable,\n  number,\n  object,\n  optional,\n  parse,\n  parseSafe,\n  record,\n  set,\n  string,\n  symbol,\n  tuple,\n  undefined_,\n  union,\n  ValidationError,\n  variant,\n} from './lib/schema.ts'\nexport type {\n  Check,\n  ErrorMap,\n  ErrorMapContext,\n  InferInput,\n  InferOutput,\n  Issue,\n  ParseOptions,\n  Schema,\n  ValidationOptions,\n  ValidationResult,\n} from './lib/schema.ts'\n"
  },
  {
    "path": "packages/data-schema/src/lazy.ts",
    "content": "export { lazy } from './lib/lazy.ts'\n"
  },
  {
    "path": "packages/data-schema/src/lib/checks.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { email, max, maxLength, min, minLength, url } from './checks.ts'\nimport { number, string } from './schema.ts'\nimport type { Issue, ValidationResult } from './schema.ts'\n\nfunction assertSuccess<output>(\n  result: ValidationResult<output>,\n): asserts result is { value: output } {\n  assert.ok(!result.issues)\n}\n\nfunction assertFailure<output>(\n  result: ValidationResult<output>,\n): asserts result is { issues: ReadonlyArray<Issue> } {\n  assert.ok(result.issues)\n}\n\ndescribe('checks', () => {\n  it('exposes code and values for message mapping', () => {\n    let minLengthCheck = minLength(2)\n    let maxLengthCheck = maxLength(4)\n\n    assert.equal(minLengthCheck.code, 'string.min_length')\n    assert.deepEqual(minLengthCheck.values, { min: 2 })\n    assert.equal(maxLengthCheck.code, 'string.max_length')\n    assert.deepEqual(maxLengthCheck.values, { max: 4 })\n  })\n\n  it('supports common string checks', () => {\n    let schema = string().pipe(minLength(2), maxLength(4))\n\n    let ok = schema['~standard'].validate('test')\n    let short = schema['~standard'].validate('a')\n    let long = schema['~standard'].validate('toolong')\n\n    assertSuccess(ok)\n    assertFailure(short)\n    assertFailure(long)\n  })\n\n  it('supports email checks', () => {\n    let schema = string().pipe(email())\n\n    let ok = schema['~standard'].validate('user@example.com')\n    let bad = schema['~standard'].validate('not-an-email')\n\n    assertSuccess(ok)\n    assertFailure(bad)\n  })\n\n  it('supports url checks', () => {\n    let schema = string().pipe(url())\n\n    let ok = schema['~standard'].validate('https://example.com')\n    let bad = schema['~standard'].validate('not-a-url')\n\n    assertSuccess(ok)\n    assertFailure(bad)\n  })\n\n  it('supports number checks', () => {\n    let schema = number().pipe(min(3), max(5))\n\n    let ok = schema['~standard'].validate(4)\n    let low = schema['~standard'].validate(2)\n    let high = schema['~standard'].validate(6)\n\n    assertSuccess(ok)\n    assertFailure(low)\n    assertFailure(high)\n  })\n})\n"
  },
  {
    "path": "packages/data-schema/src/lib/checks.ts",
    "content": "import type { Check } from './schema.ts'\n\n/**\n * Require a string to be at least `length` characters long.\n *\n * @param length The minimum number of characters\n * @returns A {@link Check} that enforces the minimum length\n */\nexport function minLength(length: number): Check<string> {\n  return {\n    check(value) {\n      return value.length >= length\n    },\n    code: 'string.min_length',\n    values: { min: length },\n    message: 'Expected at least ' + String(length) + ' characters',\n  }\n}\n\n/**\n * Require a string to be at most `length` characters long.\n *\n * @param length The maximum number of characters\n * @returns A {@link Check} that enforces the maximum length\n */\nexport function maxLength(length: number): Check<string> {\n  return {\n    check(value) {\n      return value.length <= length\n    },\n    code: 'string.max_length',\n    values: { max: length },\n    message: 'Expected at most ' + String(length) + ' characters',\n  }\n}\n\n/**\n * Require a string to be a valid email address.\n *\n * @returns A {@link Check} that validates email-like strings\n */\nexport function email(): Check<string> {\n  return {\n    check(value) {\n      return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)\n    },\n    code: 'string.email',\n    message: 'Expected valid email',\n  }\n}\n\n/**\n * Require a string to be a valid URL.\n *\n * @returns A {@link Check} that validates URL-like strings\n */\nexport function url(): Check<string> {\n  return {\n    check(value) {\n      try {\n        new URL(value)\n        return true\n      } catch {\n        return false\n      }\n    },\n    code: 'string.url',\n    message: 'Expected valid URL',\n  }\n}\n\n/**\n * Require a number to be greater than or equal to `limit`.\n *\n * @param limit The inclusive minimum value\n * @returns A {@link Check} that enforces the lower bound\n */\nexport function min(limit: number): Check<number> {\n  return {\n    check(value) {\n      return value >= limit\n    },\n    code: 'number.min',\n    values: { min: limit },\n    message: 'Expected number greater than or equal to ' + String(limit),\n  }\n}\n\n/**\n * Require a number to be less than or equal to `limit`.\n *\n * @param limit The inclusive maximum value\n * @returns A {@link Check} that enforces the upper bound\n */\nexport function max(limit: number): Check<number> {\n  return {\n    check(value) {\n      return value <= limit\n    },\n    code: 'number.max',\n    values: { max: limit },\n    message: 'Expected number less than or equal to ' + String(limit),\n  }\n}\n"
  },
  {
    "path": "packages/data-schema/src/lib/coerce.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { coerceBigint, coerceBoolean, coerceDate, coerceNumber, coerceString } from './coerce.ts'\nimport type { Issue, ValidationResult } from './schema.ts'\n\nfunction assertSuccess<output>(\n  result: ValidationResult<output>,\n): asserts result is { value: output } {\n  assert.ok(!result.issues)\n}\n\nfunction assertFailure<output>(\n  result: ValidationResult<output>,\n): asserts result is { issues: ReadonlyArray<Issue> } {\n  assert.ok(result.issues)\n}\n\ndescribe('coerce', () => {\n  it('coerces numbers from strings', () => {\n    let schema = coerceNumber()\n\n    let ok = schema['~standard'].validate('42')\n    let okTrimmed = schema['~standard'].validate(' 3.5 ')\n    let bad = schema['~standard'].validate('nope')\n\n    assertSuccess(ok)\n    assertSuccess(okTrimmed)\n    assert.equal(ok.value, 42)\n    assert.equal(okTrimmed.value, 3.5)\n    assertFailure(bad)\n  })\n\n  it('coerces numbers and passes through valid numbers', () => {\n    let schema = coerceNumber()\n    let result = schema['~standard'].validate(3.14)\n\n    assertSuccess(result)\n    assert.equal(result.value, 3.14)\n  })\n\n  it('rejects Infinity and NaN for coerced numbers', () => {\n    let schema = coerceNumber()\n\n    let posInf = schema['~standard'].validate(Infinity)\n    let negInf = schema['~standard'].validate(-Infinity)\n    let nan = schema['~standard'].validate(NaN)\n    let infString = schema['~standard'].validate('Infinity')\n    let negInfString = schema['~standard'].validate('-Infinity')\n\n    assertFailure(posInf)\n    assertFailure(negInf)\n    assertFailure(nan)\n    assertFailure(infString)\n    assertFailure(negInfString)\n  })\n\n  it('rejects empty strings for coerced numbers', () => {\n    let schema = coerceNumber()\n    let result = schema['~standard'].validate('   ')\n\n    assertFailure(result)\n  })\n\n  it('coerces booleans from strings', () => {\n    let schema = coerceBoolean()\n\n    let okTrue = schema['~standard'].validate('true')\n    let okFalse = schema['~standard'].validate('FALSE')\n    let bad = schema['~standard'].validate('yes')\n\n    assertSuccess(okTrue)\n    assertSuccess(okFalse)\n    assert.equal(okTrue.value, true)\n    assert.equal(okFalse.value, false)\n    assertFailure(bad)\n  })\n\n  it('passes through boolean values', () => {\n    let schema = coerceBoolean()\n\n    let okTrue = schema['~standard'].validate(true)\n    let okFalse = schema['~standard'].validate(false)\n\n    assertSuccess(okTrue)\n    assertSuccess(okFalse)\n    assert.equal(okTrue.value, true)\n    assert.equal(okFalse.value, false)\n  })\n\n  it('trims whitespace for boolean strings', () => {\n    let schema = coerceBoolean()\n    let result = schema['~standard'].validate('  TRUE  ')\n\n    assertSuccess(result)\n    assert.equal(result.value, true)\n  })\n\n  it('rejects non-boolean non-string values for coerceBoolean', () => {\n    let schema = coerceBoolean()\n\n    let numResult = schema['~standard'].validate(1)\n    let objResult = schema['~standard'].validate({})\n\n    assertFailure(numResult)\n    assertFailure(objResult)\n  })\n\n  it('coerces dates from strings', () => {\n    let schema = coerceDate()\n\n    let ok = schema['~standard'].validate('2025-01-01T00:00:00Z')\n    let bad = schema['~standard'].validate('not-a-date')\n\n    assertSuccess(ok)\n    assert.ok(ok.value instanceof Date)\n    assertFailure(bad)\n  })\n\n  it('passes through valid Date instances', () => {\n    let schema = coerceDate()\n    let input = new Date('2025-06-15')\n    let result = schema['~standard'].validate(input)\n\n    assertSuccess(result)\n    assert.equal(result.value, input)\n  })\n\n  it('rejects invalid Date instances', () => {\n    let schema = coerceDate()\n    let invalidDate = new Date('invalid')\n    let result = schema['~standard'].validate(invalidDate)\n\n    assertFailure(result)\n  })\n\n  it('rejects non-string non-date values for coerceDate', () => {\n    let schema = coerceDate()\n\n    let numResult = schema['~standard'].validate(1234567890)\n    let objResult = schema['~standard'].validate({})\n\n    assertFailure(numResult)\n    assertFailure(objResult)\n  })\n\n  it('coerces bigint from strings and integers', () => {\n    let schema = coerceBigint()\n\n    let okString = schema['~standard'].validate('9007199254740993')\n    let okNumber = schema['~standard'].validate(10)\n    let bad = schema['~standard'].validate(1.5)\n\n    assertSuccess(okString)\n    assertSuccess(okNumber)\n    assert.equal(okString.value, BigInt('9007199254740993'))\n    assert.equal(okNumber.value, BigInt(10))\n    assertFailure(bad)\n  })\n\n  it('passes through bigint values', () => {\n    let schema = coerceBigint()\n    let result = schema['~standard'].validate(BigInt(999))\n\n    assertSuccess(result)\n    assert.equal(result.value, BigInt(999))\n  })\n\n  it('rejects empty strings for coerceBigint', () => {\n    let schema = coerceBigint()\n    let result = schema['~standard'].validate('   ')\n\n    assertFailure(result)\n  })\n\n  it('rejects invalid bigint strings', () => {\n    let schema = coerceBigint()\n    let result = schema['~standard'].validate('12.5')\n\n    assertFailure(result)\n  })\n\n  it('rejects Infinity and NaN for coerceBigint', () => {\n    let schema = coerceBigint()\n\n    let infResult = schema['~standard'].validate(Infinity)\n    let nanResult = schema['~standard'].validate(NaN)\n\n    assertFailure(infResult)\n    assertFailure(nanResult)\n  })\n\n  it('coerces strings from primitives', () => {\n    let schema = coerceString()\n\n    let okNumber = schema['~standard'].validate(5)\n    let okBoolean = schema['~standard'].validate(false)\n    let okBigint = schema['~standard'].validate(BigInt(7))\n    let bad = schema['~standard'].validate({})\n\n    assertSuccess(okNumber)\n    assertSuccess(okBoolean)\n    assertSuccess(okBigint)\n    assert.equal(okNumber.value, '5')\n    assert.equal(okBoolean.value, 'false')\n    assert.equal(okBigint.value, '7')\n    assertFailure(bad)\n  })\n\n  it('passes through string values', () => {\n    let schema = coerceString()\n    let result = schema['~standard'].validate('hello')\n\n    assertSuccess(result)\n    assert.equal(result.value, 'hello')\n  })\n\n  it('coerces symbols to strings', () => {\n    let schema = coerceString()\n    let mySymbol = Symbol('test')\n    let result = schema['~standard'].validate(mySymbol)\n\n    assertSuccess(result)\n    assert.equal(result.value, 'Symbol(test)')\n  })\n\n  it('rejects null and undefined for coerceString', () => {\n    let schema = coerceString()\n\n    let nullResult = schema['~standard'].validate(null)\n    let undefinedResult = schema['~standard'].validate(undefined)\n\n    assertFailure(nullResult)\n    assertFailure(undefinedResult)\n  })\n\n  it('rejects arrays and objects for coerceString', () => {\n    let schema = coerceString()\n\n    let arrayResult = schema['~standard'].validate([1, 2])\n    let objectResult = schema['~standard'].validate({ a: 1 })\n\n    assertFailure(arrayResult)\n    assertFailure(objectResult)\n  })\n})\n"
  },
  {
    "path": "packages/data-schema/src/lib/coerce.ts",
    "content": "import { createSchema, fail } from './schema.ts'\nimport type { Schema } from './schema.ts'\n\n/**\n * Coerce input into a number.\n *\n * Accepts:\n * - finite `number` values (excluding `NaN` and `Infinity`)\n * - strings parsed with `Number(...)` after trimming (must produce finite result)\n *\n * @returns A schema that produces a `number`\n */\nexport function coerceNumber(): Schema<unknown, number> {\n  return createSchema(function validate(value, context) {\n    if (typeof value === 'number' && Number.isFinite(value)) {\n      return { value }\n    }\n\n    if (typeof value === 'string') {\n      let trimmed = value.trim()\n\n      if (trimmed.length === 0) {\n        return fail('Expected number', context.path, {\n          code: 'coerce.number',\n          input: value,\n          parseOptions: context.options,\n        })\n      }\n\n      let parsed = Number(trimmed)\n\n      if (Number.isFinite(parsed)) {\n        return { value: parsed }\n      }\n    }\n\n    return fail('Expected number', context.path, {\n      code: 'coerce.number',\n      input: value,\n      parseOptions: context.options,\n    })\n  })\n}\n\n/**\n * Coerce input into a boolean.\n *\n * Accepts:\n * - `boolean` values as-is\n * - strings `\"true\"` and `\"false\"` (case-insensitive, trimmed)\n *\n * @returns A schema that produces a `boolean`\n */\nexport function coerceBoolean(): Schema<unknown, boolean> {\n  return createSchema(function validate(value, context) {\n    if (typeof value === 'boolean') {\n      return { value }\n    }\n\n    if (typeof value === 'string') {\n      let normalized = value.trim().toLowerCase()\n\n      if (normalized === 'true') {\n        return { value: true }\n      }\n\n      if (normalized === 'false') {\n        return { value: false }\n      }\n    }\n\n    return fail('Expected boolean', context.path, {\n      code: 'coerce.boolean',\n      input: value,\n      parseOptions: context.options,\n    })\n  })\n}\n\n/**\n * Coerce input into a `Date`.\n *\n * Accepts:\n * - valid `Date` instances\n * - date strings supported by `new Date(value)`\n *\n * @returns A schema that produces a `Date`\n */\nexport function coerceDate(): Schema<unknown, Date> {\n  return createSchema(function validate(value, context) {\n    if (value instanceof Date && !Number.isNaN(value.getTime())) {\n      return { value }\n    }\n\n    if (typeof value === 'string') {\n      let parsed = new Date(value)\n\n      if (!Number.isNaN(parsed.getTime())) {\n        return { value: parsed }\n      }\n    }\n\n    return fail('Expected date', context.path, {\n      code: 'coerce.date',\n      input: value,\n      parseOptions: context.options,\n    })\n  })\n}\n\n/**\n * Coerce input into a `bigint`.\n *\n * Accepts:\n * - `bigint` values as-is\n * - integer `number` values\n * - integer strings parsed via `BigInt(...)`\n *\n * @returns A schema that produces a `bigint`\n */\nexport function coerceBigint(): Schema<unknown, bigint> {\n  return createSchema(function validate(value, context) {\n    if (typeof value === 'bigint') {\n      return { value }\n    }\n\n    if (typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value)) {\n      return { value: BigInt(value) }\n    }\n\n    if (typeof value === 'string') {\n      let trimmed = value.trim()\n\n      if (trimmed.length === 0) {\n        return fail('Expected bigint', context.path, {\n          code: 'coerce.bigint',\n          input: value,\n          parseOptions: context.options,\n        })\n      }\n\n      try {\n        return { value: BigInt(trimmed) }\n      } catch {\n        return fail('Expected bigint', context.path, {\n          code: 'coerce.bigint',\n          input: value,\n          parseOptions: context.options,\n        })\n      }\n    }\n\n    return fail('Expected bigint', context.path, {\n      code: 'coerce.bigint',\n      input: value,\n      parseOptions: context.options,\n    })\n  })\n}\n\n/**\n * Coerce input into a string.\n *\n * Accepts:\n * - `string` values as-is\n * - primitive values that can be stringified (`number`, `boolean`, `bigint`, `symbol`)\n *\n * @returns A schema that produces a `string`\n */\nexport function coerceString(): Schema<unknown, string> {\n  return createSchema(function validate(value, context) {\n    if (typeof value === 'string') {\n      return { value }\n    }\n\n    if (\n      typeof value === 'number' ||\n      typeof value === 'boolean' ||\n      typeof value === 'bigint' ||\n      typeof value === 'symbol'\n    ) {\n      return { value: String(value) }\n    }\n\n    return fail('Expected string', context.path, {\n      code: 'coerce.string',\n      input: value,\n      parseOptions: context.options,\n    })\n  })\n}\n"
  },
  {
    "path": "packages/data-schema/src/lib/form-data.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport * as coerce from '../coerce.ts'\nimport * as f from '../form-data.ts'\nimport { minLength } from './checks.ts'\nimport * as s from './schema.ts'\n\ntype Equal<left, right> =\n  (<value>() => value extends left ? 1 : 2) extends <value>() => value extends right ? 1 : 2\n    ? true\n    : false\n\nfunction expectType<condition extends true>(_value?: condition): void {}\n\ndescribe('form-data.object', () => {\n  it('parses single text fields', () => {\n    let formData = new FormData()\n    formData.set('email', 'ada@example.com')\n    formData.set('password', 'secret')\n\n    let result = s.parse(\n      f.object({\n        email: f.field(s.string()),\n        password: f.field(s.string().pipe(minLength(1))),\n      }),\n      formData,\n    )\n\n    assert.deepEqual(result, {\n      email: 'ada@example.com',\n      password: 'secret',\n    })\n  })\n\n  it('supports custom form field names', () => {\n    let formData = new FormData()\n    formData.set('user-email', 'ada@example.com')\n\n    let result = s.parse(\n      f.object({\n        email: f.field(s.string(), { name: 'user-email' }),\n      }),\n      formData,\n    )\n\n    assert.deepEqual(result, {\n      email: 'ada@example.com',\n    })\n  })\n\n  it('parses repeated text fields', () => {\n    let formData = new FormData()\n    formData.append('tags', 'one')\n    formData.append('tags', 'two')\n\n    let result = s.parse(\n      f.object({\n        tags: f.fields(s.array(s.string())),\n      }),\n      formData,\n    )\n\n    assert.deepEqual(result, {\n      tags: ['one', 'two'],\n    })\n  })\n\n  it('parses text fields from URLSearchParams', () => {\n    let searchParams = new URLSearchParams()\n    searchParams.set('email', 'ada@example.com')\n    searchParams.set('password', 'secret')\n\n    let result = s.parse(\n      f.object({\n        email: f.field(s.string()),\n        password: f.field(s.string().pipe(minLength(1))),\n      }),\n      searchParams,\n    )\n\n    assert.deepEqual(result, {\n      email: 'ada@example.com',\n      password: 'secret',\n    })\n  })\n\n  it('parses repeated text fields from URLSearchParams', () => {\n    let searchParams = new URLSearchParams()\n    searchParams.append('tags', 'one')\n    searchParams.append('tags', 'two')\n\n    let result = s.parse(\n      f.object({\n        tags: f.fields(s.array(s.string())),\n      }),\n      searchParams,\n    )\n\n    assert.deepEqual(result, {\n      tags: ['one', 'two'],\n    })\n  })\n\n  it('parses a single file field', async () => {\n    let formData = new FormData()\n    let avatar = new File(['avatar'], 'avatar.png', { type: 'image/png' })\n    formData.set('avatar', avatar)\n\n    let result = s.parse(\n      f.object({\n        avatar: f.file(s.instanceof_(File)),\n      }),\n      formData,\n    )\n\n    assert.equal(result.avatar.name, 'avatar.png')\n    assert.equal(await result.avatar.text(), 'avatar')\n  })\n\n  it('parses repeated file fields', async () => {\n    let formData = new FormData()\n    formData.append('attachments', new File(['one'], 'one.txt', { type: 'text/plain' }))\n    formData.append('attachments', new File(['two'], 'two.txt', { type: 'text/plain' }))\n\n    let result = s.parse(\n      f.object({\n        attachments: f.files(s.array(s.instanceof_(File))),\n      }),\n      formData,\n    )\n\n    assert.equal(result.attachments.length, 2)\n    assert.equal(result.attachments[0]?.name, 'one.txt')\n    assert.equal(await result.attachments[1]!.text(), 'two')\n  })\n\n  it('passes undefined to optional fields when a value is missing', () => {\n    let formData = new FormData()\n\n    let result = s.parse(\n      f.object({\n        nickname: f.field(s.optional(s.string())),\n      }),\n      formData,\n    )\n\n    assert.deepEqual(result, {\n      nickname: undefined,\n    })\n  })\n\n  it('infers defaulted fields as required outputs', () => {\n    let formData = new FormData()\n\n    let result = s.parse(\n      f.object({\n        query: f.field(s.defaulted(s.string(), '')),\n      }),\n      formData,\n    )\n\n    expectType<Equal<(typeof result)['query'], string>>()\n    assert.deepEqual(result, {\n      query: '',\n    })\n  })\n\n  it('throws a ValidationError when parsing fails', () => {\n    let formData = new FormData()\n    formData.set('age', 'not-a-number')\n\n    assert.throws(\n      () =>\n        s.parse(\n          f.object({\n            age: f.field(coerce.number()),\n          }),\n          formData,\n        ),\n      (error: unknown) => {\n        assert.ok(error instanceof s.ValidationError)\n        assert.deepEqual(error.issues[0]?.path, ['age'])\n        return true\n      },\n    )\n  })\n\n  it('reports text/file kind mismatches in the safe result', () => {\n    let formData = new FormData()\n    formData.set('avatar', new File(['avatar'], 'avatar.png', { type: 'image/png' }))\n\n    let result = s.parseSafe(\n      f.object({\n        avatar: f.field(s.string()),\n      }),\n      formData,\n    )\n\n    assert.equal(result.success, false)\n    assert.equal(result.issues[0]?.message, 'Expected text field \"avatar\"')\n    assert.deepEqual(result.issues[0]?.path, ['avatar'])\n  })\n\n  it('reports file mismatches from URLSearchParams in the safe result', () => {\n    let searchParams = new URLSearchParams()\n    searchParams.set('avatar', 'avatar.png')\n\n    let result = s.parseSafe(\n      f.object({\n        avatar: f.file(s.instanceof_(File)),\n      }),\n      searchParams,\n    )\n\n    assert.equal(result.success, false)\n    assert.equal(result.issues[0]?.message, 'Expected file field \"avatar\"')\n    assert.deepEqual(result.issues[0]?.path, ['avatar'])\n  })\n\n  it('prefixes nested schema issues with the parsed field path', () => {\n    let formData = new FormData()\n    formData.append('ages', '1')\n    formData.append('ages', 'x')\n\n    let result = s.parseSafe(\n      f.object({\n        ages: f.fields(s.array(coerce.number())),\n      }),\n      formData,\n    )\n\n    assert.equal(result.success, false)\n    assert.deepEqual(result.issues[0]?.path, ['ages', 1])\n    assert.equal(result.issues[0]?.message, 'Expected number')\n  })\n\n  it('rejects unsupported root input values', () => {\n    let result = s.parseSafe(\n      f.object({\n        email: f.field(s.string()),\n      }),\n      { email: 'ada@example.com' },\n    )\n\n    assert.equal(result.success, false)\n    assert.equal(result.issues[0]?.message, 'Expected FormData or URLSearchParams')\n    assert.deepEqual(result.issues[0]?.path, undefined)\n  })\n})\n"
  },
  {
    "path": "packages/data-schema/src/lib/form-data.ts",
    "content": "import type { InferOutput, Issue, ParseOptions, Schema } from './schema.ts'\nimport { createIssue, createSchema, fail } from './schema.ts'\n\ntype FormDataEntryKind = 'field' | 'fields' | 'file' | 'files'\n\n/**\n * A Standard Schema-compatible input type for form-like data containers.\n */\nexport type FormDataSource = FormData | URLSearchParams\n\ntype FormDataParseResult<output> = { value: output } | { issues: ReadonlyArray<Issue> }\n\ntype FormDataValidationContext = {\n  path: NonNullable<Issue['path']>\n  options?: ParseOptions\n}\n\n/**\n * A schema entry that reads one or more values from `FormData` or `URLSearchParams` and validates\n * them.\n */\nexport interface FormDataEntrySchema<output> {\n  /** The parsing mode used to read values from the input object. */\n  kind: FormDataEntryKind\n  /** The form field name to read. Defaults to the object key passed to `object()`. */\n  name?: string\n  /** The schema used to validate the parsed value or values. */\n  schema: Schema<any, output>\n}\n\n/**\n * Options for parsing a single text field from `FormData` or `URLSearchParams`.\n */\nexport interface FormDataFieldOptions {\n  /** The form field name to read. Defaults to the object key passed to `object()`. */\n  name?: string\n}\n\n/**\n * Options for parsing repeated text fields from `FormData` or `URLSearchParams`.\n */\nexport interface FormDataFieldsOptions {\n  /** The form field name to read. Defaults to the object key passed to `object()`. */\n  name?: string\n}\n\n/**\n * Options for parsing a single file field from `FormData`.\n */\nexport interface FormDataFileOptions {\n  /** The form field name to read. Defaults to the object key passed to `object()`. */\n  name?: string\n}\n\n/**\n * Options for parsing repeated file fields from `FormData`.\n */\nexport interface FormDataFilesOptions {\n  /** The form field name to read. Defaults to the object key passed to `object()`. */\n  name?: string\n}\n\n/**\n * A schema-like object that describes the fields to parse from `FormData` or `URLSearchParams`.\n */\nexport type FormDataSchema = Record<string, FormDataEntrySchema<any>>\n\n/**\n * A Standard Schema-compatible schema that validates a `FormData` or `URLSearchParams` object.\n */\nexport type FormDataObjectSchema<schema extends FormDataSchema> = Schema<\n  FormDataSource,\n  ParsedFormData<schema>\n>\n\n/**\n * The typed result produced by `object()` for a given form-data shape.\n */\nexport type ParsedFormData<schema extends FormDataSchema> = {\n  [key in keyof schema]: schema[key] extends FormDataEntrySchema<infer output> ? output : never\n}\n\n/**\n * Creates a Standard Schema-compatible schema that reads typed values from a `FormData` or\n * `URLSearchParams` object.\n *\n * Use the returned schema with `parse()` or `parseSafe()` from `@remix-run/data-schema`.\n *\n * @param schema The form-data shape describing the fields to read and validate.\n * @returns A schema that validates a `FormData` or `URLSearchParams` object and produces typed\n * output.\n */\nexport function object<schema extends FormDataSchema>(\n  schema: schema,\n): FormDataObjectSchema<schema> {\n  return createSchema(function validate(value, context) {\n    if (!isFormDataSource(value)) {\n      return fail('Expected FormData or URLSearchParams', context.path, {\n        code: 'type.form_data_source',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    let abortEarly = shouldAbortEarly(context.options)\n    let issues: Issue[] = []\n    let output: Partial<Record<keyof schema, unknown>> = {}\n\n    for (let [key, entrySchema] of Object.entries(schema) as [\n      keyof schema & string,\n      FormDataEntrySchema<any>,\n    ][]) {\n      let result = parseField(value, key, entrySchema, context)\n\n      if ('issues' in result) {\n        if (abortEarly) {\n          return { issues: result.issues }\n        }\n\n        issues.push(...result.issues)\n        continue\n      }\n\n      output[key] = result.value\n    }\n\n    if (issues.length > 0) {\n      return { issues }\n    }\n\n    return { value: output as ParsedFormData<schema> }\n  })\n}\n\n/**\n * Creates a schema entry for a single text field from `FormData` or `URLSearchParams`.\n *\n * @param schema The schema used to validate the parsed field value.\n * @param options Parsing options for the field.\n * @returns A field schema entry for use with `object()`.\n */\nexport function field<schema extends Schema<any, any>>(\n  schema: schema,\n  options?: FormDataFieldOptions,\n): FormDataEntrySchema<InferOutput<schema>> {\n  return {\n    kind: 'field',\n    name: options?.name,\n    schema,\n  }\n}\n\n/**\n * Creates a schema entry for repeated text fields from `FormData` or `URLSearchParams`.\n *\n * @param schema The schema used to validate the parsed field values.\n * @param options Parsing options for the field.\n * @returns A field schema entry for use with `object()`.\n */\nexport function fields<schema extends Schema<any, any>>(\n  schema: schema,\n  options?: FormDataFieldsOptions,\n): FormDataEntrySchema<InferOutput<schema>> {\n  return {\n    kind: 'fields',\n    name: options?.name,\n    schema,\n  }\n}\n\n/**\n * Creates a schema entry for a single file field from `FormData`.\n *\n * @param schema The schema used to validate the parsed file value.\n * @param options Parsing options for the field.\n * @returns A file schema entry for use with `object()`.\n */\nexport function file<schema extends Schema<any, any>>(\n  schema: schema,\n  options?: FormDataFileOptions,\n): FormDataEntrySchema<InferOutput<schema>> {\n  return {\n    kind: 'file',\n    name: options?.name,\n    schema,\n  }\n}\n\n/**\n * Creates a schema entry for repeated file fields from `FormData`.\n *\n * @param schema The schema used to validate the parsed file values.\n * @param options Parsing options for the field.\n * @returns A file schema entry for use with `object()`.\n */\nexport function files<schema extends Schema<any, any>>(\n  schema: schema,\n  options?: FormDataFilesOptions,\n): FormDataEntrySchema<InferOutput<schema>> {\n  return {\n    kind: 'files',\n    name: options?.name,\n    schema,\n  }\n}\n\nfunction parseField(\n  formData: FormDataSource,\n  key: string,\n  entrySchema: FormDataEntrySchema<any>,\n  context: FormDataValidationContext,\n): FormDataParseResult<unknown> {\n  let fieldName = entrySchema.name ?? key\n  let keyPath = withPath(context.path, key)\n\n  switch (entrySchema.kind) {\n    case 'field': {\n      let value = formData.get(fieldName)\n\n      if (value instanceof Blob) {\n        return {\n          issues: [createIssue(`Expected text field \"${fieldName}\"`, keyPath)],\n        }\n      }\n\n      return validateParsedValue(keyPath, entrySchema.schema, value ?? undefined, context.options)\n    }\n    case 'fields': {\n      let values = formData.getAll(fieldName)\n      let parsedValues: string[] = []\n      let issues: Issue[] = []\n\n      values.forEach((value, index) => {\n        if (value instanceof Blob) {\n          issues.push(createIssue(`Expected text field \"${fieldName}\"`, withPath(keyPath, index)))\n        } else {\n          parsedValues.push(value)\n        }\n      })\n\n      if (issues.length > 0) {\n        return { issues }\n      }\n\n      return validateParsedValue(keyPath, entrySchema.schema, parsedValues, context.options)\n    }\n    case 'file': {\n      let value = formData.get(fieldName)\n\n      if (value != null && !(value instanceof Blob)) {\n        return {\n          issues: [createIssue(`Expected file field \"${fieldName}\"`, keyPath)],\n        }\n      }\n\n      return validateParsedValue(keyPath, entrySchema.schema, value ?? undefined, context.options)\n    }\n    case 'files': {\n      let values = formData.getAll(fieldName)\n      let parsedValues: Blob[] = []\n      let issues: Issue[] = []\n\n      values.forEach((value, index) => {\n        if (!(value instanceof Blob)) {\n          issues.push(createIssue(`Expected file field \"${fieldName}\"`, withPath(keyPath, index)))\n        } else {\n          parsedValues.push(value)\n        }\n      })\n\n      if (issues.length > 0) {\n        return { issues }\n      }\n\n      return validateParsedValue(keyPath, entrySchema.schema, parsedValues, context.options)\n    }\n  }\n}\n\nfunction validateParsedValue(\n  path: NonNullable<Issue['path']>,\n  schema: Schema<any, any>,\n  value: unknown,\n  options?: ParseOptions,\n): FormDataParseResult<unknown> {\n  let result = schema['~run'](value, { path, options })\n\n  if (result.issues) {\n    return { issues: result.issues }\n  }\n\n  return {\n    value: result.value,\n  }\n}\n\nfunction shouldAbortEarly(options?: ParseOptions): boolean {\n  let libraryAbortEarly = (options?.libraryOptions as { abortEarly?: unknown } | undefined)\n    ?.abortEarly\n\n  return Boolean(options?.abortEarly ?? libraryAbortEarly)\n}\n\nfunction withPath(path: NonNullable<Issue['path']>, key: PropertyKey): NonNullable<Issue['path']> {\n  return path.length === 0 ? [key] : [...path, key]\n}\n\nfunction isFormDataSource(value: unknown): value is FormDataSource {\n  return value instanceof FormData || value instanceof URLSearchParams\n}\n"
  },
  {
    "path": "packages/data-schema/src/lib/lazy.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { lazy } from './lazy.ts'\nimport { array, object, string } from './schema.ts'\nimport type { Issue, Schema, ValidationResult } from './schema.ts'\n\ntype NodeOutput = {\n  id: string\n  children: NodeOutput[]\n}\n\nfunction assertSuccess<output>(\n  result: ValidationResult<output>,\n): asserts result is { value: output } {\n  assert.ok(!result.issues)\n}\n\nfunction assertFailure<output>(\n  result: ValidationResult<output>,\n): asserts result is { issues: ReadonlyArray<Issue> } {\n  assert.ok(result.issues)\n}\n\ndescribe('lazy', () => {\n  it('validates recursive schemas', () => {\n    let Node: Schema<unknown, NodeOutput>\n\n    Node = lazy(function () {\n      return object({\n        id: string(),\n        children: array(Node),\n      })\n    })\n\n    let result = Node['~standard'].validate({\n      id: 'root',\n      children: [{ id: 'child', children: [] }],\n    })\n\n    assertSuccess(result)\n    assert.equal(result.value.children.length, 1)\n  })\n\n  it('fails when recursive node is invalid', () => {\n    let Node: Schema<unknown, NodeOutput>\n\n    Node = lazy(function () {\n      return object({\n        id: string(),\n        children: array(Node),\n      })\n    })\n\n    let result = Node['~standard'].validate({ id: 'root', children: [{ id: 123, children: [] }] })\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['children', 0, 'id'])\n  })\n})\n"
  },
  {
    "path": "packages/data-schema/src/lib/lazy.ts",
    "content": "import { createSchema } from './schema.ts'\nimport type { InferInput, InferOutput, Schema } from './schema.ts'\n\n/**\n * Create a lazily-evaluated schema.\n *\n * This is useful for recursive schemas without circular module references.\n *\n * @param getSchema A function that returns the schema when first needed\n * @returns A schema that delegates validation to the resolved schema\n */\nexport function lazy<schema extends Schema<any, any>>(\n  getSchema: () => schema,\n): Schema<InferInput<schema>, InferOutput<schema>> {\n  let cached: schema | undefined\n\n  return createSchema(function validate(value, context) {\n    if (!cached) {\n      cached = getSchema()\n    }\n\n    return cached['~run'](value, context)\n  })\n}\n"
  },
  {
    "path": "packages/data-schema/src/lib/parse.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { minLength } from './checks.ts'\nimport { number, object, parse, parseSafe, string } from './schema.ts'\n\ndescribe('parse', () => {\n  it('returns validated output', () => {\n    let schema = object({ name: string(), age: number() })\n    let result = parse(schema, { name: 'Ada', age: 37 })\n\n    assert.deepEqual(result, { name: 'Ada', age: 37 })\n  })\n\n  it('throws ValidationError with issues', () => {\n    let schema = object({ name: string(), age: number() })\n\n    assert.throws(\n      function () {\n        parse(schema, { name: 123, age: 'x' })\n      },\n      function (error: unknown) {\n        return (\n          error instanceof Error &&\n          error.name === 'ValidationError' &&\n          'issues' in error &&\n          Array.isArray((error as { issues: unknown }).issues)\n        )\n      },\n    )\n  })\n})\n\ndescribe('parseSafe', () => {\n  it('returns success with value', () => {\n    let schema = object({ name: string(), age: number() })\n    let result = parseSafe(schema, { name: 'Ada', age: 37 })\n\n    assert.ok(result.success)\n    assert.deepEqual(result.value, { name: 'Ada', age: 37 })\n  })\n\n  it('returns issues on failure', () => {\n    let schema = object({ name: string(), age: number() })\n    let result = parseSafe(schema, { name: 123, age: 'x' })\n\n    assert.ok(!result.success)\n    assert.equal(result.issues.length, 2)\n  })\n\n  it('supports abortEarly option', () => {\n    let schema = object({ name: string(), age: number() })\n    let result = parseSafe(schema, { name: 123, age: 'x' }, { abortEarly: true })\n\n    assert.ok(!result.success)\n    assert.equal(result.issues.length, 1)\n  })\n\n  it('supports errorMap for custom issue messages', () => {\n    let schema = object({ name: string(), age: number() })\n    let result = parseSafe(\n      schema,\n      { name: 123, age: 'x' },\n      {\n        errorMap(context) {\n          if (context.code === 'type.string') {\n            return 'Expected text input'\n          }\n\n          return undefined\n        },\n      },\n    )\n\n    assert.ok(!result.success)\n    assert.equal(result.issues[0].message, 'Expected text input')\n    assert.equal(result.issues[1].message, 'Expected number')\n  })\n\n  it('passes locale and values to errorMap', () => {\n    let schema = string().pipe(minLength(3))\n    let captured: { code: string; locale: string | undefined; values: unknown } | undefined\n\n    let result = parseSafe(schema, 'ab', {\n      locale: 'es',\n      errorMap(context) {\n        captured = {\n          code: context.code,\n          locale: context.locale,\n          values: context.values,\n        }\n\n        if (context.code === 'string.min_length') {\n          return (\n            'Debe tener al menos ' + String((context.values as { min: number }).min) + ' caracteres'\n          )\n        }\n      },\n    })\n\n    assert.ok(!result.success)\n    assert.equal(result.issues[0].message, 'Debe tener al menos 3 caracteres')\n    assert.deepEqual(captured, {\n      code: 'string.min_length',\n      locale: 'es',\n      values: { min: 3 },\n    })\n  })\n\n  it('falls back to default message when errorMap returns undefined', () => {\n    let schema = number()\n    let result = parseSafe(schema, 'x', {\n      errorMap() {\n        return undefined\n      },\n    })\n\n    assert.ok(!result.success)\n    assert.equal(result.issues[0].message, 'Expected number')\n  })\n})\n"
  },
  {
    "path": "packages/data-schema/src/lib/pipe.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { minLength, maxLength } from './checks.ts'\nimport { string } from './schema.ts'\nimport type { Issue, ValidationResult } from './schema.ts'\n\nfunction assertSuccess<output>(\n  result: ValidationResult<output>,\n): asserts result is { value: output } {\n  assert.ok(!result.issues)\n}\n\nfunction assertFailure<output>(\n  result: ValidationResult<output>,\n): asserts result is { issues: ReadonlyArray<Issue> } {\n  assert.ok(result.issues)\n}\n\ndescribe('pipe', () => {\n  it('applies checks in order', () => {\n    let schema = string().pipe(minLength(2), maxLength(4))\n\n    let ok = schema['~standard'].validate('test')\n    let short = schema['~standard'].validate('a')\n    let long = schema['~standard'].validate('toolong')\n\n    assertSuccess(ok)\n    assertFailure(short)\n    assertFailure(long)\n  })\n})\n"
  },
  {
    "path": "packages/data-schema/src/lib/schema.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport {\n  any,\n  array,\n  bigint,\n  boolean,\n  defaulted,\n  enum_,\n  instanceof_,\n  literal,\n  map,\n  null_,\n  number,\n  object,\n  optional,\n  record,\n  nullable,\n  set,\n  string,\n  symbol,\n  tuple,\n  undefined_,\n  union,\n} from './schema.ts'\nimport { minLength } from './checks.ts'\nimport type { InferOutput, Issue, ValidationResult } from './schema.ts'\n\ntype Equal<left, right> =\n  (<value>() => value extends left ? 1 : 2) extends <value>() => value extends right ? 1 : 2\n    ? true\n    : false\n\nfunction expectType<condition extends true>(_value?: condition): void {}\n\nfunction assertSuccess<output>(\n  result: ValidationResult<output>,\n): asserts result is { value: output } {\n  assert.ok(!result.issues)\n}\n\nfunction assertFailure<output>(\n  result: ValidationResult<output>,\n): asserts result is { issues: ReadonlyArray<Issue> } {\n  assert.ok(result.issues)\n}\n\ndescribe('primitives', () => {\n  it('validates strings', () => {\n    let schema = string()\n    let result = schema['~standard'].validate('ok')\n\n    assertSuccess(result)\n    assert.equal(result.value, 'ok')\n  })\n\n  it('rejects non-strings', () => {\n    let schema = string()\n    let result = schema['~standard'].validate(123)\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 1)\n    assert.equal(result.issues[0].message, 'Expected string')\n  })\n\n  it('validates numbers and rejects NaN', () => {\n    let schema = number()\n    let ok = schema['~standard'].validate(42)\n\n    assertSuccess(ok)\n    assert.equal(ok.value, 42)\n\n    let bad = schema['~standard'].validate(Number.NaN)\n    assertFailure(bad)\n  })\n\n  it('validates booleans', () => {\n    let schema = boolean()\n    let result = schema['~standard'].validate(true)\n\n    assertSuccess(result)\n    assert.equal(result.value, true)\n  })\n\n  it('validates null and undefined', () => {\n    let nullSchema = null_()\n    let undefinedSchema = undefined_()\n\n    let nullResult = nullSchema['~standard'].validate(null)\n    let undefinedResult = undefinedSchema['~standard'].validate(undefined)\n\n    assertSuccess(nullResult)\n    assert.equal(nullResult.value, null)\n    assertSuccess(undefinedResult)\n    assert.equal(undefinedResult.value, undefined)\n  })\n\n  it('validates literals', () => {\n    let schema = literal('yes')\n    let ok = schema['~standard'].validate('yes')\n    let bad = schema['~standard'].validate('no')\n\n    assertSuccess(ok)\n    assert.equal(ok.value, 'yes')\n    assertFailure(bad)\n  })\n\n  it('validates bigints', () => {\n    let schema = bigint()\n    let ok = schema['~standard'].validate(BigInt(42))\n    let bad = schema['~standard'].validate(42)\n\n    assertSuccess(ok)\n    assert.equal(ok.value, BigInt(42))\n    assertFailure(bad)\n  })\n\n  it('validates symbols', () => {\n    let mySymbol = Symbol('test')\n    let schema = symbol()\n    let ok = schema['~standard'].validate(mySymbol)\n    let bad = schema['~standard'].validate('symbol')\n\n    assertSuccess(ok)\n    assert.equal(ok.value, mySymbol)\n    assertFailure(bad)\n  })\n\n  it('rejects Infinity and -Infinity for numbers', () => {\n    let schema = number()\n\n    let posInf = schema['~standard'].validate(Infinity)\n    let negInf = schema['~standard'].validate(-Infinity)\n\n    assertFailure(posInf)\n    assertFailure(negInf)\n  })\n\n  it('rejects non-booleans', () => {\n    let schema = boolean()\n    let bad = schema['~standard'].validate('true')\n\n    assertFailure(bad)\n    assert.equal(bad.issues[0].message, 'Expected boolean')\n  })\n\n  it('rejects non-null for null_ schema', () => {\n    let schema = null_()\n    let bad = schema['~standard'].validate(undefined)\n\n    assertFailure(bad)\n    assert.equal(bad.issues[0].message, 'Expected null')\n  })\n\n  it('rejects non-undefined for undefined_ schema', () => {\n    let schema = undefined_()\n    let bad = schema['~standard'].validate(null)\n\n    assertFailure(bad)\n    assert.equal(bad.issues[0].message, 'Expected undefined')\n  })\n})\n\ndescribe('array', () => {\n  it('validates array elements and provides paths', () => {\n    let schema = array(string())\n    let result = schema['~standard'].validate(['ok', 123])\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, [1])\n  })\n\n  it('returns validated array on success', () => {\n    let schema = array(number())\n    let result = schema['~standard'].validate([1, 2, 3])\n\n    assertSuccess(result)\n    assert.deepEqual(result.value, [1, 2, 3])\n  })\n\n  it('rejects non-array values', () => {\n    let schema = array(string())\n    let result = schema['~standard'].validate({ 0: 'a', length: 1 })\n\n    assertFailure(result)\n    assert.equal(result.issues[0].message, 'Expected array')\n  })\n\n  it('collects all issues by default', () => {\n    let schema = array(number())\n    let result = schema['~standard'].validate(['a', 'b', 'c'])\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 3)\n  })\n\n  it('returns first issue when abortEarly is enabled', () => {\n    let schema = array(number())\n    let result = schema['~standard'].validate(['a', 'b', 'c'], {\n      libraryOptions: { abortEarly: true },\n    })\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 1)\n    assert.deepEqual(result.issues[0].path, [0])\n  })\n})\n\ndescribe('object', () => {\n  it('strips unknown keys by default', () => {\n    let schema = object({ name: string() })\n    let result = schema['~standard'].validate({ name: 'Ada', extra: 'x' })\n\n    assertSuccess(result)\n    assert.deepEqual(result.value, { name: 'Ada' })\n  })\n\n  it('passes through unknown keys when configured', () => {\n    let schema = object({ name: string() }, { unknownKeys: 'passthrough' })\n    let result = schema['~standard'].validate({ name: 'Ada', extra: 'x' })\n\n    assertSuccess(result)\n    assert.deepEqual(result.value, { name: 'Ada', extra: 'x' })\n  })\n\n  it('errors on unknown keys when configured', () => {\n    let schema = object({ name: string() }, { unknownKeys: 'error' })\n    let result = schema['~standard'].validate({ name: 'Ada', extra: 'x' })\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['extra'])\n  })\n\n  it('rejects non-object values', () => {\n    let schema = object({ name: string() })\n\n    let stringResult = schema['~standard'].validate('not-object')\n    let arrayResult = schema['~standard'].validate(['a', 'b'])\n    let nullResult = schema['~standard'].validate(null)\n\n    assertFailure(stringResult)\n    assertFailure(arrayResult)\n    assertFailure(nullResult)\n    assert.equal(stringResult.issues[0].message, 'Expected object')\n  })\n\n  it('validates nested objects with paths', () => {\n    let schema = object({\n      user: object({\n        profile: object({\n          email: string(),\n        }),\n      }),\n    })\n\n    let result = schema['~standard'].validate({\n      user: { profile: { email: 123 } },\n    })\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['user', 'profile', 'email'])\n  })\n\n  it('collects all issues by default', () => {\n    let schema = object({ name: string(), age: number() })\n    let result = schema['~standard'].validate({ name: 123, age: 'x' })\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 2)\n  })\n\n  it('returns first issue when abortEarly is enabled', () => {\n    let schema = object({ name: string(), age: number() })\n    let result = schema['~standard'].validate(\n      { name: 123, age: 'x' },\n      { libraryOptions: { abortEarly: true } },\n    )\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 1)\n  })\n\n  it('handles missing keys by passing undefined to schema', () => {\n    let schema = object({ name: string(), age: optional(number()) })\n    let result = schema['~standard'].validate({ name: 'Ada' })\n\n    assertSuccess(result)\n    assert.deepEqual(result.value, { name: 'Ada' })\n  })\n\n  it('does not include undefined values unless present in input', () => {\n    let schema = object({ a: optional(string()), b: string() })\n    let result = schema['~standard'].validate({ b: 'hello' })\n\n    assertSuccess(result)\n    assert.ok(!('a' in result.value))\n  })\n})\n\ndescribe('modifiers', () => {\n  it('supports optional values', () => {\n    let schema = optional(string())\n    let result = schema['~standard'].validate(undefined)\n\n    assertSuccess(result)\n    assert.equal(result.value, undefined)\n  })\n\n  it('optional still validates non-undefined values', () => {\n    let schema = optional(string())\n    let ok = schema['~standard'].validate('hello')\n    let bad = schema['~standard'].validate(123)\n\n    assertSuccess(ok)\n    assertFailure(bad)\n  })\n\n  it('supports defaulted values', () => {\n    let schema = defaulted(string(), 'hello')\n    let result = schema['~standard'].validate(undefined)\n\n    assertSuccess(result)\n    assert.equal(result.value, 'hello')\n  })\n\n  it('supports defaulted with function', () => {\n    let callCount = 0\n    let schema = defaulted(number(), () => {\n      callCount += 1\n      return callCount\n    })\n\n    let result1 = schema['~standard'].validate(undefined)\n    let result2 = schema['~standard'].validate(undefined)\n\n    assertSuccess(result1)\n    assertSuccess(result2)\n    assert.equal(result1.value, 1)\n    assert.equal(result2.value, 2)\n  })\n\n  it('defaulted still validates non-undefined values', () => {\n    let schema = defaulted(number(), 0)\n    let ok = schema['~standard'].validate(42)\n    let bad = schema['~standard'].validate('not-a-number')\n\n    assertSuccess(ok)\n    assert.equal(ok.value, 42)\n    assertFailure(bad)\n  })\n\n  it('supports nullable values', () => {\n    let schema = nullable(string())\n    let result = schema['~standard'].validate(null)\n\n    assertSuccess(result)\n    assert.equal(result.value, null)\n  })\n\n  it('nullable still validates non-null values', () => {\n    let schema = nullable(string())\n    let ok = schema['~standard'].validate('hello')\n    let bad = schema['~standard'].validate(123)\n\n    assertSuccess(ok)\n    assertFailure(bad)\n  })\n\n  it('supports refine predicates', () => {\n    let schema = number().refine(function isPositive(value) {\n      return value > 0\n    })\n\n    let ok = schema['~standard'].validate(1)\n    let bad = schema['~standard'].validate(-1)\n\n    assertSuccess(ok)\n    assertFailure(bad)\n  })\n\n  it('refine uses custom message when provided', () => {\n    let schema = number().refine((value) => value > 0, 'Must be positive')\n    let result = schema['~standard'].validate(-1)\n\n    assertFailure(result)\n    assert.equal(result.issues[0].message, 'Must be positive')\n  })\n})\n\ndescribe('tuple', () => {\n  it('validates tuples and enforces length', () => {\n    let schema = tuple([string(), number()])\n    let result = schema['~standard'].validate(['ok'])\n\n    assertFailure(result)\n  })\n\n  it('returns validated tuple on success', () => {\n    let schema = tuple([string(), number(), boolean()])\n    let result = schema['~standard'].validate(['hello', 42, true])\n\n    assertSuccess(result)\n    assert.deepEqual(result.value, ['hello', 42, true])\n  })\n\n  it('rejects non-array values', () => {\n    let schema = tuple([string()])\n    let result = schema['~standard'].validate('not-array')\n\n    assertFailure(result)\n    assert.equal(result.issues[0].message, 'Expected array')\n  })\n\n  it('reports length mismatch for extra elements', () => {\n    let schema = tuple([string(), number()])\n    let result = schema['~standard'].validate(['a', 1, 'extra'])\n\n    assertFailure(result)\n    assert.ok(result.issues[0].message.includes('Expected tuple length'))\n  })\n\n  it('validates element types with paths', () => {\n    let schema = tuple([string(), number()])\n    let result = schema['~standard'].validate(['ok', 'not-a-number'])\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, [1])\n  })\n})\n\ndescribe('union', () => {\n  it('returns the first successful variant', () => {\n    let schema = union([string(), number()])\n    let result = schema['~standard'].validate(123)\n\n    assertSuccess(result)\n    assert.equal(result.value, 123)\n  })\n\n  it('fails when no variant matches', () => {\n    let schema = union([string(), number()])\n    let result = schema['~standard'].validate(false)\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 2)\n  })\n\n  it('returns first variant issues when abortEarly is enabled', () => {\n    let schema = union([string(), number()])\n    let result = schema['~standard'].validate(false, { libraryOptions: { abortEarly: true } })\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 1)\n    assert.equal(result.issues[0].message, 'Expected string')\n  })\n\n  it('handles empty schemas array', () => {\n    let schema = union([])\n    let result = schema['~standard'].validate('anything')\n\n    assertFailure(result)\n    assert.equal(result.issues[0].message, 'No union variant matched')\n  })\n})\n\ndescribe('record', () => {\n  it('validates record values', () => {\n    let schema = record(string(), number())\n    let result = schema['~standard'].validate({ a: 1, b: 2 })\n\n    assertSuccess(result)\n    assert.deepEqual(result.value, { a: 1, b: 2 })\n  })\n\n  it('reports invalid record values with paths', () => {\n    let schema = record(string(), number())\n    let result = schema['~standard'].validate({ a: 'nope' })\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['a'])\n  })\n\n  it('validates record keys', () => {\n    let emailKey = string().refine((s) => s.includes('@'))\n    let schema = record(emailKey, number())\n    let result = schema['~standard'].validate({ 'not-an-email': 1 })\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['not-an-email'])\n  })\n\n  it('rejects non-object values', () => {\n    let schema = record(string(), number())\n\n    let arrayResult = schema['~standard'].validate([1, 2])\n    let nullResult = schema['~standard'].validate(null)\n    let primitiveResult = schema['~standard'].validate('string')\n\n    assertFailure(arrayResult)\n    assertFailure(nullResult)\n    assertFailure(primitiveResult)\n    assert.equal(arrayResult.issues[0].message, 'Expected object')\n  })\n\n  it('handles empty records', () => {\n    let schema = record(string(), number())\n    let result = schema['~standard'].validate({})\n\n    assertSuccess(result)\n    assert.deepEqual(result.value, {})\n  })\n})\n\ndescribe('map', () => {\n  it('validates map entries', () => {\n    let schema = map(string(), number())\n    let result = schema['~standard'].validate(\n      new Map([\n        ['a', 1],\n        ['b', 2],\n      ]),\n    )\n\n    assertSuccess(result)\n    assert.ok(result.value instanceof Map)\n    assert.equal(result.value.get('a'), 1)\n    assert.equal(result.value.get('b'), 2)\n  })\n\n  it('reports invalid map values with paths', () => {\n    let schema = map(string(), number())\n    let result = schema['~standard'].validate(new Map([['a', 'nope']]))\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['a'])\n  })\n\n  it('validates map keys', () => {\n    let emailKey = string().refine((s) => s.includes('@'))\n    let schema = map(emailKey, number())\n    let result = schema['~standard'].validate(new Map([['not-an-email', 1]]))\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['not-an-email'])\n  })\n\n  it('rejects non-Map values', () => {\n    let schema = map(string(), number())\n\n    let objectResult = schema['~standard'].validate({ a: 1 })\n    let arrayResult = schema['~standard'].validate([1, 2])\n    let nullResult = schema['~standard'].validate(null)\n\n    assertFailure(objectResult)\n    assertFailure(arrayResult)\n    assertFailure(nullResult)\n    assert.equal(objectResult.issues[0].message, 'Expected Map')\n  })\n\n  it('handles empty maps', () => {\n    let schema = map(string(), number())\n    let result = schema['~standard'].validate(new Map())\n\n    assertSuccess(result)\n    assert.ok(result.value instanceof Map)\n    assert.equal(result.value.size, 0)\n  })\n\n  it('supports non-string keys', () => {\n    let schema = map(number(), string())\n    let result = schema['~standard'].validate(\n      new Map([\n        [1, 'one'],\n        [2, 'two'],\n      ]),\n    )\n\n    assertSuccess(result)\n    assert.equal(result.value.get(1), 'one')\n    assert.equal(result.value.get(2), 'two')\n  })\n})\n\ndescribe('set', () => {\n  it('validates set values', () => {\n    let schema = set(number())\n    let result = schema['~standard'].validate(new Set([1, 2, 3]))\n\n    assertSuccess(result)\n    assert.ok(result.value instanceof Set)\n    assert.equal(result.value.size, 3)\n    assert.ok(result.value.has(1))\n    assert.ok(result.value.has(2))\n    assert.ok(result.value.has(3))\n  })\n\n  it('reports invalid set values with paths', () => {\n    let schema = set(number())\n    let result = schema['~standard'].validate(new Set([1, 'nope', 3]))\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, [1])\n  })\n\n  it('rejects non-Set values', () => {\n    let schema = set(number())\n\n    let arrayResult = schema['~standard'].validate([1, 2, 3])\n    let objectResult = schema['~standard'].validate({ a: 1 })\n    let nullResult = schema['~standard'].validate(null)\n\n    assertFailure(arrayResult)\n    assertFailure(objectResult)\n    assertFailure(nullResult)\n    assert.equal(arrayResult.issues[0].message, 'Expected Set')\n  })\n\n  it('handles empty sets', () => {\n    let schema = set(string())\n    let result = schema['~standard'].validate(new Set())\n\n    assertSuccess(result)\n    assert.ok(result.value instanceof Set)\n    assert.equal(result.value.size, 0)\n  })\n\n  it('validates complex values', () => {\n    let schema = set(object({ id: number() }))\n    let result = schema['~standard'].validate(new Set([{ id: 1 }, { id: 2 }]))\n\n    assertSuccess(result)\n    assert.equal(result.value.size, 2)\n  })\n\n  it('collects all issues by default', () => {\n    let schema = set(number())\n    let result = schema['~standard'].validate(new Set(['a', 'b', 'c']))\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 3)\n  })\n})\n\ndescribe('any', () => {\n  it('accepts any value', () => {\n    let schema = any()\n\n    assertSuccess(schema['~standard'].validate('hello'))\n    assertSuccess(schema['~standard'].validate(42))\n    assertSuccess(schema['~standard'].validate(null))\n    assertSuccess(schema['~standard'].validate(undefined))\n    assertSuccess(schema['~standard'].validate({ a: 1 }))\n    assertSuccess(schema['~standard'].validate([1, 2]))\n  })\n\n  it('preserves the original value', () => {\n    let schema = any()\n    let obj = { nested: true }\n    let result = schema['~standard'].validate(obj)\n\n    assertSuccess(result)\n    assert.equal(result.value, obj)\n  })\n})\n\ndescribe('enum_', () => {\n  it('accepts allowed values', () => {\n    let schema = enum_(['active', 'inactive', 'pending'])\n\n    assertSuccess(schema['~standard'].validate('active'))\n    assertSuccess(schema['~standard'].validate('inactive'))\n    assertSuccess(schema['~standard'].validate('pending'))\n  })\n\n  it('rejects values not in the list', () => {\n    let schema = enum_(['active', 'inactive'])\n    let result = schema['~standard'].validate('deleted')\n\n    assertFailure(result)\n    assert.ok(result.issues[0].message.includes('active'))\n    assert.ok(result.issues[0].message.includes('inactive'))\n  })\n\n  it('works with numeric values', () => {\n    let schema = enum_([0, 1, 2])\n\n    assertSuccess(schema['~standard'].validate(1))\n    assertFailure(schema['~standard'].validate(3))\n  })\n\n  it('uses strict equality', () => {\n    let schema = enum_([1, 2, 3])\n\n    assertFailure(schema['~standard'].validate('1'))\n  })\n\n  it('infers literal types without as const', () => {\n    let stringEnum = enum_(['active', 'inactive', 'pending'])\n    expectType<Equal<InferOutput<typeof stringEnum>, 'active' | 'inactive' | 'pending'>>()\n\n    let numericEnum = enum_([0, 1, 2])\n    expectType<Equal<InferOutput<typeof numericEnum>, 0 | 1 | 2>>()\n  })\n})\n\ndescribe('instanceof_', () => {\n  it('accepts instances of the class', () => {\n    let schema = instanceof_(Date)\n    let date = new Date()\n    let result = schema['~standard'].validate(date)\n\n    assertSuccess(result)\n    assert.equal(result.value, date)\n  })\n\n  it('rejects non-instances', () => {\n    let schema = instanceof_(Date)\n    let result = schema['~standard'].validate('2025-01-01')\n\n    assertFailure(result)\n    assert.ok(result.issues[0].message.includes('Date'))\n  })\n\n  it('works with custom classes', () => {\n    class MyClass {\n      value = 42\n    }\n\n    let schema = instanceof_(MyClass)\n\n    assertSuccess(schema['~standard'].validate(new MyClass()))\n    assertFailure(schema['~standard'].validate({ value: 42 }))\n  })\n\n  it('works with subclasses', () => {\n    class Base {}\n    class Child extends Base {}\n\n    let schema = instanceof_(Base)\n\n    assertSuccess(schema['~standard'].validate(new Child()))\n  })\n\n  it('provides path in nested contexts', () => {\n    let schema = object({ created: instanceof_(Date) })\n    let result = schema['~standard'].validate({ created: 'not-a-date' })\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['created'])\n  })\n})\n\ndescribe('modifiers (additional)', () => {\n  it('refine propagates path inside objects', () => {\n    let schema = object({ age: number().refine((v) => v > 0, 'Must be positive') })\n    let result = schema['~standard'].validate({ age: -1 })\n\n    assertFailure(result)\n    assert.equal(result.issues[0].message, 'Must be positive')\n    assert.deepEqual(result.issues[0].path, ['age'])\n  })\n\n  it('defaulted fills missing keys in objects', () => {\n    let schema = object({ name: string(), role: defaulted(string(), 'user') })\n    let result = schema['~standard'].validate({ name: 'Ada' })\n\n    assertSuccess(result)\n    assert.deepEqual(result.value, { name: 'Ada', role: 'user' })\n  })\n\n  it('defaulted fills explicit undefined in objects', () => {\n    let schema = object({ role: defaulted(string(), 'user') })\n    let result = schema['~standard'].validate({ role: undefined })\n\n    assertSuccess(result)\n    assert.deepEqual(result.value, { role: 'user' })\n  })\n\n  it('chains pipe then refine', () => {\n    let schema = string()\n      .pipe(minLength(3))\n      .refine((s) => s.startsWith('a'), 'Must start with a')\n\n    assertSuccess(schema['~standard'].validate('abc'))\n    assertFailure(schema['~standard'].validate('ab'))\n    assertFailure(schema['~standard'].validate('bcd'))\n  })\n\n  it('chains refine then pipe', () => {\n    let schema = string()\n      .refine((s) => s.startsWith('a'), 'Must start with a')\n      .pipe(minLength(3))\n\n    assertSuccess(schema['~standard'].validate('abc'))\n    assertFailure(schema['~standard'].validate('bcd'))\n    assertFailure(schema['~standard'].validate('ab'))\n  })\n})\n\ndescribe('abortEarly', () => {\n  it('collects all issues by default', () => {\n    let schema = object({ name: string(), age: number() })\n    let result = schema['~standard'].validate({ name: 123, age: 'x' })\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 2)\n  })\n\n  it('returns the first issue when enabled', () => {\n    let schema = object({ name: string(), age: number() })\n    let result = schema['~standard'].validate(\n      { name: 123, age: 'x' },\n      { libraryOptions: { abortEarly: true } },\n    )\n\n    assertFailure(result)\n    assert.equal(result.issues.length, 1)\n  })\n})\n"
  },
  {
    "path": "packages/data-schema/src/lib/schema.ts",
    "content": "import type { StandardSchemaV1 } from '@standard-schema/spec'\n\n/**\n * A validation issue returned by a schema, compatible with Standard Schema v1.\n *\n * Issues include a human-readable `message` and an optional `path` that points to the\n * failing location in the input (e.g. `['user', 'email']` or `[0, 'id']`).\n */\nexport type Issue = StandardSchemaV1.Issue\n\n/**\n * The result of schema validation.\n *\n * On success, `value` is present and `issues` is absent. On failure, `issues` is present.\n */\nexport type ValidationResult<output> = StandardSchemaV1.Result<output>\n\n/**\n * Options passed to `~standard.validate`.\n */\nexport type ValidationOptions = StandardSchemaV1.Options\n\n/**\n * Context passed to `errorMap` to customize issue messages.\n */\nexport type ErrorMapContext = {\n  code: string\n  defaultMessage: string\n  path?: Issue['path']\n  values?: Record<string, unknown>\n  input: unknown\n  locale?: string\n}\n\n/**\n * Function used to customize issue messages.\n *\n * Return `undefined` to use the default message.\n */\nexport type ErrorMap = (context: ErrorMapContext) => string | undefined\n\n/**\n * Options passed to {@link parse} and {@link parseSafe}.\n *\n * This mirrors {@link ValidationOptions}, but also supports a convenience `abortEarly`\n * option at the top level.\n */\nexport type ParseOptions = StandardSchemaV1.Options & {\n  abortEarly?: boolean\n  errorMap?: ErrorMap\n  locale?: string\n}\n\ntype SyncStandardSchemaProps<input, output> = Omit<\n  StandardSchemaV1.Props<input, output>,\n  'validate'\n> & {\n  // data-schema is sync-first; keep validate sync.\n  validate: (value: unknown, options?: ValidationOptions) => ValidationResult<output>\n  // Preserve Standard Schema's compile-time type channel.\n  types?: StandardSchemaV1.Types<input, output> | undefined\n}\n\ntype SyncStandardSchema<input, output = input> = {\n  '~standard': SyncStandardSchemaProps<input, output>\n}\n\n/**\n * A reusable check for use with `schema.pipe(...)`.\n */\nexport type Check<output> = {\n  check: (value: output) => boolean\n  message?: string\n  code?: string\n  values?: Record<string, unknown>\n}\n\n/**\n * A sync, Standard Schema v1-compatible schema with a small chainable API.\n */\nexport type Schema<input, output = input> = SyncStandardSchema<input, output> & {\n  /**\n   * Compose one or more reusable checks onto this schema.\n   *\n   * Checks run after the underlying schema has validated and produced an `output` value.\n   * If any check fails, validation fails with an issue (using the check's `message` if provided).\n   *\n   * @param checks One or more `Check`s to apply in order\n   * @returns A new schema with the checks applied\n   */\n  pipe: (...checks: Check<output>[]) => Schema<input, output>\n  /**\n   * Add an inline predicate check onto this schema.\n   *\n   * The predicate runs after the underlying schema has validated and produced an `output` value.\n   * If the predicate returns `false`, validation fails with an issue.\n   *\n   * @param predicate A function that returns `true` for valid values\n   * @param message Optional issue message when the predicate fails\n   * @returns A new schema with the refinement applied\n   */\n  refine: (predicate: (value: output) => boolean, message?: string) => Schema<input, output>\n  /**\n   * Internal validator used to validate nested values while preserving `path`/`options`.\n   */\n  '~run': (value: unknown, context: ValidationContext) => ValidationResult<output>\n}\n\n/**\n * Infers the input type of a schema-like value.\n */\nexport type InferInput<schema> = schema extends StandardSchemaV1<infer input, any> ? input : never\n\n/**\n * Infers the output type of a schema-like value.\n */\nexport type InferOutput<schema> =\n  schema extends StandardSchemaV1<any, infer output> ? output : never\n\ntype ValidationContext = {\n  path: NonNullable<Issue['path']>\n  options?: ParseOptions\n}\n\ntype IssueDescriptor = {\n  code: string\n  defaultMessage: string\n  input: unknown\n  path?: Issue['path']\n  values?: Record<string, unknown>\n}\n\n/**\n * Creates a sync Standard Schema-compatible schema from a validation function.\n *\n * @param validator Validator that returns either a parsed value or validation issues.\n * @returns A chainable schema object.\n */\nexport function createSchema<input, output>(\n  validator: (\n    value: unknown,\n    context: { path: NonNullable<Issue['path']>; options?: ParseOptions },\n  ) => ValidationResult<output>,\n): Schema<input, output> {\n  let schema: Schema<input, output> = {\n    '~standard': {\n      version: 1,\n      vendor: 'data-schema',\n      validate(value: unknown, options?: ValidationOptions) {\n        return validator(value, { path: [], options })\n      },\n    },\n    '~run'(value: unknown, context: ValidationContext) {\n      return validator(value, context)\n    },\n    pipe(...checks: Check<output>[]) {\n      if (checks.length === 0) {\n        return schema\n      }\n\n      return createSchema(function validate(value, context) {\n        let result = schema['~run'](value, context)\n\n        if (result.issues) {\n          return result\n        }\n\n        for (let check of checks) {\n          if (!check.check(result.value)) {\n            if (!check.code) {\n              return { issues: [createIssue(check.message ?? 'Check failed', context.path)] }\n            }\n\n            return {\n              issues: [\n                createIssueFromContext(context, {\n                  code: check.code,\n                  defaultMessage: check.message ?? 'Check failed',\n                  input: result.value,\n                  values: check.values,\n                }),\n              ],\n            }\n          }\n        }\n\n        return result\n      })\n    },\n    refine(predicate: (value: output) => boolean, message?: string) {\n      return createSchema<input, output>(function validate(value, context) {\n        let result = schema['~run'](value, context)\n\n        if (result.issues) {\n          return result\n        }\n\n        if (!predicate(result.value)) {\n          if (message !== undefined) {\n            return { issues: [createIssue(message, context.path)] }\n          }\n\n          return {\n            issues: [\n              createIssueFromContext(context, {\n                code: 'refine.failed',\n                defaultMessage: 'Refinement failed',\n                input: result.value,\n              }),\n            ],\n          }\n        }\n\n        return result\n      })\n    },\n  }\n\n  return schema\n}\n\nfunction shouldAbortEarly(options?: ParseOptions): boolean {\n  let libraryAbortEarly = (options?.libraryOptions as { abortEarly?: unknown } | undefined)\n    ?.abortEarly\n  let abortEarly = options?.abortEarly ?? libraryAbortEarly\n  return Boolean(abortEarly)\n}\n\nfunction withPath(path: NonNullable<Issue['path']>, key: PropertyKey): NonNullable<Issue['path']> {\n  return path.length === 0 ? [key] : [...path, key]\n}\n\nfunction getErrorMap(options?: ParseOptions): ErrorMap | undefined {\n  let libraryErrorMap = (options?.libraryOptions as { errorMap?: unknown } | undefined)?.errorMap\n\n  if (typeof options?.errorMap === 'function') {\n    return options.errorMap\n  }\n\n  if (typeof libraryErrorMap === 'function') {\n    return libraryErrorMap as ErrorMap\n  }\n}\n\nfunction getLocale(options?: ParseOptions): string | undefined {\n  let libraryLocale = (options?.libraryOptions as { locale?: unknown } | undefined)?.locale\n\n  if (typeof options?.locale === 'string') {\n    return options.locale\n  }\n\n  if (typeof libraryLocale === 'string') {\n    return libraryLocale\n  }\n}\n\nfunction resolveIssueMessage(options: ParseOptions | undefined, context: ErrorMapContext): string {\n  let errorMap = getErrorMap(options)\n\n  if (!errorMap) {\n    return context.defaultMessage\n  }\n\n  let message = errorMap(context)\n  return message ?? context.defaultMessage\n}\n\nfunction createIssueFromContext(context: ValidationContext, descriptor: IssueDescriptor): Issue {\n  let path = descriptor.path ?? context.path\n  let message = resolveIssueMessage(context.options, {\n    code: descriptor.code,\n    defaultMessage: descriptor.defaultMessage,\n    path,\n    values: descriptor.values,\n    input: descriptor.input,\n    locale: getLocale(context.options),\n  })\n\n  return createIssue(message, path)\n}\n\n/**\n * Creates a Standard Schema issue object.\n *\n * @param message Human-readable validation message.\n * @param path Optional issue path within the input value.\n * @returns A Standard Schema issue.\n */\nexport function createIssue(message: string, path: Issue['path']): Issue {\n  return !path || path.length === 0 ? { message } : { message, path }\n}\n\n/**\n * Creates a Standard Schema failure result with a single issue.\n *\n * @param message Human-readable validation message.\n * @param path Optional issue path within the input value.\n * @param options Optional issue metadata used for localized error mapping.\n * @param options.code Optional error code passed to the error map.\n * @param options.values Optional values passed to the error map.\n * @param options.input Optional input value passed to the error map.\n * @param options.parseOptions Optional parse options used for localization and error mapping.\n * @returns A failure result containing one issue.\n */\nexport function fail(\n  message: string,\n  path: Issue['path'],\n  options?: {\n    code?: string\n    values?: Record<string, unknown>\n    input?: unknown\n    parseOptions?: ParseOptions\n  },\n): StandardSchemaV1.FailureResult {\n  if (!options?.code) {\n    return { issues: [createIssue(message, path)] }\n  }\n\n  let resolvedMessage = resolveIssueMessage(options.parseOptions, {\n    code: options.code,\n    defaultMessage: message,\n    path,\n    values: options.values,\n    input: options.input,\n    locale: getLocale(options.parseOptions),\n  })\n\n  return { issues: [createIssue(resolvedMessage, path)] }\n}\n\n/**\n * Create a schema that accepts any value without validation.\n *\n * @returns A schema that produces `unknown`\n */\nexport function any(): Schema<any, unknown> {\n  return createSchema(function validate(value) {\n    return { value }\n  })\n}\n\n/**\n * Create a schema that validates an array by validating each element with `elementSchema`.\n *\n * @param elementSchema The schema to validate each element\n * @returns A schema that produces an array of validated outputs\n */\nexport function array<input, output>(\n  elementSchema: Schema<input, output>,\n): Schema<unknown, output[]> {\n  return createSchema(function validate(value, context) {\n    if (!Array.isArray(value)) {\n      return fail('Expected array', context.path, {\n        code: 'type.array',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    let abortEarly = shouldAbortEarly(context.options)\n    let issues: Issue[] = []\n    let outputValues: output[] = []\n    let index = 0\n\n    for (let item of value) {\n      let result = elementSchema['~run'](item, {\n        path: withPath(context.path, index),\n        options: context.options,\n      })\n\n      if (result.issues) {\n        if (abortEarly) {\n          return result\n        }\n\n        issues.push(...result.issues)\n      } else {\n        outputValues.push(result.value)\n      }\n\n      index += 1\n    }\n\n    if (issues.length > 0) {\n      return { issues }\n    }\n\n    return { value: outputValues }\n  })\n}\n\n/**\n * Create a schema that accepts bigints.\n *\n * @returns A schema that produces a `bigint`\n */\nexport function bigint(): Schema<unknown, bigint> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'bigint') {\n      return fail('Expected bigint', context.path, {\n        code: 'type.bigint',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    return { value }\n  })\n}\n\n/**\n * Create a schema that accepts booleans.\n *\n * @returns A schema that produces a `boolean`\n */\nexport function boolean(): Schema<unknown, boolean> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'boolean') {\n      return fail('Expected boolean', context.path, {\n        code: 'type.boolean',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    return { value }\n  })\n}\n\n/**\n * Provide a default when the input is `undefined`.\n *\n * @param schema The wrapped schema\n * @param defaultValue A value or function used to produce the default\n * @returns A schema that produces the default when the input is `undefined`\n */\nexport function defaulted<input, output>(\n  schema: Schema<input, output>,\n  defaultValue: output | (() => output),\n): Schema<input | undefined, output> {\n  return createSchema(function validate(value, context) {\n    if (value === undefined) {\n      let resolved =\n        typeof defaultValue === 'function' ? (defaultValue as () => output)() : defaultValue\n\n      return { value: resolved }\n    }\n\n    return schema['~run'](value, context)\n  })\n}\n\n/**\n * Create a schema that accepts one of the given values using strict equality (`===`).\n *\n * @param values The allowed values\n * @returns A schema that produces the union of allowed value types\n */\nexport function enum_<const values extends readonly [unknown, ...unknown[]]>(\n  values: values,\n): Schema<unknown, values[number]> {\n  return createSchema(function validate(value, context) {\n    for (let allowed of values) {\n      if (value === allowed) {\n        return { value: value as values[number] }\n      }\n    }\n\n    return fail('Expected one of: ' + values.map(String).join(', '), context.path, {\n      code: 'enum.invalid_value',\n      input: value,\n      values: { values: [...values] },\n      parseOptions: context.options,\n    })\n  })\n}\n\n/**\n * Create a schema that validates a value is an instance of a class.\n *\n * @param constructor The class constructor to check against\n * @returns A schema that produces the instance type\n */\nexport function instanceof_<constructor extends abstract new (...args: any[]) => any>(\n  constructor: constructor,\n): Schema<unknown, InstanceType<constructor>> {\n  return createSchema(function validate(value, context) {\n    if (!(value instanceof constructor)) {\n      return fail('Expected instance of ' + constructor.name, context.path, {\n        code: 'instanceof.invalid_type',\n        input: value,\n        values: { constructorName: constructor.name },\n        parseOptions: context.options,\n      })\n    }\n\n    return { value: value as InstanceType<constructor> }\n  })\n}\n\n/**\n * Create a schema that accepts a single literal value using strict equality (`===`).\n *\n * @param literalValue The literal value to match\n * @returns A schema that produces the literal type\n */\nexport function literal<value>(literalValue: value): Schema<unknown, value> {\n  return createSchema(function validate(value, context) {\n    if (value !== literalValue) {\n      return fail('Expected literal value', context.path, {\n        code: 'literal.invalid_value',\n        input: value,\n        values: { expected: literalValue },\n        parseOptions: context.options,\n      })\n    }\n\n    return { value: literalValue }\n  })\n}\n\n/**\n * Create a schema that validates a Map with typed keys and values.\n *\n * @param keySchema Schema for Map keys\n * @param valueSchema Schema for Map values\n * @returns A schema that produces a `Map<keyOutput, valueOutput>`\n */\nexport function map<keyInput, keyOutput, valueInput, valueOutput>(\n  keySchema: Schema<keyInput, keyOutput>,\n  valueSchema: Schema<valueInput, valueOutput>,\n): Schema<unknown, Map<keyOutput, valueOutput>> {\n  return createSchema(function validate(value, context) {\n    if (!(value instanceof Map)) {\n      return fail('Expected Map', context.path, {\n        code: 'type.map',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    let abortEarly = shouldAbortEarly(context.options)\n    let issues: Issue[] = []\n    let outputMap = new Map<keyOutput, valueOutput>()\n\n    for (let [key, val] of value) {\n      let keyResult = keySchema['~run'](key, {\n        path: withPath(context.path, key),\n        options: context.options,\n      })\n\n      if (keyResult.issues) {\n        if (abortEarly) {\n          return keyResult\n        }\n\n        issues.push(...keyResult.issues)\n        continue\n      }\n\n      let valueResult = valueSchema['~run'](val, {\n        path: withPath(context.path, key),\n        options: context.options,\n      })\n\n      if (valueResult.issues) {\n        if (abortEarly) {\n          return valueResult\n        }\n\n        issues.push(...valueResult.issues)\n        continue\n      }\n\n      outputMap.set(keyResult.value, valueResult.value)\n    }\n\n    if (issues.length > 0) {\n      return { issues }\n    }\n\n    return { value: outputMap }\n  })\n}\n\n/**\n * Create a schema that accepts `null`.\n *\n * @returns A schema that produces `null`\n */\nexport function null_(): Schema<unknown, null> {\n  return createSchema(function validate(value, context) {\n    if (value !== null) {\n      return fail('Expected null', context.path, {\n        code: 'type.null',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    return { value: null }\n  })\n}\n\n/**\n * Allow `null` as an input value, short-circuiting validation when `null` is provided.\n *\n * @param schema The wrapped schema\n * @returns A schema that accepts `null` in addition to the wrapped schema\n */\nexport function nullable<input, output>(\n  schema: Schema<input, output>,\n): Schema<input | null, output | null> {\n  return createSchema<input | null, output | null>(function validate(value, context) {\n    if (value === null) {\n      return { value: null }\n    }\n\n    return schema['~run'](value, context)\n  })\n}\n\n/**\n * Create a schema that accepts finite numbers (excluding `NaN` and `Infinity`).\n *\n * @returns A schema that produces a `number`\n */\nexport function number(): Schema<unknown, number> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'number' || !Number.isFinite(value)) {\n      return fail('Expected number', context.path, {\n        code: 'type.number',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    return { value }\n  })\n}\n\ntype ObjectShape = Record<string, Schema<any, any>>\n\ntype ObjectOptions = {\n  unknownKeys?: 'strip' | 'passthrough' | 'error'\n}\n\n/**\n * Create a schema that validates an object with a fixed shape.\n *\n * By default, unknown keys are stripped. You can change this via `options.unknownKeys`.\n *\n * @param shape A mapping of keys to schemas\n * @param options Controls unknown key behavior\n * @returns A schema that produces a typed object matching the shape\n */\nexport function object<shape extends ObjectShape>(\n  shape: shape,\n  options?: ObjectOptions,\n): Schema<unknown, { [key in keyof shape]: InferOutput<shape[key]> }> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n      return fail('Expected object', context.path, {\n        code: 'type.object',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    let abortEarly = shouldAbortEarly(context.options)\n    let issues: Issue[] = []\n    let outputValues: Record<string, unknown> = {}\n    let input = value as Record<string, unknown>\n    let unknownKeys = options?.unknownKeys ?? 'strip'\n\n    for (let key of Object.keys(shape)) {\n      let result = shape[key]['~run'](input[key], {\n        path: withPath(context.path, key),\n        options: context.options,\n      })\n\n      if (result.issues) {\n        if (abortEarly) {\n          return result\n        }\n\n        issues.push(...result.issues)\n      } else {\n        if (Object.prototype.hasOwnProperty.call(input, key) || result.value !== undefined) {\n          outputValues[key] = result.value\n        }\n      }\n    }\n\n    if (unknownKeys === 'passthrough' || unknownKeys === 'error') {\n      for (let key in input) {\n        if (!Object.prototype.hasOwnProperty.call(input, key)) {\n          continue\n        }\n\n        if (Object.prototype.hasOwnProperty.call(shape, key)) {\n          continue\n        }\n\n        if (unknownKeys === 'passthrough') {\n          outputValues[key] = input[key]\n        } else {\n          let issue = createIssueFromContext(context, {\n            code: 'object.unknown_key',\n            defaultMessage: 'Unknown key',\n            input: input[key],\n            path: withPath(context.path, key),\n            values: { key },\n          })\n\n          if (abortEarly) {\n            return { issues: [issue] }\n          }\n\n          issues.push(issue)\n        }\n      }\n    }\n\n    if (issues.length > 0) {\n      return { issues }\n    }\n\n    return { value: outputValues as { [key in keyof shape]: InferOutput<shape[key]> } }\n  })\n}\n\n/**\n * Allow `undefined` as an input value, short-circuiting validation when `undefined` is provided.\n *\n * @param schema The wrapped schema\n * @returns A schema that accepts `undefined` in addition to the wrapped schema\n */\nexport function optional<input, output>(\n  schema: Schema<input, output>,\n): Schema<input | undefined, output | undefined> {\n  return createSchema<input | undefined, output | undefined>(function validate(value, context) {\n    if (value === undefined) {\n      return { value: undefined }\n    }\n\n    return schema['~run'](value, context)\n  })\n}\n\n/**\n * Create a schema that validates a record (object map) by validating each key and value.\n *\n * @param keySchema Schema used to validate and transform each key\n * @param valueSchema Schema used to validate and transform each value\n * @returns A schema that produces a record of validated keys and values\n */\nexport function record<keyInput, keyOutput extends PropertyKey, valueInput, valueOutput>(\n  keySchema: Schema<keyInput, keyOutput>,\n  valueSchema: Schema<valueInput, valueOutput>,\n): Schema<unknown, Record<keyOutput, valueOutput>> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n      return fail('Expected object', context.path, {\n        code: 'type.object',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    let abortEarly = shouldAbortEarly(context.options)\n    let issues: Issue[] = []\n    let outputValues: Record<PropertyKey, valueOutput> = {}\n    let input = value as Record<string, unknown>\n\n    for (let key in input) {\n      if (!Object.prototype.hasOwnProperty.call(input, key)) {\n        continue\n      }\n\n      let keyResult = keySchema['~run'](key, {\n        path: withPath(context.path, key),\n        options: context.options,\n      })\n\n      if (keyResult.issues) {\n        if (abortEarly) {\n          return keyResult\n        }\n\n        issues.push(...keyResult.issues)\n        continue\n      }\n\n      let valueResult = valueSchema['~run'](input[key], {\n        path: withPath(context.path, key),\n        options: context.options,\n      })\n\n      if (valueResult.issues) {\n        if (abortEarly) {\n          return valueResult\n        }\n\n        issues.push(...valueResult.issues)\n        continue\n      }\n\n      outputValues[keyResult.value] = valueResult.value\n    }\n\n    if (issues.length > 0) {\n      return { issues }\n    }\n\n    return { value: outputValues as Record<keyOutput, valueOutput> }\n  })\n}\n\n/**\n * Create a schema that validates a Set with typed values.\n *\n * @param valueSchema Schema for Set values\n * @returns A schema that produces a `Set<valueOutput>`\n */\nexport function set<valueInput, valueOutput>(\n  valueSchema: Schema<valueInput, valueOutput>,\n): Schema<unknown, Set<valueOutput>> {\n  return createSchema(function validate(value, context) {\n    if (!(value instanceof Set)) {\n      return fail('Expected Set', context.path, {\n        code: 'type.set',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    let abortEarly = shouldAbortEarly(context.options)\n    let issues: Issue[] = []\n    let outputSet = new Set<valueOutput>()\n    let index = 0\n\n    for (let item of value) {\n      let result = valueSchema['~run'](item, {\n        path: withPath(context.path, index),\n        options: context.options,\n      })\n\n      if (result.issues) {\n        if (abortEarly) {\n          return result\n        }\n\n        issues.push(...result.issues)\n      } else {\n        outputSet.add(result.value)\n      }\n\n      index++\n    }\n\n    if (issues.length > 0) {\n      return { issues }\n    }\n\n    return { value: outputSet }\n  })\n}\n\n/**\n * Create a schema that accepts strings.\n *\n * @returns A schema that produces a `string`\n */\nexport function string(): Schema<unknown, string> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'string') {\n      return fail('Expected string', context.path, {\n        code: 'type.string',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    return { value }\n  })\n}\n\n/**\n * Create a schema that accepts symbols.\n *\n * @returns A schema that produces a `symbol`\n */\nexport function symbol(): Schema<unknown, symbol> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'symbol') {\n      return fail('Expected symbol', context.path, {\n        code: 'type.symbol',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    return { value }\n  })\n}\n\n/**\n * Create a schema that validates a fixed-length tuple.\n *\n * @param items Schemas for each tuple position\n * @returns A schema that produces a typed tuple\n */\nexport function tuple<items extends Schema<any, any>[]>(\n  items: items,\n): Schema<unknown, { [index in keyof items]: InferOutput<items[index]> }> {\n  return createSchema(function validate(value, context) {\n    if (!Array.isArray(value)) {\n      return fail('Expected array', context.path, {\n        code: 'type.array',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    let abortEarly = shouldAbortEarly(context.options)\n    let issues: Issue[] = []\n    let outputValues: unknown[] = []\n\n    if (value.length !== items.length) {\n      let issue = createIssueFromContext(context, {\n        code: 'tuple.length',\n        defaultMessage: 'Expected tuple length ' + String(items.length),\n        input: value,\n        values: { length: items.length },\n      })\n\n      if (abortEarly) {\n        return { issues: [issue] }\n      }\n\n      issues.push(issue)\n    }\n\n    let index = 0\n    let max = Math.min(value.length, items.length)\n\n    while (index < max) {\n      let result = items[index]['~run'](value[index], {\n        path: withPath(context.path, index),\n        options: context.options,\n      })\n\n      if (result.issues) {\n        if (abortEarly) {\n          return result\n        }\n\n        issues.push(...result.issues)\n      } else {\n        outputValues[index] = result.value\n      }\n\n      index += 1\n    }\n\n    if (issues.length > 0) {\n      return { issues }\n    }\n\n    return { value: outputValues as { [index in keyof items]: InferOutput<items[index]> } }\n  })\n}\n\n/**\n * Create a schema that accepts `undefined`.\n *\n * @returns A schema that produces `undefined`\n */\nexport function undefined_(): Schema<unknown, undefined> {\n  return createSchema(function validate(value, context) {\n    if (value !== undefined) {\n      return fail('Expected undefined', context.path, {\n        code: 'type.undefined',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    return { value: undefined }\n  })\n}\n\n/**\n * Create a discriminated-union schema.\n *\n * The returned schema expects an object with a `discriminator` property and selects a variant schema\n * based on that value.\n *\n * @param discriminator The property name used to select a variant\n * @param variants A mapping from discriminator value to schema\n * @returns A schema that produces the selected variant output type\n */\nexport function variant<\n  key extends PropertyKey,\n  variants extends Record<PropertyKey, Schema<any, any>>,\n>(discriminator: key, variants: variants): Schema<unknown, InferOutput<variants[keyof variants]>> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n      return fail('Expected object', context.path, {\n        code: 'type.object',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    let input = value as Record<PropertyKey, unknown>\n    let tag = input[discriminator]\n\n    if (tag === undefined) {\n      return fail('Expected discriminator', [...context.path, discriminator], {\n        code: 'variant.missing_discriminator',\n        input: value,\n        values: { discriminator: String(discriminator) },\n        parseOptions: context.options,\n      })\n    }\n\n    if (typeof tag !== 'string' && typeof tag !== 'number' && typeof tag !== 'symbol') {\n      return fail('Unknown discriminator', [...context.path, discriminator], {\n        code: 'variant.unknown_discriminator',\n        input: tag,\n        values: { discriminator: String(discriminator) },\n        parseOptions: context.options,\n      })\n    }\n\n    if (!Object.prototype.hasOwnProperty.call(variants, tag)) {\n      return fail('Unknown discriminator', [...context.path, discriminator], {\n        code: 'variant.unknown_discriminator',\n        input: tag,\n        values: { discriminator: String(discriminator) },\n        parseOptions: context.options,\n      })\n    }\n\n    let schema = variants[tag as keyof variants]\n    return schema['~run'](value, context)\n  })\n}\n\n/**\n * Create a schema that tries multiple schemas in order and returns the first success.\n *\n * When `abortEarly` is disabled (default), issues are collected from all failing variants.\n *\n * @param schemas Candidate schemas to try\n * @returns A schema that produces the first successful variant output\n */\nexport function union<schemas extends Schema<any, any>[]>(\n  schemas: schemas,\n): Schema<unknown, InferOutput<schemas[number]>> {\n  return createSchema(function validate(value, context) {\n    if (schemas.length === 0) {\n      return fail('No union variant matched', context.path, {\n        code: 'union.no_variants',\n        input: value,\n        parseOptions: context.options,\n      })\n    }\n\n    let abortEarly = shouldAbortEarly(context.options)\n    let issues: Issue[] = []\n\n    for (let schema of schemas) {\n      let result = schema['~run'](value, context)\n\n      if (result.issues) {\n        if (abortEarly) {\n          return { issues: result.issues }\n        }\n\n        issues.push(...result.issues)\n\n        continue\n      }\n\n      return result\n    }\n\n    return { issues }\n  })\n}\n\n/**\n * Error thrown by {@link parse} when validation fails.\n */\nexport class ValidationError extends Error {\n  /**\n   * The validation issues produced by the schema.\n   */\n  issues: ReadonlyArray<Issue>\n\n  /**\n   * @param issues The issues produced by schema validation\n   * @param message Optional error message (defaults to \"Validation failed\")\n   */\n  constructor(issues: ReadonlyArray<Issue>, message = 'Validation failed') {\n    super(message)\n    this.name = 'ValidationError'\n    this.issues = issues\n  }\n}\n\n/**\n * Validate a value and return the typed output or throw a {@link ValidationError}.\n *\n * @param schema The schema to validate against\n * @param value The value to validate\n * @param options Validation options\n * @returns The validated output value\n * @throws {ValidationError} If validation fails\n */\nexport function parse<input, output>(\n  schema: StandardSchemaV1<input, output>,\n  value: unknown,\n  options?: ParseOptions,\n): output {\n  let result = schema['~standard'].validate(value, options) as ValidationResult<output>\n\n  if (result.issues) {\n    throw new ValidationError(result.issues)\n  }\n\n  return result.value\n}\n\n/**\n * Validate a value without throwing.\n *\n * @param schema The schema to validate against\n * @param value The value to validate\n * @param options Validation options\n * @returns A success result with the value, or a failure result with issues\n */\nexport function parseSafe<input, output>(\n  schema: StandardSchemaV1<input, output>,\n  value: unknown,\n  options?: ParseOptions,\n):\n  | { success: true; value: output }\n  | { success: false; issues: ReadonlyArray<StandardSchemaV1.Issue> } {\n  let result = schema['~standard'].validate(value, options) as ValidationResult<output>\n\n  if (result.issues) {\n    return { success: false, issues: result.issues }\n  }\n\n  return { success: true, value: result.value }\n}\n"
  },
  {
    "path": "packages/data-schema/src/lib/variant.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { literal, number, object, string, variant } from './schema.ts'\nimport type { Issue, ValidationResult } from './schema.ts'\n\nfunction assertSuccess<output>(\n  result: ValidationResult<output>,\n): asserts result is { value: output } {\n  assert.ok(!result.issues)\n}\n\nfunction assertFailure<output>(\n  result: ValidationResult<output>,\n): asserts result is { issues: ReadonlyArray<Issue> } {\n  assert.ok(result.issues)\n}\n\ndescribe('variant', () => {\n  it('validates based on discriminator', () => {\n    let schema = variant('type', {\n      created: object({ type: literal('created'), id: string() }),\n      updated: object({ type: literal('updated'), id: string(), version: number() }),\n    })\n\n    let created = schema['~standard'].validate({ type: 'created', id: 'a' })\n    let updated = schema['~standard'].validate({ type: 'updated', id: 'b', version: 2 })\n\n    assertSuccess(created)\n    assertSuccess(updated)\n  })\n\n  it('reports unknown discriminator values', () => {\n    let schema = variant('type', {\n      created: object({ type: literal('created'), id: string() }),\n    })\n\n    let result = schema['~standard'].validate({ type: 'other', id: 'a' })\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['type'])\n  })\n\n  it('reports missing discriminator', () => {\n    let schema = variant('type', {\n      created: object({ type: literal('created'), id: string() }),\n    })\n\n    let result = schema['~standard'].validate({ id: 'a' })\n\n    assertFailure(result)\n    assert.deepEqual(result.issues[0].path, ['type'])\n  })\n})\n"
  },
  {
    "path": "packages/data-schema/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/data-schema/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"types\": [\"node\"],\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"vendor\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/data-table/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/data-table/.changes/minor.database-class-export.md",
    "content": "`@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.\n"
  },
  {
    "path": "packages/data-table/.changes/minor.migration-system-features.md",
    "content": "Add a first-class migration system under `remix/data-table/migrations` with:\n\n- `createMigration(...)` and timestamp-based migration loading\n- chainable `column` builders plus schema APIs for create, alter, drop, and index work\n- `createMigrationRunner(adapter, migrations)` for `up`, `down`, `status`, and `dryRun`\n- migration journaling, checksum tracking, and optional Node loading from `remix/data-table/migrations/node`\n\nMigration callbacks now use split handles: `{ db, schema }`.\n\n- `db` is the immediate data runtime (`query/create/update/delete/exec/transaction`)\n- `schema` owns migration operations like `createTable`, `alterTable`, `plan`, and introspection\n\nMigration-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.\n\nAdd 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`.\n\n`@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.\n"
  },
  {
    "path": "packages/data-table/.changes/minor.operation-contract-split.md",
    "content": "BREAKING CHANGE: Rename adapter operation contracts and fields.\n\n`AdapterStatement` becomes `DataManipulationOperation`, and `statement` becomes `operation`.\n\nAdd separate adapter execution methods for DML and migration/DDL operations: `execute` for `DataManipulationOperation` requests and `migrate` for `DataMigrationOperation` requests.\n\nAdd adapter introspection methods with optional transaction context: `hasTable(table, transaction?)` and `hasColumn(table, column, transaction?)`.\n"
  },
  {
    "path": "packages/data-table/.changes/minor.query-object-api.md",
    "content": "BREAKING CHANGE: Replace the public `QueryBuilder` API with `Query` objects that can be created with `query(table)` and executed with `db.exec(...)`.\n\n`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.\n\nThe incidental `QueryMethod` type export has also been removed; use `Database['query']` or `QueryForTable<table>` when you need that type shape.\n"
  },
  {
    "path": "packages/data-table/.changes/minor.sql-root-api.md",
    "content": "BREAKING CHANGE: Remove the `@remix-run/data-table/sql` export. Import `SqlStatement`, `sql`, and `rawSql` from `@remix-run/data-table` instead.\n\n`@remix-run/data-table/sql-helpers` remains available as the adapter-facing SQL helper module.\n"
  },
  {
    "path": "packages/data-table/.changes/minor.table-column-cutover.md",
    "content": "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 })`.\n\nRemove `~standard` table-schema compatibility and `getTableValidationSchemas(...)`, and stop runtime validation/coercion for predicate values.\n"
  },
  {
    "path": "packages/data-table/.changes/minor.table-lifecycle-callbacks.md",
    "content": "Add optional table lifecycle callbacks for write/delete/read flows: `beforeWrite`, `afterWrite`, `beforeDelete`, `afterDelete`, and `afterRead`.\n\nAdd `fail(...)` as a helper for returning structured validation/lifecycle issues from `validate(...)`, `beforeWrite(...)`, and `beforeDelete(...)`.\n"
  },
  {
    "path": "packages/data-table/CHANGELOG.md",
    "content": "# `data-table` CHANGELOG\n\nThis is the changelog for [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table). It follows [semantic versioning](https://semver.org/).\n\n## v0.1.0\n\n### Minor Changes\n\n- Add support for cross-schema column resolution\n\n- Initial release of `@remix-run/data-table`.\n\n- Make `createTable()` results Standard Schema-compatible so tables can be used directly with `parse()`/`parseSafe()` from `remix/data-schema`.\n\n  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.\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`data-schema@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-schema@0.1.0)\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/data-table/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/data-table/README.md",
    "content": "# data-table\n\nTyped relational query toolkit for JavaScript runtimes.\n\n## Features\n\n- **One API Across Databases**: Same query and relation APIs across PostgreSQL, MySQL, and SQLite adapters\n- **One Query API**: Build reusable `Query` objects with `query(table)` and execute them with `db.exec(...)`, or use `db.query(table)` as shorthand\n- **Type-Safe Reads**: Typed `select`, relation loading, and predicate keys\n- **Optional Runtime Validation**: Add `validate(context)` at the table level for create/update validation and coercion\n- **Relation-First Queries**: `hasMany`, `hasOne`, `belongsTo`, `hasManyThrough`, and nested eager loading\n- **Safe Scoped Writes**: `update`/`delete` with `orderBy`/`limit` run safely in a transaction\n- **First-Class Migrations**: Up/down migrations with schema builders, runner controls, and dry-run planning\n- **Raw SQL Escape Hatch**: Execute SQL directly with `db.exec(sql\\`...\\`)`\n\n`data-table` gives you two complementary APIs:\n\n- [**Query Objects**](#query-objects) for expressive joins, aggregates, eager loading, and scoped writes\n- [**CRUD Helpers**](#crud-helpers) for common create/read/update/delete flows (`find`, `create`, `update`, `delete`)\n\nBoth APIs are type-safe. Runtime validation is opt-in with table-level `validate(context)`.\n\n## Installation\n\n```sh\nnpm i remix\nnpm i pg\n# or\nnpm i mysql2\n# or\nnpm i better-sqlite3\n```\n\n## Setup\n\nDefine tables once, then create a database with an adapter.\n\n```ts\nimport { Pool } from 'pg'\nimport { column as c, createDatabase, hasMany, query, table } from 'remix/data-table'\nimport { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'\n\nlet users = table({\n  name: 'users',\n  columns: {\n    id: c.uuid(),\n    email: c.varchar(255),\n    role: c.enum(['customer', 'admin']),\n    created_at: c.integer(),\n  },\n})\n\nlet orders = table({\n  name: 'orders',\n  columns: {\n    id: c.uuid(),\n    user_id: c.uuid(),\n    status: c.enum(['pending', 'processing', 'shipped', 'delivered']),\n    total: c.decimal(10, 2),\n    created_at: c.integer(),\n  },\n})\n\nlet userOrders = hasMany(users, orders)\n\nlet pool = new Pool({ connectionString: process.env.DATABASE_URL })\nlet db = createDatabase(createPostgresDatabaseAdapter(pool))\n```\n\n## Query Objects\n\nUse `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.\n\n### Standalone Query Builder\n\n`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.\n\n```ts\nimport { eq, ilike, query } from 'remix/data-table'\n\nlet pendingOrdersForExampleUsers = query(orders)\n  .join(users, eq(orders.user_id, users.id))\n  .where({ status: 'pending' })\n  .where(ilike(users.email, '%@example.com'))\n  .select({\n    orderId: orders.id,\n    customerEmail: users.email,\n    total: orders.total,\n    placedAt: orders.created_at,\n  })\n  .orderBy(orders.created_at, 'desc')\n  .limit(20)\n\nlet recentPendingOrders = await db.exec(pendingOrdersForExampleUsers)\n```\n\nUnbound queries stay lazy until you pass them to `db.exec(...)`:\n\n```ts\nlet shippedCustomerQuery = query(users)\n  .where({ role: 'customer' })\n  .with({\n    recentOrders: userOrders.where({ status: 'shipped' }).orderBy('created_at', 'desc').limit(3),\n  })\n\nlet customers = await db.exec(shippedCustomerQuery)\n\n// customers[0].recentOrders is fully typed\n```\n\nThe same standalone query builder also handles terminal read and write operations:\n\n```ts\nlet nextPendingOrder = await db.exec(\n  query(orders).where({ status: 'pending' }).orderBy('created_at', 'asc').first(),\n)\n\nawait db.exec(\n  query(orders)\n    .where({ status: 'pending' })\n    .orderBy('created_at', 'asc')\n    .limit(100)\n    .update({ status: 'processing' }),\n)\n```\n\n### Bound Query Shorthand\n\nIf 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:\n\n```ts\nlet recentPendingOrders = await db\n  .query(orders)\n  .where({ status: 'pending' })\n  .orderBy('created_at', 'desc')\n  .limit(20)\n  .all()\n```\n\n## CRUD Helpers\n\n`data-table` provides helpers for common create/read/update/delete operations. Use these helpers for common operations without building a full query chain.\n\n### Read operations\n\n```ts\nimport { or } from 'remix/data-table'\n\nlet user = await db.find(users, 'u_001')\n\nlet firstPending = await db.findOne(orders, {\n  where: { status: 'pending' },\n  orderBy: ['created_at', 'asc'],\n})\n\nlet page = await db.findMany(orders, {\n  where: or({ status: 'pending' }, { status: 'processing' }),\n  orderBy: [\n    ['status', 'asc'],\n    ['created_at', 'desc'],\n  ],\n  limit: 50,\n  offset: 0,\n})\n```\n\n`where` accepts the same single-table object/predicate inputs as `query().where(...)`, and `orderBy` uses tuple form:\n\n- `['column', 'asc' | 'desc']`\n- `[['columnA', 'asc'], ['columnB', 'desc']]`\n\n### Create helpers\n\n```ts\n// Default: metadata (affectedRows/insertId)\nlet createResult = await db.create(users, {\n  id: 'u_002',\n  email: 'sam@example.com',\n  role: 'customer',\n  created_at: Date.now(),\n})\n\n// Return a typed row (with optional relations)\nlet createdUser = await db.create(\n  users,\n  {\n    id: 'u_003',\n    email: 'pat@example.com',\n    role: 'customer',\n    created_at: Date.now(),\n  },\n  {\n    returnRow: true,\n    with: { recentOrders: userOrders.orderBy('created_at', 'desc').limit(1) },\n  },\n)\n\n// Bulk insert metadata\nlet createManyResult = await db.createMany(orders, [\n  { id: 'o_101', user_id: 'u_002', status: 'pending', total: 24.99, created_at: Date.now() },\n  { id: 'o_102', user_id: 'u_003', status: 'pending', total: 48.5, created_at: Date.now() },\n])\n\n// Return inserted rows (requires adapter RETURNING support)\nlet insertedRows = await db.createMany(\n  orders,\n  [{ id: 'o_103', user_id: 'u_003', status: 'pending', total: 12, created_at: Date.now() }],\n  { returnRows: true },\n)\n```\n\n`createMany`/`insertMany` throw when every row in the batch is empty (no explicit values).\n\n### Update and delete helpers\n\n```ts\nlet updatedUser = await db.update(users, 'u_003', { role: 'admin' })\n\nlet updateManyResult = await db.updateMany(\n  orders,\n  { status: 'processing' },\n  {\n    where: { status: 'pending' },\n    orderBy: ['created_at', 'asc'],\n    limit: 25,\n  },\n)\n\nlet deletedUser = await db.delete(users, 'u_002')\n\nlet deleteManyResult = await db.deleteMany(orders, {\n  where: { status: 'delivered' },\n  orderBy: [['created_at', 'asc']],\n  limit: 200,\n})\n```\n\n`db.update(...)` throws when the target row cannot be found.\n\nReturn behavior:\n\n- `find`/`findOne` -> row or `null`\n- `findMany` -> rows\n- `create` -> `WriteResult` by default, row when `returnRow: true`\n- `createMany` -> `WriteResult` by default, rows when `returnRows: true` (not supported in MySQL because it doesn't support `RETURNING`)\n- `update` -> updated row (throws when target row is missing)\n- `updateMany`/`deleteMany` -> `WriteResult`\n- `delete` -> `boolean`\n\n### Validation and Lifecycle\n\nValidation is optional and table-scoped. Define `validate(context)` to validate/coerce write\npayloads, and add lifecycle callbacks when you need custom read/write/delete behavior.\n\n```ts\nimport { column as c, fail, table } from 'remix/data-table'\n\nlet payments = table({\n  name: 'payments',\n  columns: {\n    id: c.uuid(),\n    amount: c.decimal(10, 2),\n  },\n  beforeWrite({ value }) {\n    return {\n      value: {\n        ...value,\n        amount: typeof value.amount === 'string' ? value.amount.trim() : value.amount,\n      },\n    }\n  },\n  validate({ operation, value }) {\n    if (operation === 'create' && typeof value.amount === 'string') {\n      let amount = Number(value.amount)\n\n      if (!Number.isFinite(amount)) {\n        return fail('Expected a numeric amount', ['amount'])\n      }\n\n      return { value: { ...value, amount } }\n    }\n\n    return { value }\n  },\n  beforeDelete({ where }) {\n    if (where.length === 0) {\n      return fail('Refusing unscoped delete')\n    }\n  },\n  afterRead({ value }) {\n    if (!('amount' in value)) {\n      return { value }\n    }\n\n    return {\n      value: {\n        ...value,\n        // Example read-time shaping\n        amount:\n          typeof value.amount === 'number' ? Math.round(value.amount * 100) / 100 : value.amount,\n      },\n    }\n  },\n})\n```\n\nUse `fail(...)` in hooks when you want to return issues without manually building `{ issues: [...] }`.\n\nValidation and lifecycle semantics:\n\n- Write order is `beforeWrite -> validate -> timestamp/default touch -> execute -> afterWrite`\n- `validate` runs for writes (`create`, `createMany`, `insert`, `insertMany`, `update`, `updateMany`, `upsert`)\n- Hook context includes `{ operation: 'create' | 'update', tableName, value }`\n- Write payloads are partial objects\n- Unknown columns fail validation before and after hook processing\n- `beforeDelete` can veto deletes by returning `{ issues }`\n- `afterDelete` runs after successful deletes with `affectedRows`\n- `afterRead` runs for each loaded row (root rows, eager-loaded relation rows, and write-returning rows)\n- `afterRead` receives the current read shape, which may be partial/projection rows; guard field access accordingly\n- Predicate values (`where`, `having`, join predicates) are not runtime-validated\n- Lifecycle callbacks are synchronous; returning a Promise throws a validation error\n- Callback validation errors include `metadata.source` (`beforeWrite`, `validate`, `beforeDelete`, `afterRead`, etc.) for easier debugging\n- Callbacks do not introduce implicit transactions (use `db.transaction(...)` when you need rollback guarantees)\n\n## Transactions\n\n```ts\nawait db.transaction(async (tx) => {\n  let user = await tx.create(\n    users,\n    { id: 'u_010', email: 'new@example.com', role: 'customer', created_at: Date.now() },\n    { returnRow: true },\n  )\n\n  await tx.create(orders, {\n    id: 'o_500',\n    user_id: user.id,\n    status: 'pending',\n    total: 79,\n    created_at: Date.now(),\n  })\n})\n```\n\n## Migrations\n\n`data-table` includes a first-class migration system under `remix/data-table/migrations`.\nMigrations are adapter-driven: adapters execute SQL for their dialect/runtime, and SQL compilation\nis handled by adapter-owned compilers (with optional shared pure helpers from `data-table`).\nFor adapter authors (including third-party adapters), shared SQL helper utilities are available at\n`remix/data-table/sql-helpers`.\n\n### Example Setup\n\n```txt\napp/\n  db/\n    migrations/\n      20260228090000_create_users.ts\n      20260301113000_add_user_status.ts\n    migrate.ts\n```\n\n- Keep migration files in one directory (for example `app/db/migrations`).\n- Name each file as `YYYYMMDDHHmmss_name.ts` (or `.js`, `.mjs`, `.cjs`, `.cts`).\n- Each file must `default` export `createMigration(...)`; `id` and `name` are inferred from filename.\n\n### Migration File Example\n\n```ts\nimport { column as c, table } from 'remix/data-table'\nimport { createMigration } from 'remix/data-table/migrations'\n\nlet users = table({\n  name: 'users',\n  columns: {\n    id: c.integer().primaryKey(),\n    email: c.varchar(255).notNull().unique(),\n    created_at: c.timestamp({ withTimezone: true }).defaultNow(),\n  },\n})\n\nexport default createMigration({\n  async up({ db, schema }) {\n    await schema.createTable(users)\n    await schema.createIndex(users, 'email', { unique: true })\n\n    if (db.adapter.dialect === 'sqlite') {\n      await db.exec('pragma foreign_keys = on')\n    }\n  },\n  async down({ schema }) {\n    await schema.dropTable(users, { ifExists: true })\n  },\n})\n```\n\n### Runner Script Example\n\nIn `app/db/migrate.ts`:\n\n```ts\nimport path from 'node:path'\nimport { Pool } from 'pg'\nimport { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'\nimport { createMigrationRunner } from 'remix/data-table/migrations'\nimport { loadMigrations } from 'remix/data-table/migrations/node'\n\nlet directionArg = process.argv[2] ?? 'up'\nlet direction = directionArg === 'down' ? 'down' : 'up'\nlet to = process.argv[3]\n\nlet pool = new Pool({ connectionString: process.env.DATABASE_URL })\nlet adapter = createPostgresDatabaseAdapter(pool)\nlet migrations = await loadMigrations(path.resolve('app/db/migrations'))\nlet runner = createMigrationRunner(adapter, migrations)\n\ntry {\n  let result = direction === 'up' ? await runner.up({ to }) : await runner.down({ to })\n  console.log(direction + ' complete', {\n    applied: result.applied.map((entry) => entry.id),\n    reverted: result.reverted.map((entry) => entry.id),\n  })\n} finally {\n  await pool.end()\n}\n```\n\nUse `journalTable` if you want a custom migrations journal table name:\n\n```ts\nlet runner = createMigrationRunner(adapter, migrations, {\n  journalTable: 'app_migrations',\n})\n```\n\nRun it with your runtime, for example:\n\n```sh\nnode ./app/db/migrate.ts up\nnode ./app/db/migrate.ts up 20260301113000\nnode ./app/db/migrate.ts down\nnode ./app/db/migrate.ts down 20260228090000\n```\n\nUse `step` when you want bounded rollforward/rollback behavior instead of a target id:\n\n```ts\nawait runner.up({ step: 1 })\nawait runner.down({ step: 1 })\n```\n\n`to` and `step` are mutually exclusive. Use one or the other for a given run.\n\nUse `dryRun` to compile and inspect the SQL plan without applying migrations:\n\n```ts\nlet dryRunResult = await runner.up({ dryRun: true })\nconsole.log(dryRunResult.sql)\n```\n\nWhen migration transactions are enabled, migration-time `schema.createTable(...)`, `db.exec(...)`,\nquery-builder data operations, and `schema.hasTable(...)` / `schema.hasColumn(...)` all run in the same\nadapter transaction context.\n\nYou can also pass a pre-built SQL statement into `schema.plan(...)` when authoring migrations:\n\n```ts\nimport { sql } from 'remix/data-table'\n\nawait schema.plan(sql`update users set status = ${'active'} where status is null`)\n```\n\nYou can run lightweight schema checks inside a migration with `schema.hasTable(...)` and\n`schema.hasColumn(...)` when you need defensive conditional behavior. Methods that take a table name\naccept either a string (`'app.users'`) or a `table(...)` object.\n\nIn `dryRun` mode, introspection methods still check the live database state. They do not simulate\ntables/columns from pending operations in the current dry-run plan.\n\nFor key-oriented migration APIs, single-column and compound forms are both supported:\n\n```ts\nawait schema.alterTable(users, (table) => {\n  table.addPrimaryKey('id')\n  table.addForeignKey('account_id', 'accounts', 'id')\n  table.addForeignKey(['tenant_id', 'account_id'], 'accounts', ['tenant_id', 'id'])\n})\n```\n\nConstraint and index names are optional in migration APIs. When omitted, `data-table` generates\ndeterministic names for primary keys, uniques, foreign keys, checks, and indexes.\n\nThis is useful when you want to:\n\n- Review generated SQL in CI before deploying\n- Verify migration ordering and target/step selection\n- Audit dialect-specific SQL differences across adapters\n\nFor non-filesystem runtimes, register migrations manually:\n\n```ts\nimport { createMigrationRegistry, createMigrationRunner } from 'remix/data-table/migrations'\nimport createUsers from './db/migrations/20260228090000_create_users.ts'\n\nlet registry = createMigrationRegistry()\nregistry.register({ id: '20260228090000', name: 'create_users', migration: createUsers })\n\n// adapter from createPostgresDatabaseAdapter/createMysqlDatabaseAdapter/createSqliteDatabaseAdapter\nlet runner = createMigrationRunner(adapter, registry)\nawait runner.up()\n```\n\n## Raw SQL Escape Hatch\n\n```ts\nimport { rawSql, sql } from 'remix/data-table'\n\nawait db.exec(sql`select * from users where id = ${'u_001'}`)\nawait db.exec(rawSql('update users set role = ? where id = ?', ['admin', 'u_001']))\n```\n\nUse `sql` when you need raw SQL plus safe value interpolation:\n\n```ts\nimport { sql } from 'remix/data-table'\n\nlet email = input.email\nlet minCreatedAt = input.minCreatedAt\n\nlet result = await db.exec(sql`\n  select id, email\n  from users\n  where email = ${email}\n    and created_at >= ${minCreatedAt}\n`)\n```\n\n`sql` keeps values parameterized per adapter dialect, so you can avoid manual string concatenation.\n\n## Related Packages\n\n- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Optional schema parsing you can use inside table-level `validate(...)` hooks\n- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL adapter\n- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL adapter\n- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite adapter\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/data-table/package.json",
    "content": "{\n  \"name\": \"@remix-run/data-table\",\n  \"version\": \"0.1.0\",\n  \"description\": \"A typed, relational query toolkit for Remix\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/data-table\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/data-table#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./migrations\": \"./src/migrations.ts\",\n    \"./migrations/node\": \"./src/migrations/node.ts\",\n    \"./operators\": \"./src/operators.ts\",\n    \"./sql-helpers\": \"./src/sql-helpers.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./migrations\": {\n        \"types\": \"./dist/migrations.d.ts\",\n        \"default\": \"./dist/migrations.js\"\n      },\n      \"./migrations/node\": {\n        \"types\": \"./dist/migrations/node.d.ts\",\n        \"default\": \"./dist/migrations/node.js\"\n      },\n      \"./operators\": {\n        \"types\": \"./dist/operators.d.ts\",\n        \"default\": \"./dist/operators.js\"\n      },\n      \"./sql-helpers\": {\n        \"types\": \"./dist/sql-helpers.d.ts\",\n        \"default\": \"./dist/sql-helpers.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/better-sqlite3\": \"^7.6.13\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"better-sqlite3\": \"^12.4.1\"\n  },\n  \"dependencies\": {},\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"test:coverage\": \"pnpm run test:coverage:core && pnpm run test:coverage:migrations\",\n    \"test:coverage:core\": \"node --experimental-test-coverage --test-coverage-include='src/lib/operators.ts' --test-coverage-include='src/lib/references.ts' --test-coverage-include='src/lib/inflection.ts' --test-coverage-include='src/lib/sql.ts' --test-coverage-include='src/lib/errors.ts' --test-coverage-lines=90 --test-coverage-branches=90 --test-coverage-functions=90 --test ./src/lib/operators.test.ts ./src/lib/references.test.ts ./src/lib/inflection.test.ts ./src/lib/sql.test.ts ./src/lib/errors.test.ts\",\n    \"test:coverage:migrations\": \"node --experimental-test-coverage --test-coverage-include='src/lib/migrations.ts' --test-coverage-lines=90 --test-coverage-branches=90 --test-coverage-functions=90 --test ./src/lib/migrations.test.ts\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"remix\",\n    \"orm\",\n    \"sql\",\n    \"database\",\n    \"query-builder\",\n    \"relational\"\n  ]\n}\n"
  },
  {
    "path": "packages/data-table/src/index.ts",
    "content": "export type {\n  AdapterCapabilityOverrides,\n  AdapterCapabilities,\n  DataManipulationRequest,\n  DataMigrationRequest,\n  AddCheckChange,\n  AddCheckOperation,\n  AddColumnChange,\n  AddForeignKeyChange,\n  AddForeignKeyOperation,\n  AddPrimaryKeyChange,\n  AddUniqueChange,\n  AlterTableChange,\n  AlterTableOperation,\n  ChangeColumnChange,\n  CheckConstraint,\n  ColumnCheck,\n  ColumnComputed,\n  ColumnDefault,\n  ColumnDefinition,\n  ColumnTypeName,\n  CountOperation,\n  CreateIndexOperation,\n  CreateTableOperation,\n  DataMigrationResult,\n  DataMigrationOperation,\n  DataManipulationResult,\n  DataManipulationOperation,\n  DeleteOperation,\n  DatabaseAdapter,\n  DropCheckChange,\n  DropCheckOperation,\n  DropColumnChange,\n  DropForeignKeyChange,\n  DropForeignKeyOperation,\n  DropIndexOperation,\n  DropPrimaryKeyChange,\n  DropTableOperation,\n  DropUniqueChange,\n  ExistsOperation,\n  ForeignKeyAction,\n  ForeignKeyConstraint,\n  IndexDefinition,\n  IndexMethod,\n  InsertManyOperation,\n  InsertOperation,\n  JoinClause,\n  JoinType,\n  PrimaryKeyConstraint,\n  RawOperation,\n  RenameColumnChange,\n  RenameIndexOperation,\n  RenameTableOperation,\n  ReturningSelection,\n  SelectColumn,\n  SelectOperation,\n  SetTableCommentChange,\n  TableRef,\n  TransactionOptions,\n  TransactionToken,\n  UniqueConstraint,\n  UpdateOperation,\n  UpsertOperation,\n} from './lib/adapter.ts'\n\nexport {\n  DataTableAdapterError,\n  DataTableConstraintError,\n  DataTableError,\n  DataTableQueryError,\n  DataTableValidationError,\n} from './lib/errors.ts'\n\nexport type {\n  AnyRelation,\n  AnyColumn,\n  AnyTable,\n  BelongsToOptions,\n  ColumnReference,\n  ColumnReferenceForQualifiedName,\n  HasManyOptions,\n  HasManyThroughOptions,\n  HasOneOptions,\n  KeySelector,\n  OrderByClause,\n  OrderDirection,\n  PrimaryKeyInput,\n  Relation,\n  RelationCardinality,\n  RelationKind,\n  RelationMapForTable,\n  Table,\n  TableAfterDelete,\n  TableAfterDeleteContext,\n  TableAfterRead,\n  TableAfterReadContext,\n  TableAfterReadResult,\n  TableAfterWrite,\n  TableAfterWriteContext,\n  TableBeforeDelete,\n  TableBeforeDeleteContext,\n  TableBeforeDeleteResult,\n  TableBeforeWrite,\n  TableBeforeWriteContext,\n  TableBeforeWriteResult,\n  TableColumnInput,\n  TableColumnName,\n  TableColumns,\n  TableLifecycleOperation,\n  TableName,\n  TablePrimaryKey,\n  TableReference,\n  TableRow,\n  TableRowWith,\n  TableColumnsDefinition,\n  TableValidate,\n  TableValidationContext,\n  TableWriteOperation,\n  TableValidationOperation,\n  TableValidationResult,\n  TimestampConfig,\n  TimestampOptions,\n  ValidationFailure,\n  ValidationIssue,\n} from './lib/table.ts'\nexport {\n  belongsTo,\n  columnMetadataKey,\n  fail,\n  getTableColumns,\n  getTableColumnDefinitions,\n  getTableBeforeDelete,\n  getTableBeforeWrite,\n  getTableAfterDelete,\n  getTableAfterRead,\n  getTableAfterWrite,\n  getTableName,\n  getTablePrimaryKey,\n  getTableReference,\n  getTableTimestamps,\n  getTableValidator,\n  hasMany,\n  hasManyThrough,\n  hasOne,\n  table,\n  tableMetadataKey,\n  timestamps,\n} from './lib/table.ts'\nexport type { ColumnNamespace } from './lib/column.ts'\nexport { ColumnBuilder, column } from './lib/column.ts'\n\nexport type { Predicate, WhereInput, WhereObject } from './lib/operators.ts'\nexport {\n  and,\n  between,\n  eq,\n  gt,\n  gte,\n  ilike,\n  inList,\n  isNull,\n  like,\n  lt,\n  lte,\n  ne,\n  notInList,\n  notNull,\n  or,\n} from './lib/operators.ts'\n\nexport type { SqlStatement } from './lib/sql.ts'\nexport { rawSql, sql } from './lib/sql.ts'\n\nexport type {\n  CountOptions,\n  CreateManyResultOptions,\n  CreateManyRowsOptions,\n  CreateResultOptions,\n  CreateRowOptions,\n  DeleteManyOptions,\n  FindManyOptions,\n  FindOneOptions,\n  OrderByInput,\n  OrderByTuple,\n  QueryColumnTypesForTable,\n  QueryForTable,\n  QueryTableInput,\n  SingleTableColumn,\n  SingleTableWhere,\n  UpdateManyOptions,\n  UpdateOptions,\n  WriteResult,\n  WriteRowResult,\n  WriteRowsResult,\n} from './lib/database.ts'\nexport { createDatabase, Database } from './lib/database.ts'\nexport type { AnyQuery } from './lib/query.ts'\nexport { Query, query } from './lib/query.ts'\n"
  },
  {
    "path": "packages/data-table/src/lib/adapter.ts",
    "content": "import type { AnyTable, OrderByClause } from './table.ts'\nimport type { Predicate } from './operators.ts'\nimport type { SqlStatement } from './sql.ts'\nimport type { Pretty } from './types.ts'\n\n/**\n * Supported SQL join kinds.\n */\nexport type JoinType = 'inner' | 'left' | 'right'\n\n/**\n * Join configuration used in compiled select statements.\n */\nexport type JoinClause = {\n  type: JoinType\n  table: AnyTable\n  on: Predicate\n}\n\n/**\n * Selected output column with optional alias.\n */\nexport type SelectColumn = {\n  column: string\n  alias: string\n}\n\n/**\n * Returning selection for write statements.\n */\nexport type ReturningSelection = '*' | string[]\n\n/**\n * Canonical select statement shape consumed by adapters.\n */\nexport type SelectOperation<table extends AnyTable = AnyTable> = {\n  kind: 'select'\n  table: table\n  select: '*' | SelectColumn[]\n  distinct: boolean\n  joins: JoinClause[]\n  where: Predicate[]\n  groupBy: string[]\n  having: Predicate[]\n  orderBy: OrderByClause[]\n  limit?: number\n  offset?: number\n}\n\n/**\n * Canonical count statement shape consumed by adapters.\n */\nexport type CountOperation<table extends AnyTable = AnyTable> = {\n  kind: 'count'\n  table: table\n  joins: JoinClause[]\n  where: Predicate[]\n  groupBy: string[]\n  having: Predicate[]\n}\n\n/**\n * Canonical exists statement shape consumed by adapters.\n */\nexport type ExistsOperation<table extends AnyTable = AnyTable> = {\n  kind: 'exists'\n  table: table\n  joins: JoinClause[]\n  where: Predicate[]\n  groupBy: string[]\n  having: Predicate[]\n}\n\n/**\n * Canonical insert statement shape consumed by adapters.\n */\nexport type InsertOperation<table extends AnyTable = AnyTable> = {\n  kind: 'insert'\n  table: table\n  values: Record<string, unknown>\n  returning?: ReturningSelection\n}\n\n/**\n * Canonical bulk-insert statement shape consumed by adapters.\n */\nexport type InsertManyOperation<table extends AnyTable = AnyTable> = {\n  kind: 'insertMany'\n  table: table\n  values: Record<string, unknown>[]\n  returning?: ReturningSelection\n}\n\n/**\n * Canonical update statement shape consumed by adapters.\n */\nexport type UpdateOperation<table extends AnyTable = AnyTable> = {\n  kind: 'update'\n  table: table\n  changes: Record<string, unknown>\n  where: Predicate[]\n  returning?: ReturningSelection\n}\n\n/**\n * Canonical delete statement shape consumed by adapters.\n */\nexport type DeleteOperation<table extends AnyTable = AnyTable> = {\n  kind: 'delete'\n  table: table\n  where: Predicate[]\n  returning?: ReturningSelection\n}\n\n/**\n * Canonical upsert statement shape consumed by adapters.\n */\nexport type UpsertOperation<table extends AnyTable = AnyTable> = {\n  kind: 'upsert'\n  table: table\n  values: Record<string, unknown>\n  conflictTarget?: string[]\n  update?: Record<string, unknown>\n  returning?: ReturningSelection\n}\n\n/**\n * Raw SQL statement execution descriptor.\n */\nexport type RawOperation = {\n  kind: 'raw'\n  sql: SqlStatement\n}\n\n/**\n * Union of all data-manipulation statement shapes.\n */\nexport type DataManipulationOperation =\n  | SelectOperation\n  | CountOperation\n  | ExistsOperation\n  | InsertOperation\n  | InsertManyOperation\n  | UpdateOperation\n  | DeleteOperation\n  | UpsertOperation\n  | RawOperation\n\n/**\n * Qualified table reference used in migration operations.\n */\nexport type TableRef = {\n  name: string\n  schema?: string\n}\n\n/**\n * Referential actions supported by foreign key constraints.\n */\nexport type ForeignKeyAction = 'cascade' | 'restrict' | 'set null' | 'set default' | 'no action'\n\n/**\n * Logical column type names used by schema definitions.\n */\nexport type ColumnTypeName =\n  | 'varchar'\n  | 'text'\n  | 'integer'\n  | 'bigint'\n  | 'decimal'\n  | 'boolean'\n  | 'uuid'\n  | 'date'\n  | 'time'\n  | 'timestamp'\n  | 'json'\n  | 'binary'\n  | 'enum'\n\n/**\n * Default value definition for a column.\n */\nexport type ColumnDefault =\n  | { kind: 'literal'; value: unknown }\n  | { kind: 'now' }\n  | { kind: 'sql'; expression: string }\n\n/**\n * Definition for a computed or generated column.\n */\nexport type ColumnComputed = {\n  expression: string\n  stored: boolean\n}\n\n/** Options for configuring identity column generation. */\nexport type IdentityOptions = {\n  always?: boolean\n  start?: number\n  increment?: number\n}\n\n/** Foreign-key reference metadata declared on a column. */\nexport type ColumnReference = {\n  table: TableRef\n  columns: string[]\n  name: string\n  onDelete?: ForeignKeyAction\n  onUpdate?: ForeignKeyAction\n}\n\n/**\n * Check constraint declared on a column definition.\n */\nexport type ColumnCheck = {\n  expression: string\n  name: string\n}\n\n/**\n * Normalized column definition used in schema operations.\n */\nexport type ColumnDefinition = {\n  type: ColumnTypeName\n  nullable?: boolean\n  primaryKey?: boolean\n  unique?: boolean | { name?: string }\n  default?: ColumnDefault\n  computed?: ColumnComputed\n  references?: ColumnReference\n  checks?: ColumnCheck[]\n  comment?: string\n  length?: number\n  precision?: number\n  scale?: number\n  unsigned?: boolean\n  withTimezone?: boolean\n  enumValues?: string[]\n  autoIncrement?: boolean\n  identity?: IdentityOptions\n  collate?: string\n  charset?: string\n}\n\n/**\n * Primary key constraint definition.\n */\nexport type PrimaryKeyConstraint = {\n  columns: string[]\n  name: string\n}\n\n/**\n * Unique constraint definition.\n */\nexport type UniqueConstraint = {\n  columns: string[]\n  name: string\n}\n\n/**\n * Check constraint definition.\n */\nexport type CheckConstraint = {\n  expression: string\n  name: string\n}\n\n/**\n * Foreign key constraint definition.\n */\nexport type ForeignKeyConstraint = {\n  columns: string[]\n  references: {\n    table: TableRef\n    columns: string[]\n  }\n  name: string\n  onDelete?: ForeignKeyAction\n  onUpdate?: ForeignKeyAction\n}\n\n/**\n * Index method used when creating an index.\n */\nexport type IndexMethod = 'btree' | 'hash' | 'gin' | 'gist' | 'fulltext' | (string & {})\n\n/**\n * Index definition used in schema operations.\n */\nexport type IndexDefinition = {\n  table: TableRef\n  name: string\n  columns: string[]\n  unique?: boolean\n  where?: string\n  using?: IndexMethod\n}\n\n/**\n * Operation that creates a new table.\n */\nexport type CreateTableOperation = {\n  kind: 'createTable'\n  table: TableRef\n  ifNotExists?: boolean\n  columns: Record<string, ColumnDefinition>\n  primaryKey?: PrimaryKeyConstraint\n  uniques?: UniqueConstraint[]\n  checks?: CheckConstraint[]\n  foreignKeys?: ForeignKeyConstraint[]\n  comment?: string\n}\n\n/**\n * Alter-table change that adds a column.\n */\nexport type AddColumnChange = {\n  kind: 'addColumn'\n  column: string\n  definition: ColumnDefinition\n}\n\n/**\n * Alter-table change that replaces a column definition.\n */\nexport type ChangeColumnChange = {\n  kind: 'changeColumn'\n  column: string\n  definition: ColumnDefinition\n}\n\n/**\n * Alter-table change that renames a column.\n */\nexport type RenameColumnChange = {\n  kind: 'renameColumn'\n  from: string\n  to: string\n}\n\n/**\n * Alter-table change that drops a column.\n */\nexport type DropColumnChange = {\n  kind: 'dropColumn'\n  column: string\n  ifExists?: boolean\n}\n\n/**\n * Alter-table change that adds a primary key.\n */\nexport type AddPrimaryKeyChange = {\n  kind: 'addPrimaryKey'\n  constraint: PrimaryKeyConstraint\n}\n\n/**\n * Alter-table change that drops a primary key.\n */\nexport type DropPrimaryKeyChange = {\n  kind: 'dropPrimaryKey'\n  name: string\n}\n\n/**\n * Alter-table change that adds a unique constraint.\n */\nexport type AddUniqueChange = {\n  kind: 'addUnique'\n  constraint: UniqueConstraint\n}\n\n/**\n * Alter-table change that drops a unique constraint.\n */\nexport type DropUniqueChange = {\n  kind: 'dropUnique'\n  name: string\n}\n\n/**\n * Alter-table change that adds a foreign key constraint.\n */\nexport type AddForeignKeyChange = {\n  kind: 'addForeignKey'\n  constraint: ForeignKeyConstraint\n}\n\n/**\n * Alter-table change that drops a foreign key constraint.\n */\nexport type DropForeignKeyChange = {\n  kind: 'dropForeignKey'\n  name: string\n}\n\n/**\n * Alter-table change that adds a check constraint.\n */\nexport type AddCheckChange = {\n  kind: 'addCheck'\n  constraint: CheckConstraint\n}\n\n/**\n * Alter-table change that drops a check constraint.\n */\nexport type DropCheckChange = {\n  kind: 'dropCheck'\n  name: string\n}\n\n/**\n * Alter-table change that updates a table comment.\n */\nexport type SetTableCommentChange = {\n  kind: 'setTableComment'\n  comment: string\n}\n\n/**\n * Union of supported `alterTable` changes.\n */\nexport type AlterTableChange =\n  | AddColumnChange\n  | ChangeColumnChange\n  | RenameColumnChange\n  | DropColumnChange\n  | AddPrimaryKeyChange\n  | DropPrimaryKeyChange\n  | AddUniqueChange\n  | DropUniqueChange\n  | AddForeignKeyChange\n  | DropForeignKeyChange\n  | AddCheckChange\n  | DropCheckChange\n  | SetTableCommentChange\n\n/**\n * Operation that applies one or more table changes.\n */\nexport type AlterTableOperation = {\n  kind: 'alterTable'\n  table: TableRef\n  changes: AlterTableChange[]\n  ifExists?: boolean\n}\n\n/**\n * Operation that renames a table.\n */\nexport type RenameTableOperation = {\n  kind: 'renameTable'\n  from: TableRef\n  to: TableRef\n}\n\n/**\n * Operation that drops a table.\n */\nexport type DropTableOperation = {\n  kind: 'dropTable'\n  table: TableRef\n  ifExists?: boolean\n  cascade?: boolean\n}\n\n/**\n * Operation that creates an index.\n */\nexport type CreateIndexOperation = {\n  kind: 'createIndex'\n  index: IndexDefinition\n  ifNotExists?: boolean\n}\n\n/**\n * Operation that drops an index.\n */\nexport type DropIndexOperation = {\n  kind: 'dropIndex'\n  table: TableRef\n  name: string\n  ifExists?: boolean\n}\n\n/**\n * Operation that renames an index.\n */\nexport type RenameIndexOperation = {\n  kind: 'renameIndex'\n  table: TableRef\n  from: string\n  to: string\n}\n\n/**\n * Operation that adds a table-level foreign key.\n */\nexport type AddForeignKeyOperation = {\n  kind: 'addForeignKey'\n  table: TableRef\n  constraint: ForeignKeyConstraint\n}\n\n/**\n * Operation that drops a table-level foreign key.\n */\nexport type DropForeignKeyOperation = {\n  kind: 'dropForeignKey'\n  table: TableRef\n  name: string\n}\n\n/**\n * Operation that adds a table-level check constraint.\n */\nexport type AddCheckOperation = {\n  kind: 'addCheck'\n  table: TableRef\n  constraint: CheckConstraint\n}\n\n/**\n * Operation that drops a table-level check constraint.\n */\nexport type DropCheckOperation = {\n  kind: 'dropCheck'\n  table: TableRef\n  name: string\n}\n\n/**\n * Union of schema and migration operations understood by adapters.\n */\nexport type DataMigrationOperation =\n  | CreateTableOperation\n  | AlterTableOperation\n  | RenameTableOperation\n  | DropTableOperation\n  | CreateIndexOperation\n  | DropIndexOperation\n  | RenameIndexOperation\n  | AddForeignKeyOperation\n  | DropForeignKeyOperation\n  | AddCheckOperation\n  | DropCheckOperation\n  | RawOperation\n\n/**\n * Opaque transaction handle supplied by adapters.\n */\nexport type TransactionToken = {\n  id: string\n  metadata?: Record<string, unknown>\n}\n\n/**\n * Transaction hints that adapters may apply when supported by the dialect.\n */\nexport type TransactionOptions = {\n  isolationLevel?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable'\n  readOnly?: boolean\n}\n\n/**\n * Adapter execution request payload.\n */\nexport type DataManipulationRequest = {\n  operation: DataManipulationOperation\n  transaction?: TransactionToken\n}\n\n/**\n * Adapter migration request payload.\n */\nexport type DataMigrationRequest = {\n  operation: DataMigrationOperation\n  transaction?: TransactionToken\n}\n\n/**\n * Adapter data-manipulation result payload.\n */\nexport type DataManipulationResult = {\n  rows?: Record<string, unknown>[]\n  affectedRows?: number\n  insertId?: unknown\n}\n\n/**\n * Adapter data-migration result payload.\n */\nexport type DataMigrationResult = {\n  /**\n   * Number of migration operations processed by the adapter call.\n   */\n  affectedOperations?: number\n}\n\n/**\n * Declares adapter feature support.\n */\nexport type AdapterCapabilities = {\n  returning: boolean\n  savepoints: boolean\n  upsert: boolean\n  transactionalDdl: boolean\n  migrationLock: boolean\n}\n\n/**\n * Partial capabilities used to override adapter defaults.\n */\nexport type AdapterCapabilityOverrides = Pretty<Partial<AdapterCapabilities>>\n\n/**\n * Runtime contract implemented by concrete database adapters.\n */\nexport interface DatabaseAdapter {\n  /** Database dialect name exposed by the adapter. */\n  dialect: string\n  /** Feature flags describing the adapter's supported behaviors. */\n  capabilities: AdapterCapabilities\n  /** Compiles a data or migration operation into executable SQL statements. */\n  compileSql(operation: DataManipulationOperation | DataMigrationOperation): SqlStatement[]\n  /** Executes a data-manipulation request. */\n  execute(request: DataManipulationRequest): Promise<DataManipulationResult>\n  /** Executes a migration request. */\n  migrate(request: DataMigrationRequest): Promise<DataMigrationResult>\n  /** Checks whether a table exists. */\n  hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean>\n  /** Checks whether a column exists on a table. */\n  hasColumn(table: TableRef, column: string, transaction?: TransactionToken): Promise<boolean>\n  /** Starts a new database transaction. */\n  beginTransaction(options?: TransactionOptions): Promise<TransactionToken>\n  /** Commits an open transaction. */\n  commitTransaction(token: TransactionToken): Promise<void>\n  /** Rolls back an open transaction. */\n  rollbackTransaction(token: TransactionToken): Promise<void>\n  /** Creates a savepoint inside an open transaction. */\n  createSavepoint(token: TransactionToken, name: string): Promise<void>\n  /** Rolls back to a previously created savepoint. */\n  rollbackToSavepoint(token: TransactionToken, name: string): Promise<void>\n  /** Releases a previously created savepoint. */\n  releaseSavepoint(token: TransactionToken, name: string): Promise<void>\n  /** Acquires the adapter's migration lock when supported. */\n  acquireMigrationLock?(): Promise<void>\n  /** Releases the adapter's migration lock when supported. */\n  releaseMigrationLock?(): Promise<void>\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/column.ts",
    "content": "import type { ColumnDefinition, ForeignKeyAction, IdentityOptions } from './adapter.ts'\nimport { toTableRef } from './migrations/helpers.ts'\n\n/**\n * Chainable builder used to describe physical column definitions.\n */\nexport class ColumnBuilder<output = unknown> {\n  #definition: ColumnDefinition\n\n  constructor(definition: ColumnDefinition) {\n    this.#definition = definition\n  }\n\n  /**\n   * Marks the column as nullable.\n   * @returns The column builder with `null` added to its output type.\n   */\n  nullable(): ColumnBuilder<output | null> {\n    this.#definition.nullable = true\n    return this as unknown as ColumnBuilder<output | null>\n  }\n\n  /**\n   * Marks the column as non-nullable.\n   * @returns The column builder with `null` removed from its output type.\n   */\n  notNull(): ColumnBuilder<Exclude<output, null>> {\n    this.#definition.nullable = false\n    return this as unknown as ColumnBuilder<Exclude<output, null>>\n  }\n\n  /**\n   * Sets a literal default value for the column.\n   * @param value Default value to apply when the column is omitted.\n   * @returns The column builder.\n   */\n  default(value: unknown): ColumnBuilder<output> {\n    this.#definition.default = {\n      kind: 'literal',\n      value,\n    }\n    return this\n  }\n\n  /**\n   * Sets the column default to the current timestamp at write time.\n   * @returns The column builder.\n   */\n  defaultNow(): ColumnBuilder<output> {\n    this.#definition.default = {\n      kind: 'now',\n    }\n    return this\n  }\n\n  /**\n   * Sets a raw SQL expression as the column default.\n   * @param expression SQL expression used as the default value.\n   * @returns The column builder.\n   */\n  defaultSql(expression: string): ColumnBuilder<output> {\n    this.#definition.default = {\n      kind: 'sql',\n      expression,\n    }\n    return this\n  }\n\n  /**\n   * Marks the column as part of the primary key.\n   * @returns The column builder.\n   */\n  primaryKey(): ColumnBuilder<output> {\n    this.#definition.primaryKey = true\n    return this\n  }\n\n  /**\n   * Marks the column as unique.\n   * @param name Optional constraint name.\n   * @returns The column builder.\n   */\n  unique(name?: string): ColumnBuilder<output> {\n    this.#definition.unique = name ? { name } : true\n    return this\n  }\n\n  /**\n   * Adds a foreign-key reference for the column.\n   * @param table Referenced table name.\n   * @param name Constraint name.\n   * @returns The column builder.\n   */\n  references(table: string, name: string): ColumnBuilder<output>\n  /**\n   * Adds a foreign-key reference for the column.\n   * @param table Referenced table name.\n   * @param columns Referenced column list.\n   * @param name Constraint name.\n   * @returns The column builder.\n   */\n  references(table: string, columns: string | string[], name: string): ColumnBuilder<output>\n  references(\n    table: string,\n    columnsOrName: string | string[],\n    maybeName?: string,\n  ): ColumnBuilder<output> {\n    let columns = maybeName === undefined ? 'id' : columnsOrName\n    let name = maybeName === undefined ? String(columnsOrName) : maybeName\n\n    this.#definition.references = {\n      table: toTableRef(table),\n      columns: Array.isArray(columns) ? [...columns] : [columns],\n      onDelete: this.#definition.references?.onDelete,\n      onUpdate: this.#definition.references?.onUpdate,\n      name,\n    }\n    return this\n  }\n\n  /**\n   * Sets the foreign-key action used when the referenced row is deleted.\n   * @param action Delete action to apply.\n   * @returns The column builder.\n   */\n  onDelete(action: ForeignKeyAction): ColumnBuilder<output> {\n    if (!this.#definition.references) {\n      throw new Error('onDelete() requires references() to be set first')\n    }\n\n    this.#definition.references.onDelete = action\n    return this\n  }\n\n  /**\n   * Sets the foreign-key action used when the referenced row is updated.\n   * @param action Update action to apply.\n   * @returns The column builder.\n   */\n  onUpdate(action: ForeignKeyAction): ColumnBuilder<output> {\n    if (!this.#definition.references) {\n      throw new Error('onUpdate() requires references() to be set first')\n    }\n\n    this.#definition.references.onUpdate = action\n    return this\n  }\n\n  /**\n   * Adds a check constraint for the column.\n   * @param expression SQL check expression.\n   * @param name Constraint name.\n   * @returns The column builder.\n   */\n  check(expression: string, name: string): ColumnBuilder<output> {\n    let checks = this.#definition.checks ?? []\n    checks.push({ expression, name })\n    this.#definition.checks = checks\n    return this\n  }\n\n  /**\n   * Adds a database comment for the column.\n   * @param text Comment text.\n   * @returns The column builder.\n   */\n  comment(text: string): ColumnBuilder<output> {\n    this.#definition.comment = text\n    return this\n  }\n\n  /**\n   * Marks the column as computed from a SQL expression.\n   * @param expression SQL expression for the computed value.\n   * @param options Computed-column options.\n   * @param options.stored Whether the computed column should be stored instead of virtual.\n   * @returns The column builder.\n   */\n  computed(expression: string, options?: { stored?: boolean }): ColumnBuilder<output> {\n    this.#definition.computed = {\n      expression,\n      stored: options?.stored ?? true,\n    }\n    return this\n  }\n\n  /**\n   * Marks the column as unsigned when the dialect supports it.\n   * @returns The column builder.\n   */\n  unsigned(): ColumnBuilder<output> {\n    this.#definition.unsigned = true\n    return this\n  }\n\n  /**\n   * Marks the column as auto-incrementing when the dialect supports it.\n   * @returns The column builder.\n   */\n  autoIncrement(): ColumnBuilder<output> {\n    this.#definition.autoIncrement = true\n    return this\n  }\n\n  /**\n   * Configures an identity column strategy when the dialect supports it.\n   * @param options Identity sequence options.\n   * @returns The column builder.\n   */\n  identity(options?: IdentityOptions): ColumnBuilder<output> {\n    this.#definition.identity = options ?? {}\n    return this\n  }\n\n  /**\n   * Sets the collation for the column.\n   * @param name Collation name.\n   * @returns The column builder.\n   */\n  collate(name: string): ColumnBuilder<output> {\n    this.#definition.collate = name\n    return this\n  }\n\n  /**\n   * Sets the character set for the column.\n   * @param name Character set name.\n   * @returns The column builder.\n   */\n  charset(name: string): ColumnBuilder<output> {\n    this.#definition.charset = name\n    return this\n  }\n\n  /**\n   * Sets the column length.\n   * @param value Maximum length value.\n   * @returns The column builder.\n   */\n  length(value: number): ColumnBuilder<output> {\n    this.#definition.length = value\n    return this\n  }\n\n  /**\n   * Sets numeric precision and optional scale for the column.\n   * @param value Precision value.\n   * @param scale Optional scale value.\n   * @returns The column builder.\n   */\n  precision(value: number, scale?: number): ColumnBuilder<output> {\n    this.#definition.precision = value\n\n    if (scale !== undefined) {\n      this.#definition.scale = scale\n    }\n\n    return this\n  }\n\n  /**\n   * Sets numeric scale for the column.\n   * @param value Scale value.\n   * @returns The column builder.\n   */\n  scale(value: number): ColumnBuilder<output> {\n    this.#definition.scale = value\n    return this\n  }\n\n  /**\n   * Enables or disables timezone support for time-based columns.\n   * @param enabled Whether timezone support should be enabled.\n   * @returns The column builder.\n   */\n  timezone(enabled = true): ColumnBuilder<output> {\n    this.#definition.withTimezone = enabled\n    return this\n  }\n\n  /**\n   * Builds the immutable column definition.\n   * @returns A normalized column definition.\n   */\n  build(): ColumnDefinition {\n    return {\n      ...this.#definition,\n      checks: this.#definition.checks ? [...this.#definition.checks] : undefined,\n    }\n  }\n}\n\n/** Resolves the runtime output type from a column builder. */\nexport type ColumnOutput<column extends ColumnBuilder<any>> =\n  column extends ColumnBuilder<infer output> ? output : never\n\n/** Input type accepted when writing values for a column builder. */\nexport type ColumnInput<column extends ColumnBuilder<any>> = ColumnOutput<column>\n\n/**\n * Public constructor namespace for column builders.\n */\nexport type ColumnNamespace = {\n  varchar(length: number): ColumnBuilder<string>\n  text(): ColumnBuilder<string>\n  integer(): ColumnBuilder<number>\n  bigint(): ColumnBuilder\n  decimal(precision: number, scale: number): ColumnBuilder<number>\n  boolean(): ColumnBuilder<boolean>\n  uuid(): ColumnBuilder<string>\n  date(): ColumnBuilder\n  time(options?: { precision?: number; withTimezone?: boolean }): ColumnBuilder\n  timestamp(options?: { precision?: number; withTimezone?: boolean }): ColumnBuilder\n  json(): ColumnBuilder\n  binary(length?: number): ColumnBuilder\n  enum<values extends readonly string[]>(values: values): ColumnBuilder<values[number]>\n}\n\nfunction createColumnBuilder<output = unknown>(\n  type: ColumnDefinition['type'],\n): ColumnBuilder<output> {\n  return new ColumnBuilder({ type })\n}\n\n/**\n * Chainable column builder namespace.\n * @example\n * ```ts\n * import { column as c } from 'remix/data-table'\n *\n * let email = c.varchar(255).notNull().unique('users_email_uq')\n * ```\n */\nexport let column: ColumnNamespace = {\n  varchar(length: number) {\n    return new ColumnBuilder({ type: 'varchar', length })\n  },\n  text() {\n    return createColumnBuilder<string>('text')\n  },\n  integer() {\n    return createColumnBuilder<number>('integer')\n  },\n  bigint() {\n    return createColumnBuilder('bigint')\n  },\n  decimal(precision: number, scale: number) {\n    return new ColumnBuilder({ type: 'decimal', precision, scale })\n  },\n  boolean() {\n    return createColumnBuilder<boolean>('boolean')\n  },\n  uuid() {\n    return createColumnBuilder<string>('uuid')\n  },\n  date() {\n    return createColumnBuilder('date')\n  },\n  time(options?: { precision?: number; withTimezone?: boolean }) {\n    return new ColumnBuilder({\n      type: 'time',\n      precision: options?.precision,\n      withTimezone: options?.withTimezone,\n    })\n  },\n  timestamp(options?: { precision?: number; withTimezone?: boolean }) {\n    return new ColumnBuilder({\n      type: 'timestamp',\n      precision: options?.precision,\n      withTimezone: options?.withTimezone,\n    })\n  },\n  json() {\n    return createColumnBuilder('json')\n  },\n  binary(length?: number) {\n    return new ColumnBuilder({ type: 'binary', length })\n  },\n  enum<values extends readonly string[]>(values: values) {\n    return new ColumnBuilder({ type: 'enum', enumValues: [...values] }) as ColumnBuilder<\n      values[number]\n    >\n  },\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/database/execution-context.ts",
    "content": "import type {\n  DataManipulationOperation,\n  DataManipulationResult,\n  DatabaseAdapter,\n} from '../adapter.ts'\nimport type { Database } from '../database.ts'\n\nexport const executeOperation = Symbol('executeOperation')\n\nexport type QueryExecutionContext = {\n  adapter: DatabaseAdapter\n  now(): unknown\n  transaction<result>(callback: (database: Database) => Promise<result>): Promise<result>\n  [executeOperation](operation: DataManipulationOperation): Promise<DataManipulationResult>\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/database/helpers.ts",
    "content": "import { DataTableQueryError } from '../errors.ts'\nimport type {\n  OrderByInput,\n  OrderByTuple,\n  QueryColumnName,\n  QueryColumns,\n  QueryColumnTypeMap,\n  QueryTableInput,\n  SingleTableWhere,\n  TableColumnName,\n  WriteResult,\n  WriteRowsResult,\n} from '../database.ts'\nimport type { Predicate } from '../operators.ts'\nimport { and, eq, inList, or } from '../operators.ts'\nimport { query as createQuery } from '../query.ts'\nimport type { AnyTable, PrimaryKeyInput, TableName, TablePrimaryKey, TableRow } from '../table.ts'\nimport { getPrimaryKeyObject, getTableName, getTablePrimaryKey } from '../table.ts'\n\nimport type { QueryExecutionContext } from './execution-context.ts'\nimport { loadRowsWithRelationsForQuery } from './query-execution.ts'\n\nexport function asQueryTableInput<table extends AnyTable>(\n  table: table,\n): QueryTableInput<TableName<table>, TableRow<table>, TablePrimaryKey<table>> {\n  return table as unknown as QueryTableInput<\n    TableName<table>,\n    TableRow<table>,\n    TablePrimaryKey<table>\n  >\n}\n\nexport function getPrimaryKeyWhere<table extends AnyTable>(\n  table: table,\n  value: PrimaryKeyInput<table>,\n): SingleTableWhere<table> {\n  return getPrimaryKeyObject(table, value as any) as SingleTableWhere<table>\n}\n\nexport function getPrimaryKeyWhereFromRow<table extends AnyTable>(\n  table: table,\n  row: Record<string, unknown>,\n): SingleTableWhere<table> {\n  let where: Record<string, unknown> = {}\n\n  for (let key of getTablePrimaryKey(table) as string[]) {\n    where[key] = row[key]\n  }\n\n  return where as SingleTableWhere<table>\n}\n\nexport function resolveCreateRowWhere<table extends AnyTable>(\n  table: table,\n  values: Partial<TableRow<table>>,\n  insertId: unknown,\n): SingleTableWhere<table> {\n  let primaryKey = getTablePrimaryKey(table) as string[]\n\n  if (primaryKey.length === 1) {\n    let key = primaryKey[0]\n\n    if (Object.prototype.hasOwnProperty.call(values, key)) {\n      return {\n        [key]: (values as Record<string, unknown>)[key],\n      } as SingleTableWhere<table>\n    }\n\n    if (insertId !== undefined) {\n      return {\n        [key]: insertId,\n      } as SingleTableWhere<table>\n    }\n  }\n\n  let where: Record<string, unknown> = {}\n\n  for (let key of primaryKey) {\n    if (!Object.prototype.hasOwnProperty.call(values, key)) {\n      throw new DataTableQueryError(\n        'create({ returnRow: true }) requires primary key values for table \"' +\n          getTableName(table) +\n          '\" when adapter does not support RETURNING',\n      )\n    }\n\n    where[key] = (values as Record<string, unknown>)[key]\n  }\n\n  return where as SingleTableWhere<table>\n}\n\nexport function normalizeOrderByInput<table extends AnyTable>(\n  input: OrderByInput<table> | undefined,\n): OrderByTuple<table>[] {\n  if (!input) {\n    return []\n  }\n\n  if (input.length === 0) {\n    return []\n  }\n\n  if (Array.isArray(input[0])) {\n    return input as OrderByTuple<table>[]\n  }\n\n  return [input as OrderByTuple<table>]\n}\n\nexport function toWriteResult(result: WriteResult | WriteRowsResult<unknown>): WriteResult {\n  return {\n    affectedRows: result.affectedRows,\n    insertId: result.insertId,\n  }\n}\n\nexport function hasScopedWriteModifiers(state: {\n  orderBy: unknown[]\n  limit?: number\n  offset?: number\n}): boolean {\n  return state.orderBy.length > 0 || state.limit !== undefined || state.offset !== undefined\n}\n\nexport async function loadPrimaryKeyRowsForScope<table extends AnyTable>(\n  database: QueryExecutionContext,\n  table: table,\n  state: {\n    where: Predicate<string>[]\n    orderBy: Array<{ column: string; direction: 'asc' | 'desc' }>\n    limit?: number\n    offset?: number\n  },\n): Promise<Record<string, unknown>[]> {\n  let query = createQuery(\n    table as unknown as QueryTableInput<TableName<table>, TableRow<table>, TablePrimaryKey<table>>,\n  )\n\n  for (let predicate of state.where) {\n    query = query.where(predicate as Predicate<QueryColumnName<table>>)\n  }\n\n  for (let clause of state.orderBy) {\n    query = query.orderBy(\n      clause.column as QueryColumns<QueryColumnTypeMap<table>>,\n      clause.direction,\n    )\n  }\n\n  if (state.limit !== undefined) {\n    query = query.limit(state.limit)\n  }\n\n  if (state.offset !== undefined) {\n    query = query.offset(state.offset)\n  }\n\n  let rows = await loadRowsWithRelationsForQuery(\n    database,\n    query.select(...(getTablePrimaryKey(table) as (keyof TableRow<table> & string)[])),\n  )\n  let primaryKeys = getTablePrimaryKey(table) as string[]\n\n  return rows.map((row) => {\n    let keyObject: Record<string, unknown> = {}\n\n    for (let key of rowKeys(row as Record<string, unknown>, primaryKeys)) {\n      keyObject[key] = (row as Record<string, unknown>)[key]\n    }\n\n    return keyObject\n  })\n}\n\nexport function buildPrimaryKeyPredicate<table extends AnyTable>(\n  table: table,\n  keyObjects: Record<string, unknown>[],\n): Predicate<TableColumnName<table>> | undefined {\n  let primaryKey = getTablePrimaryKey(table)\n\n  if (keyObjects.length === 0) {\n    return undefined\n  }\n\n  if (primaryKey.length === 1) {\n    let key = primaryKey[0] as TableColumnName<table>\n    return inList(\n      key,\n      keyObjects.map((objectValue) => objectValue[key]),\n    )\n  }\n\n  let predicates = keyObjects.map((objectValue) => {\n    let comparisons = primaryKey.map((key) => {\n      let typedKey = key as TableColumnName<table>\n      return eq(typedKey, objectValue[typedKey])\n    })\n\n    return and(...comparisons)\n  })\n\n  return or(...predicates)\n}\n\nfunction rowKeys(row: Record<string, unknown>, keys: string[]): string[] {\n  let output: string[] = []\n\n  for (let key of keys) {\n    if (Object.prototype.hasOwnProperty.call(row, key)) {\n      output.push(key)\n    }\n  }\n\n  return output\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/database/query-execution.ts",
    "content": "import type {\n  CountOperation,\n  DataManipulationResult,\n  DeleteOperation,\n  ExistsOperation,\n  InsertManyOperation,\n  InsertOperation,\n  SelectColumn,\n  SelectOperation,\n  UpdateOperation,\n  UpsertOperation,\n} from '../adapter.ts'\nimport { DataTableQueryError } from '../errors.ts'\nimport type { ReturningInput, WriteResult, WriteRowResult, WriteRowsResult } from '../database.ts'\nimport { normalizeWhereInput } from '../operators.ts'\nimport type { AnyQuery, QueryExecutionResult, QueryState } from '../query.ts'\nimport { cloneQueryState, querySnapshot } from '../query.ts'\nimport type { AnyTable } from '../table.ts'\nimport { getPrimaryKeyObject, getTableName } from '../table.ts'\n\nimport { executeOperation, type QueryExecutionContext } from './execution-context.ts'\nimport {\n  buildPrimaryKeyPredicate,\n  hasScopedWriteModifiers,\n  loadPrimaryKeyRowsForScope,\n} from './helpers.ts'\nimport { loadRelationsForRows } from './relations.ts'\nimport {\n  applyAfterReadHooksToLoadedRows,\n  applyAfterReadHooksToRows,\n  assertReturningCapability,\n  normalizeReturningSelection,\n  prepareInsertValues,\n  prepareUpdateValues,\n  runAfterDeleteHook,\n  runAfterWriteHook,\n  runBeforeDeleteHook,\n} from './write-lifecycle.ts'\n\nexport async function executeQuery<input extends AnyQuery>(\n  database: QueryExecutionContext,\n  input: input,\n): Promise<QueryExecutionResult<input>> {\n  let snapshot = input[querySnapshot]()\n\n  switch (snapshot.plan.kind) {\n    case 'all':\n      return (await executeAll(\n        database,\n        snapshot.table,\n        snapshot.state,\n      )) as QueryExecutionResult<input>\n    case 'first':\n      return (await executeFirst(\n        database,\n        snapshot.table,\n        snapshot.state,\n      )) as QueryExecutionResult<input>\n    case 'find':\n      return (await executeFind(\n        database,\n        snapshot.table,\n        snapshot.state,\n        snapshot.plan.value,\n      )) as QueryExecutionResult<input>\n    case 'count':\n      return (await executeCount(\n        database,\n        snapshot.table,\n        snapshot.state,\n      )) as QueryExecutionResult<input>\n    case 'exists':\n      return (await executeExists(\n        database,\n        snapshot.table,\n        snapshot.state,\n      )) as QueryExecutionResult<input>\n    case 'insert':\n      return (await executeInsert(\n        database,\n        snapshot.table,\n        snapshot.plan.values as Record<string, unknown>,\n        snapshot.plan.options,\n      )) as QueryExecutionResult<input>\n    case 'insertMany':\n      return (await executeInsertMany(\n        database,\n        snapshot.table,\n        snapshot.plan.values as Record<string, unknown>[],\n        snapshot.plan.options,\n      )) as QueryExecutionResult<input>\n    case 'update':\n      return (await executeUpdate(\n        database,\n        snapshot.table,\n        snapshot.state,\n        snapshot.plan.changes as Record<string, unknown>,\n        snapshot.plan.options,\n      )) as QueryExecutionResult<input>\n    case 'delete':\n      return (await executeDelete(\n        database,\n        snapshot.table,\n        snapshot.state,\n        snapshot.plan.options,\n      )) as QueryExecutionResult<input>\n    case 'upsert':\n      return (await executeUpsert(\n        database,\n        snapshot.table,\n        snapshot.plan.values as Record<string, unknown>,\n        snapshot.plan.options,\n      )) as QueryExecutionResult<input>\n    default:\n      throw new DataTableQueryError('Unknown query execution mode')\n  }\n}\n\nexport async function loadRowsWithRelationsForQuery(\n  database: QueryExecutionContext,\n  input: AnyQuery,\n): Promise<Record<string, unknown>[]> {\n  let snapshot = input[querySnapshot]()\n  return loadRowsWithRelationsForState(database, snapshot.table, snapshot.state)\n}\n\nexport async function loadRowsWithRelationsForState(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  state: QueryState,\n): Promise<Record<string, unknown>[]> {\n  let operation = createSelectOperation(table, state)\n  let result = await database[executeOperation](operation)\n  let rows = normalizeRows(result.rows)\n\n  if (Object.keys(state.with).length === 0) {\n    return rows\n  }\n\n  return loadRelationsForRows(database, table, rows, state.with)\n}\n\nasync function executeAll(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  state: QueryState,\n): Promise<Record<string, unknown>[]> {\n  let rows = await loadRowsWithRelationsForState(database, table, state)\n  return applyAfterReadHooksToLoadedRows(table, rows, state.with)\n}\n\nasync function executeFirst(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  state: QueryState,\n): Promise<Record<string, unknown> | null> {\n  let rows = await executeAll(database, table, {\n    ...cloneQueryState(state),\n    limit: 1,\n  })\n\n  return rows[0] ?? null\n}\n\nasync function executeFind(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  state: QueryState,\n  value: unknown,\n): Promise<Record<string, unknown> | null> {\n  let scopedState = cloneQueryState(state)\n  scopedState.where.push(\n    normalizeWhereInput(getPrimaryKeyObject(table, value as never) as Record<string, unknown>),\n  )\n\n  return executeFirst(database, table, scopedState)\n}\n\nasync function executeCount(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  state: QueryState,\n): Promise<number> {\n  let operation: CountOperation<AnyTable> = {\n    kind: 'count',\n    table,\n    joins: [...state.joins],\n    where: [...state.where],\n    groupBy: [...state.groupBy],\n    having: [...state.having],\n  }\n\n  let result = await database[executeOperation](operation)\n\n  if (result.rows && result.rows[0] && typeof result.rows[0].count === 'number') {\n    return result.rows[0].count as number\n  }\n\n  if (result.rows) {\n    return result.rows.length\n  }\n\n  return 0\n}\n\nasync function executeExists(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  state: QueryState,\n): Promise<boolean> {\n  let operation: ExistsOperation<AnyTable> = {\n    kind: 'exists',\n    table,\n    joins: [...state.joins],\n    where: [...state.where],\n    groupBy: [...state.groupBy],\n    having: [...state.having],\n  }\n\n  let result = await database[executeOperation](operation)\n\n  if (result.rows && result.rows[0] && typeof result.rows[0].exists === 'boolean') {\n    return result.rows[0].exists as boolean\n  }\n\n  if (result.rows && result.rows[0] && typeof result.rows[0].count === 'number') {\n    return Number(result.rows[0].count) > 0\n  }\n\n  return Boolean(result.rows && result.rows.length > 0)\n}\n\nasync function executeInsert(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  values: Record<string, unknown>,\n  options?: { returning?: ReturningInput<Record<string, unknown>>; touch?: boolean },\n): Promise<WriteResult | WriteRowResult<Record<string, unknown>>> {\n  let preparedValues = prepareInsertValues(\n    table,\n    values as never,\n    database.now(),\n    options?.touch ?? true,\n  )\n  let returning = options?.returning\n\n  assertReturningCapability(database.adapter, 'insert', returning)\n\n  if (returning) {\n    let operation: InsertOperation<AnyTable> = {\n      kind: 'insert',\n      table,\n      values: preparedValues,\n      returning: normalizeReturningSelection(returning),\n    }\n\n    let result = await database[executeOperation](operation)\n    let row = applyAfterReadHooksToRows(table, normalizeRows(result.rows))[0] ?? null\n    let affectedRows = result.affectedRows ?? 0\n    runAfterWriteHook(table, {\n      operation: 'create',\n      tableName: getTableName(table),\n      values: [preparedValues],\n      affectedRows,\n      insertId: result.insertId,\n    })\n\n    return {\n      affectedRows,\n      insertId: result.insertId,\n      row,\n    }\n  }\n\n  let operation: InsertOperation<AnyTable> = {\n    kind: 'insert',\n    table,\n    values: preparedValues,\n  }\n\n  let result = await database[executeOperation](operation)\n  let affectedRows = result.affectedRows ?? 0\n  runAfterWriteHook(table, {\n    operation: 'create',\n    tableName: getTableName(table),\n    values: [preparedValues],\n    affectedRows,\n    insertId: result.insertId,\n  })\n\n  return {\n    affectedRows,\n    insertId: result.insertId,\n  }\n}\n\nasync function executeInsertMany(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  values: Record<string, unknown>[],\n  options?: { returning?: ReturningInput<Record<string, unknown>>; touch?: boolean },\n): Promise<WriteResult | WriteRowsResult<Record<string, unknown>>> {\n  let preparedValues = values.map((value) =>\n    prepareInsertValues(table, value as never, database.now(), options?.touch ?? true),\n  )\n\n  if (\n    preparedValues.length > 0 &&\n    preparedValues.every((preparedValue) => Object.keys(preparedValue).length === 0)\n  ) {\n    throw new DataTableQueryError(\n      'insertMany() requires at least one explicit value across the batch',\n    )\n  }\n\n  let returning = options?.returning\n  assertReturningCapability(database.adapter, 'insertMany', returning)\n\n  if (returning) {\n    let operation: InsertManyOperation<AnyTable> = {\n      kind: 'insertMany',\n      table,\n      values: preparedValues,\n      returning: normalizeReturningSelection(returning),\n    }\n\n    let result = await database[executeOperation](operation)\n    let affectedRows = result.affectedRows ?? 0\n    runAfterWriteHook(table, {\n      operation: 'create',\n      tableName: getTableName(table),\n      values: preparedValues,\n      affectedRows,\n      insertId: result.insertId,\n    })\n\n    return {\n      affectedRows,\n      insertId: result.insertId,\n      rows: applyAfterReadHooksToRows(table, normalizeRows(result.rows)),\n    }\n  }\n\n  let operation: InsertManyOperation<AnyTable> = {\n    kind: 'insertMany',\n    table,\n    values: preparedValues,\n  }\n\n  let result = await database[executeOperation](operation)\n  let affectedRows = result.affectedRows ?? 0\n  runAfterWriteHook(table, {\n    operation: 'create',\n    tableName: getTableName(table),\n    values: preparedValues,\n    affectedRows,\n    insertId: result.insertId,\n  })\n\n  return {\n    affectedRows,\n    insertId: result.insertId,\n  }\n}\n\nasync function executeUpdate(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  state: QueryState,\n  changes: Record<string, unknown>,\n  options?: { returning?: ReturningInput<Record<string, unknown>>; touch?: boolean },\n): Promise<WriteResult | WriteRowsResult<Record<string, unknown>>> {\n  let returning = options?.returning\n  assertReturningCapability(database.adapter, 'update', returning)\n  let preparedChanges = prepareUpdateValues(\n    table,\n    changes as never,\n    database.now(),\n    options?.touch ?? true,\n  )\n\n  if (Object.keys(preparedChanges).length === 0) {\n    throw new DataTableQueryError('update() requires at least one change')\n  }\n\n  let result: DataManipulationResult\n\n  if (hasScopedWriteModifiers(state)) {\n    result = await database.transaction(async (tx) => {\n      let primaryKeys = await loadPrimaryKeyRowsForScope(tx, table, state)\n      let primaryKeyPredicate = buildPrimaryKeyPredicate(table, primaryKeys)\n\n      if (!primaryKeyPredicate) {\n        return {\n          affectedRows: 0,\n          insertId: undefined,\n          rows: returning ? [] : undefined,\n        }\n      }\n\n      return tx[executeOperation]({\n        kind: 'update',\n        table,\n        changes: preparedChanges,\n        where: [primaryKeyPredicate],\n        returning: returning ? normalizeReturningSelection(returning) : undefined,\n      })\n    })\n  } else {\n    let operation: UpdateOperation<AnyTable> = {\n      kind: 'update',\n      table,\n      changes: preparedChanges,\n      where: [...state.where],\n      returning: returning ? normalizeReturningSelection(returning) : undefined,\n    }\n\n    result = await database[executeOperation](operation)\n  }\n\n  let affectedRows = result.affectedRows ?? 0\n  runAfterWriteHook(table, {\n    operation: 'update',\n    tableName: getTableName(table),\n    values: [preparedChanges],\n    affectedRows,\n    insertId: result.insertId,\n  })\n\n  if (!returning) {\n    return {\n      affectedRows,\n      insertId: result.insertId,\n    }\n  }\n\n  return {\n    affectedRows,\n    insertId: result.insertId,\n    rows: applyAfterReadHooksToRows(table, normalizeRows(result.rows)),\n  }\n}\n\nasync function executeDelete(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  state: QueryState,\n  options?: { returning?: ReturningInput<Record<string, unknown>> },\n): Promise<WriteResult | WriteRowsResult<Record<string, unknown>>> {\n  let returning = options?.returning\n  assertReturningCapability(database.adapter, 'delete', returning)\n  let tableName = getTableName(table)\n  let deleteContext = {\n    tableName,\n    where: [...state.where],\n    orderBy: [...state.orderBy],\n    limit: state.limit,\n    offset: state.offset,\n  }\n\n  runBeforeDeleteHook(table, deleteContext)\n  let result: DataManipulationResult\n\n  if (hasScopedWriteModifiers(state)) {\n    result = await database.transaction(async (tx) => {\n      let primaryKeys = await loadPrimaryKeyRowsForScope(tx, table, state)\n      let primaryKeyPredicate = buildPrimaryKeyPredicate(table, primaryKeys)\n\n      if (!primaryKeyPredicate) {\n        return {\n          affectedRows: 0,\n          insertId: undefined,\n          rows: returning ? [] : undefined,\n        }\n      }\n\n      return tx[executeOperation]({\n        kind: 'delete',\n        table,\n        where: [primaryKeyPredicate],\n        returning: returning ? normalizeReturningSelection(returning) : undefined,\n      })\n    })\n  } else {\n    let operation: DeleteOperation<AnyTable> = {\n      kind: 'delete',\n      table,\n      where: [...state.where],\n      returning: returning ? normalizeReturningSelection(returning) : undefined,\n    }\n\n    result = await database[executeOperation](operation)\n  }\n\n  let affectedRows = result.affectedRows ?? 0\n  runAfterDeleteHook(table, {\n    tableName,\n    where: deleteContext.where,\n    orderBy: deleteContext.orderBy,\n    limit: deleteContext.limit,\n    offset: deleteContext.offset,\n    affectedRows,\n  })\n\n  if (!returning) {\n    return {\n      affectedRows,\n      insertId: result.insertId,\n    }\n  }\n\n  return {\n    affectedRows,\n    insertId: result.insertId,\n    rows: applyAfterReadHooksToRows(table, normalizeRows(result.rows)),\n  }\n}\n\nasync function executeUpsert(\n  database: QueryExecutionContext,\n  table: AnyTable,\n  values: Record<string, unknown>,\n  options?: {\n    returning?: ReturningInput<Record<string, unknown>>\n    touch?: boolean\n    conflictTarget?: string[]\n    update?: Record<string, unknown>\n  },\n): Promise<WriteResult | WriteRowResult<Record<string, unknown>>> {\n  if (!database.adapter.capabilities.upsert) {\n    throw new DataTableQueryError('Adapter does not support upsert')\n  }\n\n  let preparedValues = prepareInsertValues(\n    table,\n    values as never,\n    database.now(),\n    options?.touch ?? true,\n  )\n  let updateChanges = options?.update\n    ? prepareUpdateValues(\n        table,\n        options.update as never,\n        database.now(),\n        options?.touch ?? true,\n        'create',\n      )\n    : undefined\n  let returning = options?.returning\n  assertReturningCapability(database.adapter, 'upsert', returning)\n\n  if (returning) {\n    let operation: UpsertOperation<AnyTable> = {\n      kind: 'upsert',\n      table,\n      values: preparedValues,\n      conflictTarget: options?.conflictTarget ? [...options.conflictTarget] : undefined,\n      update: updateChanges,\n      returning: normalizeReturningSelection(returning),\n    }\n\n    let result = await database[executeOperation](operation)\n    let row = applyAfterReadHooksToRows(table, normalizeRows(result.rows))[0] ?? null\n    let affectedRows = result.affectedRows ?? 0\n    let preparedWriteValues = updateChanges ? [preparedValues, updateChanges] : [preparedValues]\n    runAfterWriteHook(table, {\n      operation: 'create',\n      tableName: getTableName(table),\n      values: preparedWriteValues,\n      affectedRows,\n      insertId: result.insertId,\n    })\n\n    return {\n      affectedRows,\n      insertId: result.insertId,\n      row,\n    }\n  }\n\n  let operation: UpsertOperation<AnyTable> = {\n    kind: 'upsert',\n    table,\n    values: preparedValues,\n    conflictTarget: options?.conflictTarget ? [...options.conflictTarget] : undefined,\n    update: updateChanges,\n  }\n\n  let result = await database[executeOperation](operation)\n  let affectedRows = result.affectedRows ?? 0\n  let preparedWriteValues = updateChanges ? [preparedValues, updateChanges] : [preparedValues]\n  runAfterWriteHook(table, {\n    operation: 'create',\n    tableName: getTableName(table),\n    values: preparedWriteValues,\n    affectedRows,\n    insertId: result.insertId,\n  })\n\n  return {\n    affectedRows,\n    insertId: result.insertId,\n  }\n}\n\nfunction createSelectOperation(table: AnyTable, state: QueryState): SelectOperation<AnyTable> {\n  let clonedState = cloneQueryState(state)\n\n  return {\n    kind: 'select',\n    table,\n    select: cloneSelection(clonedState.select),\n    distinct: clonedState.distinct,\n    joins: clonedState.joins,\n    where: clonedState.where,\n    groupBy: clonedState.groupBy,\n    having: clonedState.having,\n    orderBy: clonedState.orderBy,\n    limit: clonedState.limit,\n    offset: clonedState.offset,\n  }\n}\n\nfunction cloneSelection(selection: '*' | SelectColumn[]): '*' | SelectColumn[] {\n  if (selection === '*') {\n    return '*'\n  }\n\n  return selection.map((column) => ({ ...column }))\n}\n\nfunction normalizeRows(rows: DataManipulationResult['rows']): Record<string, unknown>[] {\n  if (!rows) {\n    return []\n  }\n\n  return rows.map((row) => ({ ...row }))\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/database/relations.ts",
    "content": "import { DataTableQueryError } from '../errors.ts'\nimport type { QueryColumnName, QueryColumns, QueryColumnTypeMap } from '../database.ts'\nimport type { Predicate } from '../operators.ts'\nimport { and, eq, inList, or } from '../operators.ts'\nimport type { Query } from '../query.ts'\nimport { query as createQuery } from '../query.ts'\nimport type { AnyRelation, AnyTable, Relation } from '../table.ts'\nimport { getCompositeKey, getTableName, getTablePrimaryKey } from '../table.ts'\n\nimport type { QueryExecutionContext } from './execution-context.ts'\nimport { loadRowsWithRelationsForQuery } from './query-execution.ts'\n\ntype RelationQuery = Query<\n  any,\n  Record<string, unknown>,\n  Record<string, unknown>,\n  any,\n  { binding: 'unbound'; mode: 'all' }\n>\n\nexport async function loadRelationsForRows(\n  database: QueryExecutionContext,\n  sourceTable: AnyTable,\n  rows: Record<string, unknown>[],\n  relationMap: Record<string, AnyRelation>,\n): Promise<Record<string, unknown>[]> {\n  let output = rows.map((row) => ({ ...row }))\n\n  let relationNames = Object.keys(relationMap)\n\n  for (let relationName of relationNames) {\n    let relation = relationMap[relationName]\n\n    if (relation.sourceTable !== sourceTable) {\n      throw new DataTableQueryError(\n        'Relation \"' +\n          relationName +\n          '\" is not defined for source table \"' +\n          getTableName(sourceTable) +\n          '\"',\n      )\n    }\n\n    let values = await resolveRelationValues(database, output, relation)\n    let index = 0\n\n    while (index < output.length) {\n      output[index][relationName] = values[index]\n      index += 1\n    }\n  }\n\n  return output\n}\n\nasync function resolveRelationValues(\n  database: QueryExecutionContext,\n  sourceRows: Record<string, unknown>[],\n  relation: AnyRelation,\n): Promise<unknown[]> {\n  if (relation.relationKind === 'hasManyThrough') {\n    return loadHasManyThroughValues(database, sourceRows, relation)\n  }\n\n  return loadDirectRelationValues(database, sourceRows, relation)\n}\n\nasync function loadDirectRelationValues(\n  database: QueryExecutionContext,\n  sourceRows: Record<string, unknown>[],\n  relation: AnyRelation,\n): Promise<unknown[]> {\n  if (sourceRows.length === 0) {\n    return []\n  }\n\n  let sourceTuples = uniqueTuples(sourceRows, relation.sourceKey)\n\n  if (sourceTuples.length === 0) {\n    return sourceRows.map(() => (relation.cardinality === 'many' ? [] : null))\n  }\n\n  let query = createQuery(relation.targetTable)\n  let linkPredicate = buildLinkPredicate(relation.targetKey, sourceTuples)\n\n  if (linkPredicate) {\n    query = query.where(linkPredicate as Predicate<QueryColumnName<typeof relation.targetTable>>)\n  }\n\n  query = applyRelationModifiers(query, relation, {\n    includePagination: false,\n  })\n\n  let relatedRows = await loadRowsWithRelationsForQuery(database, query)\n  let grouped = groupRowsByTuple(relatedRows, relation.targetKey)\n\n  return sourceRows.map((sourceRow) => {\n    let key = getCompositeKey(sourceRow, relation.sourceKey)\n    let matches = grouped.get(key) ?? []\n    let pagedMatches = applyPagination(matches, relation.modifiers.limit, relation.modifiers.offset)\n\n    if (relation.cardinality === 'many') {\n      return pagedMatches\n    }\n\n    return pagedMatches[0] ?? null\n  })\n}\n\nasync function loadHasManyThroughValues(\n  database: QueryExecutionContext,\n  sourceRows: Record<string, unknown>[],\n  relation: AnyRelation,\n): Promise<unknown[]> {\n  if (!relation.through) {\n    throw new DataTableQueryError('hasManyThrough relation is missing through metadata')\n  }\n\n  if (sourceRows.length === 0) {\n    return []\n  }\n\n  let throughRelation = relation.through.relation\n  let sourceTuples = uniqueTuples(sourceRows, throughRelation.sourceKey)\n\n  if (sourceTuples.length === 0) {\n    return sourceRows.map(() => [])\n  }\n\n  let throughQuery = createQuery(throughRelation.targetTable)\n  let throughPredicate = buildLinkPredicate(throughRelation.targetKey, sourceTuples)\n\n  if (throughPredicate) {\n    throughQuery = throughQuery.where(\n      throughPredicate as Predicate<QueryColumnName<typeof throughRelation.targetTable>>,\n    )\n  }\n\n  throughQuery = applyRelationModifiers(throughQuery, throughRelation, {\n    includePagination: false,\n  })\n\n  let throughRows = await loadRowsWithRelationsForQuery(database, throughQuery)\n\n  if (throughRows.length === 0) {\n    return sourceRows.map(() => [])\n  }\n\n  let throughRowsBySource = groupRowsByTuple(throughRows, throughRelation.targetKey)\n  let pagedThroughRowsBySource = new Map<string, Record<string, unknown>[]>()\n  let pagedThroughRows: Record<string, unknown>[] = []\n\n  for (let sourceRow of sourceRows) {\n    let sourceKey = getCompositeKey(sourceRow, throughRelation.sourceKey)\n    let matchedThroughRows = throughRowsBySource.get(sourceKey) ?? []\n    let pagedMatchedRows = applyPagination(\n      matchedThroughRows,\n      throughRelation.modifiers.limit,\n      throughRelation.modifiers.offset,\n    )\n\n    pagedThroughRowsBySource.set(sourceKey, pagedMatchedRows)\n    pagedThroughRows.push(...pagedMatchedRows)\n  }\n\n  let throughTuples = uniqueTuples(pagedThroughRows, relation.through.throughSourceKey)\n\n  if (throughTuples.length === 0) {\n    return sourceRows.map(() => [])\n  }\n\n  let targetQuery = createQuery(relation.targetTable)\n  let targetPredicate = buildLinkPredicate(relation.through.throughTargetKey, throughTuples)\n\n  if (targetPredicate) {\n    targetQuery = targetQuery.where(\n      targetPredicate as Predicate<QueryColumnName<typeof relation.targetTable>>,\n    )\n  }\n\n  targetQuery = applyRelationModifiers(targetQuery, relation, {\n    includePagination: false,\n  })\n\n  let relatedRows = await loadRowsWithRelationsForQuery(database, targetQuery)\n  let targetRowsByThrough = groupRowsByTuple(relatedRows, relation.through.throughTargetKey)\n\n  return sourceRows.map((sourceRow) => {\n    let sourceKey = getCompositeKey(sourceRow, throughRelation.sourceKey)\n    let matchedThroughRows = pagedThroughRowsBySource.get(sourceKey) ?? []\n    let outputRows: Record<string, unknown>[] = []\n    let seen = new Set<string>()\n\n    for (let throughRow of matchedThroughRows) {\n      let throughKey = getCompositeKey(throughRow, relation.through!.throughSourceKey)\n      let rowsForThrough = targetRowsByThrough.get(throughKey) ?? []\n\n      for (let row of rowsForThrough) {\n        let rowIdentity = getCompositeKey(row, getTablePrimaryKey(relation.targetTable))\n\n        if (!seen.has(rowIdentity)) {\n          seen.add(rowIdentity)\n          outputRows.push(row)\n        }\n      }\n    }\n\n    return applyPagination(outputRows, relation.modifiers.limit, relation.modifiers.offset)\n  })\n}\n\nfunction applyRelationModifiers<table extends AnyTable>(\n  query: RelationQuery,\n  relation: Relation<any, table, any, any>,\n  options: { includePagination: boolean },\n): RelationQuery {\n  let next = query\n\n  for (let predicate of relation.modifiers.where) {\n    next = next.where(predicate)\n  }\n\n  for (let clause of relation.modifiers.orderBy) {\n    next = next.orderBy(clause.column as QueryColumns<QueryColumnTypeMap<table>>, clause.direction)\n  }\n\n  if (options.includePagination && relation.modifiers.limit !== undefined) {\n    next = next.limit(relation.modifiers.limit)\n  }\n\n  if (options.includePagination && relation.modifiers.offset !== undefined) {\n    next = next.offset(relation.modifiers.offset)\n  }\n\n  if (Object.keys(relation.modifiers.with).length > 0) {\n    next = next.with(relation.modifiers.with)\n  }\n\n  return next\n}\n\nfunction applyPagination<row>(\n  rows: row[],\n  limit: number | undefined,\n  offset: number | undefined,\n): row[] {\n  let offsetRows = offset === undefined ? rows : rows.slice(offset)\n  return limit === undefined ? offsetRows : offsetRows.slice(0, limit)\n}\n\nfunction uniqueTuples(rows: Record<string, unknown>[], columns: string[]): unknown[][] {\n  let output: unknown[][] = []\n  let seen = new Set<string>()\n\n  for (let row of rows) {\n    let tuple = columns.map((column) => row[column])\n    let key = tuple.map(stringifyForKey).join('::')\n\n    if (!seen.has(key)) {\n      seen.add(key)\n      output.push(tuple)\n    }\n  }\n\n  return output\n}\n\nfunction buildLinkPredicate(targetColumns: string[], tuples: unknown[][]): Predicate | undefined {\n  if (tuples.length === 0) {\n    return undefined\n  }\n\n  if (targetColumns.length === 1) {\n    return inList(\n      targetColumns[0],\n      tuples.map((tuple) => tuple[0]),\n    )\n  }\n\n  let tuplePredicates = tuples.map((tuple) => {\n    let comparisons = targetColumns.map((column, index) => eq(column, tuple[index]))\n\n    return and(...comparisons)\n  })\n\n  return or(...tuplePredicates)\n}\n\nfunction groupRowsByTuple(\n  rows: Record<string, unknown>[],\n  columns: string[],\n): Map<string, Record<string, unknown>[]> {\n  let output = new Map<string, Record<string, unknown>[]>()\n\n  for (let row of rows) {\n    let key = getCompositeKey(row, columns)\n    let group = output.get(key)\n\n    if (group) {\n      group.push(row)\n      continue\n    }\n\n    output.set(key, [row])\n  }\n\n  return output\n}\n\nfunction stringifyForKey(value: unknown): string {\n  if (value === null) {\n    return 'null'\n  }\n\n  if (value === undefined) {\n    return 'undefined'\n  }\n\n  if (value instanceof Date) {\n    return 'date:' + value.toISOString()\n  }\n\n  if (typeof value === 'string') {\n    return JSON.stringify(value)\n  }\n\n  if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n    return String(value)\n  }\n\n  return JSON.stringify(value)\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/database/write-lifecycle.ts",
    "content": "import type { DatabaseAdapter, ReturningSelection } from '../adapter.ts'\nimport { DataTableQueryError, DataTableValidationError } from '../errors.ts'\nimport type { ReturningInput } from '../database.ts'\nimport type {\n  AnyRelation,\n  AnyTable,\n  TableAfterDeleteContext,\n  TableAfterWriteContext,\n  TableBeforeDeleteContext,\n  TableLifecycleOperation,\n  TableRow,\n  TableWriteOperation,\n  ValidationIssue,\n} from '../table.ts'\nimport {\n  getTableAfterDelete,\n  getTableAfterRead,\n  getTableAfterWrite,\n  getTableBeforeDelete,\n  getTableBeforeWrite,\n  getTableColumns,\n  getTableName,\n  getTableTimestamps,\n  getTableValidator,\n} from '../table.ts'\n\ntype LifecycleCallbackSource =\n  | 'beforeWrite'\n  | 'validate'\n  | 'afterWrite'\n  | 'beforeDelete'\n  | 'afterDelete'\n  | 'afterRead'\n\nexport function prepareInsertValues<table extends AnyTable>(\n  table: table,\n  values: Partial<TableRow<table>>,\n  now: unknown,\n  touch: boolean,\n): Record<string, unknown> {\n  let output = validateWriteValues(table, values, 'create')\n  let timestamps = getTableTimestamps(table)\n  let columns = getTableColumns(table)\n\n  if (touch && timestamps) {\n    let createdAt = timestamps.createdAt\n    let updatedAt = timestamps.updatedAt\n\n    if (\n      Object.prototype.hasOwnProperty.call(columns, createdAt) &&\n      output[createdAt] === undefined\n    ) {\n      output[createdAt] = now\n    }\n\n    if (\n      Object.prototype.hasOwnProperty.call(columns, updatedAt) &&\n      output[updatedAt] === undefined\n    ) {\n      output[updatedAt] = now\n    }\n  }\n\n  return output\n}\n\nexport function prepareUpdateValues<table extends AnyTable>(\n  table: table,\n  values: Partial<TableRow<table>>,\n  now: unknown,\n  touch: boolean,\n  operation: TableWriteOperation = 'update',\n): Record<string, unknown> {\n  let output = validateWriteValues(table, values, operation)\n  let timestamps = getTableTimestamps(table)\n  let columns = getTableColumns(table)\n\n  if (touch && timestamps) {\n    let updatedAt = timestamps.updatedAt\n\n    if (\n      Object.prototype.hasOwnProperty.call(columns, updatedAt) &&\n      output[updatedAt] === undefined\n    ) {\n      output[updatedAt] = now\n    }\n  }\n\n  return output\n}\n\nexport function applyAfterReadHooksToRows<table extends AnyTable>(\n  table: table,\n  rows: Record<string, unknown>[],\n): Record<string, unknown>[] {\n  let callback = getTableAfterRead(table)\n\n  if (!callback || rows.length === 0) {\n    return rows\n  }\n\n  let tableName = getTableName(table)\n\n  return rows.map((row) => {\n    let callbackResult = callback({\n      tableName,\n      value: row as Partial<TableRow<table>>,\n    })\n    assertSynchronousCallbackResult(tableName, 'read', 'afterRead', callbackResult)\n\n    if (hasIssues(callbackResult)) {\n      throwValidationIssues(tableName, callbackResult.issues, 'read', 'afterRead')\n    }\n\n    if (!hasValue(callbackResult)) {\n      throw new DataTableValidationError(\n        'Invalid afterRead callback result for table \"' + tableName + '\"',\n        [{ message: 'Expected afterRead to return { value } or { issues }' }],\n        {\n          metadata: {\n            table: tableName,\n            operation: 'read',\n            source: 'afterRead',\n          },\n        },\n      )\n    }\n\n    return normalizeReadObject(tableName, callbackResult.value)\n  })\n}\n\nexport function applyAfterReadHooksToLoadedRows(\n  table: AnyTable,\n  rows: Record<string, unknown>[],\n  relationMap: Record<string, AnyRelation>,\n): Record<string, unknown>[] {\n  if (rows.length === 0) {\n    return rows\n  }\n\n  let relationNames = Object.keys(relationMap)\n\n  if (relationNames.length > 0) {\n    for (let row of rows) {\n      for (let relationName of relationNames) {\n        let relation = relationMap[relationName]\n        let relationValue = row[relationName]\n\n        if (relation.cardinality === 'many') {\n          if (!Array.isArray(relationValue)) {\n            continue\n          }\n\n          row[relationName] = applyAfterReadHooksToLoadedRows(\n            relation.targetTable,\n            relationValue as Record<string, unknown>[],\n            relation.modifiers.with,\n          )\n          continue\n        }\n\n        if (relationValue === null || relationValue === undefined) {\n          continue\n        }\n\n        if (typeof relationValue !== 'object' || Array.isArray(relationValue)) {\n          continue\n        }\n\n        let transformed = applyAfterReadHooksToLoadedRows(\n          relation.targetTable,\n          [relationValue as Record<string, unknown>],\n          relation.modifiers.with,\n        )\n        row[relationName] = transformed[0] ?? null\n      }\n    }\n  }\n\n  return applyAfterReadHooksToRows(table, rows)\n}\n\nexport function runBeforeDeleteHook<table extends AnyTable>(\n  table: table,\n  context: TableBeforeDeleteContext,\n): void {\n  let callback = getTableBeforeDelete(table)\n\n  if (!callback) {\n    return\n  }\n\n  let callbackResult = callback(context)\n  assertSynchronousCallbackResult(context.tableName, 'delete', 'beforeDelete', callbackResult)\n\n  if (callbackResult === undefined) {\n    return\n  }\n\n  if (hasIssues(callbackResult)) {\n    throwValidationIssues(context.tableName, callbackResult.issues, 'delete', 'beforeDelete')\n  }\n\n  throw new DataTableValidationError(\n    'Invalid beforeDelete callback result for table \"' + context.tableName + '\"',\n    [{ message: 'Expected beforeDelete to return nothing or { issues }' }],\n    {\n      metadata: {\n        table: context.tableName,\n        operation: 'delete',\n        source: 'beforeDelete',\n      },\n    },\n  )\n}\n\nexport function runAfterWriteHook<table extends AnyTable>(\n  table: table,\n  context: TableAfterWriteContext<TableRow<table>>,\n): void {\n  let callback = getTableAfterWrite(table)\n\n  if (!callback) {\n    return\n  }\n\n  let callbackResult = callback(context)\n  assertSynchronousCallbackResult(\n    context.tableName,\n    context.operation,\n    'afterWrite',\n    callbackResult,\n  )\n}\n\nexport function runAfterDeleteHook<table extends AnyTable>(\n  table: table,\n  context: TableAfterDeleteContext,\n): void {\n  let callback = getTableAfterDelete(table)\n\n  if (!callback) {\n    return\n  }\n\n  let callbackResult = callback(context)\n  assertSynchronousCallbackResult(context.tableName, 'delete', 'afterDelete', callbackResult)\n}\n\nexport function assertReturningCapability<row extends Record<string, unknown>>(\n  adapter: DatabaseAdapter,\n  operation: 'insert' | 'insertMany' | 'update' | 'delete' | 'upsert',\n  returning: ReturningInput<row> | undefined,\n): void {\n  if (returning && !adapter.capabilities.returning) {\n    throw new DataTableQueryError(operation + '() returning is not supported by this adapter')\n  }\n}\n\nexport function normalizeReturningSelection<row extends Record<string, unknown>>(\n  returning: ReturningInput<row>,\n): ReturningSelection {\n  if (returning === '*') {\n    return '*'\n  }\n\n  return [...returning]\n}\n\nfunction validateWriteValues<table extends AnyTable>(\n  table: table,\n  values: Partial<TableRow<table>>,\n  operation: TableWriteOperation,\n): Record<string, unknown> {\n  let tableName = getTableName(table)\n  let normalizedInput = normalizeWriteObject(table, values, operation)\n  let beforeWrite = getTableBeforeWrite(table)\n\n  if (beforeWrite) {\n    let beforeWriteResult = beforeWrite({\n      operation,\n      tableName,\n      value: normalizedInput as Partial<TableRow<table>>,\n    })\n    assertSynchronousCallbackResult(tableName, operation, 'beforeWrite', beforeWriteResult)\n\n    if (hasIssues(beforeWriteResult)) {\n      throwValidationIssues(tableName, beforeWriteResult.issues, operation, 'beforeWrite')\n    }\n\n    if (!hasValue(beforeWriteResult)) {\n      throw new DataTableValidationError(\n        'Invalid beforeWrite callback result for table \"' + tableName + '\"',\n        [{ message: 'Expected beforeWrite to return { value } or { issues }' }],\n        {\n          metadata: {\n            table: tableName,\n            operation,\n            source: 'beforeWrite',\n          },\n        },\n      )\n    }\n\n    normalizedInput = normalizeWriteObject(table, beforeWriteResult.value, operation, 'beforeWrite')\n  }\n\n  let validator = getTableValidator(table)\n\n  if (!validator) {\n    return normalizedInput\n  }\n\n  let validationResult = validator({\n    operation,\n    tableName,\n    value: normalizedInput as Partial<TableRow<table>>,\n  })\n  assertSynchronousCallbackResult(tableName, operation, 'validate', validationResult)\n\n  if (hasIssues(validationResult)) {\n    throwValidationIssues(tableName, validationResult.issues, operation, 'validate')\n  }\n\n  if (!hasValue(validationResult)) {\n    throw new DataTableValidationError(\n      'Invalid validator result for table \"' + tableName + '\"',\n      [{ message: 'Expected validator to return { value } or { issues }' }],\n      {\n        metadata: {\n          table: tableName,\n          operation,\n          source: 'validate',\n        },\n      },\n    )\n  }\n\n  return normalizeWriteObject(table, validationResult.value, operation, 'validate')\n}\n\nfunction hasIssues(value: unknown): value is { issues: ReadonlyArray<ValidationIssue> } {\n  return typeof value === 'object' && value !== null && 'issues' in value\n}\n\nfunction hasValue(value: unknown): value is { value: unknown } {\n  return typeof value === 'object' && value !== null && 'value' in value\n}\n\nfunction normalizeWriteObject<table extends AnyTable>(\n  table: table,\n  value: unknown,\n  operation: TableWriteOperation,\n  source?: LifecycleCallbackSource,\n): Record<string, unknown> {\n  let tableName = getTableName(table)\n  let columns = getTableColumns(table)\n\n  if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n    throw new DataTableValidationError(\n      'Invalid value for table \"' + tableName + '\"',\n      [{ message: 'Expected object' }],\n      {\n        metadata: {\n          table: tableName,\n          operation,\n          ...(source ? { source } : {}),\n        },\n      },\n    )\n  }\n\n  let output: Record<string, unknown> = {}\n\n  for (let key in value) {\n    if (!Object.prototype.hasOwnProperty.call(value, key)) {\n      continue\n    }\n\n    if (!Object.prototype.hasOwnProperty.call(columns, key)) {\n      throw new DataTableValidationError(\n        'Unknown column \"' + key + '\" for table \"' + tableName + '\"',\n        [],\n        {\n          metadata: {\n            table: tableName,\n            column: key,\n            operation,\n            ...(source ? { source } : {}),\n          },\n        },\n      )\n    }\n\n    output[key] = (value as Record<string, unknown>)[key]\n  }\n\n  return output\n}\n\nfunction throwValidationIssues(\n  tableName: string,\n  issues: ReadonlyArray<ValidationIssue>,\n  operation: TableLifecycleOperation,\n  source?: LifecycleCallbackSource,\n): never {\n  let firstIssue = issues[0]\n  let issuePath = firstIssue?.path\n  let firstPathSegment = issuePath && issuePath.length > 0 ? issuePath[0] : undefined\n  let column = typeof firstPathSegment === 'string' ? firstPathSegment : undefined\n\n  if (column) {\n    throw new DataTableValidationError(\n      'Invalid value for column \"' + column + '\" in table \"' + tableName + '\"',\n      issues,\n      {\n        metadata: {\n          table: tableName,\n          column,\n          operation,\n          ...(source ? { source } : {}),\n        },\n      },\n    )\n  }\n\n  throw new DataTableValidationError('Invalid value for table \"' + tableName + '\"', issues, {\n    metadata: {\n      table: tableName,\n      operation,\n      ...(source ? { source } : {}),\n    },\n  })\n}\n\nfunction assertSynchronousCallbackResult(\n  tableName: string,\n  operation: TableLifecycleOperation,\n  callbackName: LifecycleCallbackSource,\n  value: unknown,\n): void {\n  if (!isPromiseLike(value)) {\n    return\n  }\n\n  throw new DataTableValidationError(\n    'Invalid ' + callbackName + ' callback result for table \"' + tableName + '\"',\n    [{ message: callbackName + ' callbacks must be synchronous and cannot return a Promise' }],\n    {\n      metadata: {\n        table: tableName,\n        operation,\n        source: callbackName,\n      },\n    },\n  )\n}\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n  return (\n    (typeof value === 'object' || typeof value === 'function') &&\n    value !== null &&\n    'then' in value &&\n    typeof (value as { then?: unknown }).then === 'function'\n  )\n}\n\nfunction normalizeReadObject(tableName: string, value: unknown): Record<string, unknown> {\n  if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n    throw new DataTableValidationError(\n      'Invalid afterRead callback result for table \"' + tableName + '\"',\n      [{ message: 'Expected afterRead to return an object value' }],\n      {\n        metadata: {\n          table: tableName,\n          operation: 'read',\n          source: 'afterRead',\n        },\n      },\n    )\n  }\n\n  return {\n    ...(value as Record<string, unknown>),\n  }\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/database.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { afterEach, describe, it } from 'node:test'\n\nimport type { DataManipulationOperation, DatabaseAdapter } from './adapter.ts'\nimport { column } from './column.ts'\nimport { createDatabase, Database } from './database.ts'\nimport { query } from './query.ts'\nimport { DataTableAdapterError, DataTableQueryError, DataTableValidationError } from './errors.ts'\nimport { belongsTo, table, hasMany, hasManyThrough, hasOne, timestamps } from './table.ts'\nimport { eq } from './operators.ts'\nimport { sql } from './sql.ts'\nimport type { SqliteTestAdapterOptions, SqliteTestSeed } from '../../test/sqlite-test-database.ts'\nimport { createSqliteTestAdapter } from '../../test/sqlite-test-database.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n    email: column.text(),\n    status: column.text(),\n    ...timestamps(),\n  },\n  timestamps: true,\n  validate({ value }) {\n    if ('id' in value && typeof value.id !== 'number') {\n      return { issues: [{ message: 'Expected number', path: ['id'] }] }\n    }\n\n    if ('email' in value && typeof value.email !== 'string') {\n      return { issues: [{ message: 'Expected string', path: ['email'] }] }\n    }\n\n    if ('status' in value && typeof value.status !== 'string') {\n      return { issues: [{ message: 'Expected string', path: ['status'] }] }\n    }\n\n    return { value }\n  },\n})\n\nlet projects = table({\n  name: 'projects',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n    name: column.text(),\n    archived: column.boolean(),\n  },\n})\n\nlet profiles = table({\n  name: 'profiles',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n    display_name: column.text(),\n  },\n})\n\nlet tasks = table({\n  name: 'tasks',\n  columns: {\n    id: column.integer(),\n    project_id: column.integer(),\n    title: column.text(),\n    state: column.text(),\n  },\n})\n\nlet memberships = table({\n  name: 'memberships',\n  primaryKey: ['organization_id', 'account_id'],\n  columns: {\n    organization_id: column.integer(),\n    account_id: column.integer(),\n    role: column.text(),\n  },\n})\n\nlet invoices = table({\n  name: 'billing.invoices',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n    total: column.integer(),\n  },\n})\n\nlet accountProjects = hasMany(accounts, projects)\nlet accountProfile = hasOne(accounts, profiles)\nlet projectAccount = belongsTo(projects, accounts)\nlet accountTasks = hasManyThrough(accounts, tasks, {\n  through: accountProjects,\n})\n\nlet cleanups = new Set<() => void>()\n\nafterEach(() => {\n  for (let cleanup of cleanups) {\n    cleanup()\n  }\n\n  cleanups.clear()\n})\n\ndescribe('queries', () => {\n  it('supports direct construction and createDatabase wrapper', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n    let direct = new Database(adapter, {\n      now() {\n        return '2026-01-01T00:00:00.000Z'\n      },\n    })\n    let wrapped = createDatabase(adapter, {\n      now() {\n        return '2026-01-01T00:00:00.000Z'\n      },\n    })\n\n    let directRows = await direct.query(accounts).all()\n    let wrappedRows = await wrapped.query(accounts).all()\n\n    assert.equal(direct instanceof Database, true)\n    assert.equal(wrapped instanceof Database, true)\n    assert.equal(direct.now(), '2026-01-01T00:00:00.000Z')\n    assert.equal(wrapped.now(), '2026-01-01T00:00:00.000Z')\n    assert.equal(directRows.length, 1)\n    assert.equal(wrappedRows.length, 1)\n  })\n\n  it('executes unbound Query objects through db.exec() in every execution mode', async () => {\n    let db = createTestDatabase(\n      createAdapter({\n        accounts: [\n          { id: 1, email: 'amy@studio.test', status: 'active' },\n          { id: 2, email: 'brad@studio.test', status: 'inactive' },\n        ],\n        projects: [],\n        tasks: [],\n        memberships: [],\n      }),\n    )\n\n    let rows = await db.exec(query(accounts).where({ status: 'active' }).orderBy('id', 'asc'))\n    let first = await db.exec(query(accounts).where({ id: 1 }).first())\n    let found = await db.exec(query(accounts).find(1))\n    let count = await db.exec(query(accounts).where({ status: 'active' }).count())\n    let exists = await db.exec(query(accounts).where({ status: 'archived' }).exists())\n    let insertResult = await db.exec(\n      query(accounts).insert({ id: 3, email: 'casey@studio.test', status: 'inactive' }),\n    )\n    let insertManyResult = await db.exec(\n      query(accounts).insertMany([\n        { id: 4, email: 'devon@studio.test', status: 'archived' },\n        { id: 5, email: 'elliot@studio.test', status: 'active' },\n      ]),\n    )\n    let updateResult = await db.exec(\n      query(accounts).where({ status: 'inactive' }).update({ status: 'active' }),\n    )\n    let deleteResult = await db.exec(query(accounts).where({ id: 4 }).delete())\n    let upsertResult = await db.exec(\n      query(accounts).upsert(\n        { id: 6, email: 'fran@studio.test', status: 'active' },\n        { conflictTarget: ['id'] },\n      ),\n    )\n\n    assert.equal(rows.length, 1)\n    assert.equal(rows[0].email, 'amy@studio.test')\n    assert.equal(first?.id, 1)\n    assert.equal(found?.id, 1)\n    assert.equal(count, 1)\n    assert.equal(exists, false)\n    assert.equal(insertResult.affectedRows, 1)\n    assert.equal(insertManyResult.affectedRows, 2)\n    assert.equal(updateResult.affectedRows, 2)\n    assert.equal(deleteResult.affectedRows, 1)\n    assert.equal(upsertResult.affectedRows, 1)\n\n    let reloaded = await db.query(accounts).orderBy('id', 'asc').all()\n    assert.equal(reloaded.length, 5)\n    assert.equal(reloaded[1].status, 'active')\n    assert.equal(reloaded[2].status, 'active')\n    assert.equal(reloaded[3].email, 'elliot@studio.test')\n    assert.equal(reloaded[4].email, 'fran@studio.test')\n  })\n\n  it('is immutable and supports eager hasMany loading', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'inactive' },\n      ],\n      projects: [\n        { id: 100, account_id: 1, name: 'Spring Campaign', archived: false },\n        { id: 101, account_id: 1, name: 'Legacy Data Migration', archived: true },\n        { id: 102, account_id: 2, name: 'Customer Onboarding', archived: false },\n      ],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let archivedExcludedProjects = accountProjects.where({ archived: false }).orderBy('id', 'asc')\n\n    let allAccountsQuery = db.query(accounts)\n    let activeAccountsQuery = allAccountsQuery.where({ status: 'active' })\n\n    let allAccounts = await allAccountsQuery.all()\n    let activeAccounts = await activeAccountsQuery\n      .with({ projects: archivedExcludedProjects })\n      .all()\n\n    assert.equal(allAccounts.length, 2)\n    assert.equal(activeAccounts.length, 1)\n    assert.equal(activeAccounts[0].email, 'amy@studio.test')\n    assert.equal(activeAccounts[0].projects.length, 1)\n    assert.equal(activeAccounts[0].projects[0].name, 'Spring Campaign')\n  })\n\n  it('supports hasManyThrough loading', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'active' },\n      ],\n      projects: [\n        { id: 100, account_id: 1, name: 'Spring Campaign', archived: false },\n        { id: 101, account_id: 1, name: 'Fall Campaign', archived: false },\n        { id: 102, account_id: 2, name: 'Launch Site', archived: false },\n      ],\n      tasks: [\n        { id: 1000, project_id: 100, title: 'Define Ad Sets', state: 'open' },\n        { id: 1001, project_id: 100, title: 'Build UTM Links', state: 'done' },\n        { id: 1002, project_id: 101, title: 'Draft Landing Page', state: 'open' },\n        { id: 1003, project_id: 102, title: 'Collect Testimonials', state: 'open' },\n      ],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let openTasks = accountTasks.where({ state: 'open' }).orderBy('id', 'asc')\n\n    let accountRows = await db.query(accounts).orderBy('id', 'asc').with({ tasks: openTasks }).all()\n\n    assert.equal(accountRows[0].tasks.length, 2)\n    assert.equal(accountRows[0].tasks[0].title, 'Define Ad Sets')\n    assert.equal(accountRows[0].tasks[1].title, 'Draft Landing Page')\n    assert.equal(accountRows[1].tasks.length, 1)\n    assert.equal(accountRows[1].tasks[0].title, 'Collect Testimonials')\n  })\n\n  it('applies hasMany relation limit/offset per parent row', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'active' },\n      ],\n      projects: [\n        { id: 100, account_id: 1, name: 'A-1', archived: false },\n        { id: 101, account_id: 1, name: 'A-2', archived: false },\n        { id: 200, account_id: 2, name: 'B-1', archived: false },\n        { id: 201, account_id: 2, name: 'B-2', archived: false },\n      ],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n\n    let firstProjectPerAccount = await db\n      .query(accounts)\n      .orderBy('id', 'asc')\n      .with({\n        projects: accountProjects.orderBy('id', 'asc').limit(1),\n      })\n      .all()\n\n    assert.equal(firstProjectPerAccount[0].projects.length, 1)\n    assert.equal(firstProjectPerAccount[0].projects[0].id, 100)\n    assert.equal(firstProjectPerAccount[1].projects.length, 1)\n    assert.equal(firstProjectPerAccount[1].projects[0].id, 200)\n\n    let secondProjectPerAccount = await db\n      .query(accounts)\n      .orderBy('id', 'asc')\n      .with({\n        projects: accountProjects.orderBy('id', 'asc').offset(1).limit(1),\n      })\n      .all()\n\n    assert.equal(secondProjectPerAccount[0].projects.length, 1)\n    assert.equal(secondProjectPerAccount[0].projects[0].id, 101)\n    assert.equal(secondProjectPerAccount[1].projects.length, 1)\n    assert.equal(secondProjectPerAccount[1].projects[0].id, 201)\n  })\n\n  it('applies hasManyThrough relation pagination per parent row', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'active' },\n      ],\n      projects: [\n        { id: 100, account_id: 1, name: 'A-1', archived: false },\n        { id: 101, account_id: 1, name: 'A-2', archived: false },\n        { id: 200, account_id: 2, name: 'B-1', archived: false },\n        { id: 201, account_id: 2, name: 'B-2', archived: false },\n      ],\n      tasks: [\n        { id: 1000, project_id: 100, title: 'A-1 Task', state: 'open' },\n        { id: 1001, project_id: 101, title: 'A-2 Task', state: 'open' },\n        { id: 2000, project_id: 200, title: 'B-1 Task', state: 'open' },\n        { id: 2001, project_id: 201, title: 'B-2 Task', state: 'open' },\n      ],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n\n    let firstTaskPerAccount = await db\n      .query(accounts)\n      .orderBy('id', 'asc')\n      .with({\n        tasks: accountTasks.orderBy('id', 'asc').limit(1),\n      })\n      .all()\n\n    assert.equal(firstTaskPerAccount[0].tasks.length, 1)\n    assert.equal(firstTaskPerAccount[0].tasks[0].id, 1000)\n    assert.equal(firstTaskPerAccount[1].tasks.length, 1)\n    assert.equal(firstTaskPerAccount[1].tasks[0].id, 2000)\n\n    let secondTaskPerAccount = await db\n      .query(accounts)\n      .orderBy('id', 'asc')\n      .with({\n        tasks: accountTasks.orderBy('id', 'asc').offset(1).limit(1),\n      })\n      .all()\n\n    assert.equal(secondTaskPerAccount[0].tasks.length, 1)\n    assert.equal(secondTaskPerAccount[0].tasks[0].id, 1001)\n    assert.equal(secondTaskPerAccount[1].tasks.length, 1)\n    assert.equal(secondTaskPerAccount[1].tasks[0].id, 2001)\n  })\n\n  it('applies hasManyThrough through-relation pagination per parent row', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'active' },\n      ],\n      projects: [\n        { id: 100, account_id: 1, name: 'A-1', archived: false },\n        { id: 101, account_id: 1, name: 'A-2', archived: false },\n        { id: 200, account_id: 2, name: 'B-1', archived: false },\n        { id: 201, account_id: 2, name: 'B-2', archived: false },\n      ],\n      tasks: [\n        { id: 1000, project_id: 100, title: 'A-1 Task', state: 'open' },\n        { id: 1001, project_id: 101, title: 'A-2 Task', state: 'open' },\n        { id: 2000, project_id: 200, title: 'B-1 Task', state: 'open' },\n        { id: 2001, project_id: 201, title: 'B-2 Task', state: 'open' },\n      ],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let firstProjectPerAccount = accountProjects.orderBy('id', 'asc').limit(1)\n    let tasksThroughFirstProject = hasManyThrough(accounts, tasks, {\n      through: firstProjectPerAccount,\n    }).orderBy('id', 'asc')\n\n    let accountRows = await db\n      .query(accounts)\n      .orderBy('id', 'asc')\n      .with({ tasks: tasksThroughFirstProject })\n      .all()\n\n    assert.equal(accountRows[0].tasks.length, 1)\n    assert.equal(accountRows[0].tasks[0].id, 1000)\n    assert.equal(accountRows[1].tasks.length, 1)\n    assert.equal(accountRows[1].tasks[0].id, 2000)\n  })\n\n  it('supports composite primary keys in find()', async () => {\n    let adapter = createAdapter({\n      accounts: [],\n      projects: [],\n      tasks: [],\n      memberships: [\n        { organization_id: 9, account_id: 1, role: 'owner' },\n        { organization_id: 9, account_id: 2, role: 'member' },\n      ],\n    })\n\n    let db = createTestDatabase(adapter)\n    let membership = await db.query(memberships).find({ organization_id: 9, account_id: 2 })\n\n    assert.ok(membership)\n    assert.equal(membership.role, 'member')\n  })\n\n  it('supports database-level find helpers', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'inactive' },\n      ],\n      projects: [\n        { id: 100, account_id: 1, name: 'Spring Campaign', archived: false },\n        { id: 101, account_id: 1, name: 'Legacy Data Migration', archived: true },\n        { id: 102, account_id: 2, name: 'Customer Onboarding', archived: false },\n      ],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let openProjects = accountProjects.where({ archived: false }).orderBy('id', 'asc')\n\n    let account = await db.find(accounts, 1)\n    let activeAccount = await db.findOne(accounts, {\n      where: { status: 'active' },\n      orderBy: ['id', 'asc'],\n    })\n    let activeAccounts = await db.findMany(accounts, {\n      where: { status: 'active' },\n      orderBy: [\n        ['status', 'asc'],\n        ['id', 'asc'],\n      ],\n      limit: 1,\n    })\n    let activeCount = await db.count(accounts, { where: { status: 'active' } })\n    let accountsWithProjects = await db.findMany(accounts, {\n      orderBy: ['id', 'asc'],\n      with: { projects: openProjects },\n    })\n\n    assert.equal(account?.email, 'amy@studio.test')\n    assert.equal(activeAccount?.id, 1)\n    assert.equal(activeAccounts.length, 1)\n    assert.equal(activeAccounts[0].id, 1)\n    assert.equal(activeCount, 1)\n    assert.equal(accountsWithProjects[0].projects.length, 1)\n    assert.equal(accountsWithProjects[0].projects[0].id, 100)\n    assert.equal(accountsWithProjects[1].projects.length, 1)\n    assert.equal(accountsWithProjects[1].projects[0].id, 102)\n  })\n\n  it('returns null from database-level find helper for nullish primary keys', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let nullResult = await db.find(accounts, null as never)\n    let undefinedResult = await db.find(accounts, undefined as never)\n\n    assert.equal(nullResult, null)\n    assert.equal(undefinedResult, null)\n  })\n\n  it('supports database-level update helper', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'inactive' },\n      ],\n      projects: [\n        { id: 100, account_id: 1, name: 'Spring Campaign', archived: false },\n        { id: 101, account_id: 1, name: 'Legacy Data Migration', archived: true },\n      ],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let openProjects = accountProjects.where({ archived: false }).orderBy('id', 'asc')\n\n    let updated = await db.update(\n      accounts,\n      1,\n      {\n        status: 'inactive',\n      },\n      { with: { projects: openProjects } },\n    )\n\n    assert.equal(updated.status, 'inactive')\n    assert.equal(updated.projects.length, 1)\n    assert.equal(updated.projects[0].id, 100)\n  })\n\n  it('throws from database-level update helper when row is missing', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async function () {\n        await db.update(accounts, 999, { status: 'active' })\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message === 'update() failed to find row for table \"accounts\"'\n        )\n      },\n    )\n  })\n\n  it('does not pre-read when update helper uses RETURNING', async () => {\n    let operationKinds: string[] = []\n\n    let adapter: DatabaseAdapter = {\n      dialect: 'fake',\n      capabilities: {\n        returning: true,\n        savepoints: true,\n        upsert: true,\n        transactionalDdl: false,\n        migrationLock: false,\n      },\n      async execute(request) {\n        operationKinds.push(request.operation.kind)\n\n        if (request.operation.kind === 'update') {\n          return {\n            rows: [\n              {\n                id: 1,\n                email: 'amy@studio.test',\n                status: 'inactive',\n              },\n            ],\n            affectedRows: 1,\n          }\n        }\n\n        throw new Error('unexpected operation kind: ' + request.operation.kind)\n      },\n      compileSql() {\n        return []\n      },\n      async migrate() {\n        return {}\n      },\n      async beginTransaction() {\n        return { id: 'tx_1' }\n      },\n      async commitTransaction() {},\n      async rollbackTransaction() {},\n      async createSavepoint() {},\n      async rollbackToSavepoint() {},\n      async releaseSavepoint() {},\n      async hasTable() {\n        return false\n      },\n      async hasColumn() {\n        return false\n      },\n    }\n\n    let db = createTestDatabase(adapter)\n    let updated = await db.update(accounts, 1, { status: 'inactive' })\n\n    assert.equal(updated.id, 1)\n    assert.deepEqual(operationKinds, ['update'])\n  })\n\n  it('does not throw on no-op updates for non-RETURNING adapters when row still exists', async () => {\n    let operationKinds: string[] = []\n\n    let adapter: DatabaseAdapter = {\n      dialect: 'fake',\n      capabilities: {\n        returning: false,\n        savepoints: true,\n        upsert: true,\n        transactionalDdl: false,\n        migrationLock: false,\n      },\n      async execute(request) {\n        operationKinds.push(request.operation.kind)\n\n        if (request.operation.kind === 'update') {\n          return {\n            affectedRows: 0,\n          }\n        }\n\n        if (request.operation.kind === 'select') {\n          return {\n            rows: [\n              {\n                id: 1,\n                email: 'amy@studio.test',\n                status: 'active',\n              },\n            ],\n          }\n        }\n\n        throw new Error('unexpected operation kind: ' + request.operation.kind)\n      },\n      compileSql() {\n        return []\n      },\n      async migrate() {\n        return {}\n      },\n      async beginTransaction() {\n        return { id: 'tx_1' }\n      },\n      async commitTransaction() {},\n      async rollbackTransaction() {},\n      async createSavepoint() {},\n      async rollbackToSavepoint() {},\n      async releaseSavepoint() {},\n      async hasTable() {\n        return false\n      },\n      async hasColumn() {\n        return false\n      },\n    }\n\n    let db = createTestDatabase(adapter)\n    let updated = await db.update(accounts, 1, { status: 'active' })\n\n    assert.equal(updated.id, 1)\n    assert.equal(updated.status, 'active')\n    assert.deepEqual(operationKinds, ['update', 'select'])\n  })\n\n  it('supports database-level updateMany helper', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'inactive' },\n        { id: 2, email: 'brad@studio.test', status: 'inactive' },\n        { id: 3, email: 'cory@studio.test', status: 'active' },\n      ],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let result = await db.updateMany(\n      accounts,\n      {\n        status: 'archived',\n      },\n      {\n        where: { status: 'inactive' },\n        orderBy: ['id', 'asc'],\n        limit: 1,\n      },\n    )\n\n    let rows = await db.query(accounts).orderBy('id', 'asc').all()\n\n    assert.equal(result.affectedRows, 1)\n    assert.equal(rows[0].status, 'archived')\n    assert.equal(rows[1].status, 'inactive')\n    assert.equal(rows[2].status, 'active')\n  })\n\n  it('supports database-level delete helper', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'inactive' },\n      ],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let deleted = await db.delete(accounts, 2)\n    let deletedMissing = await db.delete(accounts, 999)\n    let rows = await db.query(accounts).orderBy('id', 'asc').all()\n\n    assert.equal(deleted, true)\n    assert.equal(deletedMissing, false)\n    assert.deepEqual(\n      rows.map((row) => row.id),\n      [1],\n    )\n  })\n\n  it('supports database-level deleteMany helper', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'inactive' },\n        { id: 2, email: 'brad@studio.test', status: 'inactive' },\n        { id: 3, email: 'cory@studio.test', status: 'active' },\n      ],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let result = await db.deleteMany(accounts, {\n      where: { status: 'inactive' },\n      orderBy: ['id', 'asc'],\n      limit: 1,\n    })\n\n    let rows = await db.query(accounts).orderBy('id', 'asc').all()\n\n    assert.equal(result.affectedRows, 1)\n    assert.deepEqual(\n      rows.map((row) => row.id),\n      [2, 3],\n    )\n  })\n\n  it('supports database-level create helper returning result metadata by default', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'existing@studio.test', status: 'active' }],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let result = await db.create(accounts, {\n      email: 'new@studio.test',\n      status: 'active',\n    })\n    let rows = await db.query(accounts).orderBy('id', 'asc').all()\n\n    assert.equal(result.affectedRows, 1)\n    assert.equal(rows.length, 2)\n    assert.equal(rows[1].email, 'new@studio.test')\n  })\n\n  it('supports database-level create helper returning a loaded row', async () => {\n    let adapter = createAdapter({\n      accounts: [],\n      projects: [{ id: 100, account_id: 99, name: 'Onboarding', archived: false }],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let created = await db.create(\n      accounts,\n      {\n        id: 99,\n        email: 'new@studio.test',\n        status: 'active',\n      },\n      {\n        returnRow: true,\n        with: { projects: accountProjects.orderBy('id', 'asc') },\n      },\n    )\n\n    assert.equal(created.id, 99)\n    assert.equal(created.email, 'new@studio.test')\n    assert.equal(created.projects.length, 1)\n    assert.equal(created.projects[0].id, 100)\n  })\n\n  it('supports createMany() metadata and rows return modes', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'existing@studio.test', status: 'active' }],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n    let result = await db.createMany(accounts, [\n      { id: 2, email: 'a@studio.test', status: 'active' },\n      { id: 3, email: 'b@studio.test', status: 'inactive' },\n    ])\n    let rows = await db.createMany(\n      accounts,\n      [{ id: 4, email: 'c@studio.test', status: 'active' }],\n      { returnRows: true },\n    )\n\n    assert.equal(result.affectedRows, 2)\n    assert.equal(rows.length, 1)\n    assert.equal(rows[0].id, 4)\n    assert.equal(rows[0].email, 'c@studio.test')\n  })\n\n  it('throws for createMany() batches with only empty rows', async () => {\n    let adapter = createAdapter({\n      accounts: [],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async function () {\n        await db.createMany(tasks, [{}, {}])\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message === 'insertMany() requires at least one explicit value across the batch'\n        )\n      },\n    )\n  })\n\n  it('throws for insertMany() batches with only empty rows', async () => {\n    let adapter = createAdapter({\n      accounts: [],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async function () {\n        await db.query(tasks).insertMany([{}])\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message === 'insertMany() requires at least one explicit value across the batch'\n        )\n      },\n    )\n  })\n\n  it('supports insertMany() batches that include at least one explicit value', async () => {\n    let statements: DataManipulationOperation[] = []\n\n    let adapter: DatabaseAdapter = {\n      dialect: 'fake',\n      capabilities: {\n        returning: true,\n        savepoints: true,\n        upsert: true,\n        transactionalDdl: false,\n        migrationLock: false,\n      },\n      async execute(request) {\n        statements.push(request.operation)\n\n        if (request.operation.kind === 'insertMany') {\n          return {\n            affectedRows: request.operation.values.length,\n          }\n        }\n\n        return {}\n      },\n      compileSql() {\n        return []\n      },\n      async migrate() {\n        return {}\n      },\n      async beginTransaction() {\n        return { id: 'tx_1' }\n      },\n      async commitTransaction() {},\n      async rollbackTransaction() {},\n      async createSavepoint() {},\n      async rollbackToSavepoint() {},\n      async releaseSavepoint() {},\n      async hasTable() {\n        return false\n      },\n      async hasColumn() {\n        return false\n      },\n    }\n\n    let db = createTestDatabase(adapter)\n    let result = await db.query(tasks).insertMany([{}, { title: 'hello world' }])\n\n    assert.equal(result.affectedRows, 2)\n    assert.equal(statements.length, 1)\n    assert.equal(statements[0].kind, 'insertMany')\n  })\n\n  it('throws for createMany({ returnRows: true }) when adapter has no RETURNING support', async () => {\n    let adapter = createAdapter(\n      {\n        accounts: [],\n        projects: [],\n        tasks: [],\n        memberships: [],\n      },\n      { returning: false },\n    )\n\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async function () {\n        await db.createMany(accounts, [{ id: 1, email: 'a@studio.test', status: 'active' }], {\n          returnRows: true,\n        })\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message === 'createMany({ returnRows: true }) is not supported by this adapter'\n        )\n      },\n    )\n  })\n\n  it('supports join/groupBy/having with count()', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'inactive' },\n      ],\n      projects: [\n        { id: 100, account_id: 1, name: 'Campaign', archived: false },\n        { id: 101, account_id: 2, name: 'Legacy', archived: false },\n      ],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n\n    let count = await db\n      .query(accounts)\n      .join(projects, eq('archived', false))\n      .groupBy('status')\n      .having({ status: 'active' })\n      .count()\n\n    assert.equal(count, 1)\n  })\n\n  it('supports alias object selection across joined tables', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [{ id: 100, account_id: 1, name: 'Campaign', archived: false }],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n\n    let db = createTestDatabase(adapter)\n\n    let rows = await db\n      .query(accounts)\n      .join(projects, eq('accounts.id', 'projects.account_id'))\n      .select({\n        accountId: 'accounts.id',\n        accountEmail: 'accounts.email',\n        projectId: 'projects.id',\n        projectName: 'projects.name',\n      })\n      .orderBy('projects.id', 'asc')\n      .all()\n\n    assert.deepEqual(rows, [\n      {\n        accountId: 1,\n        accountEmail: 'amy@studio.test',\n        projectId: 100,\n        projectName: 'Campaign',\n      },\n    ])\n  })\n\n  it('supports count() and exists()', async () => {\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'inactive' },\n      ],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    let activeCount = await db.query(accounts).where({ status: 'active' }).count()\n    let hasInactive = await db.query(accounts).where({ status: 'inactive' }).exists()\n    let hasArchived = await db.query(accounts).where({ status: 'archived' }).exists()\n\n    assert.equal(activeCount, 1)\n    assert.equal(hasInactive, true)\n    assert.equal(hasArchived, false)\n  })\n\n  it('supports eager loading for hasOne and belongsTo', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [{ id: 100, account_id: 1, name: 'Campaign', archived: false }],\n      profiles: [{ id: 10, account_id: 1, display_name: 'Amy' }],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    let accountRows = await db.query(accounts).with({ profile: accountProfile }).all()\n    let projectRows = await db.query(projects).with({ account: projectAccount }).all()\n\n    assert.equal(accountRows.length, 1)\n    assert.equal(accountRows[0].profile?.display_name, 'Amy')\n    assert.equal(projectRows.length, 1)\n    assert.equal(projectRows[0].account?.email, 'amy@studio.test')\n  })\n\n  it('supports cross schema query', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      invoices: [{ id: 100, account_id: 1, total: 1000 }],\n    })\n    let db = createTestDatabase(adapter)\n    let invoiceRows = await db\n      .query(invoices)\n      .join(accounts, eq(accounts.id, invoices.account_id))\n      .select({\n        email: accounts.email,\n        total: invoices.total,\n      })\n      .all()\n\n    assert.equal(invoiceRows.length, 1)\n    assert.equal(invoiceRows[0].email, 'amy@studio.test')\n    assert.equal(invoiceRows[0].total, 1000)\n  })\n})\n\ndescribe('writes and validation', () => {\n  it('validates values and applies timestamps', async () => {\n    let adapter = createAdapter({\n      accounts: [],\n      projects: [],\n      tasks: [],\n      memberships: [],\n    })\n    let createdAt = '2026-01-15T10:00:00.000Z'\n    let db = createDatabase(adapter, {\n      now() {\n        return createdAt\n      },\n    })\n\n    let insertResult = await db.query(accounts).insert(\n      {\n        id: 10,\n        email: 'ops@studio.test',\n        status: 'active',\n      },\n      { returning: ['id', 'email'] },\n    )\n\n    if ('row' in insertResult) {\n      assert.equal(insertResult.row?.id, 10)\n      assert.equal(insertResult.row?.email, 'ops@studio.test')\n    } else {\n      assert.fail('Expected row in insert result')\n    }\n\n    let savedAccount = await db.query(accounts).find(10)\n    assert.ok(savedAccount)\n    assert.deepEqual(savedAccount.created_at, createdAt)\n    assert.deepEqual(savedAccount.updated_at, createdAt)\n\n    await assert.rejects(\n      async function () {\n        await db\n          .query(accounts)\n          .insert({ id: 11, email: 'billing@studio.test', status: 300 as never })\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableValidationError &&\n          error.metadata?.operation === 'create' &&\n          error.metadata?.source === 'validate'\n        )\n      },\n    )\n  })\n\n  it('passes create/update operation context to table validators', async () => {\n    let operations: Array<'create' | 'update'> = []\n    let validatedAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n        ...timestamps(),\n      },\n      timestamps: true,\n      validate({ operation, value }) {\n        operations.push(operation)\n        return { value }\n      },\n    })\n\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'a@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await db.create(validatedAccounts, { id: 2, email: 'b@studio.test', status: 'active' })\n    await db.createMany(validatedAccounts, [{ id: 3, email: 'c@studio.test', status: 'active' }])\n    await db.update(validatedAccounts, 1, { status: 'inactive' })\n    await db.updateMany(\n      validatedAccounts,\n      { status: 'active' },\n      {\n        where: { id: 2 },\n      },\n    )\n\n    assert.deepEqual(operations, ['create', 'create', 'update', 'update'])\n  })\n\n  it('uses create operation for both upsert payloads', async () => {\n    let operations: Array<'create' | 'update'> = []\n    let validatedAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n      },\n      validate({ operation, value }) {\n        operations.push(operation)\n        return { value }\n      },\n    })\n\n    let adapter = {\n      dialect: 'test',\n      capabilities: {\n        returning: false,\n        savepoints: true,\n        upsert: true,\n        transactionalDdl: false,\n        migrationLock: false,\n      },\n      compileSql() {\n        return []\n      },\n      async execute() {\n        return { affectedRows: 1 }\n      },\n      async migrate() {\n        return {}\n      },\n      async beginTransaction() {\n        return { id: 'tx' }\n      },\n      async commitTransaction() {},\n      async rollbackTransaction() {},\n      async createSavepoint() {},\n      async rollbackToSavepoint() {},\n      async releaseSavepoint() {},\n      async hasTable() {\n        return false\n      },\n      async hasColumn() {\n        return false\n      },\n    } satisfies DatabaseAdapter\n\n    let db = createDatabase(adapter)\n    await db\n      .query(validatedAccounts)\n      .upsert(\n        { id: 1, email: 'a@studio.test', status: 'inactive' },\n        { conflictTarget: ['id'], update: { status: 'active' } },\n      )\n\n    assert.deepEqual(operations, ['create', 'create'])\n  })\n\n  it('rejects unknown columns before and after table validation', async () => {\n    let strictAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n        created_at: column.text(),\n        updated_at: column.text(),\n      },\n      validate({ value }) {\n        return {\n          value: {\n            ...value,\n            ghost: true,\n          },\n        }\n      },\n    })\n    let adapter = createAdapter({\n      accounts: [],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async () => {\n        await db.create(strictAccounts, { id: 1, email: 'a@studio.test', status: 'active' })\n      },\n      (error: unknown) =>\n        error instanceof DataTableValidationError &&\n        error.message === 'Unknown column \"ghost\" for table \"accounts\"',\n    )\n\n    await assert.rejects(\n      async () => {\n        await db.create(strictAccounts, {\n          id: 1,\n          email: 'a@studio.test',\n          status: 'active',\n          unknown: true,\n        } as never)\n      },\n      (error: unknown) =>\n        error instanceof DataTableValidationError &&\n        error.message === 'Unknown column \"unknown\" for table \"accounts\"',\n    )\n  })\n\n  it('runs beforeWrite -> validate -> touch -> afterWrite for create', async () => {\n    let callbackOrder: string[] = []\n    let validateSawTouchedColumn = false\n    let writeTrackedAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n        ...timestamps(),\n      },\n      timestamps: true,\n      beforeWrite({ value }) {\n        callbackOrder.push('beforeWrite')\n        return {\n          value: {\n            ...value,\n            status: String(value.status).toUpperCase(),\n          },\n        }\n      },\n      validate({ value }) {\n        callbackOrder.push('validate')\n        validateSawTouchedColumn = Object.prototype.hasOwnProperty.call(value, 'created_at')\n        return { value }\n      },\n      afterWrite({ values }) {\n        callbackOrder.push('afterWrite')\n        let payload = values[0] as Record<string, unknown>\n        assert.equal(payload.status, 'ACTIVE')\n        assert.equal(payload.created_at, '2026-01-01T00:00:00.000Z')\n      },\n    })\n\n    let adapter = createAdapter({\n      accounts: [],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await db.create(writeTrackedAccounts, {\n      id: 1,\n      email: 'ops@studio.test',\n      status: 'active',\n    })\n\n    let saved = await db.find(writeTrackedAccounts, 1)\n    assert.ok(saved)\n    assert.equal(saved.status, 'ACTIVE')\n    assert.equal(saved.created_at, '2026-01-01T00:00:00.000Z')\n    assert.equal(validateSawTouchedColumn, false)\n    assert.deepEqual(callbackOrder, ['beforeWrite', 'validate', 'afterWrite'])\n  })\n\n  it('passes scoped delete context to callbacks and reports affected rows', async () => {\n    let beforeDeleteCalls: Array<{\n      tableName: string\n      whereLength: number\n      orderByColumn?: string\n      orderByDirection?: string\n      limit?: number\n    }> = []\n    let afterDeleteAffectedRows: number[] = []\n    let deleteTrackedAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n      },\n      beforeDelete(context) {\n        beforeDeleteCalls.push({\n          tableName: context.tableName,\n          whereLength: context.where.length,\n          orderByColumn: context.orderBy[0]?.column,\n          orderByDirection: context.orderBy[0]?.direction,\n          limit: context.limit,\n        })\n      },\n      afterDelete(context) {\n        afterDeleteAffectedRows.push(context.affectedRows)\n      },\n    })\n\n    let adapter = createAdapter({\n      accounts: [\n        { id: 1, email: 'amy@studio.test', status: 'active' },\n        { id: 2, email: 'brad@studio.test', status: 'active' },\n        { id: 3, email: 'cara@studio.test', status: 'inactive' },\n      ],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    let result = await db\n      .query(deleteTrackedAccounts)\n      .where({ status: 'active' })\n      .orderBy('id', 'asc')\n      .limit(1)\n      .delete()\n\n    assert.equal(result.affectedRows, 1)\n    assert.deepEqual(beforeDeleteCalls, [\n      {\n        tableName: 'accounts',\n        whereLength: 1,\n        orderByColumn: 'id',\n        orderByDirection: 'asc',\n        limit: 1,\n      },\n    ])\n    assert.deepEqual(afterDeleteAffectedRows, [1])\n  })\n\n  it('allows beforeDelete to veto deletes with issues', async () => {\n    let vetoedAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n      },\n      beforeDelete() {\n        return {\n          issues: [{ message: 'Deletes are disabled' }],\n        }\n      },\n    })\n\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async () => {\n        await db.query(vetoedAccounts).where({ id: 1 }).delete()\n      },\n      (error: unknown) =>\n        error instanceof DataTableValidationError &&\n        error.message === 'Invalid value for table \"accounts\"' &&\n        error.metadata?.operation === 'delete' &&\n        error.metadata?.source === 'beforeDelete',\n    )\n  })\n\n  it('includes callback source metadata for afterRead issues', async () => {\n    let issueAfterReadAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n      },\n      afterRead() {\n        return {\n          issues: [{ message: 'Row rejected' }],\n        }\n      },\n    })\n\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async () => {\n        await db.find(issueAfterReadAccounts, 1)\n      },\n      (error: unknown) =>\n        error instanceof DataTableValidationError &&\n        error.message === 'Invalid value for table \"accounts\"' &&\n        error.metadata?.operation === 'read' &&\n        error.metadata?.source === 'afterRead',\n    )\n  })\n\n  it('passes projected row shapes to afterRead callbacks', async () => {\n    let sawMissingStatus = false\n    let projectedAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n      },\n      afterRead({ value }) {\n        sawMissingStatus = !('status' in value)\n        return { value }\n      },\n    })\n\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    let rows = await db.query(projectedAccounts).select({ id: projectedAccounts.id }).all()\n\n    assert.equal(rows.length, 1)\n    assert.equal(rows[0].id, 1)\n    assert.equal(sawMissingStatus, true)\n  })\n\n  it('applies afterRead to root rows, related rows, and write-returning rows', async () => {\n    let readableAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n      },\n      afterRead({ value }) {\n        return {\n          value: {\n            ...value,\n            email: typeof value.email === 'string' ? value.email.toUpperCase() : value.email,\n          },\n        }\n      },\n    })\n    let readableProjects = table({\n      name: 'projects',\n      columns: {\n        id: column.integer(),\n        account_id: column.integer(),\n        name: column.text(),\n        archived: column.boolean(),\n      },\n      afterRead({ value }) {\n        return {\n          value: {\n            ...value,\n            name: typeof value.name === 'string' ? value.name + '!' : value.name,\n          },\n        }\n      },\n    })\n    let readableAccountProjects = hasMany(readableAccounts, readableProjects)\n\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [{ id: 100, account_id: 1, name: 'Spring Campaign', archived: false }],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    let rows = await db.query(readableAccounts).with({ projects: readableAccountProjects }).all()\n    assert.equal(rows[0].email, 'AMY@STUDIO.TEST')\n    assert.equal(rows[0].projects[0].name, 'Spring Campaign!')\n\n    let insertResult = await db\n      .query(readableAccounts)\n      .insert({ id: 2, email: 'new@studio.test', status: 'active' }, { returning: '*' })\n\n    if ('row' in insertResult && insertResult.row) {\n      assert.equal(insertResult.row.email, 'NEW@STUDIO.TEST')\n    } else {\n      assert.fail('Expected row in insert result')\n    }\n  })\n\n  it('enforces synchronous lifecycle callbacks', async () => {\n    let asyncBeforeWriteAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n      },\n      beforeWrite({ value }) {\n        return Promise.resolve({ value }) as never\n      },\n    })\n    let asyncAfterReadAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n      },\n      afterRead({ value }) {\n        return Promise.resolve({ value }) as never\n      },\n    })\n\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async () => {\n        await db.create(asyncBeforeWriteAccounts, { id: 2, email: 'new@studio.test' })\n      },\n      (error: unknown) =>\n        error instanceof DataTableValidationError &&\n        error.message === 'Invalid beforeWrite callback result for table \"accounts\"' &&\n        error.metadata?.source === 'beforeWrite',\n    )\n\n    await assert.rejects(\n      async () => {\n        await db.find(asyncAfterReadAccounts, 1)\n      },\n      (error: unknown) =>\n        error instanceof DataTableValidationError &&\n        error.message === 'Invalid afterRead callback result for table \"accounts\"' &&\n        error.metadata?.source === 'afterRead',\n    )\n  })\n\n  it('throws for invalid beforeDelete callback return values', async () => {\n    let invalidBeforeDeleteAccounts = table({\n      name: 'accounts',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n        status: column.text(),\n      },\n      beforeDelete() {\n        return { value: { allowed: false } } as never\n      },\n    })\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'amy@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async () => {\n        await db.query(invalidBeforeDeleteAccounts).where({ id: 1 }).delete()\n      },\n      (error: unknown) =>\n        error instanceof DataTableValidationError &&\n        error.message === 'Invalid beforeDelete callback result for table \"accounts\"' &&\n        error.metadata?.source === 'beforeDelete',\n    )\n  })\n\n  it('throws for update returning when adapter has no RETURNING support', async () => {\n    let adapter = createAdapter(\n      {\n        accounts: [\n          { id: 1, email: 'amy@studio.test', status: 'active' },\n          { id: 2, email: 'brad@studio.test', status: 'active' },\n        ],\n        projects: [],\n        profiles: [],\n        tasks: [],\n        memberships: [],\n      },\n      { returning: false },\n    )\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async () => {\n        await db\n          .query(accounts)\n          .where({ status: 'active' })\n          .orderBy('id', 'asc')\n          .limit(1)\n          .update({ status: 'inactive' }, { returning: ['id', 'status'] })\n      },\n      (error: unknown) =>\n        error instanceof DataTableQueryError &&\n        error.message === 'update() returning is not supported by this adapter',\n    )\n  })\n\n  it('throws for delete returning when adapter has no RETURNING support', async () => {\n    let adapter = createAdapter(\n      {\n        accounts: [\n          { id: 1, email: 'amy@studio.test', status: 'active' },\n          { id: 2, email: 'brad@studio.test', status: 'active' },\n          { id: 3, email: 'cara@studio.test', status: 'inactive' },\n        ],\n        projects: [],\n        profiles: [],\n        tasks: [],\n        memberships: [],\n      },\n      { returning: false },\n    )\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async () => {\n        await db\n          .query(accounts)\n          .where({ status: 'active' })\n          .orderBy('id', 'asc')\n          .limit(1)\n          .delete({ returning: ['id'] })\n      },\n      (error: unknown) =>\n        error instanceof DataTableQueryError &&\n        error.message === 'delete() returning is not supported by this adapter',\n    )\n  })\n\n  it('throws for write returning when adapter has no RETURNING support', async () => {\n    let adapter = createAdapter(\n      {\n        accounts: [{ id: 1, email: 'founder@studio.test', status: 'active' }],\n        projects: [],\n        profiles: [],\n        tasks: [],\n        memberships: [],\n      },\n      { returning: false },\n    )\n\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async () => {\n        await db.query(accounts).insert(\n          {\n            id: 2,\n            email: 'finance@studio.test',\n            status: 'active',\n          },\n          { returning: ['id', 'email'] },\n        )\n      },\n      (error: unknown) =>\n        error instanceof DataTableQueryError &&\n        error.message === 'insert() returning is not supported by this adapter',\n    )\n\n    await assert.rejects(\n      async () => {\n        await db.query(accounts).insertMany(\n          [\n            { id: 2, email: 'finance@studio.test', status: 'active' },\n            { id: 3, email: 'ops@studio.test', status: 'active' },\n          ],\n          { returning: ['id', 'email'] },\n        )\n      },\n      (error: unknown) =>\n        error instanceof DataTableQueryError &&\n        error.message === 'insertMany() returning is not supported by this adapter',\n    )\n\n    await assert.rejects(\n      async () => {\n        await db.query(accounts).upsert(\n          {\n            id: 1,\n            email: 'founder@studio.test',\n            status: 'inactive',\n          },\n          { returning: ['id', 'status'] },\n        )\n      },\n      (error: unknown) =>\n        error instanceof DataTableQueryError &&\n        error.message === 'upsert() returning is not supported by this adapter',\n    )\n  })\n\n  it('supports insertMany() and delete() returning with RETURNING adapters', async () => {\n    let adapter = createAdapter({\n      accounts: [],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    let inserted = await db.query(accounts).insertMany(\n      [\n        { id: 1, email: 'a@studio.test', status: 'active' },\n        { id: 2, email: 'b@studio.test', status: 'active' },\n      ],\n      { returning: ['id', 'email'] },\n    )\n    assert.ok('rows' in inserted)\n\n    let deleted = await db\n      .query(accounts)\n      .where({ id: 2 })\n      .delete({ returning: ['id'] })\n    assert.ok('rows' in deleted)\n    if ('rows' in deleted) {\n      assert.equal(deleted.rows.length, 1)\n      assert.equal(deleted.rows[0].id, 2)\n    }\n  })\n\n  it('supports upsert() and conflictTarget', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'a@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    let result = await db\n      .query(accounts)\n      .upsert(\n        { id: 1, email: 'a@studio.test', status: 'inactive' },\n        { conflictTarget: ['id'], returning: ['id', 'status'] },\n      )\n\n    assert.ok('row' in result)\n    if ('row' in result) {\n      assert.equal(result.row?.status, 'inactive')\n    }\n  })\n\n  it('throws for upsert() when adapter does not support it', async () => {\n    let adapter = createAdapter(\n      {\n        accounts: [],\n        projects: [],\n        profiles: [],\n        tasks: [],\n        memberships: [],\n      },\n      { upsert: false },\n    )\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async function () {\n        await db\n          .query(accounts)\n          .upsert({ id: 1, email: 'a@studio.test', status: 'active' }, { conflictTarget: ['id'] })\n      },\n      function (error: unknown) {\n        return error instanceof DataTableQueryError\n      },\n    )\n  })\n\n  it('throws when read-only query modifiers are used with write terminals', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'a@studio.test', status: 'active' }],\n      projects: [{ id: 10, account_id: 1, name: 'Alpha', archived: false }],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async function () {\n        await db\n          .query(accounts)\n          .join(projects, eq('accounts.id', 'projects.account_id'))\n          .update({ status: 'inactive' })\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message.includes('update() does not support these query modifiers: join()')\n        )\n      },\n    )\n\n    await assert.rejects(\n      async function () {\n        await db.query(accounts).groupBy('status').delete()\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message.includes('delete() does not support these query modifiers: groupBy()')\n        )\n      },\n    )\n\n    await assert.rejects(\n      async function () {\n        await db\n          .query(accounts)\n          .with({ projects: accountProjects })\n          .upsert({ id: 1, email: 'a@studio.test', status: 'active' })\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message.includes('upsert() does not support these query modifiers: with()')\n        )\n      },\n    )\n  })\n\n  it('throws when scoped query modifiers are used with insert-like terminals', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'a@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async function () {\n        await db.query(accounts).where({ id: 1 }).insert({\n          id: 2,\n          email: 'b@studio.test',\n          status: 'active',\n        })\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message.includes('insert() does not support these query modifiers: where()')\n        )\n      },\n    )\n\n    await assert.rejects(\n      async function () {\n        await db\n          .query(accounts)\n          .orderBy('id', 'asc')\n          .insertMany([{ id: 3, email: 'c@studio.test', status: 'active' }])\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message.includes('insertMany() does not support these query modifiers: orderBy()')\n        )\n      },\n    )\n\n    await assert.rejects(\n      async function () {\n        await db.query(accounts).limit(1).upsert({\n          id: 1,\n          email: 'a@studio.test',\n          status: 'inactive',\n        })\n      },\n      function (error: unknown) {\n        return (\n          error instanceof DataTableQueryError &&\n          error.message.includes('upsert() does not support these query modifiers: limit()')\n        )\n      },\n    )\n  })\n\n  it('does not validate filter values at runtime', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'a@studio.test', status: 'active' }],\n      projects: [{ id: 100, account_id: 1, name: 'Alpha', archived: false }],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await db\n      .query(accounts)\n      .where({ id: 'not-a-number' as never })\n      .all()\n\n    await db\n      .query(accounts)\n      .join(projects, eq('projects.archived', 'nope' as never))\n      .all()\n\n    await db\n      .query(accounts)\n      .groupBy('status')\n      .having(eq('status', 123 as never))\n      .count()\n  })\n})\n\ndescribe('transactions and raw sql', () => {\n  it('supports nested transactions using savepoints', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'founder@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await db.transaction(async (outerTransaction) => {\n      await outerTransaction\n        .query(accounts)\n        .insert({ id: 2, email: 'pm@studio.test', status: 'active' })\n\n      await outerTransaction\n        .transaction(async (innerTransaction) => {\n          await innerTransaction\n            .query(accounts)\n            .insert({ id: 3, email: 'design@studio.test', status: 'active' })\n\n          throw new Error('Abort inner transaction')\n        })\n        .catch(() => undefined)\n    })\n\n    let rows = await db.query(accounts).orderBy('id', 'asc').all()\n\n    assert.equal(rows.length, 2)\n    assert.equal(rows[1].email, 'pm@studio.test')\n    assert.deepEqual(\n      rows.map((row) => row.id),\n      [1, 2],\n    )\n  })\n\n  it('treats transaction options as best-effort adapter hints', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'founder@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await db.transaction(\n      async (transactionDatabase) => {\n        await transactionDatabase\n          .query(accounts)\n          .insert({ id: 2, email: 'pm@studio.test', status: 'active' })\n      },\n      {\n        isolationLevel: 'serializable',\n        readOnly: true,\n      },\n    )\n\n    let rows = await db.query(accounts).orderBy('id', 'asc').all()\n    assert.equal(rows.length, 2)\n    assert.equal(rows[0].id, 1)\n    assert.equal(rows[0].email, 'founder@studio.test')\n    assert.equal(rows[1].id, 2)\n    assert.equal(rows[1].email, 'pm@studio.test')\n    assert.equal(rows[1].status, 'active')\n  })\n\n  it('routes raw sql through db.exec', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 42, email: 'raw@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    let result = await db.exec(sql`select * from accounts where id = ${42}`)\n    assert.ok(result.rows)\n    assert.equal(result.rows?.length, 1)\n    assert.equal(result.rows?.[0].id, 42)\n  })\n\n  it('rolls back outer transactions on errors', async () => {\n    let adapter = createAdapter({\n      accounts: [{ id: 1, email: 'founder@studio.test', status: 'active' }],\n      projects: [],\n      profiles: [],\n      tasks: [],\n      memberships: [],\n    })\n    let db = createTestDatabase(adapter)\n\n    await db\n      .transaction(async (transactionDatabase) => {\n        await transactionDatabase\n          .query(accounts)\n          .insert({ id: 2, email: 'pm@studio.test', status: 'active' })\n\n        throw new Error('Abort transaction')\n      })\n      .catch(() => undefined)\n\n    let rows = await db.query(accounts).orderBy('id', 'asc').all()\n    assert.deepEqual(\n      rows.map((row) => ({ id: row.id, email: row.email, status: row.status })),\n      [{ id: 1, email: 'founder@studio.test', status: 'active' }],\n    )\n  })\n\n  it('throws for nested transactions without savepoints', async () => {\n    let adapter = createAdapter(\n      {\n        accounts: [],\n        projects: [],\n        profiles: [],\n        tasks: [],\n        memberships: [],\n      },\n      { savepoints: false },\n    )\n\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async function () {\n        await db.transaction(async (transactionDatabase) => {\n          await transactionDatabase.transaction(async () => undefined)\n        })\n      },\n      function (error: unknown) {\n        return error instanceof DataTableQueryError\n      },\n    )\n  })\n})\n\ndescribe('adapter errors', () => {\n  it('wraps adapter failures in DataTableAdapterError', async () => {\n    let tokens = 0\n\n    let adapter: DatabaseAdapter = {\n      dialect: 'failing',\n      capabilities: {\n        returning: true,\n        savepoints: true,\n        upsert: true,\n        transactionalDdl: false,\n        migrationLock: false,\n      },\n      async execute() {\n        throw new Error('boom')\n      },\n      compileSql() {\n        return []\n      },\n      async migrate() {\n        return {}\n      },\n      async beginTransaction() {\n        tokens += 1\n        return { id: 'tx_' + String(tokens) }\n      },\n      async commitTransaction() {},\n      async rollbackTransaction() {},\n      async createSavepoint() {},\n      async rollbackToSavepoint() {},\n      async releaseSavepoint() {},\n      async hasTable() {\n        return false\n      },\n      async hasColumn() {\n        return false\n      },\n    }\n\n    let db = createTestDatabase(adapter)\n\n    await assert.rejects(\n      async function () {\n        await db.query(accounts).all()\n      },\n      function (error: unknown) {\n        if (!(error instanceof DataTableAdapterError)) {\n          return false\n        }\n\n        return (\n          error.metadata?.dialect === 'failing' &&\n          error.metadata?.operationKind === 'select' &&\n          error.cause instanceof Error &&\n          error.cause.message === 'boom'\n        )\n      },\n    )\n  })\n})\n\nfunction createAdapter(\n  seed: SqliteTestSeed = {},\n  options?: SqliteTestAdapterOptions,\n): DatabaseAdapter {\n  let { adapter, close } = createSqliteTestAdapter(seed, options)\n  cleanups.add(close)\n  return adapter\n}\n\nfunction createTestDatabase(adapter: DatabaseAdapter) {\n  return new Database(adapter, {\n    now() {\n      return '2026-01-01T00:00:00.000Z'\n    },\n  })\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/database.ts",
    "content": "import type {\n  ColumnDefinition,\n  DataManipulationOperation,\n  DataManipulationResult,\n  DatabaseAdapter,\n  TransactionOptions,\n  TransactionToken,\n} from './adapter.ts'\nimport type { ColumnBuilder } from './column.ts'\nimport { DataTableAdapterError, DataTableQueryError } from './errors.ts'\nimport { executeOperation, type QueryExecutionContext } from './database/execution-context.ts'\nimport {\n  asQueryTableInput,\n  getPrimaryKeyWhere,\n  getPrimaryKeyWhereFromRow,\n  normalizeOrderByInput,\n  resolveCreateRowWhere,\n  toWriteResult,\n} from './database/helpers.ts'\nimport { executeQuery } from './database/query-execution.ts'\nimport type {\n  AnyQuery,\n  BoundQueryPhase,\n  Query as QueryObject,\n  QueryExecutionResult,\n} from './query.ts'\nimport { bindQueryRuntime, query as createQuery } from './query.ts'\nimport type { ColumnInput, NormalizeColumnInput, TableMetadataLike } from './references.ts'\nimport type { SqlStatement } from './sql.ts'\nimport { isSqlStatement, rawSql } from './sql.ts'\nimport type {\n  AnyRelation,\n  AnyTable,\n  LoadedRelationMap,\n  OrderDirection,\n  PrimaryKeyInput,\n  TableName,\n  TablePrimaryKey,\n  TableRow,\n  TableRowWith,\n  TableValidate,\n  tableMetadataKey,\n  TimestampConfig,\n} from './table.ts'\nimport { getTableName } from './table.ts'\nimport type { Pretty } from './types.ts'\nimport type { WhereInput } from './operators.ts'\n\nexport type TableColumnName<table extends AnyTable> = keyof TableRow<table> & string\nexport type QualifiedTableColumnName<table extends AnyTable> =\n  `${TableName<table>}.${TableColumnName<table>}`\nexport type QueryColumnName<table extends AnyTable> =\n  | TableColumnName<table>\n  | QualifiedTableColumnName<table>\n\ntype RowColumnName<row extends Record<string, unknown>> = keyof row & string\ntype QualifiedRowColumnName<\n  tableName extends string,\n  row extends Record<string, unknown>,\n> = `${tableName}.${RowColumnName<row>}`\n\nexport type QueryColumnTypeMapFromRow<\n  tableName extends string,\n  row extends Record<string, unknown>,\n> = {\n  [column in\n    | RowColumnName<row>\n    | QualifiedRowColumnName<tableName, row>]: column extends RowColumnName<row>\n    ? row[column]\n    : column extends `${tableName}.${infer name extends RowColumnName<row>}`\n      ? row[name]\n      : never\n}\n\nexport type QueryColumnTypeMap<table extends AnyTable> = Pretty<\n  QueryColumnTypeMapFromRow<TableName<table>, TableRow<table>>\n>\n\nexport type MergeColumnTypeMaps<\n  left extends Record<string, unknown>,\n  right extends Record<string, unknown>,\n> = Pretty<{\n  [column in Extract<keyof left | keyof right, string>]: column extends keyof right\n    ? column extends keyof left\n      ? left[column] | right[column]\n      : right[column]\n    : column extends keyof left\n      ? left[column]\n      : never\n}>\n\nexport type QueryColumns<columnTypes extends Record<string, unknown>> = Extract<\n  keyof columnTypes,\n  string\n>\n\nexport type QueryColumnInput<columnTypes extends Record<string, unknown>> = ColumnInput<\n  QueryColumns<columnTypes>\n>\n\nexport type SelectedAliasRow<\n  columnTypes extends Record<string, unknown>,\n  selection extends Record<string, QueryColumnInput<columnTypes>>,\n> = Pretty<{\n  [alias in keyof selection]: NormalizeColumnInput<selection[alias]> extends keyof columnTypes\n    ? columnTypes[NormalizeColumnInput<selection[alias]>]\n    : never\n}>\n\nexport type RelationMapForSourceName<tableName extends string> = Record<\n  string,\n  AnyRelation & {\n    sourceTable: {\n      [tableMetadataKey]: {\n        name: tableName\n      }\n    }\n  }\n>\n\nexport type PrimaryKeyInputForRow<\n  row extends Record<string, unknown>,\n  primaryKey extends readonly string[],\n> = primaryKey extends readonly [infer column extends keyof row & string]\n  ? row[column]\n  : {\n      [column in primaryKey[number] & keyof row]: row[column]\n    }\n\nexport type ReturningInput<row extends Record<string, unknown>> = '*' | (keyof row & string)[]\n\n/**\n * Table-like metadata accepted by `database.query()`.\n */\nexport type QueryTableInput<\n  tableName extends string,\n  row extends Record<string, unknown>,\n  primaryKey extends readonly (keyof row & string)[],\n> = TableMetadataLike<\n  tableName,\n  {\n    [column in keyof row & string]: ColumnBuilder<row[column]>\n  },\n  primaryKey,\n  TimestampConfig | null\n> & {\n  [tableMetadataKey]: {\n    name: tableName\n    columns: {\n      [column in keyof row & string]: ColumnBuilder<row[column]>\n    }\n    primaryKey: primaryKey\n    timestamps: TimestampConfig | null\n    columnDefinitions: Record<string, ColumnDefinition>\n    validate?: TableValidate<Record<string, unknown>>\n  }\n} & Record<string, unknown>\n\n/**\n * Result metadata for write operations that do not return rows.\n */\nexport type WriteResult = {\n  affectedRows: number\n  insertId?: unknown\n}\n\n/**\n * Result metadata for write operations that return multiple rows.\n */\nexport type WriteRowsResult<row> = {\n  affectedRows: number\n  insertId?: unknown\n  rows: row[]\n}\n\n/**\n * Result metadata for write operations that return a single row.\n */\nexport type WriteRowResult<row> = {\n  affectedRows: number\n  insertId?: unknown\n  row: row | null\n}\n\n/**\n * Queryable column type map for a concrete table.\n */\nexport type QueryColumnTypesForTable<table extends AnyTable> = QueryColumnTypeMap<table>\n\n/**\n * Query type produced for a concrete table.\n */\nexport type QueryForTable<\n  table extends AnyTable,\n  loaded extends Record<string, unknown> = {},\n> = QueryObject<\n  QueryTableInput<TableName<table>, TableRow<table>, TablePrimaryKey<table>>,\n  QueryColumnTypesForTable<table>,\n  TableRow<table>,\n  loaded,\n  BoundQueryPhase<'all'>\n>\n\n/**\n * Column names accepted in single-table queries.\n */\nexport type SingleTableColumn<table extends AnyTable> = QueryColumns<QueryColumnTypeMap<table>>\n\n/**\n * `where` input accepted in single-table queries.\n */\nexport type SingleTableWhere<table extends AnyTable> = WhereInput<SingleTableColumn<table>>\n\n/**\n * Tuple form accepted by `orderBy` for a single table.\n */\nexport type OrderByTuple<table extends AnyTable> = [\n  column: SingleTableColumn<table>,\n  direction?: OrderDirection,\n]\n\n/**\n * `orderBy` input accepted in single-table queries.\n */\nexport type OrderByInput<table extends AnyTable> = OrderByTuple<table> | OrderByTuple<table>[]\n\n/**\n * Options for loading many rows from a table.\n */\nexport type FindManyOptions<\n  table extends AnyTable,\n  relations extends RelationMapForSourceName<TableName<table>> = {},\n> = {\n  where?: SingleTableWhere<table>\n  orderBy?: OrderByInput<table>\n  limit?: number\n  offset?: number\n  with?: relations\n}\n\n/**\n * Options for loading a single row from a table.\n */\nexport type FindOneOptions<\n  table extends AnyTable,\n  relations extends RelationMapForSourceName<TableName<table>> = {},\n> = Omit<FindManyOptions<table, relations>, 'limit' | 'offset'> & {\n  where: SingleTableWhere<table>\n}\n\n/**\n * Options for updating a single row.\n */\nexport type UpdateOptions<\n  table extends AnyTable,\n  relations extends RelationMapForSourceName<TableName<table>> = {},\n> = {\n  touch?: boolean\n  with?: relations\n}\n\n/**\n * Options for updating many rows.\n */\nexport type UpdateManyOptions<table extends AnyTable> = {\n  where: SingleTableWhere<table>\n  orderBy?: OrderByInput<table>\n  limit?: number\n  offset?: number\n  touch?: boolean\n}\n\n/**\n * Options for deleting many rows.\n */\nexport type DeleteManyOptions<table extends AnyTable> = {\n  where: SingleTableWhere<table>\n  orderBy?: OrderByInput<table>\n  limit?: number\n  offset?: number\n}\n\n/**\n * Options for counting rows.\n */\nexport type CountOptions<table extends AnyTable> = {\n  where?: SingleTableWhere<table>\n}\n\n/**\n * Options for create operations that return only write metadata.\n */\nexport type CreateResultOptions = {\n  touch?: boolean\n  returnRow?: false\n}\n\n/**\n * Options for create operations that return the inserted row.\n */\nexport type CreateRowOptions<\n  table extends AnyTable,\n  relations extends RelationMapForSourceName<TableName<table>> = {},\n> = {\n  touch?: boolean\n  with?: relations\n  returnRow: true\n}\n\n/**\n * Options for bulk-create operations that return only write metadata.\n */\nexport type CreateManyResultOptions = {\n  touch?: boolean\n  returnRows?: false\n}\n\n/**\n * Options for bulk-create operations that return inserted rows.\n */\nexport type CreateManyRowsOptions = {\n  touch?: boolean\n  returnRows: true\n}\n\ntype SavepointCounter = {\n  value: number\n}\n\ntype DatabaseOptions = {\n  now?: () => unknown\n}\n\ntype DatabaseInternalState = {\n  token?: TransactionToken\n  savepointCounter: SavepointCounter\n}\n\nconst createInternalDatabase = Symbol('createInternalDatabase')\n\n/**\n * High-level database runtime used to build and execute data manipulation operations.\n *\n * Create instances directly with `new Database(adapter, options)` or use\n * `createDatabase(adapter, options)` as a thin wrapper.\n */\nexport class Database implements QueryExecutionContext {\n  #adapter: DatabaseAdapter\n  #token?: TransactionToken\n  #now: () => unknown\n  #savepointCounter: SavepointCounter\n\n  constructor(adapter: DatabaseAdapter, options?: DatabaseOptions) {\n    this.#adapter = adapter\n    this.#now = options?.now ?? defaultNow\n    this.#savepointCounter = { value: 0 }\n  }\n\n  static [createInternalDatabase](\n    adapter: DatabaseAdapter,\n    options: DatabaseOptions | undefined,\n    internal: DatabaseInternalState,\n  ): Database {\n    let database = new Database(adapter, options)\n    database.#token = internal.token\n    database.#savepointCounter = internal.savepointCounter\n    return database\n  }\n\n  get adapter(): DatabaseAdapter {\n    return this.#adapter\n  }\n\n  now(): unknown {\n    return this.#now()\n  }\n\n  query<\n    tableName extends string,\n    row extends Record<string, unknown>,\n    primaryKey extends readonly (keyof row & string)[],\n  >(\n    table: QueryTableInput<tableName, row, primaryKey>,\n  ): QueryObject<\n    QueryTableInput<tableName, row, primaryKey>,\n    Pretty<QueryColumnTypeMapFromRow<tableName, row>>,\n    row,\n    {},\n    BoundQueryPhase<'all'>\n  > {\n    return createQuery(table)[bindQueryRuntime](this) as QueryObject<\n      QueryTableInput<tableName, row, primaryKey>,\n      Pretty<QueryColumnTypeMapFromRow<tableName, row>>,\n      row,\n      {},\n      BoundQueryPhase<'all'>\n    >\n  }\n\n  create<table extends AnyTable>(\n    table: table,\n    values: Partial<TableRow<table>>,\n    options?: CreateResultOptions,\n  ): Promise<WriteResult>\n  create<table extends AnyTable, relations extends RelationMapForSourceName<TableName<table>> = {}>(\n    table: table,\n    values: Partial<TableRow<table>>,\n    options: CreateRowOptions<table, relations>,\n  ): Promise<TableRowWith<table, LoadedRelationMap<relations>>>\n  async create<\n    table extends AnyTable,\n    relations extends RelationMapForSourceName<TableName<table>> = {},\n  >(\n    table: table,\n    values: Partial<TableRow<table>>,\n    options?: CreateResultOptions | CreateRowOptions<table, relations>,\n  ): Promise<WriteResult | TableRowWith<table, LoadedRelationMap<relations>>> {\n    let touch = options?.touch\n    let query: QueryForTable<table> = this.query(asQueryTableInput(table))\n\n    if (options?.returnRow !== true) {\n      let result = await query.insert(values, { touch })\n      return toWriteResult(result)\n    }\n\n    if (this.#adapter.capabilities.returning) {\n      let result = (await query.insert(values, {\n        returning: '*',\n        touch,\n      })) as { row: TableRow<table> | null }\n      let row = result.row\n\n      if (!row) {\n        throw new DataTableQueryError(\n          'create({ returnRow: true }) failed to return an inserted row',\n        )\n      }\n\n      if (!options.with) {\n        return row as TableRowWith<table, LoadedRelationMap<relations>>\n      }\n\n      let where = getPrimaryKeyWhereFromRow(table, row)\n      let loaded = await this.findOne(table, {\n        where,\n        with: options.with,\n      })\n\n      if (!loaded) {\n        throw new DataTableQueryError('create({ returnRow: true }) failed to load inserted row')\n      }\n\n      return loaded\n    }\n\n    let insertResult = await query.insert(values, { touch })\n    let where = resolveCreateRowWhere(table, values, toWriteResult(insertResult).insertId)\n    let loaded = await this.findOne(table, {\n      where,\n      with: options.with,\n    })\n\n    if (!loaded) {\n      throw new DataTableQueryError('create({ returnRow: true }) failed to load inserted row')\n    }\n\n    return loaded\n  }\n\n  createMany<table extends AnyTable>(\n    table: table,\n    values: Array<Partial<TableRow<table>>>,\n    options?: CreateManyResultOptions,\n  ): Promise<WriteResult>\n  createMany<table extends AnyTable>(\n    table: table,\n    values: Array<Partial<TableRow<table>>>,\n    options: CreateManyRowsOptions,\n  ): Promise<TableRow<table>[]>\n  async createMany<table extends AnyTable>(\n    table: table,\n    values: Array<Partial<TableRow<table>>>,\n    options?: CreateManyResultOptions | CreateManyRowsOptions,\n  ): Promise<WriteResult | TableRow<table>[]> {\n    let query: QueryForTable<table> = this.query(asQueryTableInput(table))\n\n    if (options?.returnRows === true) {\n      if (!this.#adapter.capabilities.returning) {\n        throw new DataTableQueryError(\n          'createMany({ returnRows: true }) is not supported by this adapter',\n        )\n      }\n\n      let result = (await query.insertMany(values, {\n        returning: '*',\n        touch: options.touch,\n      })) as { rows: TableRow<table>[] }\n\n      return result.rows\n    }\n\n    let result = await query.insertMany(values, {\n      touch: options?.touch,\n    })\n\n    return toWriteResult(result)\n  }\n\n  async find<\n    table extends AnyTable,\n    relations extends RelationMapForSourceName<TableName<table>> = {},\n  >(\n    table: table,\n    value: PrimaryKeyInput<table>,\n    options?: { with?: relations },\n  ): Promise<TableRowWith<table, LoadedRelationMap<relations>> | null> {\n    if (value == null) {\n      return null\n    }\n\n    let query: QueryForTable<table> = this.query(asQueryTableInput(table))\n\n    if (options?.with) {\n      return query.with(options.with).find(value as any) as Promise<TableRowWith<\n        table,\n        LoadedRelationMap<relations>\n      > | null>\n    }\n\n    return query.find(value as any) as Promise<TableRowWith<\n      table,\n      LoadedRelationMap<relations>\n    > | null>\n  }\n\n  async findOne<\n    table extends AnyTable,\n    relations extends RelationMapForSourceName<TableName<table>> = {},\n  >(\n    table: table,\n    options: FindOneOptions<table, relations>,\n  ): Promise<TableRowWith<table, LoadedRelationMap<relations>> | null> {\n    let query: QueryForTable<table> = this.query(asQueryTableInput(table)).where(options.where)\n    let orderBy = normalizeOrderByInput(options.orderBy)\n\n    for (let [column, direction] of orderBy) {\n      query = query.orderBy(column, direction)\n    }\n\n    if (options.with) {\n      return query.with(options.with).first() as Promise<TableRowWith<\n        table,\n        LoadedRelationMap<relations>\n      > | null>\n    }\n\n    return query.first() as Promise<TableRowWith<table, LoadedRelationMap<relations>> | null>\n  }\n\n  async findMany<\n    table extends AnyTable,\n    relations extends RelationMapForSourceName<TableName<table>> = {},\n  >(\n    table: table,\n    options?: FindManyOptions<table, relations>,\n  ): Promise<Array<TableRowWith<table, LoadedRelationMap<relations>>>> {\n    let query: QueryForTable<table> = this.query(asQueryTableInput(table))\n\n    if (options?.where) {\n      query = query.where(options.where)\n    }\n\n    let orderBy = normalizeOrderByInput(options?.orderBy)\n    for (let [column, direction] of orderBy) {\n      query = query.orderBy(column, direction)\n    }\n\n    if (options?.limit !== undefined) {\n      query = query.limit(options.limit)\n    }\n\n    if (options?.offset !== undefined) {\n      query = query.offset(options.offset)\n    }\n\n    if (options?.with) {\n      return query.with(options.with).all() as Promise<\n        Array<TableRowWith<table, LoadedRelationMap<relations>>>\n      >\n    }\n\n    return query.all() as Promise<Array<TableRowWith<table, LoadedRelationMap<relations>>>>\n  }\n\n  async count<table extends AnyTable>(\n    table: table,\n    options?: CountOptions<table>,\n  ): Promise<number> {\n    let query: QueryForTable<table> = this.query(asQueryTableInput(table))\n\n    if (options?.where) {\n      query = query.where(options.where)\n    }\n\n    return query.count()\n  }\n\n  async update<\n    table extends AnyTable,\n    relations extends RelationMapForSourceName<TableName<table>> = {},\n  >(\n    table: table,\n    value: PrimaryKeyInput<table>,\n    changes: Partial<TableRow<table>>,\n    options?: UpdateOptions<table, relations>,\n  ): Promise<TableRowWith<table, LoadedRelationMap<relations>>> {\n    let where = getPrimaryKeyWhere(table, value)\n\n    if (this.#adapter.capabilities.returning) {\n      let updateResult = (await this.query(asQueryTableInput(table)).where(where).update(changes, {\n        touch: options?.touch,\n        returning: '*',\n      })) as { rows: TableRow<table>[] }\n      let updatedRow = updateResult.rows[0]\n\n      if (!updatedRow) {\n        throw new DataTableQueryError(\n          'update() failed to find row for table \"' + getTableName(table) + '\"',\n        )\n      }\n\n      if (!options?.with) {\n        return updatedRow as TableRowWith<table, LoadedRelationMap<relations>>\n      }\n\n      let loaded = await this.findOne(table, {\n        where: getPrimaryKeyWhereFromRow(table, updatedRow),\n        with: options.with,\n      })\n\n      if (!loaded) {\n        throw new DataTableQueryError(\n          'update() failed to find row for table \"' + getTableName(table) + '\"',\n        )\n      }\n\n      return loaded\n    }\n\n    await this.query(asQueryTableInput(table)).where(where).update(changes, {\n      touch: options?.touch,\n    })\n\n    let loaded = await this.find(table, value, { with: options?.with })\n\n    if (!loaded) {\n      throw new DataTableQueryError(\n        'update() failed to find row for table \"' + getTableName(table) + '\"',\n      )\n    }\n\n    return loaded\n  }\n\n  async updateMany<table extends AnyTable>(\n    table: table,\n    changes: Partial<TableRow<table>>,\n    options: UpdateManyOptions<table>,\n  ): Promise<WriteResult> {\n    let query: QueryForTable<table> = this.query(asQueryTableInput(table)).where(options.where)\n    let orderBy = normalizeOrderByInput(options.orderBy)\n\n    for (let [column, direction] of orderBy) {\n      query = query.orderBy(column, direction)\n    }\n\n    if (options.limit !== undefined) {\n      query = query.limit(options.limit)\n    }\n\n    if (options.offset !== undefined) {\n      query = query.offset(options.offset)\n    }\n\n    let result = await query.update(changes, { touch: options.touch })\n    return toWriteResult(result)\n  }\n\n  async delete<table extends AnyTable>(\n    table: table,\n    value: PrimaryKeyInput<table>,\n  ): Promise<boolean> {\n    let where = getPrimaryKeyWhere(table, value)\n    let result = await this.query(asQueryTableInput(table)).where(where).delete()\n    return toWriteResult(result).affectedRows > 0\n  }\n\n  async deleteMany<table extends AnyTable>(\n    table: table,\n    options: DeleteManyOptions<table>,\n  ): Promise<WriteResult> {\n    let query: QueryForTable<table> = this.query(asQueryTableInput(table)).where(options.where)\n    let orderBy = normalizeOrderByInput(options.orderBy)\n\n    for (let [column, direction] of orderBy) {\n      query = query.orderBy(column, direction)\n    }\n\n    if (options.limit !== undefined) {\n      query = query.limit(options.limit)\n    }\n\n    if (options.offset !== undefined) {\n      query = query.offset(options.offset)\n    }\n\n    let result = await query.delete()\n    return toWriteResult(result)\n  }\n\n  async exec(statement: string | SqlStatement, values?: unknown[]): Promise<DataManipulationResult>\n  async exec<input extends AnyQuery>(input: input): Promise<QueryExecutionResult<input>>\n  async exec<input extends AnyQuery>(\n    statementOrInput: string | SqlStatement | input,\n    values: unknown[] = [],\n  ): Promise<DataManipulationResult | QueryExecutionResult<input>> {\n    if (typeof statementOrInput === 'string' || isSqlStatement(statementOrInput)) {\n      let sqlStatement = isSqlStatement(statementOrInput)\n        ? statementOrInput\n        : rawSql(statementOrInput, values)\n\n      return this[executeOperation]({\n        kind: 'raw',\n        sql: sqlStatement,\n      })\n    }\n\n    return executeQuery(this, statementOrInput)\n  }\n\n  async transaction<result>(\n    callback: (database: Database) => Promise<result>,\n    options?: TransactionOptions,\n  ): Promise<result> {\n    if (!this.#token) {\n      let token = await this.#adapter.beginTransaction(options)\n      let tx = Database[createInternalDatabase](\n        this.#adapter,\n        { now: this.#now },\n        {\n          token,\n          savepointCounter: this.#savepointCounter,\n        },\n      )\n\n      try {\n        let result = await callback(tx)\n        await this.#adapter.commitTransaction(token)\n        return result\n      } catch (error) {\n        await this.#adapter.rollbackTransaction(token)\n        throw error\n      }\n    }\n\n    if (!this.#adapter.capabilities.savepoints) {\n      throw new DataTableQueryError('Nested transactions require adapter savepoint support')\n    }\n\n    let savepointName = 'sp_' + String(this.#savepointCounter.value)\n    this.#savepointCounter.value += 1\n\n    await this.#adapter.createSavepoint(this.#token, savepointName)\n\n    try {\n      let result = await callback(this)\n      await this.#adapter.releaseSavepoint(this.#token, savepointName)\n      return result\n    } catch (error) {\n      await this.#adapter.rollbackToSavepoint(this.#token, savepointName)\n      await this.#adapter.releaseSavepoint(this.#token, savepointName)\n      throw error\n    }\n  }\n\n  async [executeOperation](operation: DataManipulationOperation): Promise<DataManipulationResult> {\n    try {\n      return await this.#adapter.execute({\n        operation,\n        transaction: this.#token,\n      })\n    } catch (error) {\n      throw new DataTableAdapterError('Adapter execution failed', {\n        cause: error,\n        metadata: {\n          dialect: this.#adapter.dialect,\n          operationKind: operation.kind,\n        },\n      })\n    }\n  }\n}\n\n/**\n * Creates a database runtime from an adapter.\n * Thin wrapper around `new Database(adapter, options)`.\n * @param adapter Adapter implementation responsible for SQL execution.\n * @param options Optional runtime options.\n * @param options.now Clock function used for auto-managed timestamps.\n * @returns A {@link Database} API instance.\n * @example\n * ```ts\n * import { column as c, createDatabase, table } from 'remix/data-table'\n *\n * let users = table({\n *   name: 'users',\n *   columns: {\n *     id: c.integer(),\n *     email: c.varchar(255),\n *   },\n * })\n *\n * let db = createDatabase(adapter)\n * let rows = await db.query(users).where({ id: 1 }).all()\n * ```\n */\nexport function createDatabase(\n  adapter: DatabaseAdapter,\n  options?: { now?: () => unknown },\n): Database {\n  return new Database(adapter, options)\n}\n\n/**\n * Creates a database runtime bound to an existing adapter transaction token.\n * This is an internal helper used by the migration runner.\n * @param adapter Adapter implementation responsible for SQL execution.\n * @param token Active adapter transaction token.\n * @param options Optional runtime options.\n * @param options.now Clock function used for auto-managed timestamps.\n * @returns A {@link Database} API instance bound to the provided transaction.\n */\nexport function createDatabaseWithTransaction(\n  adapter: DatabaseAdapter,\n  token: TransactionToken,\n  options?: { now?: () => unknown },\n): Database {\n  return Database[createInternalDatabase](adapter, options, {\n    token,\n    savepointCounter: { value: 0 },\n  })\n}\n\nfunction defaultNow(): Date {\n  return new Date()\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/errors.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport {\n  DataTableAdapterError,\n  DataTableConstraintError,\n  DataTableError,\n  DataTableQueryError,\n  DataTableValidationError,\n} from './errors.ts'\n\ndescribe('data-table errors', () => {\n  it('constructs base errors with defaults and options', () => {\n    let base = new DataTableError('boom')\n    assert.equal(base.name, 'DataTableError')\n    assert.equal(base.code, 'DATA_TABLE_ERROR')\n    assert.equal(base.metadata, undefined)\n\n    let cause = new Error('cause')\n    let withOptions = new DataTableError('oops', {\n      code: 'CUSTOM',\n      cause,\n      metadata: { scope: 'test' },\n    })\n\n    assert.equal(withOptions.code, 'CUSTOM')\n    assert.equal(withOptions.cause, cause)\n    assert.deepEqual(withOptions.metadata, { scope: 'test' })\n  })\n\n  it('constructs validation errors', () => {\n    let cause = new Error('invalid')\n    let error = new DataTableValidationError('invalid row', ['missing id'], {\n      cause,\n      metadata: { table: 'accounts' },\n    })\n\n    assert.equal(error.name, 'DataTableValidationError')\n    assert.equal(error.code, 'DATA_TABLE_VALIDATION_ERROR')\n    assert.equal(error.cause, cause)\n    assert.deepEqual(error.issues, ['missing id'])\n    assert.deepEqual(error.metadata, { table: 'accounts' })\n  })\n\n  it('constructs query, adapter, and constraint errors', () => {\n    let queryError = new DataTableQueryError('bad query')\n    assert.equal(queryError.name, 'DataTableQueryError')\n    assert.equal(queryError.code, 'DATA_TABLE_QUERY_ERROR')\n\n    let adapterError = new DataTableAdapterError('adapter failed', {\n      metadata: { adapter: 'postgres' },\n    })\n    assert.equal(adapterError.name, 'DataTableAdapterError')\n    assert.equal(adapterError.code, 'DATA_TABLE_ADAPTER_ERROR')\n    assert.deepEqual(adapterError.metadata, { adapter: 'postgres' })\n\n    let constraintError = new DataTableConstraintError('duplicate key', {\n      metadata: { constraint: 'accounts_pkey' },\n    })\n    assert.equal(constraintError.name, 'DataTableConstraintError')\n    assert.equal(constraintError.code, 'DATA_TABLE_CONSTRAINT_ERROR')\n    assert.deepEqual(constraintError.metadata, { constraint: 'accounts_pkey' })\n  })\n})\n"
  },
  {
    "path": "packages/data-table/src/lib/errors.ts",
    "content": "/**\n * Base error for all `data-table` failures.\n */\nexport class DataTableError extends Error {\n  /**\n   * Stable error code identifying the failure category.\n   */\n  code: string\n\n  /**\n   * Optional structured metadata attached to the failure.\n   */\n  metadata?: Record<string, unknown>\n\n  constructor(\n    message: string,\n    options?: {\n      code?: string\n      cause?: unknown\n      metadata?: Record<string, unknown>\n    },\n  ) {\n    super(message, { cause: options?.cause })\n    this.name = 'DataTableError'\n    this.code = options?.code ?? 'DATA_TABLE_ERROR'\n    this.metadata = options?.metadata\n  }\n}\n\n/**\n * Thrown when input data fails schema validation.\n */\nexport class DataTableValidationError extends DataTableError {\n  /**\n   * Validation issues reported by the schema validator.\n   */\n  issues: ReadonlyArray<unknown>\n\n  constructor(\n    message: string,\n    issues: ReadonlyArray<unknown>,\n    options?: {\n      cause?: unknown\n      metadata?: Record<string, unknown>\n    },\n  ) {\n    super(message, {\n      code: 'DATA_TABLE_VALIDATION_ERROR',\n      cause: options?.cause,\n      metadata: options?.metadata,\n    })\n\n    this.name = 'DataTableValidationError'\n    this.issues = issues\n  }\n}\n\n/**\n * Thrown when a query is invalid for the current builder state.\n */\nexport class DataTableQueryError extends DataTableError {\n  constructor(\n    message: string,\n    options?: {\n      cause?: unknown\n      metadata?: Record<string, unknown>\n    },\n  ) {\n    super(message, {\n      code: 'DATA_TABLE_QUERY_ERROR',\n      cause: options?.cause,\n      metadata: options?.metadata,\n    })\n\n    this.name = 'DataTableQueryError'\n  }\n}\n\n/**\n * Thrown when adapter execution fails.\n */\nexport class DataTableAdapterError extends DataTableError {\n  constructor(\n    message: string,\n    options?: {\n      cause?: unknown\n      metadata?: Record<string, unknown>\n    },\n  ) {\n    super(message, {\n      code: 'DATA_TABLE_ADAPTER_ERROR',\n      cause: options?.cause,\n      metadata: options?.metadata,\n    })\n\n    this.name = 'DataTableAdapterError'\n  }\n}\n\n/**\n * Thrown when a database constraint is violated.\n */\nexport class DataTableConstraintError extends DataTableError {\n  constructor(\n    message: string,\n    options?: {\n      cause?: unknown\n      metadata?: Record<string, unknown>\n    },\n  ) {\n    super(message, {\n      code: 'DATA_TABLE_CONSTRAINT_ERROR',\n      cause: options?.cause,\n      metadata: options?.metadata,\n    })\n\n    this.name = 'DataTableConstraintError'\n  }\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/inflection.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { inferForeignKey, singularize } from './inflection.ts'\n\ndescribe('singularize', () => {\n  it('handles irregular forms', () => {\n    assert.equal(singularize('people'), 'person')\n    assert.equal(singularize('children'), 'child')\n    assert.equal(singularize('indices'), 'index')\n  })\n\n  it('applies suffix rules', () => {\n    assert.equal(singularize('categories'), 'category')\n    assert.equal(singularize('parties'), 'party')\n    assert.equal(singularize('addresses'), 'address')\n    assert.equal(singularize('brushes'), 'brush')\n    assert.equal(singularize('matches'), 'match')\n    assert.equal(singularize('boxes'), 'box')\n    assert.equal(singularize('statuses'), 'status')\n    assert.equal(singularize('projects'), 'project')\n    assert.equal(singularize('class'), 'class')\n  })\n\n  it('preserves leading capitalization', () => {\n    assert.equal(singularize('People'), 'Person')\n    assert.equal(singularize('Categories'), 'Category')\n  })\n\n  it('preserves empty words', () => {\n    assert.equal(singularize(''), '')\n  })\n})\n\ndescribe('inferForeignKey', () => {\n  it('infers single-segment table names', () => {\n    assert.equal(inferForeignKey('accounts'), 'account_id')\n    assert.equal(inferForeignKey('people'), 'person_id')\n  })\n\n  it('infers snake_case table names by singularizing the final segment', () => {\n    assert.equal(inferForeignKey('company_accounts'), 'company_account_id')\n    assert.equal(inferForeignKey('admin_people'), 'admin_person_id')\n  })\n})\n"
  },
  {
    "path": "packages/data-table/src/lib/inflection.ts",
    "content": "const IRREGULAR_SINGULAR_FORMS: Record<string, string> = {\n  people: 'person',\n  men: 'man',\n  women: 'woman',\n  children: 'child',\n  teeth: 'tooth',\n  feet: 'foot',\n  geese: 'goose',\n  mice: 'mouse',\n  data: 'datum',\n  media: 'medium',\n  indices: 'index',\n  vertices: 'vertex',\n  analyses: 'analysis',\n  statuses: 'status',\n  categories: 'category',\n  companies: 'company',\n  addresses: 'address',\n}\n\nexport function singularize(word: string): string {\n  let lower = word.toLowerCase()\n\n  if (IRREGULAR_SINGULAR_FORMS[lower]) {\n    return preserveCase(word, IRREGULAR_SINGULAR_FORMS[lower])\n  }\n\n  if (lower.endsWith('ies') && lower.length > 3) {\n    return preserveCase(word, word.slice(0, -3) + 'y')\n  }\n\n  if (lower.endsWith('sses') || lower.endsWith('shes') || lower.endsWith('ches')) {\n    return preserveCase(word, word.slice(0, -2))\n  }\n\n  if (lower.endsWith('xes') || lower.endsWith('zes')) {\n    return preserveCase(word, word.slice(0, -2))\n  }\n\n  if (lower.endsWith('s') && !lower.endsWith('ss')) {\n    return preserveCase(word, word.slice(0, -1))\n  }\n\n  return word\n}\n\nexport function inferForeignKey(tableName: string): string {\n  let segments = tableName.split('_')\n  let tail = segments.pop() ?? tableName\n  let singularTail = singularize(tail)\n\n  if (segments.length === 0) {\n    return singularTail + '_id'\n  }\n\n  return [...segments, singularTail + '_id'].join('_')\n}\n\nfunction preserveCase(source: string, replacement: string): string {\n  if (source.length === 0) {\n    return replacement\n  }\n\n  if (source[0] === source[0].toUpperCase()) {\n    return replacement[0].toUpperCase() + replacement.slice(1)\n  }\n\n  return replacement\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations/filename.ts",
    "content": "let migrationFilenamePattern = /^(\\d{14})_(.+)\\.(?:m?ts|m?js|cts|cjs)$/\n\n/**\n * Parses a migration filename into `{ id, name }`.\n *\n * Expected format: `YYYYMMDDHHmmss_name.(ts|js|mts|mjs|cts|cjs)`.\n * @param filename Migration file basename.\n * @returns Parsed migration id and name.\n */\nexport function parseMigrationFilename(filename: string): { id: string; name: string } {\n  let match = filename.match(migrationFilenamePattern)\n\n  if (!match) {\n    throw new Error(\n      'Invalid migration filename \"' +\n        filename +\n        '\". Expected format YYYYMMDDHHmmss_name.ts (or .js/.mts/.mjs/.cts/.cjs)',\n    )\n  }\n\n  return {\n    id: match[1],\n    name: match[2],\n  }\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations/helpers.ts",
    "content": "import type { TableRef } from '../adapter.ts'\nimport type { IndexColumns, KeyColumns } from '../migrations.ts'\n\nexport function toTableRef(name: string): TableRef {\n  let segments = name.split('.')\n\n  if (segments.length === 1) {\n    return { name }\n  }\n\n  return {\n    schema: segments[0],\n    name: segments.slice(1).join('.'),\n  }\n}\n\nexport function normalizeIndexColumns(columns: IndexColumns): string[] {\n  return normalizeKeyColumns(columns)\n}\n\nexport function normalizeKeyColumns(columns: KeyColumns): string[] {\n  if (Array.isArray(columns)) {\n    return [...columns]\n  }\n\n  return [columns]\n}\n\nfunction normalizeNamePart(value: string): string {\n  let normalized = value\n    .toLowerCase()\n    .replace(/[^a-z0-9]+/g, '_')\n    .replace(/^_+|_+$/g, '')\n\n  if (normalized.length === 0) {\n    return 'item'\n  }\n\n  return normalized\n}\n\nfunction tableNamePart(table: TableRef): string {\n  if (table.schema) {\n    return normalizeNamePart(table.schema + '_' + table.name)\n  }\n\n  return normalizeNamePart(table.name)\n}\n\nfunction hashString(value: string): number {\n  let hash = 5381\n\n  for (let index = 0; index < value.length; index++) {\n    hash = ((hash << 5) + hash) ^ value.charCodeAt(index)\n  }\n\n  return hash >>> 0\n}\n\nfunction withNameLimit(name: string): string {\n  let limit = 63\n\n  if (name.length <= limit) {\n    return name\n  }\n\n  let suffix = hashString(name).toString(36).padStart(8, '0').slice(0, 8)\n  return name.slice(0, limit - 9) + '_' + suffix\n}\n\nfunction columnsNamePart(columns: string[]): string {\n  return columns.map((column) => normalizeNamePart(column)).join('_')\n}\n\nexport function createPrimaryKeyName(table: TableRef): string {\n  return withNameLimit(tableNamePart(table) + '_pk')\n}\n\nexport function createUniqueName(table: TableRef, columns: string[]): string {\n  return withNameLimit(tableNamePart(table) + '_' + columnsNamePart(columns) + '_uq')\n}\n\nexport function createForeignKeyName(\n  table: TableRef,\n  columns: string[],\n  references: TableRef,\n  referenceColumns: string[],\n): string {\n  let base =\n    tableNamePart(table) +\n    '_' +\n    columnsNamePart(columns) +\n    '_' +\n    tableNamePart(references) +\n    '_' +\n    columnsNamePart(referenceColumns) +\n    '_fk'\n  return withNameLimit(base)\n}\n\nexport function createCheckName(table: TableRef, expression: string): string {\n  let suffix = hashString(expression).toString(36)\n  return withNameLimit(tableNamePart(table) + '_chk_' + suffix)\n}\n\nexport function createIndexName(table: TableRef, columns: string[]): string {\n  return withNameLimit(tableNamePart(table) + '_' + columnsNamePart(columns) + '_idx')\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations/journal-store.ts",
    "content": "import type { DatabaseAdapter, TransactionToken } from '../adapter.ts'\nimport { rawSql } from '../sql.ts'\nimport type { MigrationDescriptor, MigrationJournalRow } from '../migrations.ts'\n\nexport function normalizeChecksum(migration: MigrationDescriptor): string {\n  if (migration.checksum) {\n    return migration.checksum\n  }\n\n  return migration.id + ':' + migration.name\n}\n\nexport async function ensureMigrationJournal(\n  adapter: DatabaseAdapter,\n  tableName: string,\n): Promise<void> {\n  await adapter.migrate({\n    operation: {\n      kind: 'createTable',\n      table: { name: tableName },\n      ifNotExists: true,\n      columns: {\n        id: { type: 'varchar', length: 64, nullable: false, primaryKey: true },\n        name: { type: 'varchar', length: 255, nullable: false },\n        checksum: { type: 'varchar', length: 128, nullable: false },\n        batch: { type: 'integer', nullable: false },\n        applied_at: { type: 'timestamp', nullable: false, default: { kind: 'now' } },\n      },\n    },\n  })\n}\n\nexport async function hasMigrationJournal(\n  adapter: DatabaseAdapter,\n  tableName: string,\n): Promise<boolean> {\n  try {\n    await adapter.execute({\n      operation: {\n        kind: 'raw',\n        sql: rawSql('select 1 from ' + tableName + ' limit 1'),\n      },\n    })\n\n    return true\n  } catch {\n    return false\n  }\n}\n\nexport async function loadJournalRows(\n  adapter: DatabaseAdapter,\n  tableName: string,\n): Promise<MigrationJournalRow[]> {\n  let result = await adapter.execute({\n    operation: {\n      kind: 'raw',\n      sql: rawSql(\n        'select id, name, checksum, batch, applied_at from ' + tableName + ' order by id asc',\n      ),\n    },\n  })\n\n  let rows = result.rows ?? []\n\n  return rows.map((row) => ({\n    id: String(row.id),\n    name: String(row.name),\n    checksum: String(row.checksum),\n    batch: Number(row.batch),\n    appliedAt: new Date(String(row.applied_at)),\n  }))\n}\n\nexport async function insertJournalRow(\n  adapter: DatabaseAdapter,\n  tableName: string,\n  row: {\n    id: string\n    name: string\n    checksum: string\n    batch: number\n  },\n  transaction?: TransactionToken,\n): Promise<void> {\n  await adapter.execute({\n    operation: {\n      kind: 'raw',\n      sql: rawSql('insert into ' + tableName + ' (id, name, checksum, batch) values (?, ?, ?, ?)', [\n        row.id,\n        row.name,\n        row.checksum,\n        row.batch,\n      ]),\n    },\n    transaction,\n  })\n}\n\nexport async function deleteJournalRow(\n  adapter: DatabaseAdapter,\n  tableName: string,\n  id: string,\n  transaction?: TransactionToken,\n): Promise<void> {\n  await adapter.execute({\n    operation: {\n      kind: 'raw',\n      sql: rawSql('delete from ' + tableName + ' where id = ?', [id]),\n    },\n    transaction,\n  })\n}\n\nexport function getBatch(rows: MigrationJournalRow[]): number {\n  if (rows.length === 0) {\n    return 1\n  }\n\n  let max = Math.max(...rows.map((row) => row.batch))\n  return max + 1\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations/registry.ts",
    "content": "import type { MigrationDescriptor, MigrationRegistry } from '../migrations.ts'\n\n/**\n * Returns a new array of migrations ordered by migration id.\n * @param migrations Migration descriptors to sort.\n * @returns A newly sorted migration descriptor array.\n */\nexport function sortMigrations(migrations: MigrationDescriptor[]): MigrationDescriptor[] {\n  return [...migrations].sort((left, right) => left.id.localeCompare(right.id))\n}\n\n/**\n * Resolves a migration source into a sorted migration list.\n * @param input Migration list or registry.\n * @returns A sorted migration descriptor list.\n */\nexport function resolveMigrations(\n  input: MigrationDescriptor[] | MigrationRegistry,\n): MigrationDescriptor[] {\n  if (Array.isArray(input)) {\n    return sortMigrations(input)\n  }\n\n  return input.list()\n}\n\n/**\n * Creates an in-memory migration registry.\n * @param initial Optional initial migration list.\n * @returns A migration registry with duplicate-id protection.\n * @example\n * ```ts\n * import { createMigrationRegistry } from 'remix/data-table/migrations'\n *\n * let registry = createMigrationRegistry()\n * registry.register({ id, name, migration })\n * ```\n */\nexport function createMigrationRegistry(initial: MigrationDescriptor[] = []): MigrationRegistry {\n  let migrations = new Map<string, MigrationDescriptor>()\n\n  for (let migration of initial) {\n    if (migrations.has(migration.id)) {\n      throw new Error('Duplicate migration id: ' + migration.id)\n    }\n\n    migrations.set(migration.id, migration)\n  }\n\n  return {\n    register(migration: MigrationDescriptor) {\n      if (migrations.has(migration.id)) {\n        throw new Error('Duplicate migration id: ' + migration.id)\n      }\n\n      migrations.set(migration.id, migration)\n    },\n    list() {\n      return sortMigrations(Array.from(migrations.values()))\n    },\n  }\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations/runner.ts",
    "content": "import { createDatabase, createDatabaseWithTransaction } from '../database.ts'\nimport type { Database } from '../database.ts'\nimport type { DatabaseAdapter, TransactionToken } from '../adapter.ts'\nimport type { SqlStatement } from '../sql.ts'\nimport type {\n  MigrateOptions,\n  MigrateResult,\n  MigrationContext,\n  MigrationDescriptor,\n  MigrationDirection,\n  MigrationJournalRow,\n  MigrationRegistry,\n  MigrationRunner,\n  MigrationRunnerOptions,\n  MigrationStatus,\n  MigrationStatusEntry,\n} from '../migrations.ts'\n\nimport {\n  deleteJournalRow,\n  ensureMigrationJournal,\n  getBatch,\n  hasMigrationJournal,\n  insertJournalRow,\n  loadJournalRows,\n  normalizeChecksum,\n} from './journal-store.ts'\nimport { resolveMigrations } from './registry.ts'\nimport { createMigrationSchema } from './schema-api.ts'\n\ntype RunMigrationsInput = {\n  adapter: DatabaseAdapter\n  migrations: MigrationDescriptor[]\n  journalTable: string\n  direction: MigrationDirection\n  options: MigrateOptions\n}\n\nfunction assertStepOption(step: number | undefined): void {\n  if (step === undefined) {\n    return\n  }\n\n  if (!Number.isInteger(step) || step < 1) {\n    throw new Error('Invalid migration step option. Expected a positive integer.')\n  }\n}\n\nfunction assertMigrateOptions(options: MigrateOptions): void {\n  if (options.to !== undefined && options.step !== undefined) {\n    throw new Error('Cannot combine \"to\" and \"step\" migration options in the same run')\n  }\n}\n\nfunction assertTargetOption(migrations: MigrationDescriptor[], to: string | undefined): void {\n  if (!to) {\n    return\n  }\n\n  let target = migrations.find((migration) => migration.id === to)\n\n  if (!target) {\n    throw new Error('Unknown migration target: ' + to)\n  }\n}\n\nfunction assertNoMigrationDrift(\n  migrations: MigrationDescriptor[],\n  journal: MigrationJournalRow[],\n): void {\n  let migrationMap = new Map(migrations.map((migration) => [migration.id, migration]))\n\n  for (let row of journal) {\n    let migration = migrationMap.get(row.id)\n\n    if (!migration) {\n      continue\n    }\n\n    let expected = normalizeChecksum(migration)\n\n    if (expected !== row.checksum) {\n      throw new Error(\n        'Migration checksum drift detected for \"' +\n          row.id +\n          '\" (journal=' +\n          row.checksum +\n          ', current=' +\n          expected +\n          ')',\n      )\n    }\n  }\n}\n\nfunction createDryRunDatabase(adapter: DatabaseAdapter): Database {\n  let error = new Error('Cannot execute data operations while running migrations with dryRun')\n  let throwDryRunError = async (): Promise<never> => {\n    throw error\n  }\n  let dryRunAdapter: DatabaseAdapter = {\n    dialect: adapter.dialect,\n    capabilities: adapter.capabilities,\n    compileSql(operation) {\n      return adapter.compileSql(operation)\n    },\n    async hasTable(table) {\n      return adapter.hasTable(table)\n    },\n    async hasColumn(table, column) {\n      return adapter.hasColumn(table, column)\n    },\n    execute: throwDryRunError,\n    migrate: throwDryRunError,\n    beginTransaction: throwDryRunError,\n    commitTransaction: throwDryRunError,\n    rollbackTransaction: throwDryRunError,\n    createSavepoint: throwDryRunError,\n    rollbackToSavepoint: throwDryRunError,\n    releaseSavepoint: throwDryRunError,\n  }\n\n  return createDatabase(dryRunAdapter)\n}\n\nasync function runMigrations(input: RunMigrationsInput): Promise<MigrateResult> {\n  let adapter = input.adapter\n  let migrations = input.migrations\n  let journalTable = input.journalTable\n  let dryRun = Boolean(input.options.dryRun)\n  let target = input.options.to\n  let step = input.options.step\n\n  assertMigrateOptions(input.options)\n  assertStepOption(step)\n  assertTargetOption(migrations, target)\n\n  let sql: SqlStatement[] = []\n\n  await adapter.acquireMigrationLock?.()\n\n  try {\n    let journal: MigrationJournalRow[] = []\n\n    if (dryRun) {\n      let canReadJournal = await hasMigrationJournal(adapter, journalTable)\n\n      if (canReadJournal) {\n        journal = await loadJournalRows(adapter, journalTable)\n      }\n    } else {\n      await ensureMigrationJournal(adapter, journalTable)\n      journal = await loadJournalRows(adapter, journalTable)\n    }\n\n    let appliedMap = new Map(journal.map((row) => [row.id, row]))\n    assertNoMigrationDrift(migrations, journal)\n    let toRun: MigrationDescriptor[] = []\n\n    if (input.direction === 'up') {\n      for (let migration of migrations) {\n        if (!appliedMap.has(migration.id)) {\n          toRun.push(migration)\n        }\n      }\n\n      if (target) {\n        toRun = toRun.filter((migration) => migration.id <= target)\n      }\n\n      if (step !== undefined) {\n        toRun = toRun.slice(0, step)\n      }\n    } else {\n      let appliedMigrations = migrations\n        .filter((migration) => appliedMap.has(migration.id))\n        .reverse()\n\n      if (target) {\n        appliedMigrations = appliedMigrations.filter((migration) => migration.id >= target)\n      }\n\n      if (step !== undefined) {\n        appliedMigrations = appliedMigrations.slice(0, step)\n      }\n\n      toRun = appliedMigrations\n    }\n\n    let applied: MigrationStatusEntry[] = []\n    let reverted: MigrationStatusEntry[] = []\n    let batch = getBatch(journal)\n\n    for (let migration of toRun) {\n      if (\n        migration.migration.transaction === 'required' &&\n        !adapter.capabilities.transactionalDdl\n      ) {\n        throw new Error(\n          'Migration \"' +\n            migration.id +\n            '\" requires transactional DDL, but adapter does not support it',\n        )\n      }\n\n      let shouldUseTransaction =\n        !dryRun &&\n        migration.migration.transaction !== 'none' &&\n        adapter.capabilities.transactionalDdl\n      let token: TransactionToken | undefined\n\n      if (shouldUseTransaction) {\n        token = await adapter.beginTransaction()\n      }\n\n      let db = dryRun\n        ? createDryRunDatabase(adapter)\n        : token\n          ? createDatabaseWithTransaction(adapter, token)\n          : createDatabase(adapter)\n\n      let schema = createMigrationSchema(\n        db,\n        async (operation) => {\n          let compiled = adapter.compileSql(operation)\n          sql.push(...compiled)\n\n          if (!dryRun) {\n            await adapter.migrate({ operation, transaction: token })\n          }\n        },\n        { transaction: token },\n      )\n      let context: MigrationContext = {\n        db,\n        schema,\n      }\n\n      try {\n        if (input.direction === 'up') {\n          await migration.migration.up(context)\n\n          if (!dryRun) {\n            await insertJournalRow(\n              adapter,\n              journalTable,\n              {\n                id: migration.id,\n                name: migration.name,\n                checksum: normalizeChecksum(migration),\n                batch,\n              },\n              token,\n            )\n          }\n\n          applied.push({\n            id: migration.id,\n            name: migration.name,\n            status: 'applied',\n          })\n        } else {\n          await migration.migration.down(context)\n\n          if (!dryRun) {\n            await deleteJournalRow(adapter, journalTable, migration.id, token)\n          }\n\n          reverted.push({\n            id: migration.id,\n            name: migration.name,\n            status: 'pending',\n          })\n        }\n\n        if (token) {\n          await adapter.commitTransaction(token)\n        }\n      } catch (error) {\n        if (token) {\n          await adapter.rollbackTransaction(token)\n        }\n\n        throw error\n      }\n    }\n\n    return {\n      applied,\n      reverted,\n      sql,\n    }\n  } finally {\n    await adapter.releaseMigrationLock?.()\n  }\n}\n\n/**\n * Creates a migration runner for applying/reverting migrations against an adapter.\n * @param adapter Database adapter used to compile and execute migration operations.\n * @param migrations Migration descriptors or registry.\n * @param options Optional runner configuration.\n * @returns A migration runner instance.\n * @example\n * ```ts\n * import { createMigrationRunner } from 'remix/data-table/migrations'\n *\n * let runner = createMigrationRunner(adapter, migrations, {\n *   journalTable: 'app_migrations',\n * })\n * await runner.up()\n * ```\n */\nexport function createMigrationRunner(\n  adapter: DatabaseAdapter,\n  migrations: MigrationDescriptor[] | MigrationRegistry,\n  options: MigrationRunnerOptions = {},\n): MigrationRunner {\n  let journalTable = options.journalTable ?? 'data_table_migrations'\n\n  return {\n    async up(runOptions: MigrateOptions = {}): Promise<MigrateResult> {\n      return runMigrations({\n        adapter,\n        migrations: resolveMigrations(migrations),\n        journalTable,\n        direction: 'up',\n        options: runOptions,\n      })\n    },\n    async down(runOptions: MigrateOptions = {}): Promise<MigrateResult> {\n      return runMigrations({\n        adapter,\n        migrations: resolveMigrations(migrations),\n        journalTable,\n        direction: 'down',\n        options: runOptions,\n      })\n    },\n    async status(): Promise<MigrationStatusEntry[]> {\n      await ensureMigrationJournal(adapter, journalTable)\n\n      let journal = await loadJournalRows(adapter, journalTable)\n      let journalMap = new Map(journal.map((row) => [row.id, row]))\n      let sortedMigrations = resolveMigrations(migrations)\n\n      return sortedMigrations.map((migration) => {\n        let journalRow = journalMap.get(migration.id)\n\n        if (!journalRow) {\n          return {\n            id: migration.id,\n            name: migration.name,\n            status: 'pending' as MigrationStatus,\n          }\n        }\n\n        let checksum = normalizeChecksum(migration)\n\n        return {\n          id: migration.id,\n          name: migration.name,\n          status:\n            checksum === journalRow.checksum\n              ? ('applied' as MigrationStatus)\n              : ('drifted' as MigrationStatus),\n          appliedAt: journalRow.appliedAt,\n          batch: journalRow.batch,\n          checksum: journalRow.checksum,\n        }\n      })\n    },\n  }\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations/schema-api.ts",
    "content": "import type { Database } from '../database.ts'\nimport type {\n  AlterTableChange,\n  CheckConstraint,\n  ColumnDefinition,\n  CreateTableOperation,\n  DataMigrationOperation,\n  ForeignKeyConstraint,\n  PrimaryKeyConstraint,\n  TableRef,\n  TransactionToken,\n  UniqueConstraint,\n} from '../adapter.ts'\nimport { rawSql } from '../sql.ts'\nimport { getTableColumnDefinitions, getTableName, getTablePrimaryKey } from '../table.ts'\nimport type { AnyTable } from '../table.ts'\nimport type {\n  AlterTableBuilder,\n  CreateIndexOptions,\n  ForeignKeyOptions,\n  KeyColumns,\n  MigrationSchema,\n  NamedConstraintOptions,\n  TableInput,\n} from '../migrations.ts'\n\nimport { ColumnBuilder } from '../column.ts'\nimport {\n  createCheckName,\n  createForeignKeyName,\n  createIndexName,\n  createPrimaryKeyName,\n  createUniqueName,\n  normalizeIndexColumns,\n  normalizeKeyColumns,\n  toTableRef,\n} from './helpers.ts'\n\nfunction asColumnDefinition(definition: ColumnDefinition | ColumnBuilder): ColumnDefinition {\n  if (definition instanceof ColumnBuilder) {\n    return definition.build()\n  }\n\n  return definition\n}\n\nfunction asTableRef(value: TableInput): TableRef {\n  if (typeof value === 'string') {\n    return toTableRef(value)\n  }\n\n  return toTableRef(getTableName(value))\n}\n\nfunction lowerTableForCreate(table: AnyTable): CreateTableOperation {\n  let tableRef = toTableRef(getTableName(table))\n  let sourceColumnDefinitions = getTableColumnDefinitions(table)\n  let columns: Record<string, ColumnDefinition> = {}\n  let uniques: UniqueConstraint[] = []\n  let checks: CheckConstraint[] = []\n  let foreignKeys: ForeignKeyConstraint[] = []\n\n  for (let columnName in sourceColumnDefinitions) {\n    if (!Object.prototype.hasOwnProperty.call(sourceColumnDefinitions, columnName)) {\n      continue\n    }\n\n    let sourceDefinition = sourceColumnDefinitions[columnName]\n    let columnDefinition: ColumnDefinition = {\n      ...sourceDefinition,\n      checks: undefined,\n      references: undefined,\n      primaryKey: undefined,\n    }\n\n    let unique = sourceDefinition.unique\n\n    if (unique) {\n      let uniqueName =\n        typeof unique === 'object' && unique.name\n          ? unique.name\n          : createUniqueName(tableRef, [columnName])\n      uniques.push({\n        name: uniqueName,\n        columns: [columnName],\n      })\n      columnDefinition.unique = undefined\n    }\n\n    if (sourceDefinition.checks) {\n      for (let check of sourceDefinition.checks) {\n        checks.push({\n          name: check.name || createCheckName(tableRef, check.expression),\n          expression: check.expression,\n        })\n      }\n    }\n\n    if (sourceDefinition.references) {\n      let referenceColumns = [...sourceDefinition.references.columns]\n      let referencesTable = { ...sourceDefinition.references.table }\n      foreignKeys.push({\n        name:\n          sourceDefinition.references.name ||\n          createForeignKeyName(tableRef, [columnName], referencesTable, referenceColumns),\n        columns: [columnName],\n        references: {\n          table: referencesTable,\n          columns: referenceColumns,\n        },\n        onDelete: sourceDefinition.references.onDelete,\n        onUpdate: sourceDefinition.references.onUpdate,\n      })\n    }\n\n    columns[columnName] = columnDefinition\n  }\n\n  let primaryKeyColumns = [...getTablePrimaryKey(table)]\n  let primaryKey: PrimaryKeyConstraint | undefined\n\n  if (primaryKeyColumns.length > 0) {\n    primaryKey = {\n      columns: primaryKeyColumns,\n      name: createPrimaryKeyName(tableRef),\n    }\n  }\n\n  return {\n    kind: 'createTable',\n    table: tableRef,\n    columns,\n    primaryKey,\n    uniques: uniques.length > 0 ? uniques : undefined,\n    checks: checks.length > 0 ? checks : undefined,\n    foreignKeys: foreignKeys.length > 0 ? foreignKeys : undefined,\n  }\n}\n\nclass AlterTableBuilderRuntime implements AlterTableBuilder {\n  alterChanges: AlterTableChange[] = []\n  extraStatements: DataMigrationOperation[] = []\n  table: TableRef\n\n  constructor(table: TableRef) {\n    this.table = table\n  }\n\n  addColumn(name: string, definition: ColumnDefinition | ColumnBuilder): void {\n    this.alterChanges.push({\n      kind: 'addColumn',\n      column: name,\n      definition: asColumnDefinition(definition),\n    })\n  }\n\n  changeColumn(name: string, definition: ColumnDefinition | ColumnBuilder): void {\n    this.alterChanges.push({\n      kind: 'changeColumn',\n      column: name,\n      definition: asColumnDefinition(definition),\n    })\n  }\n\n  renameColumn(from: string, to: string): void {\n    this.alterChanges.push({ kind: 'renameColumn', from, to })\n  }\n\n  dropColumn(name: string, options?: { ifExists?: boolean }): void {\n    this.alterChanges.push({ kind: 'dropColumn', column: name, ifExists: options?.ifExists })\n  }\n\n  addPrimaryKey(columns: KeyColumns, options?: NamedConstraintOptions): void {\n    let normalizedColumns = normalizeKeyColumns(columns)\n    this.alterChanges.push({\n      kind: 'addPrimaryKey',\n      constraint: {\n        columns: normalizedColumns,\n        name: options?.name ?? createPrimaryKeyName(this.table),\n      },\n    })\n  }\n\n  dropPrimaryKey(name: string): void {\n    this.alterChanges.push({ kind: 'dropPrimaryKey', name })\n  }\n\n  addUnique(columns: KeyColumns, options?: NamedConstraintOptions): void {\n    let normalizedColumns = normalizeKeyColumns(columns)\n    this.alterChanges.push({\n      kind: 'addUnique',\n      constraint: {\n        columns: normalizedColumns,\n        name: options?.name ?? createUniqueName(this.table, normalizedColumns),\n      },\n    })\n  }\n\n  dropUnique(name: string): void {\n    this.alterChanges.push({ kind: 'dropUnique', name })\n  }\n\n  addForeignKey(\n    columns: KeyColumns,\n    refTable: TableInput,\n    refColumns?: KeyColumns,\n    options?: ForeignKeyOptions,\n  ): void {\n    let normalizedColumns = normalizeKeyColumns(columns)\n    let normalizedReferenceColumns = refColumns ? normalizeKeyColumns(refColumns) : ['id']\n    let referenceTable = asTableRef(refTable)\n    this.alterChanges.push({\n      kind: 'addForeignKey',\n      constraint: {\n        columns: normalizedColumns,\n        references: {\n          table: referenceTable,\n          columns: normalizedReferenceColumns,\n        },\n        name:\n          options?.name ??\n          createForeignKeyName(\n            this.table,\n            normalizedColumns,\n            referenceTable,\n            normalizedReferenceColumns,\n          ),\n        onDelete: options?.onDelete,\n        onUpdate: options?.onUpdate,\n      },\n    })\n  }\n\n  dropForeignKey(name: string): void {\n    this.alterChanges.push({ kind: 'dropForeignKey', name })\n  }\n\n  addCheck(expression: string, options?: NamedConstraintOptions): void {\n    this.alterChanges.push({\n      kind: 'addCheck',\n      constraint: {\n        expression,\n        name: options?.name ?? createCheckName(this.table, expression),\n      },\n    })\n  }\n\n  dropCheck(name: string): void {\n    this.alterChanges.push({ kind: 'dropCheck', name })\n  }\n\n  addIndex(columns: string | string[], options?: CreateIndexOptions): void {\n    let normalizedColumns = normalizeIndexColumns(columns)\n    let { name, ifNotExists, ...indexOptions } = options ?? {}\n    this.extraStatements.push({\n      kind: 'createIndex',\n      index: {\n        table: this.table,\n        name: name ?? createIndexName(this.table, normalizedColumns),\n        columns: normalizedColumns,\n        ...indexOptions,\n      },\n      ifNotExists,\n    })\n  }\n\n  dropIndex(name: string): void {\n    this.extraStatements.push({\n      kind: 'dropIndex',\n      table: this.table,\n      name,\n    })\n  }\n\n  comment(text: string): void {\n    this.alterChanges.push({ kind: 'setTableComment', comment: text })\n  }\n}\n\nexport function createMigrationSchema(\n  db: Database,\n  emit: (operation: DataMigrationOperation) => Promise<void>,\n  options?: { transaction?: TransactionToken },\n): MigrationSchema {\n  return {\n    async createTable(table, options) {\n      let operation = lowerTableForCreate(table)\n      operation.ifNotExists = options?.ifNotExists\n      await emit(operation)\n    },\n    async alterTable(input, migrate, options) {\n      let tableRef = asTableRef(input)\n      let builder = new AlterTableBuilderRuntime(tableRef)\n      migrate(builder)\n\n      if (builder.alterChanges.length > 0) {\n        await emit({\n          kind: 'alterTable',\n          table: tableRef,\n          changes: builder.alterChanges,\n          ifExists: options?.ifExists,\n        })\n      }\n\n      for (let operation of builder.extraStatements) {\n        await emit(operation)\n      }\n    },\n    async renameTable(from, to) {\n      await emit({ kind: 'renameTable', from: asTableRef(from), to: toTableRef(to) })\n    },\n    async dropTable(table, options) {\n      await emit({\n        kind: 'dropTable',\n        table: asTableRef(table),\n        ifExists: options?.ifExists,\n        cascade: options?.cascade,\n      })\n    },\n    async createIndex(table, columns, options) {\n      let tableRef = asTableRef(table)\n      let normalizedColumns = normalizeIndexColumns(columns)\n      let { name, ifNotExists, ...indexOptions } = options ?? {}\n      await emit({\n        kind: 'createIndex',\n        index: {\n          table: tableRef,\n          name: name ?? createIndexName(tableRef, normalizedColumns),\n          columns: normalizedColumns,\n          ...indexOptions,\n        },\n        ifNotExists,\n      })\n    },\n    async dropIndex(table, name, options) {\n      await emit({\n        kind: 'dropIndex',\n        table: asTableRef(table),\n        name,\n        ifExists: options?.ifExists,\n      })\n    },\n    async renameIndex(table, from, to) {\n      await emit({\n        kind: 'renameIndex',\n        table: asTableRef(table),\n        from,\n        to,\n      })\n    },\n    async addForeignKey(table, columns, refTable, refColumns, options) {\n      let tableRef = asTableRef(table)\n      let normalizedColumns = normalizeKeyColumns(columns)\n      let referenceTable = asTableRef(refTable)\n      let normalizedReferenceColumns = refColumns ? normalizeKeyColumns(refColumns) : ['id']\n      await emit({\n        kind: 'addForeignKey',\n        table: tableRef,\n        constraint: {\n          columns: normalizedColumns,\n          references: {\n            table: referenceTable,\n            columns: normalizedReferenceColumns,\n          },\n          name:\n            options?.name ??\n            createForeignKeyName(\n              tableRef,\n              normalizedColumns,\n              referenceTable,\n              normalizedReferenceColumns,\n            ),\n          onDelete: options?.onDelete,\n          onUpdate: options?.onUpdate,\n        },\n      })\n    },\n    async dropForeignKey(table, name) {\n      await emit({\n        kind: 'dropForeignKey',\n        table: asTableRef(table),\n        name,\n      })\n    },\n    async addCheck(table, expression, options) {\n      let tableRef = asTableRef(table)\n      await emit({\n        kind: 'addCheck',\n        table: tableRef,\n        constraint: {\n          expression,\n          name: options?.name ?? createCheckName(tableRef, expression),\n        },\n      })\n    },\n    async dropCheck(table, name) {\n      await emit({\n        kind: 'dropCheck',\n        table: asTableRef(table),\n        name,\n      })\n    },\n    async plan(sql) {\n      let statement = typeof sql === 'string' ? rawSql(sql) : sql\n      await emit({\n        kind: 'raw',\n        sql: statement,\n      })\n    },\n    async hasTable(table) {\n      return db.adapter.hasTable(asTableRef(table), options?.transaction)\n    },\n    async hasColumn(table, columnName) {\n      return db.adapter.hasColumn(asTableRef(table), columnName, options?.transaction)\n    },\n  }\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations-node.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { mkdtemp, rm, writeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport path from 'node:path'\nimport { describe, it } from 'node:test'\n\nimport { loadMigrations } from './migrations-node.ts'\n\ndescribe('migration node loader', () => {\n  it('loads migrations and infers ids and names from filenames', async () => {\n    let directory = await mkdtemp(path.join(tmpdir(), 'data-table-migrations-'))\n\n    try {\n      await writeFile(\n        path.join(directory, '20260101000000_create_users.mjs'),\n        ['export default {', '  async up() {},', '  async down() {},', '}', ''].join('\\n'),\n      )\n\n      await writeFile(\n        path.join(directory, '20260102000000_add_posts.mjs'),\n        ['export default {', '  async up() {},', '  async down() {},', '}', ''].join('\\n'),\n      )\n\n      let migrations = await loadMigrations(directory)\n\n      assert.equal(migrations.length, 2)\n      assert.equal(migrations[0].id, '20260101000000')\n      assert.equal(migrations[0].name, 'create_users')\n      assert.equal(migrations[1].id, '20260102000000')\n      assert.equal(migrations[1].name, 'add_posts')\n      assert.match(migrations[0].checksum ?? '', /^[a-f0-9]{64}$/)\n    } finally {\n      await rm(directory, { recursive: true, force: true })\n    }\n  })\n\n  it('throws for invalid migration filename formats', async () => {\n    let directory = await mkdtemp(path.join(tmpdir(), 'data-table-migrations-'))\n\n    try {\n      await writeFile(\n        path.join(directory, 'create_users.mjs'),\n        ['export default {', '  async up() {},', '  async down() {},', '}', ''].join('\\n'),\n      )\n\n      await assert.rejects(\n        () => loadMigrations(directory),\n        /Expected format YYYYMMDDHHmmss_name\\.ts/,\n      )\n    } finally {\n      await rm(directory, { recursive: true, force: true })\n    }\n  })\n\n  it('throws for duplicate ids inferred from filenames', async () => {\n    let directory = await mkdtemp(path.join(tmpdir(), 'data-table-migrations-'))\n\n    try {\n      await writeFile(\n        path.join(directory, '20260101000000_create_users.mjs'),\n        ['export default {', '  async up() {},', '  async down() {},', '}', ''].join('\\n'),\n      )\n\n      await writeFile(\n        path.join(directory, '20260101000000_add_users_index.mjs'),\n        ['export default {', '  async up() {},', '  async down() {},', '}', ''].join('\\n'),\n      )\n\n      await assert.rejects(() => loadMigrations(directory), /Duplicate migration id/)\n    } finally {\n      await rm(directory, { recursive: true, force: true })\n    }\n  })\n})\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations-node.ts",
    "content": "import { createHash } from 'node:crypto'\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport type { Migration, MigrationDescriptor } from './migrations.ts'\nimport { parseMigrationFilename } from './migrations/filename.ts'\n\n/**\n * Loads migration modules from a directory on Node.js.\n *\n * Filenames are used to infer migration `id` and `name`.\n * Each file must default-export `createMigration(...)`.\n * @param directory Absolute or relative directory containing migration files.\n * @returns A sorted list of loaded migration descriptors.\n * @example\n * ```ts\n * import { loadMigrations } from 'remix/data-table/migrations/node'\n *\n * let migrations = await loadMigrations('./app/db/migrations')\n * ```\n */\nexport async function loadMigrations(directory: string): Promise<MigrationDescriptor[]> {\n  let allFiles = (await fs.readdir(directory, { withFileTypes: true }))\n    .filter((entry) => entry.isFile())\n    .map((entry) => entry.name)\n    .sort((left, right) => left.localeCompare(right))\n  let files: Array<{ file: string; id: string; name: string }> = []\n\n  for (let file of allFiles) {\n    if (!/\\.(?:m?ts|m?js|cts|cjs)$/.test(file)) {\n      continue\n    }\n\n    let parsed = parseMigrationFilename(file)\n    files.push({ file, id: parsed.id, name: parsed.name })\n  }\n\n  let migrations: MigrationDescriptor[] = []\n  let seenIds = new Set<string>()\n\n  for (let entry of files) {\n    if (seenIds.has(entry.id)) {\n      throw new Error(\n        'Duplicate migration id \"' + entry.id + '\" inferred from filename \"' + entry.file + '\"',\n      )\n    }\n\n    seenIds.add(entry.id)\n    let fullPath = path.join(directory, entry.file)\n    let source = await fs.readFile(fullPath, 'utf8')\n    let checksum = createHash('sha256').update(source).digest('hex')\n    let module = (await import(pathToFileURL(fullPath).href)) as { default?: Migration }\n    let migration = module.default\n\n    if (!migration || typeof migration.up !== 'function' || typeof migration.down !== 'function') {\n      throw new Error(\n        'Migration file \"' + entry.file + '\" must default-export createMigration(...)',\n      )\n    }\n\n    migrations.push({\n      id: entry.id,\n      name: entry.name,\n      path: fullPath,\n      checksum,\n      migration,\n    })\n  }\n\n  return migrations\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\nimport { rawSql, sql } from './sql.ts'\nimport type {\n  DataManipulationRequest,\n  DataMigrationRequest,\n  DataMigrationResult,\n  DataMigrationOperation,\n  DataManipulationResult,\n  DatabaseAdapter,\n  TableRef,\n  TransactionToken,\n} from './adapter.ts'\nimport { column } from './column.ts'\nimport { parseMigrationFilename } from './migrations/filename.ts'\nimport { createMigrationRegistry } from './migrations/registry.ts'\nimport { createMigrationRunner } from './migrations/runner.ts'\nimport { createMigration } from './migrations.ts'\nimport type { SqlStatement } from './sql.ts'\nimport { table } from './table.ts'\n\ntype JournalRow = {\n  id: string\n  name: string\n  checksum: string\n  batch: number\n  applied_at: string\n}\n\nclass MemoryMigrationAdapter implements DatabaseAdapter {\n  dialect = 'memory'\n  capabilities = {\n    returning: true,\n    savepoints: true,\n    upsert: true,\n    transactionalDdl: true,\n    migrationLock: true,\n  }\n  journalTableCreated = false\n  journalTableName = 'data_table_migrations'\n  journalRows: JournalRow[] = []\n  migratedOperations: DataMigrationOperation[] = []\n  executedRawSql: SqlStatement[] = []\n  executeTransactionIds: Array<string | undefined> = []\n  migrateTransactionIds: Array<string | undefined> = []\n  hasTableTransactionIds: Array<string | undefined> = []\n  hasColumnTransactionIds: Array<string | undefined> = []\n  knownTables = new Map<string, Set<string>>()\n  lockAcquireCount = 0\n  lockReleaseCount = 0\n  beginTransactionCount = 0\n  commitTransactionCount = 0\n  rollbackTransactionCount = 0\n  failOnMigrateKind: DataMigrationOperation['kind'] | undefined\n  #transactionCounter = 0\n  #tokens = new Set<string>()\n\n  compileSql(\n    operation: DataMigrationOperation | DataManipulationRequest['operation'],\n  ): SqlStatement[] {\n    return [{ text: operation.kind, values: [] }]\n  }\n\n  async execute(request: DataManipulationRequest): Promise<DataManipulationResult> {\n    this.executeTransactionIds.push(request.transaction?.id)\n\n    if (request.transaction) {\n      this.#assertToken(request.transaction)\n    }\n\n    if (request.operation.kind !== 'raw') {\n      throw new Error('MemoryMigrationAdapter only supports raw execute operations')\n    }\n\n    let statement = request.operation.sql\n    let text = statement.text.toLowerCase()\n\n    if (text.startsWith('select 1 from ')) {\n      if (!this.journalTableCreated) {\n        throw new Error('Journal table does not exist')\n      }\n\n      return { rows: [] }\n    }\n\n    if (text.includes('select id, name, checksum, batch, applied_at from ')) {\n      if (!this.journalTableCreated) {\n        throw new Error('Journal table does not exist')\n      }\n\n      return {\n        rows: this.journalRows.map((row) => ({\n          id: row.id,\n          name: row.name,\n          checksum: row.checksum,\n          batch: row.batch,\n          applied_at: row.applied_at,\n        })),\n      }\n    }\n\n    if (text.startsWith('insert into ')) {\n      let [id, name, checksum, batch, appliedAt] = statement.values\n\n      this.journalRows.push({\n        id: String(id),\n        name: String(name),\n        checksum: String(checksum),\n        batch: Number(batch),\n        applied_at:\n          typeof appliedAt === 'string' && appliedAt.length > 0\n            ? appliedAt\n            : new Date().toISOString(),\n      })\n\n      return { affectedRows: 1 }\n    }\n\n    if (text.startsWith('delete from ')) {\n      let [id] = statement.values\n      this.journalRows = this.journalRows.filter((row) => row.id !== String(id))\n\n      return { affectedRows: 1 }\n    }\n\n    this.executedRawSql.push(statement)\n    return { affectedRows: 0 }\n  }\n\n  async migrate(request: DataMigrationRequest): Promise<DataMigrationResult> {\n    this.migrateTransactionIds.push(request.transaction?.id)\n\n    if (request.transaction) {\n      this.#assertToken(request.transaction)\n    }\n\n    let operation = request.operation\n\n    if (\n      operation.kind === 'createTable' &&\n      operation.table.name === this.journalTableName &&\n      operation.table.schema === undefined\n    ) {\n      this.journalTableCreated = true\n      this.knownTables.set(\n        tableRefKey(operation.table),\n        new Set<string>(Object.keys(operation.columns)),\n      )\n      return { affectedOperations: 1 }\n    }\n\n    if (this.failOnMigrateKind && operation.kind === this.failOnMigrateKind) {\n      throw new Error('Forced migrate failure for kind ' + operation.kind)\n    }\n\n    if (operation.kind === 'createTable') {\n      this.knownTables.set(\n        tableRefKey(operation.table),\n        new Set<string>(Object.keys(operation.columns)),\n      )\n    }\n\n    if (operation.kind === 'dropTable') {\n      this.knownTables.delete(tableRefKey(operation.table))\n    }\n\n    if (operation.kind === 'renameTable') {\n      let fromKey = tableRefKey(operation.from)\n      let toKey = tableRefKey(operation.to)\n      let columns = this.knownTables.get(fromKey)\n\n      if (columns) {\n        this.knownTables.delete(fromKey)\n        this.knownTables.set(toKey, columns)\n      }\n    }\n\n    if (operation.kind === 'alterTable') {\n      let key = tableRefKey(operation.table)\n      let columns = this.knownTables.get(key) ?? new Set<string>()\n\n      for (let change of operation.changes) {\n        if (change.kind === 'addColumn') {\n          columns.add(change.column)\n        } else if (change.kind === 'dropColumn') {\n          columns.delete(change.column)\n        } else if (change.kind === 'renameColumn') {\n          columns.delete(change.from)\n          columns.add(change.to)\n        }\n      }\n\n      this.knownTables.set(key, columns)\n    }\n\n    this.migratedOperations.push(operation)\n    return { affectedOperations: 1 }\n  }\n\n  async hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {\n    this.hasTableTransactionIds.push(transaction?.id)\n\n    if (transaction) {\n      this.#assertToken(transaction)\n    }\n\n    return this.knownTables.has(tableRefKey(table))\n  }\n\n  async hasColumn(\n    table: TableRef,\n    column: string,\n    transaction?: TransactionToken,\n  ): Promise<boolean> {\n    this.hasColumnTransactionIds.push(transaction?.id)\n\n    if (transaction) {\n      this.#assertToken(transaction)\n    }\n\n    let columns = this.knownTables.get(tableRefKey(table))\n    return columns?.has(column) === true\n  }\n\n  async beginTransaction(): Promise<TransactionToken> {\n    this.beginTransactionCount += 1\n    this.#transactionCounter += 1\n    let token = { id: 'tx_' + String(this.#transactionCounter) }\n    this.#tokens.add(token.id)\n    return token\n  }\n\n  async commitTransaction(token: TransactionToken): Promise<void> {\n    this.#assertToken(token)\n    this.commitTransactionCount += 1\n    this.#tokens.delete(token.id)\n  }\n\n  async rollbackTransaction(token: TransactionToken): Promise<void> {\n    this.#assertToken(token)\n    this.rollbackTransactionCount += 1\n    this.#tokens.delete(token.id)\n  }\n\n  async createSavepoint(token: TransactionToken): Promise<void> {\n    this.#assertToken(token)\n  }\n\n  async rollbackToSavepoint(token: TransactionToken): Promise<void> {\n    this.#assertToken(token)\n  }\n\n  async releaseSavepoint(token: TransactionToken): Promise<void> {\n    this.#assertToken(token)\n  }\n\n  async acquireMigrationLock(): Promise<void> {\n    this.lockAcquireCount += 1\n  }\n\n  async releaseMigrationLock(): Promise<void> {\n    this.lockReleaseCount += 1\n  }\n\n  #assertToken(token: TransactionToken): void {\n    if (!this.#tokens.has(token.id)) {\n      throw new Error('Unknown transaction token: ' + token.id)\n    }\n  }\n}\n\nfunction tableRefKey(table: TableRef): string {\n  if (table.schema) {\n    return table.schema + '.' + table.name\n  }\n\n  return table.name\n}\n\nfunction createIdTable(name: string) {\n  return table({\n    name,\n    columns: {\n      id: column.integer().primaryKey(),\n    },\n  })\n}\n\ndescribe('migration column builder', () => {\n  it('builds canonical column specs with chainable methods', () => {\n    let columnSpec = column\n      .varchar(255)\n      .notNull()\n      .default('hello')\n      .unique('users_email_unique')\n      .references('auth.users', ['id'], 'users_auth_fk')\n      .onDelete('cascade')\n      .onUpdate('restrict')\n      .check('length(email) > 3', 'users_email_len')\n      .comment('Primary email')\n      .computed('lower(email)', { stored: false })\n      .collate('en_US')\n      .charset('utf8mb4')\n      .build()\n\n    assert.deepEqual(columnSpec, {\n      type: 'varchar',\n      length: 255,\n      nullable: false,\n      default: { kind: 'literal', value: 'hello' },\n      unique: { name: 'users_email_unique' },\n      references: {\n        table: { schema: 'auth', name: 'users' },\n        columns: ['id'],\n        name: 'users_auth_fk',\n        onDelete: 'cascade',\n        onUpdate: 'restrict',\n      },\n      checks: [{ expression: 'length(email) > 3', name: 'users_email_len' }],\n      comment: 'Primary email',\n      computed: { expression: 'lower(email)', stored: false },\n      collate: 'en_US',\n      charset: 'utf8mb4',\n    })\n  })\n\n  it('throws when onDelete is called before references', () => {\n    assert.throws(\n      () => column.integer().onDelete('cascade'),\n      /requires references\\(\\) to be set first/,\n    )\n  })\n\n  it('throws when onUpdate is called before references', () => {\n    assert.throws(\n      () => column.integer().onUpdate('cascade'),\n      /requires references\\(\\) to be set first/,\n    )\n  })\n\n  it('supports every column constructor and modifier', () => {\n    let textSpec = column.text().nullable().defaultNow().build()\n    assert.equal(textSpec.type, 'text')\n    assert.equal(textSpec.nullable, true)\n    assert.deepEqual(textSpec.default, { kind: 'now' })\n\n    let integerSpec = column\n      .integer()\n      .defaultSql('42')\n      .unsigned()\n      .autoIncrement()\n      .identity({ always: true, start: 10, increment: 2 })\n      .build()\n    assert.equal(integerSpec.type, 'integer')\n    assert.deepEqual(integerSpec.default, { kind: 'sql', expression: '42' })\n    assert.equal(integerSpec.unsigned, true)\n    assert.equal(integerSpec.autoIncrement, true)\n    assert.deepEqual(integerSpec.identity, { always: true, start: 10, increment: 2 })\n\n    let bigintSpec = column.bigint().build()\n    assert.equal(bigintSpec.type, 'bigint')\n\n    let decimalSpec = column.decimal(8, 2).precision(12).scale(4).build()\n    assert.equal(decimalSpec.type, 'decimal')\n    assert.equal(decimalSpec.precision, 12)\n    assert.equal(decimalSpec.scale, 4)\n\n    let decimalWithPrecisionScale = column.decimal(4, 1).precision(10, 3).build()\n    assert.equal(decimalWithPrecisionScale.type, 'decimal')\n    assert.equal(decimalWithPrecisionScale.precision, 10)\n    assert.equal(decimalWithPrecisionScale.scale, 3)\n\n    let booleanSpec = column.boolean().build()\n    assert.equal(booleanSpec.type, 'boolean')\n\n    let uuidSpec = column.uuid().build()\n    assert.equal(uuidSpec.type, 'uuid')\n\n    let dateSpec = column.date().build()\n    assert.equal(dateSpec.type, 'date')\n\n    let timeSpec = column.time({ precision: 6, withTimezone: true }).timezone(false).build()\n    assert.equal(timeSpec.type, 'time')\n    assert.equal(timeSpec.precision, 6)\n    assert.equal(timeSpec.withTimezone, false)\n\n    let timestampSpec = column.timestamp({ precision: 3, withTimezone: true }).build()\n    assert.equal(timestampSpec.type, 'timestamp')\n    assert.equal(timestampSpec.precision, 3)\n    assert.equal(timestampSpec.withTimezone, true)\n\n    let jsonSpec = column.json().build()\n    assert.equal(jsonSpec.type, 'json')\n\n    let binarySpec = column.binary(64).length(128).build()\n    assert.equal(binarySpec.type, 'binary')\n    assert.equal(binarySpec.length, 128)\n\n    let enumSpec = column.enum(['one', 'two']).build()\n    assert.equal(enumSpec.type, 'enum')\n    assert.deepEqual(enumSpec.enumValues, ['one', 'two'])\n  })\n\n  it('retains existing reference metadata when references() is called again', () => {\n    let spec = column\n      .integer()\n      .references('auth.accounts', 'id', 'accounts_fk')\n      .onDelete('cascade')\n      .onUpdate('restrict')\n      .references('auth.users', 'accounts_fk')\n      .build()\n\n    assert.deepEqual(spec.references, {\n      table: { schema: 'auth', name: 'users' },\n      columns: ['id'],\n      name: 'accounts_fk',\n      onDelete: 'cascade',\n      onUpdate: 'restrict',\n    })\n  })\n})\n\ndescribe('migration runner', () => {\n  it('builds deterministic schema plans from migration APIs', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up({ db, schema }) {\n        let usersTable = table({\n          name: 'app.users',\n          columns: {\n            id: column.integer().primaryKey(),\n            email: column.text().notNull(),\n          },\n        })\n        await schema.createTable(usersTable)\n        await schema.createIndex(usersTable, 'email', { name: 'users_email_idx', unique: true })\n\n        await schema.alterTable(usersTable, (table) => {\n          table.addColumn('status', column.text().default('active'))\n          table.addCheck(\"status in ('active', 'disabled')\", { name: 'users_status_check' })\n          table.addIndex('status', { name: 'users_status_idx' })\n        })\n\n        await schema.renameIndex(usersTable, 'users_status_idx', 'users_status_idx_v2')\n        await schema.plan('vacuum')\n        await schema.plan(sql`select ${123}`)\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'users', migration },\n    ])\n\n    await runner.up()\n\n    assert.deepEqual(\n      adapter.migratedOperations.map((operation) => operation.kind),\n      ['createTable', 'createIndex', 'alterTable', 'createIndex', 'renameIndex', 'raw', 'raw'],\n    )\n\n    let createTableOperation = adapter.migratedOperations[0]\n    assert.equal(createTableOperation.kind, 'createTable')\n    assert.deepEqual(createTableOperation.table, { schema: 'app', name: 'users' })\n    assert.deepEqual(createTableOperation.primaryKey, {\n      name: 'app_users_pk',\n      columns: ['id'],\n    })\n\n    let createIndexOperation = adapter.migratedOperations[1]\n    assert.equal(createIndexOperation.kind, 'createIndex')\n    assert.deepEqual(createIndexOperation.index.columns, ['email'])\n\n    let alterIndexOperation = adapter.migratedOperations[3]\n    assert.equal(alterIndexOperation.kind, 'createIndex')\n    assert.deepEqual(alterIndexOperation.index.columns, ['status'])\n\n    let rawStringOperation = adapter.migratedOperations[5]\n    assert.equal(rawStringOperation.kind, 'raw')\n    assert.deepEqual(rawStringOperation.sql, rawSql('vacuum'))\n\n    let rawStatementOperation = adapter.migratedOperations[6]\n    assert.equal(rawStatementOperation.kind, 'raw')\n    assert.deepEqual(rawStatementOperation.sql, sql`select ${123}`)\n  })\n\n  it('auto-generates deterministic names when omitted', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up({ db, schema }) {\n        let accountsTable = table({\n          name: 'app.accounts',\n          columns: {\n            id: column.integer().primaryKey(),\n          },\n        })\n        let usersTable = table({\n          name: 'app.users',\n          columns: {\n            id: column.integer().primaryKey(),\n            account_id: column.integer().notNull(),\n            email: column.text().notNull().unique(),\n          },\n        })\n\n        await schema.createTable(accountsTable)\n        await schema.createTable(usersTable)\n        await schema.createIndex(usersTable, 'email')\n\n        await schema.alterTable(usersTable, (table) => {\n          table.addPrimaryKey('id')\n          table.addUnique('email')\n          table.addForeignKey('account_id', accountsTable, 'id')\n          table.addCheck('id > 0')\n          table.addIndex('email')\n        })\n\n        await schema.addForeignKey(usersTable, 'account_id', accountsTable, 'id')\n        await schema.addCheck(usersTable, 'id > 0')\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'names', migration },\n    ])\n    await runner.up()\n\n    let createUsers = adapter.migratedOperations[1]\n    assert.equal(createUsers?.kind, 'createTable')\n    if (createUsers.kind !== 'createTable') {\n      throw new Error('Expected createTable operation at index 1')\n    }\n    assert.equal(createUsers.primaryKey?.name, 'app_users_pk')\n    assert.equal(createUsers.uniques?.[0]?.name, 'app_users_email_uq')\n\n    let createIndex = adapter.migratedOperations[2]\n    assert.equal(createIndex?.kind, 'createIndex')\n    if (createIndex.kind !== 'createIndex') {\n      throw new Error('Expected createIndex operation at index 2')\n    }\n    assert.equal(createIndex.index.name, 'app_users_email_idx')\n\n    let alterTable = adapter.migratedOperations[3]\n    assert.equal(alterTable?.kind, 'alterTable')\n    if (alterTable.kind !== 'alterTable') {\n      throw new Error('Expected alterTable operation at index 3')\n    }\n\n    let alterAddPrimaryKey = alterTable.changes.find((change) => change.kind === 'addPrimaryKey')\n    if (!alterAddPrimaryKey || alterAddPrimaryKey.kind !== 'addPrimaryKey') {\n      throw new Error('Expected addPrimaryKey change')\n    }\n    assert.equal(alterAddPrimaryKey.constraint.name, 'app_users_pk')\n\n    let alterAddUnique = alterTable.changes.find((change) => change.kind === 'addUnique')\n    if (!alterAddUnique || alterAddUnique.kind !== 'addUnique') {\n      throw new Error('Expected addUnique change')\n    }\n    assert.equal(alterAddUnique.constraint.name, 'app_users_email_uq')\n\n    let alterAddForeignKey = alterTable.changes.find((change) => change.kind === 'addForeignKey')\n    if (!alterAddForeignKey || alterAddForeignKey.kind !== 'addForeignKey') {\n      throw new Error('Expected addForeignKey change')\n    }\n    assert.equal(alterAddForeignKey.constraint.name, 'app_users_account_id_app_accounts_id_fk')\n\n    let alterAddCheck = alterTable.changes.find((change) => change.kind === 'addCheck')\n    if (!alterAddCheck || alterAddCheck.kind !== 'addCheck') {\n      throw new Error('Expected addCheck change')\n    }\n    assert.ok(alterAddCheck.constraint.name.startsWith('app_users_chk_'))\n\n    let topLevelAddForeignKey = adapter.migratedOperations[5]\n    assert.equal(topLevelAddForeignKey?.kind, 'addForeignKey')\n    if (topLevelAddForeignKey.kind !== 'addForeignKey') {\n      throw new Error('Expected addForeignKey operation at index 5')\n    }\n    assert.equal(topLevelAddForeignKey.constraint.name, 'app_users_account_id_app_accounts_id_fk')\n\n    let topLevelAddCheck = adapter.migratedOperations[6]\n    assert.equal(topLevelAddCheck?.kind, 'addCheck')\n    if (topLevelAddCheck.kind !== 'addCheck') {\n      throw new Error('Expected addCheck operation at index 6')\n    }\n    assert.equal(topLevelAddCheck.constraint.name, alterAddCheck.constraint.name)\n  })\n\n  it('prefers explicit names over generated defaults', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up({ db, schema }) {\n        let usersTable = table({\n          name: 'users',\n          columns: {\n            id: column.integer().primaryKey(),\n            email: column.text().notNull(),\n          },\n        })\n\n        await schema.createTable(usersTable)\n        await schema.createIndex(usersTable, 'email', { name: 'users_email_idx' })\n        await schema.alterTable(usersTable, (table) => {\n          table.addPrimaryKey('id', { name: 'users_pk_named' })\n          table.addUnique('email', { name: 'users_email_uq_named' })\n          table.addCheck('id > 0', { name: 'users_id_check_named' })\n          table.addIndex('email', { name: 'users_email_alter_idx_named' })\n        })\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'named', migration },\n    ])\n    await runner.up()\n\n    let createIndex = adapter.migratedOperations[1]\n    assert.equal(createIndex?.kind, 'createIndex')\n    if (createIndex.kind !== 'createIndex') {\n      throw new Error('Expected createIndex operation at index 1')\n    }\n    assert.equal(createIndex.index.name, 'users_email_idx')\n\n    let alterTable = adapter.migratedOperations[2]\n    assert.equal(alterTable?.kind, 'alterTable')\n    if (alterTable.kind !== 'alterTable') {\n      throw new Error('Expected alterTable operation at index 2')\n    }\n\n    let addPrimaryKey = alterTable.changes.find((change) => change.kind === 'addPrimaryKey')\n    if (!addPrimaryKey || addPrimaryKey.kind !== 'addPrimaryKey') {\n      throw new Error('Expected addPrimaryKey change')\n    }\n    assert.equal(addPrimaryKey.constraint.name, 'users_pk_named')\n\n    let addUnique = alterTable.changes.find((change) => change.kind === 'addUnique')\n    if (!addUnique || addUnique.kind !== 'addUnique') {\n      throw new Error('Expected addUnique change')\n    }\n    assert.equal(addUnique.constraint.name, 'users_email_uq_named')\n\n    let addCheck = alterTable.changes.find((change) => change.kind === 'addCheck')\n    if (!addCheck || addCheck.kind !== 'addCheck') {\n      throw new Error('Expected addCheck change')\n    }\n    assert.equal(addCheck.constraint.name, 'users_id_check_named')\n\n    let alterIndex = adapter.migratedOperations[3]\n    assert.equal(alterIndex?.kind, 'createIndex')\n    if (alterIndex.kind !== 'createIndex') {\n      throw new Error('Expected createIndex operation at index 3')\n    }\n    assert.equal(alterIndex.index.name, 'users_email_alter_idx_named')\n  })\n\n  it('applies, reverts by step, and reverts by target', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migrations = [\n      {\n        id: '20260101000000',\n        name: 'users',\n        migration: createMigration({\n          async up({ db, schema }) {\n            await schema.createTable(createIdTable('users'))\n          },\n          async down({ db, schema }) {\n            await schema.dropTable('users')\n          },\n        }),\n      },\n      {\n        id: '20260102000000',\n        name: 'posts',\n        migration: createMigration({\n          async up({ db, schema }) {\n            await schema.createTable(createIdTable('posts'))\n          },\n          async down({ db, schema }) {\n            await schema.dropTable('posts')\n          },\n        }),\n      },\n    ]\n\n    let runner = createMigrationRunner(adapter, migrations)\n\n    await runner.up()\n    let statusAfterUp = await runner.status()\n    assert.deepEqual(\n      statusAfterUp.map((entry) => entry.status),\n      ['applied', 'applied'],\n    )\n\n    await runner.down({ step: 1 })\n    let statusAfterStepDown = await runner.status()\n    assert.deepEqual(\n      statusAfterStepDown.map((entry) => entry.status),\n      ['applied', 'pending'],\n    )\n\n    await runner.down({ to: '20260101000000' })\n    let statusAfterTargetDown = await runner.status()\n    assert.deepEqual(\n      statusAfterTargetDown.map((entry) => entry.status),\n      ['pending', 'pending'],\n    )\n  })\n\n  it('supports dryRun planning without executing migration DDL', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up({ db, schema }) {\n        await schema.createTable(createIdTable('users'))\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'users', migration },\n    ])\n\n    let result = await runner.up({ dryRun: true })\n\n    assert.deepEqual(result.sql, [{ text: 'createTable', values: [] }])\n    assert.equal(adapter.migratedOperations.length, 0)\n    assert.equal(adapter.journalRows.length, 0)\n  })\n\n  it('detects checksum drift before applying more migrations', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let appliedMigration = createMigration({\n      async up({ db, schema }) {\n        await schema.createTable(createIdTable('users'))\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      {\n        id: '20260101000000',\n        name: 'users',\n        checksum: 'checksum_a',\n        migration: appliedMigration,\n      },\n    ])\n\n    await runner.up()\n\n    let driftedRunner = createMigrationRunner(adapter, [\n      {\n        id: '20260101000000',\n        name: 'users',\n        checksum: 'checksum_b',\n        migration: appliedMigration,\n      },\n    ])\n\n    await assert.rejects(() => driftedRunner.up(), /checksum drift detected/)\n  })\n\n  it('balances migration lock hooks when migration execution fails', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    adapter.failOnMigrateKind = 'createTable'\n\n    let migration = createMigration({\n      async up({ db, schema }) {\n        await schema.createTable(createIdTable('users'))\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'users', migration },\n    ])\n\n    await assert.rejects(() => runner.up(), /Forced migrate failure/)\n    assert.equal(adapter.lockAcquireCount, 1)\n    assert.equal(adapter.lockReleaseCount, 1)\n  })\n\n  it('throws when required transactions are requested on non-transactional adapters', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    adapter.capabilities.transactionalDdl = false\n\n    let migration = createMigration({\n      transaction: 'required',\n      async up({ db, schema }) {\n        await schema.createTable(createIdTable('users'))\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'users', migration },\n    ])\n\n    await assert.rejects(() => runner.up(), /requires transactional DDL/)\n  })\n\n  it('throws for unknown migration targets and invalid step values', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up({ db, schema }) {\n        await schema.createTable(createIdTable('users'))\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'users', migration },\n    ])\n\n    await assert.rejects(() => runner.up({ to: '99999999999999' }), /Unknown migration target/)\n    await assert.rejects(() => runner.up({ step: 0 }), /positive integer/)\n    await assert.rejects(\n      () => runner.up({ to: '20260101000000', step: 1 } as never),\n      /Cannot combine \"to\" and \"step\"/,\n    )\n  })\n\n  it('supports up() target and step boundaries', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migrations = [\n      {\n        id: '20260101000000',\n        name: 'users',\n        migration: createMigration({\n          async up({ db, schema }) {\n            await schema.createTable(createIdTable('users'))\n          },\n          async down() {},\n        }),\n      },\n      {\n        id: '20260102000000',\n        name: 'posts',\n        migration: createMigration({\n          async up({ db, schema }) {\n            await schema.createTable(createIdTable('posts'))\n          },\n          async down() {},\n        }),\n      },\n    ]\n\n    let targetRunner = createMigrationRunner(adapter, migrations)\n    await targetRunner.up({ to: '20260101000000' })\n    assert.deepEqual(\n      adapter.journalRows.map((row) => row.id),\n      ['20260101000000'],\n    )\n\n    adapter.journalRows = []\n    adapter.journalTableCreated = false\n    adapter.migratedOperations = []\n\n    let stepRunner = createMigrationRunner(adapter, migrations)\n    await stepRunner.up({ step: 1 })\n    assert.deepEqual(\n      adapter.journalRows.map((row) => row.id),\n      ['20260101000000'],\n    )\n  })\n\n  it('emits full schema operation shapes from builder methods', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up({ db, schema }) {\n        let accountsTable = table({\n          name: 'app.accounts',\n          columns: {\n            id: column\n              .integer()\n              .primaryKey()\n              .references('auth.users', ['id'], 'accounts_user_fk')\n              .onDelete('cascade')\n              .onUpdate('restrict')\n              .check('id > 0', 'accounts_id_check'),\n            email: column.text().notNull().unique('accounts_email_uq'),\n            nickname: column.text(),\n          },\n        })\n        let accountsV2Table = table({\n          name: 'app.accounts_v2',\n          columns: {\n            id: column.integer().primaryKey(),\n          },\n        })\n        let authUsersTable = table({\n          name: 'auth.users',\n          columns: {\n            id: column.integer().primaryKey(),\n          },\n        })\n        await schema.createTable(accountsTable)\n        await schema.createIndex(accountsTable, ['email', 'id'], {\n          name: 'accounts_email_idx',\n          ifNotExists: true,\n          unique: true,\n          where: 'id > 0',\n          using: 'btree',\n        })\n\n        await schema.alterTable(accountsTable, (table) => {\n          table.addColumn('status', column.text().default('active'))\n          table.changeColumn('status', column.varchar(20).notNull())\n          table.renameColumn('status', 'account_status')\n          table.dropColumn('legacy_status', { ifExists: true })\n          table.addPrimaryKey('id', { name: 'accounts_pk_v2' })\n          table.dropPrimaryKey('accounts_pk_v2')\n          table.addUnique(['account_status'], { name: 'accounts_status_uq' })\n          table.dropUnique('accounts_status_uq')\n          table.addForeignKey('id', accountsTable, 'id', { name: 'accounts_self_fk' })\n          table.dropForeignKey('accounts_self_fk')\n          table.addCheck(\"account_status in ('active', 'disabled')\", {\n            name: 'accounts_status_check',\n          })\n          table.dropCheck('accounts_status_check')\n          table.addIndex(['account_status', 'id'], {\n            name: 'accounts_status_idx',\n            ifNotExists: true,\n          })\n          table.dropIndex('accounts_status_idx')\n          table.comment('Accounts table v2')\n        })\n\n        let accountsExists = await schema.hasTable(accountsTable)\n        let idColumnExists = await schema.hasColumn(accountsTable, 'id')\n\n        if (!accountsExists || !idColumnExists) {\n          throw new Error('Expected schema introspection checks to succeed')\n        }\n\n        await schema.renameTable(accountsTable, 'app.accounts_v2')\n        await schema.dropTable(accountsV2Table, { ifExists: true, cascade: true })\n        await schema.createIndex(accountsTable, ['id', 'email'], {\n          name: 'accounts_compound_idx',\n          unique: true,\n        })\n        await schema.dropIndex(accountsTable, 'accounts_compound_idx', { ifExists: true })\n        await schema.renameIndex(accountsTable, 'accounts_old_idx', 'accounts_new_idx')\n        await schema.addForeignKey(accountsTable, 'id', authUsersTable, undefined, {\n          name: 'accounts_fk_global',\n          onDelete: 'cascade',\n          onUpdate: 'restrict',\n        })\n        await schema.dropForeignKey(accountsTable, 'accounts_fk_global')\n        await schema.addCheck(accountsTable, 'id > 0', { name: 'accounts_global_check' })\n        await schema.dropCheck(accountsTable, 'accounts_global_check')\n        await schema.plan('analyze')\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'accounts', migration },\n    ])\n    await runner.up()\n\n    let kinds = adapter.migratedOperations.map((operation) => operation.kind)\n    assert.deepEqual(kinds, [\n      'createTable',\n      'createIndex',\n      'alterTable',\n      'createIndex',\n      'dropIndex',\n      'renameTable',\n      'dropTable',\n      'createIndex',\n      'dropIndex',\n      'renameIndex',\n      'addForeignKey',\n      'dropForeignKey',\n      'addCheck',\n      'dropCheck',\n      'raw',\n    ])\n\n    let createIndexOperation = adapter.migratedOperations[1]\n    assert.equal(createIndexOperation?.kind, 'createIndex')\n    if (createIndexOperation.kind !== 'createIndex') {\n      throw new Error('Expected createIndex operation at index 1')\n    }\n    assert.equal(createIndexOperation.ifNotExists, true)\n\n    let alterTableOperation = adapter.migratedOperations[2]\n    assert.equal(alterTableOperation?.kind, 'alterTable')\n    if (alterTableOperation.kind !== 'alterTable') {\n      throw new Error('Expected alterTable operation at index 2')\n    }\n\n    let addPrimaryKeyChange = alterTableOperation.changes.find(\n      (change) => change.kind === 'addPrimaryKey',\n    )\n    assert.ok(addPrimaryKeyChange)\n    if (!addPrimaryKeyChange || addPrimaryKeyChange.kind !== 'addPrimaryKey') {\n      throw new Error('Expected addPrimaryKey change')\n    }\n    assert.deepEqual(addPrimaryKeyChange.constraint.columns, ['id'])\n\n    let addForeignKeyChange = alterTableOperation.changes.find(\n      (change) => change.kind === 'addForeignKey',\n    )\n    assert.ok(addForeignKeyChange)\n    if (!addForeignKeyChange || addForeignKeyChange.kind !== 'addForeignKey') {\n      throw new Error('Expected addForeignKey change')\n    }\n    assert.deepEqual(addForeignKeyChange.constraint.columns, ['id'])\n    assert.deepEqual(addForeignKeyChange.constraint.references.columns, ['id'])\n\n    let alterCreateIndexOperation = adapter.migratedOperations[3]\n    assert.equal(alterCreateIndexOperation?.kind, 'createIndex')\n    if (alterCreateIndexOperation.kind !== 'createIndex') {\n      throw new Error('Expected createIndex operation at index 3')\n    }\n    assert.equal(alterCreateIndexOperation.ifNotExists, true)\n\n    let addForeignKeyOperation = adapter.migratedOperations[10]\n    assert.equal(addForeignKeyOperation?.kind, 'addForeignKey')\n    if (addForeignKeyOperation.kind !== 'addForeignKey') {\n      throw new Error('Expected addForeignKey operation at index 10')\n    }\n    assert.deepEqual(addForeignKeyOperation.constraint.columns, ['id'])\n    assert.deepEqual(addForeignKeyOperation.constraint.references.columns, ['id'])\n  })\n\n  it('uses the same transaction token for migrate, exec, and introspection', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      transaction: 'required',\n      async up({ db, schema }) {\n        await schema.createTable(createIdTable('users'))\n        await db.exec(rawSql('select 1'))\n        await schema.hasTable('users')\n        await schema.hasColumn('users', 'id')\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'tx_scope', migration },\n    ])\n    await runner.up()\n\n    assert.equal(adapter.beginTransactionCount, 1)\n    assert.equal(adapter.commitTransactionCount, 1)\n    assert.equal(adapter.rollbackTransactionCount, 0)\n\n    let migrateTokenIds = adapter.migrateTransactionIds.filter((id) => id !== undefined)\n    assert.ok(migrateTokenIds.length > 0)\n    assert.ok(migrateTokenIds.every((id) => id === migrateTokenIds[0]))\n\n    let executeTokenIds = adapter.executeTransactionIds.filter((id) => id !== undefined)\n    assert.ok(executeTokenIds.length > 0)\n    assert.ok(executeTokenIds.every((id) => id === migrateTokenIds[0]))\n\n    let hasTableTokenIds = adapter.hasTableTransactionIds.filter((id) => id !== undefined)\n    assert.ok(hasTableTokenIds.length > 0)\n    assert.ok(hasTableTokenIds.every((id) => id === migrateTokenIds[0]))\n\n    let hasColumnTokenIds = adapter.hasColumnTransactionIds.filter((id) => id !== undefined)\n    assert.ok(hasColumnTokenIds.length > 0)\n    assert.ok(hasColumnTokenIds.every((id) => id === migrateTokenIds[0]))\n  })\n\n  it('uses live adapter introspection during dryRun without simulating planned operations', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    adapter.knownTables.set('app.users', new Set(['email']))\n\n    let migration = createMigration({\n      async up({ db, schema }) {\n        let existingTable = await schema.hasTable('app.users')\n        let existingColumn = await schema.hasColumn('app.users', 'email')\n        let plannedTableBefore = await schema.hasTable('app.pending')\n\n        await schema.createTable(createIdTable('app.pending'))\n\n        let plannedTableAfter = await schema.hasTable('app.pending')\n\n        if (!existingTable || !existingColumn || plannedTableBefore || plannedTableAfter) {\n          throw new Error('Unexpected dry-run introspection behavior')\n        }\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'dry_run', migration },\n    ])\n    await runner.up({ dryRun: true })\n  })\n\n  it('throws when a dryRun migration attempts to use db.exec', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up({ db, schema }) {\n        await db.exec(rawSql('select 1'))\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'dry_run_exec', migration },\n    ])\n    await assert.rejects(\n      () => runner.up({ dryRun: true }),\n      (error: unknown) =>\n        error instanceof Error &&\n        error.message === 'Adapter execution failed' &&\n        'cause' in error &&\n        error.cause instanceof Error &&\n        error.cause.message ===\n          'Cannot execute data operations while running migrations with dryRun',\n    )\n  })\n\n  it('allows compiling SQL through the dryRun database adapter', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up({ db, schema }) {\n        let compiled = db.adapter.compileSql({\n          kind: 'raw',\n          sql: rawSql('select 1'),\n        })\n\n        if (compiled.length !== 1 || compiled[0]?.text !== 'raw') {\n          throw new Error('Unexpected compiled SQL shape')\n        }\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'dry_run_compile', migration },\n    ])\n    await runner.up({ dryRun: true })\n  })\n\n  it('throws when a dryRun migration attempts to open a transaction', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up({ db, schema }) {\n        await db.transaction(async () => {})\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'dry_run_transaction', migration },\n    ])\n    await assert.rejects(\n      () => runner.up({ dryRun: true }),\n      /Cannot execute data operations while running migrations with dryRun/,\n    )\n  })\n\n  it('reads dryRun journal rows when the migration journal table already exists', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    adapter.journalTableCreated = true\n    adapter.journalRows = [\n      {\n        id: '20250101000000',\n        name: 'legacy',\n        checksum: 'legacy:legacy',\n        batch: 1,\n        applied_at: new Date().toISOString(),\n      },\n    ]\n\n    let migration = createMigration({\n      async up({ db, schema }) {\n        await schema.createTable(createIdTable('users'))\n      },\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'users', migration },\n    ])\n    let result = await runner.up({ dryRun: true })\n\n    assert.deepEqual(\n      result.applied.map((entry) => entry.id),\n      ['20260101000000'],\n    )\n    assert.deepEqual(result.sql, [{ text: 'createTable', values: [] }])\n  })\n\n  it('ignores journal rows for migrations that are no longer registered', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    adapter.journalTableCreated = true\n    adapter.journalRows = [\n      {\n        id: '20200101000000',\n        name: 'orphaned',\n        checksum: 'orphaned:orphaned',\n        batch: 1,\n        applied_at: new Date().toISOString(),\n      },\n    ]\n\n    let migration = createMigration({\n      async up() {},\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      { id: '20260101000000', name: 'current', migration },\n    ])\n    await runner.up({ dryRun: true })\n  })\n\n  it('reports drifted status entries when journal checksum does not match migration checksum', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let migration = createMigration({\n      async up() {},\n      async down() {},\n    })\n\n    let runner = createMigrationRunner(adapter, [\n      {\n        id: '20260101000000',\n        name: 'users',\n        checksum: 'checksum_a',\n        migration,\n      },\n    ])\n\n    await runner.up()\n\n    let driftedRunner = createMigrationRunner(adapter, [\n      {\n        id: '20260101000000',\n        name: 'users',\n        checksum: 'checksum_b',\n        migration,\n      },\n    ])\n\n    let statuses = await driftedRunner.status()\n    assert.deepEqual(\n      statuses.map((status) => status.status),\n      ['drifted'],\n    )\n  })\n})\n\ndescribe('migration registry', () => {\n  it('defaults transaction mode to auto when omitted', () => {\n    let migration = createMigration({\n      async up() {},\n      async down() {},\n    })\n\n    assert.equal(migration.transaction, 'auto')\n  })\n\n  it('accepts explicit transaction mode values', () => {\n    let migration = createMigration({\n      transaction: 'none',\n      async up() {},\n      async down() {},\n    })\n\n    assert.equal(migration.transaction, 'none')\n  })\n\n  it('sorts migrations by id and rejects duplicate ids', () => {\n    let first = {\n      id: '20260101000000',\n      name: 'first',\n      migration: createMigration({ async up() {}, async down() {} }),\n    }\n    let second = {\n      id: '20260102000000',\n      name: 'second',\n      migration: createMigration({ async up() {}, async down() {} }),\n    }\n\n    let registry = createMigrationRegistry([second, first])\n    assert.deepEqual(\n      registry.list().map((migration) => migration.id),\n      ['20260101000000', '20260102000000'],\n    )\n\n    assert.throws(\n      () =>\n        createMigrationRegistry([\n          first,\n          {\n            ...first,\n            name: 'duplicate',\n          },\n        ]),\n      /Duplicate migration id/,\n    )\n\n    assert.throws(\n      () =>\n        registry.register({\n          ...first,\n          name: 'duplicate',\n        }),\n      /Duplicate migration id/,\n    )\n  })\n\n  it('works when migration runners are created from a registry input', async () => {\n    let adapter = new MemoryMigrationAdapter()\n    let registry = createMigrationRegistry()\n\n    registry.register({\n      id: '20260101000000',\n      name: 'users',\n      migration: createMigration({\n        async up({ db, schema }) {\n          await schema.createTable(createIdTable('users'))\n        },\n        async down() {},\n      }),\n    })\n\n    let runner = createMigrationRunner(adapter, registry)\n    await runner.up()\n\n    assert.deepEqual(\n      adapter.journalRows.map((row) => row.id),\n      ['20260101000000'],\n    )\n  })\n})\n\ndescribe('migration filename parsing', () => {\n  it('parses migration ids and names from standard filenames', () => {\n    let parsed = parseMigrationFilename('20260101010101_create_users_table.ts')\n\n    assert.deepEqual(parsed, {\n      id: '20260101010101',\n      name: 'create_users_table',\n    })\n  })\n\n  it('rejects invalid migration filenames', () => {\n    assert.throws(\n      () => parseMigrationFilename('create_users_table.ts'),\n      /Expected format YYYYMMDDHHmmss_name\\.ts/,\n    )\n  })\n})\n"
  },
  {
    "path": "packages/data-table/src/lib/migrations.ts",
    "content": "import type { Database } from './database.ts'\nimport type { ColumnDefinition, ForeignKeyAction, IndexDefinition } from './adapter.ts'\nimport type { ColumnBuilder } from './column.ts'\nimport type { SqlStatement } from './sql.ts'\nimport type { AnyTable } from './table.ts'\n\n/**\n * Controls how each migration is wrapped in transactions.\n */\nexport type MigrationTransactionMode = 'auto' | 'required' | 'none'\n\n/**\n * Runtime context passed to migration `up`/`down` handlers.\n */\nexport type MigrationContext = {\n  /**\n   * Immediate data runtime (`query/create/update/exec/transaction`).\n   */\n  db: Database\n  /**\n   * Migration schema runtime (`createTable/alterTable/createIndex/...`).\n   */\n  schema: MigrationSchema\n}\n\n/**\n * Authoring shape for `createMigration(...)`.\n */\nexport type CreateMigrationInput = {\n  up: (context: MigrationContext) => Promise<void> | void\n  down: (context: MigrationContext) => Promise<void> | void\n  transaction?: MigrationTransactionMode\n}\n\n/**\n * Normalized migration object consumed by the registry/runner.\n */\nexport type Migration = {\n  up: CreateMigrationInput['up']\n  down: CreateMigrationInput['down']\n  transaction: MigrationTransactionMode\n}\n\n/**\n * Creates a migration descriptor with normalized defaults.\n * @param input Migration handlers and transaction mode.\n * @returns A normalized migration object.\n * @example\n * ```ts\n * import { createMigration, column as c } from 'remix/data-table/migrations'\n * import { table } from 'remix/data-table'\n *\n * let users = table({\n *   name: 'users',\n *   columns: {\n *     id: c.integer().primaryKey().autoIncrement(),\n *     email: c.varchar(255).notNull().unique(),\n *   },\n * })\n *\n * export default createMigration({\n *   async up({ db, schema }) {\n *     await schema.createTable(users)\n *\n *     if (db.adapter.dialect === 'sqlite') {\n *       await db.exec('pragma foreign_keys = on')\n *     }\n *   },\n *   async down({ schema }) {\n *     await schema.dropTable('users', { ifExists: true })\n *   },\n * })\n * ```\n */\nexport function createMigration(input: CreateMigrationInput): Migration {\n  return {\n    up: input.up,\n    down: input.down,\n    transaction: input.transaction ?? 'auto',\n  }\n}\n\n/**\n * Migration metadata stored in registries and returned by loaders.\n */\nexport type MigrationDescriptor = {\n  id: string\n  name: string\n  path?: string\n  checksum?: string\n  migration: Migration\n}\n\n/**\n * Direction used by migration runner operations.\n */\nexport type MigrationDirection = 'up' | 'down'\n\n/**\n * Row shape persisted in the migration journal table.\n */\nexport type MigrationJournalRow = {\n  id: string\n  name: string\n  checksum: string\n  batch: number\n  appliedAt: Date\n}\n\n/**\n * Effective status for a known migration.\n */\nexport type MigrationStatus = 'applied' | 'pending' | 'drifted'\n\n/**\n * Status row returned by `runner.status()` and `runner.up/down(...)`.\n */\nexport type MigrationStatusEntry = {\n  id: string\n  name: string\n  status: MigrationStatus\n  appliedAt?: Date\n  batch?: number\n  checksum?: string\n}\n\n/**\n * Common options for `runner.up(...)` and `runner.down(...)`.\n * `to` and `step` are mutually exclusive.\n */\nexport type MigrateOptions =\n  | {\n      to: string\n      step?: never\n      dryRun?: boolean\n    }\n  | {\n      to?: never\n      step: number\n      dryRun?: boolean\n    }\n  | {\n      to?: undefined\n      step?: undefined\n      dryRun?: boolean\n    }\n\n/**\n * Result shape returned by migration runner commands.\n */\nexport type MigrateResult = {\n  applied: MigrationStatusEntry[]\n  reverted: MigrationStatusEntry[]\n  /**\n   * Compiled SQL statements for operations processed during this run.\n   * Includes planned SQL when running with `dryRun: true`.\n   */\n  sql: SqlStatement[]\n}\n\n/**\n * Options for `schema.createTable(...)` migration operations.\n */\nexport type CreateTableOptions = { ifNotExists?: boolean }\n/**\n * Options for `schema.alterTable(...)` migration operations.\n */\nexport type AlterTableOptions = { ifExists?: boolean }\n/**\n * Options for `schema.dropTable(...)` migration operations.\n */\nexport type DropTableOptions = { ifExists?: boolean; cascade?: boolean }\n/**\n * Accepts either one index column or multiple (compound index).\n */\nexport type IndexColumns = string | string[]\n\n/**\n * Accepts either one key column or multiple (compound key).\n */\nexport type KeyColumns = string | string[]\n\n/**\n * Accepts either a SQL table name or a `table(...)` object.\n */\nexport type TableInput = string | AnyTable\n\n/**\n * Optional name override for constraints and indexes.\n */\nexport type NamedConstraintOptions = {\n  name?: string\n}\n\n/**\n * Foreign key options for migration APIs.\n */\nexport type ForeignKeyOptions = NamedConstraintOptions & {\n  onDelete?: ForeignKeyAction\n  onUpdate?: ForeignKeyAction\n}\n\n/**\n * Index options for migration APIs.\n */\nexport type CreateIndexOptions = NamedConstraintOptions &\n  Omit<IndexDefinition, 'table' | 'name' | 'columns'> & {\n    ifNotExists?: boolean\n  }\n\n/**\n * Builder API available inside `schema.alterTable(name, table => ...)`.\n */\nexport interface AlterTableBuilder {\n  /** Adds a column during an `alterTable` migration. */\n  addColumn(name: string, definition: ColumnDefinition | ColumnBuilder): void\n  /** Changes an existing column during an `alterTable` migration. */\n  changeColumn(name: string, definition: ColumnDefinition | ColumnBuilder): void\n  /** Renames a column during an `alterTable` migration. */\n  renameColumn(from: string, to: string): void\n  /** Drops a column during an `alterTable` migration. */\n  dropColumn(name: string, options?: { ifExists?: boolean }): void\n  /** Adds a primary key during an `alterTable` migration. */\n  addPrimaryKey(columns: KeyColumns, options?: NamedConstraintOptions): void\n  /** Drops a primary key during an `alterTable` migration. */\n  dropPrimaryKey(name: string): void\n  /** Adds a unique constraint during an `alterTable` migration. */\n  addUnique(columns: KeyColumns, options?: NamedConstraintOptions): void\n  /** Drops a unique constraint during an `alterTable` migration. */\n  dropUnique(name: string): void\n  /** Adds a foreign key during an `alterTable` migration. */\n  addForeignKey(\n    columns: KeyColumns,\n    refTable: TableInput,\n    refColumns?: KeyColumns,\n    options?: ForeignKeyOptions,\n  ): void\n  /** Drops a foreign key during an `alterTable` migration. */\n  dropForeignKey(name: string): void\n  /** Adds a check constraint during an `alterTable` migration. */\n  addCheck(expression: string, options?: NamedConstraintOptions): void\n  /** Drops a check constraint during an `alterTable` migration. */\n  dropCheck(name: string): void\n  /** Adds an index during an `alterTable` migration. */\n  addIndex(columns: IndexColumns, options?: CreateIndexOptions): void\n  /** Drops an index during an `alterTable` migration. */\n  dropIndex(name: string): void\n  /** Sets the table comment during an `alterTable` migration. */\n  comment(text: string): void\n}\n\n/**\n * DDL-focused operations mixed into the migration `db` object.\n */\nexport interface MigrationSchema {\n  /** Creates a table in the migration schema. */\n  createTable<table extends AnyTable>(table: table, options?: CreateTableOptions): Promise<void>\n  /** Alters an existing table in the migration schema. */\n  alterTable(\n    table: TableInput,\n    migrate: (table: AlterTableBuilder) => void,\n    options?: AlterTableOptions,\n  ): Promise<void>\n  /** Renames a table in the migration schema. */\n  renameTable(from: TableInput, to: string): Promise<void>\n  /** Drops a table from the migration schema. */\n  dropTable(table: TableInput, options?: DropTableOptions): Promise<void>\n  /** Creates an index in the migration schema. */\n  createIndex(table: TableInput, columns: IndexColumns, options?: CreateIndexOptions): Promise<void>\n  /** Drops an index from the migration schema. */\n  dropIndex(table: TableInput, name: string, options?: { ifExists?: boolean }): Promise<void>\n  /** Renames an index in the migration schema. */\n  renameIndex(table: TableInput, from: string, to: string): Promise<void>\n  /** Adds a foreign key in the migration schema. */\n  addForeignKey(\n    table: TableInput,\n    columns: KeyColumns,\n    refTable: TableInput,\n    refColumns?: KeyColumns,\n    options?: ForeignKeyOptions,\n  ): Promise<void>\n  /** Drops a foreign key in the migration schema. */\n  dropForeignKey(table: TableInput, name: string): Promise<void>\n  /** Adds a check constraint in the migration schema. */\n  addCheck(table: TableInput, expression: string, options?: NamedConstraintOptions): Promise<void>\n  /** Drops a check constraint in the migration schema. */\n  dropCheck(table: TableInput, name: string): Promise<void>\n  /**\n   * Adds raw SQL to the migration plan as a migration operation.\n   */\n  plan(sql: string | SqlStatement): Promise<void>\n  /**\n   * Returns `true` when the table exists in the current database.\n   */\n  hasTable(table: TableInput): Promise<boolean>\n  /**\n   * Returns `true` when the column exists on the given table.\n   */\n  hasColumn(table: TableInput, column: string): Promise<boolean>\n}\n\n/**\n * Runtime-agnostic migration registry abstraction.\n */\nexport type MigrationRegistry = {\n  register(migration: MigrationDescriptor): void\n  list(): MigrationDescriptor[]\n}\n\n/**\n * Options for creating a migration runner.\n */\nexport type MigrationRunnerOptions = {\n  /**\n   * Journal table used to record applied migrations.\n   * Defaults to `data_table_migrations`.\n   */\n  journalTable?: string\n}\n\n/**\n * Migration runner API for applying, reverting, and inspecting migration state.\n */\nexport type MigrationRunner = {\n  up(options?: MigrateOptions): Promise<MigrateResult>\n  down(options?: MigrateOptions): Promise<MigrateResult>\n  status(): Promise<MigrationStatusEntry[]>\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/operators.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { column } from './column.ts'\nimport {\n  and,\n  between,\n  eq,\n  getPredicateColumns,\n  gt,\n  gte,\n  ilike,\n  inList,\n  isNull,\n  isPredicate,\n  like,\n  lt,\n  lte,\n  ne,\n  normalizeWhereInput,\n  notInList,\n  notNull,\n  or,\n} from './operators.ts'\nimport { table } from './table.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n  },\n})\n\nlet projects = table({\n  name: 'projects',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n  },\n})\n\nlet invoices = table({\n  name: 'billing.invoices',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n    total: column.integer(),\n  },\n})\n\ndescribe('comparison predicates', () => {\n  it('treats qualified string values as column references', () => {\n    let predicate = eq('accounts.id', 'projects.account_id')\n\n    assert.deepEqual(predicate, {\n      type: 'comparison',\n      operator: 'eq',\n      column: 'accounts.id',\n      value: 'projects.account_id',\n      valueType: 'column',\n    })\n  })\n\n  it('supports column reference inputs', () => {\n    let predicate = eq(accounts.id, projects.account_id)\n\n    assert.deepEqual(predicate, {\n      type: 'comparison',\n      operator: 'eq',\n      column: 'accounts.id',\n      value: 'projects.account_id',\n      valueType: 'column',\n    })\n  })\n\n  it('supports cross schema column reference inputs', () => {\n    let predicate = eq(accounts.id, invoices.account_id)\n\n    assert.deepEqual(predicate, {\n      type: 'comparison',\n      operator: 'eq',\n      column: 'accounts.id',\n      value: 'billing.invoices.account_id',\n      valueType: 'column',\n    })\n  })\n\n  it('treats unqualified values as scalar values', () => {\n    let predicate = eq('id', 'projects.account_id')\n\n    assert.deepEqual(predicate, {\n      type: 'comparison',\n      operator: 'eq',\n      column: 'id',\n      value: 'projects.account_id',\n      valueType: 'value',\n    })\n  })\n\n  it('builds standard comparison predicates', () => {\n    assert.deepEqual(ne('status', 'active'), {\n      type: 'comparison',\n      operator: 'ne',\n      column: 'status',\n      value: 'active',\n      valueType: 'value',\n    })\n\n    assert.deepEqual(gt('score', 10), {\n      type: 'comparison',\n      operator: 'gt',\n      column: 'score',\n      value: 10,\n      valueType: 'value',\n    })\n\n    assert.deepEqual(gte('score', 10), {\n      type: 'comparison',\n      operator: 'gte',\n      column: 'score',\n      value: 10,\n      valueType: 'value',\n    })\n\n    assert.deepEqual(lt('score', 10), {\n      type: 'comparison',\n      operator: 'lt',\n      column: 'score',\n      value: 10,\n      valueType: 'value',\n    })\n\n    assert.deepEqual(lte('score', 10), {\n      type: 'comparison',\n      operator: 'lte',\n      column: 'score',\n      value: 10,\n      valueType: 'value',\n    })\n\n    assert.deepEqual(like('email', '%@example.com'), {\n      type: 'comparison',\n      operator: 'like',\n      column: 'email',\n      value: '%@example.com',\n      valueType: 'value',\n    })\n\n    assert.deepEqual(ilike('email', '%@example.com'), {\n      type: 'comparison',\n      operator: 'ilike',\n      column: 'email',\n      value: '%@example.com',\n      valueType: 'value',\n    })\n  })\n})\n\ndescribe('collection and null predicates', () => {\n  it('clones inList and notInList arrays', () => {\n    let values = [1, 2, 3]\n    let inPredicate = inList('id', values)\n    let notInPredicate = notInList('id', values)\n    values.push(4)\n\n    assert.deepEqual(inPredicate, {\n      type: 'comparison',\n      operator: 'in',\n      column: 'id',\n      value: [1, 2, 3],\n      valueType: 'value',\n    })\n    assert.deepEqual(notInPredicate, {\n      type: 'comparison',\n      operator: 'notIn',\n      column: 'id',\n      value: [1, 2, 3],\n      valueType: 'value',\n    })\n  })\n\n  it('builds between and null predicates', () => {\n    assert.deepEqual(between('created_at', 1, 10), {\n      type: 'between',\n      column: 'created_at',\n      lower: 1,\n      upper: 10,\n    })\n    assert.deepEqual(isNull('deleted_at'), {\n      type: 'null',\n      operator: 'isNull',\n      column: 'deleted_at',\n    })\n    assert.deepEqual(notNull('deleted_at'), {\n      type: 'null',\n      operator: 'notNull',\n      column: 'deleted_at',\n    })\n  })\n})\n\ndescribe('logical predicates', () => {\n  it('normalizes object filters into and(eq()) predicates', () => {\n    let predicate = normalizeWhereInput({\n      id: 10,\n      status: 'active',\n    })\n\n    assert.equal(predicate.type, 'logical')\n    assert.equal(predicate.operator, 'and')\n    assert.equal(predicate.predicates.length, 2)\n    assert.deepEqual(predicate.predicates[0], eq('id', 10))\n    assert.deepEqual(predicate.predicates[1], eq('status', 'active'))\n  })\n\n  it('returns predicate inputs unchanged', () => {\n    let input = eq('status', 'active')\n    let normalized = normalizeWhereInput(input)\n\n    assert.equal(normalized, input)\n  })\n\n  it('filters falsy values when combining logical predicates', () => {\n    let predicate = and(eq('id', 10), null as never, undefined as never)\n\n    assert.deepEqual(predicate, {\n      type: 'logical',\n      operator: 'and',\n      predicates: [eq('id', 10)],\n    })\n\n    let disjunction = or(eq('status', 'active'), false as never)\n\n    assert.deepEqual(disjunction, {\n      type: 'logical',\n      operator: 'or',\n      predicates: [eq('status', 'active')],\n    })\n  })\n\n  it('collects columns across nested predicates', () => {\n    let predicate = and(\n      eq('accounts.id', 'projects.account_id'),\n      or(\n        between('accounts.id', 1, 5),\n        and(isNull('projects.deleted_at'), notNull('accounts.email')),\n      ),\n    )\n\n    assert.deepEqual(getPredicateColumns(predicate), [\n      'accounts.id',\n      'projects.account_id',\n      'accounts.id',\n      'projects.deleted_at',\n      'accounts.email',\n    ])\n  })\n\n  it('collects columns for scalar comparison predicates', () => {\n    assert.deepEqual(getPredicateColumns(eq('accounts.id', 1)), ['accounts.id'])\n  })\n\n  it('identifies predicate-like inputs', () => {\n    assert.equal(isPredicate(eq('id', 1)), true)\n    assert.equal(isPredicate({ type: 'logical', operator: 'and', predicates: [] }), true)\n    assert.equal(isPredicate({}), false)\n    assert.equal(isPredicate(null), false)\n    assert.equal(isPredicate({ type: 'unknown' }), false)\n  })\n})\n"
  },
  {
    "path": "packages/data-table/src/lib/operators.ts",
    "content": "import { isColumnReference, normalizeColumnInput } from './references.ts'\nimport type { ColumnInput, ColumnReferenceLike, NormalizeColumnInput } from './references.ts'\n\n/**\n * Comparison operators supported by `comparison` predicates.\n */\nexport type ComparisonOperator =\n  | 'eq'\n  | 'ne'\n  | 'gt'\n  | 'gte'\n  | 'lt'\n  | 'lte'\n  | 'in'\n  | 'notIn'\n  | 'like'\n  | 'ilike'\n\ntype QualifiedColumnReference = `${string}.${string}`\n\ntype PredicateColumn<input extends string | ColumnReferenceLike> = NormalizeColumnInput<input> &\n  string\n\n/**\n * Normalized predicate representation consumed by adapters.\n */\nexport type Predicate<column extends string = string> =\n  | {\n      type: 'comparison'\n      operator: ComparisonOperator\n      column: column\n      value: unknown\n      valueType: 'value'\n    }\n  | {\n      type: 'comparison'\n      operator: Exclude<ComparisonOperator, 'in' | 'notIn'>\n      column: column\n      value: column\n      valueType: 'column'\n    }\n  | {\n      type: 'between'\n      column: column\n      lower: unknown\n      upper: unknown\n    }\n  | {\n      type: 'null'\n      operator: 'isNull' | 'notNull'\n      column: column\n    }\n  | {\n      type: 'logical'\n      operator: 'and' | 'or'\n      predicates: Predicate<column>[]\n    }\n\n/**\n * Object shorthand accepted in `where` clauses.\n */\nexport type WhereObject<column extends string = string> = Partial<Record<column, unknown>>\n\n/**\n * User-facing where input accepted by `query.where()` and relation modifiers.\n */\nexport type WhereInput<column extends string = string> = Predicate<column> | WhereObject<column>\n\n/**\n * Builds an equality predicate.\n */\nexport function eq<\n  left extends ColumnInput<QualifiedColumnReference>,\n  right extends ColumnInput<QualifiedColumnReference>,\n>(\n  column: left,\n  value: right & (right extends `${string}@${string}` ? never : right),\n): Predicate<PredicateColumn<left> | PredicateColumn<right>>\nexport function eq<column extends string | ColumnReferenceLike>(\n  column: column,\n  value: unknown,\n): Predicate<PredicateColumn<column>>\nexport function eq(column: string | ColumnReferenceLike, value: unknown): Predicate<string> {\n  return createComparisonPredicate('eq', column, value)\n}\n\n/**\n * Builds an inequality predicate.\n */\nexport function ne<\n  left extends ColumnInput<QualifiedColumnReference>,\n  right extends ColumnInput<QualifiedColumnReference>,\n>(\n  column: left,\n  value: right & (right extends `${string}@${string}` ? never : right),\n): Predicate<PredicateColumn<left> | PredicateColumn<right>>\nexport function ne<column extends string | ColumnReferenceLike>(\n  column: column,\n  value: unknown,\n): Predicate<PredicateColumn<column>>\nexport function ne(column: string | ColumnReferenceLike, value: unknown): Predicate<string> {\n  return createComparisonPredicate('ne', column, value)\n}\n\n/**\n * Builds a greater-than predicate.\n */\nexport function gt<\n  left extends ColumnInput<QualifiedColumnReference>,\n  right extends ColumnInput<QualifiedColumnReference>,\n>(\n  column: left,\n  value: right & (right extends `${string}@${string}` ? never : right),\n): Predicate<PredicateColumn<left> | PredicateColumn<right>>\nexport function gt<column extends string | ColumnReferenceLike>(\n  column: column,\n  value: unknown,\n): Predicate<PredicateColumn<column>>\nexport function gt(column: string | ColumnReferenceLike, value: unknown): Predicate<string> {\n  return createComparisonPredicate('gt', column, value)\n}\n\n/**\n * Builds a greater-than-or-equal predicate.\n */\nexport function gte<\n  left extends ColumnInput<QualifiedColumnReference>,\n  right extends ColumnInput<QualifiedColumnReference>,\n>(\n  column: left,\n  value: right & (right extends `${string}@${string}` ? never : right),\n): Predicate<PredicateColumn<left> | PredicateColumn<right>>\nexport function gte<column extends string | ColumnReferenceLike>(\n  column: column,\n  value: unknown,\n): Predicate<PredicateColumn<column>>\nexport function gte(column: string | ColumnReferenceLike, value: unknown): Predicate<string> {\n  return createComparisonPredicate('gte', column, value)\n}\n\n/**\n * Builds a less-than predicate.\n */\nexport function lt<\n  left extends ColumnInput<QualifiedColumnReference>,\n  right extends ColumnInput<QualifiedColumnReference>,\n>(\n  column: left,\n  value: right & (right extends `${string}@${string}` ? never : right),\n): Predicate<PredicateColumn<left> | PredicateColumn<right>>\nexport function lt<column extends string | ColumnReferenceLike>(\n  column: column,\n  value: unknown,\n): Predicate<PredicateColumn<column>>\nexport function lt(column: string | ColumnReferenceLike, value: unknown): Predicate<string> {\n  return createComparisonPredicate('lt', column, value)\n}\n\n/**\n * Builds a less-than-or-equal predicate.\n */\nexport function lte<\n  left extends ColumnInput<QualifiedColumnReference>,\n  right extends ColumnInput<QualifiedColumnReference>,\n>(\n  column: left,\n  value: right & (right extends `${string}@${string}` ? never : right),\n): Predicate<PredicateColumn<left> | PredicateColumn<right>>\nexport function lte<column extends string | ColumnReferenceLike>(\n  column: column,\n  value: unknown,\n): Predicate<PredicateColumn<column>>\nexport function lte(column: string | ColumnReferenceLike, value: unknown): Predicate<string> {\n  return createComparisonPredicate('lte', column, value)\n}\n\n/**\n * Builds an `IN` predicate.\n * @param column Column to compare.\n * @param values Candidate values.\n * @returns An `in` comparison predicate.\n */\nexport function inList<column extends string | ColumnReferenceLike>(\n  column: column,\n  values: readonly unknown[],\n): Predicate<PredicateColumn<column>> {\n  return {\n    type: 'comparison',\n    operator: 'in',\n    column: resolvePredicateColumn(column),\n    value: [...values],\n    valueType: 'value',\n  }\n}\n\n/**\n * Builds a `NOT IN` predicate.\n * @param column Column to compare.\n * @param values Candidate values.\n * @returns A `notIn` comparison predicate.\n */\nexport function notInList<column extends string | ColumnReferenceLike>(\n  column: column,\n  values: readonly unknown[],\n): Predicate<PredicateColumn<column>> {\n  return {\n    type: 'comparison',\n    operator: 'notIn',\n    column: resolvePredicateColumn(column),\n    value: [...values],\n    valueType: 'value',\n  }\n}\n\n/**\n * Builds a case-sensitive SQL `LIKE` predicate.\n * @param column Column to compare.\n * @param value Match pattern.\n * @returns A `like` comparison predicate.\n */\nexport function like<column extends string | ColumnReferenceLike>(\n  column: column,\n  value: string,\n): Predicate<PredicateColumn<column>> {\n  return {\n    type: 'comparison',\n    operator: 'like',\n    column: resolvePredicateColumn(column),\n    value,\n    valueType: 'value',\n  }\n}\n\n/**\n * Builds a case-insensitive SQL `LIKE` predicate.\n * @param column Column to compare.\n * @param value Match pattern.\n * @returns An `ilike` comparison predicate.\n */\nexport function ilike<column extends string | ColumnReferenceLike>(\n  column: column,\n  value: string,\n): Predicate<PredicateColumn<column>> {\n  return {\n    type: 'comparison',\n    operator: 'ilike',\n    column: resolvePredicateColumn(column),\n    value,\n    valueType: 'value',\n  }\n}\n\n/**\n * Builds a `BETWEEN` predicate.\n * @param column Column to compare.\n * @param lower Lower bound value.\n * @param upper Upper bound value.\n * @returns A `between` predicate.\n */\nexport function between<column extends string | ColumnReferenceLike>(\n  column: column,\n  lower: unknown,\n  upper: unknown,\n): Predicate<PredicateColumn<column>> {\n  return {\n    type: 'between',\n    column: resolvePredicateColumn(column),\n    lower,\n    upper,\n  }\n}\n\n/**\n * Builds an `IS NULL` predicate.\n * @param column Column to compare.\n * @returns An `isNull` predicate.\n */\nexport function isNull<column extends string | ColumnReferenceLike>(\n  column: column,\n): Predicate<PredicateColumn<column>> {\n  return { type: 'null', operator: 'isNull', column: resolvePredicateColumn(column) }\n}\n\n/**\n * Builds an `IS NOT NULL` predicate.\n * @param column Column to compare.\n * @returns A `notNull` predicate.\n */\nexport function notNull<column extends string | ColumnReferenceLike>(\n  column: column,\n): Predicate<PredicateColumn<column>> {\n  return { type: 'null', operator: 'notNull', column: resolvePredicateColumn(column) }\n}\n\n/**\n * Combines predicates with logical `AND`.\n * @param predicates Child predicates.\n * @returns A logical `and` predicate.\n */\nexport function and<column extends string>(...predicates: Predicate<column>[]): Predicate<column> {\n  let filtered = predicates.filter(Boolean)\n  return { type: 'logical', operator: 'and', predicates: filtered }\n}\n\n/**\n * Combines predicates with logical `OR`.\n * @param predicates Child predicates.\n * @returns A logical `or` predicate.\n */\nexport function or<column extends string>(...predicates: Predicate<column>[]): Predicate<column> {\n  let filtered = predicates.filter(Boolean)\n  return { type: 'logical', operator: 'or', predicates: filtered }\n}\n\n/**\n * Returns `true` when a value is a normalized predicate object.\n * @param value Value to inspect.\n * @returns Whether the value is a predicate.\n */\nexport function isPredicate<column extends string = string>(\n  value: unknown,\n): value is Predicate<column> {\n  if (typeof value !== 'object' || value === null) {\n    return false\n  }\n\n  if (!('type' in value)) {\n    return false\n  }\n\n  let input = value as { type?: unknown }\n  return (\n    input.type === 'comparison' ||\n    input.type === 'between' ||\n    input.type === 'null' ||\n    input.type === 'logical'\n  )\n}\n\n/**\n * Normalizes object shorthand into a predicate tree.\n * @param input Predicate object or shorthand where map.\n * @returns A normalized predicate.\n */\nexport function normalizeWhereInput<column extends string>(\n  input: WhereInput<column>,\n): Predicate<column> {\n  if (isPredicate(input)) {\n    return input\n  }\n\n  let keys = Object.keys(input) as column[]\n  let predicates = keys.map((column) => eq(column, input[column]) as Predicate<column>)\n\n  return and(...predicates)\n}\n\n/**\n * Collects referenced columns from a predicate tree.\n * @param predicate Predicate to inspect.\n * @returns Referenced column names.\n */\nexport function getPredicateColumns(predicate: Predicate): string[] {\n  if (predicate.type === 'comparison') {\n    if (predicate.valueType === 'column') {\n      return [predicate.column, predicate.value]\n    }\n\n    return [predicate.column]\n  }\n\n  if (predicate.type === 'between') {\n    return [predicate.column]\n  }\n\n  if (predicate.type === 'null') {\n    return [predicate.column]\n  }\n\n  let columns: string[] = []\n\n  for (let child of predicate.predicates) {\n    columns.push(...getPredicateColumns(child))\n  }\n\n  return columns\n}\n\nfunction isQualifiedColumnReference(value: unknown): value is QualifiedColumnReference {\n  if (typeof value !== 'string') {\n    return false\n  }\n\n  return /^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)+$/.test(value)\n}\n\nfunction resolvePredicateColumn<input extends string | ColumnReferenceLike>(\n  column: input,\n): PredicateColumn<input> {\n  return normalizeColumnInput(column) as PredicateColumn<input>\n}\n\nfunction resolveComparisonValue(value: unknown): unknown {\n  if (isColumnReference(value)) {\n    return normalizeColumnInput(value)\n  }\n\n  return value\n}\n\nfunction createComparisonPredicate(\n  operator: Exclude<ComparisonOperator, 'in' | 'notIn'>,\n  column: string | ColumnReferenceLike,\n  value: unknown,\n): Predicate<string> {\n  let normalizedColumn = resolvePredicateColumn(column)\n  let normalizedValue = resolveComparisonValue(value)\n\n  if (isQualifiedColumnReference(normalizedColumn) && isQualifiedColumnReference(normalizedValue)) {\n    return {\n      type: 'comparison',\n      operator,\n      column: normalizedColumn,\n      value: normalizedValue,\n      valueType: 'column',\n    }\n  }\n\n  return {\n    type: 'comparison',\n    operator,\n    column: normalizedColumn,\n    value: normalizedValue,\n    valueType: 'value',\n  }\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/query.ts",
    "content": "import type { JoinClause, JoinType, SelectColumn } from './adapter.ts'\nimport { DataTableQueryError, DataTableValidationError } from './errors.ts'\nimport type {\n  MergeColumnTypeMaps,\n  PrimaryKeyInputForRow,\n  QueryColumnInput,\n  QueryColumnName,\n  QueryColumnTypeMap,\n  QueryColumnTypeMapFromRow,\n  QueryColumns,\n  QueryTableInput,\n  RelationMapForSourceName,\n  ReturningInput,\n  SelectedAliasRow,\n  WriteResult,\n  WriteRowResult,\n  WriteRowsResult,\n} from './database.ts'\nimport type { Predicate, WhereInput } from './operators.ts'\nimport { normalizeWhereInput } from './operators.ts'\nimport { normalizeColumnInput } from './references.ts'\nimport type { AnyRelation, AnyTable, LoadedRelationMap, OrderByClause } from './table.ts'\nimport { getTableColumns, getTableName } from './table.ts'\n\ntype QueryBindingState = 'bound' | 'unbound'\n\ntype InsertQueryOptions<row extends Record<string, unknown>> = {\n  returning?: ReturningInput<row>\n  touch?: boolean\n}\n\ntype DeleteQueryOptions<row extends Record<string, unknown>> = {\n  returning?: ReturningInput<row>\n}\n\ntype UpsertQueryOptions<row extends Record<string, unknown>> = {\n  returning?: ReturningInput<row>\n  touch?: boolean\n  conflictTarget?: (keyof row & string)[]\n  update?: Partial<row>\n}\n\nexport type QueryState = {\n  select: '*' | SelectColumn[]\n  distinct: boolean\n  joins: JoinClause[]\n  where: Predicate<string>[]\n  groupBy: string[]\n  having: Predicate<string>[]\n  orderBy: OrderByClause[]\n  limit?: number\n  offset?: number\n  with: Record<string, AnyRelation>\n}\n\ntype QueryPlanMap<row extends Record<string, unknown>, primaryKey extends readonly string[]> = {\n  all: { kind: 'all' }\n  first: { kind: 'first' }\n  find: {\n    kind: 'find'\n    value: PrimaryKeyInputForRow<row, primaryKey>\n  }\n  count: { kind: 'count' }\n  exists: { kind: 'exists' }\n  insert: {\n    kind: 'insert'\n    values: Partial<row>\n    options?: InsertQueryOptions<row>\n  }\n  insertMany: {\n    kind: 'insertMany'\n    values: Partial<row>[]\n    options?: InsertQueryOptions<row>\n  }\n  update: {\n    kind: 'update'\n    changes: Partial<row>\n    options?: InsertQueryOptions<row>\n  }\n  delete: {\n    kind: 'delete'\n    options?: DeleteQueryOptions<row>\n  }\n  upsert: {\n    kind: 'upsert'\n    values: Partial<row>\n    options?: UpsertQueryOptions<row>\n  }\n}\n\ntype QueryExecutionMode = keyof QueryPlanMap<Record<string, unknown>, readonly string[]>\n\ntype QueryPhase<\n  binding extends QueryBindingState = QueryBindingState,\n  mode extends QueryExecutionMode = QueryExecutionMode,\n> = {\n  binding: binding\n  mode: mode\n}\n\nexport type BoundQueryPhase<mode extends QueryExecutionMode = QueryExecutionMode> = QueryPhase<\n  'bound',\n  mode\n>\n\nexport type UnboundQueryPhase<mode extends QueryExecutionMode = QueryExecutionMode> = QueryPhase<\n  'unbound',\n  mode\n>\n\ntype AnyQuerySource = QueryTableInput<string, Record<string, unknown>, readonly string[]>\n\ntype QuerySourceTableName<source extends AnyQuerySource> =\n  source extends QueryTableInput<infer tableName, any, any> ? tableName : never\n\ntype QuerySourceRow<source extends AnyQuerySource> =\n  source extends QueryTableInput<any, infer row, any> ? row : never\n\ntype QuerySourcePrimaryKey<source extends AnyQuerySource> =\n  source extends QueryTableInput<any, any, infer primaryKey> ? primaryKey : never\n\ntype QuerySourceColumnTypes<source extends AnyQuerySource> = QueryColumnTypeMapFromRow<\n  QuerySourceTableName<source>,\n  QuerySourceRow<source>\n>\n\ntype QueryPlan<\n  row extends Record<string, unknown>,\n  primaryKey extends readonly string[],\n  mode extends QueryExecutionMode = QueryExecutionMode,\n> = QueryPlanMap<row, primaryKey>[mode]\n\ntype QueryResultMap<row extends Record<string, unknown>, loaded extends Record<string, unknown>> = {\n  all: Array<row & loaded>\n  first: (row & loaded) | null\n  find: (row & loaded) | null\n  count: number\n  exists: boolean\n  insert: WriteResult | WriteRowResult<row>\n  insertMany: WriteResult | WriteRowsResult<row>\n  update: WriteResult | WriteRowsResult<row>\n  delete: WriteResult | WriteRowsResult<row>\n  upsert: WriteResult | WriteRowResult<row>\n}\n\nexport type AnyQuery = Query<any, any, any, any, any>\n\ntype QuerySource<input extends AnyQuery> =\n  input extends Query<infer source, any, any, any, any> ? source : never\n\ntype QueryColumnTypes<input extends AnyQuery> =\n  input extends Query<any, infer columnTypes, any, any, any> ? columnTypes : never\n\ntype QueryRow<input extends AnyQuery> =\n  input extends Query<any, any, infer row, any, any> ? row : never\n\ntype QueryLoaded<input extends AnyQuery> =\n  input extends Query<any, any, any, infer loaded, any> ? loaded : never\n\ntype QueryPhaseOf<input extends AnyQuery> =\n  input extends Query<any, any, any, any, infer phase> ? phase : never\n\ntype QueryBinding<input extends AnyQuery> = QueryPhaseOf<input>['binding']\n\ntype QueryMode<input extends AnyQuery> = QueryPhaseOf<input>['mode']\n\ntype QueryPhaseBinding<phase extends QueryPhase> = phase['binding']\n\ntype QueryPhaseMode<phase extends QueryPhase> = phase['mode']\n\ntype QueryAllPhase<phase extends QueryPhase> = QueryPhase<QueryPhaseBinding<phase>, 'all'>\n\ntype QueryNextPhase<phase extends QueryPhase, mode extends QueryExecutionMode> = QueryPhase<\n  QueryPhaseBinding<phase>,\n  mode\n>\n\ntype QueryWith<input extends AnyQuery, phase extends QueryPhase> = Query<\n  QuerySource<input>,\n  QueryColumnTypes<input>,\n  QueryRow<input>,\n  QueryLoaded<input>,\n  phase\n>\n\ntype QueryTerminalResult<input extends AnyQuery, mode extends QueryExecutionMode, result> =\n  QueryBinding<input> extends 'bound' ? Promise<result> : QueryWith<input, UnboundQueryPhase<mode>>\n\nexport type QueryExecutionResult<input> = input extends AnyQuery\n  ? QueryResultMap<QueryRow<input>, QueryLoaded<input>>[Extract<\n      QueryMode<input>,\n      QueryExecutionMode\n    >]\n  : never\n\ntype QueryRuntime = {\n  exec<input extends AnyQuery>(input: input): Promise<QueryExecutionResult<input>>\n}\n\ntype QuerySnapshot<\n  source extends AnyQuerySource = AnyQuerySource,\n  row extends Record<string, unknown> = Record<string, unknown>,\n  mode extends QueryExecutionMode = QueryExecutionMode,\n> = {\n  table: source\n  state: QueryState\n  plan: QueryPlan<row, QuerySourcePrimaryKey<source>, mode>\n}\n\nexport const bindQueryRuntime = Symbol('bindQueryRuntime')\nexport const querySnapshot = Symbol('querySnapshot')\n\ndeclare const queryTypeBrand: unique symbol\n\nexport class Query<\n  source extends AnyQuerySource,\n  columnTypes extends Record<string, unknown> = QuerySourceColumnTypes<source>,\n  row extends Record<string, unknown> = QuerySourceRow<source>,\n  loaded extends Record<string, unknown> = {},\n  phase extends QueryPhase = UnboundQueryPhase<'all'>,\n> {\n  declare readonly [queryTypeBrand]: {\n    binding: QueryPhaseBinding<phase>\n    mode: QueryPhaseMode<phase>\n  }\n\n  #table: source\n  #state: QueryState\n  #plan: QueryPlan<row, QuerySourcePrimaryKey<source>, QueryPhaseMode<phase>>\n  #runtime?: QueryRuntime\n\n  constructor(table: source) {\n    this.#table = table\n    this.#state = createInitialQueryState()\n    this.#plan = { kind: 'all' } as QueryPlan<\n      row,\n      QuerySourcePrimaryKey<source>,\n      QueryPhaseMode<phase>\n    >\n  }\n\n  static #createInternal<\n    source extends AnyQuerySource,\n    columnTypes extends Record<string, unknown>,\n    row extends Record<string, unknown>,\n    loaded extends Record<string, unknown>,\n    phase extends QueryPhase,\n  >(\n    table: source,\n    state: QueryState,\n    plan: QueryPlan<row, QuerySourcePrimaryKey<source>, QueryPhaseMode<phase>>,\n    runtime?: QueryRuntime,\n  ): Query<source, columnTypes, row, loaded, phase> {\n    let output = new Query(table) as Query<source, columnTypes, row, loaded, phase>\n\n    output.#state = cloneQueryState(state)\n    output.#plan = cloneQueryPlan(\n      plan as QueryPlan<row, QuerySourcePrimaryKey<source>>,\n    ) as QueryPlan<row, QuerySourcePrimaryKey<source>, QueryPhaseMode<phase>>\n    output.#runtime = runtime\n\n    return output\n  }\n\n  select<selection extends (keyof row & string)[]>(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    ...columns: selection\n  ): Query<source, columnTypes, Pick<row, selection[number]>, loaded, QueryAllPhase<phase>>\n  select<selection extends Record<string, QueryColumnInput<columnTypes>>>(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    selection: selection,\n  ): Query<\n    source,\n    columnTypes,\n    SelectedAliasRow<columnTypes, selection>,\n    loaded,\n    QueryAllPhase<phase>\n  >\n  select(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    ...input: [Record<string, QueryColumnInput<columnTypes>>] | (keyof row & string)[]\n  ): Query<source, columnTypes, any, loaded, QueryAllPhase<phase>> {\n    if (\n      input.length === 1 &&\n      typeof input[0] === 'object' &&\n      input[0] !== null &&\n      !Array.isArray(input[0])\n    ) {\n      let selection = input[0] as Record<string, QueryColumnInput<columnTypes>>\n      let aliases = Object.keys(selection)\n      let select = aliases.map((alias) => ({\n        column: normalizeColumnInput(selection[alias]),\n        alias,\n      }))\n\n      return this.#clone({ select }) as Query<\n        source,\n        columnTypes,\n        any,\n        loaded,\n        QueryAllPhase<phase>\n      >\n    }\n\n    let columns = input as (keyof row & string)[]\n\n    return this.#clone({\n      select: columns.map((column) => ({ column, alias: column })),\n    }) as Query<source, columnTypes, any, loaded, QueryAllPhase<phase>>\n  }\n\n  distinct(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    value = true,\n  ): Query<source, columnTypes, row, loaded, QueryAllPhase<phase>> {\n    return this.#clone({ distinct: value })\n  }\n\n  where(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    input: WhereInput<QueryColumns<columnTypes>>,\n  ): Query<source, columnTypes, row, loaded, QueryAllPhase<phase>> {\n    let predicate = normalizeWhereInput(input)\n    let normalizedPredicate = normalizePredicateValues(\n      predicate,\n      createPredicateColumnResolver([this.#table, ...this.#state.joins.map((join) => join.table)]),\n    )\n\n    return this.#clone({\n      where: [...this.#state.where, normalizedPredicate],\n    })\n  }\n\n  having(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    input: WhereInput<QueryColumns<columnTypes>>,\n  ): Query<source, columnTypes, row, loaded, QueryAllPhase<phase>> {\n    let predicate = normalizeWhereInput(input)\n    let normalizedPredicate = normalizePredicateValues(\n      predicate,\n      createPredicateColumnResolver([this.#table, ...this.#state.joins.map((join) => join.table)]),\n    )\n\n    return this.#clone({\n      having: [...this.#state.having, normalizedPredicate],\n    })\n  }\n\n  join<target extends AnyTable>(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    target: target,\n    on: Predicate<QueryColumns<columnTypes> | QueryColumnName<target>>,\n    type: JoinType = 'inner',\n  ): Query<\n    source,\n    MergeColumnTypeMaps<columnTypes, QueryColumnTypeMap<target>>,\n    row,\n    loaded,\n    QueryAllPhase<phase>\n  > {\n    let normalizedOn = normalizePredicateValues(\n      on,\n      createPredicateColumnResolver([\n        this.#table,\n        ...this.#state.joins.map((join) => join.table),\n        target,\n      ]),\n    ) as Predicate<QueryColumns<columnTypes> | QueryColumnName<target>>\n\n    return this.#clone({\n      joins: [...this.#state.joins, { type, table: target, on: normalizedOn }],\n    }) as Query<\n      source,\n      MergeColumnTypeMaps<columnTypes, QueryColumnTypeMap<target>>,\n      row,\n      loaded,\n      QueryAllPhase<phase>\n    >\n  }\n\n  leftJoin<target extends AnyTable>(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    target: target,\n    on: Predicate<QueryColumns<columnTypes> | QueryColumnName<target>>,\n  ): Query<\n    source,\n    MergeColumnTypeMaps<columnTypes, QueryColumnTypeMap<target>>,\n    row,\n    loaded,\n    QueryAllPhase<phase>\n  > {\n    return this.join(target, on, 'left')\n  }\n\n  rightJoin<target extends AnyTable>(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    target: target,\n    on: Predicate<QueryColumns<columnTypes> | QueryColumnName<target>>,\n  ): Query<\n    source,\n    MergeColumnTypeMaps<columnTypes, QueryColumnTypeMap<target>>,\n    row,\n    loaded,\n    QueryAllPhase<phase>\n  > {\n    return this.join(target, on, 'right')\n  }\n\n  orderBy(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    column: QueryColumnInput<columnTypes>,\n    direction: 'asc' | 'desc' = 'asc',\n  ): Query<source, columnTypes, row, loaded, QueryAllPhase<phase>> {\n    return this.#clone({\n      orderBy: [...this.#state.orderBy, { column: normalizeColumnInput(column), direction }],\n    })\n  }\n\n  groupBy(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    ...columns: QueryColumnInput<columnTypes>[]\n  ): Query<source, columnTypes, row, loaded, QueryAllPhase<phase>> {\n    return this.#clone({\n      groupBy: [...this.#state.groupBy, ...columns.map((column) => normalizeColumnInput(column))],\n    })\n  }\n\n  limit(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    value: number,\n  ): Query<source, columnTypes, row, loaded, QueryAllPhase<phase>> {\n    return this.#clone({ limit: value })\n  }\n\n  offset(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    value: number,\n  ): Query<source, columnTypes, row, loaded, QueryAllPhase<phase>> {\n    return this.#clone({ offset: value })\n  }\n\n  with<relations extends RelationMapForSourceName<QuerySourceTableName<source>>>(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    relations: relations,\n  ): Query<source, columnTypes, row, loaded & LoadedRelationMap<relations>, QueryAllPhase<phase>> {\n    return this.#clone({\n      with: {\n        ...this.#state.with,\n        ...relations,\n      },\n    }) as Query<\n      source,\n      columnTypes,\n      row,\n      loaded & LoadedRelationMap<relations>,\n      QueryAllPhase<phase>\n    >\n  }\n\n  all(\n    this: Query<source, columnTypes, row, loaded, BoundQueryPhase<'all'>>,\n  ): Promise<Array<row & loaded>> {\n    return this.#boundRuntime().exec(this) as Promise<Array<row & loaded>>\n  }\n\n  first(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    'first',\n    (row & loaded) | null\n  > {\n    return this.#resolveTerminal({ kind: 'first' })\n  }\n\n  find(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    value: PrimaryKeyInputForRow<row, QuerySourcePrimaryKey<source>>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    'find',\n    (row & loaded) | null\n  > {\n    return this.#resolveTerminal({ kind: 'find', value })\n  }\n\n  count(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    'count',\n    number\n  > {\n    return this.#resolveTerminal({ kind: 'count' })\n  }\n\n  exists(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    'exists',\n    boolean\n  > {\n    return this.#resolveTerminal({ kind: 'exists' })\n  }\n\n  insert(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    values: Partial<row>,\n    options?: InsertQueryOptions<row>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    'insert',\n    WriteResult | WriteRowResult<row>\n  > {\n    assertWriteState(this.#state, 'insert', {\n      where: false,\n      orderBy: false,\n      limit: false,\n      offset: false,\n    })\n\n    return this.#resolveTerminal({ kind: 'insert', values, options })\n  }\n\n  insertMany(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    values: Partial<row>[],\n    options?: InsertQueryOptions<row>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    'insertMany',\n    WriteResult | WriteRowsResult<row>\n  > {\n    assertWriteState(this.#state, 'insertMany', {\n      where: false,\n      orderBy: false,\n      limit: false,\n      offset: false,\n    })\n\n    return this.#resolveTerminal({ kind: 'insertMany', values, options })\n  }\n\n  update(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    changes: Partial<row>,\n    options?: InsertQueryOptions<row>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    'update',\n    WriteResult | WriteRowsResult<row>\n  > {\n    assertWriteState(this.#state, 'update', {\n      where: true,\n      orderBy: true,\n      limit: true,\n      offset: true,\n    })\n\n    return this.#resolveTerminal({ kind: 'update', changes, options })\n  }\n\n  delete(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    options?: DeleteQueryOptions<row>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    'delete',\n    WriteResult | WriteRowsResult<row>\n  > {\n    assertWriteState(this.#state, 'delete', {\n      where: true,\n      orderBy: true,\n      limit: true,\n      offset: true,\n    })\n\n    return this.#resolveTerminal({ kind: 'delete', options })\n  }\n\n  upsert(\n    this: Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    values: Partial<row>,\n    options?: UpsertQueryOptions<row>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    'upsert',\n    WriteResult | WriteRowResult<row>\n  > {\n    assertWriteState(this.#state, 'upsert', {\n      where: false,\n      orderBy: false,\n      limit: false,\n      offset: false,\n    })\n\n    return this.#resolveTerminal({ kind: 'upsert', values, options })\n  }\n\n  [querySnapshot](): QuerySnapshot<source, row, QueryPhaseMode<phase>> {\n    return this.#snapshot()\n  }\n\n  #resolveTerminal<nextMode extends QueryExecutionMode, result>(\n    plan: QueryPlan<row, QuerySourcePrimaryKey<source>, nextMode>,\n  ): QueryTerminalResult<\n    Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n    nextMode,\n    result\n  > {\n    let next = this.#withPlan(plan)\n    return (this.#runtime ? this.#runtime.exec(next) : next) as QueryTerminalResult<\n      Query<source, columnTypes, row, loaded, QueryAllPhase<phase>>,\n      nextMode,\n      result\n    >\n  }\n\n  [bindQueryRuntime](\n    runtime: QueryRuntime,\n  ): Query<source, columnTypes, row, loaded, BoundQueryPhase<QueryPhaseMode<phase>>> {\n    return Query.#createInternal<\n      source,\n      columnTypes,\n      row,\n      loaded,\n      BoundQueryPhase<QueryPhaseMode<phase>>\n    >(\n      this.#table,\n      this.#state,\n      this.#plan as QueryPlan<row, QuerySourcePrimaryKey<source>, QueryPhaseMode<phase>>,\n      runtime,\n    )\n  }\n\n  #clone(patch: Partial<QueryState>): Query<source, columnTypes, row, loaded, phase> {\n    return Query.#createInternal<source, columnTypes, row, loaded, phase>(\n      this.#table,\n      {\n        select: patch.select ?? cloneSelection(this.#state.select),\n        distinct: patch.distinct ?? this.#state.distinct,\n        joins: patch.joins ? [...patch.joins] : [...this.#state.joins],\n        where: patch.where ? [...patch.where] : [...this.#state.where],\n        groupBy: patch.groupBy ? [...patch.groupBy] : [...this.#state.groupBy],\n        having: patch.having ? [...patch.having] : [...this.#state.having],\n        orderBy: patch.orderBy ? [...patch.orderBy] : [...this.#state.orderBy],\n        limit: patch.limit === undefined ? this.#state.limit : patch.limit,\n        offset: patch.offset === undefined ? this.#state.offset : patch.offset,\n        with: patch.with ? { ...patch.with } : { ...this.#state.with },\n      },\n      this.#plan as QueryPlan<row, QuerySourcePrimaryKey<source>, QueryPhaseMode<phase>>,\n      this.#runtime,\n    )\n  }\n\n  #withPlan<nextMode extends QueryExecutionMode>(\n    plan: QueryPlan<row, QuerySourcePrimaryKey<source>, nextMode>,\n  ): Query<source, columnTypes, row, loaded, QueryNextPhase<phase, nextMode>> {\n    return Query.#createInternal<source, columnTypes, row, loaded, QueryNextPhase<phase, nextMode>>(\n      this.#table,\n      this.#state,\n      plan,\n      this.#runtime,\n    )\n  }\n\n  #snapshot(): QuerySnapshot<source, row, QueryPhaseMode<phase>> {\n    return {\n      table: this.#table,\n      state: cloneQueryState(this.#state),\n      plan: cloneQueryPlan(\n        this.#plan as QueryPlan<row, QuerySourcePrimaryKey<source>>,\n      ) as QueryPlan<row, QuerySourcePrimaryKey<source>, QueryPhaseMode<phase>>,\n    }\n  }\n\n  #boundRuntime(): QueryRuntime {\n    if (!this.#runtime) {\n      throw new DataTableQueryError('Use db.exec(query) to execute an unbound Query')\n    }\n\n    return this.#runtime\n  }\n}\n\nexport function query<\n  tableName extends string,\n  row extends Record<string, unknown>,\n  primaryKey extends readonly (keyof row & string)[],\n>(\n  table: QueryTableInput<tableName, row, primaryKey>,\n): Query<\n  QueryTableInput<tableName, row, primaryKey>,\n  QueryColumnTypeMapFromRow<tableName, row>,\n  row,\n  {},\n  UnboundQueryPhase<'all'>\n> {\n  return new Query(table) as Query<\n    QueryTableInput<tableName, row, primaryKey>,\n    QueryColumnTypeMapFromRow<tableName, row>,\n    row,\n    {},\n    UnboundQueryPhase<'all'>\n  >\n}\n\nexport function cloneQueryState(state: QueryState): QueryState {\n  return {\n    select: cloneSelection(state.select),\n    distinct: state.distinct,\n    joins: [...state.joins],\n    where: [...state.where],\n    groupBy: [...state.groupBy],\n    having: [...state.having],\n    orderBy: [...state.orderBy],\n    limit: state.limit,\n    offset: state.offset,\n    with: { ...state.with },\n  }\n}\n\nfunction createInitialQueryState(): QueryState {\n  return {\n    select: '*',\n    distinct: false,\n    joins: [],\n    where: [],\n    groupBy: [],\n    having: [],\n    orderBy: [],\n    with: {},\n  }\n}\n\nfunction cloneQueryPlan<row extends Record<string, unknown>, primaryKey extends readonly string[]>(\n  plan: QueryPlan<row, primaryKey>,\n): QueryPlan<row, primaryKey> {\n  switch (plan.kind) {\n    case 'all':\n      return { kind: 'all' } as QueryPlan<row, primaryKey>\n    case 'first':\n      return { kind: 'first' } as QueryPlan<row, primaryKey>\n    case 'find':\n      return {\n        kind: 'find',\n        value: clonePrimaryKeyValue(plan.value) as PrimaryKeyInputForRow<row, primaryKey>,\n      } as QueryPlan<row, primaryKey>\n    case 'count':\n      return { kind: 'count' } as QueryPlan<row, primaryKey>\n    case 'exists':\n      return { kind: 'exists' } as QueryPlan<row, primaryKey>\n    case 'insert':\n      return {\n        kind: 'insert',\n        values: { ...plan.values },\n        options: plan.options ? { ...plan.options } : undefined,\n      } as QueryPlan<row, primaryKey>\n    case 'insertMany':\n      return {\n        kind: 'insertMany',\n        values: plan.values.map((value: Partial<row>) => ({ ...value })),\n        options: plan.options ? { ...plan.options } : undefined,\n      } as QueryPlan<row, primaryKey>\n    case 'update':\n      return {\n        kind: 'update',\n        changes: { ...plan.changes },\n        options: plan.options ? { ...plan.options } : undefined,\n      } as QueryPlan<row, primaryKey>\n    case 'delete':\n      return {\n        kind: 'delete',\n        options: plan.options ? { ...plan.options } : undefined,\n      } as QueryPlan<row, primaryKey>\n    case 'upsert':\n      return {\n        kind: 'upsert',\n        values: { ...plan.values },\n        options: plan.options\n          ? {\n              ...plan.options,\n              conflictTarget: plan.options.conflictTarget\n                ? [...plan.options.conflictTarget]\n                : undefined,\n              update: plan.options.update ? { ...plan.options.update } : undefined,\n            }\n          : undefined,\n      } as QueryPlan<row, primaryKey>\n  }\n}\n\nfunction clonePrimaryKeyValue(value: unknown): unknown {\n  if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n    return value\n  }\n\n  return { ...value }\n}\n\nfunction cloneSelection(selection: '*' | SelectColumn[]): '*' | SelectColumn[] {\n  if (selection === '*') {\n    return '*'\n  }\n\n  return selection.map((column) => ({ ...column }))\n}\n\ntype WriteStatePolicy = {\n  where: boolean\n  orderBy: boolean\n  limit: boolean\n  offset: boolean\n}\n\nfunction assertWriteState(\n  state: QueryState,\n  operation: 'insert' | 'insertMany' | 'update' | 'delete' | 'upsert',\n  policy: WriteStatePolicy,\n): void {\n  let unsupported: string[] = []\n\n  if (state.select !== '*') unsupported.push('select()')\n  if (state.distinct) unsupported.push('distinct()')\n  if (state.joins.length > 0) unsupported.push('join()')\n  if (state.groupBy.length > 0) unsupported.push('groupBy()')\n  if (state.having.length > 0) unsupported.push('having()')\n  if (Object.keys(state.with).length > 0) unsupported.push('with()')\n  if (!policy.where && state.where.length > 0) unsupported.push('where()')\n  if (!policy.orderBy && state.orderBy.length > 0) unsupported.push('orderBy()')\n  if (!policy.limit && state.limit !== undefined) unsupported.push('limit()')\n  if (!policy.offset && state.offset !== undefined) unsupported.push('offset()')\n\n  if (unsupported.length > 0) {\n    throw new DataTableQueryError(\n      operation + '() does not support these query modifiers: ' + unsupported.join(', '),\n    )\n  }\n}\n\ntype ResolvedPredicateColumn = {\n  tableName: string\n  columnName: string\n}\n\nfunction createPredicateColumnResolver(\n  tables: AnyTable[],\n): (column: string) => ResolvedPredicateColumn {\n  let qualifiedColumns = new Map<string, ResolvedPredicateColumn>()\n  let unqualifiedColumns = new Map<string, ResolvedPredicateColumn>()\n  let ambiguousColumns = new Set<string>()\n\n  for (let table of tables) {\n    let tableColumns = getTableColumns(table)\n    let tableName = getTableName(table)\n\n    for (let columnName in tableColumns) {\n      if (!Object.prototype.hasOwnProperty.call(tableColumns, columnName)) {\n        continue\n      }\n\n      let resolvedColumn: ResolvedPredicateColumn = {\n        tableName,\n        columnName,\n      }\n\n      qualifiedColumns.set(tableName + '.' + columnName, resolvedColumn)\n\n      if (ambiguousColumns.has(columnName)) {\n        continue\n      }\n\n      if (unqualifiedColumns.has(columnName)) {\n        unqualifiedColumns.delete(columnName)\n        ambiguousColumns.add(columnName)\n        continue\n      }\n\n      unqualifiedColumns.set(columnName, resolvedColumn)\n    }\n  }\n\n  return function resolveColumn(column: string): ResolvedPredicateColumn {\n    let qualified = qualifiedColumns.get(column)\n    if (qualified) return qualified\n\n    if (column.includes('.')) {\n      throw new DataTableQueryError('Unknown predicate column \"' + column + '\"')\n    }\n\n    if (ambiguousColumns.has(column)) {\n      throw new DataTableQueryError(\n        'Ambiguous predicate column \"' + column + '\". Use a qualified column name',\n      )\n    }\n\n    let unqualified = unqualifiedColumns.get(column)\n\n    if (!unqualified) {\n      throw new DataTableQueryError('Unknown predicate column \"' + column + '\"')\n    }\n\n    return unqualified\n  }\n}\n\nfunction normalizePredicateValues(\n  predicate: Predicate,\n  resolveColumn: (column: string) => ResolvedPredicateColumn,\n): Predicate {\n  if (predicate.type === 'comparison') {\n    let column = resolveColumn(predicate.column)\n\n    if (predicate.valueType === 'column') {\n      resolveColumn(predicate.value)\n      return predicate\n    }\n\n    if (predicate.operator === 'in' || predicate.operator === 'notIn') {\n      if (!Array.isArray(predicate.value)) {\n        throw new DataTableValidationError(\n          'Invalid filter value for column \"' +\n            column.columnName +\n            '\" in table \"' +\n            column.tableName +\n            '\"',\n          [{ message: 'Expected an array value for \"' + predicate.operator + '\" predicate' }],\n          {\n            metadata: {\n              table: column.tableName,\n              column: column.columnName,\n            },\n          },\n        )\n      }\n\n      return predicate\n    }\n\n    return predicate\n  }\n\n  if (predicate.type === 'between') {\n    resolveColumn(predicate.column)\n    return predicate\n  }\n\n  if (predicate.type === 'null') {\n    resolveColumn(predicate.column)\n    return predicate\n  }\n\n  return {\n    ...predicate,\n    predicates: predicate.predicates.map((child) => normalizePredicateValues(child, resolveColumn)),\n  }\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/references.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { column } from './column.ts'\nimport { isColumnReference, normalizeColumnInput } from './references.ts'\nimport { table } from './table.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n  },\n})\n\ndescribe('column references', () => {\n  it('identifies valid and invalid column references', () => {\n    assert.equal(isColumnReference(accounts.id), true)\n    assert.equal(isColumnReference({ kind: 'column' }), false)\n    assert.equal(isColumnReference({ kind: 'not-column' }), false)\n    assert.equal(isColumnReference('accounts.id'), false)\n    assert.equal(isColumnReference(null), false)\n  })\n\n  it('normalizes string and column-reference inputs', () => {\n    assert.equal(normalizeColumnInput('accounts.id'), 'accounts.id')\n    assert.equal(normalizeColumnInput(accounts.id), 'accounts.id')\n  })\n})\n"
  },
  {
    "path": "packages/data-table/src/lib/references.ts",
    "content": "/**\n * Symbol key used to store non-enumerable table metadata.\n */\nexport const tableMetadataKey = Symbol('data-table.tableMetadata')\n\n/**\n * Symbol key used to store non-enumerable column metadata.\n */\nexport const columnMetadataKey = Symbol('data-table.columnMetadata')\n\ntype UnknownTableMetadata<\n  name extends string = string,\n  columns extends Record<string, unknown> = Record<string, unknown>,\n  primaryKey extends readonly string[] = readonly string[],\n  timestamps = unknown,\n> = {\n  name: name\n  columns: columns\n  primaryKey: primaryKey\n  timestamps: timestamps\n}\n\nexport type TableMetadataLike<\n  name extends string = string,\n  columns extends Record<string, unknown> = Record<string, unknown>,\n  primaryKey extends readonly string[] = readonly string[],\n  timestamps = unknown,\n> = {\n  [tableMetadataKey]: UnknownTableMetadata<name, columns, primaryKey, timestamps>\n}\n\nexport type ColumnReferenceLike<qualifiedName extends string = string> = {\n  kind: 'column'\n  [columnMetadataKey]: {\n    tableName: string\n    columnName: string\n    qualifiedName: qualifiedName\n  }\n}\n\nexport type ColumnInput<qualifiedName extends string = string> =\n  | qualifiedName\n  | ColumnReferenceLike<qualifiedName>\n\nexport type NormalizeColumnInput<input> =\n  input extends ColumnReferenceLike<infer qualifiedName> ? qualifiedName : input\n\n/**\n * Returns `true` when a value is a `data-table` column reference.\n * @param value Value to inspect.\n * @returns Whether the value is a column reference object.\n */\nexport function isColumnReference(value: unknown): value is ColumnReferenceLike {\n  if (typeof value !== 'object' || value === null) {\n    return false\n  }\n\n  if (!('kind' in value) || (value as { kind?: unknown }).kind !== 'column') {\n    return false\n  }\n\n  return columnMetadataKey in (value as object)\n}\n\n/**\n * Normalizes string/column-reference inputs to a qualified column name.\n * @param input Column name or column reference.\n * @returns The normalized qualified column name.\n */\nexport function normalizeColumnInput<input extends string | ColumnReferenceLike>(\n  input: input,\n): NormalizeColumnInput<input> {\n  if (typeof input === 'string') {\n    return input as NormalizeColumnInput<input>\n  }\n\n  return input[columnMetadataKey].qualifiedName as NormalizeColumnInput<input>\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/sql-helpers.ts",
    "content": "import type { DataManipulationOperation, DataMigrationOperation, TableRef } from './adapter.ts'\n\n/**\n * Function used to quote SQL identifiers for a dialect.\n */\nexport type QuoteIdentifier = (value: string) => string\n\n/**\n * Type guard that narrows an operation to the data-manipulation union.\n * @param operation Operation to inspect.\n * @returns `true` when the operation is a data-manipulation operation.\n */\nexport function isDataManipulationOperation(\n  operation: DataManipulationOperation | DataMigrationOperation,\n): operation is DataManipulationOperation {\n  return (\n    operation.kind === 'select' ||\n    operation.kind === 'count' ||\n    operation.kind === 'exists' ||\n    operation.kind === 'insert' ||\n    operation.kind === 'insertMany' ||\n    operation.kind === 'update' ||\n    operation.kind === 'delete' ||\n    operation.kind === 'upsert' ||\n    operation.kind === 'raw'\n  )\n}\n\n/**\n * Normalizes an arbitrary join type string into `inner`, `left`, or `right`.\n * @param type Input join type.\n * @returns Normalized join type.\n */\nexport function normalizeJoinType(type: string): string {\n  if (type === 'left') {\n    return 'left'\n  }\n\n  if (type === 'right') {\n    return 'right'\n  }\n\n  return 'inner'\n}\n\n/**\n * Returns stable column order from the union of keys in the provided rows.\n * @param rows Row objects to scan for keys.\n * @returns Deduplicated key list in encounter order.\n */\nexport function collectColumns(rows: Record<string, unknown>[]): string[] {\n  let columns: string[] = []\n  let seen = new Set<string>()\n\n  for (let row of rows) {\n    for (let key in row) {\n      if (!Object.prototype.hasOwnProperty.call(row, key)) {\n        continue\n      }\n\n      if (seen.has(key)) {\n        continue\n      }\n\n      seen.add(key)\n      columns.push(key)\n    }\n  }\n\n  return columns\n}\n\n/**\n * Quotes each segment of a dotted identifier path.\n *\n * Wildcard segments (`*`) are preserved.\n * @param path Dotted path to quote.\n * @param quoteIdentifier Dialect-specific identifier quote function.\n * @returns Quoted path string.\n */\nexport function quotePath(path: string, quoteIdentifier: QuoteIdentifier): string {\n  if (path === '*') {\n    return '*'\n  }\n\n  return path\n    .split('.')\n    .map((segment) => {\n      if (segment === '*') {\n        return '*'\n      }\n\n      return quoteIdentifier(segment)\n    })\n    .join('.')\n}\n\n/**\n * Quotes a `{ schema?, name }` table reference using a dialect quote function.\n * @param table Table reference to quote.\n * @param quoteIdentifier Dialect-specific identifier quote function.\n * @returns Quoted table reference.\n */\nexport function quoteTableRef(table: TableRef, quoteIdentifier: QuoteIdentifier): string {\n  if (table.schema) {\n    return quoteIdentifier(table.schema) + '.' + quoteIdentifier(table.name)\n  }\n\n  return quoteIdentifier(table.name)\n}\n\n/**\n * Converts a JavaScript value into a SQL literal string.\n * @param value Value to serialize.\n * @param options Serialization options.\n * @param options.booleansAsIntegers When `true`, booleans render as `1`/`0`.\n * @returns SQL literal text.\n */\nexport function quoteLiteral(\n  value: unknown,\n  options?: {\n    booleansAsIntegers?: boolean\n  },\n): string {\n  if (value === null) {\n    return 'null'\n  }\n\n  if (typeof value === 'number' || typeof value === 'bigint') {\n    return String(value)\n  }\n\n  if (typeof value === 'boolean') {\n    if (options?.booleansAsIntegers) {\n      return value ? '1' : '0'\n    }\n\n    return value ? 'true' : 'false'\n  }\n\n  if (value instanceof Date) {\n    return quoteLiteral(value.toISOString(), options)\n  }\n\n  return \"'\" + String(value).replace(/'/g, \"''\") + \"'\"\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/sql.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { isSqlStatement, rawSql, sql } from './sql.ts'\n\ndescribe('sql statements', () => {\n  it('builds placeholder statements from scalar values', () => {\n    let statement = sql`select * from accounts where id = ${1} and email = ${'a@example.com'}`\n\n    assert.deepEqual(statement, {\n      text: 'select * from accounts where id = ? and email = ?',\n      values: [1, 'a@example.com'],\n    })\n  })\n\n  it('inlines nested sql statements and merges values', () => {\n    let condition = sql`status = ${'active'}`\n    let statement = sql`select * from accounts where ${condition} and id > ${10}`\n\n    assert.deepEqual(statement, {\n      text: 'select * from accounts where status = ? and id > ?',\n      values: ['active', 10],\n    })\n  })\n\n  it('checks sql statement shapes', () => {\n    assert.equal(isSqlStatement({ text: 'select 1', values: [] }), true)\n    assert.equal(isSqlStatement({ text: 123, values: [] }), false)\n    assert.equal(isSqlStatement({ text: 'select 1', values: 'oops' }), false)\n    assert.equal(isSqlStatement(null), false)\n  })\n\n  it('creates raw sql statements', () => {\n    assert.deepEqual(rawSql('select 1'), {\n      text: 'select 1',\n      values: [],\n    })\n    assert.deepEqual(rawSql('select * from accounts where id = ?', [5]), {\n      text: 'select * from accounts where id = ?',\n      values: [5],\n    })\n  })\n})\n"
  },
  {
    "path": "packages/data-table/src/lib/sql.ts",
    "content": "/**\n * Parameterized SQL payload.\n *\n * The `text` may contain positional placeholders (`?`) or dialect-native\n * placeholders (for example `$1`, `$2`) depending on compiler stage.\n */\nexport type SqlStatement = {\n  text: string\n  values: unknown[]\n}\n\n/**\n * Tagged-template helper for building parameterized SQL statements.\n * @param strings Template string parts.\n * @param values Interpolated values or nested {@link SqlStatement} values.\n * @returns A normalized {@link SqlStatement}.\n * @example\n * ```ts\n * import { sql } from 'remix/data-table'\n *\n * let email = 'user@example.com'\n * let statement = sql`select * from users where email = ${email}`\n * // => { text: 'select * from users where email = ?', values: ['user@example.com'] }\n * ```\n */\nexport function sql(strings: TemplateStringsArray, ...values: unknown[]): SqlStatement {\n  let text = ''\n  let parameters: unknown[] = []\n  let index = 0\n\n  while (index < strings.length) {\n    text += strings[index]\n\n    if (index < values.length) {\n      let value = values[index]\n\n      if (isSqlStatement(value)) {\n        text += value.text\n        parameters.push(...value.values)\n      } else {\n        text += '?'\n        parameters.push(value)\n      }\n    }\n\n    index += 1\n  }\n\n  return {\n    text,\n    values: parameters,\n  }\n}\n\n/**\n * Returns `true` when a value matches the {@link SqlStatement} shape.\n * @param value Value to inspect.\n * @returns Whether the value is a {@link SqlStatement} object.\n */\nexport function isSqlStatement(value: unknown): value is SqlStatement {\n  if (typeof value !== 'object' || value === null) {\n    return false\n  }\n\n  let statement = value as { text?: unknown; values?: unknown }\n\n  return typeof statement.text === 'string' && Array.isArray(statement.values)\n}\n\n/**\n * Creates a SQL statement from raw text and values.\n * @param text SQL text containing placeholders expected by the target adapter.\n * @param values Placeholder values.\n * @returns A normalized SQL statement.\n * @example\n * ```ts\n * import { rawSql } from 'remix/data-table'\n *\n * let statement = rawSql('select * from users where id = ?', [1])\n * ```\n */\nexport function rawSql(text: string, values: unknown[] = []): SqlStatement {\n  return { text, values }\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/table.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { column } from './column.ts'\nimport {\n  columnMetadataKey,\n  fail,\n  getTableAfterDelete,\n  getTableAfterRead,\n  getTableAfterWrite,\n  getTableBeforeDelete,\n  getTableBeforeWrite,\n  getTableValidator,\n  getTableName,\n  getTablePrimaryKey,\n  getTableReference,\n  hasMany,\n  table,\n  tableMetadataKey,\n} from './table.ts'\n\ndescribe('table metadata', () => {\n  it('stores table internals on a symbol key and exposes column refs as properties', () => {\n    let users = table({\n      name: 'users',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n      },\n    })\n\n    assert.deepEqual(Object.keys(users).sort(), ['email', 'id'])\n    assert.equal('name' in users, false)\n    assert.equal(getTableName(users), 'users')\n    assert.deepEqual(getTablePrimaryKey(users), ['id'])\n\n    let tableReference = getTableReference(users)\n    assert.equal(tableReference.name, 'users')\n    assert.equal(tableReference.kind, 'table')\n    assert.equal(users.id[columnMetadataKey].qualifiedName, 'users.id')\n    assert.equal(users[tableMetadataKey].name, 'users')\n  })\n\n  it('supports an optional table-level validator hook', () => {\n    let calls: Array<'create' | 'update'> = []\n    let users = table({\n      name: 'users',\n      columns: {\n        id: column.integer(),\n        email: column.text(),\n      },\n      validate({ operation, value }) {\n        calls.push(operation)\n\n        if (operation === 'create') {\n          return {\n            value: {\n              ...value,\n              id: typeof value.id === 'string' ? Number(value.id) : value.id,\n            },\n          }\n        }\n\n        return {\n          value,\n        }\n      },\n    })\n\n    let validator = getTableValidator(users)\n    assert.ok(validator)\n    if (!validator) {\n      throw new Error('Expected validator')\n    }\n\n    let createResult = validator({\n      operation: 'create',\n      tableName: 'users',\n      value: { id: '1' as never },\n    })\n    assert.deepEqual(createResult, { value: { id: 1 } })\n\n    let updateResult = validator({ operation: 'update', tableName: 'users', value: { id: 2 } })\n    assert.deepEqual(updateResult, { value: { id: 2 } })\n\n    assert.deepEqual(calls, ['create', 'update'])\n  })\n\n  it('creates validation failure results with fail()', () => {\n    assert.deepEqual(fail('Expected email', ['email']), {\n      issues: [{ message: 'Expected email', path: ['email'] }],\n    })\n\n    assert.deepEqual(\n      fail([\n        { message: 'Missing id', path: ['id'] },\n        { message: 'Missing email', path: ['email'] },\n      ]),\n      {\n        issues: [\n          { message: 'Missing id', path: ['id'] },\n          { message: 'Missing email', path: ['email'] },\n        ],\n      },\n    )\n  })\n\n  it('stores optional lifecycle callbacks on table metadata', () => {\n    let beforeWrite = () => ({ value: {} })\n    let afterWrite = () => {}\n    let beforeDelete = () => undefined\n    let afterDelete = () => {}\n    let afterRead = ({ value }: { value: Partial<{ id: number }> }) => ({ value })\n\n    let users = table({\n      name: 'users',\n      columns: {\n        id: column.integer(),\n      },\n      beforeWrite,\n      afterWrite,\n      beforeDelete,\n      afterDelete,\n      afterRead,\n    })\n\n    assert.equal(getTableBeforeWrite(users), beforeWrite)\n    assert.equal(getTableAfterWrite(users), afterWrite)\n    assert.equal(getTableBeforeDelete(users), beforeDelete)\n    assert.equal(getTableAfterDelete(users), afterDelete)\n    assert.equal(getTableAfterRead(users), afterRead)\n  })\n\n  it('builds relations with functional helpers', () => {\n    let users = table({\n      name: 'users',\n      columns: {\n        id: column.integer(),\n      },\n    })\n    let orders = table({\n      name: 'orders',\n      columns: {\n        id: column.integer(),\n        user_id: column.integer(),\n      },\n    })\n\n    let userOrders = hasMany(users, orders).orderBy(orders.id)\n\n    assert.deepEqual(userOrders.sourceKey, ['id'])\n    assert.deepEqual(userOrders.targetKey, ['user_id'])\n    assert.equal(userOrders.modifiers.orderBy[0].column, 'orders.id')\n  })\n})\n"
  },
  {
    "path": "packages/data-table/src/lib/table.ts",
    "content": "import type { ColumnDefinition } from './adapter.ts'\nimport { ColumnBuilder } from './column.ts'\nimport type { ColumnInput as ColumnBuilderInput, ColumnOutput } from './column.ts'\nimport type { Predicate, WhereInput } from './operators.ts'\nimport { inferForeignKey } from './inflection.ts'\nimport { normalizeWhereInput } from './operators.ts'\nimport { columnMetadataKey, normalizeColumnInput, tableMetadataKey } from './references.ts'\nimport type { ColumnInput, ColumnReferenceLike, TableMetadataLike } from './references.ts'\nimport type { Pretty } from './types.ts'\n\n/**\n * Symbol key used to store non-enumerable table metadata.\n */\nexport { columnMetadataKey, tableMetadataKey } from './references.ts'\n\n/**\n * Column builder map used when declaring a table.\n */\nexport type TableColumnsDefinition = Record<string, ColumnBuilder<any>>\n\n/**\n * Validation lifecycle operations.\n */\nexport type TableValidationOperation = 'create' | 'update'\n/**\n * Write lifecycle operations.\n */\nexport type TableWriteOperation = TableValidationOperation\n/**\n * All lifecycle operations exposed by table hooks.\n */\nexport type TableLifecycleOperation = TableWriteOperation | 'delete' | 'read'\n\n/**\n * Single validation issue reported by table hooks.\n */\nexport type ValidationIssue = {\n  message: string\n  path?: Array<string | number>\n}\n\n/**\n * Validation failure returned from table hooks.\n */\nexport type ValidationFailure = {\n  issues: ReadonlyArray<ValidationIssue>\n}\n\n/**\n * Context passed to the `validate` hook.\n */\nexport type TableValidationContext<row extends Record<string, unknown>> = {\n  operation: TableValidationOperation\n  tableName: string\n  value: Partial<row>\n}\n\n/**\n * Result returned from the `validate` hook.\n */\nexport type TableValidationResult<row extends Record<string, unknown>> =\n  | { value: Partial<row> }\n  | ValidationFailure\n\n/**\n * Validation hook that runs before writes.\n */\nexport type TableValidate<row extends Record<string, unknown>> = (\n  context: TableValidationContext<row>,\n) => TableValidationResult<row>\n\n/**\n * Context passed to the `beforeWrite` hook.\n */\nexport type TableBeforeWriteContext<row extends Record<string, unknown>> = {\n  operation: TableWriteOperation\n  tableName: string\n  value: Partial<row>\n}\n\n/**\n * Result returned from the `beforeWrite` hook.\n */\nexport type TableBeforeWriteResult<row extends Record<string, unknown>> =\n  | { value: Partial<row> }\n  | ValidationFailure\n\n/**\n * Hook invoked before a row write executes.\n */\nexport type TableBeforeWrite<row extends Record<string, unknown>> = (\n  context: TableBeforeWriteContext<row>,\n) => TableBeforeWriteResult<row>\n\n/**\n * Context passed to the `afterWrite` hook.\n */\nexport type TableAfterWriteContext<row extends Record<string, unknown>> = {\n  operation: TableWriteOperation\n  tableName: string\n  values: ReadonlyArray<Partial<row>>\n  affectedRows: number\n  insertId?: unknown\n}\n\n/**\n * Hook invoked after a row write completes.\n */\nexport type TableAfterWrite<row extends Record<string, unknown>> = (\n  context: TableAfterWriteContext<row>,\n) => void\n\n/**\n * Context passed to the `beforeDelete` hook.\n */\nexport type TableBeforeDeleteContext = {\n  tableName: string\n  where: ReadonlyArray<Predicate<string>>\n  orderBy: ReadonlyArray<OrderByClause>\n  limit?: number\n  offset?: number\n}\n\n/**\n * Result returned from the `beforeDelete` hook.\n */\nexport type TableBeforeDeleteResult = void | ValidationFailure\n\n/**\n * Hook invoked before a delete operation executes.\n */\nexport type TableBeforeDelete = (context: TableBeforeDeleteContext) => TableBeforeDeleteResult\n\n/**\n * Context passed to the `afterDelete` hook.\n */\nexport type TableAfterDeleteContext = {\n  tableName: string\n  where: ReadonlyArray<Predicate<string>>\n  orderBy: ReadonlyArray<OrderByClause>\n  limit?: number\n  offset?: number\n  affectedRows: number\n}\n\n/**\n * Hook invoked after a delete operation completes.\n */\nexport type TableAfterDelete = (context: TableAfterDeleteContext) => void\n\n/**\n * Context passed to the `afterRead` hook.\n */\nexport type TableAfterReadContext<row extends Record<string, unknown>> = {\n  tableName: string\n  /**\n   * The current row shape being returned. This may be a projection/partial row.\n   */\n  value: Partial<row>\n}\n\n/**\n * Result returned from the `afterRead` hook.\n */\nexport type TableAfterReadResult<row extends Record<string, unknown>> =\n  | { value: Partial<row> }\n  | ValidationFailure\n\n/**\n * Hook invoked after a row is read.\n */\nexport type TableAfterRead<row extends Record<string, unknown>> = (\n  context: TableAfterReadContext<row>,\n) => TableAfterReadResult<row>\n\ntype ColumnNameFromColumns<columns extends TableColumnsDefinition> = keyof columns & string\n\ntype DefaultPrimaryKey<columns extends TableColumnsDefinition> =\n  'id' extends ColumnNameFromColumns<columns>\n    ? readonly ['id']\n    : readonly ColumnNameFromColumns<columns>[]\n\ntype NormalizePrimaryKey<\n  columns extends TableColumnsDefinition,\n  primaryKey extends\n    | ColumnNameFromColumns<columns>\n    | readonly ColumnNameFromColumns<columns>[]\n    | undefined,\n> = primaryKey extends readonly (infer column extends ColumnNameFromColumns<columns>)[]\n  ? readonly [...column[]]\n  : primaryKey extends ColumnNameFromColumns<columns>\n    ? readonly [primaryKey]\n    : DefaultPrimaryKey<columns>\n\n/**\n * Timestamp configuration accepted by {@link table}.\n */\nexport type TimestampOptions = boolean | { createdAt?: string; updatedAt?: string }\n\n/**\n * Resolved timestamp column names for a table.\n */\nexport type TimestampConfig = {\n  createdAt: string\n  updatedAt: string\n}\n\ntype TableMetadata<\n  name extends string,\n  columns extends TableColumnsDefinition,\n  primaryKey extends readonly ColumnNameFromColumns<columns>[],\n> = {\n  name: name\n  columns: columns\n  primaryKey: primaryKey\n  timestamps: TimestampConfig | null\n  columnDefinitions: {\n    [column in keyof columns & string]: ColumnDefinition\n  }\n  beforeWrite?: TableBeforeWrite<TableRowFromColumns<columns>>\n  afterWrite?: TableAfterWrite<TableRowFromColumns<columns>>\n  beforeDelete?: TableBeforeDelete\n  afterDelete?: TableAfterDelete\n  afterRead?: TableAfterRead<TableRowFromColumns<columns>>\n  validate?: TableValidate<TableRowFromColumns<columns>>\n}\n\n/**\n * Typed reference to a table column.\n */\nexport type ColumnReference<\n  tableName extends string,\n  columnName extends string,\n> = ColumnReferenceLike<`${tableName}.${columnName}`> & {\n  [columnMetadataKey]: {\n    tableName: tableName\n    columnName: columnName\n    qualifiedName: `${tableName}.${columnName}`\n  }\n}\n\n/**\n * Any column reference.\n */\nexport type AnyColumn = ColumnReference<string, string>\n\n/**\n * Column reference narrowed by a qualified column name string.\n */\nexport type ColumnReferenceForQualifiedName<qualifiedName extends string> = AnyColumn & {\n  [columnMetadataKey]: {\n    qualifiedName: qualifiedName\n  }\n}\n\ntype TableColumnReferences<name extends string, columns extends TableColumnsDefinition> = {\n  [column in keyof columns & string]: ColumnReference<name, column>\n}\n\ntype TableRowFromColumns<columns extends TableColumnsDefinition> = Pretty<{\n  [column in keyof columns & string]: ColumnOutput<columns[column]>\n}>\n\n/**\n * Fully-typed table object returned by {@link table}.\n */\nexport type Table<\n  name extends string,\n  columns extends TableColumnsDefinition,\n  primaryKey extends readonly ColumnNameFromColumns<columns>[],\n> = TableMetadataLike<name, columns, primaryKey, TimestampConfig | null> & {\n  [tableMetadataKey]: TableMetadata<name, columns, primaryKey>\n} & TableColumnReferences<name, columns>\n\n/**\n * Table-like object with erased concrete column types.\n */\nexport type AnyTable = TableMetadataLike<\n  string,\n  TableColumnsDefinition,\n  readonly string[],\n  TimestampConfig | null\n> & {\n  [tableMetadataKey]: {\n    name: string\n    columns: TableColumnsDefinition\n    primaryKey: readonly string[]\n    timestamps: TimestampConfig | null\n    columnDefinitions: Record<string, ColumnDefinition>\n    beforeWrite?: unknown\n    afterWrite?: unknown\n    beforeDelete?: unknown\n    afterDelete?: unknown\n    afterRead?: unknown\n    validate?: TableValidate<Record<string, unknown>>\n  }\n} & Record<string, unknown>\n\n/**\n * Name of a concrete table.\n */\nexport type TableName<table extends AnyTable> = table[typeof tableMetadataKey]['name']\n\n/**\n * Column builder map for a concrete table.\n */\nexport type TableColumns<table extends AnyTable> = table[typeof tableMetadataKey]['columns']\n\n/**\n * Primary-key column list for a concrete table.\n */\nexport type TablePrimaryKey<table extends AnyTable> = table[typeof tableMetadataKey]['primaryKey']\n\nexport type TableTimestamps<table extends AnyTable> = table[typeof tableMetadataKey]['timestamps']\n\n/**\n * Row shape produced by a concrete table.\n */\nexport type TableRow<table extends AnyTable> = TableRowFromColumns<TableColumns<table>>\n\n/**\n * Row shape with loaded relations merged in.\n */\nexport type TableRowWith<\n  table extends AnyTable,\n  loaded extends Record<string, unknown> = {},\n> = Pretty<TableRow<table> & loaded>\n\n/**\n * Unqualified column names for a concrete table.\n */\nexport type TableColumnName<table extends AnyTable> = keyof TableColumns<table> & string\n\nexport type QualifiedTableColumnName<table extends AnyTable> =\n  `${TableName<table>}.${TableColumnName<table>}`\n\n/**\n * Column input accepted for a concrete table.\n */\nexport type TableColumnInput<table extends AnyTable> = ColumnInput<\n  TableColumnName<table> | QualifiedTableColumnName<table>\n>\n\n/**\n * Plain metadata snapshot of a table.\n */\nexport type TableReference<table extends AnyTable = AnyTable> = {\n  kind: 'table'\n  name: TableName<table>\n  columns: TableColumns<table>\n  primaryKey: TablePrimaryKey<table>\n  timestamps: TableTimestamps<table>\n}\n\n/**\n * Creates a plain table reference snapshot from a table instance.\n * @param table Source table instance.\n * @returns Table metadata snapshot.\n */\nexport function getTableReference<table extends AnyTable>(table: table): TableReference<table> {\n  let metadata = table[tableMetadataKey]\n\n  return {\n    kind: 'table',\n    name: metadata.name as TableName<table>,\n    columns: metadata.columns as TableColumns<table>,\n    primaryKey: metadata.primaryKey as TablePrimaryKey<table>,\n    timestamps: metadata.timestamps as TableTimestamps<table>,\n  }\n}\n\n/**\n * Returns a table's SQL name.\n * @param table Source table instance.\n * @returns Table SQL name.\n */\nexport function getTableName<table extends AnyTable>(table: table): TableName<table> {\n  return table[tableMetadataKey].name as TableName<table>\n}\n\n/**\n * Returns a table's column builder map.\n * @param table Source table instance.\n * @returns Table column builder map.\n */\nexport function getTableColumns<table extends AnyTable>(table: table): TableColumns<table> {\n  return table[tableMetadataKey].columns as TableColumns<table>\n}\n\n/**\n * Returns a table's resolved physical column definitions.\n * @param table Source table instance.\n * @returns Column definition map.\n */\nexport function getTableColumnDefinitions<table extends AnyTable>(\n  table: table,\n): {\n  [column in keyof TableColumns<table> & string]: ColumnDefinition\n} {\n  return table[tableMetadataKey].columnDefinitions as {\n    [column in keyof TableColumns<table> & string]: ColumnDefinition\n  }\n}\n\n/**\n * Returns a table's optional write validator.\n * @param table Source table instance.\n * @returns Validation function or `undefined`.\n */\nexport function getTableValidator<table extends AnyTable>(\n  table: table,\n): TableValidate<TableRow<table>> | undefined {\n  return table[tableMetadataKey].validate as TableValidate<TableRow<table>> | undefined\n}\n\n/**\n * Returns a table's optional before-write lifecycle callback.\n * @param table Source table instance.\n * @returns Before-write callback or `undefined`.\n */\nexport function getTableBeforeWrite<table extends AnyTable>(\n  table: table,\n): TableBeforeWrite<TableRow<table>> | undefined {\n  return table[tableMetadataKey].beforeWrite as TableBeforeWrite<TableRow<table>> | undefined\n}\n\n/**\n * Returns a table's optional after-write lifecycle callback.\n * @param table Source table instance.\n * @returns After-write callback or `undefined`.\n */\nexport function getTableAfterWrite<table extends AnyTable>(\n  table: table,\n): TableAfterWrite<TableRow<table>> | undefined {\n  return table[tableMetadataKey].afterWrite as TableAfterWrite<TableRow<table>> | undefined\n}\n\n/**\n * Returns a table's optional before-delete lifecycle callback.\n * @param table Source table instance.\n * @returns Before-delete callback or `undefined`.\n */\nexport function getTableBeforeDelete<table extends AnyTable>(\n  table: table,\n): TableBeforeDelete | undefined {\n  return table[tableMetadataKey].beforeDelete as TableBeforeDelete | undefined\n}\n\n/**\n * Returns a table's optional after-delete lifecycle callback.\n * @param table Source table instance.\n * @returns After-delete callback or `undefined`.\n */\nexport function getTableAfterDelete<table extends AnyTable>(\n  table: table,\n): TableAfterDelete | undefined {\n  return table[tableMetadataKey].afterDelete as TableAfterDelete | undefined\n}\n\n/**\n * Returns a table's optional after-read lifecycle callback.\n * The callback receives the current read shape, which may be a projected partial row.\n * @param table Source table instance.\n * @returns After-read callback or `undefined`.\n */\nexport function getTableAfterRead<table extends AnyTable>(\n  table: table,\n): TableAfterRead<TableRow<table>> | undefined {\n  return table[tableMetadataKey].afterRead as TableAfterRead<TableRow<table>> | undefined\n}\n\n/**\n * Returns a table's primary key columns.\n * @param table Source table instance.\n * @returns Primary key columns.\n */\nexport function getTablePrimaryKey<table extends AnyTable>(table: table): TablePrimaryKey<table> {\n  return table[tableMetadataKey].primaryKey as TablePrimaryKey<table>\n}\n\n/**\n * Returns a table's resolved timestamp configuration.\n * @param table Source table instance.\n * @returns Timestamp configuration or `null`.\n */\nexport function getTableTimestamps<table extends AnyTable>(table: table): TableTimestamps<table> {\n  return table[tableMetadataKey].timestamps as TableTimestamps<table>\n}\n\n/**\n * Sort direction accepted by `orderBy`.\n */\nexport type OrderDirection = 'asc' | 'desc'\n\n/**\n * Normalized `orderBy` clause.\n */\nexport type OrderByClause = {\n  column: string\n  direction: OrderDirection\n}\n\n/**\n * Cardinality of a relation.\n */\nexport type RelationCardinality = 'one' | 'many'\n\n/**\n * Supported relation kinds.\n */\nexport type RelationKind = 'hasMany' | 'hasOne' | 'belongsTo' | 'hasManyThrough'\n\nexport type RelationResult<relation extends AnyRelation> =\n  relation extends Relation<any, infer target, infer cardinality, infer loaded>\n    ? cardinality extends 'many'\n      ? Array<TableRowWith<target, loaded>>\n      : TableRowWith<target, loaded> | null\n    : never\n\n/**\n * Named relation map for a source table.\n */\nexport type RelationMapForTable<table extends AnyTable> = Record<\n  string,\n  Relation<table, AnyTable, RelationCardinality, any>\n>\n\nexport type LoadedRelationMap<relations extends RelationMapForTable<any>> = Pretty<{\n  [name in keyof relations]: RelationResult<relations[name]>\n}>\n\n/**\n * Column or column list used to join relations.\n */\nexport type KeySelector<table extends AnyTable> =\n  | (keyof TableRow<table> & string)\n  | readonly (keyof TableRow<table> & string)[]\n\n/**\n * Options for defining a {@link hasMany} relation.\n */\nexport type HasManyOptions<source extends AnyTable, target extends AnyTable> = {\n  foreignKey?: KeySelector<target>\n  targetKey?: KeySelector<source>\n}\n\n/**\n * Options for defining a {@link hasOne} relation.\n */\nexport type HasOneOptions<source extends AnyTable, target extends AnyTable> = {\n  foreignKey?: KeySelector<target>\n  targetKey?: KeySelector<source>\n}\n\n/**\n * Options for defining a {@link belongsTo} relation.\n */\nexport type BelongsToOptions<source extends AnyTable, target extends AnyTable> = {\n  foreignKey?: KeySelector<source>\n  targetKey?: KeySelector<target>\n}\n\n/**\n * Options for defining a {@link hasManyThrough} relation.\n */\nexport type HasManyThroughOptions<source extends AnyTable, target extends AnyTable> = {\n  through: Relation<source, AnyTable, RelationCardinality, any>\n  throughForeignKey?: KeySelector<target>\n  throughTargetKey?: string | string[]\n}\n\nexport type RelationModifiers<target extends AnyTable> = {\n  where: Predicate[]\n  orderBy: OrderByClause[]\n  limit?: number\n  offset?: number\n  with: RelationMapForTable<target>\n}\n\nexport type ThroughRelationMetadata = {\n  relation: AnyRelation\n  throughSourceKey: string[]\n  throughTargetKey: string[]\n}\n\n/**\n * Relation descriptor used by query loading.\n */\nexport type Relation<\n  source extends AnyTable,\n  target extends AnyTable,\n  cardinality extends RelationCardinality,\n  loaded extends Record<string, unknown> = {},\n> = {\n  kind: 'relation'\n  relationKind: RelationKind\n  sourceTable: source\n  targetTable: target\n  cardinality: cardinality\n  sourceKey: string[]\n  targetKey: string[]\n  through?: ThroughRelationMetadata\n  modifiers: RelationModifiers<target>\n  where(\n    input: WhereInput<TableColumnName<target> | QualifiedTableColumnName<target>>,\n  ): Relation<source, target, cardinality, loaded>\n  orderBy(\n    column: TableColumnInput<target>,\n    direction?: OrderDirection,\n  ): Relation<source, target, cardinality, loaded>\n  limit(value: number): Relation<source, target, cardinality, loaded>\n  offset(value: number): Relation<source, target, cardinality, loaded>\n  with<relations extends RelationMapForTable<target>>(\n    relations: relations,\n  ): Relation<source, target, cardinality, loaded & LoadedRelationMap<relations>>\n}\n\n/**\n * Relation descriptor with erased table types.\n */\nexport type AnyRelation = Relation<AnyTable, AnyTable, RelationCardinality, any>\n\nexport type CreateTableOptions<\n  name extends string,\n  columns extends TableColumnsDefinition,\n  primaryKey extends\n    | ColumnNameFromColumns<columns>\n    | readonly ColumnNameFromColumns<columns>[]\n    | undefined,\n> = {\n  name: name\n  columns: columns\n  primaryKey?: primaryKey\n  timestamps?: TimestampOptions\n  beforeWrite?: TableBeforeWrite<TableRowFromColumns<columns>>\n  afterWrite?: TableAfterWrite<TableRowFromColumns<columns>>\n  beforeDelete?: TableBeforeDelete\n  afterDelete?: TableAfterDelete\n  afterRead?: TableAfterRead<TableRowFromColumns<columns>>\n  validate?: TableValidate<TableRowFromColumns<columns>>\n}\n\nlet defaultTimestampConfig: TimestampConfig = {\n  createdAt: 'created_at',\n  updatedAt: 'updated_at',\n}\n\n/**\n * Creates a lifecycle/validation failure result with one or more issues.\n * @param message A single issue message.\n * @param path Optional issue path.\n * @returns A {@link ValidationFailure} result object for `validate` and lifecycle callbacks.\n * @example\n * ```ts\n * import { column as c, fail, table } from 'remix/data-table'\n *\n * let users = table({\n *   name: 'users',\n *   columns: {\n *     id: c.integer(),\n *     email: c.varchar(255),\n *   },\n *   validate({ value }) {\n *     if (!value.email) {\n *       // Fail with a single issue message and optional path\n *       return fail('Email is required', ['email'])\n *\n *       // Or fail with multiple issues at once\n *       return fail([\n *         { message: 'Id is required', path: ['id'] },\n *         { message: 'Email is required', path: ['email'] },\n *       ])\n *     }\n *\n *     return { value }\n *   },\n * })\n * ```\n */\nexport function fail(message: string, path?: Array<string | number>): ValidationFailure\n/**\n * @param issues An array of issues.\n */\nexport function fail(issues: ReadonlyArray<ValidationIssue>): ValidationFailure\nexport function fail(\n  messageOrIssues: string | ReadonlyArray<ValidationIssue>,\n  path?: Array<string | number>,\n): ValidationFailure {\n  if (typeof messageOrIssues === 'string') {\n    return {\n      issues: [{ message: messageOrIssues, path }],\n    }\n  }\n\n  return {\n    issues: [...messageOrIssues],\n  }\n}\n\n/**\n * Creates a table object with symbol-backed metadata and direct column references.\n * @param options Table declaration options.\n * @returns A frozen table object.\n * @example\n * ```ts\n * import { column as c, table } from 'remix/data-table'\n *\n * let users = table({\n *   name: 'users',\n *   columns: {\n *     id: c.integer(),\n *     email: c.varchar(255),\n *   },\n *   primaryKey: 'id',\n * })\n * ```\n */\nexport function table<\n  name extends string,\n  columns extends TableColumnsDefinition,\n  primaryKey extends\n    | ColumnNameFromColumns<columns>\n    | readonly ColumnNameFromColumns<columns>[]\n    | undefined = undefined,\n>(\n  options: CreateTableOptions<name, columns, primaryKey>,\n): Table<name, columns, NormalizePrimaryKey<columns, primaryKey>> {\n  let tableName = options.name\n  let columns = options.columns\n\n  let resolvedPrimaryKey = normalizePrimaryKey(tableName, columns, options.primaryKey)\n  let timestampConfig = normalizeTimestampConfig(options.timestamps)\n  let columnDefinitions = resolveTableColumns(tableName, columns)\n  let table = Object.create(null) as Table<name, columns, NormalizePrimaryKey<columns, primaryKey>>\n\n  Object.defineProperty(table, tableMetadataKey, {\n    value: Object.freeze({\n      name: tableName,\n      columns,\n      primaryKey: resolvedPrimaryKey,\n      timestamps: timestampConfig,\n      columnDefinitions,\n      beforeWrite: options.beforeWrite as\n        | TableBeforeWrite<TableRowFromColumns<columns>>\n        | undefined,\n      afterWrite: options.afterWrite as TableAfterWrite<TableRowFromColumns<columns>> | undefined,\n      beforeDelete: options.beforeDelete as TableBeforeDelete | undefined,\n      afterDelete: options.afterDelete as TableAfterDelete | undefined,\n      afterRead: options.afterRead as TableAfterRead<TableRowFromColumns<columns>> | undefined,\n      validate: options.validate as TableValidate<TableRowFromColumns<columns>> | undefined,\n    }),\n    enumerable: false,\n    writable: false,\n    configurable: false,\n  })\n\n  for (let columnName in columns) {\n    if (!Object.prototype.hasOwnProperty.call(columns, columnName)) {\n      continue\n    }\n\n    let column = createColumnReference(tableName, columnName)\n\n    Object.defineProperty(table, columnName, {\n      value: column,\n      enumerable: true,\n      writable: false,\n      configurable: false,\n    })\n  }\n\n  return Object.freeze(table) as Table<name, columns, NormalizePrimaryKey<columns, primaryKey>>\n}\n\nfunction createColumnReference<tableName extends string, columnName extends string>(\n  tableName: tableName,\n  columnName: columnName,\n): ColumnReference<tableName, columnName> {\n  return Object.freeze({\n    kind: 'column',\n    [columnMetadataKey]: Object.freeze({\n      tableName,\n      columnName,\n      qualifiedName: tableName + '.' + columnName,\n    }),\n  }) as ColumnReference<tableName, columnName>\n}\n\nfunction resolveTableColumns<columns extends TableColumnsDefinition>(\n  tableName: string,\n  columns: columns,\n): { [column in keyof columns & string]: ColumnDefinition } {\n  let columnDefinitions: Record<string, ColumnDefinition> = {}\n\n  for (let columnName in columns) {\n    if (!Object.prototype.hasOwnProperty.call(columns, columnName)) {\n      continue\n    }\n\n    let column = columns[columnName]\n\n    if (!(column instanceof ColumnBuilder)) {\n      throw new Error(\n        'Invalid column \"' +\n          columnName +\n          '\" for table \"' +\n          tableName +\n          '\". Expected a column(...) builder',\n      )\n    }\n\n    columnDefinitions[columnName] = column.build()\n  }\n\n  return Object.freeze(columnDefinitions) as {\n    [column in keyof columns & string]: ColumnDefinition\n  }\n}\n\n/**\n * Defines a one-to-many relation from `source` to `target`.\n * @param source Source table.\n * @param target Target table.\n * @param relationOptions Relation key configuration.\n * @returns A relation descriptor.\n */\nexport function hasMany<source extends AnyTable, target extends AnyTable>(\n  source: source,\n  target: target,\n  relationOptions?: HasManyOptions<source, target>,\n): Relation<source, target, 'many'> {\n  let sourceKey = normalizeKeySelector(\n    source,\n    relationOptions?.targetKey,\n    'targetKey',\n    getTablePrimaryKey(source) as string[],\n  )\n  let targetKey = normalizeKeySelector(target, relationOptions?.foreignKey, 'foreignKey', [\n    inferForeignKey(getTableName(source)),\n  ])\n\n  assertKeyLengths(getTableName(source), getTableName(target), sourceKey, targetKey)\n\n  return createRelation({\n    relationKind: 'hasMany',\n    cardinality: 'many',\n    sourceTable: source,\n    targetTable: target,\n    sourceKey,\n    targetKey,\n  })\n}\n\n/**\n * Defines a one-to-one relation from `source` to `target` where the foreign key lives on `target`.\n * @param source Source table.\n * @param target Target table.\n * @param relationOptions Relation key configuration.\n * @returns A relation descriptor.\n */\nexport function hasOne<source extends AnyTable, target extends AnyTable>(\n  source: source,\n  target: target,\n  relationOptions?: HasOneOptions<source, target>,\n): Relation<source, target, 'one'> {\n  let sourceKey = normalizeKeySelector(\n    source,\n    relationOptions?.targetKey,\n    'targetKey',\n    getTablePrimaryKey(source) as string[],\n  )\n  let targetKey = normalizeKeySelector(target, relationOptions?.foreignKey, 'foreignKey', [\n    inferForeignKey(getTableName(source)),\n  ])\n\n  assertKeyLengths(getTableName(source), getTableName(target), sourceKey, targetKey)\n\n  return createRelation({\n    relationKind: 'hasOne',\n    cardinality: 'one',\n    sourceTable: source,\n    targetTable: target,\n    sourceKey,\n    targetKey,\n  })\n}\n\n/**\n * Defines a one-to-one relation from `source` to `target`.\n * @param source Source table.\n * @param target Target table.\n * @param relationOptions Relation key configuration.\n * @returns A relation descriptor.\n */\nexport function belongsTo<source extends AnyTable, target extends AnyTable>(\n  source: source,\n  target: target,\n  relationOptions?: BelongsToOptions<source, target>,\n): Relation<source, target, 'one'> {\n  let sourceKey = normalizeKeySelector(source, relationOptions?.foreignKey, 'foreignKey', [\n    inferForeignKey(getTableName(target)),\n  ])\n  let targetKey = normalizeKeySelector(\n    target,\n    relationOptions?.targetKey,\n    'targetKey',\n    getTablePrimaryKey(target) as string[],\n  )\n\n  assertKeyLengths(getTableName(source), getTableName(target), sourceKey, targetKey)\n\n  return createRelation({\n    relationKind: 'belongsTo',\n    cardinality: 'one',\n    sourceTable: source,\n    targetTable: target,\n    sourceKey,\n    targetKey,\n  })\n}\n\n/**\n * Defines a one-to-many relation from `source` to `target` through an intermediate relation.\n * @param source Source table.\n * @param target Target table.\n * @param relationOptions Through relation configuration.\n * @returns A relation descriptor.\n */\nexport function hasManyThrough<source extends AnyTable, target extends AnyTable>(\n  source: source,\n  target: target,\n  relationOptions: HasManyThroughOptions<source, target>,\n): Relation<source, target, 'many'> {\n  let throughRelation = relationOptions.through\n\n  if (throughRelation.sourceTable !== source) {\n    throw new Error(\n      'hasManyThrough expects a through relation whose source table matches ' +\n        getTableName(source),\n    )\n  }\n\n  let throughTargetKey = normalizeKeysForTable(\n    throughRelation.targetTable,\n    relationOptions.throughTargetKey,\n    'throughTargetKey',\n    getTablePrimaryKey(throughRelation.targetTable),\n  )\n  let throughForeignKey = normalizeKeySelector(\n    target,\n    relationOptions.throughForeignKey,\n    'throughForeignKey',\n    [inferForeignKey(getTableName(throughRelation.targetTable))],\n  )\n\n  assertKeyLengths(\n    getTableName(throughRelation.targetTable),\n    getTableName(target),\n    throughTargetKey,\n    throughForeignKey,\n  )\n\n  return createRelation({\n    relationKind: 'hasManyThrough',\n    cardinality: 'many',\n    sourceTable: source,\n    targetTable: target,\n    sourceKey: [...throughRelation.sourceKey],\n    targetKey: [...throughRelation.targetKey],\n    through: {\n      relation: throughRelation as AnyRelation,\n      throughSourceKey: throughTargetKey,\n      throughTargetKey: throughForeignKey,\n    },\n  })\n}\n\n/**\n * Convenience helper for standard snake_case timestamp columns.\n * @returns Column-builder map for `created_at`/`updated_at`.\n */\nexport function timestamps(): Record<\n  'created_at' | 'updated_at',\n  ColumnBuilder<Date | string | number>\n> {\n  let timestampColumn = () => new ColumnBuilder<Date | string | number>({ type: 'timestamp' })\n\n  return {\n    created_at: timestampColumn(),\n    updated_at: timestampColumn(),\n  }\n}\n\n/**\n * Primary-key input accepted by `find()`, `update()`, and similar helpers.\n */\nexport type PrimaryKeyInput<table extends AnyTable> =\n  TablePrimaryKey<table> extends readonly [infer column extends string]\n    ? column extends keyof TableColumns<table> & string\n      ? ColumnBuilderInput<TableColumns<table>[column]>\n      : never\n    : Pretty<{\n        [column in TablePrimaryKey<table>[number] &\n          keyof TableColumns<table> &\n          string]: ColumnBuilderInput<TableColumns<table>[column]>\n      }>\n\n/**\n * Normalizes a primary-key input into an object keyed by primary-key columns.\n * @param table Source table.\n * @param value Primary-key input value.\n * @returns Primary-key object.\n */\nexport function getPrimaryKeyObject<table extends AnyTable>(\n  table: table,\n  value: PrimaryKeyInput<table>,\n): Partial<TableRow<table>> {\n  let keys = getTablePrimaryKey(table)\n\n  if (keys.length === 1 && (typeof value !== 'object' || value === null || Array.isArray(value))) {\n    let key = keys[0] as keyof TableRow<table>\n    return { [key]: value } as Partial<TableRow<table>>\n  }\n\n  if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n    throw new Error('Composite primary keys require an object value')\n  }\n\n  let objectValue = value as Record<string, unknown>\n  let output: Partial<TableRow<table>> = {}\n\n  for (let key of keys) {\n    if (!(key in objectValue)) {\n      throw new Error(\n        'Missing key \"' + key + '\" for primary key lookup on \"' + getTableName(table) + '\"',\n      )\n    }\n\n    ;(output as Record<string, unknown>)[key] = objectValue[key]\n  }\n\n  return output\n}\n\n/**\n * Builds a stable key for a row tuple.\n * @param row Source row.\n * @param columns Columns included in the tuple.\n * @returns Stable tuple key.\n */\nexport function getCompositeKey(row: Record<string, unknown>, columns: readonly string[]): string {\n  let values = columns.map((column) => stableSerialize(row[column]))\n\n  return values.join('::')\n}\n\n/**\n * Serializes values into stable string representations for key generation.\n * @param value Value to serialize.\n * @returns Stable serialized value.\n */\nexport function stableSerialize(value: unknown): string {\n  if (value === null) {\n    return 'null'\n  }\n\n  if (value === undefined) {\n    return 'undefined'\n  }\n\n  if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n    return String(value)\n  }\n\n  if (typeof value === 'string') {\n    return JSON.stringify(value)\n  }\n\n  if (value instanceof Date) {\n    return 'date:' + value.toISOString()\n  }\n\n  return JSON.stringify(value)\n}\n\nfunction normalizePrimaryKey(\n  tableName: string,\n  columns: TableColumnsDefinition,\n  primaryKey?: string | readonly string[],\n): string[] {\n  if (primaryKey === undefined) {\n    if (!Object.prototype.hasOwnProperty.call(columns, 'id')) {\n      throw new Error(\n        'Table \"' + tableName + '\" must include an \"id\" column or an explicit primaryKey',\n      )\n    }\n\n    return ['id']\n  }\n\n  let keys = Array.isArray(primaryKey) ? [...primaryKey] : [primaryKey]\n\n  if (keys.length === 0) {\n    throw new Error('Table \"' + tableName + '\" primaryKey must contain at least one column')\n  }\n\n  for (let key of keys) {\n    if (!Object.prototype.hasOwnProperty.call(columns, key)) {\n      throw new Error('Table \"' + tableName + '\" primaryKey column \"' + key + '\" does not exist')\n    }\n  }\n\n  return keys\n}\n\nfunction normalizeKeySelector<table extends AnyTable>(\n  table: table,\n  selector: KeySelector<table> | undefined,\n  optionName: string,\n  defaultValue: readonly string[],\n): string[] {\n  return normalizeKeysForTable(table, selector, optionName, defaultValue)\n}\n\nfunction normalizeKeysForTable(\n  table: AnyTable,\n  selector: string | readonly string[] | undefined,\n  optionName: string,\n  defaultValue: readonly string[],\n): string[] {\n  if (selector === undefined) {\n    return [...defaultValue]\n  }\n\n  let keys = Array.isArray(selector) ? [...selector] : [selector]\n\n  if (keys.length === 0) {\n    throw new Error(\n      'Option \"' + optionName + '\" for table \"' + getTableName(table) + '\" must not be empty',\n    )\n  }\n\n  let columns = getTableColumns(table)\n\n  for (let key of keys) {\n    if (!Object.prototype.hasOwnProperty.call(columns, key)) {\n      throw new Error(\n        'Unknown column \"' +\n          key +\n          '\" in option \"' +\n          optionName +\n          '\" for table \"' +\n          getTableName(table) +\n          '\"',\n      )\n    }\n  }\n\n  return keys\n}\n\nfunction normalizeTimestampConfig(options: TimestampOptions | undefined): TimestampConfig | null {\n  if (!options) {\n    return null\n  }\n\n  if (options === true) {\n    return { ...defaultTimestampConfig }\n  }\n\n  return {\n    createdAt: options.createdAt ?? defaultTimestampConfig.createdAt,\n    updatedAt: options.updatedAt ?? defaultTimestampConfig.updatedAt,\n  }\n}\n\nfunction assertKeyLengths(\n  sourceTableName: string,\n  targetTableName: string,\n  sourceKey: string[],\n  targetKey: string[],\n): void {\n  if (sourceKey.length !== targetKey.length) {\n    throw new Error(\n      'Relation key mismatch between \"' +\n        sourceTableName +\n        '\" (' +\n        sourceKey.join(', ') +\n        ') and \"' +\n        targetTableName +\n        '\" (' +\n        targetKey.join(', ') +\n        ')',\n    )\n  }\n}\n\ntype CreateRelationOptions<\n  source extends AnyTable,\n  target extends AnyTable,\n  cardinality extends RelationCardinality,\n> = {\n  relationKind: RelationKind\n  cardinality: cardinality\n  sourceTable: source\n  targetTable: target\n  sourceKey: string[]\n  targetKey: string[]\n  through?: ThroughRelationMetadata\n  modifiers?: Partial<RelationModifiers<target>>\n}\n\nfunction createRelation<\n  source extends AnyTable,\n  target extends AnyTable,\n  cardinality extends RelationCardinality,\n  loaded extends Record<string, unknown> = {},\n>(\n  options: CreateRelationOptions<source, target, cardinality>,\n): Relation<source, target, cardinality, loaded> {\n  let baseModifiers: RelationModifiers<target> = {\n    where: options.modifiers?.where ? [...options.modifiers.where] : [],\n    orderBy: options.modifiers?.orderBy ? [...options.modifiers.orderBy] : [],\n    limit: options.modifiers?.limit,\n    offset: options.modifiers?.offset,\n    with: options.modifiers?.with ? { ...options.modifiers.with } : {},\n  }\n\n  let relation: Relation<source, target, cardinality, loaded> = {\n    kind: 'relation',\n    relationKind: options.relationKind,\n    sourceTable: options.sourceTable,\n    targetTable: options.targetTable,\n    cardinality: options.cardinality,\n    sourceKey: [...options.sourceKey],\n    targetKey: [...options.targetKey],\n    through: options.through,\n    modifiers: baseModifiers,\n\n    where(input: WhereInput<TableColumnName<target> | QualifiedTableColumnName<target>>) {\n      let predicate = normalizeWhereInput(input)\n      return cloneRelation(relation, {\n        where: [...relation.modifiers.where, predicate],\n      })\n    },\n\n    orderBy(column: TableColumnInput<target>, direction: OrderDirection = 'asc') {\n      return cloneRelation(relation, {\n        orderBy: [\n          ...relation.modifiers.orderBy,\n          {\n            column: normalizeColumnInput(column),\n            direction,\n          },\n        ],\n      })\n    },\n\n    limit(value: number) {\n      return cloneRelation(relation, {\n        limit: value,\n      })\n    },\n\n    offset(value: number) {\n      return cloneRelation(relation, {\n        offset: value,\n      })\n    },\n\n    with<relations extends RelationMapForTable<target>>(relations: relations) {\n      return cloneRelation(relation, {\n        with: {\n          ...relation.modifiers.with,\n          ...relations,\n        },\n      }) as Relation<source, target, cardinality, loaded & LoadedRelationMap<relations>>\n    },\n  }\n\n  return relation\n}\n\nfunction cloneRelation<\n  source extends AnyTable,\n  target extends AnyTable,\n  cardinality extends RelationCardinality,\n  loaded extends Record<string, unknown>,\n>(\n  relation: Relation<source, target, cardinality, loaded>,\n  patch: Partial<RelationModifiers<target>>,\n): Relation<source, target, cardinality, loaded> {\n  return createRelation({\n    relationKind: relation.relationKind,\n    cardinality: relation.cardinality,\n    sourceTable: relation.sourceTable,\n    targetTable: relation.targetTable,\n    sourceKey: relation.sourceKey,\n    targetKey: relation.targetKey,\n    through: relation.through,\n    modifiers: {\n      where: patch.where ?? relation.modifiers.where,\n      orderBy: patch.orderBy ?? relation.modifiers.orderBy,\n      limit: patch.limit ?? relation.modifiers.limit,\n      offset: patch.offset ?? relation.modifiers.offset,\n      with: patch.with ?? relation.modifiers.with,\n    },\n  })\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/type-safety.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { afterEach, describe, it } from 'node:test'\n\nimport { column } from './column.ts'\nimport { createDatabase, Database } from './database.ts'\nimport type {\n  QueryColumnTypesForTable,\n  QueryForTable,\n  QueryTableInput,\n  WriteResult,\n} from './database.ts'\nimport type { AnyQuery, Query } from './query.ts'\nimport { query } from './query.ts'\nimport type { AnyQuery as PublicAnyQuery } from '../index.ts'\n// @ts-expect-error CountQuery is no longer exported\nimport type { CountQuery as _CountQuery } from '../index.ts'\n// @ts-expect-error InsertCommand is no longer exported\nimport type { InsertCommand as _InsertCommand } from '../index.ts'\n// @ts-expect-error QueryMethod is no longer exported\nimport type { QueryMethod as _QueryMethod } from '../index.ts'\nimport { table, hasMany } from './table.ts'\nimport type { TableReference, TableRow } from './table.ts'\nimport { eq } from './operators.ts'\nimport type { SqliteTestSeed } from '../../test/sqlite-test-database.ts'\nimport { createSqliteTestAdapter } from '../../test/sqlite-test-database.ts'\n\ntype Equal<left, right> =\n  (<value>() => value extends left ? 1 : 2) extends <value>() => value extends right ? 1 : 2\n    ? true\n    : false\n\nfunction expectType<condition extends true>(_value?: condition): void {}\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n    email: column.text(),\n    status: column.text(),\n  },\n})\n\nlet projects = table({\n  name: 'projects',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n    archived: column.boolean(),\n  },\n})\n\nlet accountProjects = hasMany(accounts, projects)\n\nlet inferredColumns = table({\n  name: 'inferred_columns',\n  columns: {\n    id: column.integer(),\n    title: column.text(),\n    is_active: column.boolean(),\n    amount: column.decimal(10, 2),\n    status: column.enum(['draft', 'published'] as const),\n    metadata: column.json(),\n    happened_at: column.timestamp(),\n    big_counter: column.bigint(),\n    validated_payload: column.json(),\n  },\n})\n\nlet cleanups = new Set<() => void>()\n\nafterEach(() => {\n  for (let cleanup of cleanups) {\n    cleanup()\n  }\n\n  cleanups.clear()\n})\n\ndescribe('type safety', () => {\n  it('infers unvalidated column types from physical types and falls back to unknown', () => {\n    type Row = TableRow<typeof inferredColumns>\n\n    expectType<Equal<Row['title'], string>>()\n    expectType<Equal<Row['is_active'], boolean>>()\n    expectType<Equal<Row['amount'], number>>()\n    expectType<Equal<Row['status'], 'draft' | 'published'>>()\n    expectType<Equal<Row['metadata'], unknown>>()\n    expectType<Equal<Row['happened_at'], unknown>>()\n    expectType<Equal<Row['big_counter'], unknown>>()\n    expectType<Equal<Row['validated_payload'], unknown>>()\n  })\n\n  it('types direct construction, helper construction, and transaction callbacks as Database', async () => {\n    let direct = new Database(createAdapter())\n    let wrapped = createDatabase(createAdapter())\n\n    expectType<Equal<typeof direct, Database>>()\n    expectType<Equal<typeof wrapped, Database>>()\n\n    await direct.transaction(async (transactionDatabase) => {\n      expectType<Equal<typeof transactionDatabase, Database>>()\n      return undefined\n    })\n\n    assert.equal(direct instanceof Database, true)\n    assert.equal(wrapped instanceof Database, true)\n  })\n\n  it('exposes Query generics as column and row output maps', () => {\n    let db = createDatabase(createAdapter())\n    let query = db.query(accounts)\n\n    type QueryType = typeof query\n    type QuerySource = QueryType extends Query<infer source, any, any, any, any> ? source : never\n    type QueryColumns =\n      QueryType extends Query<any, infer columnTypes, any, any, any> ? columnTypes : never\n    type QueryRow = QueryType extends Query<any, any, infer row, any, any> ? row : never\n    type QueryTableName = QuerySource extends QueryTableInput<infer name, any, any> ? name : never\n    type QueryPrimaryKey = QuerySource extends QueryTableInput<any, any, infer key> ? key : never\n    type QueryBinding =\n      QueryType extends Query<any, any, any, any, infer queryPhase>\n        ? queryPhase extends { binding: infer binding }\n          ? binding\n          : never\n        : never\n    type DatabaseQueryMethod = Database['query']\n    type QueryFromTableAlias = QueryForTable<typeof accounts>\n    type QueryMethodReturnsBoundQuery = DatabaseQueryMethod extends (\n      ...args: any[]\n    ) => Query<any, any, any, any, { binding: 'bound'; mode: 'all' }>\n      ? true\n      : false\n    type QueryColumnsFromAlias = QueryColumnTypesForTable<typeof accounts>\n    type AccountsReference = TableReference<typeof accounts>\n    type AccountsReferenceColumns = keyof AccountsReference['columns'] & string\n\n    type ExpectedColumns = {\n      id: number\n      email: string\n      status: string\n      'accounts.id': number\n      'accounts.email': string\n      'accounts.status': string\n    }\n    type ExpectedRow = {\n      id: number\n      email: string\n      status: string\n    }\n\n    expectType<Equal<QueryColumns, ExpectedColumns>>()\n    expectType<Equal<QueryRow, ExpectedRow>>()\n    expectType<Equal<QueryTableName, 'accounts'>>()\n    expectType<Equal<QueryPrimaryKey, readonly ['id']>>()\n    expectType<Equal<QueryType, QueryFromTableAlias>>()\n    expectType<Equal<PublicAnyQuery, AnyQuery>>()\n    expectType<Equal<QueryMethodReturnsBoundQuery, true>>()\n    expectType<Equal<QueryBinding, 'bound'>>()\n    expectType<Equal<QueryColumns, QueryColumnsFromAlias>>()\n    expectType<Equal<AccountsReference['kind'], 'table'>>()\n    expectType<Equal<AccountsReference['name'], 'accounts'>>()\n    expectType<Equal<AccountsReference['primaryKey'], readonly ['id']>>()\n    expectType<Equal<AccountsReferenceColumns, 'email' | 'id' | 'status'>>()\n  })\n\n  it('types query(table) and db.exec(...) for unbound queries in every execution mode', async () => {\n    let db = createDatabase(\n      createAdapter({\n        accounts: [\n          { id: 1, email: 'a@example.com', status: 'active' },\n          { id: 2, email: 'b@example.com', status: 'inactive' },\n        ],\n        projects: [{ id: 100, account_id: 1, archived: false }],\n      }),\n    )\n\n    let unbound = query(accounts).where({ status: 'active' })\n    let firstQuery = query(accounts).first()\n    let updateQuery = query(accounts).where({ status: 'inactive' }).update({ status: 'active' })\n    let rows = await db.exec(unbound)\n    let first = await db.exec(firstQuery)\n    let found = await db.exec(query(accounts).find(1))\n    let count = await db.exec(query(accounts).count())\n    let exists = await db.exec(query(accounts).where({ status: 'active' }).exists())\n    let insertResult = await db.exec(\n      query(accounts).insert({ id: 3, email: 'c@example.com', status: 'inactive' }),\n    )\n    let insertManyResult = await db.exec(\n      query(accounts).insertMany([\n        { id: 4, email: 'd@example.com', status: 'archived' },\n        { id: 5, email: 'e@example.com', status: 'active' },\n      ]),\n    )\n    let updateResult = await db.exec(updateQuery)\n    let deleteResult = await db.exec(query(accounts).where({ id: 4 }).delete())\n    let upsertResult = await db.exec(\n      query(accounts).upsert(\n        { id: 6, email: 'f@example.com', status: 'active' },\n        { conflictTarget: ['id'] },\n      ),\n    )\n\n    assert.equal(rows.length, 1)\n    assert.equal(first?.id, 1)\n    assert.equal(found?.id, 1)\n    assert.equal(count, 2)\n    assert.equal(exists, true)\n    assert.equal(insertResult.affectedRows, 1)\n    assert.equal(insertManyResult.affectedRows, 2)\n    assert.equal(updateResult.affectedRows, 2)\n    assert.equal(deleteResult.affectedRows, 1)\n    assert.equal(upsertResult.affectedRows, 1)\n\n    type Unbound = typeof unbound\n    type FirstQuery = typeof firstQuery\n    type UpdateQuery = typeof updateQuery\n    type UnboundBinding =\n      Unbound extends Query<any, any, any, any, infer queryPhase>\n        ? queryPhase extends { binding: infer binding }\n          ? binding\n          : never\n        : never\n    type FirstMode =\n      FirstQuery extends Query<any, any, any, any, infer queryPhase>\n        ? queryPhase extends { mode: infer mode }\n          ? mode\n          : never\n        : never\n    type UpdateMode =\n      UpdateQuery extends Query<any, any, any, any, infer queryPhase>\n        ? queryPhase extends { mode: infer mode }\n          ? mode\n          : never\n        : never\n    type Row = (typeof rows)[number]\n\n    expectType<Equal<UnboundBinding, 'unbound'>>()\n    expectType<Equal<FirstMode, 'first'>>()\n    expectType<Equal<UpdateMode, 'update'>>()\n    expectType<Equal<Row['id'], number>>()\n    expectType<Equal<Row['email'], string>>()\n\n    function verifyTypeErrors(): void {\n      // @ts-expect-error unbound queries do not expose all()\n      query(accounts).all()\n      // @ts-expect-error terminal queries do not expose builder methods\n      query(accounts).first().where({ id: 1 })\n      // @ts-expect-error terminal queries do not expose builder methods\n      query(accounts).update({ status: 'active' }).limit(1)\n      // @ts-expect-error values are only accepted for raw SQL exec()\n      db.exec(query(accounts), [])\n      // @ts-expect-error db.exec only accepts Query values or raw SQL\n      db.exec({ kind: 'count' })\n    }\n\n    void verifyTypeErrors\n  })\n\n  it('narrows select() result types while preserving relation types', async () => {\n    let db = createDatabase(\n      createAdapter({\n        accounts: [{ id: 1, email: 'a@example.com', status: 'active' }],\n        projects: [\n          { id: 100, account_id: 1, archived: false },\n          { id: 101, account_id: 3, archived: false },\n        ],\n      }),\n    )\n\n    let rows = await db.query(accounts).select('id').with({ projects: accountProjects }).all()\n\n    assert.equal(rows.length, 1)\n    assert.equal(rows[0].id, 1)\n    assert.equal(rows[0].projects.length, 1)\n    assert.equal(Boolean(rows[0].projects[0].archived), false)\n    assert.deepEqual(Object.keys(rows[0]).sort(), ['id', 'projects'])\n\n    type Row = (typeof rows)[number]\n    expectType<Equal<Row['id'], number>>()\n    expectType<Equal<Row['projects'][number]['id'], number>>()\n    expectType<Equal<Row['projects'][number]['account_id'], number>>()\n    expectType<Equal<Row['projects'][number]['archived'], boolean>>()\n\n    // @ts-expect-error select('id') should not expose non-selected account columns\n    rows[0].email\n  })\n\n  it('supports typed alias select() and joined order/group columns', async () => {\n    let db = createDatabase(\n      createAdapter({\n        accounts: [{ id: 1, email: 'a@example.com', status: 'active' }],\n        projects: [\n          { id: 100, account_id: 1, archived: false },\n          { id: 101, account_id: 3, archived: false },\n        ],\n      }),\n    )\n\n    let rows = await db\n      .query(accounts)\n      .join(projects, eq(accounts.id, projects.account_id))\n      .select({\n        accountId: accounts.id,\n        accountEmail: accounts.email,\n        projectId: projects.id,\n        projectArchived: projects.archived,\n      })\n      .orderBy(projects.id, 'asc')\n      .all()\n\n    assert.equal(rows.length, 1)\n    assert.equal(rows[0].accountId, 1)\n    assert.equal(rows[0].accountEmail, 'a@example.com')\n    assert.equal(rows[0].projectId, 100)\n    assert.equal(Boolean(rows[0].projectArchived), false)\n\n    let groupedCount = await db\n      .query(accounts)\n      .join(projects, eq(accounts.id, projects.account_id))\n      .groupBy(projects.account_id)\n      .having(eq(projects.account_id, 1))\n      .count()\n\n    assert.equal(groupedCount, 1)\n\n    type Row = (typeof rows)[number]\n    expectType<Equal<Row['accountId'], number>>()\n    expectType<Equal<Row['accountEmail'], string>>()\n    expectType<Equal<Row['projectId'], number>>()\n    expectType<Equal<Row['projectArchived'], boolean>>()\n\n    // @ts-expect-error alias select should not expose original source column names\n    rows[0].email\n\n    function verifyTypeErrors(): void {\n      db.query(accounts)\n        .join(projects, eq(accounts.id, projects.account_id))\n        // @ts-expect-error unknown joined column for orderBy\n        .orderBy(projects.nope)\n      db.query(accounts)\n        .join(projects, eq(accounts.id, projects.account_id))\n        // @ts-expect-error unknown joined column for groupBy\n        .groupBy(projects.nope)\n      db.query(accounts)\n        .join(projects, eq(accounts.id, projects.account_id))\n        // @ts-expect-error unknown source column in alias selection\n        .select({ bad: projects.nope })\n    }\n\n    void verifyTypeErrors\n  })\n\n  it('enforces typed keys for where/having/join/relation filters while running real queries', async () => {\n    let db = createDatabase(\n      createAdapter({\n        accounts: [{ id: 1, email: 'a@example.com', status: 'active' }],\n        projects: [{ id: 100, account_id: 1, archived: false }],\n      }),\n    )\n\n    let filtered = await db.query(accounts).where({ status: 'active' }).all()\n    let groupedCount = await db\n      .query(accounts)\n      .groupBy('status')\n      .having({ status: 'active' })\n      .count()\n    let joined = await db\n      .query(accounts)\n      .join(projects, eq(accounts.id, projects.account_id))\n      .where(eq(projects.archived, false))\n      .all()\n    let withRelations = await db\n      .query(accounts)\n      .with({ projects: accountProjects.where({ archived: false }) })\n      .all()\n\n    assert.equal(filtered.length, 1)\n    assert.equal(groupedCount, 1)\n    assert.equal(joined.length, 1)\n    assert.equal(withRelations[0].projects.length, 1)\n\n    function verifyTypeErrors(): void {\n      // @ts-expect-error unknown predicate key\n      db.query(accounts).where({ not_a_column: 'active' })\n      // @ts-expect-error unknown predicate key\n      db.query(accounts).having({ not_a_column: 'active' })\n      // @ts-expect-error join predicate key must be from source or target table\n      db.query(accounts).join(projects, eq('not_a_column', true))\n      // @ts-expect-error right-hand column reference must be from source or target table\n      db.query(accounts).join(projects, eq(accounts.id, 'projects.not_a_column'))\n      // @ts-expect-error relation predicate key must be from relation target table\n      accountProjects.where({ not_a_column: true })\n    }\n\n    void verifyTypeErrors\n  })\n\n  it('keeps findOne/findMany where and orderBy typing symmetric for single-table queries', async () => {\n    let db = createDatabase(\n      createAdapter({\n        accounts: [{ id: 1, email: 'a@example.com', status: 'active' }],\n        projects: [{ id: 100, account_id: 1, archived: false }],\n      }),\n    )\n\n    let first = await db.find(accounts, 1)\n    let active = await db.findOne(accounts, {\n      where: { status: 'active' },\n      orderBy: ['accounts.id', 'asc'],\n    })\n    let rows = await db.findMany(accounts, {\n      where: eq('status', 'active'),\n      orderBy: [\n        ['status', 'asc'],\n        ['id', 'desc'],\n      ],\n      with: { projects: accountProjects },\n    })\n    let count = await db.count(accounts, { where: { status: 'active' } })\n\n    assert.equal(first?.id, 1)\n    assert.equal(active?.email, 'a@example.com')\n    assert.equal(rows.length, 1)\n    assert.equal(rows[0].projects.length, 1)\n    assert.equal(count, 1)\n\n    type Row = (typeof rows)[number]\n    expectType<Equal<Row['projects'][number]['id'], number>>()\n    expectType<Equal<Row['projects'][number]['account_id'], number>>()\n    expectType<Equal<Row['projects'][number]['archived'], boolean>>()\n\n    function verifyTypeErrors(): void {\n      // @ts-expect-error unknown where key\n      db.findOne(accounts, { where: { not_a_column: 'active' } })\n      // @ts-expect-error unknown orderBy column\n      db.findMany(accounts, { orderBy: ['not_a_column', 'asc'] })\n      db.findMany(accounts, {\n        orderBy: [\n          ['id', 'asc'],\n          // @ts-expect-error unknown orderBy column in tuple list\n          ['not_a_column', 'desc'],\n        ],\n      })\n      // @ts-expect-error unknown where key\n      db.count(accounts, { where: { not_a_column: 'active' } })\n      // @ts-expect-error orderBy is not supported by db.count()\n      db.count(accounts, { orderBy: ['id', 'asc'] })\n      // @ts-expect-error limit is not supported by db.count()\n      db.count(accounts, { limit: 1 })\n      // @ts-expect-error offset is not supported by db.count()\n      db.count(accounts, { offset: 1 })\n    }\n\n    void verifyTypeErrors\n  })\n\n  it('keeps update/delete helper typing symmetric for single-table queries', async () => {\n    let db = createDatabase(\n      createAdapter({\n        accounts: [\n          { id: 1, email: 'a@example.com', status: 'active' },\n          { id: 2, email: 'b@example.com', status: 'inactive' },\n        ],\n        projects: [{ id: 100, account_id: 1, archived: false }],\n      }),\n    )\n\n    let updated = await db.update(\n      accounts,\n      1,\n      { status: 'inactive' },\n      { with: { projects: accountProjects } },\n    )\n    let updateManyResult = await db.updateMany(\n      accounts,\n      { status: 'active' },\n      {\n        where: { status: 'inactive' },\n        orderBy: ['id', 'asc'],\n        limit: 1,\n      },\n    )\n    let deleted = await db.delete(accounts, 2)\n    let deleteManyResult = await db.deleteMany(accounts, {\n      where: eq('status', 'active'),\n      orderBy: [['id', 'desc']],\n      limit: 1,\n    })\n\n    assert.equal(updated.id, 1)\n    assert.equal(updated.projects.length, 1)\n    assert.equal(updateManyResult.affectedRows, 1)\n    assert.equal(deleted, true)\n    assert.equal(deleteManyResult.affectedRows, 1)\n\n    function verifyTypeErrors(): void {\n      // @ts-expect-error unknown update key\n      db.update(accounts, 1, { not_a_column: 'x' })\n      // @ts-expect-error unknown where key\n      db.updateMany(accounts, { status: 'active' }, { where: { not_a_column: 'x' } })\n      db.updateMany(\n        accounts,\n        { status: 'active' },\n        {\n          where: { status: 'active' },\n          // @ts-expect-error unknown orderBy key\n          orderBy: ['nope', 'asc'],\n        },\n      )\n      // @ts-expect-error unknown where key\n      db.deleteMany(accounts, { where: { not_a_column: 'x' } })\n      // @ts-expect-error unknown orderBy key\n      db.deleteMany(accounts, { where: { status: 'active' }, orderBy: [['nope', 'asc']] })\n    }\n\n    void verifyTypeErrors\n  })\n\n  it('supports typed create/createMany helper return modes', async () => {\n    let db = createDatabase(\n      createAdapter({\n        accounts: [{ id: 1, email: 'a@example.com', status: 'active' }],\n        projects: [\n          { id: 100, account_id: 1, archived: false },\n          { id: 101, account_id: 3, archived: false },\n        ],\n      }),\n    )\n\n    let createResult = await db.create(accounts, {\n      id: 2,\n      email: 'b@example.com',\n      status: 'active',\n    })\n    let created = await db.create(\n      accounts,\n      {\n        id: 3,\n        email: 'c@example.com',\n        status: 'inactive',\n      },\n      {\n        returnRow: true,\n        with: { projects: accountProjects },\n      },\n    )\n    let createManyResult = await db.createMany(accounts, [\n      { id: 4, email: 'd@example.com', status: 'active' },\n      { id: 5, email: 'e@example.com', status: 'inactive' },\n    ])\n    let createdRows = await db.createMany(\n      accounts,\n      [{ id: 6, email: 'f@example.com', status: 'active' }],\n      { returnRows: true },\n    )\n\n    expectType<Equal<typeof createResult, WriteResult>>()\n    expectType<Equal<typeof createManyResult, WriteResult>>()\n    expectType<Equal<(typeof created)['id'], number>>()\n    expectType<Equal<(typeof created)['projects'][number]['id'], number>>()\n    expectType<Equal<(typeof createdRows)[number]['id'], number>>()\n    expectType<Equal<(typeof createdRows)[number]['email'], string>>()\n\n    assert.equal(createResult.affectedRows, 1)\n    assert.equal(created.id, 3)\n    assert.equal(created.projects.length, 1)\n    assert.equal(createManyResult.affectedRows, 2)\n    assert.equal(createdRows.length, 1)\n\n    function verifyTypeErrors(): void {\n      // @ts-expect-error unknown insert key\n      db.create(accounts, { not_a_column: 'x' })\n      db.create(\n        accounts,\n        { id: 7, email: 'g@example.com', status: 'active' },\n        // @ts-expect-error with is only supported when returnRow: true\n        { with: { projects: accountProjects } },\n      )\n      // @ts-expect-error unknown createMany insert key\n      db.createMany(accounts, [{ not_a_column: 'x' }])\n      db.createMany(\n        accounts,\n        [{ id: 8, email: 'h@example.com', status: 'active' }],\n        // @ts-expect-error invalid createMany option key\n        { returnRow: true },\n      )\n    }\n\n    void verifyTypeErrors\n  })\n})\n\nfunction createAdapter(seed: SqliteTestSeed = {}) {\n  let { adapter, close } = createSqliteTestAdapter(seed)\n  cleanups.add(close)\n  return adapter\n}\n"
  },
  {
    "path": "packages/data-table/src/lib/types.ts",
    "content": "export type Pretty<value> = {\n  [key in keyof value]: value[key]\n} & {}\n"
  },
  {
    "path": "packages/data-table/src/migrations/node.ts",
    "content": "export { loadMigrations } from '../lib/migrations-node.ts'\n"
  },
  {
    "path": "packages/data-table/src/migrations.ts",
    "content": "export type {\n  AlterTableBuilder,\n  CreateMigrationInput,\n  Migration,\n  MigrationContext,\n  MigrationDescriptor,\n  MigrationDirection,\n  MigrationJournalRow,\n  MigrationSchema,\n  MigrationRegistry,\n  MigrationRunner,\n  MigrationRunnerOptions,\n  MigrationStatus,\n  MigrationStatusEntry,\n  MigrationTransactionMode,\n  MigrateOptions,\n  MigrateResult,\n  KeyColumns,\n  TableInput,\n} from './lib/migrations.ts'\nexport { createMigration } from './lib/migrations.ts'\nexport type { ColumnNamespace } from './lib/column.ts'\nexport { ColumnBuilder, column } from './lib/column.ts'\nexport { createMigrationRegistry } from './lib/migrations/registry.ts'\nexport { createMigrationRunner } from './lib/migrations/runner.ts'\nexport { parseMigrationFilename } from './lib/migrations/filename.ts'\n"
  },
  {
    "path": "packages/data-table/src/operators.ts",
    "content": "export type { Predicate, WhereInput, WhereObject } from './lib/operators.ts'\nexport {\n  and,\n  between,\n  eq,\n  gt,\n  gte,\n  ilike,\n  inList,\n  isNull,\n  like,\n  lt,\n  lte,\n  ne,\n  notInList,\n  notNull,\n  or,\n} from './lib/operators.ts'\n"
  },
  {
    "path": "packages/data-table/src/sql-helpers.ts",
    "content": "export type { QuoteIdentifier } from './lib/sql-helpers.ts'\nexport {\n  collectColumns,\n  isDataManipulationOperation,\n  normalizeJoinType,\n  quoteLiteral,\n  quotePath,\n  quoteTableRef,\n} from './lib/sql-helpers.ts'\n"
  },
  {
    "path": "packages/data-table/test/adapter-integration-contract.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { beforeEach, it } from 'node:test'\n\nimport { column } from '../src/lib/column.ts'\nimport type { Database } from '../src/lib/database.ts'\nimport { createMigration } from '../src/lib/migrations.ts'\nimport { createMigrationRunner } from '../src/lib/migrations/runner.ts'\nimport { table, hasMany, hasManyThrough } from '../src/lib/table.ts'\nimport { between, eq, ilike, inList, ne } from '../src/lib/operators.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n    email: column.text(),\n    status: column.text(),\n    nickname: column.text().nullable(),\n  },\n})\n\nlet projects = table({\n  name: 'projects',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n    name: column.text(),\n    archived: column.boolean(),\n  },\n})\n\nlet tasks = table({\n  name: 'tasks',\n  columns: {\n    id: column.integer(),\n    project_id: column.integer(),\n    title: column.text(),\n    state: column.text(),\n  },\n})\n\nlet accountProjects = hasMany(accounts, projects)\nlet accountTasks = hasManyThrough(accounts, tasks, {\n  through: accountProjects,\n})\n\nexport type IntegrationContractOptions = {\n  integrationEnabled: boolean\n  createDatabase: () => Database\n  resetDatabase: () => Promise<void>\n}\n\nexport function runAdapterIntegrationContract(options: IntegrationContractOptions): void {\n  beforeEach(async () => {\n    if (!options.integrationEnabled) {\n      return\n    }\n\n    await options.resetDatabase()\n  })\n\n  it(\n    'supports joined alias select with groupBy/having',\n    { skip: !options.integrationEnabled },\n    async function () {\n      let db = options.createDatabase()\n\n      await db.query(accounts).insertMany([\n        {\n          id: 1,\n          email: 'a@example.com',\n          status: 'active',\n          nickname: null,\n        },\n        {\n          id: 2,\n          email: 'b@example.com',\n          status: 'inactive',\n          nickname: 'bee',\n        },\n      ])\n      await db.query(projects).insertMany([\n        {\n          id: 100,\n          account_id: 1,\n          name: 'Alpha',\n          archived: false,\n        },\n        {\n          id: 101,\n          account_id: 1,\n          name: 'Beta',\n          archived: true,\n        },\n        {\n          id: 200,\n          account_id: 2,\n          name: 'Gamma',\n          archived: false,\n        },\n      ])\n\n      let joined = await db\n        .query(accounts)\n        .join(projects, eq('accounts.id', 'projects.account_id'))\n        .where(eq('projects.archived', false))\n        .select({\n          accountId: 'accounts.id',\n          accountEmail: 'accounts.email',\n          projectId: 'projects.id',\n          projectName: 'projects.name',\n        })\n        .orderBy('projects.id', 'asc')\n        .all()\n\n      assert.deepEqual(joined, [\n        {\n          accountId: 1,\n          accountEmail: 'a@example.com',\n          projectId: 100,\n          projectName: 'Alpha',\n        },\n        {\n          accountId: 2,\n          accountEmail: 'b@example.com',\n          projectId: 200,\n          projectName: 'Gamma',\n        },\n      ])\n\n      let groupedCount = await db\n        .query(accounts)\n        .join(projects, eq('accounts.id', 'projects.account_id'))\n        .where(eq('projects.archived', false))\n        .groupBy('accounts.id')\n        .having(eq('accounts.id', 1))\n        .count()\n\n      assert.equal(groupedCount, 1)\n    },\n  )\n\n  it(\n    'supports eager relations with per-parent relation pagination',\n    { skip: !options.integrationEnabled },\n    async function () {\n      let db = options.createDatabase()\n\n      await db.query(accounts).insertMany([\n        { id: 1, email: 'a@example.com', status: 'active', nickname: null },\n        { id: 2, email: 'b@example.com', status: 'active', nickname: null },\n      ])\n      await db.query(projects).insertMany([\n        { id: 100, account_id: 1, name: 'A-1', archived: false },\n        { id: 101, account_id: 1, name: 'A-2', archived: false },\n        { id: 200, account_id: 2, name: 'B-1', archived: false },\n        { id: 201, account_id: 2, name: 'B-2', archived: false },\n      ])\n      await db.query(tasks).insertMany([\n        { id: 1000, project_id: 100, title: 'A1-T1', state: 'open' },\n        { id: 1001, project_id: 101, title: 'A2-T1', state: 'open' },\n        { id: 2000, project_id: 200, title: 'B1-T1', state: 'open' },\n        { id: 2001, project_id: 201, title: 'B2-T1', state: 'open' },\n      ])\n\n      let accountRows = await db\n        .query(accounts)\n        .orderBy('id', 'asc')\n        .with({\n          projects: accountProjects.orderBy('id', 'asc').limit(1),\n          tasks: accountTasks.orderBy('id', 'asc').limit(1),\n        })\n        .all()\n\n      assert.equal(accountRows.length, 2)\n      assert.equal(accountRows[0].projects.length, 1)\n      assert.equal(accountRows[0].projects[0].id, 100)\n      assert.equal(accountRows[1].projects.length, 1)\n      assert.equal(accountRows[1].projects[0].id, 200)\n      assert.equal(accountRows[0].tasks.length, 1)\n      assert.equal(accountRows[0].tasks[0].id, 1000)\n      assert.equal(accountRows[1].tasks.length, 1)\n      assert.equal(accountRows[1].tasks[0].id, 2000)\n    },\n  )\n\n  it(\n    'scopes update/delete writes with orderBy and limit',\n    { skip: !options.integrationEnabled },\n    async function () {\n      let db = options.createDatabase()\n\n      await db.query(accounts).insertMany([\n        { id: 1, email: 'a@example.com', status: 'active', nickname: null },\n        { id: 2, email: 'b@example.com', status: 'active', nickname: null },\n        { id: 3, email: 'c@example.com', status: 'active', nickname: null },\n      ])\n\n      await db\n        .query(accounts)\n        .where({ status: 'active' })\n        .orderBy('id', 'asc')\n        .limit(1)\n        .update({ status: 'paused' })\n\n      await db.query(accounts).where({ status: 'active' }).orderBy('id', 'desc').limit(1).delete()\n\n      let rows = await db.query(accounts).orderBy('id', 'asc').all()\n\n      assert.deepEqual(\n        rows.map((row) => ({ id: row.id, status: row.status })),\n        [\n          { id: 1, status: 'paused' },\n          { id: 2, status: 'active' },\n        ],\n      )\n    },\n  )\n\n  it(\n    'supports transactions and nested savepoints',\n    { skip: !options.integrationEnabled },\n    async function () {\n      let db = options.createDatabase()\n\n      await db.query(accounts).insert({\n        id: 1,\n        email: 'a@example.com',\n        status: 'active',\n        nickname: null,\n      })\n\n      await db.transaction(async (outerTransaction) => {\n        await outerTransaction.query(accounts).insert({\n          id: 2,\n          email: 'b@example.com',\n          status: 'active',\n          nickname: null,\n        })\n\n        await outerTransaction\n          .transaction(async (innerTransaction) => {\n            await innerTransaction.query(accounts).insert({\n              id: 3,\n              email: 'c@example.com',\n              status: 'active',\n              nickname: null,\n            })\n\n            throw new Error('rollback inner')\n          })\n          .catch(() => undefined)\n      })\n\n      await db\n        .transaction(async (transactionDatabase) => {\n          await transactionDatabase.query(accounts).insert({\n            id: 4,\n            email: 'd@example.com',\n            status: 'active',\n            nickname: null,\n          })\n\n          throw new Error('rollback outer')\n        })\n        .catch(() => undefined)\n\n      let rows = await db.query(accounts).orderBy('id', 'asc').all()\n\n      assert.deepEqual(\n        rows.map((row) => row.id),\n        [1, 2],\n      )\n    },\n  )\n\n  it(\n    'supports upsert with conflict target',\n    { skip: !options.integrationEnabled },\n    async function () {\n      let db = options.createDatabase()\n\n      await db.query(accounts).insert({\n        id: 1,\n        email: 'a@example.com',\n        status: 'active',\n        nickname: null,\n      })\n\n      await db.query(accounts).upsert(\n        {\n          id: 1,\n          email: 'a@example.com',\n          status: 'inactive',\n          nickname: 'alpha',\n        },\n        { conflictTarget: ['id'] },\n      )\n      await db.query(accounts).upsert(\n        {\n          id: 2,\n          email: 'b@example.com',\n          status: 'active',\n          nickname: null,\n        },\n        { conflictTarget: ['id'] },\n      )\n\n      let rows = await db.query(accounts).orderBy('id', 'asc').all()\n\n      assert.equal(rows.length, 2)\n      assert.equal(rows[0].status, 'inactive')\n      assert.equal(rows[0].nickname, 'alpha')\n      assert.equal(rows[1].id, 2)\n    },\n  )\n\n  it(\n    'supports null and value operators in real queries',\n    { skip: !options.integrationEnabled },\n    async function () {\n      let db = options.createDatabase()\n\n      await db.query(accounts).insertMany([\n        {\n          id: 1,\n          email: 'A@Example.com',\n          status: 'active',\n          nickname: null,\n        },\n        {\n          id: 2,\n          email: 'b@example.com',\n          status: 'inactive',\n          nickname: 'bee',\n        },\n        {\n          id: 3,\n          email: 'c@example.com',\n          status: 'active',\n          nickname: null,\n        },\n      ])\n\n      let nullNickname = await db\n        .query(accounts)\n        .where(eq('nickname', null))\n        .orderBy('id', 'asc')\n        .all()\n      let nonNullNickname = await db.query(accounts).where(ne('nickname', null)).all()\n      let inRows = await db\n        .query(accounts)\n        .where(inList('id', [1, 3]))\n        .orderBy('id', 'asc')\n        .all()\n      let betweenRows = await db\n        .query(accounts)\n        .where(between('id', 2, 3))\n        .orderBy('id', 'asc')\n        .all()\n      let ilikeRows = await db\n        .query(accounts)\n        .where(ilike('email', '%@example.com'))\n        .orderBy('id', 'asc')\n        .all()\n\n      assert.deepEqual(\n        nullNickname.map((row) => row.id),\n        [1, 3],\n      )\n      assert.equal(nonNullNickname.length, 1)\n      assert.equal(nonNullNickname[0].id, 2)\n      assert.deepEqual(\n        inRows.map((row) => row.id),\n        [1, 3],\n      )\n      assert.deepEqual(\n        betweenRows.map((row) => row.id),\n        [2, 3],\n      )\n      assert.deepEqual(\n        ilikeRows.map((row) => row.id),\n        [1, 2, 3],\n      )\n    },\n  )\n\n  it(\n    'supports migration up/down with lifecycle hooks and returned reads',\n    { skip: !options.integrationEnabled },\n    async function () {\n      let db = options.createDatabase()\n      let lifecycleEvents: string[] = []\n      let lifecycleAccounts = table({\n        name: 'lifecycle_accounts',\n        columns: {\n          id: column.integer(),\n          email: column.text(),\n          status: column.text(),\n        },\n        beforeWrite({ value }) {\n          lifecycleEvents.push('beforeWrite')\n          return {\n            value: {\n              ...value,\n              email:\n                typeof value.email === 'string' ? value.email.trim().toLowerCase() : value.email,\n              status:\n                value.status === undefined\n                  ? 'active'\n                  : typeof value.status === 'string'\n                    ? value.status.trim().toLowerCase()\n                    : value.status,\n            },\n          }\n        },\n        validate({ value }) {\n          lifecycleEvents.push('validate')\n          if (typeof value.email !== 'string' || !value.email.includes('@')) {\n            return { issues: [{ message: 'Expected valid email', path: ['email'] }] }\n          }\n          return { value }\n        },\n        afterWrite({ operation, affectedRows }) {\n          lifecycleEvents.push('afterWrite:' + operation + ':' + String(affectedRows))\n        },\n        afterRead({ value }) {\n          if (typeof value.email !== 'string') {\n            return { value }\n          }\n          return {\n            value: {\n              ...value,\n              email: value.email.toUpperCase(),\n            },\n          }\n        },\n      })\n      let migration = createMigration({\n        async up({ schema }) {\n          await schema.createTable(lifecycleAccounts, { ifNotExists: true })\n        },\n        async down({ schema }) {\n          await schema.dropTable('lifecycle_accounts', { ifExists: true })\n        },\n      })\n      let runner = createMigrationRunner(\n        db.adapter,\n        [{ id: '20260228001000', name: 'create_lifecycle_accounts', migration }],\n        { journalTable: 'adapter_contract_migrations' },\n      )\n\n      await runner.up()\n\n      let created = await db.create(\n        lifecycleAccounts,\n        {\n          id: 1,\n          email: '  User@Example.com  ',\n        },\n        { returnRow: true },\n      )\n      let loaded = await db.find(lifecycleAccounts, 1)\n      let statusAfterUp = await runner.status()\n\n      assert.equal(created.email, 'USER@EXAMPLE.COM')\n      assert.equal(created.status, 'active')\n      assert.equal(loaded?.email, 'USER@EXAMPLE.COM')\n      assert.deepEqual(lifecycleEvents, ['beforeWrite', 'validate', 'afterWrite:create:1'])\n      assert.equal(statusAfterUp.length, 1)\n      assert.equal(statusAfterUp[0].status, 'applied')\n\n      await runner.down({ step: 1 })\n\n      let statusAfterDown = await runner.status()\n      assert.equal(statusAfterDown.length, 1)\n      assert.equal(statusAfterDown[0].status, 'pending')\n    },\n  )\n}\n"
  },
  {
    "path": "packages/data-table/test/adapter-integration-schema.ts",
    "content": "export type AdapterIntegrationDialect = 'mysql' | 'postgres' | 'sqlite'\n\nexport type AdapterIntegrationStatementRunner = (statement: string) => Promise<void>\n\ntype AdapterIntegrationSchemaStatements = {\n  drop: string[]\n  create: string[]\n  reset: string[]\n}\n\nlet mysqlSchemaStatements: AdapterIntegrationSchemaStatements = {\n  drop: [\n    'drop table if exists tasks',\n    'drop table if exists projects',\n    'drop table if exists accounts',\n  ],\n  create: [\n    [\n      'create table accounts (',\n      '  id int primary key,',\n      '  email varchar(255) not null,',\n      '  status varchar(32) not null,',\n      '  nickname varchar(255) null',\n      ')',\n    ].join('\\n'),\n    [\n      'create table projects (',\n      '  id int primary key,',\n      '  account_id int not null,',\n      '  name varchar(255) not null,',\n      '  archived boolean not null',\n      ')',\n    ].join('\\n'),\n    [\n      'create table tasks (',\n      '  id int primary key,',\n      '  project_id int not null,',\n      '  title varchar(255) not null,',\n      '  state varchar(32) not null',\n      ')',\n    ].join('\\n'),\n  ],\n  reset: ['delete from tasks', 'delete from projects', 'delete from accounts'],\n}\n\nlet postgresSchemaStatements: AdapterIntegrationSchemaStatements = {\n  drop: [\n    'drop table if exists tasks',\n    'drop table if exists projects',\n    'drop table if exists accounts',\n  ],\n  create: [\n    [\n      'create table accounts (',\n      '  id integer primary key,',\n      '  email text not null,',\n      '  status text not null,',\n      '  nickname text',\n      ')',\n    ].join('\\n'),\n    [\n      'create table projects (',\n      '  id integer primary key,',\n      '  account_id integer not null,',\n      '  name text not null,',\n      '  archived boolean not null',\n      ')',\n    ].join('\\n'),\n    [\n      'create table tasks (',\n      '  id integer primary key,',\n      '  project_id integer not null,',\n      '  title text not null,',\n      '  state text not null',\n      ')',\n    ].join('\\n'),\n  ],\n  reset: ['delete from tasks', 'delete from projects', 'delete from accounts'],\n}\n\nlet sqliteSchemaStatements: AdapterIntegrationSchemaStatements = {\n  drop: [\n    'drop table if exists tasks',\n    'drop table if exists projects',\n    'drop table if exists accounts',\n  ],\n  create: [\n    [\n      'create table accounts (',\n      '  id integer primary key,',\n      '  email text not null,',\n      '  status text not null,',\n      '  nickname text',\n      ')',\n    ].join('\\n'),\n    [\n      'create table projects (',\n      '  id integer primary key,',\n      '  account_id integer not null,',\n      '  name text not null,',\n      '  archived boolean not null',\n      ')',\n    ].join('\\n'),\n    [\n      'create table tasks (',\n      '  id integer primary key,',\n      '  project_id integer not null,',\n      '  title text not null,',\n      '  state text not null',\n      ')',\n    ].join('\\n'),\n  ],\n  reset: ['delete from tasks', 'delete from projects', 'delete from accounts'],\n}\n\nexport async function setupAdapterIntegrationSchema(\n  runStatement: AdapterIntegrationStatementRunner,\n  dialect: AdapterIntegrationDialect,\n): Promise<void> {\n  let statements = getAdapterIntegrationSchemaStatements(dialect)\n  await runStatements(runStatement, [...statements.drop, ...statements.create])\n}\n\nexport async function teardownAdapterIntegrationSchema(\n  runStatement: AdapterIntegrationStatementRunner,\n  dialect: AdapterIntegrationDialect,\n): Promise<void> {\n  let statements = getAdapterIntegrationSchemaStatements(dialect)\n  await runStatements(runStatement, statements.drop)\n}\n\nexport async function resetAdapterIntegrationSchema(\n  runStatement: AdapterIntegrationStatementRunner,\n  dialect: AdapterIntegrationDialect,\n): Promise<void> {\n  let statements = getAdapterIntegrationSchemaStatements(dialect)\n  await runStatements(runStatement, statements.reset)\n}\n\nfunction getAdapterIntegrationSchemaStatements(\n  dialect: AdapterIntegrationDialect,\n): AdapterIntegrationSchemaStatements {\n  if (dialect === 'mysql') {\n    return mysqlSchemaStatements\n  }\n\n  if (dialect === 'postgres') {\n    return postgresSchemaStatements\n  }\n\n  return sqliteSchemaStatements\n}\n\nasync function runStatements(\n  runStatement: AdapterIntegrationStatementRunner,\n  statements: string[],\n): Promise<void> {\n  for (let statement of statements) {\n    await runStatement(statement)\n  }\n}\n"
  },
  {
    "path": "packages/data-table/test/sqlite-adapter.ts",
    "content": "// Test-only bridge to the sqlite adapter source package.\n// We intentionally avoid a `@remix-run/data-table-sqlite` devDependency here because it creates\n// a cyclic workspace dependency warning in pnpm (`data-table` <-> `data-table-sqlite`).\nexport type { SqliteDatabaseAdapterOptions } from '../../data-table-sqlite/src/lib/adapter.ts'\nexport { createSqliteDatabaseAdapter } from '../../data-table-sqlite/src/lib/adapter.ts'\n"
  },
  {
    "path": "packages/data-table/test/sqlite-test-database.ts",
    "content": "import BetterSqlite3, { type Database as BetterSqliteDatabase } from 'better-sqlite3'\n\nimport type { DatabaseAdapter } from '../src/lib/adapter.ts'\nimport type { SqliteDatabaseAdapterOptions } from './sqlite-adapter.ts'\nimport { createSqliteDatabaseAdapter } from './sqlite-adapter.ts'\n\nexport type SqliteTestSeed = Record<string, Array<Record<string, unknown>>>\n\nexport type SqliteTestAdapterOptions = {\n  returning?: boolean\n  savepoints?: boolean\n  upsert?: boolean\n}\n\nexport function createSqliteTestAdapter(\n  seed: SqliteTestSeed = {},\n  options?: SqliteTestAdapterOptions,\n): {\n  adapter: DatabaseAdapter\n  close(): void\n} {\n  let sqlite = new BetterSqlite3(':memory:')\n  initializeSchema(sqlite)\n  seedDatabase(sqlite, seed)\n\n  let adapterOptions: SqliteDatabaseAdapterOptions | undefined = options\n    ? {\n        capabilities: {\n          returning: options.returning,\n          savepoints: options.savepoints,\n          upsert: options.upsert,\n        },\n      }\n    : undefined\n\n  let adapter = createSqliteDatabaseAdapter(sqlite, adapterOptions)\n\n  return {\n    adapter,\n    close() {\n      sqlite.close()\n    },\n  }\n}\n\nfunction initializeSchema(database: BetterSqliteDatabase): void {\n  database.exec(\n    [\n      'create table accounts (',\n      '  id integer primary key,',\n      '  email text not null,',\n      '  status text not null,',\n      '  created_at text,',\n      '  updated_at text',\n      ')',\n    ].join('\\n'),\n  )\n  database.exec(\n    [\n      'create table projects (',\n      '  id integer primary key,',\n      '  account_id integer not null,',\n      '  name text,',\n      '  archived boolean not null',\n      ')',\n    ].join('\\n'),\n  )\n  database.exec(\n    [\n      'create table profiles (',\n      '  id integer primary key,',\n      '  account_id integer not null,',\n      '  display_name text not null',\n      ')',\n    ].join('\\n'),\n  )\n  database.exec(\n    [\n      'create table tasks (',\n      '  id integer primary key,',\n      '  project_id integer not null,',\n      '  title text not null,',\n      '  state text not null',\n      ')',\n    ].join('\\n'),\n  )\n  database.exec(\n    [\n      'create table memberships (',\n      '  organization_id integer not null,',\n      '  account_id integer not null,',\n      '  role text not null,',\n      '  primary key (organization_id, account_id)',\n      ')',\n    ].join('\\n'),\n  )\n\n  database.exec(`attach database '' as billing`)\n  database.exec(\n    [\n      'create table billing.invoices (',\n      '  id integer primary key,',\n      '  account_id integer not null,',\n      '  total integer not null',\n      ')',\n    ].join('\\n'),\n  )\n}\n\nfunction seedDatabase(database: BetterSqliteDatabase, seed: SqliteTestSeed): void {\n  for (let tableName in seed) {\n    if (!Object.prototype.hasOwnProperty.call(seed, tableName)) {\n      continue\n    }\n\n    let rows = seed[tableName]\n\n    if (!rows || rows.length === 0) {\n      continue\n    }\n\n    for (let row of rows) {\n      let columns = Object.keys(row)\n\n      if (columns.length === 0) {\n        continue\n      }\n\n      let placeholders = columns.map(() => '?').join(', ')\n      let values = columns.map((column) => normalizeValue(row[column]))\n      let statement = database.prepare(\n        'insert into ' +\n          quoteIdentifier(tableName) +\n          ' (' +\n          columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ') values (' +\n          placeholders +\n          ')',\n      )\n\n      statement.run(...values)\n    }\n  }\n}\n\nfunction normalizeValue(value: unknown): unknown {\n  if (typeof value === 'boolean') {\n    return value ? 1 : 0\n  }\n\n  if (value instanceof Date) {\n    return value.toISOString()\n  }\n\n  return value\n}\n\nfunction quoteIdentifier(value: string): string {\n  return '\"' + value.replace(/\"/g, '\"\"') + '\"'\n}\n"
  },
  {
    "path": "packages/data-table/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/data-table/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"types\": [\"node\"],\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"vendor\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/data-table-mysql/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/data-table-mysql/.changes/minor.ddl-migration-contract.md",
    "content": "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.\n\nNormal reads/writes continue through `execute(...)`, while migration/DDL work runs through `migrate(...)`.\n\nSQL compilation remains adapter-owned and can share helpers from `remix/data-table/sql-helpers`.\n"
  },
  {
    "path": "packages/data-table-mysql/.changes/minor.introspection-migration-transaction-tokens.md",
    "content": "Add transaction-aware migration introspection to the mysql adapter.\n\n`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.\n"
  },
  {
    "path": "packages/data-table-mysql/CHANGELOG.md",
    "content": "# `data-table-mysql` CHANGELOG\n\nThis 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/).\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release of `@remix-run/data-table-mysql`.\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`data-table@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.1.0)\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/data-table-mysql/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/data-table-mysql/README.md",
    "content": "# data-table-mysql\n\nMySQL adapter for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table).\nUse this package when you want `data-table` APIs backed by `mysql2`.\n\n## Features\n\n- **Native `mysql2` Integration**: Works with `mysql2/promise` `Pool` and `PoolConnection` instances\n- **Full `data-table` API Support**: Queries, relations, writes, and transactions\n- **Adapter-Owned Compiler**: SQL compilation lives in this adapter, with optional shared pure helpers from `data-table`\n- **Migration DDL Support**: Compiles and executes `DataMigrationOperation` operations for `remix/data-table/migrations`\n- **MySQL Capabilities Enabled By Default**:\n  - `returning: false`\n  - `savepoints: true`\n  - `upsert: true`\n  - `transactionalDdl: false`\n  - `migrationLock: true`\n\n## Installation\n\n```sh\nnpm i remix mysql2\n```\n\n## Usage\n\n```ts\nimport { createPool } from 'mysql2/promise'\nimport { createDatabase } from 'remix/data-table'\nimport { createMysqlDatabaseAdapter } from 'remix/data-table-mysql'\n\nlet pool = createPool(process.env.DATABASE_URL as string)\nlet db = createDatabase(createMysqlDatabaseAdapter(pool))\n```\n\nUse `db.query(...)`, relation loading, and transactions from `remix/data-table`.\nImport any driver-specific types you need directly from `mysql2/promise`.\n\n## Adapter Capabilities\n\n`data-table-mysql` reports this capability set by default:\n\n- `returning: false`\n- `savepoints: true`\n- `upsert: true`\n- `transactionalDdl: false`\n- `migrationLock: true`\n\n## Advanced Usage\n\n### Capability Overrides For Testing\n\nCapability overrides are mainly for tests where you want to force or disable specific behavior\nchecks. In production, keep defaults so adapter behavior matches MySQL behavior.\n\n```ts\nimport { createMysqlDatabaseAdapter } from 'remix/data-table-mysql'\n\nlet adapter = createMysqlDatabaseAdapter(pool, {\n  capabilities: {\n    upsert: false,\n  },\n})\n```\n\n### `returning` On MySQL\n\nMySQL does not natively support SQL `RETURNING`. In this adapter, using `returning` on write\noperations throws `DataTableQueryError`.\n\nUse write metadata (`affectedRows`, `insertId`) on MySQL, or switch adapters when returned rows\nare required.\n\n```ts\nimport { DataTableQueryError } from 'remix/data-table'\n\ntry {\n  await db\n    .query(Accounts)\n    .insert({ email: 'a@example.com', status: 'active' }, { returning: ['id'] })\n} catch (error) {\n  if (error instanceof DataTableQueryError) {\n    // insert() returning is not supported by this adapter\n  }\n}\n```\n\n## Related Packages\n\n- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API\n- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema parsing and validation\n- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL adapter\n- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite adapter\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/data-table-mysql/package.json",
    "content": "{\n  \"name\": \"@remix-run/data-table-mysql\",\n  \"version\": \"0.1.0\",\n  \"description\": \"MySQL adapter for remix/data-table\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/data-table-mysql\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/data-table-mysql#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/data-table\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"mysql2\": \"^3.15.3\"\n  },\n  \"dependencies\": {\n    \"@remix-run/data-table\": \"workspace:^\"\n  },\n  \"peerDependencies\": {\n    \"mysql2\": \"^3.15.3\"\n  },\n  \"peerDependenciesMeta\": {\n    \"mysql2\": {\n      \"optional\": true\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"test:coverage\": \"node --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-lines=90 --test-coverage-branches=90 --test-coverage-functions=90 --test ./src/lib/adapter.test.ts ./src/lib/sql-compiler.test.ts\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"remix\",\n    \"orm\",\n    \"mysql\",\n    \"database\",\n    \"sql\"\n  ]\n}\n"
  },
  {
    "path": "packages/data-table-mysql/src/index.ts",
    "content": "export type { MysqlDatabaseAdapterOptions } from './lib/adapter.ts'\nexport { createMysqlDatabaseAdapter, MysqlDatabaseAdapter } from './lib/adapter.ts'\n"
  },
  {
    "path": "packages/data-table-mysql/src/lib/adapter.integration.test.ts",
    "content": "import { after, before, describe } from 'node:test'\nimport { createDatabase } from '@remix-run/data-table'\nimport { createPool, type Pool } from 'mysql2/promise'\n\nimport {\n  resetAdapterIntegrationSchema,\n  setupAdapterIntegrationSchema,\n  teardownAdapterIntegrationSchema,\n} from '../../../data-table/test/adapter-integration-schema.ts'\nimport { runAdapterIntegrationContract } from '../../../data-table/test/adapter-integration-contract.ts'\n\nimport { createMysqlDatabaseAdapter } from './adapter.ts'\n\nlet integrationEnabled =\n  process.env.DATA_TABLE_INTEGRATION === '1' && typeof process.env.DATA_TABLE_MYSQL_URL === 'string'\n\ndescribe('mysql adapter integration', () => {\n  let pool: Pool\n\n  before(async () => {\n    if (!integrationEnabled) {\n      return\n    }\n\n    pool = createPool(process.env.DATA_TABLE_MYSQL_URL as string)\n    await setupAdapterIntegrationSchema(async (statement) => {\n      await pool.query(statement)\n    }, 'mysql')\n  })\n\n  after(async () => {\n    if (!integrationEnabled) {\n      return\n    }\n\n    await teardownAdapterIntegrationSchema(async (statement) => {\n      await pool.query(statement)\n    }, 'mysql')\n    await pool.end()\n  })\n\n  runAdapterIntegrationContract({\n    integrationEnabled,\n    createDatabase: () => createDatabase(createMysqlDatabaseAdapter(pool)),\n    resetDatabase: async () => {\n      await resetAdapterIntegrationSchema(async (statement) => {\n        await pool.query(statement)\n      }, 'mysql')\n    },\n  })\n})\n"
  },
  {
    "path": "packages/data-table-mysql/src/lib/adapter.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\nimport type { DataMigrationOperation } from '@remix-run/data-table'\nimport { column, createDatabase, table, eq, ilike, inList } from '@remix-run/data-table'\n\nimport { createMysqlDatabaseAdapter } from './adapter.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n    email: column.text(),\n  },\n})\n\nlet projects = table({\n  name: 'projects',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n    name: column.text(),\n  },\n})\n\nlet invoices = table({\n  name: 'billing.invoices',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n  },\n})\n\nlet accountProjects = table({\n  name: 'account_projects',\n  columns: {\n    account_id: column.integer(),\n    project_id: column.integer(),\n    email: column.text(),\n  },\n  primaryKey: ['account_id', 'project_id'],\n})\n\ndescribe('mysql adapter', () => {\n  it('applies explicit capability overrides', () => {\n    let adapter = createMysqlDatabaseAdapter(\n      {\n        async query() {\n          return [[], []]\n        },\n        async beginTransaction() {},\n        async commit() {},\n        async rollback() {},\n      } as never,\n      {\n        capabilities: {\n          returning: true,\n          savepoints: false,\n          upsert: false,\n          transactionalDdl: true,\n          migrationLock: false,\n        },\n      },\n    )\n\n    assert.deepEqual(adapter.capabilities, {\n      returning: true,\n      savepoints: false,\n      upsert: false,\n      transactionalDdl: true,\n      migrationLock: false,\n    })\n  })\n\n  it('checks table and column existence through adapter introspection hooks', async () => {\n    let statements: Array<{ text: string; values: unknown[] | undefined }> = []\n\n    let connection = {\n      async query(text: string, values?: unknown[]) {\n        statements.push({ text, values })\n        return [[{ exists: 1 }], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let adapter = createMysqlDatabaseAdapter(connection as never)\n    let hasTable = await adapter.hasTable({ schema: 'app', name: 'users' })\n    let hasColumn = await adapter.hasColumn({ name: 'users' }, 'email')\n\n    assert.equal(hasTable, true)\n    assert.equal(hasColumn, true)\n    assert.equal(\n      statements[0]?.text,\n      'select exists(select 1 from information_schema.tables where table_schema = ? and table_name = ?) as `exists`',\n    )\n    assert.deepEqual(statements[0]?.values, ['app', 'users'])\n    assert.equal(\n      statements[1]?.text,\n      'select exists(select 1 from information_schema.columns where table_schema = database() and table_name = ? and column_name = ?) as `exists`',\n    )\n    assert.deepEqual(statements[1]?.values, ['users', 'email'])\n  })\n\n  it('routes introspection through transaction connections when a token is provided', async () => {\n    let rootQueries = 0\n    let connectionStatements: string[] = []\n\n    let connection = {\n      async query(text: string) {\n        connectionStatements.push(text)\n        return [[{ exists: 1 }], []]\n      },\n      async beginTransaction() {\n        connectionStatements.push('begin')\n      },\n      async commit() {\n        connectionStatements.push('commit')\n      },\n      async rollback() {\n        connectionStatements.push('rollback')\n      },\n      release() {},\n    }\n\n    let pool = {\n      async query() {\n        rootQueries += 1\n        return [[], []]\n      },\n      async getConnection() {\n        return connection\n      },\n    }\n\n    let adapter = createMysqlDatabaseAdapter(pool as never)\n    let token = await adapter.beginTransaction()\n\n    await adapter.hasTable({ name: 'users' }, token)\n    await adapter.hasColumn({ name: 'users' }, 'email', token)\n    await adapter.commitTransaction(token)\n\n    assert.equal(rootQueries, 0)\n    assert.deepEqual(connectionStatements, [\n      'begin',\n      'select exists(select 1 from information_schema.tables where table_schema = database() and table_name = ?) as `exists`',\n      'select exists(select 1 from information_schema.columns where table_schema = database() and table_name = ? and column_name = ?) as `exists`',\n      'commit',\n    ])\n  })\n\n  it('short-circuits insertMany([]) and returns empty rows for returning queries', async () => {\n    let calls = 0\n\n    let connection = {\n      async query() {\n        calls += 1\n        return [{ affectedRows: 0, insertId: undefined }, []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let adapter = createMysqlDatabaseAdapter(connection as never)\n\n    let result = await adapter.execute({\n      operation: {\n        kind: 'insertMany',\n        table: accounts,\n        values: [],\n        returning: ['id'],\n      },\n      transaction: undefined,\n    })\n\n    assert.deepEqual(result, {\n      affectedRows: 0,\n      insertId: undefined,\n      rows: [],\n    })\n    assert.equal(calls, 0)\n  })\n\n  it('compiles ilike() with lower() and parses count results', async () => {\n    let statements: Array<{ text: string; values: unknown[] }> = []\n\n    let connection = {\n      async query(text: string, values: unknown[] = []) {\n        statements.push({ text, values })\n\n        return [[{ count: '3' }], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n\n    let count = await db.query(accounts).where(ilike('email', '%EXAMPLE%')).count()\n\n    assert.equal(count, 3)\n    assert.match(statements[0].text, /lower\\(`email`\\) like lower\\(\\?\\)/)\n    assert.deepEqual(statements[0].values, ['%EXAMPLE%'])\n  })\n\n  it('starts and commits transactions on pooled connections', async () => {\n    let lifecycle: string[] = []\n\n    let poolConnection = {\n      async query() {\n        return [{ affectedRows: 1, insertId: 1 }, []]\n      },\n      async beginTransaction() {\n        lifecycle.push('begin')\n      },\n      async commit() {\n        lifecycle.push('commit')\n      },\n      async rollback() {\n        lifecycle.push('rollback')\n      },\n      release() {\n        lifecycle.push('release')\n      },\n    }\n\n    let pool = {\n      async query() {\n        throw new Error('unexpected root query')\n      },\n      async getConnection() {\n        lifecycle.push('getConnection')\n        return poolConnection\n      },\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(pool as never))\n\n    await db.transaction(async (transactionDatabase) => {\n      await transactionDatabase.query(accounts).insert({ id: 1, email: 'a@example.com' })\n    })\n\n    assert.deepEqual(lifecycle, ['getConnection', 'begin', 'commit', 'release'])\n  })\n\n  it('supports pooled transactions when getConnection() omits release()', async () => {\n    let lifecycle: string[] = []\n\n    let poolConnection = {\n      async query() {\n        return [{ affectedRows: 1, insertId: 1 }, []]\n      },\n      async beginTransaction() {\n        lifecycle.push('begin')\n      },\n      async commit() {\n        lifecycle.push('commit')\n      },\n      async rollback() {\n        lifecycle.push('rollback')\n      },\n    }\n\n    let pool = {\n      async query() {\n        throw new Error('unexpected root query')\n      },\n      async getConnection() {\n        lifecycle.push('getConnection')\n        return poolConnection\n      },\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(pool as never))\n\n    await db.transaction(async (transactionDatabase) => {\n      await transactionDatabase.query(accounts).insert({ id: 1, email: 'a@example.com' })\n    })\n\n    assert.deepEqual(lifecycle, ['getConnection', 'begin', 'commit'])\n  })\n\n  it('applies transaction options when provided', async () => {\n    let lifecycle: string[] = []\n\n    let connection = {\n      async query(text: string) {\n        lifecycle.push(text)\n        return [{ affectedRows: 0, insertId: undefined }, []]\n      },\n      async beginTransaction() {\n        lifecycle.push('begin')\n      },\n      async commit() {\n        lifecycle.push('commit')\n      },\n      async rollback() {\n        lifecycle.push('rollback')\n      },\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n\n    await db.transaction(async () => undefined, {\n      isolationLevel: 'serializable',\n      readOnly: true,\n    })\n\n    assert.deepEqual(lifecycle, [\n      'set transaction isolation level serializable',\n      'set transaction read only',\n      'begin',\n      'commit',\n    ])\n  })\n\n  it('applies read write transaction mode when readOnly is false', async () => {\n    let lifecycle: string[] = []\n\n    let connection = {\n      async query(text: string) {\n        lifecycle.push(text)\n        return [{ affectedRows: 0, insertId: undefined }, []]\n      },\n      async beginTransaction() {\n        lifecycle.push('begin')\n      },\n      async commit() {\n        lifecycle.push('commit')\n      },\n      async rollback() {\n        lifecycle.push('rollback')\n      },\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n\n    await db.transaction(async () => undefined, { readOnly: false })\n\n    assert.deepEqual(lifecycle, ['set transaction read write', 'begin', 'commit'])\n  })\n\n  it('rolls back transactions and releases pooled connections', async () => {\n    let lifecycle: string[] = []\n\n    let poolConnection = {\n      async query() {\n        return [{ affectedRows: 0, insertId: undefined }, []]\n      },\n      async beginTransaction() {\n        lifecycle.push('begin')\n      },\n      async commit() {\n        lifecycle.push('commit')\n      },\n      async rollback() {\n        lifecycle.push('rollback')\n      },\n      release() {\n        lifecycle.push('release')\n      },\n    }\n\n    let pool = {\n      async query() {\n        throw new Error('unexpected root query')\n      },\n      async getConnection() {\n        lifecycle.push('getConnection')\n        return poolConnection\n      },\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(pool as never))\n\n    await assert.rejects(\n      () =>\n        db.transaction(async () => {\n          throw new Error('force rollback')\n        }),\n      /force rollback/,\n    )\n\n    assert.deepEqual(lifecycle, ['getConnection', 'begin', 'rollback', 'release'])\n  })\n\n  it('rolls back pooled transactions when getConnection() omits release()', async () => {\n    let lifecycle: string[] = []\n\n    let poolConnection = {\n      async query() {\n        return [{ affectedRows: 0, insertId: undefined }, []]\n      },\n      async beginTransaction() {\n        lifecycle.push('begin')\n      },\n      async commit() {\n        lifecycle.push('commit')\n      },\n      async rollback() {\n        lifecycle.push('rollback')\n      },\n    }\n\n    let pool = {\n      async query() {\n        throw new Error('unexpected root query')\n      },\n      async getConnection() {\n        lifecycle.push('getConnection')\n        return poolConnection\n      },\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(pool as never))\n\n    await assert.rejects(\n      () =>\n        db.transaction(async () => {\n          throw new Error('force rollback')\n        }),\n      /force rollback/,\n    )\n\n    assert.deepEqual(lifecycle, ['getConnection', 'begin', 'rollback'])\n  })\n\n  it('supports savepoint lifecycle and escapes savepoint names', async () => {\n    let statements: string[] = []\n\n    let connection = {\n      async query(text: string) {\n        statements.push(text)\n        return [{ affectedRows: 0, insertId: undefined }, []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let adapter = createMysqlDatabaseAdapter(connection as never)\n    let token = await adapter.beginTransaction()\n\n    await adapter.createSavepoint(token, 'sp`0')\n    await adapter.rollbackToSavepoint(token, 'sp`0')\n    await adapter.releaseSavepoint(token, 'sp`0')\n    await adapter.commitTransaction(token)\n\n    assert.deepEqual(statements, [\n      'savepoint `sp``0`',\n      'rollback to savepoint `sp``0`',\n      'release savepoint `sp``0`',\n    ])\n  })\n\n  it('throws for unknown transaction tokens', async () => {\n    let connection = {\n      async query() {\n        return [{ affectedRows: 0, insertId: undefined }, []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let adapter = createMysqlDatabaseAdapter(connection as never)\n\n    await assert.rejects(\n      () => adapter.commitTransaction({ id: 'tx_missing' }),\n      /Unknown transaction token: tx_missing/,\n    )\n    await assert.rejects(\n      () => adapter.rollbackTransaction({ id: 'tx_missing' }),\n      /Unknown transaction token: tx_missing/,\n    )\n    await assert.rejects(\n      () => adapter.createSavepoint({ id: 'tx_missing' }, 'sp'),\n      /Unknown transaction token: tx_missing/,\n    )\n  })\n\n  it('compiles column-to-column comparisons from string references', async () => {\n    let statements: Array<{ text: string; values: unknown[] }> = []\n\n    let connection = {\n      async query(text: string, values: unknown[] = []) {\n        statements.push({ text, values })\n        return [[{ count: '0' }], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n\n    await db\n      .query(accounts)\n      .join(projects, eq('accounts.id', 'projects.account_id'))\n      .where(eq('accounts.email', 'ops@example.com'))\n      .count()\n\n    assert.match(statements[0].text, /`accounts`\\.`id`\\s*=\\s*`projects`\\.`account_id`/)\n    assert.match(statements[0].text, /`accounts`\\.`email`\\s*=\\s*\\?/)\n    assert.deepEqual(statements[0].values, ['ops@example.com'])\n  })\n\n  it('compiles cross-schema table references in joins', async () => {\n    let statements: Array<{ text: string; values: unknown[] }> = []\n\n    let connection = {\n      async query(text: string, values: unknown[] = []) {\n        statements.push({ text, values })\n        return [[{ count: '0' }], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n\n    await db.query(invoices).join(accounts, eq(accounts.id, invoices.account_id)).count()\n\n    assert.match(statements[0].text, /from `billing`\\.`invoices`/)\n    assert.match(statements[0].text, /join `accounts`/)\n    assert.match(statements[0].text, /`accounts`\\.`id`\\s*=\\s*`billing`\\.`invoices`\\.`account_id`/)\n  })\n\n  it('treats dotted select aliases as single identifiers', async () => {\n    let statements: Array<{ text: string; values: unknown[] }> = []\n\n    let connection = {\n      async query(text: string, values: unknown[] = []) {\n        statements.push({ text, values })\n        return [[], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n\n    await db.query(accounts).select({ 'account.email': accounts.email }).all()\n\n    assert.match(statements[0].text, /as `account\\.email`/)\n  })\n\n  it('does not create dangling bind parameters for inList predicates', async () => {\n    let statements: Array<{ text: string; values: unknown[] }> = []\n\n    let connection = {\n      async query(text: string, values: unknown[] = []) {\n        statements.push({ text, values })\n        return [[{ count: '0' }], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n\n    await db\n      .query(accounts)\n      .where(inList('id', [1, 3]))\n      .count()\n\n    assert.match(statements[0].text, /`id`\\s+in\\s+\\(\\?,\\s*\\?\\)/)\n    assert.deepEqual(statements[0].values, [1, 3])\n  })\n\n  it('loads the inserted row for create({ returnRow: true }) without RETURNING support', async () => {\n    let statements: Array<{ text: string; values: unknown[] }> = []\n    let calls = 0\n\n    let connection = {\n      async query(text: string, values: unknown[] = []) {\n        statements.push({ text, values })\n        calls += 1\n\n        if (calls === 1) {\n          return [{ affectedRows: 1, insertId: 2 }, []]\n        }\n\n        if (calls === 2) {\n          return [[{ id: 2, email: 'fallback@example.com' }], []]\n        }\n\n        throw new Error('unexpected query call')\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n\n    let created = await db.create(\n      accounts,\n      {\n        email: 'fallback@example.com',\n      },\n      { returnRow: true },\n    )\n\n    assert.equal(created.id, 2)\n    assert.equal(created.email, 'fallback@example.com')\n    assert.equal(statements.length, 2)\n    assert.match(statements[0].text, /^insert into `accounts`/)\n    assert.match(statements[1].text, /^select \\* from `accounts`/)\n    assert.match(statements[1].text, /where \\(\\(`id` = \\?\\)\\)/)\n    assert.deepEqual(statements[1].values, [2])\n  })\n\n  it('normalizes bigint count rows', async () => {\n    let connection = {\n      async query() {\n        return [[{ count: 5n }], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n    let count = await db.query(accounts).count()\n\n    assert.equal(count, 5)\n  })\n\n  it('defaults write metadata when mysql returns a non-object header', async () => {\n    let connection = {\n      async query() {\n        return [0, []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n    let result = await db.query(accounts).insert({ id: 1, email: 'a@example.com' })\n\n    assert.equal(result.affectedRows, 0)\n    assert.equal(result.insertId, undefined)\n  })\n\n  it('does not expose insertId for composite primary keys', async () => {\n    let connection = {\n      async query() {\n        return [{ affectedRows: 1, insertId: 123 }, []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n    let result = await db.query(accountProjects).insert({\n      account_id: 1,\n      project_id: 2,\n      email: 'team@example.com',\n    })\n\n    assert.equal(result.affectedRows, 1)\n    assert.equal(result.insertId, undefined)\n  })\n\n  it('preserves numeric count values without coercion', async () => {\n    let connection = {\n      async query() {\n        return [[{ count: 7 }], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n    let count = await db.query(accounts).count()\n\n    assert.equal(count, 7)\n  })\n\n  it('does not expose insertId for non-insert writes', async () => {\n    let connection = {\n      async query() {\n        return [{ affectedRows: 1, insertId: 99 }, []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let db = createDatabase(createMysqlDatabaseAdapter(connection as never))\n    let result = await db.updateMany(\n      accounts,\n      { email: 'updated@example.com' },\n      { where: { id: 1 } },\n    )\n\n    assert.equal(result.affectedRows, 1)\n    assert.equal(result.insertId, undefined)\n  })\n\n  it('executes migrate operations and migration lock hooks', async () => {\n    let statements: Array<{ text: string; values: unknown[] }> = []\n\n    let connection = {\n      async query(text: string, values: unknown[] = []) {\n        statements.push({ text, values })\n        return [{ affectedRows: 0, insertId: undefined }, []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    }\n\n    let adapter = createMysqlDatabaseAdapter(connection as never)\n\n    let result = await adapter.migrate({\n      operation: {\n        kind: 'alterTable',\n        table: { name: 'accounts' },\n        changes: [{ kind: 'setTableComment', comment: \"owner's table\" }],\n      },\n    })\n\n    await adapter.acquireMigrationLock()\n    await adapter.releaseMigrationLock()\n\n    assert.equal(result.affectedOperations, 1)\n    assert.deepEqual(statements[0], {\n      text: \"alter table `accounts` comment = 'owner''s table'\",\n      values: [],\n    })\n    assert.deepEqual(statements[1], {\n      text: 'select get_lock(?, 60)',\n      values: ['data_table_migrations'],\n    })\n    assert.deepEqual(statements[2], {\n      text: 'select release_lock(?)',\n      values: ['data_table_migrations'],\n    })\n  })\n\n  it('compiles migration statements for rich create and alter table operations', () => {\n    let adapter = createMysqlDatabaseAdapter({\n      async query() {\n        return [[], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    } as never)\n\n    let createTableStatements = adapter.compileSql({\n      kind: 'createTable',\n      table: { name: 'users' },\n      ifNotExists: true,\n      columns: {\n        id: { type: 'integer', nullable: false, autoIncrement: true, primaryKey: true },\n        email: { type: 'varchar' },\n        display_name: { type: 'text', default: { kind: 'literal', value: \"o'hare\" } },\n        created_at: { type: 'timestamp', default: { kind: 'now' } },\n        updated_at: {\n          type: 'timestamp',\n          default: { kind: 'sql', expression: '(current_timestamp)' },\n        },\n        reviewed_at: {\n          type: 'timestamp',\n          default: { kind: 'literal', value: new Date('2026-01-01T00:00:00.000Z') },\n        },\n        optional_note: { type: 'text', default: { kind: 'literal', value: null } },\n        total: {\n          type: 'decimal',\n          precision: 10,\n          scale: 2,\n          default: { kind: 'literal', value: 3.5 },\n        },\n        fallback_total: { type: 'decimal' },\n        is_active: { type: 'boolean', default: { kind: 'literal', value: false }, unique: true },\n        public_id: { type: 'uuid' },\n        due_on: { type: 'date' },\n        starts_at: { type: 'time' },\n        payload: { type: 'json' },\n        blob_data: { type: 'binary' },\n        big_total: { type: 'bigint', unsigned: true, default: { kind: 'literal', value: 9n } },\n        status: { type: 'enum', enumValues: ['active', 'disabled'] },\n        fallback_status: { type: 'enum', enumValues: [] },\n        derived_score: { type: 'integer', computed: { expression: '(points + 1)', stored: false } },\n        account_id: {\n          type: 'integer',\n          references: {\n            table: { schema: 'app', name: 'accounts' },\n            columns: ['id'],\n            name: 'users_account_inline_fk',\n            onDelete: 'set null',\n            onUpdate: 'cascade',\n          },\n        },\n        guarded_value: {\n          type: 'integer',\n          checks: [{ expression: 'guarded_value > 0', name: 'users_guarded_value_check' }],\n        },\n        unknown_type: {\n          type: 'mystery' as any,\n        },\n      },\n      primaryKey: { name: 'users_pk', columns: ['id'] },\n      uniques: [\n        { name: 'users_email_unique', columns: ['email'] },\n        { name: 'users_status_unique', columns: ['status'] },\n      ],\n      checks: [\n        { name: 'users_id_check', expression: 'id > 0' },\n        { name: 'users_active_check', expression: 'is_active in (0, 1)' },\n      ],\n      foreignKeys: [\n        {\n          name: 'users_account_fk',\n          columns: ['account_id'],\n          references: { table: { name: 'accounts' }, columns: ['id'] },\n          onDelete: 'cascade',\n          onUpdate: 'restrict',\n        },\n      ],\n      comment: \"users' table\",\n    })\n\n    assert.equal(createTableStatements.length, 2)\n    assert.match(createTableStatements[0].text, /^create table if not exists `users`/)\n    assert.match(createTableStatements[0].text, /`email` varchar\\(255\\)/)\n    assert.match(createTableStatements[0].text, /`display_name` text default 'o''hare'/)\n    assert.match(\n      createTableStatements[0].text,\n      /`reviewed_at` timestamp default '2026-01-01T00:00:00\\.000Z'/,\n    )\n    assert.match(createTableStatements[0].text, /`optional_note` text default null/)\n    assert.match(createTableStatements[0].text, /`total` decimal\\(10, 2\\) default 3\\.5/)\n    assert.match(createTableStatements[0].text, /`fallback_total` decimal/)\n    assert.match(createTableStatements[0].text, /`is_active` boolean default false unique/)\n    assert.match(createTableStatements[0].text, /`public_id` char\\(36\\)/)\n    assert.match(createTableStatements[0].text, /`due_on` date/)\n    assert.match(createTableStatements[0].text, /`starts_at` time/)\n    assert.match(createTableStatements[0].text, /`payload` json/)\n    assert.match(createTableStatements[0].text, /`blob_data` blob/)\n    assert.match(createTableStatements[0].text, /`big_total` bigint unsigned default 9/)\n    assert.match(createTableStatements[0].text, /`status` enum\\('active', 'disabled'\\)/)\n    assert.match(createTableStatements[0].text, /`fallback_status` text/)\n    assert.match(\n      createTableStatements[0].text,\n      /`derived_score` int generated always as \\(\\(points \\+ 1\\)\\) virtual/,\n    )\n    assert.match(\n      createTableStatements[0].text,\n      /`account_id` int references `app`\\.`accounts` \\(`id`\\) on delete set null on update cascade/,\n    )\n    assert.match(createTableStatements[0].text, /`guarded_value` int check \\(guarded_value > 0\\)/)\n    assert.match(createTableStatements[0].text, /`unknown_type` text/)\n    assert.match(createTableStatements[0].text, /primary key \\(`id`\\)/)\n    assert.match(createTableStatements[0].text, /unique \\(`email`\\)/)\n    assert.match(\n      createTableStatements[0].text,\n      /constraint `users_status_unique` unique \\(`status`\\)/,\n    )\n    assert.match(createTableStatements[0].text, /check \\(id > 0\\)/)\n    assert.match(\n      createTableStatements[0].text,\n      /constraint `users_active_check` check \\(is_active in \\(0, 1\\)\\)/,\n    )\n    assert.match(\n      createTableStatements[0].text,\n      /constraint `users_account_fk` foreign key \\(`account_id`\\) references `accounts` \\(`id`\\) on delete cascade on update restrict/,\n    )\n    assert.deepEqual(createTableStatements[1], {\n      text: \"alter table `users` comment = 'users'' table'\",\n      values: [],\n    })\n\n    let alterTableStatements = adapter.compileSql({\n      kind: 'alterTable',\n      table: { schema: 'app', name: 'users' },\n      changes: [\n        { kind: 'addColumn', column: 'nickname', definition: { type: 'text' } },\n        {\n          kind: 'changeColumn',\n          column: 'nickname',\n          definition: {\n            type: 'text',\n            default: { kind: 'sql', expression: '(concat(first_name, last_name))' },\n            checks: [{ expression: 'char_length(nickname) > 1', name: 'users_nickname_len_check' }],\n          },\n        },\n        { kind: 'renameColumn', from: 'nickname', to: 'handle' },\n        { kind: 'dropColumn', column: 'legacy_handle' },\n        { kind: 'addPrimaryKey', constraint: { name: 'users_pk', columns: ['id'] } },\n        { kind: 'dropPrimaryKey', name: 'users_pk' },\n        { kind: 'addUnique', constraint: { columns: ['email'], name: 'users_email_unique' } },\n        { kind: 'dropUnique', name: 'users_email_unique' },\n        {\n          kind: 'addForeignKey',\n          constraint: {\n            columns: ['account_id'],\n            references: { table: { name: 'accounts' }, columns: ['id'] },\n            name: 'users_account_fk',\n          },\n        },\n        { kind: 'dropForeignKey', name: 'users_account_fk' },\n        {\n          kind: 'addCheck',\n          constraint: { expression: 'char_length(email) > 3', name: 'users_email_check' },\n        },\n        { kind: 'dropCheck', name: 'users_email_check' },\n        { kind: 'setTableComment', comment: \"owner's users\" },\n        { kind: 'somethingElse' as any },\n      ] as any,\n    })\n\n    assert.equal(alterTableStatements.length, 13)\n    assert.match(\n      alterTableStatements[1].text,\n      /alter table `app`\\.`users` modify column `nickname` text default \\(concat\\(first_name, last_name\\)\\) check \\(char_length\\(nickname\\) > 1\\)/,\n    )\n    assert.match(alterTableStatements[2].text, /rename column `nickname` to `handle`/)\n    assert.match(alterTableStatements[3].text, /drop column `legacy_handle`/)\n    assert.match(alterTableStatements[4].text, /add primary key \\(`id`\\)/)\n    assert.match(alterTableStatements[5].text, /drop primary key/)\n    assert.match(\n      alterTableStatements[6].text,\n      /add constraint `users_email_unique` unique \\(`email`\\)/,\n    )\n    assert.match(alterTableStatements[7].text, /drop index `users_email_unique`/)\n    assert.match(\n      alterTableStatements[8].text,\n      /add constraint `users_account_fk` foreign key \\(`account_id`\\) references `accounts` \\(`id`\\)/,\n    )\n    assert.match(alterTableStatements[9].text, /drop foreign key `users_account_fk`/)\n    assert.match(\n      alterTableStatements[10].text,\n      /add constraint `users_email_check` check \\(char_length\\(email\\) > 3\\)/,\n    )\n    assert.match(alterTableStatements[11].text, /drop check `users_email_check`/)\n    assert.equal(\n      alterTableStatements[12].text,\n      \"alter table `app`.`users` comment = 'owner''s users'\",\n    )\n\n    let createIndexWithName = adapter.compileSql({\n      kind: 'createIndex',\n      index: {\n        table: { name: 'users' },\n        name: 'email_idx',\n        columns: ['email'],\n      },\n    })\n\n    assert.equal(createIndexWithName.length, 1)\n    assert.match(createIndexWithName[0].text, /create index `email_idx` on `users`/)\n  })\n\n  it('throws for unsupported data migration operation kinds', () => {\n    let adapter = createMysqlDatabaseAdapter({\n      async query() {\n        return [[], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    } as never)\n\n    assert.throws(\n      () => adapter.compileSql({ kind: 'unsupported_migration_operation' } as any),\n      /Unsupported data migration operation kind/,\n    )\n  })\n\n  it('compiles every DDL operation kind through compileSql()', () => {\n    let adapter = createMysqlDatabaseAdapter({\n      async query() {\n        return [[], []]\n      },\n      async beginTransaction() {},\n      async commit() {},\n      async rollback() {},\n    } as never)\n\n    let operations: DataMigrationOperation[] = [\n      {\n        kind: 'createTable',\n        table: { schema: 'app', name: 'users' },\n        ifNotExists: true,\n        columns: {\n          id: { type: 'integer', nullable: false, primaryKey: true },\n        },\n      },\n      {\n        kind: 'alterTable',\n        table: { schema: 'app', name: 'users' },\n        changes: [\n          { kind: 'addColumn', column: 'email', definition: { type: 'text', nullable: false } },\n        ],\n      },\n      {\n        kind: 'renameTable',\n        from: { schema: 'app', name: 'users' },\n        to: { schema: 'app', name: 'accounts' },\n      },\n      { kind: 'dropTable', table: { schema: 'app', name: 'accounts' }, ifExists: true },\n      {\n        kind: 'createIndex',\n        index: {\n          table: { schema: 'app', name: 'users' },\n          columns: ['email'],\n          name: 'users_email_idx',\n        },\n      },\n      { kind: 'dropIndex', table: { schema: 'app', name: 'users' }, name: 'users_email_idx' },\n      {\n        kind: 'renameIndex',\n        table: { schema: 'app', name: 'users' },\n        from: 'users_email_idx',\n        to: 'users_email_idx_new',\n      },\n      {\n        kind: 'addForeignKey',\n        table: { schema: 'app', name: 'projects' },\n        constraint: {\n          columns: ['account_id'],\n          references: {\n            table: { schema: 'app', name: 'accounts' },\n            columns: ['id'],\n          },\n          name: 'projects_account_id_fk',\n          onDelete: 'cascade',\n        },\n      },\n      {\n        kind: 'dropForeignKey',\n        table: { schema: 'app', name: 'projects' },\n        name: 'projects_account_id_fk',\n      },\n      {\n        kind: 'addCheck',\n        table: { schema: 'app', name: 'users' },\n        constraint: {\n          name: 'users_email_check',\n          expression: \"email like '%@%'\",\n        },\n      },\n      { kind: 'dropCheck', table: { schema: 'app', name: 'users' }, name: 'users_email_check' },\n      { kind: 'raw', sql: { text: 'select 1', values: [] } },\n    ]\n\n    for (let operation of operations) {\n      let compiled = adapter.compileSql(operation)\n      assert.ok(compiled.length > 0, operation.kind)\n    }\n  })\n})\n"
  },
  {
    "path": "packages/data-table-mysql/src/lib/adapter.ts",
    "content": "import type {\n  AdapterCapabilityOverrides,\n  DataManipulationRequest,\n  DataMigrationRequest,\n  DataMigrationResult,\n  DataMigrationOperation,\n  DataManipulationResult,\n  DataManipulationOperation,\n  DatabaseAdapter,\n  ColumnDefinition,\n  SqlStatement,\n  TableRef,\n  TransactionOptions,\n  TransactionToken,\n} from '@remix-run/data-table'\nimport { getTablePrimaryKey } from '@remix-run/data-table'\nimport {\n  isDataManipulationOperation as isDataManipulationOperationHelper,\n  quoteLiteral as quoteLiteralHelper,\n  quoteTableRef as quoteTableRefHelper,\n} from '@remix-run/data-table/sql-helpers'\nimport type {\n  Connection as MysqlConnection,\n  Pool as MysqlPool,\n  PoolConnection as MysqlPoolConnection,\n  ResultSetHeader,\n  RowDataPacket,\n} from 'mysql2/promise'\n\nimport { compileMysqlOperation } from './sql-compiler.ts'\n\n/**\n * Mysql adapter configuration.\n */\nexport type MysqlDatabaseAdapterOptions = {\n  capabilities?: AdapterCapabilityOverrides\n}\n\ntype TransactionState = {\n  connection: MysqlTransactionConnection\n  releaseOnClose: boolean\n}\n\ntype MysqlQueryRows = RowDataPacket[]\ntype MysqlQueryResultHeader = {\n  affectedRows: number\n  insertId: unknown\n}\ntype MysqlTransactionConnection = MysqlConnection | MysqlPoolConnection\ntype MysqlQueryable = MysqlPool | MysqlTransactionConnection\n\n/**\n * `DatabaseAdapter` implementation for mysql-compatible clients.\n */\nexport class MysqlDatabaseAdapter implements DatabaseAdapter {\n  /**\n   * The SQL dialect identifier reported by this adapter.\n   */\n  dialect = 'mysql'\n\n  /**\n   * Feature flags describing the mysql behaviors supported by this adapter.\n   */\n  capabilities\n\n  #client: MysqlQueryable\n  #transactions = new Map<string, TransactionState>()\n  #transactionCounter = 0\n\n  constructor(client: MysqlQueryable, options?: MysqlDatabaseAdapterOptions) {\n    this.#client = client\n    this.capabilities = {\n      returning: options?.capabilities?.returning ?? false,\n      savepoints: options?.capabilities?.savepoints ?? true,\n      upsert: options?.capabilities?.upsert ?? true,\n      transactionalDdl: options?.capabilities?.transactionalDdl ?? false,\n      migrationLock: options?.capabilities?.migrationLock ?? true,\n    }\n  }\n\n  /**\n   * Compiles a data or migration operation to mysql SQL statements.\n   * @param operation Operation to compile.\n   * @returns Compiled SQL statements.\n   */\n  compileSql(operation: DataManipulationOperation | DataMigrationOperation): SqlStatement[] {\n    if (isDataManipulationOperation(operation)) {\n      let compiled = compileMysqlOperation(operation)\n      return [{ text: compiled.text, values: compiled.values }]\n    }\n\n    return compileMysqlMigrationOperations(operation)\n  }\n\n  /**\n   * Executes a mysql data-manipulation request.\n   * @param request Request to execute.\n   * @returns Execution result.\n   */\n  async execute(request: DataManipulationRequest): Promise<DataManipulationResult> {\n    if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {\n      return {\n        affectedRows: 0,\n        insertId: undefined,\n        rows: request.operation.returning ? [] : undefined,\n      }\n    }\n\n    let statements = this.compileSql(request.operation)\n    let statement = statements[0]\n    let client = this.#resolveClient(request.transaction)\n    let [result] = await client.query(statement.text, statement.values)\n\n    if (isRowsResult(result)) {\n      let rows = normalizeRows(result)\n\n      if (request.operation.kind === 'count' || request.operation.kind === 'exists') {\n        rows = normalizeCountRows(rows)\n      }\n\n      return { rows }\n    }\n\n    let header = normalizeHeader(result)\n\n    return {\n      affectedRows: header.affectedRows,\n      insertId: normalizeInsertId(request.operation.kind, request.operation, header),\n    }\n  }\n\n  /**\n   * Executes mysql migration operations.\n   * @param request Migration request to execute.\n   * @returns Migration result.\n   */\n  async migrate(request: DataMigrationRequest): Promise<DataMigrationResult> {\n    let statements = this.compileSql(request.operation)\n    let client = this.#resolveClient(request.transaction)\n\n    for (let statement of statements) {\n      await client.query(statement.text, statement.values)\n    }\n\n    return {\n      affectedOperations: statements.length,\n    }\n  }\n\n  /**\n   * Checks whether a table exists in mysql.\n   * @param table Table reference to inspect.\n   * @param transaction Optional transaction token.\n   * @returns `true` when the table exists.\n   */\n  async hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {\n    let schema = table.schema\n    let sql = schema\n      ? 'select exists(select 1 from information_schema.tables where table_schema = ? and table_name = ?) as `exists`'\n      : 'select exists(select 1 from information_schema.tables where table_schema = database() and table_name = ?) as `exists`'\n    let values = schema ? [schema, table.name] : [table.name]\n    let client = this.#resolveClient(transaction)\n    let [result] = await client.query(sql, values)\n\n    if (!isRowsResult(result)) {\n      return false\n    }\n\n    return toBooleanExists(result[0]?.exists)\n  }\n\n  /**\n   * Checks whether a column exists in mysql.\n   * @param table Table reference to inspect.\n   * @param column Column name to look up.\n   * @param transaction Optional transaction token.\n   * @returns `true` when the column exists.\n   */\n  async hasColumn(\n    table: TableRef,\n    column: string,\n    transaction?: TransactionToken,\n  ): Promise<boolean> {\n    let schema = table.schema\n    let sql = schema\n      ? 'select exists(select 1 from information_schema.columns where table_schema = ? and table_name = ? and column_name = ?) as `exists`'\n      : 'select exists(select 1 from information_schema.columns where table_schema = database() and table_name = ? and column_name = ?) as `exists`'\n    let values = schema ? [schema, table.name, column] : [table.name, column]\n    let client = this.#resolveClient(transaction)\n    let [result] = await client.query(sql, values)\n\n    if (!isRowsResult(result)) {\n      return false\n    }\n\n    return toBooleanExists(result[0]?.exists)\n  }\n\n  /**\n   * Starts a mysql transaction.\n   * @param options Transaction options.\n   * @returns Transaction token.\n   */\n  async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {\n    let releaseOnClose = false\n    let connection: MysqlTransactionConnection\n\n    if (isMysqlPool(this.#client)) {\n      connection = await this.#client.getConnection()\n      releaseOnClose = true\n    } else {\n      connection = this.#client\n    }\n\n    if (options?.isolationLevel) {\n      await connection.query('set transaction isolation level ' + options.isolationLevel)\n    }\n\n    if (options?.readOnly !== undefined) {\n      await connection.query(\n        options.readOnly ? 'set transaction read only' : 'set transaction read write',\n      )\n    }\n\n    await connection.beginTransaction()\n\n    this.#transactionCounter += 1\n    let token = { id: 'tx_' + String(this.#transactionCounter) }\n\n    this.#transactions.set(token.id, {\n      connection,\n      releaseOnClose,\n    })\n\n    return token\n  }\n\n  /**\n   * Commits an open mysql transaction.\n   * @param token Transaction token to commit.\n   * @returns A promise that resolves when the transaction is committed.\n   */\n  async commitTransaction(token: TransactionToken): Promise<void> {\n    let transaction = this.#transactions.get(token.id)\n\n    if (!transaction) {\n      throw new Error('Unknown transaction token: ' + token.id)\n    }\n\n    try {\n      await transaction.connection.commit()\n    } finally {\n      this.#transactions.delete(token.id)\n\n      if (transaction.releaseOnClose && isMysqlPoolConnection(transaction.connection)) {\n        transaction.connection.release()\n      }\n    }\n  }\n\n  /**\n   * Rolls back an open mysql transaction.\n   * @param token Transaction token to roll back.\n   * @returns A promise that resolves when the transaction is rolled back.\n   */\n  async rollbackTransaction(token: TransactionToken): Promise<void> {\n    let transaction = this.#transactions.get(token.id)\n\n    if (!transaction) {\n      throw new Error('Unknown transaction token: ' + token.id)\n    }\n\n    try {\n      await transaction.connection.rollback()\n    } finally {\n      this.#transactions.delete(token.id)\n\n      if (transaction.releaseOnClose && isMysqlPoolConnection(transaction.connection)) {\n        transaction.connection.release()\n      }\n    }\n  }\n\n  /**\n   * Creates a savepoint in an open mysql transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the savepoint is created.\n   */\n  async createSavepoint(token: TransactionToken, name: string): Promise<void> {\n    let connection = this.#transactionConnection(token)\n    await connection.query('savepoint ' + quoteIdentifier(name))\n  }\n\n  /**\n   * Rolls back to a savepoint in an open mysql transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the rollback completes.\n   */\n  async rollbackToSavepoint(token: TransactionToken, name: string): Promise<void> {\n    let connection = this.#transactionConnection(token)\n    await connection.query('rollback to savepoint ' + quoteIdentifier(name))\n  }\n\n  /**\n   * Releases a savepoint in an open mysql transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the savepoint is released.\n   */\n  async releaseSavepoint(token: TransactionToken, name: string): Promise<void> {\n    let connection = this.#transactionConnection(token)\n    await connection.query('release savepoint ' + quoteIdentifier(name))\n  }\n\n  /**\n   * Acquires the mysql migration lock.\n   * @returns A promise that resolves when the lock is acquired.\n   */\n  async acquireMigrationLock(): Promise<void> {\n    await this.#client.query('select get_lock(?, 60)', ['data_table_migrations'])\n  }\n\n  /**\n   * Releases the mysql migration lock.\n   * @returns A promise that resolves when the lock is released.\n   */\n  async releaseMigrationLock(): Promise<void> {\n    await this.#client.query('select release_lock(?)', ['data_table_migrations'])\n  }\n\n  #resolveClient(token: TransactionToken | undefined): MysqlQueryable {\n    if (!token) {\n      return this.#client\n    }\n\n    return this.#transactionConnection(token)\n  }\n\n  #transactionConnection(token: TransactionToken): MysqlTransactionConnection {\n    let transaction = this.#transactions.get(token.id)\n\n    if (!transaction) {\n      throw new Error('Unknown transaction token: ' + token.id)\n    }\n\n    return transaction.connection\n  }\n}\n\n/**\n * Creates a mysql `DatabaseAdapter`.\n * @param client Mysql pool or connection.\n * @param options Optional adapter capability overrides.\n * @returns A configured mysql adapter.\n * @example\n * ```ts\n * import { createPool } from 'mysql2/promise'\n * import { createDatabase } from 'remix/data-table'\n * import { createMysqlDatabaseAdapter } from 'remix/data-table-mysql'\n *\n * let pool = createPool({ uri: process.env.DATABASE_URL })\n * let adapter = createMysqlDatabaseAdapter(pool)\n * let db = createDatabase(adapter)\n * ```\n */\nexport function createMysqlDatabaseAdapter(\n  client: MysqlQueryable,\n  options?: MysqlDatabaseAdapterOptions,\n): MysqlDatabaseAdapter {\n  return new MysqlDatabaseAdapter(client, options)\n}\n\nfunction isMysqlPool(client: MysqlQueryable): client is MysqlPool {\n  return 'getConnection' in client && typeof client.getConnection === 'function'\n}\n\nfunction isMysqlPoolConnection(\n  connection: MysqlTransactionConnection,\n): connection is MysqlPoolConnection {\n  return 'release' in connection && typeof connection.release === 'function'\n}\n\nfunction isRowsResult(result: unknown): result is MysqlQueryRows {\n  return Array.isArray(result) && (result.length === 0 || !Array.isArray(result[0]))\n}\n\nfunction toBooleanExists(value: unknown): boolean {\n  if (typeof value === 'boolean') {\n    return value\n  }\n\n  if (typeof value === 'number') {\n    return value > 0\n  }\n\n  if (typeof value === 'bigint') {\n    return value > 0n\n  }\n\n  if (typeof value === 'string') {\n    return value === '1' || value.toLowerCase() === 'true'\n  }\n\n  return false\n}\n\nfunction normalizeRows(rows: MysqlQueryRows): Record<string, unknown>[] {\n  return rows.map((row) => ({ ...row }))\n}\n\nfunction normalizeHeader(result: unknown): MysqlQueryResultHeader {\n  if (typeof result === 'object' && result !== null) {\n    let header = result as Partial<ResultSetHeader>\n\n    return {\n      affectedRows: typeof header.affectedRows === 'number' ? header.affectedRows : 0,\n      insertId: header.insertId,\n    }\n  }\n\n  return {\n    affectedRows: 0,\n    insertId: undefined,\n  }\n}\n\nfunction normalizeCountRows(rows: Record<string, unknown>[]): Record<string, unknown>[] {\n  return rows.map((row) => {\n    let count = row.count\n\n    if (typeof count === 'string') {\n      let numeric = Number(count)\n\n      if (!Number.isNaN(numeric)) {\n        return {\n          ...row,\n          count: numeric,\n        }\n      }\n    }\n\n    if (typeof count === 'bigint') {\n      return {\n        ...row,\n        count: Number(count),\n      }\n    }\n\n    return row\n  })\n}\n\nfunction normalizeInsertId(\n  kind: DataManipulationRequest['operation']['kind'],\n  operation: DataManipulationRequest['operation'],\n  header: MysqlQueryResultHeader,\n): unknown {\n  if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {\n    return undefined\n  }\n\n  if (getTablePrimaryKey(operation.table).length !== 1) {\n    return undefined\n  }\n\n  return header.insertId\n}\n\nfunction quoteIdentifier(value: string): string {\n  return '`' + value.replace(/`/g, '``') + '`'\n}\n\nfunction quoteTableRef(table: TableRef): string {\n  return quoteTableRefHelper(table, quoteIdentifier)\n}\n\nfunction quoteLiteral(value: unknown): string {\n  return quoteLiteralHelper(value)\n}\n\nfunction isInsertOperationKind(kind: DataManipulationRequest['operation']['kind']): boolean {\n  return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'\n}\n\nfunction isInsertOperation(\n  operation: DataManipulationRequest['operation'],\n): operation is Extract<\n  DataManipulationRequest['operation'],\n  { kind: 'insert' | 'insertMany' | 'upsert' }\n> {\n  return (\n    operation.kind === 'insert' || operation.kind === 'insertMany' || operation.kind === 'upsert'\n  )\n}\n\nfunction isDataManipulationOperation(\n  operation: DataManipulationOperation | DataMigrationOperation,\n): operation is DataManipulationOperation {\n  return isDataManipulationOperationHelper(operation)\n}\n\nfunction compileMysqlMigrationOperations(operation: DataMigrationOperation): SqlStatement[] {\n  if (operation.kind === 'raw') {\n    return [{ text: operation.sql.text, values: [...operation.sql.values] }]\n  }\n\n  if (operation.kind === 'createTable') {\n    let columns = Object.keys(operation.columns).map(\n      (columnName) =>\n        quoteIdentifier(columnName) + ' ' + compileMysqlColumn(operation.columns[columnName]),\n    )\n    let constraints: string[] = []\n\n    if (operation.primaryKey) {\n      constraints.push(\n        'constraint ' +\n          quoteIdentifier(operation.primaryKey.name) +\n          ' primary key (' +\n          operation.primaryKey.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')',\n      )\n    }\n\n    for (let unique of operation.uniques ?? []) {\n      constraints.push(\n        'constraint ' +\n          quoteIdentifier(unique.name) +\n          ' ' +\n          'unique (' +\n          unique.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')',\n      )\n    }\n\n    for (let check of operation.checks ?? []) {\n      constraints.push(\n        'constraint ' + quoteIdentifier(check.name) + ' ' + 'check (' + check.expression + ')',\n      )\n    }\n\n    for (let foreignKey of operation.foreignKeys ?? []) {\n      let clause =\n        'constraint ' +\n        quoteIdentifier(foreignKey.name) +\n        ' ' +\n        'foreign key (' +\n        foreignKey.columns.map((column) => quoteIdentifier(column)).join(', ') +\n        ') references ' +\n        quoteTableRef(foreignKey.references.table) +\n        ' (' +\n        foreignKey.references.columns.map((column) => quoteIdentifier(column)).join(', ') +\n        ')'\n\n      if (foreignKey.onDelete) {\n        clause += ' on delete ' + foreignKey.onDelete\n      }\n\n      if (foreignKey.onUpdate) {\n        clause += ' on update ' + foreignKey.onUpdate\n      }\n\n      constraints.push(clause)\n    }\n\n    let sql =\n      'create table ' +\n      (operation.ifNotExists ? 'if not exists ' : '') +\n      quoteTableRef(operation.table) +\n      ' (' +\n      [...columns, ...constraints].join(', ') +\n      ')'\n\n    let statements: SqlStatement[] = [{ text: sql, values: [] }]\n\n    if (operation.comment) {\n      statements.push({\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' comment = ' +\n          quoteLiteral(operation.comment),\n        values: [],\n      })\n    }\n\n    return statements\n  }\n\n  if (operation.kind === 'alterTable') {\n    let statements: SqlStatement[] = []\n\n    for (let change of operation.changes) {\n      let sql = 'alter table ' + quoteTableRef(operation.table) + ' '\n\n      if (change.kind === 'addColumn') {\n        sql +=\n          'add column ' +\n          quoteIdentifier(change.column) +\n          ' ' +\n          compileMysqlColumn(change.definition)\n      } else if (change.kind === 'changeColumn') {\n        sql +=\n          'modify column ' +\n          quoteIdentifier(change.column) +\n          ' ' +\n          compileMysqlColumn(change.definition)\n      } else if (change.kind === 'renameColumn') {\n        sql += 'rename column ' + quoteIdentifier(change.from) + ' to ' + quoteIdentifier(change.to)\n      } else if (change.kind === 'dropColumn') {\n        sql += 'drop column ' + quoteIdentifier(change.column)\n      } else if (change.kind === 'addPrimaryKey') {\n        sql +=\n          'add primary key (' +\n          change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')'\n      } else if (change.kind === 'dropPrimaryKey') {\n        sql += 'drop primary key'\n      } else if (change.kind === 'addUnique') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'unique (' +\n          change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')'\n      } else if (change.kind === 'dropUnique') {\n        sql += 'drop index ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'addForeignKey') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'foreign key (' +\n          change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ') references ' +\n          quoteTableRef(change.constraint.references.table) +\n          ' (' +\n          change.constraint.references.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')'\n      } else if (change.kind === 'dropForeignKey') {\n        sql += 'drop foreign key ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'addCheck') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'check (' +\n          change.constraint.expression +\n          ')'\n      } else if (change.kind === 'dropCheck') {\n        sql += 'drop check ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'setTableComment') {\n        sql += 'comment = ' + quoteLiteral(change.comment)\n      } else {\n        continue\n      }\n\n      statements.push({ text: sql, values: [] })\n    }\n\n    return statements\n  }\n\n  if (operation.kind === 'renameTable') {\n    return [\n      {\n        text:\n          'rename table ' + quoteTableRef(operation.from) + ' to ' + quoteTableRef(operation.to),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropTable') {\n    return [\n      {\n        text:\n          'drop table ' + (operation.ifExists ? 'if exists ' : '') + quoteTableRef(operation.table),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'createIndex') {\n    return [\n      {\n        text:\n          'create ' +\n          (operation.index.unique ? 'unique ' : '') +\n          'index ' +\n          quoteIdentifier(operation.index.name) +\n          ' on ' +\n          quoteTableRef(operation.index.table) +\n          (operation.index.using ? ' using ' + operation.index.using : '') +\n          ' (' +\n          operation.index.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')' +\n          (operation.index.where ? ' where ' + operation.index.where : ''),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropIndex') {\n    return [\n      {\n        text:\n          'drop index ' + quoteIdentifier(operation.name) + ' on ' + quoteTableRef(operation.table),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'renameIndex') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' rename index ' +\n          quoteIdentifier(operation.from) +\n          ' to ' +\n          quoteIdentifier(operation.to),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'addForeignKey') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' add ' +\n          'constraint ' +\n          quoteIdentifier(operation.constraint.name) +\n          ' ' +\n          'foreign key (' +\n          operation.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ') references ' +\n          quoteTableRef(operation.constraint.references.table) +\n          ' (' +\n          operation.constraint.references.columns\n            .map((column) => quoteIdentifier(column))\n            .join(', ') +\n          ')' +\n          (operation.constraint.onDelete ? ' on delete ' + operation.constraint.onDelete : '') +\n          (operation.constraint.onUpdate ? ' on update ' + operation.constraint.onUpdate : ''),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropForeignKey') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' drop foreign key ' +\n          quoteIdentifier(operation.name),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'addCheck') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' add ' +\n          'constraint ' +\n          quoteIdentifier(operation.constraint.name) +\n          ' ' +\n          'check (' +\n          operation.constraint.expression +\n          ')',\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropCheck') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' drop check ' +\n          quoteIdentifier(operation.name),\n        values: [],\n      },\n    ]\n  }\n\n  throw new Error('Unsupported data migration operation kind')\n}\n\nfunction compileMysqlColumn(definition: ColumnDefinition): string {\n  let parts = [compileMysqlColumnType(definition)]\n\n  if (definition.nullable === false) {\n    parts.push('not null')\n  }\n\n  if (definition.default) {\n    if (definition.default.kind === 'now') {\n      parts.push('default current_timestamp')\n    } else if (definition.default.kind === 'sql') {\n      parts.push('default ' + definition.default.expression)\n    } else {\n      parts.push('default ' + quoteLiteral(definition.default.value))\n    }\n  }\n\n  if (definition.autoIncrement) {\n    parts.push('auto_increment')\n  }\n\n  if (definition.primaryKey) {\n    parts.push('primary key')\n  }\n\n  if (definition.unique) {\n    parts.push('unique')\n  }\n\n  if (definition.computed) {\n    parts.push('generated always as (' + definition.computed.expression + ')')\n    parts.push(definition.computed.stored ? 'stored' : 'virtual')\n  }\n\n  if (definition.references) {\n    let clause =\n      'references ' +\n      quoteTableRef(definition.references.table) +\n      ' (' +\n      definition.references.columns.map((column) => quoteIdentifier(column)).join(', ') +\n      ')'\n\n    if (definition.references.onDelete) {\n      clause += ' on delete ' + definition.references.onDelete\n    }\n\n    if (definition.references.onUpdate) {\n      clause += ' on update ' + definition.references.onUpdate\n    }\n\n    parts.push(clause)\n  }\n\n  if (definition.checks && definition.checks.length > 0) {\n    for (let check of definition.checks) {\n      parts.push('check (' + check.expression + ')')\n    }\n  }\n\n  return parts.join(' ')\n}\n\nfunction compileMysqlColumnType(definition: ColumnDefinition): string {\n  if (definition.type === 'varchar') {\n    return 'varchar(' + String(definition.length ?? 255) + ')'\n  }\n\n  if (definition.type === 'text') {\n    return 'text'\n  }\n\n  if (definition.type === 'integer') {\n    return definition.unsigned ? 'int unsigned' : 'int'\n  }\n\n  if (definition.type === 'bigint') {\n    return definition.unsigned ? 'bigint unsigned' : 'bigint'\n  }\n\n  if (definition.type === 'decimal') {\n    if (definition.precision !== undefined && definition.scale !== undefined) {\n      return 'decimal(' + String(definition.precision) + ', ' + String(definition.scale) + ')'\n    }\n\n    return 'decimal'\n  }\n\n  if (definition.type === 'boolean') {\n    return 'boolean'\n  }\n\n  if (definition.type === 'uuid') {\n    return 'char(36)'\n  }\n\n  if (definition.type === 'date') {\n    return 'date'\n  }\n\n  if (definition.type === 'time') {\n    return 'time'\n  }\n\n  if (definition.type === 'timestamp') {\n    return 'timestamp'\n  }\n\n  if (definition.type === 'json') {\n    return 'json'\n  }\n\n  if (definition.type === 'binary') {\n    return 'blob'\n  }\n\n  if (definition.type === 'enum') {\n    if (definition.enumValues && definition.enumValues.length > 0) {\n      return 'enum(' + definition.enumValues.map((value) => quoteLiteral(value)).join(', ') + ')'\n    }\n\n    return 'text'\n  }\n\n  return 'text'\n}\n"
  },
  {
    "path": "packages/data-table-mysql/src/lib/sql-compiler.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { beforeEach, describe, it } from 'node:test'\nimport {\n  and,\n  between,\n  column,\n  createDatabase,\n  table,\n  eq,\n  gt,\n  gte,\n  inList,\n  isNull,\n  like,\n  lt,\n  lte,\n  ne,\n  notInList,\n  notNull,\n  type DataManipulationOperation,\n  type DatabaseAdapter,\n  or,\n} from '@remix-run/data-table'\n\nimport { compileMysqlOperation } from './sql-compiler.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n    email: column.text(),\n    status: column.text(),\n    deleted: column.boolean(),\n  },\n})\n\nlet tasks = table({\n  name: 'tasks',\n  columns: {\n    id: column.integer(),\n    name: column.text(),\n    account_id: column.integer(),\n  },\n})\n\nlet statements: DataManipulationOperation[] = []\n\nlet fakeAdapter = {\n  capabilities: {\n    upsert: true,\n    returning: false,\n  },\n\n  execute: async (request) => {\n    statements.push(request.operation)\n    return {}\n  },\n} as DatabaseAdapter\n\nlet db = createDatabase(fakeAdapter)\n\ndescribe('mysql sql-compiler', () => {\n  beforeEach(() => {\n    statements = []\n  })\n\n  describe('select statement', () => {\n    it('compile wildcard selection', async () => {\n      await db.query(accounts).all()\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts`',\n        values: [],\n      })\n    })\n\n    it('compile selected aliases', async () => {\n      await db\n        .query(accounts)\n        .select({\n          accountId: accounts.id,\n          accountEmail: accounts.email,\n        })\n        .all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select `accounts`.`id` as `accountId`, `accounts`.`email` as `accountEmail` from `accounts`',\n        values: [],\n      })\n    })\n\n    it('compile joins', async () => {\n      await db.query(accounts).join(tasks, eq(accounts.id, tasks.account_id)).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` inner join `tasks` on `accounts`.`id` = `tasks`.`account_id`',\n        values: [],\n      })\n    })\n\n    it('compile left join', async () => {\n      await db.query(accounts).leftJoin(tasks, eq(accounts.id, tasks.account_id)).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` left join `tasks` on `accounts`.`id` = `tasks`.`account_id`',\n        values: [],\n      })\n    })\n\n    it('compile right join', async () => {\n      await db.query(accounts).rightJoin(tasks, eq(accounts.id, tasks.account_id)).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` right join `tasks` on `accounts`.`id` = `tasks`.`account_id`',\n        values: [],\n      })\n    })\n\n    it('compile object where filters', async () => {\n      await db.query(accounts).where({ status: 'enabled' }).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where ((`status` = ?))',\n        values: ['enabled'],\n      })\n    })\n\n    it('compile null where filters', async () => {\n      await db.query(accounts).where({ status: null }).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where ((`status` is null))',\n        values: [],\n      })\n    })\n\n    it('compile predicate operators', async () => {\n      await db.query(accounts).where(ne('status', 'disabled')).where(gt('id', 10)).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where (`status` <> ?) and (`id` > ?)',\n        values: ['disabled', 10],\n      })\n    })\n\n    it('compile gte/lt/lte/between/like predicates', async () => {\n      await db\n        .query(accounts)\n        .where(gte('id', 1))\n        .where(lt('id', 20))\n        .where(lte('id', 30))\n        .where(between('id', 2, 9))\n        .where(like('email', '%@example.com'))\n        .all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where (`id` >= ?) and (`id` < ?) and (`id` <= ?) and (`id` between ? and ?) and (`email` like ?)',\n        values: [1, 20, 30, 2, 9, '%@example.com'],\n      })\n    })\n\n    it('compile in-list predicates', async () => {\n      await db\n        .query(accounts)\n        .where(inList('id', [1, 2]))\n        .all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where (`id` in (?, ?))',\n        values: [1, 2],\n      })\n    })\n\n    it('compile empty in-list predicates', async () => {\n      await db.query(accounts).where(inList('id', [])).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where (1 = 0)',\n        values: [],\n      })\n    })\n\n    it('compile not-in predicates', async () => {\n      await db\n        .query(accounts)\n        .where(notInList('id', [1, 2]))\n        .all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where (`id` not in (?, ?))',\n        values: [1, 2],\n      })\n    })\n\n    it('compile logical combinators', async () => {\n      await db\n        .query(accounts)\n        .where(\n          and(\n            eq(accounts.id, 1),\n            or(eq(accounts.status, 'enabled'), eq(accounts.status, 'disabled')),\n          ),\n        )\n        .all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where ((`accounts`.`id` = ?) and ((`accounts`.`status` = ?) or (`accounts`.`status` = ?)))',\n        values: [1, 'enabled', 'disabled'],\n      })\n    })\n\n    it('compile empty logical combinators', async () => {\n      await db.query(accounts).where(and()).where(or()).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where (1 = 1) and (1 = 0)',\n        values: [],\n      })\n    })\n\n    it('compile group by and having', async () => {\n      await db.query(tasks).groupBy(tasks.account_id).having({ account_id: 20 }).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `tasks` group by `tasks`.`account_id` having ((`account_id` = ?))',\n        values: [20],\n      })\n    })\n\n    it('compile pagination', async () => {\n      await db.query(accounts).offset(5).limit(10).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` limit 10 offset 5',\n        values: [],\n      })\n    })\n\n    it('compile distinct selection with order by', async () => {\n      await db.query(accounts).distinct().orderBy('id', 'desc').all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select distinct * from `accounts` order by `id` DESC',\n        values: [],\n      })\n    })\n\n    it('compile boolean bindings', async () => {\n      await db.query(accounts).where({ deleted: true }).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where ((`deleted` = ?))',\n        values: [true],\n      })\n    })\n\n    it('compile boolean predicates', async () => {\n      await db.query(accounts).where(isNull(accounts.status)).where(notNull(accounts.email)).all()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from `accounts` where (`accounts`.`status` is null) and (`accounts`.`email` is not null)',\n        values: [],\n      })\n    })\n\n    it('compile wildcard segment path', () => {\n      let compiled = compileMysqlOperation({\n        kind: 'select',\n        table: accounts,\n        select: [{ column: 'accounts.*', alias: 'allColumns' }],\n        joins: [],\n        where: [],\n        groupBy: [],\n        having: [],\n        orderBy: [],\n        limit: undefined,\n        offset: undefined,\n        distinct: false,\n      })\n\n      assert.deepEqual(compiled, {\n        text: 'select `accounts`.* as `allColumns` from `accounts`',\n        values: [],\n      })\n    })\n  })\n\n  describe('count - exists statement', () => {\n    it('compile count', async () => {\n      await db.query(tasks).where({ account_id: 1 }).count()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select count(*) as `count` from (select 1 from `tasks` where ((`account_id` = ?))) as `__dt_count`',\n        values: [1],\n      })\n    })\n\n    it('compile exists', async () => {\n      await db.query(tasks).where({ account_id: 1 }).exists()\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select count(*) as `count` from (select 1 from `tasks` where ((`account_id` = ?))) as `__dt_count`',\n        values: [1],\n      })\n    })\n  })\n\n  describe('insert statement', () => {\n    it('compile for one', async () => {\n      await db.create(accounts, {\n        id: 1,\n        email: 'info@remix.run',\n        status: 'enabled',\n      })\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into `accounts` (`id`, `email`, `status`) values (?, ?, ?)',\n        values: [1, 'info@remix.run', 'enabled'],\n      })\n    })\n\n    it('compile for one with default values', async () => {\n      await db.create(accounts, {})\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into `accounts` () values ()',\n        values: [],\n      })\n    })\n\n    it('compile for many', async () => {\n      await db.createMany(accounts, [\n        { id: 1, email: 'info@remix.run', status: 'enabled' },\n        { id: 2, email: 'contact@remix.run', status: 'draft' },\n      ])\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into `accounts` (`id`, `email`, `status`) values (?, ?, ?), (?, ?, ?)',\n        values: [1, 'info@remix.run', 'enabled', 2, 'contact@remix.run', 'draft'],\n      })\n    })\n\n    it('compile for many with default values', () => {\n      let compiled = compileMysqlOperation({\n        kind: 'insertMany',\n        table: accounts,\n        values: [{}, {}],\n      })\n      assert.deepEqual(compiled, {\n        text: 'insert into `accounts` () values ()',\n        values: [],\n      })\n    })\n\n    it('compile for many without data', async () => {\n      await db.createMany(accounts, [])\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select 0 where 1 = 0',\n        values: [],\n      })\n    })\n  })\n\n  describe('update statement', () => {\n    it('compile for one', async () => {\n      await db.query(accounts).where({ id: 1 }).update({\n        email: 'info@remix.run',\n        status: 'enabled',\n      })\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'update `accounts` set `email` = ?, `status` = ? where ((`id` = ?))',\n        values: ['info@remix.run', 'enabled', 1],\n      })\n    })\n\n    it('compile for many', async () => {\n      await db.updateMany(\n        accounts,\n        {\n          email: 'info@remix.run',\n          status: 'enabled',\n        },\n        {\n          where: {\n            status: 'disabled',\n          },\n        },\n      )\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'update `accounts` set `email` = ?, `status` = ? where ((`status` = ?))',\n        values: ['info@remix.run', 'enabled', 'disabled'],\n      })\n    })\n  })\n\n  describe('upsert statement', () => {\n    it('throws without values', async () => {\n      await db.query(accounts).upsert(\n        {},\n        {\n          conflictTarget: ['id'],\n        },\n      )\n\n      assert.throws(() => compileMysqlOperation(statements[0]))\n    })\n\n    it('compile with update columns', async () => {\n      await db.query(accounts).upsert(\n        {\n          status: 'enabled',\n          email: 'info@remix.run',\n        },\n        {\n          conflictTarget: ['id'],\n          update: {\n            email: 'contact@remix.run',\n          },\n        },\n      )\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into `accounts` (`status`, `email`) values (?, ?) on duplicate key update `email` = ?',\n        values: ['contact@remix.run', 'enabled', 'info@remix.run'],\n      })\n    })\n\n    it('compile without update columns', async () => {\n      await db.query(accounts).upsert(\n        {\n          status: 'enabled',\n          email: 'info@remix.run',\n        },\n        {\n          conflictTarget: ['id'],\n          update: {},\n        },\n      )\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into `accounts` (`status`, `email`) values (?, ?) on duplicate key update `id` = `id`',\n        values: ['enabled', 'info@remix.run'],\n      })\n    })\n  })\n\n  describe('delete statement', () => {\n    it('compile for one', async () => {\n      await db.delete(accounts, 10)\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'delete from `accounts` where ((`id` = ?))',\n        values: [10],\n      })\n    })\n\n    it('compile for many', async () => {\n      await db.deleteMany(accounts, {\n        where: {\n          status: 'enabled',\n        },\n      })\n\n      let compiled = compileMysqlOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'delete from `accounts` where ((`status` = ?))',\n        values: ['enabled'],\n      })\n    })\n  })\n\n  describe('raw statement', () => {\n    it('compile', () => {\n      let compiled = compileMysqlOperation({\n        kind: 'raw',\n        sql: {\n          text: 'select * from accounts where id = ? and status = ?',\n          values: [10, 'active'],\n        },\n      })\n\n      assert.deepEqual(compiled, {\n        text: 'select * from accounts where id = ? and status = ?',\n        values: [10, 'active'],\n      })\n    })\n  })\n\n  describe('error handling', () => {\n    it('throws for unsupported statements', () => {\n      assert.throws(\n        () => compileMysqlOperation({ kind: 'unknown' } as never),\n        /Unsupported operation kind/,\n      )\n    })\n\n    it('throws for unsupported predicates', () => {\n      assert.throws(\n        () =>\n          compileMysqlOperation({\n            kind: 'select',\n            table: accounts,\n            select: '*',\n            joins: [],\n            where: [{ type: 'unknown' } as never],\n            groupBy: [],\n            having: [],\n            orderBy: [],\n            limit: undefined,\n            offset: undefined,\n            distinct: false,\n          }),\n        /Unsupported predicate/,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/data-table-mysql/src/lib/sql-compiler.ts",
    "content": "import { getTableName, getTablePrimaryKey } from '@remix-run/data-table'\nimport type { DataManipulationOperation, Predicate, SqlStatement } from '@remix-run/data-table'\nimport {\n  collectColumns as collectColumnsHelper,\n  normalizeJoinType as normalizeJoinTypeHelper,\n  quotePath as quotePathHelper,\n} from '@remix-run/data-table/sql-helpers'\n\ntype JoinClause = Extract<DataManipulationOperation, { kind: 'select' }>['joins'][number]\ntype UpsertOperation = Extract<DataManipulationOperation, { kind: 'upsert' }>\ntype OperationTable = Extract<DataManipulationOperation, { kind: 'select' }>['table']\n\ntype CompileContext = {\n  values: unknown[]\n}\n\nexport function compileMysqlOperation(operation: DataManipulationOperation): SqlStatement {\n  if (operation.kind === 'raw') {\n    return {\n      text: operation.sql.text,\n      values: [...operation.sql.values],\n    }\n  }\n\n  let context: CompileContext = { values: [] }\n\n  if (operation.kind === 'select') {\n    let selection = '*'\n\n    if (operation.select !== '*') {\n      selection = operation.select\n        .map((field) => quotePath(field.column) + ' as ' + quoteIdentifier(field.alias))\n        .join(', ')\n    }\n\n    return {\n      text:\n        'select ' +\n        (operation.distinct ? 'distinct ' : '') +\n        selection +\n        compileFromClause(operation.table, operation.joins, context) +\n        compileWhereClause(operation.where, context) +\n        compileGroupByClause(operation.groupBy) +\n        compileHavingClause(operation.having, context) +\n        compileOrderByClause(operation.orderBy) +\n        compileLimitClause(operation.limit) +\n        compileOffsetClause(operation.offset),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'count' || operation.kind === 'exists') {\n    let inner =\n      'select 1' +\n      compileFromClause(operation.table, operation.joins, context) +\n      compileWhereClause(operation.where, context) +\n      compileGroupByClause(operation.groupBy) +\n      compileHavingClause(operation.having, context)\n\n    return {\n      text:\n        'select count(*) as ' +\n        quoteIdentifier('count') +\n        ' from (' +\n        inner +\n        ') as ' +\n        quoteIdentifier('__dt_count'),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'insert') {\n    return compileInsertOperation(operation.table, operation.values, context)\n  }\n\n  if (operation.kind === 'insertMany') {\n    return compileInsertManyOperation(operation.table, operation.values, context)\n  }\n\n  if (operation.kind === 'update') {\n    let columns = Object.keys(operation.changes)\n\n    return {\n      text:\n        'update ' +\n        quotePath(getTableName(operation.table)) +\n        ' set ' +\n        columns\n          .map(\n            (column) => quotePath(column) + ' = ' + pushValue(context, operation.changes[column]),\n          )\n          .join(', ') +\n        compileWhereClause(operation.where, context),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'delete') {\n    return {\n      text:\n        'delete from ' +\n        quotePath(getTableName(operation.table)) +\n        compileWhereClause(operation.where, context),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'upsert') {\n    return compileUpsertOperation(operation, context)\n  }\n\n  throw new Error('Unsupported operation kind')\n}\n\nfunction compileInsertOperation(\n  table: OperationTable,\n  values: Record<string, unknown>,\n  context: CompileContext,\n): SqlStatement {\n  let columns = Object.keys(values)\n\n  if (columns.length === 0) {\n    return {\n      text: 'insert into ' + quotePath(getTableName(table)) + ' () values ()',\n      values: context.values,\n    }\n  }\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(table)) +\n      ' (' +\n      columns.map((column) => quotePath(column)).join(', ') +\n      ') values (' +\n      columns.map((column) => pushValue(context, values[column])).join(', ') +\n      ')',\n    values: context.values,\n  }\n}\n\nfunction compileInsertManyOperation(\n  table: OperationTable,\n  rows: Record<string, unknown>[],\n  context: CompileContext,\n): SqlStatement {\n  if (rows.length === 0) {\n    return {\n      text: 'select 0 where 1 = 0',\n      values: context.values,\n    }\n  }\n\n  let columns = collectColumns(rows)\n\n  if (columns.length === 0) {\n    return {\n      text: 'insert into ' + quotePath(getTableName(table)) + ' () values ()',\n      values: context.values,\n    }\n  }\n\n  let values = rows.map(\n    (row) =>\n      '(' +\n      columns\n        .map((column) => {\n          let value = Object.prototype.hasOwnProperty.call(row, column) ? row[column] : null\n          return pushValue(context, value)\n        })\n        .join(', ') +\n      ')',\n  )\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(table)) +\n      ' (' +\n      columns.map((column) => quotePath(column)).join(', ') +\n      ') values ' +\n      values.join(', '),\n    values: context.values,\n  }\n}\n\nfunction compileUpsertOperation(operation: UpsertOperation, context: CompileContext): SqlStatement {\n  let insertColumns = Object.keys(operation.values)\n\n  if (insertColumns.length === 0) {\n    throw new Error('upsert requires at least one value')\n  }\n\n  let updateValues = operation.update ?? operation.values\n  let updateColumns = Object.keys(updateValues)\n  let fallbackNoopColumn = getTablePrimaryKey(operation.table)[0]\n\n  let onDuplicate =\n    updateColumns.length > 0\n      ? updateColumns\n          .map((column) => quotePath(column) + ' = ' + pushValue(context, updateValues[column]))\n          .join(', ')\n      : quotePath(fallbackNoopColumn) + ' = ' + quotePath(fallbackNoopColumn)\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(operation.table)) +\n      ' (' +\n      insertColumns.map((column) => quotePath(column)).join(', ') +\n      ') values (' +\n      insertColumns.map((column) => pushValue(context, operation.values[column])).join(', ') +\n      ') on duplicate key update ' +\n      onDuplicate,\n    values: context.values,\n  }\n}\n\nfunction compileFromClause(\n  table: OperationTable,\n  joins: JoinClause[],\n  context: CompileContext,\n): string {\n  let output = ' from ' + quotePath(getTableName(table))\n\n  for (let join of joins) {\n    output +=\n      ' ' +\n      normalizeJoinType(join.type) +\n      ' join ' +\n      quotePath(getTableName(join.table)) +\n      ' on ' +\n      compilePredicate(join.on, context)\n  }\n\n  return output\n}\n\nfunction compileWhereClause(predicates: Predicate[], context: CompileContext): string {\n  if (predicates.length === 0) {\n    return ''\n  }\n\n  return (\n    ' where ' +\n    predicates.map((predicate) => '(' + compilePredicate(predicate, context) + ')').join(' and ')\n  )\n}\n\nfunction compileGroupByClause(columns: string[]): string {\n  if (columns.length === 0) {\n    return ''\n  }\n\n  return ' group by ' + columns.map((column) => quotePath(column)).join(', ')\n}\n\nfunction compileHavingClause(predicates: Predicate[], context: CompileContext): string {\n  if (predicates.length === 0) {\n    return ''\n  }\n\n  return (\n    ' having ' +\n    predicates.map((predicate) => '(' + compilePredicate(predicate, context) + ')').join(' and ')\n  )\n}\n\nfunction compileOrderByClause(orderBy: { column: string; direction: 'asc' | 'desc' }[]): string {\n  if (orderBy.length === 0) {\n    return ''\n  }\n\n  return (\n    ' order by ' +\n    orderBy\n      .map((clause) => quotePath(clause.column) + ' ' + clause.direction.toUpperCase())\n      .join(', ')\n  )\n}\n\nfunction compileLimitClause(limit: number | undefined): string {\n  if (limit === undefined) {\n    return ''\n  }\n\n  return ' limit ' + String(limit)\n}\n\nfunction compileOffsetClause(offset: number | undefined): string {\n  if (offset === undefined) {\n    return ''\n  }\n\n  return ' offset ' + String(offset)\n}\n\nfunction compilePredicate(predicate: Predicate, context: CompileContext): string {\n  if (predicate.type === 'comparison') {\n    let column = quotePath(predicate.column)\n\n    if (predicate.operator === 'eq') {\n      if (\n        predicate.valueType === 'value' &&\n        (predicate.value === null || predicate.value === undefined)\n      ) {\n        return column + ' is null'\n      }\n\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' = ' + comparisonValue\n    }\n\n    if (predicate.operator === 'ne') {\n      if (\n        predicate.valueType === 'value' &&\n        (predicate.value === null || predicate.value === undefined)\n      ) {\n        return column + ' is not null'\n      }\n\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' <> ' + comparisonValue\n    }\n\n    if (predicate.operator === 'gt') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' > ' + comparisonValue\n    }\n\n    if (predicate.operator === 'gte') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' >= ' + comparisonValue\n    }\n\n    if (predicate.operator === 'lt') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' < ' + comparisonValue\n    }\n\n    if (predicate.operator === 'lte') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' <= ' + comparisonValue\n    }\n\n    if (predicate.operator === 'in' || predicate.operator === 'notIn') {\n      let values = Array.isArray(predicate.value) ? predicate.value : []\n\n      if (values.length === 0) {\n        return predicate.operator === 'in' ? '1 = 0' : '1 = 1'\n      }\n\n      let keyword = predicate.operator === 'in' ? 'in' : 'not in'\n\n      return (\n        column +\n        ' ' +\n        keyword +\n        ' (' +\n        values.map((value) => pushValue(context, value)).join(', ') +\n        ')'\n      )\n    }\n\n    if (predicate.operator === 'like') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' like ' + comparisonValue\n    }\n\n    if (predicate.operator === 'ilike') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return 'lower(' + column + ') like lower(' + comparisonValue + ')'\n    }\n  }\n\n  if (predicate.type === 'between') {\n    return (\n      quotePath(predicate.column) +\n      ' between ' +\n      pushValue(context, predicate.lower) +\n      ' and ' +\n      pushValue(context, predicate.upper)\n    )\n  }\n\n  if (predicate.type === 'null') {\n    return (\n      quotePath(predicate.column) + (predicate.operator === 'isNull' ? ' is null' : ' is not null')\n    )\n  }\n\n  if (predicate.type === 'logical') {\n    if (predicate.predicates.length === 0) {\n      return predicate.operator === 'and' ? '1 = 1' : '1 = 0'\n    }\n\n    let joiner = predicate.operator === 'and' ? ' and ' : ' or '\n\n    return predicate.predicates\n      .map((child) => '(' + compilePredicate(child, context) + ')')\n      .join(joiner)\n  }\n\n  throw new Error('Unsupported predicate')\n}\n\nfunction compileComparisonValue(\n  predicate: Extract<Predicate, { type: 'comparison' }>,\n  context: CompileContext,\n): string {\n  if (predicate.valueType === 'column') {\n    return quotePath(predicate.value)\n  }\n\n  return pushValue(context, predicate.value)\n}\n\nfunction normalizeJoinType(type: string): string {\n  return normalizeJoinTypeHelper(type)\n}\n\nfunction quoteIdentifier(value: string): string {\n  return '`' + value.replace(/`/g, '``') + '`'\n}\n\nfunction quotePath(path: string): string {\n  return quotePathHelper(path, quoteIdentifier)\n}\n\nfunction pushValue(context: CompileContext, value: unknown): string {\n  context.values.push(value)\n  return '?'\n}\n\nfunction collectColumns(rows: Record<string, unknown>[]): string[] {\n  return collectColumnsHelper(rows)\n}\n"
  },
  {
    "path": "packages/data-table-mysql/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/data-table-mysql/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"types\": [\"node\"],\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"vendor\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/data-table-postgres/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/data-table-postgres/.changes/minor.ddl-migration-contract.md",
    "content": "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.\n\nNormal reads/writes continue through `execute(...)`, while migration/DDL work runs through `migrate(...)`.\n\nSQL compilation remains adapter-owned and can share helpers from `remix/data-table/sql-helpers`.\n"
  },
  {
    "path": "packages/data-table-postgres/.changes/minor.introspection-migration-transaction-tokens.md",
    "content": "Add transaction-aware migration introspection to the postgres adapter.\n\n`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.\n"
  },
  {
    "path": "packages/data-table-postgres/CHANGELOG.md",
    "content": "# `data-table-postgres` CHANGELOG\n\nThis 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/).\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release of `@remix-run/data-table-postgres`.\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`data-table@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.1.0)\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/data-table-postgres/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/data-table-postgres/README.md",
    "content": "# data-table-postgres\n\nPostgreSQL adapter for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table).\nUse this package when you want `data-table` APIs backed by `pg`.\n\n## Features\n\n- **Native `pg` Integration**: Works with `pg` `Pool` and `PoolClient` instances\n- **Full `data-table` API Support**: Queries, relations, writes, and transactions\n- **Adapter-Owned Compiler**: SQL compilation lives in this adapter, with optional shared pure helpers from `data-table`\n- **Migration DDL Support**: Compiles and executes `DataMigrationOperation` operations for `remix/data-table/migrations`\n- **Postgres Capabilities Enabled By Default**:\n  - `returning: true`\n  - `savepoints: true`\n  - `upsert: true`\n  - `transactionalDdl: true`\n  - `migrationLock: true`\n\n## Installation\n\n```sh\nnpm i remix pg\n```\n\n## Usage\n\n```ts\nimport { Pool } from 'pg'\nimport { createDatabase } from 'remix/data-table'\nimport { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'\n\nlet pool = new Pool({\n  connectionString: process.env.DATABASE_URL,\n})\n\nlet db = createDatabase(createPostgresDatabaseAdapter(pool))\n```\n\nUse `db.query(...)`, relation loading, and transactions from `remix/data-table`.\nImport any driver-specific types you need directly from `pg`.\n\n## Adapter Capabilities\n\n`data-table-postgres` reports this capability set by default:\n\n- `returning: true`\n- `savepoints: true`\n- `upsert: true`\n- `transactionalDdl: true`\n- `migrationLock: true`\n\n## Advanced Usage\n\n### Transaction Options\n\nTransaction options are passed through to the adapter as hints.\n\n```ts\nawait db.transaction(async (txDb) => txDb.exec('select 1'), {\n  isolationLevel: 'serializable',\n  readOnly: false,\n})\n```\n\n### Capability Overrides For Testing\n\nYou can override capabilities to verify fallback paths in tests.\n\n```ts\nimport { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'\n\nlet adapter = createPostgresDatabaseAdapter(pool, {\n  capabilities: {\n    returning: false,\n  },\n})\n```\n\n## Related Packages\n\n- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API\n- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema parsing and validation\n- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL adapter\n- [`data-table-sqlite`](https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite) - SQLite adapter\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/data-table-postgres/package.json",
    "content": "{\n  \"name\": \"@remix-run/data-table-postgres\",\n  \"version\": \"0.1.0\",\n  \"description\": \"PostgreSQL adapter for remix/data-table\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/data-table-postgres\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/data-table-postgres#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/data-table\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@types/pg\": \"^8.15.6\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"pg\": \"^8.16.3\"\n  },\n  \"dependencies\": {\n    \"@remix-run/data-table\": \"workspace:^\"\n  },\n  \"peerDependencies\": {\n    \"pg\": \"^8.16.3\"\n  },\n  \"peerDependenciesMeta\": {\n    \"pg\": {\n      \"optional\": true\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"test:coverage\": \"node --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-lines=90 --test-coverage-branches=90 --test-coverage-functions=90 --test ./src/lib/adapter.test.ts ./src/lib/sql-compiler.test.ts\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"remix\",\n    \"orm\",\n    \"postgres\",\n    \"database\",\n    \"sql\"\n  ]\n}\n"
  },
  {
    "path": "packages/data-table-postgres/src/index.ts",
    "content": "export type { PostgresDatabaseAdapterOptions } from './lib/adapter.ts'\nexport { createPostgresDatabaseAdapter, PostgresDatabaseAdapter } from './lib/adapter.ts'\n"
  },
  {
    "path": "packages/data-table-postgres/src/lib/adapter.integration.test.ts",
    "content": "import { after, before, describe } from 'node:test'\nimport { createDatabase } from '@remix-run/data-table'\nimport { Pool } from 'pg'\n\nimport {\n  resetAdapterIntegrationSchema,\n  setupAdapterIntegrationSchema,\n  teardownAdapterIntegrationSchema,\n} from '../../../data-table/test/adapter-integration-schema.ts'\nimport { runAdapterIntegrationContract } from '../../../data-table/test/adapter-integration-contract.ts'\n\nimport { createPostgresDatabaseAdapter } from './adapter.ts'\n\nlet integrationEnabled =\n  process.env.DATA_TABLE_INTEGRATION === '1' &&\n  typeof process.env.DATA_TABLE_POSTGRES_URL === 'string'\n\ndescribe('postgres adapter integration', () => {\n  let pool: Pool\n\n  before(async () => {\n    if (!integrationEnabled) {\n      return\n    }\n\n    pool = new Pool({ connectionString: process.env.DATA_TABLE_POSTGRES_URL })\n    await setupAdapterIntegrationSchema(async (statement) => {\n      await pool.query(statement)\n    }, 'postgres')\n  })\n\n  after(async () => {\n    if (!integrationEnabled) {\n      return\n    }\n\n    await teardownAdapterIntegrationSchema(async (statement) => {\n      await pool.query(statement)\n    }, 'postgres')\n    await pool.end()\n  })\n\n  runAdapterIntegrationContract({\n    integrationEnabled,\n    createDatabase: () => createDatabase(createPostgresDatabaseAdapter(pool)),\n    resetDatabase: async () => {\n      await resetAdapterIntegrationSchema(async (statement) => {\n        await pool.query(statement)\n      }, 'postgres')\n    },\n  })\n})\n"
  },
  {
    "path": "packages/data-table-postgres/src/lib/adapter.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\nimport type { DataMigrationOperation } from '@remix-run/data-table'\nimport { column, createDatabase, table, eq, inList, sql } from '@remix-run/data-table'\n\nimport { createPostgresDatabaseAdapter } from './adapter.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n    email: column.text(),\n  },\n})\n\nlet projects = table({\n  name: 'projects',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n    name: column.text(),\n  },\n})\n\nlet invoices = table({\n  name: 'billing.invoices',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n  },\n})\n\nlet accountProjects = table({\n  name: 'account_projects',\n  columns: {\n    account_id: column.integer(),\n    project_id: column.integer(),\n    email: column.text(),\n  },\n  primaryKey: ['account_id', 'project_id'],\n})\n\ndescribe('postgres adapter', () => {\n  it('applies explicit capability overrides', () => {\n    let adapter = createPostgresDatabaseAdapter(\n      {\n        async query() {\n          return {\n            rows: [],\n            rowCount: 0,\n            command: 'SELECT',\n            oid: 0,\n            fields: [],\n          }\n        },\n      } as never,\n      {\n        capabilities: {\n          returning: false,\n          savepoints: false,\n          upsert: false,\n          transactionalDdl: false,\n          migrationLock: false,\n        },\n      },\n    )\n\n    assert.deepEqual(adapter.capabilities, {\n      returning: false,\n      savepoints: false,\n      upsert: false,\n      transactionalDdl: false,\n      migrationLock: false,\n    })\n  })\n\n  it('checks table and column existence through adapter introspection hooks', async () => {\n    let statements: Array<{ text: string; values: unknown[] | undefined }> = []\n\n    let client = {\n      async query(text: string, values?: unknown[]) {\n        statements.push({ text, values })\n\n        if (text.includes('pg_attribute')) {\n          return {\n            rows: [{ exists: 't' }],\n            rowCount: 1,\n          }\n        }\n\n        return {\n          rows: [{ exists: true }],\n          rowCount: 1,\n        }\n      },\n    }\n\n    let adapter = createPostgresDatabaseAdapter(client as never)\n    let hasTable = await adapter.hasTable({ schema: 'app', name: 'users' })\n    let hasColumn = await adapter.hasColumn({ schema: 'app', name: 'users' }, 'email')\n\n    assert.equal(hasTable, true)\n    assert.equal(hasColumn, true)\n    assert.equal(statements[0]?.text, 'select to_regclass($1) is not null as \"exists\"')\n    assert.equal(statements[0]?.values?.[0], '\"app\".\"users\"')\n    assert.equal(\n      statements[1]?.text,\n      'select exists (select 1 from pg_attribute where attrelid = to_regclass($1) and attname = $2 and attnum > 0 and not attisdropped) as \"exists\"',\n    )\n    assert.equal(statements[1]?.values?.[0], '\"app\".\"users\"')\n    assert.equal(statements[1]?.values?.[1], 'email')\n  })\n\n  it('routes introspection through transaction clients when a token is provided', async () => {\n    let poolQueries = 0\n    let transactionStatements: string[] = []\n\n    let transactionClient = {\n      async query(text: string) {\n        transactionStatements.push(text)\n        return {\n          rows: [{ exists: true }],\n          rowCount: 1,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n      release() {},\n    }\n\n    let pool = {\n      async query() {\n        poolQueries += 1\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n      async connect() {\n        return transactionClient\n      },\n    }\n\n    let adapter = createPostgresDatabaseAdapter(pool as never)\n    let token = await adapter.beginTransaction()\n\n    await adapter.hasTable({ name: 'users' }, token)\n    await adapter.hasColumn({ name: 'users' }, 'email', token)\n    await adapter.commitTransaction(token)\n\n    assert.equal(poolQueries, 0)\n    assert.deepEqual(transactionStatements, [\n      'begin',\n      'select to_regclass($1) is not null as \"exists\"',\n      'select exists (select 1 from pg_attribute where attrelid = to_regclass($1) and attname = $2 and attnum > 0 and not attisdropped) as \"exists\"',\n      'commit',\n    ])\n  })\n\n  it('short-circuits insertMany([]) and returns empty rows for returning queries', async () => {\n    let calls = 0\n\n    let client = {\n      async query() {\n        calls += 1\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let adapter = createPostgresDatabaseAdapter(client as never)\n    let result = await adapter.execute({\n      operation: {\n        kind: 'insertMany',\n        table: accounts,\n        values: [],\n        returning: ['id'],\n      },\n      transaction: undefined,\n    })\n\n    assert.deepEqual(result, {\n      affectedRows: 0,\n      insertId: undefined,\n      rows: [],\n    })\n    assert.equal(calls, 0)\n  })\n\n  it('converts raw sql placeholders and normalizes count rows', async () => {\n    let statements: Array<{ text: string; values: unknown[] | undefined }> = []\n\n    let client = {\n      async query(text: string, values?: unknown[]) {\n        statements.push({ text, values })\n\n        if (text.startsWith('select count(*)')) {\n          return {\n            rows: [{ count: '2' }],\n            rowCount: 1,\n            command: 'SELECT',\n            oid: 0,\n            fields: [],\n          }\n        }\n\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n\n    let count = await db.query(accounts).count()\n    await db.exec(sql`select * from accounts where id = ${42}`)\n\n    assert.equal(count, 2)\n    assert.equal(statements[1].text, 'select * from accounts where id = $1')\n    assert.deepEqual(statements[1].values, [42])\n  })\n\n  it('uses savepoints for nested transactions', async () => {\n    let statements: string[] = []\n\n    let client = {\n      async query(text: string) {\n        statements.push(text)\n\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n\n    await db.transaction(async (outerTransaction) => {\n      await outerTransaction\n        .transaction(async () => {\n          throw new Error('Abort nested transaction')\n        })\n        .catch(() => undefined)\n    })\n\n    assert.deepEqual(statements, [\n      'begin',\n      'savepoint \"sp_0\"',\n      'rollback to savepoint \"sp_0\"',\n      'release savepoint \"sp_0\"',\n      'commit',\n    ])\n  })\n\n  it('applies transaction options when provided', async () => {\n    let statements: string[] = []\n\n    let client = {\n      async query(text: string) {\n        statements.push(text)\n\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n\n    await db.transaction(async () => undefined, {\n      isolationLevel: 'serializable',\n      readOnly: true,\n    })\n\n    assert.deepEqual(statements, [\n      'begin',\n      'set transaction isolation level serializable read only',\n      'commit',\n    ])\n  })\n\n  it('applies read write transaction mode when readOnly is false', async () => {\n    let statements: string[] = []\n\n    let client = {\n      async query(text: string) {\n        statements.push(text)\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n\n    await db.transaction(async () => undefined, { readOnly: false })\n\n    assert.deepEqual(statements, ['begin', 'set transaction read write', 'commit'])\n  })\n\n  it('uses client.connect() for transactions and releases the connection', async () => {\n    let lifecycle: string[] = []\n\n    let transactionClient = {\n      async query(text: string) {\n        lifecycle.push(text)\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n      release() {\n        lifecycle.push('release')\n      },\n    }\n\n    let pool = {\n      async query() {\n        throw new Error('unexpected pool query')\n      },\n      async connect() {\n        lifecycle.push('connect')\n        return transactionClient\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(pool as never))\n\n    await db.transaction(async () => undefined)\n\n    assert.deepEqual(lifecycle, ['connect', 'begin', 'commit', 'release'])\n  })\n\n  it('supports pooled transactions when connect() clients omit release()', async () => {\n    let lifecycle: string[] = []\n\n    let transactionClient = {\n      async query(text: string) {\n        lifecycle.push(text)\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let pool = {\n      async query() {\n        throw new Error('unexpected pool query')\n      },\n      async connect() {\n        lifecycle.push('connect')\n        return transactionClient\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(pool as never))\n\n    await db.transaction(async () => undefined)\n\n    assert.deepEqual(lifecycle, ['connect', 'begin', 'commit'])\n  })\n\n  it('rolls back transactions and releases connected clients', async () => {\n    let lifecycle: string[] = []\n\n    let transactionClient = {\n      async query(text: string) {\n        lifecycle.push(text)\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n      release() {\n        lifecycle.push('release')\n      },\n    }\n\n    let pool = {\n      async query() {\n        throw new Error('unexpected pool query')\n      },\n      async connect() {\n        lifecycle.push('connect')\n        return transactionClient\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(pool as never))\n\n    await assert.rejects(\n      () =>\n        db.transaction(async () => {\n          throw new Error('force rollback')\n        }),\n      /force rollback/,\n    )\n\n    assert.deepEqual(lifecycle, ['connect', 'begin', 'rollback', 'release'])\n  })\n\n  it('rolls back pooled transactions when connect() clients omit release()', async () => {\n    let lifecycle: string[] = []\n\n    let transactionClient = {\n      async query(text: string) {\n        lifecycle.push(text)\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let pool = {\n      async query() {\n        throw new Error('unexpected pool query')\n      },\n      async connect() {\n        lifecycle.push('connect')\n        return transactionClient\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(pool as never))\n\n    await assert.rejects(\n      () =>\n        db.transaction(async () => {\n          throw new Error('force rollback')\n        }),\n      /force rollback/,\n    )\n\n    assert.deepEqual(lifecycle, ['connect', 'begin', 'rollback'])\n  })\n\n  it('supports savepoint lifecycle and escapes savepoint names', async () => {\n    let statements: string[] = []\n\n    let client = {\n      async query(text: string) {\n        statements.push(text)\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let adapter = createPostgresDatabaseAdapter(client as never)\n    let token = await adapter.beginTransaction()\n\n    await adapter.createSavepoint(token, 'sp\"name')\n    await adapter.rollbackToSavepoint(token, 'sp\"name')\n    await adapter.releaseSavepoint(token, 'sp\"name')\n    await adapter.commitTransaction(token)\n\n    assert.deepEqual(statements, [\n      'begin',\n      'savepoint \"sp\"\"name\"',\n      'rollback to savepoint \"sp\"\"name\"',\n      'release savepoint \"sp\"\"name\"',\n      'commit',\n    ])\n  })\n\n  it('throws for unknown transaction tokens', async () => {\n    let client = {\n      async query() {\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let adapter = createPostgresDatabaseAdapter(client as never)\n\n    await assert.rejects(\n      () => adapter.commitTransaction({ id: 'tx_missing' }),\n      /Unknown transaction token: tx_missing/,\n    )\n    await assert.rejects(\n      () => adapter.rollbackTransaction({ id: 'tx_missing' }),\n      /Unknown transaction token: tx_missing/,\n    )\n    await assert.rejects(\n      () => adapter.createSavepoint({ id: 'tx_missing' }, 'sp'),\n      /Unknown transaction token: tx_missing/,\n    )\n  })\n\n  it('compiles column-to-column comparisons from string references', async () => {\n    let statements: Array<{ text: string; values: unknown[] | undefined }> = []\n\n    let client = {\n      async query(text: string, values?: unknown[]) {\n        statements.push({ text, values })\n\n        return {\n          rows: [{ count: '0' }],\n          rowCount: 1,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n\n    await db\n      .query(accounts)\n      .join(projects, eq('accounts.id', 'projects.account_id'))\n      .where(eq('accounts.email', 'ops@example.com'))\n      .count()\n\n    assert.match(statements[0].text, /\"accounts\"\\.\"id\"\\s*=\\s*\"projects\"\\.\"account_id\"/)\n    assert.match(statements[0].text, /\"accounts\"\\.\"email\"\\s*=\\s*\\$1/)\n    assert.deepEqual(statements[0].values, ['ops@example.com'])\n  })\n\n  it('compiles cross-schema table references in joins', async () => {\n    let statements: Array<{ text: string; values: unknown[] | undefined }> = []\n\n    let client = {\n      async query(text: string, values?: unknown[]) {\n        statements.push({ text, values })\n\n        return {\n          rows: [{ count: '0' }],\n          rowCount: 1,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n\n    await db.query(invoices).join(accounts, eq(accounts.id, invoices.account_id)).count()\n\n    assert.match(statements[0].text, /from \"billing\"\\.\"invoices\"/)\n    assert.match(statements[0].text, /join \"accounts\"/)\n    assert.match(statements[0].text, /\"accounts\"\\.\"id\"\\s*=\\s*\"billing\"\\.\"invoices\"\\.\"account_id\"/)\n  })\n\n  it('treats dotted select aliases as single identifiers', async () => {\n    let statements: Array<{ text: string; values: unknown[] | undefined }> = []\n\n    let client = {\n      async query(text: string, values?: unknown[]) {\n        statements.push({ text, values })\n\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n\n    await db.query(accounts).select({ 'account.email': accounts.email }).all()\n\n    assert.match(statements[0].text, /as \"account\\.email\"/)\n  })\n\n  it('does not create dangling bind parameters for inList predicates', async () => {\n    let statements: Array<{ text: string; values: unknown[] | undefined }> = []\n\n    let client = {\n      async query(text: string, values?: unknown[]) {\n        statements.push({ text, values })\n\n        return {\n          rows: [{ count: '0' }],\n          rowCount: 1,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n\n    await db\n      .query(accounts)\n      .where(inList('id', [1, 3]))\n      .count()\n\n    assert.match(statements[0].text, /\"id\"\\s+in\\s+\\(\\$1,\\s*\\$2\\)/)\n    assert.deepEqual(statements[0].values, [1, 3])\n  })\n\n  it('normalizes bigint count rows', async () => {\n    let client = {\n      async query() {\n        return {\n          rows: [{ count: 5n }],\n          rowCount: 1,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n    let count = await db.query(accounts).count()\n\n    assert.equal(count, 5)\n  })\n\n  it('normalizes non-object rows and falls back count to row length', async () => {\n    let client = {\n      async query() {\n        return {\n          rows: [1, null, { count: 'oops' }],\n          rowCount: 3,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n    let count = await db.query(accounts).count()\n\n    assert.equal(count, 3)\n  })\n\n  it('uses returned row count when rowCount is null for writes', async () => {\n    let client = {\n      async query() {\n        return {\n          rows: [{ id: 1, email: 'a@example.com' }],\n          rowCount: null,\n          command: 'INSERT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n    let result = await db\n      .query(accounts)\n      .insert({ id: 1, email: 'a@example.com' }, { returning: '*' })\n\n    assert.equal(result.affectedRows, 1)\n    assert.equal(result.insertId, 1)\n  })\n\n  it('does not expose insertId for composite primary keys', async () => {\n    let client = {\n      async query() {\n        return {\n          rows: [{ account_id: 1, project_id: 2, email: 'team@example.com' }],\n          rowCount: 1,\n          command: 'INSERT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let db = createDatabase(createPostgresDatabaseAdapter(client as never))\n    let result = await db.query(accountProjects).insert(\n      {\n        account_id: 1,\n        project_id: 2,\n        email: 'team@example.com',\n      },\n      { returning: '*' },\n    )\n\n    assert.equal(result.affectedRows, 1)\n    assert.equal(result.insertId, undefined)\n  })\n\n  it('returns undefined affectedRows for raw statements with null rowCount', async () => {\n    let client = {\n      async query() {\n        return {\n          rows: [{ ok: true }],\n          rowCount: null,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let result = await createPostgresDatabaseAdapter(client as never).execute({\n      operation: {\n        kind: 'raw',\n        sql: {\n          text: 'select 1',\n          values: [],\n        },\n      },\n      transaction: undefined,\n    })\n\n    assert.equal(result.affectedRows, undefined)\n    assert.deepEqual(result.rows, [{ ok: true }])\n  })\n\n  it('executes migrate operations with transaction tokens and migration locks', async () => {\n    let statements: Array<{ text: string; values: unknown[] | undefined }> = []\n\n    let client = {\n      async query(text: string, values?: unknown[]) {\n        statements.push({ text, values })\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    }\n\n    let adapter = createPostgresDatabaseAdapter(client as never)\n    let token = await adapter.beginTransaction()\n\n    await adapter.acquireMigrationLock()\n    let result = await adapter.migrate({\n      operation: {\n        kind: 'alterTable',\n        table: { name: 'users' },\n        changes: [\n          { kind: 'addColumn', column: 'email', definition: { type: 'text', nullable: false } },\n          { kind: 'dropColumn', column: 'legacy_email', ifExists: true },\n        ],\n      },\n      transaction: token,\n    })\n    await adapter.releaseMigrationLock()\n    await adapter.commitTransaction(token)\n\n    assert.equal(result.affectedOperations, 2)\n    assert.deepEqual(\n      statements.map((statement) => statement.text),\n      [\n        'begin',\n        'select pg_advisory_lock(hashtext($1))',\n        'alter table \"users\" add column \"email\" text not null',\n        'alter table \"users\" drop column if exists \"legacy_email\"',\n        'select pg_advisory_unlock(hashtext($1))',\n        'commit',\n      ],\n    )\n    assert.deepEqual(statements[1].values, ['data_table_migrations'])\n    assert.deepEqual(statements[4].values, ['data_table_migrations'])\n  })\n\n  it('compiles rich table migrations including literals, references, and comments', () => {\n    let adapter = createPostgresDatabaseAdapter({\n      async query() {\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    } as never)\n\n    let compiled = adapter.compileSql({\n      kind: 'createTable',\n      table: { name: 'users' },\n      ifNotExists: true,\n      columns: {\n        id: { type: 'integer', nullable: false, primaryKey: true },\n        email: { type: 'varchar', length: 320, nullable: false, unique: true },\n        visits: { type: 'integer', default: { kind: 'literal', value: 0 } },\n        bigint_visits: { type: 'bigint', default: { kind: 'literal', value: 12n } },\n        is_admin: { type: 'boolean', default: { kind: 'literal', value: false } },\n        nickname: { type: 'text', default: { kind: 'literal', value: null } },\n        safe_slug: { type: 'text', default: { kind: 'sql', expression: 'md5(email)' } },\n        created_at: { type: 'timestamp', withTimezone: true, default: { kind: 'now' } },\n        birthday: {\n          type: 'date',\n          default: { kind: 'literal', value: new Date('2024-01-02T00:00:00.000Z') },\n        },\n        score: { type: 'decimal', precision: 10, scale: 2 },\n        ratio: { type: 'decimal', precision: 8 },\n        starts_at: { type: 'time', withTimezone: true },\n        metadata: { type: 'json' },\n        blob: { type: 'binary' },\n        role: { type: 'enum', enumValues: ['admin', 'user'] },\n        name: {\n          type: 'text',\n          checks: [{ expression: 'length(name) > 1', name: 'users_name_len_check' }],\n        },\n        manager_id: {\n          type: 'integer',\n          references: {\n            table: { schema: 'app', name: 'users' },\n            columns: ['id'],\n            name: 'users_manager_fk',\n            onDelete: 'set null',\n            onUpdate: 'cascade',\n          },\n        },\n        full_name: {\n          type: 'text',\n          computed: { expression: `first_name || ' ' || last_name`, stored: true },\n        },\n        escaped: { type: 'text', default: { kind: 'literal', value: \"O'Hare\" } },\n      },\n      primaryKey: { name: 'users_pk', columns: ['id'] },\n      uniques: [{ name: 'users_email_unique', columns: ['email'] }],\n      checks: [{ name: 'users_name_check', expression: 'length(name) > 1' }],\n      foreignKeys: [\n        {\n          name: 'users_account_fk',\n          columns: ['id'],\n          references: { table: { schema: 'app', name: 'accounts' }, columns: ['id'] },\n          onDelete: 'cascade',\n          onUpdate: 'restrict',\n        },\n      ],\n      comment: \"owner's table\",\n    })\n\n    assert.equal(compiled.length, 2)\n    assert.match(compiled[0].text, /create table if not exists \"users\" \\(/)\n    assert.match(compiled[0].text, /\"email\" varchar\\(320\\) not null unique/)\n    assert.match(compiled[0].text, /\"visits\" integer default 0/)\n    assert.match(compiled[0].text, /\"bigint_visits\" bigint default 12/)\n    assert.match(compiled[0].text, /\"is_admin\" boolean default false/)\n    assert.match(compiled[0].text, /\"nickname\" text default null/)\n    assert.match(compiled[0].text, /\"safe_slug\" text default md5\\(email\\)/)\n    assert.match(compiled[0].text, /\"created_at\" timestamp with time zone default now\\(\\)/)\n    assert.match(compiled[0].text, /\"birthday\" date default '2024-01-02T00:00:00.000Z'/)\n    assert.match(compiled[0].text, /\"score\" decimal\\(10, 2\\)/)\n    assert.match(compiled[0].text, /\"ratio\" decimal/)\n    assert.match(compiled[0].text, /\"starts_at\" time with time zone/)\n    assert.match(compiled[0].text, /\"metadata\" jsonb/)\n    assert.match(compiled[0].text, /\"blob\" bytea/)\n    assert.match(compiled[0].text, /\"role\" text/)\n    assert.match(compiled[0].text, /\"name\" text check \\(length\\(name\\) > 1\\)/)\n    assert.match(\n      compiled[0].text,\n      /\"manager_id\" integer references \"app\"\\.\"users\" \\(\"id\"\\) on delete set null on update cascade/,\n    )\n    assert.match(\n      compiled[0].text,\n      /\"full_name\" text generated always as \\(first_name \\|\\| ' ' \\|\\| last_name\\) stored/,\n    )\n    assert.match(compiled[0].text, /\"escaped\" text default 'O''Hare'/)\n    assert.match(compiled[0].text, /primary key \\(\"id\"\\)/)\n    assert.match(compiled[0].text, /constraint \"users_email_unique\" unique \\(\"email\"\\)/)\n    assert.match(compiled[0].text, /constraint \"users_name_check\" check \\(length\\(name\\) > 1\\)/)\n    assert.match(\n      compiled[0].text,\n      /constraint \"users_account_fk\" foreign key \\(\"id\"\\) references \"app\"\\.\"accounts\" \\(\"id\"\\) on delete cascade on update restrict/,\n    )\n    assert.equal(compiled[1].text, `comment on table \"users\" is 'owner''s table'`)\n  })\n\n  it('compiles alterTable changes and standalone DDL operations', () => {\n    let adapter = createPostgresDatabaseAdapter({\n      async query() {\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    } as never)\n\n    let alterStatements = adapter.compileSql({\n      kind: 'alterTable',\n      table: { schema: 'app', name: 'users' },\n      changes: [\n        { kind: 'addColumn', column: 'email', definition: { type: 'text', nullable: false } },\n        { kind: 'changeColumn', column: 'email', definition: { type: 'varchar', length: 255 } },\n        { kind: 'renameColumn', from: 'email', to: 'contact_email' },\n        { kind: 'dropColumn', column: 'legacy_email', ifExists: true },\n        { kind: 'addPrimaryKey', constraint: { name: 'users_pk', columns: ['id'] } },\n        { kind: 'dropPrimaryKey', name: 'users_pk' },\n        {\n          kind: 'addUnique',\n          constraint: { name: 'users_email_unique', columns: ['contact_email'] },\n        },\n        { kind: 'dropUnique', name: 'users_email_unique' },\n        {\n          kind: 'addForeignKey',\n          constraint: {\n            name: 'users_account_fk',\n            columns: ['account_id'],\n            references: { table: { name: 'accounts' }, columns: ['id'] },\n          },\n        },\n        { kind: 'dropForeignKey', name: 'users_account_fk' },\n        {\n          kind: 'addCheck',\n          constraint: { name: 'users_status_check', expression: \"status <> 'deleted'\" },\n        },\n        { kind: 'dropCheck', name: 'users_status_check' },\n        { kind: 'setTableComment', comment: 'Updated users table' },\n      ],\n    })\n\n    assert.equal(alterStatements.length, 13)\n    assert.equal(\n      alterStatements[0].text,\n      'alter table \"app\".\"users\" add column \"email\" text not null',\n    )\n    assert.equal(\n      alterStatements[1].text,\n      'alter table \"app\".\"users\" alter column \"email\" type varchar(255)',\n    )\n    assert.equal(\n      alterStatements[2].text,\n      'alter table \"app\".\"users\" rename column \"email\" to \"contact_email\"',\n    )\n    assert.equal(\n      alterStatements[3].text,\n      'alter table \"app\".\"users\" drop column if exists \"legacy_email\"',\n    )\n    assert.equal(\n      alterStatements[4].text,\n      'alter table \"app\".\"users\" add constraint \"users_pk\" primary key (\"id\")',\n    )\n    assert.equal(alterStatements[5].text, 'alter table \"app\".\"users\" drop constraint \"users_pk\"')\n    assert.equal(\n      alterStatements[6].text,\n      'alter table \"app\".\"users\" add constraint \"users_email_unique\" unique (\"contact_email\")',\n    )\n    assert.equal(\n      alterStatements[7].text,\n      'alter table \"app\".\"users\" drop constraint \"users_email_unique\"',\n    )\n    assert.equal(\n      alterStatements[8].text,\n      'alter table \"app\".\"users\" add constraint \"users_account_fk\" foreign key (\"account_id\") references \"accounts\" (\"id\")',\n    )\n    assert.equal(\n      alterStatements[9].text,\n      'alter table \"app\".\"users\" drop constraint \"users_account_fk\"',\n    )\n    assert.equal(\n      alterStatements[10].text,\n      `alter table \"app\".\"users\" add constraint \"users_status_check\" check (status <> 'deleted')`,\n    )\n    assert.equal(\n      alterStatements[11].text,\n      'alter table \"app\".\"users\" drop constraint \"users_status_check\"',\n    )\n    assert.equal(\n      alterStatements[12].text,\n      `comment on table \"app\".\"users\" is 'Updated users table'`,\n    )\n\n    let createIndex = adapter.compileSql({\n      kind: 'createIndex',\n      ifNotExists: true,\n      index: {\n        table: { name: 'users' },\n        name: 'email_idx',\n        columns: ['email'],\n        unique: true,\n        using: 'gin',\n        where: 'email is not null',\n      },\n    })\n    assert.equal(\n      createIndex[0].text,\n      'create unique index if not exists \"email_idx\" on \"users\" using gin (\"email\") where email is not null',\n    )\n\n    let dropIndex = adapter.compileSql({\n      kind: 'dropIndex',\n      table: { name: 'users' },\n      name: 'email_idx',\n      ifExists: true,\n    })\n    assert.equal(dropIndex[0].text, 'drop index if exists \"email_idx\"')\n\n    let renameIndex = adapter.compileSql({\n      kind: 'renameIndex',\n      table: { name: 'users' },\n      from: 'email_idx',\n      to: 'users_email_idx',\n    })\n    assert.equal(renameIndex[0].text, 'alter index \"email_idx\" rename to \"users_email_idx\"')\n\n    let renameTable = adapter.compileSql({\n      kind: 'renameTable',\n      from: { schema: 'app', name: 'users' },\n      to: { schema: 'app', name: 'members' },\n    })\n    assert.equal(renameTable[0].text, 'alter table \"app\".\"users\" rename to \"members\"')\n\n    let dropTable = adapter.compileSql({\n      kind: 'dropTable',\n      table: { name: 'users' },\n      ifExists: true,\n      cascade: true,\n    })\n    assert.equal(dropTable[0].text, 'drop table if exists \"users\" cascade')\n  })\n\n  it('throws for unsupported DDL kinds and non-stored computed columns', () => {\n    let adapter = createPostgresDatabaseAdapter({\n      async query() {\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    } as never)\n\n    assert.throws(\n      () => adapter.compileSql({ kind: 'unknown' } as never),\n      /Unsupported data migration operation kind/,\n    )\n    assert.throws(\n      () =>\n        adapter.compileSql({\n          kind: 'createTable',\n          table: { name: 'users' },\n          columns: {\n            full_name: {\n              type: 'text',\n              computed: {\n                expression: `first_name || ' ' || last_name`,\n                stored: false,\n              },\n            },\n          },\n        }),\n      /only supports stored computed\\/generated columns/,\n    )\n  })\n\n  it('compiles every DDL operation kind through compileSql()', () => {\n    let adapter = createPostgresDatabaseAdapter({\n      async query() {\n        return {\n          rows: [],\n          rowCount: 0,\n          command: 'SELECT',\n          oid: 0,\n          fields: [],\n        }\n      },\n    } as never)\n\n    let operations: DataMigrationOperation[] = [\n      {\n        kind: 'createTable',\n        table: { schema: 'app', name: 'users' },\n        ifNotExists: true,\n        columns: {\n          id: { type: 'integer', nullable: false, primaryKey: true },\n        },\n      },\n      {\n        kind: 'alterTable',\n        table: { schema: 'app', name: 'users' },\n        changes: [\n          { kind: 'addColumn', column: 'email', definition: { type: 'text', nullable: false } },\n        ],\n      },\n      {\n        kind: 'renameTable',\n        from: { schema: 'app', name: 'users' },\n        to: { schema: 'app', name: 'accounts' },\n      },\n      { kind: 'dropTable', table: { schema: 'app', name: 'accounts' }, ifExists: true },\n      {\n        kind: 'createIndex',\n        index: {\n          table: { schema: 'app', name: 'users' },\n          columns: ['email'],\n          name: 'users_email_idx',\n        },\n      },\n      { kind: 'dropIndex', table: { schema: 'app', name: 'users' }, name: 'users_email_idx' },\n      {\n        kind: 'renameIndex',\n        table: { schema: 'app', name: 'users' },\n        from: 'users_email_idx',\n        to: 'users_email_idx_new',\n      },\n      {\n        kind: 'addForeignKey',\n        table: { schema: 'app', name: 'projects' },\n        constraint: {\n          columns: ['account_id'],\n          references: {\n            table: { schema: 'app', name: 'accounts' },\n            columns: ['id'],\n          },\n          name: 'projects_account_id_fk',\n          onDelete: 'cascade',\n        },\n      },\n      {\n        kind: 'dropForeignKey',\n        table: { schema: 'app', name: 'projects' },\n        name: 'projects_account_id_fk',\n      },\n      {\n        kind: 'addCheck',\n        table: { schema: 'app', name: 'users' },\n        constraint: {\n          name: 'users_email_check',\n          expression: \"position('@' in email) > 1\",\n        },\n      },\n      { kind: 'dropCheck', table: { schema: 'app', name: 'users' }, name: 'users_email_check' },\n      { kind: 'raw', sql: sql`select 1` },\n    ]\n\n    for (let operation of operations) {\n      let compiled = adapter.compileSql(operation)\n      assert.ok(compiled.length > 0, operation.kind)\n    }\n  })\n})\n"
  },
  {
    "path": "packages/data-table-postgres/src/lib/adapter.ts",
    "content": "import type {\n  AdapterCapabilityOverrides,\n  DataMigrationRequest,\n  DataManipulationRequest,\n  DataMigrationResult,\n  DataMigrationOperation,\n  DataManipulationResult,\n  DataManipulationOperation,\n  DatabaseAdapter,\n  ColumnDefinition,\n  SqlStatement,\n  TableRef,\n  TransactionOptions,\n  TransactionToken,\n} from '@remix-run/data-table'\nimport { getTablePrimaryKey } from '@remix-run/data-table'\nimport {\n  isDataManipulationOperation as isDataManipulationOperationHelper,\n  quoteLiteral as quoteLiteralHelper,\n  quoteTableRef as quoteTableRefHelper,\n} from '@remix-run/data-table/sql-helpers'\nimport type { Pool as PostgresPool, PoolClient as PostgresPoolClient } from 'pg'\n\nimport { compilePostgresOperation } from './sql-compiler.ts'\n\n/**\n * Postgres adapter configuration.\n */\nexport type PostgresDatabaseAdapterOptions = {\n  capabilities?: AdapterCapabilityOverrides\n}\n\ntype TransactionState = {\n  client: PostgresPoolClient\n  releaseOnClose: boolean\n}\n\ntype PostgresQueryable = PostgresPool | PostgresPoolClient\n\n/**\n * `DatabaseAdapter` implementation for postgres-compatible clients.\n */\nexport class PostgresDatabaseAdapter implements DatabaseAdapter {\n  /**\n   * The SQL dialect identifier reported by this adapter.\n   */\n  dialect = 'postgres'\n\n  /**\n   * Feature flags describing the postgres behaviors supported by this adapter.\n   */\n  capabilities\n\n  #client: PostgresQueryable\n  #transactions = new Map<string, TransactionState>()\n  #transactionCounter = 0\n\n  constructor(client: PostgresQueryable, options?: PostgresDatabaseAdapterOptions) {\n    this.#client = client\n    this.capabilities = {\n      returning: options?.capabilities?.returning ?? true,\n      savepoints: options?.capabilities?.savepoints ?? true,\n      upsert: options?.capabilities?.upsert ?? true,\n      transactionalDdl: options?.capabilities?.transactionalDdl ?? true,\n      migrationLock: options?.capabilities?.migrationLock ?? true,\n    }\n  }\n\n  /**\n   * Compiles a data or migration operation to postgres SQL statements.\n   * @param operation Operation to compile.\n   * @returns Compiled SQL statements.\n   */\n  compileSql(operation: DataManipulationOperation | DataMigrationOperation): SqlStatement[] {\n    if (isDataManipulationOperation(operation)) {\n      let compiled = compilePostgresOperation(operation)\n      return [{ text: compiled.text, values: compiled.values }]\n    }\n\n    return compilePostgresMigrationOperations(operation)\n  }\n\n  /**\n   * Executes a postgres data-manipulation request.\n   * @param request Request to execute.\n   * @returns Execution result.\n   */\n  async execute(request: DataManipulationRequest): Promise<DataManipulationResult> {\n    if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {\n      return {\n        affectedRows: 0,\n        insertId: undefined,\n        rows: request.operation.returning ? [] : undefined,\n      }\n    }\n\n    let statement = compilePostgresOperation(request.operation)\n    let client = this.#resolveClient(request.transaction)\n    let result = await client.query(statement.text, statement.values)\n    let rows = normalizeRows(result.rows)\n\n    if (request.operation.kind === 'count' || request.operation.kind === 'exists') {\n      rows = normalizeCountRows(rows)\n    }\n\n    return {\n      rows,\n      affectedRows: normalizeAffectedRows(request.operation.kind, result.rowCount, rows),\n      insertId: normalizeInsertId(request.operation.kind, request.operation, rows),\n    }\n  }\n\n  /**\n   * Executes postgres migration operations.\n   * @param request Migration request to execute.\n   * @returns Migration result.\n   */\n  async migrate(request: DataMigrationRequest): Promise<DataMigrationResult> {\n    let statements = this.compileSql(request.operation)\n    let client = this.#resolveClient(request.transaction)\n\n    for (let statement of statements) {\n      await client.query(statement.text, statement.values)\n    }\n\n    return {\n      affectedOperations: statements.length,\n    }\n  }\n\n  /**\n   * Checks whether a table exists in postgres.\n   * @param table Table reference to inspect.\n   * @param transaction Optional transaction token.\n   * @returns `true` when the table exists.\n   */\n  async hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {\n    let relation = toPostgresRelationName(table)\n    let client = this.#resolveClient(transaction)\n    let result = await client.query('select to_regclass($1) is not null as \"exists\"', [relation])\n    let row = result.rows[0] as Record<string, unknown> | undefined\n    return toBooleanExists(row?.exists)\n  }\n\n  /**\n   * Checks whether a column exists in postgres.\n   * @param table Table reference to inspect.\n   * @param column Column name to look up.\n   * @param transaction Optional transaction token.\n   * @returns `true` when the column exists.\n   */\n  async hasColumn(\n    table: TableRef,\n    column: string,\n    transaction?: TransactionToken,\n  ): Promise<boolean> {\n    let relation = toPostgresRelationName(table)\n    let client = this.#resolveClient(transaction)\n    let result = await client.query(\n      'select exists (select 1 from pg_attribute where attrelid = to_regclass($1) and attname = $2 and attnum > 0 and not attisdropped) as \"exists\"',\n      [relation, column],\n    )\n    let row = result.rows[0] as Record<string, unknown> | undefined\n    return toBooleanExists(row?.exists)\n  }\n\n  /**\n   * Starts a postgres transaction.\n   * @param options Transaction options.\n   * @returns Transaction token.\n   */\n  async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {\n    let releaseOnClose = false\n    let transactionClient: PostgresPoolClient\n\n    if (isPostgresPool(this.#client)) {\n      transactionClient = await this.#client.connect()\n      releaseOnClose = true\n    } else {\n      transactionClient = this.#client\n    }\n\n    await transactionClient.query('begin')\n\n    if (options?.isolationLevel || options?.readOnly !== undefined) {\n      await transactionClient.query(buildSetTransactionStatement(options))\n    }\n\n    this.#transactionCounter += 1\n    let token = { id: 'tx_' + String(this.#transactionCounter) }\n\n    this.#transactions.set(token.id, {\n      client: transactionClient,\n      releaseOnClose,\n    })\n\n    return token\n  }\n\n  /**\n   * Commits an open postgres transaction.\n   * @param token Transaction token to commit.\n   * @returns A promise that resolves when the transaction is committed.\n   */\n  async commitTransaction(token: TransactionToken): Promise<void> {\n    let transaction = this.#transactions.get(token.id)\n\n    if (!transaction) {\n      throw new Error('Unknown transaction token: ' + token.id)\n    }\n\n    try {\n      await transaction.client.query('commit')\n    } finally {\n      this.#transactions.delete(token.id)\n\n      if (transaction.releaseOnClose) {\n        releasePostgresClient(transaction.client)\n      }\n    }\n  }\n\n  /**\n   * Rolls back an open postgres transaction.\n   * @param token Transaction token to roll back.\n   * @returns A promise that resolves when the transaction is rolled back.\n   */\n  async rollbackTransaction(token: TransactionToken): Promise<void> {\n    let transaction = this.#transactions.get(token.id)\n\n    if (!transaction) {\n      throw new Error('Unknown transaction token: ' + token.id)\n    }\n\n    try {\n      await transaction.client.query('rollback')\n    } finally {\n      this.#transactions.delete(token.id)\n\n      if (transaction.releaseOnClose) {\n        releasePostgresClient(transaction.client)\n      }\n    }\n  }\n\n  /**\n   * Creates a savepoint in an open postgres transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the savepoint is created.\n   */\n  async createSavepoint(token: TransactionToken, name: string): Promise<void> {\n    let client = this.#transactionClient(token)\n    await client.query('savepoint ' + quoteIdentifier(name))\n  }\n\n  /**\n   * Rolls back to a savepoint in an open postgres transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the rollback completes.\n   */\n  async rollbackToSavepoint(token: TransactionToken, name: string): Promise<void> {\n    let client = this.#transactionClient(token)\n    await client.query('rollback to savepoint ' + quoteIdentifier(name))\n  }\n\n  /**\n   * Releases a savepoint in an open postgres transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the savepoint is released.\n   */\n  async releaseSavepoint(token: TransactionToken, name: string): Promise<void> {\n    let client = this.#transactionClient(token)\n    await client.query('release savepoint ' + quoteIdentifier(name))\n  }\n\n  /**\n   * Acquires the postgres migration lock.\n   * @returns A promise that resolves when the lock is acquired.\n   */\n  async acquireMigrationLock(): Promise<void> {\n    await this.#client.query('select pg_advisory_lock(hashtext($1))', ['data_table_migrations'])\n  }\n\n  /**\n   * Releases the postgres migration lock.\n   * @returns A promise that resolves when the lock is released.\n   */\n  async releaseMigrationLock(): Promise<void> {\n    await this.#client.query('select pg_advisory_unlock(hashtext($1))', ['data_table_migrations'])\n  }\n\n  #resolveClient(token: TransactionToken | undefined): PostgresQueryable {\n    if (!token) {\n      return this.#client\n    }\n\n    return this.#transactionClient(token)\n  }\n\n  #transactionClient(token: TransactionToken): PostgresPoolClient {\n    let transaction = this.#transactions.get(token.id)\n\n    if (!transaction) {\n      throw new Error('Unknown transaction token: ' + token.id)\n    }\n\n    return transaction.client\n  }\n}\n\n/**\n * Creates a postgres `DatabaseAdapter`.\n * @param client `pg` pool or pool client.\n * @param options Optional adapter capability overrides.\n * @returns A configured postgres adapter.\n * @example\n * ```ts\n * import { Pool } from 'pg'\n * import { createDatabase } from 'remix/data-table'\n * import { createPostgresDatabaseAdapter } from 'remix/data-table-postgres'\n *\n * let pool = new Pool({ connectionString: process.env.DATABASE_URL })\n * let adapter = createPostgresDatabaseAdapter(pool)\n * let db = createDatabase(adapter)\n * ```\n */\nexport function createPostgresDatabaseAdapter(\n  client: PostgresQueryable,\n  options?: PostgresDatabaseAdapterOptions,\n): PostgresDatabaseAdapter {\n  return new PostgresDatabaseAdapter(client, options)\n}\n\nfunction isPostgresPool(client: PostgresQueryable): client is PostgresPool {\n  return 'connect' in client && typeof client.connect === 'function'\n}\n\nfunction releasePostgresClient(client: PostgresPoolClient): void {\n  let release = (client as { release?: () => void }).release\n  release?.()\n}\n\nfunction buildSetTransactionStatement(options: TransactionOptions): string {\n  let parts = ['set transaction']\n\n  if (options.isolationLevel) {\n    parts.push('isolation level ' + options.isolationLevel)\n  }\n\n  if (options.readOnly !== undefined) {\n    parts.push(options.readOnly ? 'read only' : 'read write')\n  }\n\n  return parts.join(' ')\n}\n\nfunction normalizeRows(rows: unknown[]): Record<string, unknown>[] {\n  return rows.map((row) => {\n    if (typeof row !== 'object' || row === null) {\n      return {}\n    }\n\n    return { ...(row as Record<string, unknown>) }\n  })\n}\n\nfunction normalizeCountRows(rows: Record<string, unknown>[]): Record<string, unknown>[] {\n  return rows.map((row) => {\n    let count = row.count\n\n    if (typeof count === 'string') {\n      let numeric = Number(count)\n\n      if (!Number.isNaN(numeric)) {\n        return {\n          ...row,\n          count: numeric,\n        }\n      }\n    }\n\n    if (typeof count === 'bigint') {\n      return {\n        ...row,\n        count: Number(count),\n      }\n    }\n\n    return row\n  })\n}\n\nfunction normalizeAffectedRows(\n  kind: DataManipulationRequest['operation']['kind'],\n  rowCount: number | null,\n  rows: Record<string, unknown>[],\n): number | undefined {\n  if (kind === 'select' || kind === 'count' || kind === 'exists') {\n    return undefined\n  }\n\n  if (rowCount !== null) {\n    return rowCount\n  }\n\n  if (kind === 'raw') {\n    return undefined\n  }\n\n  return rows.length\n}\n\nfunction normalizeInsertId(\n  kind: DataManipulationRequest['operation']['kind'],\n  operation: DataManipulationRequest['operation'],\n  rows: Record<string, unknown>[],\n): unknown {\n  if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {\n    return undefined\n  }\n\n  let primaryKey = getTablePrimaryKey(operation.table)\n\n  if (primaryKey.length !== 1) {\n    return undefined\n  }\n\n  let key = primaryKey[0]\n  let row = rows[rows.length - 1]\n\n  return row ? row[key] : undefined\n}\n\nfunction quoteIdentifier(value: string): string {\n  return '\"' + value.replace(/\"/g, '\"\"') + '\"'\n}\n\nfunction toPostgresRelationName(table: TableRef): string {\n  if (table.schema) {\n    return quoteIdentifier(table.schema) + '.' + quoteIdentifier(table.name)\n  }\n\n  return quoteIdentifier(table.name)\n}\n\nfunction toBooleanExists(value: unknown): boolean {\n  if (typeof value === 'boolean') {\n    return value\n  }\n\n  if (typeof value === 'number') {\n    return value > 0\n  }\n\n  if (typeof value === 'string') {\n    return value === 't' || value === 'true' || value === '1'\n  }\n\n  return false\n}\n\nfunction isInsertOperationKind(kind: DataManipulationRequest['operation']['kind']): boolean {\n  return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'\n}\n\nfunction isInsertOperation(\n  operation: DataManipulationRequest['operation'],\n): operation is Extract<\n  DataManipulationRequest['operation'],\n  { kind: 'insert' | 'insertMany' | 'upsert' }\n> {\n  return (\n    operation.kind === 'insert' || operation.kind === 'insertMany' || operation.kind === 'upsert'\n  )\n}\n\nfunction isDataManipulationOperation(\n  operation: DataManipulationOperation | DataMigrationOperation,\n): operation is DataManipulationOperation {\n  return isDataManipulationOperationHelper(operation)\n}\n\nfunction compilePostgresMigrationOperations(operation: DataMigrationOperation): SqlStatement[] {\n  if (operation.kind === 'raw') {\n    return [{ text: operation.sql.text, values: [...operation.sql.values] }]\n  }\n\n  if (operation.kind === 'createTable') {\n    let columns = Object.keys(operation.columns).map(\n      (columnName) =>\n        quoteIdentifier(columnName) + ' ' + compilePostgresColumn(operation.columns[columnName]),\n    )\n    let tableConstraints: string[] = []\n\n    if (operation.primaryKey) {\n      tableConstraints.push(\n        'constraint ' +\n          quoteIdentifier(operation.primaryKey.name) +\n          ' primary key (' +\n          operation.primaryKey.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')',\n      )\n    }\n\n    for (let unique of operation.uniques ?? []) {\n      tableConstraints.push(\n        'constraint ' +\n          quoteIdentifier(unique.name) +\n          ' ' +\n          'unique (' +\n          unique.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')',\n      )\n    }\n\n    for (let check of operation.checks ?? []) {\n      tableConstraints.push(\n        'constraint ' + quoteIdentifier(check.name) + ' ' + 'check (' + check.expression + ')',\n      )\n    }\n\n    for (let foreignKey of operation.foreignKeys ?? []) {\n      let clause =\n        'constraint ' +\n        quoteIdentifier(foreignKey.name) +\n        ' ' +\n        'foreign key (' +\n        foreignKey.columns.map((column) => quoteIdentifier(column)).join(', ') +\n        ') references ' +\n        quoteTableRef(foreignKey.references.table) +\n        ' (' +\n        foreignKey.references.columns.map((column) => quoteIdentifier(column)).join(', ') +\n        ')'\n\n      if (foreignKey.onDelete) {\n        clause += ' on delete ' + foreignKey.onDelete\n      }\n\n      if (foreignKey.onUpdate) {\n        clause += ' on update ' + foreignKey.onUpdate\n      }\n\n      tableConstraints.push(clause)\n    }\n\n    let sql =\n      'create table ' +\n      (operation.ifNotExists ? 'if not exists ' : '') +\n      quoteTableRef(operation.table) +\n      ' (' +\n      [...columns, ...tableConstraints].join(', ') +\n      ')'\n    let statements: SqlStatement[] = [{ text: sql, values: [] }]\n\n    if (operation.comment) {\n      statements.push({\n        text:\n          'comment on table ' +\n          quoteTableRef(operation.table) +\n          ' is ' +\n          quoteLiteral(operation.comment),\n        values: [],\n      })\n    }\n\n    return statements\n  }\n\n  if (operation.kind === 'alterTable') {\n    let sqlStatements: SqlStatement[] = []\n\n    for (let change of operation.changes) {\n      let sql = 'alter table ' + quoteTableRef(operation.table) + ' '\n\n      if (change.kind === 'addColumn') {\n        sql +=\n          'add column ' +\n          quoteIdentifier(change.column) +\n          ' ' +\n          compilePostgresColumn(change.definition)\n      } else if (change.kind === 'changeColumn') {\n        let typeSql = compilePostgresColumnType(change.definition)\n        sql += 'alter column ' + quoteIdentifier(change.column) + ' type ' + typeSql\n      } else if (change.kind === 'renameColumn') {\n        sql += 'rename column ' + quoteIdentifier(change.from) + ' to ' + quoteIdentifier(change.to)\n      } else if (change.kind === 'dropColumn') {\n        sql +=\n          'drop column ' + (change.ifExists ? 'if exists ' : '') + quoteIdentifier(change.column)\n      } else if (change.kind === 'addPrimaryKey') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'primary key (' +\n          change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')'\n      } else if (change.kind === 'dropPrimaryKey') {\n        sql += 'drop constraint ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'addUnique') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'unique (' +\n          change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')'\n      } else if (change.kind === 'dropUnique') {\n        sql += 'drop constraint ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'addForeignKey') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'foreign key (' +\n          change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ') references ' +\n          quoteTableRef(change.constraint.references.table) +\n          ' (' +\n          change.constraint.references.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')'\n      } else if (change.kind === 'dropForeignKey') {\n        sql += 'drop constraint ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'addCheck') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'check (' +\n          change.constraint.expression +\n          ')'\n      } else if (change.kind === 'dropCheck') {\n        sql += 'drop constraint ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'setTableComment') {\n        sqlStatements.push({\n          text:\n            'comment on table ' +\n            quoteTableRef(operation.table) +\n            ' is ' +\n            quoteLiteral(change.comment),\n          values: [],\n        })\n        continue\n      } else {\n        continue\n      }\n\n      sqlStatements.push({ text: sql, values: [] })\n    }\n\n    return sqlStatements\n  }\n\n  if (operation.kind === 'renameTable') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.from) +\n          ' rename to ' +\n          quoteIdentifier(operation.to.name),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropTable') {\n    return [\n      {\n        text:\n          'drop table ' +\n          (operation.ifExists ? 'if exists ' : '') +\n          quoteTableRef(operation.table) +\n          (operation.cascade ? ' cascade' : ''),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'createIndex') {\n    return [\n      {\n        text:\n          'create ' +\n          (operation.index.unique ? 'unique ' : '') +\n          'index ' +\n          (operation.ifNotExists ? 'if not exists ' : '') +\n          quoteIdentifier(operation.index.name) +\n          ' on ' +\n          quoteTableRef(operation.index.table) +\n          (operation.index.using ? ' using ' + operation.index.using : '') +\n          ' (' +\n          operation.index.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')' +\n          (operation.index.where ? ' where ' + operation.index.where : ''),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropIndex') {\n    return [\n      {\n        text:\n          'drop index ' +\n          (operation.ifExists ? 'if exists ' : '') +\n          quoteIdentifier(operation.name),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'renameIndex') {\n    return [\n      {\n        text:\n          'alter index ' +\n          quoteIdentifier(operation.from) +\n          ' rename to ' +\n          quoteIdentifier(operation.to),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'addForeignKey') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' add ' +\n          'constraint ' +\n          quoteIdentifier(operation.constraint.name) +\n          ' ' +\n          'foreign key (' +\n          operation.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ') references ' +\n          quoteTableRef(operation.constraint.references.table) +\n          ' (' +\n          operation.constraint.references.columns\n            .map((column) => quoteIdentifier(column))\n            .join(', ') +\n          ')' +\n          (operation.constraint.onDelete ? ' on delete ' + operation.constraint.onDelete : '') +\n          (operation.constraint.onUpdate ? ' on update ' + operation.constraint.onUpdate : ''),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropForeignKey') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' drop constraint ' +\n          quoteIdentifier(operation.name),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'addCheck') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' add ' +\n          'constraint ' +\n          quoteIdentifier(operation.constraint.name) +\n          ' ' +\n          'check (' +\n          operation.constraint.expression +\n          ')',\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropCheck') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' drop constraint ' +\n          quoteIdentifier(operation.name),\n        values: [],\n      },\n    ]\n  }\n\n  throw new Error('Unsupported data migration operation kind')\n}\n\nfunction compilePostgresColumn(definition: ColumnDefinition): string {\n  let parts = [compilePostgresColumnType(definition)]\n\n  if (definition.nullable === false) {\n    parts.push('not null')\n  }\n\n  if (definition.default) {\n    if (definition.default.kind === 'now') {\n      parts.push('default now()')\n    } else if (definition.default.kind === 'sql') {\n      parts.push('default ' + definition.default.expression)\n    } else {\n      parts.push('default ' + quoteLiteral(definition.default.value))\n    }\n  }\n\n  if (definition.primaryKey) {\n    parts.push('primary key')\n  }\n\n  if (definition.unique) {\n    parts.push('unique')\n  }\n\n  if (definition.computed) {\n    if (!definition.computed.stored) {\n      throw new Error('Postgres only supports stored computed/generated columns')\n    }\n\n    parts.push('generated always as (' + definition.computed.expression + ') stored')\n  }\n\n  if (definition.references) {\n    let clause =\n      'references ' +\n      quoteTableRef(definition.references.table) +\n      ' (' +\n      definition.references.columns.map((column) => quoteIdentifier(column)).join(', ') +\n      ')'\n\n    if (definition.references.onDelete) {\n      clause += ' on delete ' + definition.references.onDelete\n    }\n\n    if (definition.references.onUpdate) {\n      clause += ' on update ' + definition.references.onUpdate\n    }\n\n    parts.push(clause)\n  }\n\n  if (definition.checks && definition.checks.length > 0) {\n    for (let check of definition.checks) {\n      parts.push('check (' + check.expression + ')')\n    }\n  }\n\n  return parts.join(' ')\n}\n\nfunction compilePostgresColumnType(definition: ColumnDefinition): string {\n  if (definition.type === 'varchar') {\n    return 'varchar(' + String(definition.length ?? 255) + ')'\n  }\n\n  if (definition.type === 'text') {\n    return 'text'\n  }\n\n  if (definition.type === 'integer') {\n    return 'integer'\n  }\n\n  if (definition.type === 'bigint') {\n    return 'bigint'\n  }\n\n  if (definition.type === 'decimal') {\n    if (definition.precision !== undefined && definition.scale !== undefined) {\n      return 'decimal(' + String(definition.precision) + ', ' + String(definition.scale) + ')'\n    }\n\n    return 'decimal'\n  }\n\n  if (definition.type === 'boolean') {\n    return 'boolean'\n  }\n\n  if (definition.type === 'uuid') {\n    return 'uuid'\n  }\n\n  if (definition.type === 'date') {\n    return 'date'\n  }\n\n  if (definition.type === 'time') {\n    return definition.withTimezone ? 'time with time zone' : 'time without time zone'\n  }\n\n  if (definition.type === 'timestamp') {\n    return definition.withTimezone ? 'timestamp with time zone' : 'timestamp without time zone'\n  }\n\n  if (definition.type === 'json') {\n    return 'jsonb'\n  }\n\n  if (definition.type === 'binary') {\n    return 'bytea'\n  }\n\n  if (definition.type === 'enum') {\n    return 'text'\n  }\n\n  return 'text'\n}\n\nfunction quoteTableRef(table: TableRef): string {\n  return quoteTableRefHelper(table, quoteIdentifier)\n}\n\nfunction quoteLiteral(value: unknown): string {\n  return quoteLiteralHelper(value)\n}\n"
  },
  {
    "path": "packages/data-table-postgres/src/lib/sql-compiler.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { beforeEach, describe, it } from 'node:test'\nimport {\n  and,\n  between,\n  column,\n  createDatabase,\n  table,\n  eq,\n  gt,\n  gte,\n  ilike,\n  inList,\n  isNull,\n  like,\n  lt,\n  lte,\n  ne,\n  notInList,\n  notNull,\n  type DataManipulationOperation,\n  type DatabaseAdapter,\n  or,\n} from '@remix-run/data-table'\n\nimport { compilePostgresOperation } from './sql-compiler.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n    email: column.text(),\n    status: column.text(),\n    deleted: column.boolean(),\n  },\n})\n\nlet tasks = table({\n  name: 'tasks',\n  columns: {\n    id: column.integer(),\n    name: column.text(),\n    account_id: column.integer(),\n  },\n})\n\nlet statements: DataManipulationOperation[] = []\n\nlet fakeAdapter = {\n  capabilities: {\n    upsert: true,\n    returning: true,\n  },\n\n  execute: async (request) => {\n    statements.push(request.operation)\n    return {}\n  },\n} as DatabaseAdapter\n\nlet db = createDatabase(fakeAdapter)\n\ndescribe('postgres sql-compiler', () => {\n  beforeEach(() => {\n    statements = []\n  })\n\n  describe('select statement', () => {\n    it('compile wildcard selection', async () => {\n      await db.query(accounts).all()\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\"',\n        values: [],\n      })\n    })\n\n    it('compile selected aliases', async () => {\n      await db\n        .query(accounts)\n        .select({\n          accountId: accounts.id,\n          accountEmail: accounts.email,\n        })\n        .all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select \"accounts\".\"id\" as \"accountId\", \"accounts\".\"email\" as \"accountEmail\" from \"accounts\"',\n        values: [],\n      })\n    })\n\n    it('compile joins', async () => {\n      await db.query(accounts).join(tasks, eq(accounts.id, tasks.account_id)).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" inner join \"tasks\" on \"accounts\".\"id\" = \"tasks\".\"account_id\"',\n        values: [],\n      })\n    })\n\n    it('compile left join', async () => {\n      await db.query(accounts).leftJoin(tasks, eq(accounts.id, tasks.account_id)).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" left join \"tasks\" on \"accounts\".\"id\" = \"tasks\".\"account_id\"',\n        values: [],\n      })\n    })\n\n    it('compile right join', async () => {\n      await db.query(accounts).rightJoin(tasks, eq(accounts.id, tasks.account_id)).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" right join \"tasks\" on \"accounts\".\"id\" = \"tasks\".\"account_id\"',\n        values: [],\n      })\n    })\n\n    it('compile object where filters', async () => {\n      await db.query(accounts).where({ status: 'enabled' }).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"status\" = $1))',\n        values: ['enabled'],\n      })\n    })\n\n    it('compile null where filters', async () => {\n      await db.query(accounts).where({ status: null }).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"status\" is null))',\n        values: [],\n      })\n    })\n\n    it('compile predicate operators', async () => {\n      await db.query(accounts).where(ne('status', 'disabled')).where(gt('id', 10)).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"status\" <> $1) and (\"id\" > $2)',\n        values: ['disabled', 10],\n      })\n    })\n\n    it('compile gte/lt/lte/between/like/ilike predicates', async () => {\n      await db\n        .query(accounts)\n        .where(gte('id', 1))\n        .where(lt('id', 20))\n        .where(lte('id', 30))\n        .where(between('id', 2, 9))\n        .where(like('email', '%@example.com'))\n        .where(ilike('email', '%@example.com'))\n        .all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"id\" >= $1) and (\"id\" < $2) and (\"id\" <= $3) and (\"id\" between $4 and $5) and (\"email\" like $6) and (\"email\" ilike $7)',\n        values: [1, 20, 30, 2, 9, '%@example.com', '%@example.com'],\n      })\n    })\n\n    it('compile in-list predicates', async () => {\n      await db\n        .query(accounts)\n        .where(inList('id', [1, 2]))\n        .all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"id\" in ($1, $2))',\n        values: [1, 2],\n      })\n    })\n\n    it('compile empty in-list predicates', async () => {\n      await db.query(accounts).where(inList('id', [])).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (1 = 0)',\n        values: [],\n      })\n    })\n\n    it('compile not-in predicates', async () => {\n      await db\n        .query(accounts)\n        .where(notInList('id', [1, 2]))\n        .all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"id\" not in ($1, $2))',\n        values: [1, 2],\n      })\n    })\n\n    it('compile logical combinators', async () => {\n      await db\n        .query(accounts)\n        .where(\n          and(\n            eq(accounts.id, 1),\n            or(eq(accounts.status, 'enabled'), eq(accounts.status, 'disabled')),\n          ),\n        )\n        .all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"accounts\".\"id\" = $1) and ((\"accounts\".\"status\" = $2) or (\"accounts\".\"status\" = $3)))',\n        values: [1, 'enabled', 'disabled'],\n      })\n    })\n\n    it('compile empty logical combinators', async () => {\n      await db.query(accounts).where(and()).where(or()).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (1 = 1) and (1 = 0)',\n        values: [],\n      })\n    })\n\n    it('compile group by and having', async () => {\n      await db.query(tasks).groupBy(tasks.account_id).having({ account_id: 20 }).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"tasks\" group by \"tasks\".\"account_id\" having ((\"account_id\" = $1))',\n        values: [20],\n      })\n    })\n\n    it('compile pagination', async () => {\n      await db.query(accounts).offset(5).limit(10).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" limit 10 offset 5',\n        values: [],\n      })\n    })\n\n    it('compile distinct selection with order by', async () => {\n      await db.query(accounts).distinct().orderBy('id', 'desc').all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select distinct * from \"accounts\" order by \"id\" DESC',\n        values: [],\n      })\n    })\n\n    it('compile boolean bindings', async () => {\n      await db.query(accounts).where({ deleted: true }).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"deleted\" = $1))',\n        values: [true],\n      })\n    })\n\n    it('compile boolean predicates', async () => {\n      await db.query(accounts).where(isNull(accounts.status)).where(notNull(accounts.email)).all()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"accounts\".\"status\" is null) and (\"accounts\".\"email\" is not null)',\n        values: [],\n      })\n    })\n\n    it('compile wildcard segment path', () => {\n      let compiled = compilePostgresOperation({\n        kind: 'select',\n        table: accounts,\n        select: [{ column: 'accounts.*', alias: 'allColumns' }],\n        joins: [],\n        where: [],\n        groupBy: [],\n        having: [],\n        orderBy: [],\n        limit: undefined,\n        offset: undefined,\n        distinct: false,\n      })\n\n      assert.deepEqual(compiled, {\n        text: 'select \"accounts\".* as \"allColumns\" from \"accounts\"',\n        values: [],\n      })\n    })\n  })\n\n  describe('count - exists statement', () => {\n    it('compile count', async () => {\n      await db.query(tasks).where({ account_id: 1 }).count()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select count(*) as \"count\" from (select 1 from \"tasks\" where ((\"account_id\" = $1))) as \"__dt_count\"',\n        values: [1],\n      })\n    })\n\n    it('compile exists', async () => {\n      await db.query(tasks).where({ account_id: 1 }).exists()\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select count(*) as \"count\" from (select 1 from \"tasks\" where ((\"account_id\" = $1))) as \"__dt_count\"',\n        values: [1],\n      })\n    })\n  })\n\n  describe('insert statement', () => {\n    it('compile for one', async () => {\n      await db.create(accounts, {\n        id: 1,\n        email: 'info@remix.run',\n        status: 'enabled',\n      })\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"id\", \"email\", \"status\") values ($1, $2, $3)',\n        values: [1, 'info@remix.run', 'enabled'],\n      })\n    })\n\n    it('compile for one and return values', async () => {\n      await db.query(accounts).insert(\n        {\n          id: 1,\n          email: 'info@remix.run',\n          status: 'enabled',\n        },\n        { returning: '*' },\n      )\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"id\", \"email\", \"status\") values ($1, $2, $3) returning *',\n        values: [1, 'info@remix.run', 'enabled'],\n      })\n    })\n\n    it('compile for one and return selected columns', async () => {\n      await db.query(accounts).insert(\n        {\n          id: 2,\n          email: 'contact@remix.run',\n          status: 'active',\n        },\n        { returning: ['id', 'email'] },\n      )\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"id\", \"email\", \"status\") values ($1, $2, $3) returning \"id\", \"email\"',\n        values: [2, 'contact@remix.run', 'active'],\n      })\n    })\n\n    it('compile for one with default values', async () => {\n      await db.create(accounts, {})\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" default values',\n        values: [],\n      })\n    })\n\n    it('compile for many', async () => {\n      await db.createMany(accounts, [\n        { id: 1, email: 'info@remix.run', status: 'enabled' },\n        { id: 2, email: 'contact@remix.run', status: 'draft' },\n      ])\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"id\", \"email\", \"status\") values ($1, $2, $3), ($4, $5, $6)',\n        values: [1, 'info@remix.run', 'enabled', 2, 'contact@remix.run', 'draft'],\n      })\n    })\n\n    it('compile for many with default values', () => {\n      let compiled = compilePostgresOperation({\n        kind: 'insertMany',\n        table: accounts,\n        values: [{}, {}],\n      })\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" default values',\n        values: [],\n      })\n    })\n\n    it('compile for many without data', async () => {\n      await db.createMany(accounts, [])\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select 0 where 1 = 0',\n        values: [],\n      })\n    })\n  })\n\n  describe('update statement', () => {\n    it('compile for one', async () => {\n      await db.query(accounts).where({ id: 1 }).update({\n        email: 'info@remix.run',\n        status: 'enabled',\n      })\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'update \"accounts\" set \"email\" = $1, \"status\" = $2 where ((\"id\" = $3))',\n        values: ['info@remix.run', 'enabled', 1],\n      })\n    })\n\n    it('compile for many', async () => {\n      await db.updateMany(\n        accounts,\n        {\n          email: 'info@remix.run',\n          status: 'enabled',\n        },\n        {\n          where: {\n            status: 'disabled',\n          },\n        },\n      )\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'update \"accounts\" set \"email\" = $1, \"status\" = $2 where ((\"status\" = $3))',\n        values: ['info@remix.run', 'enabled', 'disabled'],\n      })\n    })\n  })\n\n  describe('upsert statement', () => {\n    it('throws without values', async () => {\n      await db.query(accounts).upsert(\n        {},\n        {\n          conflictTarget: ['id'],\n        },\n      )\n\n      assert.throws(() => compilePostgresOperation(statements[0]))\n    })\n\n    it('compile with update columns', async () => {\n      await db.query(accounts).upsert(\n        {\n          status: 'enabled',\n          email: 'info@remix.run',\n        },\n        {\n          conflictTarget: ['id'],\n          update: {\n            email: 'contact@remix.run',\n          },\n        },\n      )\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"status\", \"email\") values ($1, $2) on conflict (\"id\") do update set \"email\" = $3',\n        values: ['enabled', 'info@remix.run', 'contact@remix.run'],\n      })\n    })\n\n    it('compile without update columns', async () => {\n      await db.query(accounts).upsert(\n        {\n          status: 'enabled',\n          email: 'info@remix.run',\n        },\n        {\n          conflictTarget: ['id'],\n          update: {},\n        },\n      )\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"status\", \"email\") values ($1, $2) on conflict (\"id\") do nothing',\n        values: ['enabled', 'info@remix.run'],\n      })\n    })\n  })\n\n  describe('delete statement', () => {\n    it('compile for one', async () => {\n      await db.delete(accounts, 10)\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'delete from \"accounts\" where ((\"id\" = $1))',\n        values: [10],\n      })\n    })\n\n    it('compile for many', async () => {\n      await db.deleteMany(accounts, {\n        where: {\n          status: 'enabled',\n        },\n      })\n\n      let compiled = compilePostgresOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'delete from \"accounts\" where ((\"status\" = $1))',\n        values: ['enabled'],\n      })\n    })\n  })\n\n  describe('raw statement', () => {\n    it('compile', () => {\n      let compiled = compilePostgresOperation({\n        kind: 'raw',\n        sql: {\n          text: 'select * from accounts where id = ? and status = ?',\n          values: [10, 'active'],\n        },\n      })\n\n      assert.deepEqual(compiled, {\n        text: 'select * from accounts where id = $1 and status = $2',\n        values: [10, 'active'],\n      })\n    })\n\n    it('compile without placeholders', () => {\n      let compiled = compilePostgresOperation({\n        kind: 'raw',\n        sql: {\n          text: 'select 1',\n          values: [],\n        },\n      })\n\n      assert.deepEqual(compiled, {\n        text: 'select 1',\n        values: [],\n      })\n    })\n  })\n\n  describe('error handling', () => {\n    it('throws for unsupported statements', () => {\n      assert.throws(\n        () => compilePostgresOperation({ kind: 'unknown' } as never),\n        /Unsupported operation kind/,\n      )\n    })\n\n    it('throws for unsupported predicates', () => {\n      assert.throws(\n        () =>\n          compilePostgresOperation({\n            kind: 'select',\n            table: accounts,\n            select: '*',\n            joins: [],\n            where: [{ type: 'unknown' } as never],\n            groupBy: [],\n            having: [],\n            orderBy: [],\n            limit: undefined,\n            offset: undefined,\n            distinct: false,\n          }),\n        /Unsupported predicate/,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/data-table-postgres/src/lib/sql-compiler.ts",
    "content": "import { getTableName, getTablePrimaryKey } from '@remix-run/data-table'\nimport type { DataManipulationOperation, Predicate, SqlStatement } from '@remix-run/data-table'\nimport {\n  collectColumns as collectColumnsHelper,\n  normalizeJoinType as normalizeJoinTypeHelper,\n  quotePath as quotePathHelper,\n} from '@remix-run/data-table/sql-helpers'\n\ntype JoinClause = Extract<DataManipulationOperation, { kind: 'select' }>['joins'][number]\ntype UpsertOperation = Extract<DataManipulationOperation, { kind: 'upsert' }>\ntype OperationTable = Extract<DataManipulationOperation, { kind: 'select' }>['table']\n\ntype CompileContext = {\n  values: unknown[]\n}\n\nexport function compilePostgresOperation(operation: DataManipulationOperation): SqlStatement {\n  if (operation.kind === 'raw') {\n    return compileRawOperation(operation.sql)\n  }\n\n  let context: CompileContext = { values: [] }\n\n  if (operation.kind === 'select') {\n    let selection = '*'\n\n    if (operation.select !== '*') {\n      selection = operation.select\n        .map((field) => quotePath(field.column) + ' as ' + quoteIdentifier(field.alias))\n        .join(', ')\n    }\n\n    let text =\n      'select ' +\n      (operation.distinct ? 'distinct ' : '') +\n      selection +\n      compileFromClause(operation.table, operation.joins, context) +\n      compileWhereClause(operation.where, context) +\n      compileGroupByClause(operation.groupBy) +\n      compileHavingClause(operation.having, context) +\n      compileOrderByClause(operation.orderBy) +\n      compileLimitClause(operation.limit) +\n      compileOffsetClause(operation.offset)\n\n    return {\n      text,\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'count' || operation.kind === 'exists') {\n    let inner =\n      'select 1' +\n      compileFromClause(operation.table, operation.joins, context) +\n      compileWhereClause(operation.where, context) +\n      compileGroupByClause(operation.groupBy) +\n      compileHavingClause(operation.having, context)\n\n    return {\n      text:\n        'select count(*) as ' +\n        quoteIdentifier('count') +\n        ' from (' +\n        inner +\n        ') as ' +\n        quoteIdentifier('__dt_count'),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'insert') {\n    return compileInsertOperation(operation.table, operation.values, operation.returning, context)\n  }\n\n  if (operation.kind === 'insertMany') {\n    return compileInsertManyOperation(\n      operation.table,\n      operation.values,\n      operation.returning,\n      context,\n    )\n  }\n\n  if (operation.kind === 'update') {\n    let changes = Object.keys(operation.changes)\n    let assignments = changes\n      .map((column) => quotePath(column) + ' = ' + pushValue(context, operation.changes[column]))\n      .join(', ')\n\n    return {\n      text:\n        'update ' +\n        quotePath(getTableName(operation.table)) +\n        ' set ' +\n        assignments +\n        compileWhereClause(operation.where, context) +\n        compileReturningClause(operation.returning),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'delete') {\n    return {\n      text:\n        'delete from ' +\n        quotePath(getTableName(operation.table)) +\n        compileWhereClause(operation.where, context) +\n        compileReturningClause(operation.returning),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'upsert') {\n    return compileUpsertOperation(operation, context)\n  }\n\n  throw new Error('Unsupported operation kind')\n}\n\nfunction compileInsertOperation(\n  table: OperationTable,\n  values: Record<string, unknown>,\n  returning: '*' | string[] | undefined,\n  context: CompileContext,\n): SqlStatement {\n  let columns = Object.keys(values)\n\n  if (columns.length === 0) {\n    return {\n      text:\n        'insert into ' +\n        quotePath(getTableName(table)) +\n        ' default values' +\n        compileReturningClause(returning),\n      values: context.values,\n    }\n  }\n\n  let quotedColumns = columns.map((column) => quotePath(column))\n  let placeholders = columns.map((column) => pushValue(context, values[column]))\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(table)) +\n      ' (' +\n      quotedColumns.join(', ') +\n      ') values (' +\n      placeholders.join(', ') +\n      ')' +\n      compileReturningClause(returning),\n    values: context.values,\n  }\n}\n\nfunction compileInsertManyOperation(\n  table: OperationTable,\n  rows: Record<string, unknown>[],\n  returning: '*' | string[] | undefined,\n  context: CompileContext,\n): SqlStatement {\n  if (rows.length === 0) {\n    return {\n      text: 'select 0 where 1 = 0',\n      values: context.values,\n    }\n  }\n\n  let columns = collectColumns(rows)\n\n  if (columns.length === 0) {\n    return {\n      text:\n        'insert into ' +\n        quotePath(getTableName(table)) +\n        ' default values' +\n        compileReturningClause(returning),\n      values: context.values,\n    }\n  }\n\n  let quotedColumns = columns.map((column) => quotePath(column))\n\n  let valueSets = rows.map((row) => {\n    let placeholders = columns.map((column) => {\n      let value = Object.prototype.hasOwnProperty.call(row, column) ? row[column] : null\n      return pushValue(context, value)\n    })\n\n    return '(' + placeholders.join(', ') + ')'\n  })\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(table)) +\n      ' (' +\n      quotedColumns.join(', ') +\n      ') values ' +\n      valueSets.join(', ') +\n      compileReturningClause(returning),\n    values: context.values,\n  }\n}\n\nfunction compileUpsertOperation(operation: UpsertOperation, context: CompileContext): SqlStatement {\n  let insertColumns = Object.keys(operation.values)\n  let conflictTarget = operation.conflictTarget ?? [...getTablePrimaryKey(operation.table)]\n\n  if (insertColumns.length === 0) {\n    throw new Error('upsert requires at least one value')\n  }\n\n  let quotedInsertColumns = insertColumns.map((column) => quotePath(column))\n  let insertPlaceholders = insertColumns.map((column) =>\n    pushValue(context, operation.values[column]),\n  )\n\n  let updateValues = operation.update ?? operation.values\n  let updateColumns = Object.keys(updateValues)\n  let onConflictClause = ''\n\n  if (updateColumns.length === 0) {\n    onConflictClause =\n      ' on conflict (' +\n      conflictTarget.map((column: string) => quotePath(column)).join(', ') +\n      ') do nothing'\n  } else {\n    onConflictClause =\n      ' on conflict (' +\n      conflictTarget.map((column: string) => quotePath(column)).join(', ') +\n      ') do update set ' +\n      updateColumns\n        .map((column) => quotePath(column) + ' = ' + pushValue(context, updateValues[column]))\n        .join(', ')\n  }\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(operation.table)) +\n      ' (' +\n      quotedInsertColumns.join(', ') +\n      ') values (' +\n      insertPlaceholders.join(', ') +\n      ')' +\n      onConflictClause +\n      compileReturningClause(operation.returning),\n    values: context.values,\n  }\n}\n\nfunction compileRawOperation(statement: SqlStatement): SqlStatement {\n  if (!statement.text.includes('?')) {\n    return {\n      text: statement.text,\n      values: [...statement.values],\n    }\n  }\n\n  let index = 1\n  let text = statement.text.replace(/\\?/g, function replaceParameter() {\n    let placeholder = '$' + String(index)\n    index += 1\n    return placeholder\n  })\n\n  return {\n    text,\n    values: [...statement.values],\n  }\n}\n\nfunction compileFromClause(\n  table: OperationTable,\n  joins: JoinClause[],\n  context: CompileContext,\n): string {\n  let output = ' from ' + quotePath(getTableName(table))\n\n  for (let join of joins) {\n    output +=\n      ' ' +\n      normalizeJoinType(join.type) +\n      ' join ' +\n      quotePath(getTableName(join.table)) +\n      ' on ' +\n      compilePredicate(join.on, context)\n  }\n\n  return output\n}\n\nfunction compileWhereClause(predicates: Predicate[], context: CompileContext): string {\n  if (predicates.length === 0) {\n    return ''\n  }\n\n  let where = predicates\n    .map((predicate) => '(' + compilePredicate(predicate, context) + ')')\n    .join(' and ')\n\n  return ' where ' + where\n}\n\nfunction compileGroupByClause(columns: string[]): string {\n  if (columns.length === 0) {\n    return ''\n  }\n\n  return ' group by ' + columns.map((column) => quotePath(column)).join(', ')\n}\n\nfunction compileHavingClause(predicates: Predicate[], context: CompileContext): string {\n  if (predicates.length === 0) {\n    return ''\n  }\n\n  let having = predicates\n    .map((predicate) => '(' + compilePredicate(predicate, context) + ')')\n    .join(' and ')\n\n  return ' having ' + having\n}\n\nfunction compileOrderByClause(orderBy: { column: string; direction: 'asc' | 'desc' }[]): string {\n  if (orderBy.length === 0) {\n    return ''\n  }\n\n  return (\n    ' order by ' +\n    orderBy\n      .map((clause) => quotePath(clause.column) + ' ' + clause.direction.toUpperCase())\n      .join(', ')\n  )\n}\n\nfunction compileLimitClause(limit: number | undefined): string {\n  if (limit === undefined) {\n    return ''\n  }\n\n  return ' limit ' + String(limit)\n}\n\nfunction compileOffsetClause(offset: number | undefined): string {\n  if (offset === undefined) {\n    return ''\n  }\n\n  return ' offset ' + String(offset)\n}\n\nfunction compileReturningClause(returning: '*' | string[] | undefined): string {\n  if (!returning) {\n    return ''\n  }\n\n  if (returning === '*') {\n    return ' returning *'\n  }\n\n  return ' returning ' + returning.map((column) => quotePath(column)).join(', ')\n}\n\nfunction compilePredicate(predicate: Predicate, context: CompileContext): string {\n  if (predicate.type === 'comparison') {\n    let column = quotePath(predicate.column)\n\n    if (predicate.operator === 'eq') {\n      if (\n        predicate.valueType === 'value' &&\n        (predicate.value === null || predicate.value === undefined)\n      ) {\n        return column + ' is null'\n      }\n\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' = ' + comparisonValue\n    }\n\n    if (predicate.operator === 'ne') {\n      if (\n        predicate.valueType === 'value' &&\n        (predicate.value === null || predicate.value === undefined)\n      ) {\n        return column + ' is not null'\n      }\n\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' <> ' + comparisonValue\n    }\n\n    if (predicate.operator === 'gt') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' > ' + comparisonValue\n    }\n\n    if (predicate.operator === 'gte') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' >= ' + comparisonValue\n    }\n\n    if (predicate.operator === 'lt') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' < ' + comparisonValue\n    }\n\n    if (predicate.operator === 'lte') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' <= ' + comparisonValue\n    }\n\n    if (predicate.operator === 'in' || predicate.operator === 'notIn') {\n      let values = Array.isArray(predicate.value) ? predicate.value : []\n\n      if (values.length === 0) {\n        return predicate.operator === 'in' ? '1 = 0' : '1 = 1'\n      }\n\n      let placeholders = values.map((value) => pushValue(context, value))\n      let keyword = predicate.operator === 'in' ? 'in' : 'not in'\n\n      return column + ' ' + keyword + ' (' + placeholders.join(', ') + ')'\n    }\n\n    if (predicate.operator === 'like') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' like ' + comparisonValue\n    }\n\n    if (predicate.operator === 'ilike') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' ilike ' + comparisonValue\n    }\n  }\n\n  if (predicate.type === 'between') {\n    return (\n      quotePath(predicate.column) +\n      ' between ' +\n      pushValue(context, predicate.lower) +\n      ' and ' +\n      pushValue(context, predicate.upper)\n    )\n  }\n\n  if (predicate.type === 'null') {\n    return (\n      quotePath(predicate.column) + (predicate.operator === 'isNull' ? ' is null' : ' is not null')\n    )\n  }\n\n  if (predicate.type === 'logical') {\n    if (predicate.predicates.length === 0) {\n      return predicate.operator === 'and' ? '1 = 1' : '1 = 0'\n    }\n\n    let childOperator = predicate.operator === 'and' ? ' and ' : ' or '\n    let childPredicates = predicate.predicates\n      .map((child) => '(' + compilePredicate(child, context) + ')')\n      .join(childOperator)\n\n    return childPredicates\n  }\n\n  throw new Error('Unsupported predicate')\n}\n\nfunction compileComparisonValue(\n  predicate: Extract<Predicate, { type: 'comparison' }>,\n  context: CompileContext,\n): string {\n  if (predicate.valueType === 'column') {\n    return quotePath(predicate.value)\n  }\n\n  return pushValue(context, predicate.value)\n}\n\nfunction normalizeJoinType(type: string): string {\n  return normalizeJoinTypeHelper(type)\n}\n\nfunction quoteIdentifier(value: string): string {\n  return '\"' + value.replace(/\"/g, '\"\"') + '\"'\n}\n\nfunction quotePath(path: string): string {\n  return quotePathHelper(path, quoteIdentifier)\n}\n\nfunction pushValue(context: CompileContext, value: unknown): string {\n  context.values.push(value)\n  return '$' + String(context.values.length)\n}\n\nfunction collectColumns(rows: Record<string, unknown>[]): string[] {\n  return collectColumnsHelper(rows)\n}\n"
  },
  {
    "path": "packages/data-table-postgres/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/data-table-postgres/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"types\": [\"node\"],\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"vendor\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/data-table-sqlite/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/data-table-sqlite/.changes/minor.ddl-migration-contract.md",
    "content": "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.\n\nNormal reads/writes continue through `execute(...)`, while migration/DDL work runs through `migrate(...)`.\n\nSQL compilation remains adapter-owned and can share helpers from `remix/data-table/sql-helpers`.\n"
  },
  {
    "path": "packages/data-table-sqlite/.changes/minor.introspection-migration-transaction-tokens.md",
    "content": "Add transaction-aware migration introspection to the sqlite adapter.\n\n`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.\n"
  },
  {
    "path": "packages/data-table-sqlite/CHANGELOG.md",
    "content": "# `data-table-sqlite` CHANGELOG\n\nThis 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/).\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release of `@remix-run/data-table-sqlite`.\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`data-table@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.1.0)\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/data-table-sqlite/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/data-table-sqlite/README.md",
    "content": "# data-table-sqlite\n\nSQLite adapter for [`remix/data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table).\nUse this package when you want `data-table` APIs backed by `better-sqlite3`.\n\n## Features\n\n- **Native `better-sqlite3` Integration**: Works well for local and embedded deployments\n- **Full `data-table` API Support**: Queries, relations, writes, and transactions\n- **Adapter-Owned Compiler**: SQL compilation lives in this adapter, with optional shared pure helpers from `data-table`\n- **Migration DDL Support**: Compiles and executes `DataMigrationOperation` operations for `remix/data-table/migrations`\n- **SQLite Capabilities Enabled By Default**:\n  - `returning: true`\n  - `savepoints: true`\n  - `upsert: true`\n  - `transactionalDdl: true`\n  - `migrationLock: false`\n\n## Installation\n\n```sh\nnpm i remix better-sqlite3\n```\n\n## Usage\n\n```ts\nimport Database from 'better-sqlite3'\nimport { createDatabase } from 'remix/data-table'\nimport { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'\n\nlet sqlite = new Database('app.db')\nlet db = createDatabase(createSqliteDatabaseAdapter(sqlite))\n```\n\nThis is a good fit for local development, embedded deployments, and single-node services.\nImport any driver-specific types you need directly from `better-sqlite3`.\n\n## Adapter Capabilities\n\n`data-table-sqlite` reports this capability set by default:\n\n- `returning: true`\n- `savepoints: true`\n- `upsert: true`\n- `transactionalDdl: true`\n- `migrationLock: false`\n\n## Advanced Usage\n\n### In-Memory Database For Tests\n\n```ts\nimport Database from 'better-sqlite3'\nimport { createDatabase } from 'remix/data-table'\nimport { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'\n\nlet sqlite = new Database(':memory:')\nlet db = createDatabase(createSqliteDatabaseAdapter(sqlite))\n```\n\n### Capability Overrides For Fallback Testing\n\n```ts\nimport { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'\n\nlet adapter = createSqliteDatabaseAdapter(sqlite, {\n  capabilities: {\n    returning: false,\n  },\n})\n```\n\n## Related Packages\n\n- [`data-table`](https://github.com/remix-run/remix/tree/main/packages/data-table) - Core query/relations API\n- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Schema parsing and validation\n- [`data-table-postgres`](https://github.com/remix-run/remix/tree/main/packages/data-table-postgres) - PostgreSQL adapter\n- [`data-table-mysql`](https://github.com/remix-run/remix/tree/main/packages/data-table-mysql) - MySQL adapter\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/data-table-sqlite/package.json",
    "content": "{\n  \"name\": \"@remix-run/data-table-sqlite\",\n  \"version\": \"0.1.0\",\n  \"description\": \"SQLite adapter for remix/data-table\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/data-table-sqlite\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/data-table-sqlite#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/data-table\": \"workspace:*\",\n    \"@types/better-sqlite3\": \"^7.6.13\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"better-sqlite3\": \"^12.4.1\"\n  },\n  \"dependencies\": {\n    \"@remix-run/data-table\": \"workspace:^\"\n  },\n  \"peerDependencies\": {\n    \"better-sqlite3\": \"^12.4.1\"\n  },\n  \"peerDependenciesMeta\": {\n    \"better-sqlite3\": {\n      \"optional\": true\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"test:coverage\": \"node --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-lines=90 --test-coverage-branches=90 --test-coverage-functions=90 --test ./src/lib/adapter.test.ts ./src/lib/sql-compiler.test.ts\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"remix\",\n    \"orm\",\n    \"sqlite\",\n    \"database\",\n    \"sql\"\n  ]\n}\n"
  },
  {
    "path": "packages/data-table-sqlite/src/index.ts",
    "content": "export type { SqliteDatabaseAdapterOptions } from './lib/adapter.ts'\nexport { createSqliteDatabaseAdapter, SqliteDatabaseAdapter } from './lib/adapter.ts'\n"
  },
  {
    "path": "packages/data-table-sqlite/src/lib/adapter.integration.test.ts",
    "content": "import { after, before, describe } from 'node:test'\nimport BetterSqlite3, { type Database as BetterSqliteDatabase } from 'better-sqlite3'\nimport { createDatabase } from '@remix-run/data-table'\n\nimport {\n  resetAdapterIntegrationSchema,\n  setupAdapterIntegrationSchema,\n  teardownAdapterIntegrationSchema,\n} from '../../../data-table/test/adapter-integration-schema.ts'\nimport { runAdapterIntegrationContract } from '../../../data-table/test/adapter-integration-contract.ts'\n\nimport { createSqliteDatabaseAdapter } from './adapter.ts'\n\nlet integrationEnabled = process.env.DATA_TABLE_INTEGRATION === '1' && canOpenSqliteDatabase()\n\ndescribe('sqlite adapter integration', () => {\n  let sqlite: BetterSqliteDatabase\n\n  before(async () => {\n    if (!integrationEnabled) {\n      return\n    }\n\n    sqlite = new BetterSqlite3(':memory:')\n    await setupAdapterIntegrationSchema(async (statement) => {\n      sqlite.exec(statement)\n    }, 'sqlite')\n  })\n\n  after(async () => {\n    if (!integrationEnabled) {\n      return\n    }\n\n    await teardownAdapterIntegrationSchema(async (statement) => {\n      sqlite.exec(statement)\n    }, 'sqlite')\n    sqlite.close()\n  })\n\n  runAdapterIntegrationContract({\n    integrationEnabled,\n    createDatabase: () => createDatabase(createSqliteDatabaseAdapter(sqlite)),\n    resetDatabase: async () => {\n      await resetAdapterIntegrationSchema(async (statement) => {\n        sqlite.exec(statement)\n      }, 'sqlite')\n    },\n  })\n})\n\nfunction canOpenSqliteDatabase(): boolean {\n  try {\n    let database = new BetterSqlite3(':memory:')\n    database.close()\n    return true\n  } catch {\n    return false\n  }\n}\n"
  },
  {
    "path": "packages/data-table-sqlite/src/lib/adapter.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\nimport Database from 'better-sqlite3'\nimport type { DataMigrationOperation } from '@remix-run/data-table'\nimport { column, createDatabase, table, eq } from '@remix-run/data-table'\n\nimport { createSqliteDatabaseAdapter } from './adapter.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n    email: column.text(),\n    status: column.text(),\n  },\n})\n\nlet projects = table({\n  name: 'projects',\n  columns: {\n    id: column.integer(),\n    account_id: column.integer(),\n    name: column.text(),\n  },\n})\n\nlet accountProjects = table({\n  name: 'account_projects',\n  columns: {\n    account_id: column.integer(),\n    project_id: column.integer(),\n    email: column.text(),\n  },\n  primaryKey: ['account_id', 'project_id'],\n})\n\nlet sqliteAvailable = canOpenSqliteDatabase()\n\ndescribe('sqlite adapter', { skip: !sqliteAvailable }, () => {\n  it('short-circuits insertMany([]) and returns empty rows for returning queries', async () => {\n    let prepareCalls = 0\n    let sqlite = {\n      prepare() {\n        prepareCalls += 1\n        return {\n          reader: false,\n          run() {\n            return { changes: 0, lastInsertRowid: 0 }\n          },\n          all() {\n            return []\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let result = await adapter.execute({\n      operation: {\n        kind: 'insertMany',\n        table: accounts,\n        values: [],\n        returning: ['id'],\n      },\n      transaction: undefined,\n    })\n\n    assert.deepEqual(result, {\n      affectedRows: 0,\n      insertId: undefined,\n      rows: [],\n    })\n    assert.equal(prepareCalls, 0)\n  })\n\n  it('checks table and column existence through adapter introspection hooks', async () => {\n    let preparedStatements: string[] = []\n\n    let sqlite = {\n      prepare(statement: string) {\n        preparedStatements.push(statement)\n\n        if (statement.includes('sqlite_master')) {\n          return {\n            get() {\n              return { exists: 1 }\n            },\n            all() {\n              return []\n            },\n            run() {\n              return { changes: 0, lastInsertRowid: 0 }\n            },\n          }\n        }\n\n        return {\n          get() {\n            return undefined\n          },\n          all() {\n            return [{ name: 'id' }, { name: 'email' }]\n          },\n          run() {\n            return { changes: 0, lastInsertRowid: 0 }\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let hasTable = await adapter.hasTable({ name: 'users' })\n    let hasColumn = await adapter.hasColumn({ schema: 'app', name: 'users' }, 'email')\n\n    assert.equal(hasTable, true)\n    assert.equal(hasColumn, true)\n    assert.equal(\n      preparedStatements[0],\n      'select 1 from sqlite_master where type = ? and name = ? limit 1',\n    )\n    assert.equal(preparedStatements[1], 'pragma \"app\".table_info(\"users\")')\n  })\n\n  it('enables read uncommitted pragma for read-uncommitted transactions', async () => {\n    let pragmas: string[] = []\n    let execs: string[] = []\n\n    let sqlite = {\n      prepare() {\n        throw new Error('not used')\n      },\n      pragma(statement: string) {\n        pragmas.push(statement)\n      },\n      exec(statement: string) {\n        execs.push(statement)\n      },\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let token = await adapter.beginTransaction({ isolationLevel: 'read uncommitted' })\n    await adapter.commitTransaction(token)\n\n    assert.deepEqual(pragmas, ['read_uncommitted = true'])\n    assert.deepEqual(execs, ['begin', 'commit'])\n  })\n\n  it('supports rollback and savepoint lifecycle with escaped names', async () => {\n    let execs: string[] = []\n\n    let sqlite = {\n      prepare() {\n        throw new Error('not used')\n      },\n      pragma() {},\n      exec(statement: string) {\n        execs.push(statement)\n      },\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let token = await adapter.beginTransaction()\n\n    await adapter.createSavepoint(token, 'sp\"name')\n    await adapter.rollbackToSavepoint(token, 'sp\"name')\n    await adapter.releaseSavepoint(token, 'sp\"name')\n    await adapter.rollbackTransaction(token)\n\n    assert.deepEqual(execs, [\n      'begin',\n      'savepoint \"sp\"\"name\"',\n      'rollback to savepoint \"sp\"\"name\"',\n      'release savepoint \"sp\"\"name\"',\n      'rollback',\n    ])\n  })\n\n  it('throws for unknown transaction tokens', async () => {\n    let sqlite = {\n      prepare() {\n        throw new Error('not used')\n      },\n      pragma() {},\n      exec() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n\n    await assert.rejects(\n      () => adapter.commitTransaction({ id: 'tx_missing' }),\n      /Unknown transaction token: tx_missing/,\n    )\n    await assert.rejects(\n      () => adapter.rollbackTransaction({ id: 'tx_missing' }),\n      /Unknown transaction token: tx_missing/,\n    )\n    await assert.rejects(\n      () => adapter.createSavepoint({ id: 'tx_missing' }, 'sp'),\n      /Unknown transaction token: tx_missing/,\n    )\n    await assert.rejects(\n      () => adapter.hasTable({ name: 'users' }, { id: 'tx_missing' }),\n      /Unknown transaction token: tx_missing/,\n    )\n    await assert.rejects(\n      () => adapter.hasColumn({ name: 'users' }, 'email', { id: 'tx_missing' }),\n      /Unknown transaction token: tx_missing/,\n    )\n  })\n\n  it('normalizes non-object rows and count values in reader mode', async () => {\n    let sqlite = {\n      prepare() {\n        return {\n          reader: true,\n          all() {\n            return [1, null, { count: '2' }, { count: 'oops' }, { count: 5n }]\n          },\n          run() {\n            throw new Error('not used')\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let result = await adapter.execute({\n      operation: {\n        kind: 'count',\n        table: accounts,\n        joins: [],\n        where: [],\n        groupBy: [],\n        having: [],\n      },\n      transaction: undefined,\n    })\n\n    assert.deepEqual(result.rows, [{}, {}, { count: 2 }, { count: 'oops' }, { count: 5 }])\n    assert.equal(result.affectedRows, undefined)\n    assert.equal(result.insertId, undefined)\n  })\n\n  it('returns undefined metadata for run-mode select statements', async () => {\n    let sqlite = {\n      prepare() {\n        return {\n          reader: false,\n          all() {\n            return []\n          },\n          run() {\n            return { changes: 3, lastInsertRowid: 7 }\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let result = await adapter.execute({\n      operation: {\n        kind: 'select',\n        table: accounts,\n        select: '*',\n        joins: [],\n        where: [],\n        groupBy: [],\n        having: [],\n        orderBy: [],\n        limit: undefined,\n        offset: undefined,\n        distinct: false,\n      },\n      transaction: undefined,\n    })\n\n    assert.equal(result.affectedRows, undefined)\n    assert.equal(result.insertId, undefined)\n  })\n\n  it('does not expose insertId for composite primary keys in run mode', async () => {\n    let sqlite = {\n      prepare() {\n        return {\n          reader: false,\n          all() {\n            return []\n          },\n          run() {\n            return { changes: 1, lastInsertRowid: 42 }\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let result = await adapter.execute({\n      operation: {\n        kind: 'insert',\n        table: accountProjects,\n        values: {\n          account_id: 1,\n          project_id: 2,\n          email: 'team@example.com',\n        },\n      },\n      transaction: undefined,\n    })\n\n    assert.equal(result.affectedRows, 1)\n    assert.equal(result.insertId, undefined)\n  })\n\n  it('does not expose insertId for composite primary keys in reader mode', async () => {\n    let sqlite = {\n      prepare() {\n        return {\n          reader: true,\n          all() {\n            return [{ account_id: 1, project_id: 2 }]\n          },\n          run() {\n            return { changes: 1, lastInsertRowid: 42 }\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let result = await adapter.execute({\n      operation: {\n        kind: 'insert',\n        table: accountProjects,\n        values: {\n          account_id: 1,\n          project_id: 2,\n          email: 'team@example.com',\n        },\n        returning: ['account_id', 'project_id'],\n      },\n      transaction: undefined,\n    })\n\n    assert.equal(result.affectedRows, 1)\n    assert.equal(result.insertId, undefined)\n  })\n\n  it('does not expose insertId for non-insert writes', async () => {\n    let sqlite = {\n      prepare() {\n        return {\n          reader: false,\n          all() {\n            return []\n          },\n          run() {\n            return { changes: 1, lastInsertRowid: 99 }\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let db = createDatabase(createSqliteDatabaseAdapter(sqlite as never))\n    let result = await db.updateMany(accounts, { status: 'inactive' }, { where: { id: 1 } })\n\n    assert.equal(result.affectedRows, 1)\n    assert.equal(result.insertId, undefined)\n  })\n\n  it('executes migrate operations', async () => {\n    let statements: Array<{ text: string; values: unknown[] }> = []\n\n    let sqlite = {\n      prepare(text: string) {\n        return {\n          reader: false,\n          all() {\n            return []\n          },\n          run(...values: unknown[]) {\n            statements.push({ text, values })\n            return { changes: 0, lastInsertRowid: 0 }\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let result = await adapter.migrate({\n      operation: {\n        kind: 'dropCheck',\n        table: { name: 'accounts' },\n        name: 'accounts_status_check',\n      },\n    })\n\n    assert.equal(result.affectedOperations, 1)\n    assert.deepEqual(statements[0], {\n      text: 'alter table \"accounts\" drop constraint \"accounts_status_check\"',\n      values: [],\n    })\n  })\n\n  it('compiles migration statements for rich create and alter operations', () => {\n    let sqlite = {\n      prepare() {\n        return {\n          reader: false,\n          all() {\n            return []\n          },\n          run() {\n            return { changes: 0, lastInsertRowid: 0 }\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n\n    let createTableStatements = adapter.compileSql({\n      kind: 'createTable',\n      table: { name: 'users' },\n      ifNotExists: true,\n      columns: {\n        id: { type: 'integer', nullable: false, primaryKey: true },\n        email: { type: 'varchar', unique: true },\n        display_name: { type: 'text', default: { kind: 'literal', value: \"o'hare\" } },\n        created_at: { type: 'timestamp', default: { kind: 'now' } },\n        updated_at: {\n          type: 'timestamp',\n          default: { kind: 'sql', expression: '(current_timestamp)' },\n        },\n        reviewed_at: {\n          type: 'timestamp',\n          default: { kind: 'literal', value: new Date('2026-01-01T00:00:00.000Z') },\n        },\n        optional_note: { type: 'text', default: { kind: 'literal', value: null } },\n        total: { type: 'decimal', default: { kind: 'literal', value: 3.5 } },\n        is_active: { type: 'boolean', default: { kind: 'literal', value: false } },\n        public_id: { type: 'uuid' },\n        due_on: { type: 'date' },\n        starts_at: { type: 'time' },\n        payload: { type: 'json' },\n        blob_data: { type: 'binary' },\n        big_total: { type: 'bigint', default: { kind: 'literal', value: 9n } },\n        status: { type: 'enum', enumValues: ['active', 'disabled'] },\n        derived_score: { type: 'integer', computed: { expression: '(points + 1)', stored: false } },\n        account_id: {\n          type: 'integer',\n          references: {\n            table: { schema: 'app', name: 'accounts' },\n            columns: ['id'],\n            name: 'users_account_inline_fk',\n            onDelete: 'set null',\n            onUpdate: 'cascade',\n          },\n        },\n        guarded_value: {\n          type: 'integer',\n          checks: [{ expression: 'guarded_value > 0', name: 'users_guarded_value_check' }],\n        },\n        unknown_type: {\n          type: 'mystery' as any,\n        },\n      },\n      primaryKey: { name: 'users_pk', columns: ['id'] },\n      uniques: [\n        { name: 'users_inline_email_unique', columns: ['email'] },\n        { name: 'users_email_unique', columns: ['email'] },\n      ],\n      checks: [\n        { name: 'users_id_check', expression: 'id > 0' },\n        { name: 'users_active_check', expression: 'is_active in (0, 1)' },\n      ],\n      foreignKeys: [\n        {\n          name: 'users_account_fk',\n          columns: ['account_id'],\n          references: { table: { name: 'accounts' }, columns: ['id'] },\n          onDelete: 'cascade',\n          onUpdate: 'restrict',\n        },\n      ],\n    })\n\n    assert.equal(createTableStatements.length, 1)\n    assert.match(createTableStatements[0].text, /^create table if not exists \"users\"/)\n    assert.match(createTableStatements[0].text, /\"email\" text unique/)\n    assert.match(createTableStatements[0].text, /\"display_name\" text default 'o''hare'/)\n    assert.match(\n      createTableStatements[0].text,\n      /\"reviewed_at\" text default '2026-01-01T00:00:00\\.000Z'/,\n    )\n    assert.match(createTableStatements[0].text, /\"optional_note\" text default null/)\n    assert.match(createTableStatements[0].text, /\"total\" numeric default 3\\.5/)\n    assert.match(createTableStatements[0].text, /\"is_active\" integer default 0/)\n    assert.match(createTableStatements[0].text, /\"public_id\" text/)\n    assert.match(createTableStatements[0].text, /\"due_on\" text/)\n    assert.match(createTableStatements[0].text, /\"starts_at\" text/)\n    assert.match(createTableStatements[0].text, /\"payload\" text/)\n    assert.match(createTableStatements[0].text, /\"blob_data\" blob/)\n    assert.match(createTableStatements[0].text, /\"big_total\" integer default 9/)\n    assert.match(createTableStatements[0].text, /\"status\" text/)\n    assert.match(\n      createTableStatements[0].text,\n      /\"derived_score\" integer generated always as \\(\\(points \\+ 1\\)\\) virtual/,\n    )\n    assert.match(\n      createTableStatements[0].text,\n      /\"account_id\" integer references \"app\"\\.\"accounts\" \\(\"id\"\\) on delete set null on update cascade/,\n    )\n    assert.match(\n      createTableStatements[0].text,\n      /\"guarded_value\" integer check \\(guarded_value > 0\\)/,\n    )\n    assert.match(createTableStatements[0].text, /\"unknown_type\" text/)\n    assert.match(createTableStatements[0].text, /primary key \\(\"id\"\\)/)\n    assert.match(createTableStatements[0].text, /unique \\(\"email\"\\)/)\n    assert.match(\n      createTableStatements[0].text,\n      /constraint \"users_email_unique\" unique \\(\"email\"\\)/,\n    )\n    assert.match(createTableStatements[0].text, /check \\(id > 0\\)/)\n    assert.match(\n      createTableStatements[0].text,\n      /constraint \"users_active_check\" check \\(is_active in \\(0, 1\\)\\)/,\n    )\n    assert.match(\n      createTableStatements[0].text,\n      /constraint \"users_account_fk\" foreign key \\(\"account_id\"\\) references \"accounts\" \\(\"id\"\\) on delete cascade on update restrict/,\n    )\n\n    let alterTableStatements = adapter.compileSql({\n      kind: 'alterTable',\n      table: { schema: 'app', name: 'users' },\n      changes: [\n        { kind: 'addColumn', column: 'nickname', definition: { type: 'text' } },\n        { kind: 'changeColumn', column: 'nickname', definition: { type: 'text' } },\n        { kind: 'renameColumn', from: 'nickname', to: 'handle' },\n        { kind: 'dropColumn', column: 'legacy_handle' },\n        { kind: 'addPrimaryKey', constraint: { name: 'users_pk', columns: ['id'] } },\n        { kind: 'dropPrimaryKey', name: 'users_pk' },\n        { kind: 'addUnique', constraint: { columns: ['email'], name: 'users_email_unique' } },\n        { kind: 'dropUnique', name: 'users_email_unique' },\n        {\n          kind: 'addForeignKey',\n          constraint: {\n            columns: ['account_id'],\n            references: { table: { name: 'accounts' }, columns: ['id'] },\n            name: 'users_account_fk',\n          },\n        },\n        { kind: 'dropForeignKey', name: 'users_account_fk' },\n        {\n          kind: 'addCheck',\n          constraint: { expression: 'length(email) > 3', name: 'users_email_check' },\n        },\n        { kind: 'dropCheck', name: 'users_email_check' },\n        { kind: 'setTableComment', comment: \"owner's users\" },\n        { kind: 'somethingElse' as any },\n      ] as any,\n    })\n\n    assert.equal(alterTableStatements.length, 12)\n    assert.match(alterTableStatements[1].text, /alter column \"nickname\" type text/)\n    assert.match(alterTableStatements[2].text, /rename column \"nickname\" to \"handle\"/)\n    assert.match(alterTableStatements[3].text, /drop column \"legacy_handle\"/)\n    assert.match(alterTableStatements[4].text, /add primary key \\(\"id\"\\)/)\n    assert.match(alterTableStatements[5].text, /drop primary key/)\n    assert.match(\n      alterTableStatements[6].text,\n      /add constraint \"users_email_unique\" unique \\(\"email\"\\)/,\n    )\n    assert.match(alterTableStatements[7].text, /drop constraint \"users_email_unique\"/)\n    assert.match(\n      alterTableStatements[8].text,\n      /add constraint \"users_account_fk\" foreign key \\(\"account_id\"\\) references \"accounts\" \\(\"id\"\\)/,\n    )\n    assert.match(alterTableStatements[9].text, /drop constraint \"users_account_fk\"/)\n    assert.match(\n      alterTableStatements[10].text,\n      /add constraint \"users_email_check\" check \\(length\\(email\\) > 3\\)/,\n    )\n    assert.match(alterTableStatements[11].text, /drop constraint \"users_email_check\"/)\n\n    let createIndexWithName = adapter.compileSql({\n      kind: 'createIndex',\n      index: {\n        table: { name: 'users' },\n        name: 'email_idx',\n        columns: ['email'],\n      },\n    })\n\n    assert.equal(createIndexWithName.length, 1)\n    assert.match(createIndexWithName[0].text, /create index \"email_idx\" on \"users\"/)\n  })\n\n  it('throws for unsupported data migration operation kinds', () => {\n    let sqlite = {\n      prepare() {\n        return {\n          reader: false,\n          all() {\n            return []\n          },\n          run() {\n            return { changes: 0, lastInsertRowid: 0 }\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n\n    assert.throws(\n      () => adapter.compileSql({ kind: 'unsupported_migration_operation' } as any),\n      /Unsupported data migration operation kind/,\n    )\n  })\n\n  it('supports typed writes, reads, and nested transactions', async () => {\n    let sqlite = new Database(':memory:')\n    sqlite.exec(\n      'create table accounts (id integer primary key, email text not null, status text not null)',\n    )\n\n    let db = createDatabase(createSqliteDatabaseAdapter(sqlite))\n\n    await db.query(accounts).insert({ id: 1, email: 'a@example.com', status: 'active' })\n\n    await db.transaction(async (outerTransaction) => {\n      await outerTransaction\n        .query(accounts)\n        .insert({ id: 2, email: 'b@example.com', status: 'active' })\n\n      await outerTransaction\n        .transaction(async (innerTransactionDatabase) => {\n          await innerTransactionDatabase\n            .query(accounts)\n            .insert({ id: 3, email: 'c@example.com', status: 'active' })\n\n          throw new Error('rollback inner')\n        })\n        .catch(() => undefined)\n    })\n\n    let rows = await db.query(accounts).orderBy('id', 'asc').all()\n    let count = await db.query(accounts).count()\n\n    assert.equal(count, 2)\n    assert.deepEqual(\n      rows.map((row) => row.id),\n      [1, 2],\n    )\n\n    sqlite.close()\n  })\n\n  it('supports upsert and returning', async () => {\n    let sqlite = new Database(':memory:')\n    sqlite.exec(\n      'create table accounts (id integer primary key, email text not null, status text not null)',\n    )\n\n    let db = createDatabase(createSqliteDatabaseAdapter(sqlite))\n\n    await db.query(accounts).insert({ id: 1, email: 'a@example.com', status: 'active' })\n\n    let result = await db\n      .query(accounts)\n      .upsert(\n        { id: 1, email: 'a@example.com', status: 'inactive' },\n        { conflictTarget: ['id'], returning: ['id', 'status'] },\n      )\n\n    assert.ok('row' in result)\n    if ('row' in result) {\n      assert.equal(result.row?.status, 'inactive')\n    }\n\n    sqlite.close()\n  })\n\n  it('accepts transaction options as best-effort hints', async () => {\n    let sqlite = new Database(':memory:')\n    sqlite.exec(\n      'create table accounts (id integer primary key, email text not null, status text not null)',\n    )\n\n    let db = createDatabase(createSqliteDatabaseAdapter(sqlite))\n\n    await db.transaction(\n      async (transactionDatabase) => {\n        await transactionDatabase\n          .query(accounts)\n          .insert({ id: 1, email: 'a@example.com', status: 'active' })\n      },\n      {\n        isolationLevel: 'serializable',\n        readOnly: true,\n      },\n    )\n\n    let rows = await db.query(accounts).all()\n\n    assert.equal(rows.length, 1)\n    assert.equal(rows[0].id, 1)\n    sqlite.close()\n  })\n\n  it('supports column-to-column comparisons from string references', async () => {\n    let sqlite = new Database(':memory:')\n    sqlite.exec(\n      'create table accounts (id integer primary key, email text not null, status text not null)',\n    )\n    sqlite.exec(\n      'create table projects (id integer primary key, account_id integer not null, name text not null)',\n    )\n\n    let db = createDatabase(createSqliteDatabaseAdapter(sqlite))\n\n    await db.query(accounts).insert({ id: 1, email: 'a@example.com', status: 'active' })\n    await db.query(projects).insert({ id: 10, account_id: 1, name: 'Alpha' })\n    await db.query(projects).insert({ id: 11, account_id: 99, name: 'Beta' })\n\n    let count = await db\n      .query(accounts)\n      .join(projects, eq('accounts.id', 'projects.account_id'))\n      .where(eq('accounts.email', 'a@example.com'))\n      .count()\n\n    assert.equal(count, 1)\n    sqlite.close()\n  })\n\n  it('compiles every DDL operation kind through compileSql()', () => {\n    let sqlite = {\n      prepare() {\n        return {\n          reader: true,\n          all() {\n            return []\n          },\n          run() {\n            return { changes: 0, lastInsertRowid: 0 }\n          },\n        }\n      },\n      exec() {},\n      pragma() {},\n    }\n\n    let adapter = createSqliteDatabaseAdapter(sqlite as never)\n    let operations: DataMigrationOperation[] = [\n      {\n        kind: 'createTable',\n        table: { schema: 'app', name: 'users' },\n        ifNotExists: true,\n        columns: {\n          id: { type: 'integer', nullable: false, primaryKey: true },\n        },\n      },\n      {\n        kind: 'alterTable',\n        table: { schema: 'app', name: 'users' },\n        changes: [\n          { kind: 'addColumn', column: 'email', definition: { type: 'text', nullable: false } },\n        ],\n      },\n      {\n        kind: 'renameTable',\n        from: { schema: 'app', name: 'users' },\n        to: { schema: 'app', name: 'accounts' },\n      },\n      { kind: 'dropTable', table: { schema: 'app', name: 'accounts' }, ifExists: true },\n      {\n        kind: 'createIndex',\n        index: {\n          table: { schema: 'app', name: 'users' },\n          columns: ['email'],\n          name: 'users_email_idx',\n        },\n      },\n      { kind: 'dropIndex', table: { schema: 'app', name: 'users' }, name: 'users_email_idx' },\n      {\n        kind: 'renameIndex',\n        table: { schema: 'app', name: 'users' },\n        from: 'users_email_idx',\n        to: 'users_email_idx_new',\n      },\n      {\n        kind: 'addForeignKey',\n        table: { schema: 'app', name: 'projects' },\n        constraint: {\n          columns: ['account_id'],\n          references: {\n            table: { schema: 'app', name: 'accounts' },\n            columns: ['id'],\n          },\n          name: 'projects_account_id_fk',\n          onDelete: 'cascade',\n        },\n      },\n      {\n        kind: 'dropForeignKey',\n        table: { schema: 'app', name: 'projects' },\n        name: 'projects_account_id_fk',\n      },\n      {\n        kind: 'addCheck',\n        table: { schema: 'app', name: 'users' },\n        constraint: {\n          name: 'users_email_check',\n          expression: \"email like '%@%'\",\n        },\n      },\n      { kind: 'dropCheck', table: { schema: 'app', name: 'users' }, name: 'users_email_check' },\n      { kind: 'raw', sql: { text: 'select 1', values: [] } },\n    ]\n\n    for (let operation of operations) {\n      let compiled = adapter.compileSql(operation)\n      assert.ok(compiled.length > 0, operation.kind)\n    }\n  })\n\n  it('treats dotted select aliases as single identifiers', async () => {\n    let sqlite = new Database(':memory:')\n    sqlite.exec(\n      'create table accounts (id integer primary key, email text not null, status text not null)',\n    )\n\n    let db = createDatabase(createSqliteDatabaseAdapter(sqlite))\n\n    await db.query(accounts).insert({ id: 1, email: 'a@example.com', status: 'active' })\n\n    let rows = await db.query(accounts).select({ 'account.email': accounts.email }).all()\n\n    assert.equal(rows.length, 1)\n    assert.equal(rows[0]['account.email'], 'a@example.com')\n    sqlite.close()\n  })\n})\n\nfunction canOpenSqliteDatabase(): boolean {\n  try {\n    let database = new Database(':memory:')\n    database.close()\n    return true\n  } catch {\n    return false\n  }\n}\n"
  },
  {
    "path": "packages/data-table-sqlite/src/lib/adapter.ts",
    "content": "import type {\n  AdapterCapabilityOverrides,\n  DataManipulationRequest,\n  DataMigrationRequest,\n  DataMigrationResult,\n  DataMigrationOperation,\n  DataManipulationResult,\n  DataManipulationOperation,\n  DatabaseAdapter,\n  ColumnDefinition,\n  SqlStatement,\n  TableRef,\n  TransactionOptions,\n  TransactionToken,\n} from '@remix-run/data-table'\nimport { getTablePrimaryKey } from '@remix-run/data-table'\nimport {\n  isDataManipulationOperation as isDataManipulationOperationHelper,\n  quoteLiteral as quoteLiteralHelper,\n  quoteTableRef as quoteTableRefHelper,\n} from '@remix-run/data-table/sql-helpers'\nimport type { Database as BetterSqliteDatabase, RunResult } from 'better-sqlite3'\n\nimport { compileSqliteOperation } from './sql-compiler.ts'\n\n/**\n * Sqlite adapter configuration.\n */\nexport type SqliteDatabaseAdapterOptions = {\n  capabilities?: AdapterCapabilityOverrides\n}\n\n/**\n * `DatabaseAdapter` implementation for Better SQLite3.\n */\nexport class SqliteDatabaseAdapter implements DatabaseAdapter {\n  /**\n   * The SQL dialect identifier reported by this adapter.\n   */\n  dialect = 'sqlite'\n\n  /**\n   * Feature flags describing the sqlite behaviors supported by this adapter.\n   */\n  capabilities\n\n  #database: BetterSqliteDatabase\n  #transactions = new Set<string>()\n  #transactionCounter = 0\n\n  constructor(database: BetterSqliteDatabase, options?: SqliteDatabaseAdapterOptions) {\n    this.#database = database\n    this.capabilities = {\n      returning: options?.capabilities?.returning ?? true,\n      savepoints: options?.capabilities?.savepoints ?? true,\n      upsert: options?.capabilities?.upsert ?? true,\n      transactionalDdl: options?.capabilities?.transactionalDdl ?? true,\n      migrationLock: options?.capabilities?.migrationLock ?? false,\n    }\n  }\n\n  /**\n   * Compiles a data or migration operation to sqlite SQL statements.\n   * @param operation Operation to compile.\n   * @returns Compiled SQL statements.\n   */\n  compileSql(operation: DataManipulationOperation | DataMigrationOperation): SqlStatement[] {\n    if (isDataManipulationOperation(operation)) {\n      let compiled = compileSqliteOperation(operation)\n      return [{ text: compiled.text, values: compiled.values }]\n    }\n\n    return compileSqliteMigrationOperations(operation)\n  }\n\n  /**\n   * Executes a sqlite data-manipulation request.\n   * @param request Request to execute.\n   * @returns Execution result.\n   */\n  async execute(request: DataManipulationRequest): Promise<DataManipulationResult> {\n    if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {\n      return {\n        affectedRows: 0,\n        insertId: undefined,\n        rows: request.operation.returning ? [] : undefined,\n      }\n    }\n\n    let statement = this.compileSql(request.operation)[0]\n    let prepared = this.#database.prepare(statement.text)\n\n    if (prepared.reader) {\n      let rows = normalizeRows(prepared.all(...statement.values))\n\n      if (request.operation.kind === 'count' || request.operation.kind === 'exists') {\n        rows = normalizeCountRows(rows)\n      }\n\n      return {\n        rows,\n        affectedRows: normalizeAffectedRowsForReader(request.operation.kind, rows),\n        insertId: normalizeInsertIdForReader(request.operation.kind, request.operation, rows),\n      }\n    }\n\n    let result = prepared.run(...statement.values)\n\n    return {\n      affectedRows: normalizeAffectedRowsForRun(request.operation.kind, result),\n      insertId: normalizeInsertIdForRun(request.operation.kind, request.operation, result),\n    }\n  }\n\n  /**\n   * Executes sqlite migration operations.\n   * @param request Migration request to execute.\n   * @returns Migration result.\n   */\n  async migrate(request: DataMigrationRequest): Promise<DataMigrationResult> {\n    let statements = this.compileSql(request.operation)\n\n    for (let statement of statements) {\n      let prepared = this.#database.prepare(statement.text)\n      prepared.run(...statement.values)\n    }\n\n    return {\n      affectedOperations: statements.length,\n    }\n  }\n\n  /**\n   * Checks whether a table exists in sqlite.\n   * @param table Table reference to inspect.\n   * @param transaction Optional transaction token.\n   * @returns `true` when the table exists.\n   */\n  async hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {\n    if (transaction) {\n      this.#assertTransaction(transaction)\n    }\n\n    let masterTable = table.schema\n      ? quoteIdentifier(table.schema) + '.sqlite_master'\n      : 'sqlite_master'\n    let statement = this.#database.prepare(\n      'select 1 from ' + masterTable + ' where type = ? and name = ? limit 1',\n    )\n    let row = statement.get('table', table.name)\n    return row !== undefined\n  }\n\n  /**\n   * Checks whether a column exists in sqlite.\n   * @param table Table reference to inspect.\n   * @param column Column name to look up.\n   * @param transaction Optional transaction token.\n   * @returns `true` when the column exists.\n   */\n  async hasColumn(\n    table: TableRef,\n    column: string,\n    transaction?: TransactionToken,\n  ): Promise<boolean> {\n    if (transaction) {\n      this.#assertTransaction(transaction)\n    }\n\n    let schemaPrefix = table.schema ? quoteIdentifier(table.schema) + '.' : ''\n    let statement = this.#database.prepare(\n      'pragma ' + schemaPrefix + 'table_info(' + quoteIdentifier(table.name) + ')',\n    )\n    let rows = statement.all() as Array<Record<string, unknown>>\n\n    return rows.some((row) => row.name === column)\n  }\n\n  /**\n   * Starts a sqlite transaction.\n   * @param options Transaction options.\n   * @returns Transaction token.\n   */\n  async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {\n    if (options?.isolationLevel === 'read uncommitted') {\n      this.#database.pragma('read_uncommitted = true')\n    }\n\n    this.#database.exec('begin')\n\n    this.#transactionCounter += 1\n    let token = { id: 'tx_' + String(this.#transactionCounter) }\n    this.#transactions.add(token.id)\n\n    return token\n  }\n\n  /**\n   * Commits an open sqlite transaction.\n   * @param token Transaction token to commit.\n   * @returns A promise that resolves when the transaction is committed.\n   */\n  async commitTransaction(token: TransactionToken): Promise<void> {\n    this.#assertTransaction(token)\n    this.#database.exec('commit')\n    this.#transactions.delete(token.id)\n  }\n\n  /**\n   * Rolls back an open sqlite transaction.\n   * @param token Transaction token to roll back.\n   * @returns A promise that resolves when the transaction is rolled back.\n   */\n  async rollbackTransaction(token: TransactionToken): Promise<void> {\n    this.#assertTransaction(token)\n    this.#database.exec('rollback')\n    this.#transactions.delete(token.id)\n  }\n\n  /**\n   * Creates a savepoint in an open sqlite transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the savepoint is created.\n   */\n  async createSavepoint(token: TransactionToken, name: string): Promise<void> {\n    this.#assertTransaction(token)\n    this.#database.exec('savepoint ' + quoteIdentifier(name))\n  }\n\n  /**\n   * Rolls back to a savepoint in an open sqlite transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the rollback completes.\n   */\n  async rollbackToSavepoint(token: TransactionToken, name: string): Promise<void> {\n    this.#assertTransaction(token)\n    this.#database.exec('rollback to savepoint ' + quoteIdentifier(name))\n  }\n\n  /**\n   * Releases a savepoint in an open sqlite transaction.\n   * @param token Transaction token to use.\n   * @param name Savepoint name.\n   * @returns A promise that resolves when the savepoint is released.\n   */\n  async releaseSavepoint(token: TransactionToken, name: string): Promise<void> {\n    this.#assertTransaction(token)\n    this.#database.exec('release savepoint ' + quoteIdentifier(name))\n  }\n\n  #assertTransaction(token: TransactionToken): void {\n    if (!this.#transactions.has(token.id)) {\n      throw new Error('Unknown transaction token: ' + token.id)\n    }\n  }\n}\n\n/**\n * Creates a sqlite `DatabaseAdapter`.\n * @param database Better SQLite3 database instance.\n * @param options Optional adapter capability overrides.\n * @returns A configured sqlite adapter.\n * @example\n * ```ts\n * import BetterSqlite3 from 'better-sqlite3'\n * import { createDatabase } from 'remix/data-table'\n * import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'\n *\n * let sqlite = new BetterSqlite3('./data/app.db')\n * let adapter = createSqliteDatabaseAdapter(sqlite)\n * let db = createDatabase(adapter)\n * ```\n */\nexport function createSqliteDatabaseAdapter(\n  database: BetterSqliteDatabase,\n  options?: SqliteDatabaseAdapterOptions,\n): SqliteDatabaseAdapter {\n  return new SqliteDatabaseAdapter(database, options)\n}\n\nfunction normalizeRows(rows: unknown[]): Record<string, unknown>[] {\n  return rows.map((row) => {\n    if (typeof row !== 'object' || row === null) {\n      return {}\n    }\n\n    return { ...(row as Record<string, unknown>) }\n  })\n}\n\nfunction normalizeCountRows(rows: Record<string, unknown>[]): Record<string, unknown>[] {\n  return rows.map((row) => {\n    let count = row.count\n\n    if (typeof count === 'string') {\n      let numeric = Number(count)\n\n      if (!Number.isNaN(numeric)) {\n        return {\n          ...row,\n          count: numeric,\n        }\n      }\n    }\n\n    if (typeof count === 'bigint') {\n      return {\n        ...row,\n        count: Number(count),\n      }\n    }\n\n    return row\n  })\n}\n\nfunction normalizeAffectedRowsForReader(\n  kind: DataManipulationRequest['operation']['kind'],\n  rows: Record<string, unknown>[],\n): number | undefined {\n  if (isWriteOperationKind(kind)) {\n    return rows.length\n  }\n\n  return undefined\n}\n\nfunction normalizeInsertIdForReader(\n  kind: DataManipulationRequest['operation']['kind'],\n  operation: DataManipulationRequest['operation'],\n  rows: Record<string, unknown>[],\n): unknown {\n  if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {\n    return undefined\n  }\n\n  let primaryKey = getTablePrimaryKey(operation.table)\n\n  if (primaryKey.length !== 1) {\n    return undefined\n  }\n\n  let key = primaryKey[0]\n  let row = rows[rows.length - 1]\n\n  return row ? row[key] : undefined\n}\n\nfunction normalizeAffectedRowsForRun(\n  kind: DataManipulationRequest['operation']['kind'],\n  result: RunResult,\n): number | undefined {\n  if (kind === 'select' || kind === 'count' || kind === 'exists') {\n    return undefined\n  }\n\n  return result.changes\n}\n\nfunction normalizeInsertIdForRun(\n  kind: DataManipulationRequest['operation']['kind'],\n  operation: DataManipulationRequest['operation'],\n  result: RunResult,\n): unknown {\n  if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {\n    return undefined\n  }\n\n  if (getTablePrimaryKey(operation.table).length !== 1) {\n    return undefined\n  }\n\n  return result.lastInsertRowid\n}\n\nfunction quoteIdentifier(value: string): string {\n  return '\"' + value.replace(/\"/g, '\"\"') + '\"'\n}\n\nfunction quoteTableRef(table: TableRef): string {\n  return quoteTableRefHelper(table, quoteIdentifier)\n}\n\nfunction quoteLiteral(value: unknown): string {\n  return quoteLiteralHelper(value, { booleansAsIntegers: true })\n}\n\nfunction isWriteOperationKind(kind: DataManipulationRequest['operation']['kind']): boolean {\n  return (\n    kind === 'insert' ||\n    kind === 'insertMany' ||\n    kind === 'update' ||\n    kind === 'delete' ||\n    kind === 'upsert'\n  )\n}\n\nfunction isInsertOperationKind(kind: DataManipulationRequest['operation']['kind']): boolean {\n  return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'\n}\n\nfunction isInsertOperation(\n  operation: DataManipulationRequest['operation'],\n): operation is Extract<\n  DataManipulationRequest['operation'],\n  { kind: 'insert' | 'insertMany' | 'upsert' }\n> {\n  return (\n    operation.kind === 'insert' || operation.kind === 'insertMany' || operation.kind === 'upsert'\n  )\n}\n\nfunction isDataManipulationOperation(\n  operation: DataManipulationOperation | DataMigrationOperation,\n): operation is DataManipulationOperation {\n  return isDataManipulationOperationHelper(operation)\n}\n\nfunction compileSqliteMigrationOperations(operation: DataMigrationOperation): SqlStatement[] {\n  if (operation.kind === 'raw') {\n    return [{ text: operation.sql.text, values: [...operation.sql.values] }]\n  }\n\n  if (operation.kind === 'createTable') {\n    let columns = Object.keys(operation.columns).map(\n      (columnName) =>\n        quoteIdentifier(columnName) + ' ' + compileSqliteColumn(operation.columns[columnName]),\n    )\n    let constraints: string[] = []\n\n    if (operation.primaryKey) {\n      constraints.push(\n        'constraint ' +\n          quoteIdentifier(operation.primaryKey.name) +\n          ' primary key (' +\n          operation.primaryKey.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')',\n      )\n    }\n\n    for (let unique of operation.uniques ?? []) {\n      constraints.push(\n        'constraint ' +\n          quoteIdentifier(unique.name) +\n          ' ' +\n          'unique (' +\n          unique.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')',\n      )\n    }\n\n    for (let check of operation.checks ?? []) {\n      constraints.push(\n        'constraint ' + quoteIdentifier(check.name) + ' ' + 'check (' + check.expression + ')',\n      )\n    }\n\n    for (let foreignKey of operation.foreignKeys ?? []) {\n      let clause =\n        'constraint ' +\n        quoteIdentifier(foreignKey.name) +\n        ' ' +\n        'foreign key (' +\n        foreignKey.columns.map((column) => quoteIdentifier(column)).join(', ') +\n        ') references ' +\n        quoteTableRef(foreignKey.references.table) +\n        ' (' +\n        foreignKey.references.columns.map((column) => quoteIdentifier(column)).join(', ') +\n        ')'\n\n      if (foreignKey.onDelete) {\n        clause += ' on delete ' + foreignKey.onDelete\n      }\n\n      if (foreignKey.onUpdate) {\n        clause += ' on update ' + foreignKey.onUpdate\n      }\n\n      constraints.push(clause)\n    }\n\n    return [\n      {\n        text:\n          'create table ' +\n          (operation.ifNotExists ? 'if not exists ' : '') +\n          quoteTableRef(operation.table) +\n          ' (' +\n          [...columns, ...constraints].join(', ') +\n          ')',\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'alterTable') {\n    let statements: SqlStatement[] = []\n\n    for (let change of operation.changes) {\n      let sql = 'alter table ' + quoteTableRef(operation.table) + ' '\n\n      if (change.kind === 'addColumn') {\n        sql +=\n          'add column ' +\n          quoteIdentifier(change.column) +\n          ' ' +\n          compileSqliteColumn(change.definition)\n      } else if (change.kind === 'changeColumn') {\n        sql +=\n          'alter column ' +\n          quoteIdentifier(change.column) +\n          ' type ' +\n          compileSqliteColumnType(change.definition)\n      } else if (change.kind === 'renameColumn') {\n        sql += 'rename column ' + quoteIdentifier(change.from) + ' to ' + quoteIdentifier(change.to)\n      } else if (change.kind === 'dropColumn') {\n        sql += 'drop column ' + quoteIdentifier(change.column)\n      } else if (change.kind === 'addPrimaryKey') {\n        sql +=\n          'add primary key (' +\n          change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')'\n      } else if (change.kind === 'dropPrimaryKey') {\n        sql += 'drop primary key'\n      } else if (change.kind === 'addUnique') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'unique (' +\n          change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')'\n      } else if (change.kind === 'dropUnique') {\n        sql += 'drop constraint ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'addForeignKey') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'foreign key (' +\n          change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ') references ' +\n          quoteTableRef(change.constraint.references.table) +\n          ' (' +\n          change.constraint.references.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')'\n      } else if (change.kind === 'dropForeignKey') {\n        sql += 'drop constraint ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'addCheck') {\n        sql +=\n          'add ' +\n          'constraint ' +\n          quoteIdentifier(change.constraint.name) +\n          ' ' +\n          'check (' +\n          change.constraint.expression +\n          ')'\n      } else if (change.kind === 'dropCheck') {\n        sql += 'drop constraint ' + quoteIdentifier(change.name)\n      } else if (change.kind === 'setTableComment') {\n        continue\n      } else {\n        continue\n      }\n\n      statements.push({ text: sql, values: [] })\n    }\n\n    return statements\n  }\n\n  if (operation.kind === 'renameTable') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.from) +\n          ' rename to ' +\n          quoteIdentifier(operation.to.name),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropTable') {\n    return [\n      {\n        text:\n          'drop table ' + (operation.ifExists ? 'if exists ' : '') + quoteTableRef(operation.table),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'createIndex') {\n    return [\n      {\n        text:\n          'create ' +\n          (operation.index.unique ? 'unique ' : '') +\n          'index ' +\n          (operation.ifNotExists ? 'if not exists ' : '') +\n          quoteIdentifier(operation.index.name) +\n          ' on ' +\n          quoteTableRef(operation.index.table) +\n          ' (' +\n          operation.index.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ')' +\n          (operation.index.where ? ' where ' + operation.index.where : ''),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropIndex') {\n    return [\n      {\n        text:\n          'drop index ' +\n          (operation.ifExists ? 'if exists ' : '') +\n          quoteIdentifier(operation.name),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'renameIndex') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' rename index ' +\n          quoteIdentifier(operation.from) +\n          ' to ' +\n          quoteIdentifier(operation.to),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'addForeignKey') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' add ' +\n          'constraint ' +\n          quoteIdentifier(operation.constraint.name) +\n          ' ' +\n          'foreign key (' +\n          operation.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +\n          ') references ' +\n          quoteTableRef(operation.constraint.references.table) +\n          ' (' +\n          operation.constraint.references.columns\n            .map((column) => quoteIdentifier(column))\n            .join(', ') +\n          ')' +\n          (operation.constraint.onDelete ? ' on delete ' + operation.constraint.onDelete : '') +\n          (operation.constraint.onUpdate ? ' on update ' + operation.constraint.onUpdate : ''),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropForeignKey') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' drop constraint ' +\n          quoteIdentifier(operation.name),\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'addCheck') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' add ' +\n          'constraint ' +\n          quoteIdentifier(operation.constraint.name) +\n          ' ' +\n          'check (' +\n          operation.constraint.expression +\n          ')',\n        values: [],\n      },\n    ]\n  }\n\n  if (operation.kind === 'dropCheck') {\n    return [\n      {\n        text:\n          'alter table ' +\n          quoteTableRef(operation.table) +\n          ' drop constraint ' +\n          quoteIdentifier(operation.name),\n        values: [],\n      },\n    ]\n  }\n\n  throw new Error('Unsupported data migration operation kind')\n}\n\nfunction compileSqliteColumn(definition: ColumnDefinition): string {\n  let parts = [compileSqliteColumnType(definition)]\n\n  if (definition.nullable === false) {\n    parts.push('not null')\n  }\n\n  if (definition.default) {\n    if (definition.default.kind === 'now') {\n      parts.push('default current_timestamp')\n    } else if (definition.default.kind === 'sql') {\n      parts.push('default ' + definition.default.expression)\n    } else {\n      parts.push('default ' + quoteLiteral(definition.default.value))\n    }\n  }\n\n  if (definition.primaryKey) {\n    parts.push('primary key')\n  }\n\n  if (definition.unique) {\n    parts.push('unique')\n  }\n\n  if (definition.computed) {\n    parts.push('generated always as (' + definition.computed.expression + ')')\n    parts.push(definition.computed.stored ? 'stored' : 'virtual')\n  }\n\n  if (definition.references) {\n    let clause =\n      'references ' +\n      quoteTableRef(definition.references.table) +\n      ' (' +\n      definition.references.columns.map((column) => quoteIdentifier(column)).join(', ') +\n      ')'\n\n    if (definition.references.onDelete) {\n      clause += ' on delete ' + definition.references.onDelete\n    }\n\n    if (definition.references.onUpdate) {\n      clause += ' on update ' + definition.references.onUpdate\n    }\n\n    parts.push(clause)\n  }\n\n  if (definition.checks && definition.checks.length > 0) {\n    for (let check of definition.checks) {\n      parts.push('check (' + check.expression + ')')\n    }\n  }\n\n  return parts.join(' ')\n}\n\nfunction compileSqliteColumnType(definition: ColumnDefinition): string {\n  if (definition.type === 'varchar') {\n    return 'text'\n  }\n\n  if (definition.type === 'text') {\n    return 'text'\n  }\n\n  if (definition.type === 'integer') {\n    return 'integer'\n  }\n\n  if (definition.type === 'bigint') {\n    return 'integer'\n  }\n\n  if (definition.type === 'decimal') {\n    return 'numeric'\n  }\n\n  if (definition.type === 'boolean') {\n    return 'integer'\n  }\n\n  if (definition.type === 'uuid') {\n    return 'text'\n  }\n\n  if (definition.type === 'date') {\n    return 'text'\n  }\n\n  if (definition.type === 'time') {\n    return 'text'\n  }\n\n  if (definition.type === 'timestamp') {\n    return 'text'\n  }\n\n  if (definition.type === 'json') {\n    return 'text'\n  }\n\n  if (definition.type === 'binary') {\n    return 'blob'\n  }\n\n  if (definition.type === 'enum') {\n    return 'text'\n  }\n\n  return 'text'\n}\n"
  },
  {
    "path": "packages/data-table-sqlite/src/lib/sql-compiler.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { beforeEach, describe, it } from 'node:test'\nimport {\n  between,\n  column,\n  createDatabase,\n  table,\n  eq,\n  gt,\n  gte,\n  ilike,\n  inList,\n  like,\n  lt,\n  lte,\n  ne,\n  notInList,\n  isNull,\n  notNull,\n  type DataManipulationOperation,\n  type DatabaseAdapter,\n  or,\n  and,\n} from '@remix-run/data-table'\nimport { compileSqliteOperation } from './sql-compiler.ts'\n\nlet accounts = table({\n  name: 'accounts',\n  columns: {\n    id: column.integer(),\n    email: column.text(),\n    status: column.text(),\n    deleted: column.boolean(),\n  },\n})\n\nlet tasks = table({\n  name: 'tasks',\n  columns: {\n    id: column.integer(),\n    name: column.text(),\n    account_id: column.integer(),\n  },\n})\n\nlet statements: DataManipulationOperation[] = []\n\nlet fakeAdapter = {\n  capabilities: {\n    upsert: true,\n    returning: true,\n  },\n\n  execute: async (request) => {\n    statements.push(request.operation)\n    // usefull for update\n    if (request.operation.kind === 'select') {\n      return {\n        rows: [{ id: 1 }],\n      }\n    }\n\n    // for insert with returning\n    if (request.operation.kind === 'insert' && request.operation.returning) {\n      return {\n        rows: [{ id: 10 }],\n      }\n    }\n    return {}\n  },\n} as DatabaseAdapter\nlet db = createDatabase(fakeAdapter)\n\ndescribe('sqlite sql-compiler', () => {\n  beforeEach(() => {\n    statements = []\n  })\n\n  describe('select statement', () => {\n    it('compile wildcard selection', async () => {\n      await db.query(accounts).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\"',\n        values: [],\n      })\n    })\n\n    it('compile selected', async () => {\n      await db\n        .query(accounts)\n        .select({\n          accountId: accounts.id,\n          accountEmail: accounts.email,\n        })\n        .all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select \"accounts\".\"id\" as \"accountId\", \"accounts\".\"email\" as \"accountEmail\" from \"accounts\"',\n        values: [],\n      })\n    })\n\n    it('compile inner join', async () => {\n      await db.query(accounts).join(tasks, eq(accounts.id, tasks.account_id)).all()\n\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" inner join \"tasks\" on \"accounts\".\"id\" = \"tasks\".\"account_id\"',\n        values: [],\n      })\n    })\n\n    it('compile left join', async () => {\n      await db.query(accounts).leftJoin(tasks, eq(accounts.id, tasks.account_id)).all()\n\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" left join \"tasks\" on \"accounts\".\"id\" = \"tasks\".\"account_id\"',\n        values: [],\n      })\n    })\n\n    it('compile right join', async () => {\n      await db.query(accounts).rightJoin(tasks, eq(accounts.id, tasks.account_id)).all()\n\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" right join \"tasks\" on \"accounts\".\"id\" = \"tasks\".\"account_id\"',\n        values: [],\n      })\n    })\n\n    it('compile empty where', async () => {\n      await db.query(accounts).where({}).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (1 = 1)',\n        values: [],\n      })\n    })\n\n    it('compile eq where - filled', async () => {\n      await db.query(accounts).where({ status: 'enabled' }).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"status\" = ?))',\n        values: ['enabled'],\n      })\n    })\n\n    it('compile eq where - null', async () => {\n      await db.query(accounts).where({ status: null }).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"status\" is null))',\n        values: [],\n      })\n    })\n\n    it('compile eq where - undefined', async () => {\n      await db.query(accounts).where({ status: undefined }).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"status\" is null))',\n        values: [],\n      })\n    })\n\n    it('compile ne where - filled', async () => {\n      await db.query(accounts).where(ne('status', 'enabled')).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"status\" <> ?)',\n        values: ['enabled'],\n      })\n    })\n\n    it('compile ne where - null', async () => {\n      await db.query(accounts).where(ne('status', null)).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"status\" is not null)',\n        values: [],\n      })\n    })\n\n    it('compile ne where - undefined', async () => {\n      await db.query(accounts).where(ne('status', undefined)).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"status\" is not null)',\n        values: [],\n      })\n    })\n\n    it('compile gt where', async () => {\n      await db.query(accounts).where(gt('id', 100)).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"id\" > ?)',\n        values: [100],\n      })\n    })\n\n    it('compile gte where', async () => {\n      await db.query(accounts).where(gte('id', 100)).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"id\" >= ?)',\n        values: [100],\n      })\n    })\n\n    it('compile lt where', async () => {\n      await db.query(accounts).where(lt('id', 100)).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"id\" < ?)',\n        values: [100],\n      })\n    })\n\n    it('compile lte where', async () => {\n      await db.query(accounts).where(lte('id', 100)).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"id\" <= ?)',\n        values: [100],\n      })\n    })\n\n    it('compile in where', async () => {\n      await db\n        .query(accounts)\n        .where(inList('id', [100, 101]))\n        .all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"id\" in (?, ?))',\n        values: [100, 101],\n      })\n    })\n\n    it('compile in where - empty', async () => {\n      await db.query(accounts).where(inList('id', [])).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (1 = 0)',\n        values: [],\n      })\n    })\n\n    it('compile not in where', async () => {\n      await db\n        .query(accounts)\n        .where(notInList('id', [100, 101]))\n        .all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"id\" not in (?, ?))',\n        values: [100, 101],\n      })\n    })\n\n    it('compile not in where - empty', async () => {\n      await db.query(accounts).where(notInList('id', [])).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (1 = 1)',\n        values: [],\n      })\n    })\n\n    it('compile like where', async () => {\n      await db.query(accounts).where(like('status', 'ena')).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"status\" like ?)',\n        values: ['ena'],\n      })\n    })\n\n    it('compile ilike where', async () => {\n      await db.query(accounts).where(ilike('status', 'EnA')).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (lower(\"status\") like lower(?))',\n        values: ['EnA'],\n      })\n    })\n\n    it('compile between where', async () => {\n      await db\n        .query(accounts)\n        .where(between(accounts.id, 20, 50))\n        .all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"accounts\".\"id\" between ? and ?)',\n        values: [20, 50],\n      })\n    })\n\n    it('compile isNull where', async () => {\n      await db.query(accounts).where(isNull(accounts.status)).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"accounts\".\"status\" is null)',\n        values: [],\n      })\n    })\n\n    it('compile notNull where', async () => {\n      await db.query(accounts).where(notNull(accounts.status)).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where (\"accounts\".\"status\" is not null)',\n        values: [],\n      })\n    })\n\n    it('compile logical and', async () => {\n      await db.query(accounts).where({ status: 'enabled' }).where(gt(accounts.id, 10)).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"status\" = ?)) and (\"accounts\".\"id\" > ?)',\n        values: ['enabled', 10],\n      })\n    })\n\n    it('compile logical or', async () => {\n      await db\n        .query(accounts)\n        .where(or(eq(accounts.status, 'enabled'), eq(accounts.status, 'disabled')))\n        .all()\n\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"accounts\".\"status\" = ?) or (\"accounts\".\"status\" = ?))',\n        values: ['enabled', 'disabled'],\n      })\n    })\n\n    it('compile nested predicates', async () => {\n      await db\n        .query(accounts)\n        .where(\n          and(\n            eq(accounts.id, 1),\n            or(eq(accounts.status, 'enabled'), eq(accounts.status, 'disabled')),\n          ),\n        )\n        .all()\n\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"accounts\".\"id\" = ?) and ((\"accounts\".\"status\" = ?) or (\"accounts\".\"status\" = ?)))',\n        values: [1, 'enabled', 'disabled'],\n      })\n    })\n\n    it('compile groupBy', async () => {\n      await db.query(tasks).groupBy(tasks.account_id).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"tasks\" group by \"tasks\".\"account_id\"',\n        values: [],\n      })\n    })\n\n    it('compile having', async () => {\n      await db.query(tasks).having({ account_id: 20 }).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"tasks\" having ((\"account_id\" = ?))',\n        values: [20],\n      })\n    })\n\n    it('compile pagination', async () => {\n      await db.query(accounts).offset(5).limit(10).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" limit 10 offset 5',\n        values: [],\n      })\n    })\n\n    it('compile with normalized boolean - true', async () => {\n      await db.query(accounts).where({ deleted: true }).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"deleted\" = ?))',\n        values: [1],\n      })\n    })\n\n    it('compile with normalized boolean - false', async () => {\n      await db.query(accounts).where({ deleted: false }).all()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select * from \"accounts\" where ((\"deleted\" = ?))',\n        values: [0],\n      })\n    })\n  })\n\n  describe('count - exists statement', () => {\n    it('compile count', async () => {\n      await db.query(tasks).where({ account_id: 1 }).count()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select count(*) as \"count\" from (select 1 from \"tasks\" where ((\"account_id\" = ?))) as \"__dt_count\"',\n        values: [1],\n      })\n    })\n\n    it('compile exists', async () => {\n      await db.query(tasks).where({ account_id: 1 }).exists()\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select count(*) as \"count\" from (select 1 from \"tasks\" where ((\"account_id\" = ?))) as \"__dt_count\"',\n        values: [1],\n      })\n    })\n  })\n\n  describe('insert statement', () => {\n    it('compile for one', async () => {\n      await db.create(accounts, {\n        id: 1,\n        email: 'info@remix.run',\n        status: 'enabled',\n      })\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"id\", \"email\", \"status\") values (?, ?, ?)',\n        values: [1, 'info@remix.run', 'enabled'],\n      })\n    })\n\n    it('compile for one and return values', async () => {\n      await db.create(\n        accounts,\n        {\n          id: 1,\n          email: 'info@remix.run',\n          status: 'enabled',\n        },\n        { returnRow: true },\n      )\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"id\", \"email\", \"status\") values (?, ?, ?) returning *',\n        values: [1, 'info@remix.run', 'enabled'],\n      })\n    })\n\n    it('compile for one with default values', async () => {\n      await db.create(accounts, {})\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" default values',\n        values: [],\n      })\n    })\n\n    it('compile for many', async () => {\n      await db.createMany(accounts, [\n        {\n          id: 1,\n          email: 'info@remix.run',\n          status: 'enabled',\n        },\n        {\n          id: 2,\n          email: 'contact@remix.run',\n          status: 'draft',\n        },\n      ])\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"id\", \"email\", \"status\") values (?, ?, ?), (?, ?, ?)',\n        values: [1, 'info@remix.run', 'enabled', 2, 'contact@remix.run', 'draft'],\n      })\n    })\n\n    it('compile for many with default values', () => {\n      let compiled = compileSqliteOperation({\n        kind: 'insertMany',\n        table: accounts,\n        values: [{}, {}],\n      })\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" default values',\n        values: [],\n      })\n    })\n\n    it('compile for many without data', async () => {\n      await db.createMany(accounts, [])\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'select 0 where 1 = 0',\n        values: [],\n      })\n    })\n  })\n\n  describe('update statement', () => {\n    it('compile for one', async () => {\n      await db.query(accounts).where({ id: 1 }).update({\n        email: 'info@remix.run',\n        status: 'enabled',\n      })\n\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'update \"accounts\" set \"email\" = ?, \"status\" = ? where ((\"id\" = ?))',\n        values: ['info@remix.run', 'enabled', 1],\n      })\n    })\n\n    it('compile for many', async () => {\n      await db.updateMany(\n        accounts,\n        {\n          email: 'info@remix.run',\n          status: 'enabled',\n        },\n        {\n          where: {\n            status: 'disabled',\n          },\n        },\n      )\n\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'update \"accounts\" set \"email\" = ?, \"status\" = ? where ((\"status\" = ?))',\n        values: ['info@remix.run', 'enabled', 'disabled'],\n      })\n    })\n  })\n\n  describe('upsert statement', () => {\n    it('should throw without value', async () => {\n      await db.query(accounts).upsert(\n        {},\n        {\n          conflictTarget: ['id'],\n        },\n      )\n      assert.throws(() => compileSqliteOperation(statements[0]))\n    })\n\n    it('compile with update columns', async () => {\n      await db.query(accounts).upsert(\n        {\n          status: 'enabled',\n          email: 'info@remix.run',\n        },\n        {\n          conflictTarget: ['id'],\n          update: {\n            email: 'contact@remix.run',\n          },\n        },\n      )\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"status\", \"email\") values (?, ?) on conflict (\"id\") do update set \"email\" = ?',\n        values: ['contact@remix.run', 'enabled', 'info@remix.run'],\n      })\n    })\n\n    it('compile without update columns', async () => {\n      await db.query(accounts).upsert(\n        {\n          status: 'enabled',\n          email: 'info@remix.run',\n        },\n        {\n          conflictTarget: ['id'],\n          update: {},\n        },\n      )\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'insert into \"accounts\" (\"status\", \"email\") values (?, ?) on conflict (\"id\") do nothing',\n        values: ['enabled', 'info@remix.run'],\n      })\n    })\n  })\n\n  describe('delete statement', () => {\n    it('compile for one', async () => {\n      await db.delete(accounts, 10)\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'delete from \"accounts\" where ((\"id\" = ?))',\n        values: [10],\n      })\n    })\n\n    it('compile for many', async () => {\n      await db.deleteMany(accounts, {\n        where: {\n          status: 'enabled',\n        },\n      })\n      let compiled = compileSqliteOperation(statements[0])\n      assert.deepEqual(compiled, {\n        text: 'delete from \"accounts\" where ((\"status\" = ?))',\n        values: ['enabled'],\n      })\n    })\n  })\n\n  describe('raw statement', () => {\n    it('compile', () => {\n      let compiled = compileSqliteOperation({\n        kind: 'raw',\n        sql: {\n          text: 'select * from accounts where id = ?',\n          values: [10],\n        },\n      })\n      assert.deepEqual(compiled, {\n        text: 'select * from accounts where id = ?',\n        values: [10],\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/data-table-sqlite/src/lib/sql-compiler.ts",
    "content": "import { getTableName, getTablePrimaryKey } from '@remix-run/data-table'\nimport type { DataManipulationOperation, Predicate, SqlStatement } from '@remix-run/data-table'\nimport {\n  collectColumns as collectColumnsHelper,\n  normalizeJoinType as normalizeJoinTypeHelper,\n  quotePath as quotePathHelper,\n} from '@remix-run/data-table/sql-helpers'\n\ntype JoinClause = Extract<DataManipulationOperation, { kind: 'select' }>['joins'][number]\ntype UpsertOperation = Extract<DataManipulationOperation, { kind: 'upsert' }>\ntype OperationTable = Extract<DataManipulationOperation, { kind: 'select' }>['table']\n\ntype CompileContext = {\n  values: unknown[]\n}\n\nexport function compileSqliteOperation(operation: DataManipulationOperation): SqlStatement {\n  if (operation.kind === 'raw') {\n    return {\n      text: operation.sql.text,\n      values: [...operation.sql.values],\n    }\n  }\n\n  let context: CompileContext = { values: [] }\n\n  if (operation.kind === 'select') {\n    let selection = '*'\n\n    if (operation.select !== '*') {\n      selection = operation.select\n        .map((field) => quotePath(field.column) + ' as ' + quoteIdentifier(field.alias))\n        .join(', ')\n    }\n\n    return {\n      text:\n        'select ' +\n        (operation.distinct ? 'distinct ' : '') +\n        selection +\n        compileFromClause(operation.table, operation.joins, context) +\n        compileWhereClause(operation.where, context) +\n        compileGroupByClause(operation.groupBy) +\n        compileHavingClause(operation.having, context) +\n        compileOrderByClause(operation.orderBy) +\n        compileLimitClause(operation.limit) +\n        compileOffsetClause(operation.offset),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'count' || operation.kind === 'exists') {\n    let inner =\n      'select 1' +\n      compileFromClause(operation.table, operation.joins, context) +\n      compileWhereClause(operation.where, context) +\n      compileGroupByClause(operation.groupBy) +\n      compileHavingClause(operation.having, context)\n\n    return {\n      text:\n        'select count(*) as ' +\n        quoteIdentifier('count') +\n        ' from (' +\n        inner +\n        ') as ' +\n        quoteIdentifier('__dt_count'),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'insert') {\n    return compileInsertOperation(operation.table, operation.values, operation.returning, context)\n  }\n\n  if (operation.kind === 'insertMany') {\n    return compileInsertManyOperation(\n      operation.table,\n      operation.values,\n      operation.returning,\n      context,\n    )\n  }\n\n  if (operation.kind === 'update') {\n    let columns = Object.keys(operation.changes)\n\n    return {\n      text:\n        'update ' +\n        quotePath(getTableName(operation.table)) +\n        ' set ' +\n        columns\n          .map(\n            (column) => quotePath(column) + ' = ' + pushValue(context, operation.changes[column]),\n          )\n          .join(', ') +\n        compileWhereClause(operation.where, context) +\n        compileReturningClause(operation.returning),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'delete') {\n    return {\n      text:\n        'delete from ' +\n        quotePath(getTableName(operation.table)) +\n        compileWhereClause(operation.where, context) +\n        compileReturningClause(operation.returning),\n      values: context.values,\n    }\n  }\n\n  if (operation.kind === 'upsert') {\n    return compileUpsertOperation(operation, context)\n  }\n\n  throw new Error('Unsupported operation kind')\n}\n\nfunction compileInsertOperation(\n  table: OperationTable,\n  values: Record<string, unknown>,\n  returning: '*' | string[] | undefined,\n  context: CompileContext,\n): SqlStatement {\n  let columns = Object.keys(values)\n\n  if (columns.length === 0) {\n    return {\n      text:\n        'insert into ' +\n        quotePath(getTableName(table)) +\n        ' default values' +\n        compileReturningClause(returning),\n      values: context.values,\n    }\n  }\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(table)) +\n      ' (' +\n      columns.map((column) => quotePath(column)).join(', ') +\n      ') values (' +\n      columns.map((column) => pushValue(context, values[column])).join(', ') +\n      ')' +\n      compileReturningClause(returning),\n    values: context.values,\n  }\n}\n\nfunction compileInsertManyOperation(\n  table: OperationTable,\n  rows: Record<string, unknown>[],\n  returning: '*' | string[] | undefined,\n  context: CompileContext,\n): SqlStatement {\n  if (rows.length === 0) {\n    return {\n      text: 'select 0 where 1 = 0',\n      values: context.values,\n    }\n  }\n\n  let columns = collectColumns(rows)\n\n  if (columns.length === 0) {\n    return {\n      text:\n        'insert into ' +\n        quotePath(getTableName(table)) +\n        ' default values' +\n        compileReturningClause(returning),\n      values: context.values,\n    }\n  }\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(table)) +\n      ' (' +\n      columns.map((column) => quotePath(column)).join(', ') +\n      ') values ' +\n      rows\n        .map(\n          (row) =>\n            '(' +\n            columns\n              .map((column) => {\n                let value = Object.prototype.hasOwnProperty.call(row, column) ? row[column] : null\n                return pushValue(context, value)\n              })\n              .join(', ') +\n            ')',\n        )\n        .join(', ') +\n      compileReturningClause(returning),\n    values: context.values,\n  }\n}\n\nfunction compileUpsertOperation(operation: UpsertOperation, context: CompileContext): SqlStatement {\n  let insertColumns = Object.keys(operation.values)\n  let conflictTarget = operation.conflictTarget ?? [...getTablePrimaryKey(operation.table)]\n\n  if (insertColumns.length === 0) {\n    throw new Error('upsert requires at least one value')\n  }\n\n  let updateValues = operation.update ?? operation.values\n  let updateColumns = Object.keys(updateValues)\n\n  let conflictClause = ''\n\n  if (updateColumns.length === 0) {\n    conflictClause =\n      ' on conflict (' +\n      conflictTarget.map((column: string) => quotePath(column)).join(', ') +\n      ') do nothing'\n  } else {\n    conflictClause =\n      ' on conflict (' +\n      conflictTarget.map((column: string) => quotePath(column)).join(', ') +\n      ') do update set ' +\n      updateColumns\n        .map((column) => quotePath(column) + ' = ' + pushValue(context, updateValues[column]))\n        .join(', ')\n  }\n\n  return {\n    text:\n      'insert into ' +\n      quotePath(getTableName(operation.table)) +\n      ' (' +\n      insertColumns.map((column) => quotePath(column)).join(', ') +\n      ') values (' +\n      insertColumns.map((column) => pushValue(context, operation.values[column])).join(', ') +\n      ')' +\n      conflictClause +\n      compileReturningClause(operation.returning),\n    values: context.values,\n  }\n}\n\nfunction compileFromClause(\n  table: OperationTable,\n  joins: JoinClause[],\n  context: CompileContext,\n): string {\n  let output = ' from ' + quotePath(getTableName(table))\n\n  for (let join of joins) {\n    output +=\n      ' ' +\n      normalizeJoinType(join.type) +\n      ' join ' +\n      quotePath(getTableName(join.table)) +\n      ' on ' +\n      compilePredicate(join.on, context)\n  }\n\n  return output\n}\n\nfunction compileWhereClause(predicates: Predicate[], context: CompileContext): string {\n  if (predicates.length === 0) {\n    return ''\n  }\n\n  return (\n    ' where ' +\n    predicates.map((predicate) => '(' + compilePredicate(predicate, context) + ')').join(' and ')\n  )\n}\n\nfunction compileGroupByClause(columns: string[]): string {\n  if (columns.length === 0) {\n    return ''\n  }\n\n  return ' group by ' + columns.map((column) => quotePath(column)).join(', ')\n}\n\nfunction compileHavingClause(predicates: Predicate[], context: CompileContext): string {\n  if (predicates.length === 0) {\n    return ''\n  }\n\n  return (\n    ' having ' +\n    predicates.map((predicate) => '(' + compilePredicate(predicate, context) + ')').join(' and ')\n  )\n}\n\nfunction compileOrderByClause(orderBy: { column: string; direction: 'asc' | 'desc' }[]): string {\n  if (orderBy.length === 0) {\n    return ''\n  }\n\n  return (\n    ' order by ' +\n    orderBy\n      .map((clause) => quotePath(clause.column) + ' ' + clause.direction.toUpperCase())\n      .join(', ')\n  )\n}\n\nfunction compileLimitClause(limit: number | undefined): string {\n  if (limit === undefined) {\n    return ''\n  }\n\n  return ' limit ' + String(limit)\n}\n\nfunction compileOffsetClause(offset: number | undefined): string {\n  if (offset === undefined) {\n    return ''\n  }\n\n  return ' offset ' + String(offset)\n}\n\nfunction compileReturningClause(returning: '*' | string[] | undefined): string {\n  if (!returning) {\n    return ''\n  }\n\n  if (returning === '*') {\n    return ' returning *'\n  }\n\n  return ' returning ' + returning.map((column) => quotePath(column)).join(', ')\n}\n\nfunction compilePredicate(predicate: Predicate, context: CompileContext): string {\n  if (predicate.type === 'comparison') {\n    let column = quotePath(predicate.column)\n\n    if (predicate.operator === 'eq') {\n      if (\n        predicate.valueType === 'value' &&\n        (predicate.value === null || predicate.value === undefined)\n      ) {\n        return column + ' is null'\n      }\n\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' = ' + comparisonValue\n    }\n\n    if (predicate.operator === 'ne') {\n      if (\n        predicate.valueType === 'value' &&\n        (predicate.value === null || predicate.value === undefined)\n      ) {\n        return column + ' is not null'\n      }\n\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' <> ' + comparisonValue\n    }\n\n    if (predicate.operator === 'gt') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' > ' + comparisonValue\n    }\n\n    if (predicate.operator === 'gte') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' >= ' + comparisonValue\n    }\n\n    if (predicate.operator === 'lt') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' < ' + comparisonValue\n    }\n\n    if (predicate.operator === 'lte') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' <= ' + comparisonValue\n    }\n\n    if (predicate.operator === 'in' || predicate.operator === 'notIn') {\n      let values = Array.isArray(predicate.value) ? predicate.value : []\n\n      if (values.length === 0) {\n        return predicate.operator === 'in' ? '1 = 0' : '1 = 1'\n      }\n\n      let keyword = predicate.operator === 'in' ? 'in' : 'not in'\n\n      return (\n        column +\n        ' ' +\n        keyword +\n        ' (' +\n        values.map((value) => pushValue(context, value)).join(', ') +\n        ')'\n      )\n    }\n\n    if (predicate.operator === 'like') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return column + ' like ' + comparisonValue\n    }\n\n    if (predicate.operator === 'ilike') {\n      let comparisonValue = compileComparisonValue(predicate, context)\n      return 'lower(' + column + ') like lower(' + comparisonValue + ')'\n    }\n  }\n\n  if (predicate.type === 'between') {\n    return (\n      quotePath(predicate.column) +\n      ' between ' +\n      pushValue(context, predicate.lower) +\n      ' and ' +\n      pushValue(context, predicate.upper)\n    )\n  }\n\n  if (predicate.type === 'null') {\n    return (\n      quotePath(predicate.column) + (predicate.operator === 'isNull' ? ' is null' : ' is not null')\n    )\n  }\n\n  if (predicate.type === 'logical') {\n    if (predicate.predicates.length === 0) {\n      return predicate.operator === 'and' ? '1 = 1' : '1 = 0'\n    }\n\n    let joiner = predicate.operator === 'and' ? ' and ' : ' or '\n\n    return predicate.predicates\n      .map((child) => '(' + compilePredicate(child, context) + ')')\n      .join(joiner)\n  }\n\n  throw new Error('Unsupported predicate')\n}\n\nfunction compileComparisonValue(\n  predicate: Extract<Predicate, { type: 'comparison' }>,\n  context: CompileContext,\n): string {\n  if (predicate.valueType === 'column') {\n    return quotePath(predicate.value)\n  }\n\n  return pushValue(context, predicate.value)\n}\n\nfunction normalizeJoinType(type: string): string {\n  return normalizeJoinTypeHelper(type)\n}\n\nfunction quoteIdentifier(value: string): string {\n  return '\"' + value.replace(/\"/g, '\"\"') + '\"'\n}\n\nfunction quotePath(path: string): string {\n  return quotePathHelper(path, quoteIdentifier)\n}\n\nfunction pushValue(context: CompileContext, value: unknown): string {\n  context.values.push(normalizeBoundValue(value))\n  return '?'\n}\n\nfunction normalizeBoundValue(value: unknown): unknown {\n  if (typeof value === 'boolean') {\n    return value ? 1 : 0\n  }\n\n  return value\n}\n\nfunction collectColumns(rows: Record<string, unknown>[]): string[] {\n  return collectColumnsHelper(rows)\n}\n"
  },
  {
    "path": "packages/data-table-sqlite/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/data-table-sqlite/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"types\": [\"node\"],\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"vendor\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/fetch-proxy/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/fetch-proxy/CHANGELOG.md",
    "content": "# `fetch-proxy` CHANGELOG\n\nThis is the changelog for [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy). It follows [semantic versioning](https://semver.org/).\n\n## v0.7.1\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.7.0 (2025-11-05)\n\n- Move `@remix-run/headers` to `peerDependencies`\n- Build using `tsc` instead of `esbuild`. This means modules in the `dist` directory now mirror the layout of modules in the `src` directory.\n\n## v0.6.0 (2025-10-22)\n\n- 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()`.\n\n## v0.5.0 (2025-07-24)\n\n- Renamed package from `@mjackson/fetch-proxy` to `@remix-run/fetch-proxy`\n- FIX: A regression that stopped forwarding the method from an exising request object\n- Forward additional properties from existing request objects passed to the proxy, including:\n  - cache\n  - credentials\n  - integrity\n  - keepalive\n  - mode\n  - redirect\n  - referrer\n  - referrerPolicy\n  - signal\n\n## v0.4.0 (2025-07-11)\n\n- Forward all additional options to the proxied request object\n\n## v0.3.0 (2025-06-10)\n\n- Add `/src` to npm package, so \"go to definition\" goes to the actual source\n- Use one set of types for all built files, instead of separate types for ESM and CJS\n- Build using esbuild directly instead of tsup\n\n## v0.2.0 (2024-11-14)\n\n- Added CommonJS build\n\n## v0.1.0 (2024-09-12)\n\n- Initial release\n"
  },
  {
    "path": "packages/fetch-proxy/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/fetch-proxy/README.md",
    "content": "# fetch-proxy\n\nHTTP proxy utilities built on the web [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).\nUse `fetch-proxy` to create `fetch` handlers that forward requests to target servers while optionally rewriting headers and cookies.\n\n## Features\n\n- **Web Standards** - Built on the standard [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)\n- **Cookie Rewriting** - Supports rewriting `Set-Cookie` headers received from target server\n- **Forwarding Headers** - Supports `X-Forwarded-Proto` and `X-Forwarded-Host` headers\n- **Custom Fetch** - Supports custom `fetch` implementations\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```ts\nimport { createFetchProxy } from 'remix/fetch-proxy'\n\n// Create a proxy that sends all requests through to remix.run\nlet proxy = createFetchProxy('https://remix.run')\n\n// This fetch handler is probably running as part of your server somewhere...\nfunction handleFetch(request: Request): Promise<Response> {\n  return proxy(request)\n}\n\n// Test it out by manually throwing a Request at it\nlet response = await handleFetch(new Request('https://shopify.com'))\n\nlet text = await response.text()\nlet title = text.match(/<title>([^<]+)<\\/title>/)[1]\nassert(title.includes('Remix'))\n```\n\n## Related Packages\n\n- [`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\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/fetch-proxy/package.json",
    "content": "{\n  \"name\": \"@remix-run/fetch-proxy\",\n  \"version\": \"0.7.1\",\n  \"description\": \"An HTTP proxy for the web Fetch API\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/fetch-proxy\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/fetch-proxy#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"dependencies\": {\n    \"@remix-run/headers\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"http\",\n    \"proxy\"\n  ]\n}\n"
  },
  {
    "path": "packages/fetch-proxy/src/index.ts",
    "content": "export { type FetchProxyOptions, type FetchProxy, createFetchProxy } from './lib/fetch-proxy.ts'\n"
  },
  {
    "path": "packages/fetch-proxy/src/lib/fetch-proxy.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { type FetchProxyOptions, createFetchProxy } from './fetch-proxy.ts'\n\nasync function testProxy(\n  request: Request,\n  target: string | URL,\n  options?: FetchProxyOptions,\n): Promise<{ request: Request; response: Response }> {\n  let capturedRequest: Request\n  let proxy = createFetchProxy(target, {\n    ...options,\n    fetch(input, init) {\n      capturedRequest = new Request(input, init)\n      return options?.fetch?.(input, init) ?? Promise.resolve(new Response())\n    },\n  })\n\n  let response = await proxy(request)\n\n  assert.ok(capturedRequest!)\n\n  return { request: capturedRequest, response }\n}\n\ndescribe('fetch proxy', () => {\n  it('appends the request URL pathname + search to the target URL', async () => {\n    let { request: request1 } = await testProxy(\n      new Request('http://shopify.com'),\n      'https://remix.run:3000/dest',\n    )\n\n    assert.equal(request1.url, 'https://remix.run:3000/dest')\n\n    let { request: request2 } = await testProxy(\n      new Request('http://shopify.com/?q=remix'),\n      'https://remix.run:3000/dest',\n    )\n\n    assert.equal(request2.url, 'https://remix.run:3000/dest?q=remix')\n\n    let { request: request3 } = await testProxy(\n      new Request('http://shopify.com/search?q=remix'),\n      'https://remix.run:3000/',\n    )\n\n    assert.equal(request3.url, 'https://remix.run:3000/search?q=remix')\n\n    let { request: request4 } = await testProxy(\n      new Request('http://shopify.com/search?q=remix'),\n      'https://remix.run:3000/dest',\n    )\n\n    assert.equal(request4.url, 'https://remix.run:3000/dest/search?q=remix')\n  })\n\n  it('forwards request method, headers, and body', async () => {\n    let { request } = await testProxy(\n      new Request('http://shopify.com/search?q=remix', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'text/plain',\n        },\n        body: 'hello',\n      }),\n      'https://remix.run:3000/dest',\n    )\n\n    assert.equal(request.method, 'POST')\n    assert.equal(request.headers.get('Content-Type'), 'text/plain')\n    assert.equal(await request.text(), 'hello')\n  })\n\n  it('forwards an empty request body', async () => {\n    let { request } = await testProxy(\n      new Request('http://shopify.com/search?q=remix', {\n        method: 'POST',\n      }),\n      'https://remix.run:3000/dest',\n    )\n\n    assert.equal(request.method, 'POST')\n    assert.equal(request.headers.get('Content-Type'), null)\n    assert.equal(await request.text(), '')\n  })\n\n  it('forwards various HTTP methods correctly', async () => {\n    let methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD']\n\n    for (let method of methods) {\n      let { request } = await testProxy(\n        new Request('http://shopify.com/api/resource', {\n          method,\n        }),\n        'https://remix.run:3000/backend',\n      )\n\n      assert.equal(request.method, method, `Method ${method} should be forwarded correctly`)\n    }\n  })\n\n  it('does not append X-Forwarded-Proto and X-Forwarded-Host headers by default', async () => {\n    let { request } = await testProxy(\n      new Request('http://shopify.com:8080/search?q=remix'),\n      'https://remix.run:3000/dest',\n    )\n\n    assert.equal(request.headers.get('X-Forwarded-Proto'), null)\n    assert.equal(request.headers.get('X-Forwarded-Host'), null)\n  })\n\n  it('appends X-Forwarded-Proto and X-Forwarded-Host headers when desired', async () => {\n    let { request } = await testProxy(\n      new Request('http://shopify.com:8080/search?q=remix'),\n      'https://remix.run:3000/dest',\n      {\n        xForwardedHeaders: true,\n      },\n    )\n\n    assert.equal(request.headers.get('X-Forwarded-Proto'), 'http')\n    assert.equal(request.headers.get('X-Forwarded-Host'), 'shopify.com:8080')\n  })\n\n  it('forwards additional request init options', async () => {\n    let { request } = await testProxy(\n      new Request('http://shopify.com/search?q=remix', {\n        method: 'DELETE',\n        cache: 'no-cache',\n        credentials: 'include',\n        redirect: 'manual',\n      }),\n      'https://remix.run:3000/dest',\n    )\n\n    assert.equal(request.method, 'DELETE')\n    assert.equal(request.cache, 'no-cache')\n    assert.equal(request.credentials, 'include')\n    assert.equal(request.redirect, 'manual')\n  })\n\n  it('rewrites cookie domain and path', async () => {\n    let { response } = await testProxy(\n      new Request('http://shopify.com/search?q=remix'),\n      'https://remix.run:3000/dest',\n      {\n        async fetch() {\n          return new Response(null, {\n            headers: [\n              ['Set-Cookie', 'name=value; Domain=remix.run:3000; Path=/dest/search'],\n              ['Set-Cookie', 'name2=value2; Domain=remix.run:3000; Path=/dest'],\n            ],\n          })\n        },\n      },\n    )\n\n    let setCookie = response.headers.getSetCookie()\n    assert.ok(setCookie)\n    assert.equal(setCookie.length, 2)\n    assert.equal(setCookie[0], 'name=value; Domain=shopify.com; Path=/search')\n    assert.equal(setCookie[1], 'name2=value2; Domain=shopify.com; Path=/')\n  })\n\n  it('does not rewrite cookie domain and path when opting-out', async () => {\n    let { response } = await testProxy(\n      new Request('http://shopify.com/?q=remix'),\n      'https://remix.run:3000/dest',\n      {\n        rewriteCookieDomain: false,\n        rewriteCookiePath: false,\n        async fetch() {\n          return new Response(null, {\n            headers: [\n              ['Set-Cookie', 'name=value; Domain=remix.run:3000; Path=/dest/search'],\n              ['Set-Cookie', 'name2=value2; Domain=remix.run:3000; Path=/dest'],\n            ],\n          })\n        },\n      },\n    )\n\n    let setCookie = response.headers.getSetCookie()\n    assert.ok(setCookie)\n    assert.equal(setCookie.length, 2)\n    assert.equal(setCookie[0], 'name=value; Domain=remix.run:3000; Path=/dest/search')\n    assert.equal(setCookie[1], 'name2=value2; Domain=remix.run:3000; Path=/dest')\n  })\n\n  it('preserves all request properties when using proxy(request)', async () => {\n    let capturedRequest: Request\n    let proxy = createFetchProxy('https://remix.run:3000/dest', {\n      fetch(input, init) {\n        capturedRequest = new Request(input, init)\n        return Promise.resolve(new Response())\n      },\n    })\n\n    let originalRequest = new Request('http://shopify.com/api/resource', {\n      method: 'PUT',\n      cache: 'no-store',\n      credentials: 'omit',\n      integrity: 'sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=',\n      keepalive: true,\n      mode: 'cors',\n      redirect: 'error',\n      referrer: 'http://example.com',\n      referrerPolicy: 'no-referrer',\n    })\n\n    await proxy(originalRequest)\n\n    assert.ok(capturedRequest!)\n    assert.equal(capturedRequest.method, 'PUT')\n    assert.equal(capturedRequest.cache, 'no-store')\n    assert.equal(capturedRequest.credentials, 'omit')\n    assert.equal(capturedRequest.integrity, 'sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=')\n    assert.equal(capturedRequest.keepalive, true)\n    assert.equal(capturedRequest.mode, 'cors')\n    assert.equal(capturedRequest.redirect, 'error')\n    // Note: The actual referrer value depends on platform-specific behavior in the Request API.\n    // On some platforms, cross-origin referrers may be rejected and fall back to \"about:client\",\n    // so we can't reliably test these values.\n    // assert.equal(capturedRequest.referrer, 'http://example.com/')\n    // assert.equal(capturedRequest.referrerPolicy, 'no-referrer')\n  })\n\n  it('allows init to override request properties', async () => {\n    let capturedRequest: Request\n    let proxy = createFetchProxy('https://remix.run:3000/dest', {\n      fetch(input, init) {\n        capturedRequest = new Request(input, init)\n        return Promise.resolve(new Response())\n      },\n    })\n\n    let originalRequest = new Request('http://shopify.com/api/resource', {\n      method: 'PUT',\n      cache: 'no-store',\n      credentials: 'omit',\n    })\n\n    await proxy(originalRequest, {\n      method: 'POST',\n      cache: 'default',\n      credentials: 'include',\n    })\n\n    assert.ok(capturedRequest!)\n    assert.equal(capturedRequest.method, 'POST')\n    assert.equal(capturedRequest.cache, 'default')\n    assert.equal(capturedRequest.credentials, 'include')\n  })\n})\n\ndescribe('fetch proxy (double-arg style)', () => {\n  it('works with proxy(url, init) style', async () => {\n    let capturedRequest: Request\n    let proxy = createFetchProxy('https://remix.run:3000/dest', {\n      fetch(input, init) {\n        capturedRequest = new Request(input, init)\n        return Promise.resolve(new Response())\n      },\n    })\n\n    await proxy('http://shopify.com/api/resource', {\n      method: 'PATCH',\n      cache: 'reload',\n      credentials: 'same-origin',\n      headers: {\n        'X-Custom': 'value',\n      },\n    })\n\n    assert.ok(capturedRequest!)\n    assert.equal(capturedRequest.method, 'PATCH')\n    assert.equal(capturedRequest.cache, 'reload')\n    assert.equal(capturedRequest.credentials, 'same-origin')\n    assert.equal(capturedRequest.headers.get('X-Custom'), 'value')\n    assert.equal(capturedRequest.url, 'https://remix.run:3000/dest/api/resource')\n  })\n\n  it('handles proxy(url) with defaults', async () => {\n    let capturedRequest: Request\n    let proxy = createFetchProxy('https://remix.run:3000/dest', {\n      fetch(input, init) {\n        capturedRequest = new Request(input, init)\n        return Promise.resolve(new Response())\n      },\n    })\n\n    await proxy('http://shopify.com/api/resource')\n\n    assert.ok(capturedRequest!)\n    assert.equal(capturedRequest.method, 'GET')\n    assert.equal(capturedRequest.url, 'https://remix.run:3000/dest/api/resource')\n  })\n\n  it('forwards headers correctly with proxy(url, init)', async () => {\n    let capturedRequest: Request\n    let proxy = createFetchProxy('https://remix.run:3000/dest', {\n      fetch(input, init) {\n        capturedRequest = new Request(input, init)\n        return Promise.resolve(new Response())\n      },\n    })\n\n    await proxy('http://shopify.com/api/resource', {\n      headers: {\n        Authorization: 'Bearer token123',\n        'Content-Type': 'application/json',\n      },\n    })\n\n    assert.ok(capturedRequest!)\n    assert.equal(capturedRequest.headers.get('Authorization'), 'Bearer token123')\n    assert.equal(capturedRequest.headers.get('Content-Type'), 'application/json')\n  })\n\n  it('handles body with proxy(url, init)', async () => {\n    let capturedRequest: Request\n    let proxy = createFetchProxy('https://remix.run:3000/dest', {\n      fetch(input, init) {\n        capturedRequest = new Request(input, init)\n        return Promise.resolve(new Response())\n      },\n    })\n\n    let body = JSON.stringify({ name: 'test', value: 123 })\n    await proxy('http://shopify.com/api/resource', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body,\n    })\n\n    assert.ok(capturedRequest!)\n    assert.equal(capturedRequest.method, 'POST')\n    assert.equal(await capturedRequest.text(), body)\n  })\n})\n"
  },
  {
    "path": "packages/fetch-proxy/src/lib/fetch-proxy.ts",
    "content": "import { SetCookie } from '@remix-run/headers'\n\n/**\n * Options for {@link createFetchProxy}.\n */\nexport interface FetchProxyOptions {\n  /**\n   * The `fetch` function to use for the actual fetch.\n   *\n   * @default globalThis.fetch\n   */\n  fetch?: typeof globalThis.fetch\n  /**\n   * Set `false` to prevent the `Domain` attribute of `Set-Cookie` headers from being rewritten. By\n   * default the domain will be rewritten to the domain of the incoming request.\n   *\n   * @default true\n   */\n  rewriteCookieDomain?: boolean\n  /**\n   * Set `false` to prevent the `Path` attribute of `Set-Cookie` headers from being rewritten. By\n   * default the portion of the pathname that matches the proxy target's pathname will be removed.\n   *\n   * @default true\n   */\n  rewriteCookiePath?: boolean\n  /**\n   * Set `true` to add `X-Forwarded-Proto` and `X-Forwarded-Host` headers to the proxied request.\n   *\n   * @default false\n   */\n  xForwardedHeaders?: boolean\n}\n\n/**\n * A [`fetch` function](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch)\n * created by {@link createFetchProxy} that forwards requests to another server.\n *\n * @param input The URL or request to forward\n * @param init Optional request init options\n * @returns A promise that resolves to the proxied response\n */\nexport interface FetchProxy {\n  /**\n   * Forwards a request to the configured proxy target.\n   */\n  (input: URL | RequestInfo, init?: RequestInit): Promise<Response>\n}\n\n/**\n * Creates a `fetch` function that forwards requests to another server.\n *\n * @param target The URL of the server to proxy requests to\n * @param options Options to customize the behavior of the proxy\n * @returns A fetch function that forwards requests to the target server\n */\nexport function createFetchProxy(target: string | URL, options?: FetchProxyOptions): FetchProxy {\n  let localFetch = options?.fetch ?? globalThis.fetch\n  let rewriteCookieDomain = options?.rewriteCookieDomain ?? true\n  let rewriteCookiePath = options?.rewriteCookiePath ?? true\n  let xForwardedHeaders = options?.xForwardedHeaders ?? false\n\n  let targetUrl = new URL(target)\n  if (targetUrl.pathname.endsWith('/')) {\n    targetUrl.pathname = targetUrl.pathname.replace(/\\/+$/, '')\n  }\n\n  return async (input: URL | RequestInfo, init?: RequestInit) => {\n    let request = new Request(input, init)\n    let url = new URL(request.url)\n\n    let proxyUrl = new URL(url.search, targetUrl)\n    if (url.pathname !== '/') {\n      proxyUrl.pathname =\n        proxyUrl.pathname === '/' ? url.pathname : proxyUrl.pathname + url.pathname\n    }\n\n    let proxyHeaders = new Headers(request.headers)\n    if (xForwardedHeaders) {\n      proxyHeaders.append('X-Forwarded-Proto', url.protocol.replace(/:$/, ''))\n      proxyHeaders.append('X-Forwarded-Host', url.host)\n    }\n\n    let proxyInit: RequestInit = {\n      method: request.method,\n      headers: proxyHeaders,\n      cache: request.cache,\n      credentials: request.credentials,\n      integrity: request.integrity,\n      keepalive: request.keepalive,\n      mode: request.mode,\n      redirect: request.redirect,\n      referrer: request.referrer,\n      referrerPolicy: request.referrerPolicy,\n      signal: request.signal,\n      ...init,\n    }\n    if (request.method !== 'GET' && request.method !== 'HEAD') {\n      proxyInit.body = request.body\n\n      // init.duplex = 'half' must be set when body is a ReadableStream, and Node follows the spec.\n      // However, this property is not defined in the TypeScript types for RequestInit, so we have\n      // to cast it here in order to set it without a type error.\n      // See https://fetch.spec.whatwg.org/#dom-requestinit-duplex\n      ;(proxyInit as { duplex: 'half' }).duplex = 'half'\n    }\n\n    let response = await localFetch(proxyUrl, proxyInit)\n    let responseHeaders = new Headers(response.headers)\n\n    if (responseHeaders.has('Set-Cookie')) {\n      let setCookie = responseHeaders.getSetCookie()\n\n      responseHeaders.delete('Set-Cookie')\n\n      for (let cookie of setCookie) {\n        let header = new SetCookie(cookie)\n\n        if (rewriteCookieDomain && header.domain) {\n          header.domain = url.host\n        }\n\n        if (rewriteCookiePath && header.path) {\n          if (header.path.startsWith(targetUrl.pathname + '/')) {\n            header.path = header.path.slice(targetUrl.pathname.length)\n          } else if (header.path === targetUrl.pathname) {\n            header.path = '/'\n          }\n        }\n\n        responseHeaders.append('Set-Cookie', header.toString())\n      }\n    }\n\n    return new Response(response.body, {\n      status: response.status,\n      statusText: response.statusText,\n      headers: responseHeaders,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/fetch-proxy/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/fetch-proxy/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/fetch-router/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/fetch-router/.changes/minor.request-context-storage-methods.md",
    "content": "BREAKING CHANGE: Remove `context.storage`, `context.session`, `context.sessionStarted`, `context.formData`, and `context.files` from `@remix-run/fetch-router`, and rename `createStorageKey(...)` to `createContextKey(...)`.\n\n`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`.\n\nSession 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(...)`.\n\n`RequestContext` is now generic only over route params (`RequestContext<{ id: string }>`), and no longer accepts a request-method generic (`RequestContext<'GET', ...>`).\n"
  },
  {
    "path": "packages/fetch-router/.changes/minor.simplify-controller-shape.md",
    "content": "BREAKING CHANGE: `router.map()` controllers for route maps now require a single shape: an object with an `actions` property and optional `middleware`.\n\nMigration: Wrap existing controller objects in `actions`. Nested route maps must also use nested controllers with `{ actions, middleware? }`.\n"
  },
  {
    "path": "packages/fetch-router/.changes/patch.optional-action-middleware.md",
    "content": "The `Action`/`BuildAction` object form accepted by `router.get(...)`, `router.post(...)`, and `router.map(...)` now uses `{ action, middleware? }`, so you can omit `middleware` entirely instead of writing `middleware: []` when you do not need route middleware.\n"
  },
  {
    "path": "packages/fetch-router/CHANGELOG.md",
    "content": "# `fetch-router` CHANGELOG\n\nThis is the changelog for [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router). It follows [semantic versioning](https://semver.org/).\n\n## v0.17.0\n\n### Minor Changes\n\n- Expose `context.router` on request context\n\n  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`.\n\n- Added a new `@remix-run/fetch-router/routes` export exporting route creation utilities\n\n  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`.\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`route-pattern@0.19.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.19.0)\n\n## v0.16.0\n\n### Minor Changes\n\n- BREAKING CHANGE: Remove `Router.size` property\n\n  `Matcher`s no longer keep track of size, so `Router` cannot wrap `Matcher.size` anymore.\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`@remix-run/route-pattern@0.18.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.18.0)\n\n## v0.15.1\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.15.0\n\n### Minor Changes\n\n- 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.\n\n  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:\n\n  ```ts\n  // Before:\n  router.get('/api/users', (context) => {\n    let acceptsJson = context.headers.accept.accepts('application/json')\n    // ...\n  })\n\n  // After:\n  import { Accept } from '@remix-run/headers'\n\n  router.get('/api/users', (context) => {\n    let accept = Accept.from(context.headers.get('accept'))\n    let acceptsJson = accept.accepts('application/json')\n    // ...\n  })\n  ```\n\n## v0.14.0 (2025-12-18)\n\n- BREAKING CHANGE: Remove `BuildRequestHandler` type. Use `RequestHandler` type directly instead.\n\n- BREAKING CHANGE: Remove `T` generic parameter from `RequestHandler` type. Request handlers always return a `Response`.\n\n- Export the `MatchData` type from the public API. This type is required when creating custom matchers for use with the router's `matcher` option.\n\n## v0.13.0 (2025-12-01)\n\n- 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:\n\n  ```tsx\n  // Before\n  import type { RouteHandlers } from '@remix-run/fetch-router'\n\n  let routeHandlers = {\n    middleware: [auth()],\n    handlers: {\n      home() {\n        return new Response('Home')\n      },\n      admin: {\n        middleware: [requireAdmin()],\n        handler() {\n          return new Response('Admin')\n        },\n      },\n    },\n  } satisfies RouteHandlers<typeof routes>\n\n  router.map(routes, routeHandlers)\n\n  // After\n  import type { Controller } from '@remix-run/fetch-router'\n\n  let controller = {\n    middleware: [auth()],\n    actions: {\n      home() {\n        return new Response('Home')\n      },\n      admin: {\n        middleware: [requireAdmin()],\n        action() {\n          return new Response('Admin')\n        },\n      },\n    },\n  } satisfies Controller<typeof routes>\n\n  router.map(routes, controller)\n  ```\n\n  Summary of changes:\n\n  - `RouteHandlers` type => `Controller`\n  - `RouteHandler` type => `Action`\n  - `BuildRouteHandler` type => `BuildAction`\n  - `handlers` property => `actions`\n  - `handler` property => `action`\n\n- BREAKING CHANGE: Renamed `formAction` route helper to `form` and moved route helpers to `lib/route-helpers/` subdirectory. Update your imports:\n\n  ```tsx\n  // Before\n  import { route, formAction } from '@remix-run/fetch-router'\n\n  let routes = route({\n    login: formAction('/login'),\n  })\n\n  // After\n  import { route, form } from '@remix-run/fetch-router'\n\n  let routes = route({\n    login: form('/login'),\n  })\n  ```\n\n  The `FormActionOptions` type has also been renamed to `FormOptions`.\n\n- 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.\n\n  ```tsx\n  // Before: { action } without middleware was allowed\n  router.any(routes.home, {\n    action() {\n      return new Response('Home')\n    },\n  })\n\n  // After: just use a plain request handler function instead\n  router.any(routes.home, () => {\n    return new Response('Home')\n  })\n\n  // Before: { actions } without middleware was allowed\n  router.map(routes, {\n    actions: {\n      home() {\n        return new Response('Home')\n      },\n    },\n  })\n\n  // After: just use a plain controller object instead\n  router.map(routes, {\n    home() {\n      return new Response('Home')\n    },\n  })\n\n  // With middleware, the syntax remains the same (but middleware is now required)\n  router.map(routes, {\n    middleware: [auth()],\n    actions: {\n      home() {\n        return new Response('Home')\n      },\n    },\n  })\n  ```\n\n- Add functional aliases for creating routes that respond to a single request method\n\n  ```tsx\n  import { del, get, patch, post } from '@remix-run/fetch-router'\n\n  let routes = route({\n    home: get('/'),\n    login: post('/login'),\n    logout: post('/logout'),\n    profile: {\n      show: get('/profile'),\n      edit: get('/profile/edit'),\n      update: patch('/profile'),\n      destroy: del('/profile'),\n    },\n  })\n  ```\n\n## v0.12.0 (2025-11-25)\n\n- BREAKING CHANGE: Moved all response helpers to `@remix-run/response`. Update your imports:\n\n  ```tsx\n  // Before\n  import * as res from '@remix-run/fetch-router/response-helpers'\n\n  res.file(file, request)\n  res.html(body)\n  res.redirect(location, status, headers)\n\n  // After\n  import { createFileResponse } from '@remix-run/response/file'\n  import { createHtmlResponse } from '@remix-run/response/html'\n  import { createRedirectResponse } from '@remix-run/response/redirect'\n\n  createFileResponse(file, request)\n  createHtmlResponse(body)\n  createRedirectResponse(location, status)\n  ```\n\n- BREAKING CHANGE: Rename `InferRequestHandler` => `BuildRequestHandler`\n- Add `exclude` option to `resource()` and `resources()` route map helpers (#10858)\n\n## v0.11.0 (2025-11-21)\n\n- BREAKING CHANGE: `Router` is no longer exported as a class, use `createRouter()` instead.\n\n  ```tsx\n  // Before\n  import { Router } from '@remix-run/fetch-router'\n  let router = new Router()\n\n  // After\n  import { createRouter } from '@remix-run/fetch-router'\n  let router = createRouter()\n\n  // For type annotations, use the Router interface\n  import type { Router } from '@remix-run/fetch-router'\n  function setupRoutes(router: Router) {\n    // ...\n  }\n  ```\n\n  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)`.\n\n- Make `middleware` optional in route handler(s) objects passed to `router.map()`\n\n  ```tsx\n  // Before\n  router.map('/', {\n    middleware: [], // required\n    handler() {\n      return new Response('Home')\n    },\n  })\n\n  // After\n  router.map('/', {\n    // middleware is optional!\n    handler() {\n      return new Response('Home')\n    },\n  })\n  ```\n\n## v0.10.0 (2025-11-19)\n\n- BREAKING CHANGE: All middleware has been extracted into separate npm packages for independent versioning and deployment. Update your imports:\n\n  ```tsx\n  // Before\n  import { asyncContext } from '@remix-run/fetch-router/async-context-middleware'\n  import { formData } from '@remix-run/fetch-router/form-data-middleware'\n  import { logger } from '@remix-run/fetch-router/logger-middleware'\n  import { methodOverride } from '@remix-run/fetch-router/method-override-middleware'\n  import { session } from '@remix-run/fetch-router/session-middleware'\n  import { staticFiles } from '@remix-run/fetch-router/static-middleware'\n\n  // After\n  import { asyncContext } from '@remix-run/async-context-middleware'\n  import { formData } from '@remix-run/form-data-middleware'\n  import { logger } from '@remix-run/logger-middleware'\n  import { methodOverride } from '@remix-run/method-override-middleware'\n  import { session } from '@remix-run/session-middleware'\n  import { staticFiles } from '@remix-run/static-middleware'\n  ```\n\n  Each middleware now has its own package with independent dependencies, changelog, and versioning.\n\n- `html()` response helper now automatically prepends `<!DOCTYPE html>` to the body if it is not already present\n\n## v0.9.0 (2025-11-18)\n\n- Add `session` middleware for automatic management of `context.session` across requests\n\n  ```tsx\n  import { createCookie } from '@remix-run/cookie'\n  import { createFileStorage } from '@remix-run/session/file-storage'\n  import { session } from '@remix-run/fetch-router/session-middleware'\n\n  let cookie = createCookie('session', { secrets: ['s3cr3t'] })\n  let storage = createFileStorage('/tmp/sessions')\n\n  let router = createRouter({\n    middleware: [session(cookie, storage)],\n  })\n\n  router.map('/', ({ session }) => {\n    session.set('count', Number(session.get('count') ?? 0) + 1)\n    return new Response(`Count: ${session.get('count')}`)\n  })\n  ```\n\n- Add `asyncContext` middleware for storing the request context in `AsyncLocalStorage` so it is available to all functions in the same async execution context\n\n  ```tsx\n  import * as assert from 'node:assert/strict'\n  import { asyncContext } from '@remix-run/fetch-router/async-context-middleware'\n\n  let router = createRouter({\n    middleware: [asyncContext()],\n  })\n\n  router.map('/', (context) => {\n    assert.equal(context, getContext())\n    return new Response('Home')\n  })\n  ```\n\n- Add `file` response helper for serving files\n\n  ```tsx\n  import * as res from '@remix-run/fetch-router/response-helpers'\n  import { openFile } from '@remix-run/fs'\n\n  router.get('/assets/:filename', async ({ request, params }) => {\n    let file = openFile(`./public/assets/${params.filename}`)\n    return res.file(file, request)\n  })\n  ```\n\n- Add `staticFiles` middleware for serving static files\n\n  ```tsx\n  import { staticFiles } from '@remix-run/fetch-router/static-middleware'\n\n  let router = createRouter({\n    middleware: [staticFiles('./public')],\n  })\n  ```\n\n## v0.8.0 (2025-11-03)\n\n- BREAKING CHANGE: Rework how middleware works in the router. This change has far-reaching implications.\n\n  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.\n\n  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.\n\n  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.\n\n  To migrate, move middleware from `router.use()` to `createRouter({ middleware })`.\n\n  ```tsx\n  // before\n  let router = createRouter()\n  router.use(middleware)\n  router.map(routes.home, () => new Response('Home'))\n\n  // after\n  let router = createRouter({\n    middleware: [middleware],\n  })\n  router.map(routes.home, () => new Response('Home'))\n  ```\n\n- BREAKING CHANGE: Rename `use` => `middleware` in route handler definitions\n\n  ```tsx\n  // before\n  router.map(routes.home, {\n    use: [middleware],\n    handler() {\n      return new Response('Home')\n    },\n  })\n\n  // after\n  router.map(routes.home, {\n    middleware: [middleware],\n    handler() {\n      return new Response('Home')\n    },\n  })\n  ```\n\n- 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.\n\n- 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.\n\n  ```tsx\n  // before\n  let router = createRouter({ parseFormData: true, methodOverride: true, uploadHandler })\n\n  // after\n  import { formData } from '@remix-run/fetch-router/form-data-middleware'\n  import { methodOverride } from '@remix-run/fetch-router/method-override-middleware'\n\n  let router = createRouter()\n  router.use(formData({ uploadHandler }))\n  router.use(methodOverride())\n  ```\n\n  This change makes things a little more verbose but should ultimately lead to more flexible middleware composition and a smaller core build.\n\n## v0.7.0 (2025-10-31)\n\n- BREAKING CHANGE: Move `@remix-run/form-data-parser`, `@remix-run/headers`, and `@remix-run/route-pattern` to `peerDependencies`.\n- BREAKING CHANGE: Rename `InferRouteHandler` => `BuildRouteHandler` and add a `Method` generic parameter to build a `RouteHandler` type from a string, route pattern, or route.\n- BREAKING CHANGE: Removed support for passing a `Route` object to `redirect()` response helper. Use `redirect(routes.home.href())` instead.\n- BREAKING CHANGE: Move `html()`, `json()`, and `redirect()` response helpers to `@remix-run/fetch-router/response-helpers` export\n- Always run global middleware, even when no route matches\n- More precise type inference for `router.get()`, `router.post()`, etc. route handlers.\n- Add support for nesting route maps via object spread syntax\n\n  ```tsx\n  import { route, resources } from '@remix-run/fetch-router'\n\n  let routes = route({\n    brands: {\n      ...resources('brands', { only: ['index', 'show'] }),\n      products: resources('brands/:brandId/products', { only: ['index', 'show'] }),\n    },\n  })\n\n  routes.brands.index // Route<'GET', '/brands'>\n  routes.brands.show // Route<'GET', '/brands/:id'>\n  routes.brands.products.index // Route<'GET', '/brands/:brandId/products'>\n  routes.brands.products.show // Route<'GET', '/brands/:brandId/products/:id'>\n  ```\n\n- Add support for `URL` objects in `redirect()` response helper\n- 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\n- Fix an issue where `Router`'s `fetch` wasn't spec-compliant\n- Provide empty `context.formData` to `POST`/`PUT`/etc handlers when `parseFormData: false`\n\n## v0.6.0 (2025-10-10)\n\n- BREAKING CHANGE: Rename\n  - `resource('...', { routeNames })` to `resource('...', { names })`\n  - `resources('...', { routeNames })` to `resources('...', { names })`\n  - `formAction('...', { routeNames })` to `formAction('...', { names })`\n  - `formAction('...', { submitMethod })` to `formAction('...', { formMethod })`\n- 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.\n\n  ```tsx\n  let router = createRouter({\n    // Options for parsing form data, or `false` to disable\n    parseFormData: {\n      maxFiles: 5, // Maximum number of files that can be uploaded in a single request\n      maxFileSize: 10 * 1024 * 1024, // 10MB maximum size of each file\n      maxHeaderSize: 1024 * 1024, // 1MB maximum size of the header\n    },\n    // A function that handles file uploads. It receives a `FileUpload` object and may return any value that is valid in a `FormData` object\n    uploadHandler(file: FileUpload) {\n      // save the file to disk/storage...\n      return '/uploads/file.jpg'\n    },\n    // The name of the form field to check for method override, or `false` to disable\n    methodOverride: '_method',\n  })\n  ```\n\n- Export `InferRouteHandler` and `InferRequestHandler` types\n- Re-export `FormDataParseError`, `FileUpload`, and `FileUploadHandler` from `@remix-run/form-data-parser`\n- Fix an issue where per-route middleware was not being applied to a route handler nested inside a route map with its own middleware\n\n## v0.5.0 (2025-10-05)\n\n- Add `formData` middleware for parsing `FormData` objects from the request body\n\n  ```tsx\n  import { formData } from '@remix-run/fetch-router/form-data-middleware'\n\n  let router = createRouter()\n\n  router.use(formData())\n\n  router.map('/', ({ formData, files }) => {\n    console.log(formData) // FormData from the request body\n    console.log(files) // Record<string, File> from the request body\n    return new Response('Home')\n  })\n  ```\n\n- Add `storage.has(key)` for checking if a value is stored for a given key\n- Add `next(moreContext)` API for passing additional context to the next middleware or handler in the chain\n- Move `logger` middleware to `@remix-run/fetch-router/logger-middleware` export\n- Add `json` and `redirect` response helpers\n\n  ```tsx\n  import { json, redirect, createRouter } from '@remix-run/fetch-router'\n\n  let router = createRouter()\n\n  router.map('/api', () => {\n    return json({ message: 'Hello, world!' })\n  })\n\n  router.map('/*path/', ({ params }) => {\n    // Strip all trailing slashes from URL paths\n    return redirect(`/${params.path}`, 301)\n  })\n  ```\n\n  `redirect` also accepts a `Route` object for type-safe redirects:\n\n  ```tsx\n  let routes = createRoutes({\n    home: '/',\n  })\n\n  let response = redirect(routes.home)\n  ```\n\n  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.\n\n## v0.4.0 (2025-10-04)\n\n- 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\n\n  ```tsx\n  // before\n  router.map('/', [middleware], () => {\n    return new Response('Home')\n  })\n\n  // after\n  router.map('/', {\n    use: [middleware],\n    handler(ctx) {\n      return new Response('Home')\n    },\n  })\n  ```\n\n- 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.\n\n  ```tsx\n  let books = createResources('books', {\n    routeNames: { index: 'list', show: 'view' },\n  })\n\n  books.list // Route<'GET', '/books'>\n  books.view // Route<'GET', '/books/:id'>\n  ```\n\n- Add `route` shorthand for `createRoutes` to public exports\n- Add support for any `BodyInit` in `html(body)` response helper\n- 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.\n- Export `RouteHandlers` and `RouteHandler` types\n\n## v0.3.0 (2025-10-03)\n\n- Add `router.map()` for registering routes and middleware either one at a time or in bulk\n\n  One at a time:\n\n  ```tsx\n  let router = createRouter()\n  router.map('/', () => new Response('Home'))\n  router.map('/blog', () => new Response('Blog'))\n  ```\n\n  In bulk:\n\n  ```tsx\n  let routes = createRoutes({\n    home: '/',\n    blog: '/blog',\n  })\n\n  let router = createRouter()\n\n  router.map(routes, {\n    home() {\n      return new Response('Home')\n    },\n    blog() {\n      return new Response('Blog')\n    },\n  })\n  ```\n\n- Add `createResource` and `createResources` functions for creating resource-based route maps\n\n  ```tsx\n  import { resource, resources, createRoutes } from '@remix-run/fetch-router'\n\n  let routes = createRoutes({\n    home: '/',\n    books: resources('books'), // Plural resources\n    profile: resource('profile'), // Singleton resource\n  })\n\n  let router = createRouter()\n\n  // Plural resources\n  router.map(routes.books, {\n    // GET /books\n    index() {\n      return new Response('Books Index')\n    },\n    // POST /books\n    create() {\n      return new Response('Book Created', { status: 201 })\n    },\n    // GET /books/new\n    new() {\n      return new Response('New Book')\n    },\n    // GET /books/:id\n    show({ params }) {\n      return new Response(`Book ${params.id}`)\n    },\n    // GET /books/:id/edit\n    edit({ params }) {\n      return new Response(`Edit Book ${params.id}`)\n    },\n    // PUT /books/:id\n    update({ params }) {\n      return new Response(`Updated Book ${params.id}`)\n    },\n    // DELETE /books/:id\n    destroy({ params }) {\n      return new Response(`Destroyed Book ${params.id}`)\n    },\n  })\n\n  // Singleton resource\n  router.map(routes.profile, {\n    // GET /profile/:id\n    show({ params }) {\n      return new Response(`Profile ${params.id}`)\n    },\n    // GET /profile/new\n    new() {\n      return new Response('New Profile')\n    },\n    // POST /profile\n    create() {\n      return new Response('Profile Created', { status: 201 })\n    },\n    // GET /profile/:id/edit\n    edit({ params }) {\n      return new Response(`Edit Profile ${params.id}`)\n    },\n    // PUT /profile/:id\n    update({ params }) {\n      return new Response(`Updated Profile ${params.id}`)\n    },\n    // DELETE /profile/:id\n    destroy({ params }) {\n      return new Response(`Destroyed Profile ${params.id}`)\n    },\n  })\n  ```\n\n## v0.2.0 (2025-10-02)\n\n- Add `router.mount(prefix, router)` method for mounting a router at a given pathname prefix in another router\n\n  ```tsx\n  let apiRouter = createRouter()\n  apiRouter.get('/', () => new Response('API'))\n\n  let router = createRouter()\n  router.mount('/api', apiRouter)\n\n  let response = await router.fetch('https://remix.run/api')\n\n  assert.equal(response.status, 200)\n  assert.equal(await response.text(), 'API')\n  ```\n\n## v0.1.0 (2025-10-01)\n\n- Initial release\n"
  },
  {
    "path": "packages/fetch-router/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/fetch-router/README.md",
    "content": "# fetch-router\n\nA minimal, composable router built on the [web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and [`route-pattern`](https://github.com/remix-run/remix/tree/main/packages/route-pattern). Ideal for building APIs, web services, and server-rendered applications.\n\n## Features\n\n- **Fetch API**: Built on standard web APIs that work everywhere - Node.js, Bun, Deno, Cloudflare Workers, and browsers\n- **Type-Safe Routing**: Leverage TypeScript for compile-time route validation and parameter inference\n- **Composable Architecture**: Nest routers, combine middleware, and organize routes hierarchically\n- **Declarative Route Maps**: Define your entire route structure upfront with type-safe route names and request methods\n- **Flexible Middleware**: Apply middleware globally, per-route, or to entire route hierarchies\n- **Easy Testing**: Use standard `fetch()` to test your routes - no special test harness required\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nThe 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).\n\nImport 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`.\n\n```ts\n// routes.ts\nimport { route, form, resources } from 'remix/fetch-router/routes'\n\n// router.ts\nimport { createRouter } from 'remix/fetch-router'\n```\n\nThe example below is a small site with a home page, an \"about\" page, and a blog.\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { route } from 'remix/fetch-router/routes'\nimport { logger } from 'remix/logger-middleware'\n\n// `route()` creates a \"route map\" that organizes routes by name. The keys\n// of the map may be any name, and may be nested to group related routes.\nlet routes = route({\n  home: '/',\n  about: '/about',\n  blog: {\n    index: '/blog',\n    show: '/blog/:slug',\n  },\n})\n\nlet router = createRouter({\n  // Middleware may be used to run code before and/or after actions run.\n  // In this case, the `logger()` middleware logs the request to the console.\n  middleware: [logger()],\n})\n\n// Map the routes to a \"controller\" that defines actions for each route.\n// Controllers always use the shape: { actions, middleware? }.\nrouter.map(routes, {\n  actions: {\n    home() {\n      return new Response('Home')\n    },\n    about() {\n      return new Response('About')\n    },\n    blog: {\n      actions: {\n        index() {\n          return new Response('Blog')\n        },\n        show({ params }) {\n          // params is a type-safe object with the parameters from the route pattern\n          return new Response(`Post ${params.slug}`)\n        },\n      },\n    },\n  },\n})\n\nlet response = await router.fetch('https://remix.run/blog/hello-remix')\nconsole.log(await response.text()) // \"Post hello-remix\"\n```\n\nThe 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.\n\n```ts\ntype Routes = typeof routes\n// {\n//   home: Route<'ANY', '/'>\n//   about: Route<'ANY', '/about'>\n//   blog: {\n//     index: Route<'ANY', '/blog'>\n//     show: Route<'ANY', '/blog/:slug'>\n//   },\n// }\n```\n\nThe `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.\n\n### Links and Form Actions\n\nIn 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.\n\nNote: We're using the [`createHtmlResponse` helper from `@remix-run/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.\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { route } from 'remix/fetch-router/routes'\nimport { html } from 'remix/html-template'\nimport { createHtmlResponse } from 'remix/response/html'\n\nlet routes = route({\n  home: '/',\n  contact: '/contact',\n})\n\nlet router = createRouter()\n\n// Register an action for `GET /`\nrouter.get(routes.home, () => {\n  return createHtmlResponse(`\n    <html>\n      <body>\n        <h1>Home</h1>\n        <p>\n          <a href=\"${routes.contact.href()}\">Contact Us</a>\n        </p>\n      </body>\n    </html>\n  `)\n})\n\n// Register an action for `GET /contact`\nrouter.get(routes.contact, () => {\n  return createHtmlResponse(`\n    <html>\n      <body>\n        <h1>Contact Us</h1>\n        <form method=\"POST\" action=\"${routes.contact.href()}\">\n          <div>\n            <label for=\"message\">Message</label>\n            <input type=\"text\" name=\"message\" />\n          </div>\n          <button type=\"submit\">Send</button>\n        </form>\n        <footer>\n          <p>\n            <a href=\"${routes.home.href()}\">Home</a>\n          </p>\n        </footer>\n      </body>\n    </html>\n  `)\n})\n\n// Register an action for `POST /contact`\nrouter.post(routes.contact, ({ get }) => {\n  // POST actions can read parsed FormData from request context using FormData\n  // as the context key after the formData middleware has run.\n  let formData = get(FormData)\n  let message = formData.get('message') as string\n  let body = html`\n    <html>\n      <body>\n        <h1>Thanks!</h1>\n        <div>\n          <p>You said: ${message}</p>\n        </div>\n        <footer>\n          <p>\n            <a href=\"${routes.home.href()}\">Home</a>\n          </p>\n        </footer>\n      </body>\n    </html>\n  `\n\n  return createHtmlResponse(body)\n})\n```\n\n### Routing Based on Request Method\n\nIn 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:\n\n```tsx\ntype HomeRoute = typeof routes.home // Route<'ANY', '/'>\ntype ContactRoute = typeof routes.contact // Route<'ANY', '/contact'>\n```\n\nWe used `router.get()` and `router.post()` to register actions on each route specifically for the `GET` and `POST` request methods.\n\nHowever, 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.\n\n```ts\nimport * as assert from 'node:assert/strict'\nimport { createRouter } from 'remix/fetch-router'\nimport { route } from 'remix/fetch-router/routes'\n\nlet routes = route({\n  home: { method: 'GET', pattern: '/' },\n  contact: {\n    index: { method: 'GET', pattern: '/contact' },\n    action: { method: 'POST', pattern: '/contact' },\n  },\n})\n\ntype Routes = typeof routes\n// Each route is now typed with a specific request method.\n// {\n//   home: Route<'GET', '/'>,\n//   contact: {\n//     index: Route<'GET', '/contact'>,\n//     action: Route<'POST', '/contact'>,\n//   },\n// }\n\nlet router = createRouter()\n\nrouter.map(routes, {\n  actions: {\n    home({ method }) {\n      assert.equal(method, 'GET')\n      return new Response('Home')\n    },\n    contact: {\n      actions: {\n        index({ method }) {\n          assert.equal(method, 'GET')\n          return new Response('Contact')\n        },\n        action({ method }) {\n          assert.equal(method, 'POST')\n          return new Response('Contact Action')\n        },\n      },\n    },\n  },\n})\n```\n\n### Declaring Routes\n\nIn 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:\n\n- [`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.\n- [`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).\n\n#### Declaring Form Routes\n\nContinuing 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.\n\nA `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.\n\n```tsx\nimport { createRouter } from 'remix/fetch-router'\nimport { route, form } from 'remix/fetch-router/routes'\nimport { createHtmlResponse } from 'remix/response/html'\nimport { html } from 'remix/html-template'\n\nlet routes = route({\n  home: '/',\n  contact: form('contact'),\n})\n\ntype Routes = typeof routes\n// {\n//   home: Route<'ANY', '/'>\n//   contact: {\n//     index: Route<'GET', '/contact'> - Shows the form\n//     action: Route<'POST', '/contact'> - Handles the form submission\n//   },\n// }\n\nlet router = createRouter()\n\nrouter.map(routes, {\n  actions: {\n    home() {\n      return createHtmlResponse(`\n        <html>\n          <body>\n            <h1>Home</h1>\n            <footer>\n              <p>\n                <a href=\"${routes.contact.index.href()}\">Contact Us</a>\n              </p>\n            </footer>\n          </body>\n        </html>\n      `)\n    },\n    contact: {\n      actions: {\n        // GET /contact - shows the form\n        index() {\n          return createHtmlResponse(`\n            <html>\n              <body>\n                <h1>Contact Us</h1>\n                <form method=\"POST\" action=\"${routes.contact.action.href()}\">\n                  <label for=\"message\">Message</label>\n                  <input type=\"text\" name=\"message\" />\n                  <button type=\"submit\">Send</button>\n                </form>\n              </body>\n            </html>\n          `)\n        },\n        // POST /contact - handles the form submission\n        action({ get }) {\n          let formData = get(FormData)\n          let message = formData.get('message') as string\n          let body = html`\n            <html>\n              <body>\n                <h1>Thanks!</h1>\n                <p>You said: ${message}</p>\n\n                <p>\n                  Got more to say? <a href=\"${routes.contact.index.href()}\">Send another message</a>\n                </p>\n              </body>\n            </html>\n          `\n\n          return createHtmlResponse(body)\n        },\n      },\n    },\n  },\n})\n```\n\n#### Resource-based Routes\n\nThe 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.\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { route, resources } from 'remix/fetch-router/routes'\n\nlet routes = route({\n  brands: {\n    ...resources('brands', { only: ['index', 'show'] }),\n    products: resources('brands/:brandId/products', {\n      only: ['index', 'show'],\n    }),\n  },\n})\n\ntype Routes = typeof routes\n// {\n//   brands: {\n//     index: Route<'GET', '/brands'>\n//     show: Route<'GET', '/brands/:id'>\n//     products: {\n//       index: Route<'GET', '/brands/:brandId/products'>\n//       show: Route<'GET', '/brands/:brandId/products/:id'>\n//     },\n//   },\n// }\n\nlet router = createRouter()\n\nrouter.map(routes.brands, {\n  actions: {\n    // GET /brands\n    index() {\n      return new Response('Brands Index')\n    },\n    // GET /brands/:id\n    show({ params }) {\n      return new Response(`Brand ${params.id}`)\n    },\n    products: {\n      actions: {\n        // GET /brands/:brandId/products\n        index() {\n          return new Response('Products Index')\n        },\n        // GET /brands/:brandId/products/:id\n        show({ params }) {\n          return new Response(`Brand ${params.brandId}, Product ${params.id}`)\n        },\n      },\n    },\n  },\n})\n```\n\nThe `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.\n\n```tsx\nimport { createRouter } from 'remix/fetch-router'\nimport { route, resources, resource } from 'remix/fetch-router/routes'\n\nlet routes = route({\n  user: {\n    ...resources('users', { only: ['index', 'show'] }),\n    profile: resource('users/:userId/profile', { only: ['show', 'edit', 'update'] }),\n  },\n})\n\ntype Routes = typeof routes\n// {\n//   user: {\n//     index: Route<'GET', '/users'>\n//     show: Route<'GET', '/users/:id'>\n//     profile: {\n//       show: Route<'GET', '/users/:userId/profile'>\n//       edit: Route<'GET', '/users/:userId/profile/edit'>\n//       update: Route<'PUT', '/users/:userId/profile'>\n//     },\n//   },\n// }\n```\n\nIn 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`.\n\n```tsx\nlet routes = resources('users')\ntype Routes = typeof routes\n// {\n//   index: Route<'GET', '/users'> - Lists all users\n//   new: Route<'GET', '/users/new'> - Shows a form to create a new user\n//   show: Route<'GET', '/users/:id'> - Shows a single user\n//   create: Route<'POST', '/users'> - Creates a new user\n//   edit: Route<'GET', '/users/:id/edit'> - Shows a form to edit a user\n//   update: Route<'PUT', '/users/:id'> - Updates a user\n//   destroy: Route<'DELETE', '/users/:id'> - Deletes a user\n// }\n```\n\nSimilarly, 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.\n\n```tsx\nlet routes = resource('profile')\ntype Routes = typeof routes\n// {\n//   new: Route<'GET', '/profile/new'> - Shows a form to create the profile\n//   show: Route<'GET', '/profile'> - Shows the profile\n//   create: Route<'POST', '/profile'> - Creates the profile\n//   edit: Route<'GET', '/profile/edit'> - Shows a form to edit the profile\n//   update: Route<'PUT', '/profile'> - Updates the profile\n//   destroy: Route<'DELETE', '/profile'> - Deletes the profile\n// }\n```\n\nResource 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.\n\n```tsx\nimport { createRouter } from 'remix/fetch-router'\nimport { route, resources } from 'remix/fetch-router/routes'\n\nlet routes = route({\n  users: resources('users', {\n    only: ['index', 'show'],\n    names: { index: 'list', show: 'view' },\n  }),\n})\ntype Routes = typeof routes.users\n// {\n//   list: Route<'GET', '/users'> - Lists all users\n//   view: Route<'GET', '/users/:id'> - Shows a single user\n// }\n```\n\nIf you want to use a param name other than `id`, you can use the `param` option.\n\n```tsx\nimport { createRouter } from 'remix/fetch-router'\nimport { route, resources } from 'remix/fetch-router/routes'\n\nlet routes = route({\n  users: resources('users', {\n    only: ['index', 'show', 'edit', 'update'],\n    param: 'userId',\n  }),\n})\ntype Routes = typeof routes.users\n// {\n//   index: Route<'GET', '/users'> - Lists all users\n//   show: Route<'GET', '/users/:userId'> - Shows a single user\n//   edit: Route<'GET', '/users/:userId/edit'> - Shows a form to edit a user\n//   update: Route<'PUT', '/users/:userId'> - Updates a user\n// }\n```\n\nYou can use the `exclude` option to exclude routes from being generated.\n\n```tsx\nlet routes = resources('users', { exclude: ['edit', 'update', 'destroy'] })\ntype Routes = typeof routes\n// {\n//   index: Route<'GET', '/users'> - Lists all users\n//   new: Route<'GET', '/users/new'> - Shows a form to create a new user\n//   show: Route<'GET', '/users/:userId'> - Shows a single user\n//   create: Route<'POST', '/users'> - Creates a new user\n// }\n```\n\n### Controllers and Middleware\n\nMiddleware functions run code before and/or after actions. They are a powerful way to add functionality to your app.\n\nA basic logging middleware might look like this:\n\n```ts\nimport type { Middleware } from 'remix/fetch-router'\n\n// You can use the `Middleware` type to type middleware functions.\nfunction logger(): Middleware {\n  return async (context, next) => {\n    let start = new Date()\n\n    // Call next() to invoke the next middleware or action in the chain.\n    let response = await next()\n\n    let end = new Date()\n    let duration = end.getTime() - start.getTime()\n\n    console.log(`${context.request.method} ${context.request.url} ${response.status} ${duration}ms`)\n\n    return response\n  }\n}\n\n// Use it like this:\nlet router = createRouter({\n  middleware: [logger()],\n})\n```\n\nMiddleware 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.\n\n```tsx\ninterface AuthOptions {\n  token: string\n}\n\nfunction auth(options?: AuthOptions): Middleware {\n  let token = options?.token ?? 'secret'\n\n  return (context, next) => {\n    if (context.headers.get('Authorization') !== `Bearer ${token}`) {\n      return new Response('Unauthorized', { status: 401 })\n    }\n    return next()\n  }\n}\n```\n\nMiddleware may be used in two different contexts: globally (at the router level) or inline (at the route level).\n\nGlobal 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.\n\nInline (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 `{ action, middleware? }`, so you can omit `middleware` entirely when you do not need it.\n\n```tsx\nlet routes = route({\n  home: '/',\n  admin: {\n    dashboard: '/admin/dashboard',\n  },\n})\n\nlet router = createRouter({\n  // This middleware runs on all requests.\n  middleware: [staticFiles('./public')],\n})\n\nrouter.map(routes.home, () => new Response('Home'))\n\nrouter.map(routes.admin.dashboard, {\n  // This middleware runs only on the `/admin/dashboard` route.\n  middleware: [auth({ token: 'secret' })],\n  action() {\n    return new Response('Dashboard')\n  },\n})\n```\n\n### Request Context\n\nEvery action and middleware receives a `context` object with useful properties:\n\n```ts\nlet userKey = createContextKey<{ id: string }>()\n\nrouter.get('/posts/:id', ({ request, url, params, set, get }) => {\n  // request: The original Request object\n  console.log(request.method) // \"GET\"\n  console.log(request.headers.get('Accept'))\n\n  // url: Parsed URL object\n  console.log(url.pathname) // \"/posts/123\"\n  console.log(url.searchParams.get('sort'))\n\n  // params: Route parameters (fully typed!)\n  console.log(params.id) // \"123\"\n\n  // set/get: type-safe request-scoped context data on the context object\n  set(userKey, currentUser)\n  let user = get(userKey)\n  console.log(user.id)\n\n  return new Response(`Post ${params.id}`)\n})\n```\n\n### Additional Topics\n\n#### Scaling Your Application\n\n- how to use a TrieMatcher\n- how to spread controllers across multiple files\n\n#### Error Handling and Aborted Requests\n\n- wrap `router.fetch()` in a try/catch to handle errors\n- `AbortError` is thrown when a request is aborted\n\n#### Content Negotiation\n\n- use `Accept.from()` from `@remix-run/headers` to serve different responses based on the client's `Accept` header\n  - maybe put this on `context.accepts()` for convenience?\n\n#### Sessions\n\n- use a custom `sessionStorage` implementation to store session data\n- use `session.get()` and `session.set()` to get and set session data\n- use `session.flash()` to set a flash message\n- use `session.destroy()` to destroy the session\n\n#### Form Data and File Uploads\n\n- use the `formData()` middleware to parse the `FormData` object from the request body\n- use `context.get(FormData)` to access parsed form data\n- use `context.get(FormData).get(name)`/`getAll(name)` to access uploaded files\n- use the `uploadHandler` option of the `formData()` middleware to handle file uploads\n\n#### Request Method Override\n\n- use the `methodOverride()` middleware to override the request method\n- use a hidden `<input name=\"_method\" value=\"...\">` to override the request method\n\n### Response Helpers\n\nResponse helpers for creating common HTTP responses are available in the [`@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response) package:\n\n```tsx\nimport { createFileResponse } from 'remix/response/file'\nimport { createHtmlResponse } from 'remix/response/html'\nimport { createRedirectResponse } from 'remix/response/redirect'\nimport { compressResponse } from 'remix/response/compress'\n\nlet response = createHtmlResponse('<h1>Hello</h1>')\nlet response = Response.json({ message: 'Hello' })\nlet response = createRedirectResponse('/')\nlet response = compressResponse(uncompressedResponse, request)\n```\n\nSee the [`@remix-run/response` documentation](https://github.com/remix-run/remix/tree/main/packages/response#readme) for more details.\n\n### Working with HTML\n\nFor 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.\n\n```ts\nimport { html } from 'remix/html-template'\nimport { createHtmlResponse } from 'remix/response/html'\n\n// Use the template tag to escape unsafe variables in HTML.\nlet unsafe = '<script>alert(1)</script>'\nlet response = createHtmlResponse(html`<h1>${unsafe}</h1>`, { status: 400 })\n```\n\nThe `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:\n\n```ts\n// Use html.raw as a template tag to skip escaping interpolations\nlet safeHtml = '<b>Bold</b>'\nlet content = html.raw`<div class=\"content\">${safeHtml}</div>`\nlet response = createHtmlResponse(content)\n\n// This is particularly useful when building HTML from multiple safe fragments\nlet header = '<header>Title</header>'\nlet body = '<main>Content</main>'\nlet footer = '<footer>Footer</footer>'\nlet page = html.raw`\n  <!DOCTYPE html>\n  <html>\n    <body>\n      ${header}\n      ${body}\n      ${footer}\n    </body>\n  </html>\n`\n\n// You can nest html.raw inside html to preserve SafeHtml fragments\nlet icon = html.raw`<svg>...</svg>`\nlet button = html`<button>${icon} Click me</button>` // icon is not escaped\n```\n\n**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.\n\nSee the [`@remix-run/html-template` documentation](https://github.com/remix-run/remix/tree/main/packages/html-template#readme) for more details.\n\n### Testing\n\nTesting is straightforward because `fetch-router` uses the standard `fetch()` API:\n\n```ts\nimport * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\ndescribe('blog routes', () => {\n  it('creates a new post', async () => {\n    let response = await router.fetch('https://api.remix.run/posts', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ title: 'Hello', content: 'World' }),\n    })\n\n    assert.equal(response.status, 201)\n    let post = await response.json()\n    assert.equal(post.title, 'Hello')\n  })\n\n  it('returns 404 for missing posts', async () => {\n    let response = await router.fetch('https://api.remix.run/posts/not-found')\n    assert.equal(response.status, 404)\n  })\n})\n```\n\nNo special test harness or mocking required! Just use `fetch()` like you would in production.\n\n## Related Work\n\n- [@remix-run/response](../response) - Response helpers for HTML, JSON, files, and redirects\n- [@remix-run/headers](../headers) - A library for working with HTTP headers\n- [@remix-run/form-data-parser](../form-data-parser) - A library for parsing multipart/form-data requests\n- [@remix-run/route-pattern](../route-pattern) - The pattern matching library that powers `fetch-router`\n- [Express](https://expressjs.com/) - The classic Node.js web framework\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/fetch-router/demos/bun/README.md",
    "content": "# fetch-router Bun Example\n\nThis example is a [Bun](https://bun.sh/) server that handles routing using `@remix-run/fetch-router`.\n"
  },
  {
    "path": "packages/fetch-router/demos/bun/app/data.ts",
    "content": "export interface Post {\n  id: string\n  title: string\n  content: string\n  author: string\n  createdAt: Date\n}\n\nlet posts: Post[] = [\n  {\n    id: '1',\n    title: 'Welcome to the Blog',\n    content: 'This is a simple blog demo built with fetch-router on Bun.',\n    author: 'Admin',\n    createdAt: new Date('2025-01-01'),\n  },\n  {\n    id: '2',\n    title: 'Getting Started with fetch-router',\n    content: 'fetch-router is a minimal, composable router built on the web Fetch API.',\n    author: 'Admin',\n    createdAt: new Date('2025-01-02'),\n  },\n]\n\nexport function getPosts() {\n  return posts.toSorted((a, b) => b.createdAt.getTime() - a.createdAt.getTime())\n}\n\nexport function getPost(id: string) {\n  return posts.find((p) => p.id === id)\n}\n\nexport function createPost(title: string, content: string, author: string) {\n  let post: Post = {\n    id: String(posts.length + 1),\n    title,\n    content,\n    author,\n    createdAt: new Date(),\n  }\n  posts.push(post)\n  return post\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/bun/app/router.ts",
    "content": "import { createRouter } from '@remix-run/fetch-router'\nimport { createCookie } from '@remix-run/cookie'\nimport * as s from '@remix-run/data-schema'\nimport * as f from '@remix-run/data-schema/form-data'\nimport { Session } from '@remix-run/session'\nimport { createCookieSessionStorage } from '@remix-run/session/cookie-storage'\nimport { formData } from '@remix-run/form-data-middleware'\nimport { logger } from '@remix-run/logger-middleware'\nimport { session } from '@remix-run/session-middleware'\nimport { html } from '@remix-run/html-template'\nimport { createHtmlResponse } from '@remix-run/response/html'\nimport { createRedirectResponse as redirect } from '@remix-run/response/redirect'\nimport type { Middleware } from '@remix-run/fetch-router'\n\nimport { routes } from './routes.ts'\nimport * as data from './data.ts'\nimport path from 'node:path'\n\nconst textField = f.field(s.defaulted(s.string(), ''))\nconst loginSchema = f.object({\n  username: textField,\n})\nconst postSchema = f.object({\n  title: textField,\n  content: textField,\n})\n\nlet sessionCookie = createCookie('__sess', {\n  secrets: ['s3cr3t'],\n})\nlet sessionStorage = createCookieSessionStorage()\n\nfunction requireAuth(): Middleware {\n  return ({ get }, next) => {\n    let session = get(Session)\n    let username = session.get('username')\n\n    if (!username) {\n      return redirect(routes.login.index.href())\n    }\n\n    return next()\n  }\n}\n\nexport let router = createRouter({\n  middleware: [logger(), formData(), session(sessionCookie, sessionStorage)],\n})\n\nrouter.map(routes.home, ({ get }) => {\n  let session = get(Session)\n  let posts = data.getPosts()\n  let username = session.get('username') as string | undefined\n\n  return createHtmlResponse(html`\n    <html>\n      <head>\n        <title>Simple Blog - fetch-router Demo</title>\n        <meta charset=\"utf-8\" />\n        <link rel=\"icon\" href=\"/favicon.ico\" />\n      </head>\n      <body>\n        <nav>\n          <h1>Simple Blog</h1>\n          <div>\n            ${username\n              ? html`\n                  <span>Hello, ${username}!</span>\n                  <form\n                    method=\"POST\"\n                    action=\"${routes.logout.href()}\"\n                    style=\"display: inline; margin-left: 10px;\"\n                  >\n                    <button type=\"submit\">Logout</button>\n                  </form>\n                  <a href=\"${routes.posts.new.href()}\" style=\"margin-left: 10px;\">New Post</a>\n                `\n              : html`<a href=\"${routes.login.index.href()}\">Login</a>`}\n          </div>\n        </nav>\n        <main>\n          ${posts.length === 0 ? html`<p>No posts yet.</p>` : null}\n          ${posts.map(\n            (post) => html`\n              <article>\n                <h2><a href=\"${routes.posts.show.href({ id: post.id })}\">${post.title}</a></h2>\n                <p>${post.content.substring(0, 150)}${post.content.length > 150 ? '...' : ''}</p>\n                <div>By ${post.author} on ${post.createdAt.toLocaleDateString()}</div>\n              </article>\n            `,\n          )}\n        </main>\n      </body>\n    </html>\n  `)\n})\n\nrouter.map(routes.login, {\n  actions: {\n    index({ get }) {\n      let session = get(Session)\n      let username = session.get('username') as string | undefined\n      if (username) {\n        return redirect(routes.home.href())\n      }\n\n      return createHtmlResponse(html`\n        <html>\n          <head>\n            <title>Login - Simple Blog</title>\n            <meta charset=\"utf-8\" />\n            <link rel=\"icon\" href=\"/favicon.ico\" />\n          </head>\n          <body>\n            <h1>Login</h1>\n            <p>Enter any username to login (no password required for demo)</p>\n            <form method=\"POST\" action=\"${routes.login.action.href()}\">\n              <div style=\"display: flex; flex-direction: column; gap: 10px; width: 150px;\">\n                <label for=\"username\">Username:</label>\n                <input type=\"text\" id=\"username\" name=\"username\" required />\n                <label for=\"password\">Password:</label>\n                <input type=\"password\" id=\"password\" name=\"password\" required />\n              </div>\n              <br />\n              <button type=\"submit\">Login</button>\n            </form>\n            <p><a href=\"${routes.home.href()}\">← Back to Home</a></p>\n          </body>\n        </html>\n      `)\n    },\n    async action({ get }) {\n      let session = get(Session)\n      let formData = get(FormData)\n      let { username } = s.parse(loginSchema, formData)\n\n      if (username) {\n        session.set('username', username)\n        return redirect(routes.home.href())\n      }\n\n      return redirect(routes.login.index.href())\n    },\n  },\n})\n\nrouter.post(routes.logout, ({ get }) => {\n  let session = get(Session)\n  session.destroy()\n  return redirect(routes.home.href())\n})\n\nrouter.map(routes.posts, {\n  actions: {\n    new: {\n      middleware: [requireAuth()],\n      action() {\n        return createHtmlResponse(html`\n          <html>\n            <head>\n              <title>New Post - Simple Blog</title>\n              <meta charset=\"utf-8\" />\n              <link rel=\"icon\" href=\"/favicon.ico\" />\n            </head>\n            <body>\n              <h1>New Post</h1>\n              <form method=\"POST\" action=\"${routes.posts.create.href()}\">\n                <div>\n                  <label for=\"title\">Title:</label>\n                  <input type=\"text\" id=\"title\" name=\"title\" required />\n                </div>\n                <div>\n                  <label for=\"content\">Content:</label>\n                  <textarea id=\"content\" name=\"content\" required></textarea>\n                </div>\n                <button type=\"submit\">Create Post</button>\n              </form>\n              <p><a href=\"${routes.home.href()}\">← Back to Home</a></p>\n            </body>\n          </html>\n        `)\n      },\n    },\n    async create({ get }) {\n      let session = get(Session)\n      let formData = get(FormData)\n      let username = session.get('username') as string\n      if (!username) {\n        return redirect(routes.login.index.href())\n      }\n\n      let { content, title } = s.parse(postSchema, formData)\n\n      if (!title || !content) {\n        return redirect(routes.posts.new.href())\n      }\n\n      let post = data.createPost(title, content, username)\n      return redirect(routes.posts.show.href({ id: post.id }))\n    },\n    show({ params }) {\n      let post = data.getPost(params.id)\n      if (!post) {\n        return new Response('Post not found', { status: 404 })\n      }\n\n      return createHtmlResponse(html`\n        <html>\n          <head>\n            <title>${post.title} - Simple Blog</title>\n            <meta charset=\"utf-8\" />\n            <link rel=\"icon\" href=\"/favicon.ico\" />\n          </head>\n          <body>\n            <h1>${post.title}</h1>\n            <div>By ${post.author} on ${post.createdAt.toLocaleDateString()}</div>\n            <div>${post.content.replace(/\\n/g, '<br>')}</div>\n            <p><a href=\"${routes.home.href()}\">← Back to Home</a></p>\n          </body>\n        </html>\n      `)\n    },\n  },\n})\n\nconst __dirname = new URL('.', import.meta.url).pathname\nconst publicDir = path.resolve(__dirname, '../public')\n\n// Serve static files from the public directory\nrouter.get('/*', async ({ request }) => {\n  let file = Bun.file(path.join(publicDir, new URL(request.url).pathname))\n\n  if (await file.exists()) {\n    return new Response(file)\n  }\n\n  return new Response('Not Found', { status: 404 })\n})\n"
  },
  {
    "path": "packages/fetch-router/demos/bun/app/routes.ts",
    "content": "import { route, form, resources } from '@remix-run/fetch-router/routes'\n\nexport let routes = route({\n  home: '/',\n  login: form('/login'),\n  logout: { method: 'POST', pattern: '/logout' },\n  posts: resources('posts', { only: ['new', 'create', 'show'] }),\n})\n"
  },
  {
    "path": "packages/fetch-router/demos/bun/index.ts",
    "content": "import { router } from './app/router.ts'\n\nconst PORT = 44100\n\nBun.serve({\n  port: PORT,\n  async fetch(request) {\n    try {\n      return await router.fetch(request)\n    } catch (error) {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  },\n})\n\nconsole.log(`Server running at http://localhost:${PORT}`)\n"
  },
  {
    "path": "packages/fetch-router/demos/bun/package.json",
    "content": "{\n  \"name\": \"fetch-router-bun-demo\",\n  \"module\": \"index.ts\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@remix-run/cookie\": \"workspace:*\",\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/form-data-middleware\": \"workspace:*\",\n    \"@remix-run/html-template\": \"workspace:*\",\n    \"@remix-run/logger-middleware\": \"workspace:*\",\n    \"@remix-run/response\": \"workspace:*\",\n    \"@remix-run/session\": \"workspace:*\",\n    \"@remix-run/session-middleware\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"^1.1.8\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"dev\": \"bun --watch index.ts\",\n    \"start\": \"bun index.ts\",\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/bun/tsconfig.json",
    "content": "{\n  \"include\": [\"index.ts\", \"../../global.d.ts\"],\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/.gitignore",
    "content": ".dev.vars*\n!.dev.vars.example\n.env*\n!.env.example\n.wrangler/"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/README.md",
    "content": "# fetch-router Cloudflare Workers Example\n\nThis example is a [Cloudflare Workers](https://workers.cloudflare.com/) application that handles routing using `@remix-run/fetch-router` with data stored in a [Cloudflare D1](https://developers.cloudflare.com/d1/) database.\n\n## Setup\n\n1. Install dependencies:\n\n```sh\npnpm install\n```\n\n2. Apply database migrations:\n\n```sh\npnpm run db:migrate\n```\n\n3. Start the development server:\n\n```sh\npnpm run dev\n```\n\nThe application will be available at `http://localhost:44100`.\n\n## Deployment\n\n1. Create the D1 database (if it doesn't exist):\n\n```sh\nnpx wrangler d1 create fetch-router-blog\n```\n\n2. Update `wrangler.jsonc` with the database ID from the previous step (replace `\"local\"` in `database_id`).\n\n3. Apply migrations to the remote database:\n\n```sh\npnpm run db:migrate --remote\n```\n\n4. Deploy the worker:\n\n```sh\npnpm run deploy\n```\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/app/data.ts",
    "content": "export interface Post {\n  id: string\n  title: string\n  content: string\n  author: string\n  createdAt: Date\n}\n\ninterface PostRow {\n  id: number\n  title: string\n  content: string\n  author: string\n  created_at: number\n}\n\nfunction rowToPost(row: PostRow): Post {\n  return {\n    id: String(row.id),\n    title: row.title,\n    content: row.content,\n    author: row.author,\n    createdAt: new Date(row.created_at * 1000),\n  }\n}\n\nexport async function getPosts(db: D1Database): Promise<Post[]> {\n  let result = await db.prepare('SELECT * FROM posts ORDER BY created_at DESC').all<PostRow>()\n  return result.results.map(rowToPost)\n}\n\nexport async function getPost(db: D1Database, id: string): Promise<Post | null> {\n  let result = await db.prepare('SELECT * FROM posts WHERE id = ?').bind(id).first<PostRow>()\n  return result ? rowToPost(result) : null\n}\n\nexport async function createPost(\n  db: D1Database,\n  title: string,\n  content: string,\n  author: string,\n): Promise<Post> {\n  let createdAt = Math.floor(Date.now() / 1000)\n\n  let result = await db\n    .prepare('INSERT INTO posts (title, content, author, created_at) VALUES (?, ?, ?, ?)')\n    .bind(title, content, author, createdAt)\n    .run()\n\n  let id = result.meta.last_row_id\n\n  return {\n    id: String(id),\n    title,\n    content,\n    author,\n    createdAt: new Date(createdAt * 1000),\n  }\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/app/router.ts",
    "content": "import { createRouter } from '@remix-run/fetch-router'\nimport { createCookie } from '@remix-run/cookie'\nimport * as s from '@remix-run/data-schema'\nimport * as f from '@remix-run/data-schema/form-data'\nimport { Session } from '@remix-run/session'\nimport { createCookieSessionStorage } from '@remix-run/session/cookie-storage'\nimport { formData } from '@remix-run/form-data-middleware'\nimport { logger } from '@remix-run/logger-middleware'\nimport { session } from '@remix-run/session-middleware'\nimport { html } from '@remix-run/html-template'\nimport { createHtmlResponse } from '@remix-run/response/html'\nimport { createRedirectResponse as redirect } from '@remix-run/response/redirect'\nimport type { Middleware } from '@remix-run/fetch-router'\nimport { env } from 'cloudflare:workers'\n\nimport { routes } from './routes.ts'\nimport * as data from './data.ts'\n\nconst textField = f.field(s.defaulted(s.string(), ''))\nconst loginSchema = f.object({\n  username: textField,\n})\nconst postSchema = f.object({\n  title: textField,\n  content: textField,\n})\n\nlet sessionCookie = createCookie('__sess', {\n  secrets: ['s3cr3t'],\n})\nlet sessionStorage = createCookieSessionStorage()\n\nfunction requireAuth(): Middleware {\n  return ({ get }, next) => {\n    let session = get(Session)\n    let username = session.get('username')\n\n    if (!username) {\n      return redirect(routes.login.index.href())\n    }\n\n    return next()\n  }\n}\n\nexport let router = createRouter({\n  middleware: [logger(), formData(), session(sessionCookie, sessionStorage)],\n})\n\nrouter.map(routes.home, async ({ get }) => {\n  let session = get(Session)\n  let posts = await data.getPosts(env.DB)\n  let username = session.get('username') as string | undefined\n\n  return createHtmlResponse(html`\n    <html>\n      <head>\n        <title>Simple Blog - fetch-router Demo</title>\n        <meta charset=\"utf-8\" />\n      </head>\n      <body>\n        <nav>\n          <h1>Simple Blog</h1>\n          <div>\n            ${username\n              ? html`\n                  <span>Hello, ${username}!</span>\n                  <form\n                    method=\"POST\"\n                    action=\"${routes.logout.href()}\"\n                    style=\"display: inline; margin-left: 10px;\"\n                  >\n                    <button type=\"submit\">Logout</button>\n                  </form>\n                  <a href=\"${routes.posts.new.href()}\" style=\"margin-left: 10px;\">New Post</a>\n                `\n              : html`<a href=\"${routes.login.index.href()}\">Login</a>`}\n          </div>\n        </nav>\n        <main>\n          ${posts.length === 0 ? html`<p>No posts yet.</p>` : null}\n          ${posts.map(\n            (post) => html`\n              <article>\n                <h2><a href=\"${routes.posts.show.href({ id: post.id })}\">${post.title}</a></h2>\n                <p>${post.content.substring(0, 150)}${post.content.length > 150 ? '...' : ''}</p>\n                <div>By ${post.author} on ${post.createdAt.toLocaleDateString()}</div>\n              </article>\n            `,\n          )}\n        </main>\n      </body>\n    </html>\n  `)\n})\n\nrouter.map(routes.login, {\n  actions: {\n    index({ get }) {\n      let session = get(Session)\n      let username = session.get('username') as string | undefined\n      if (username) {\n        return redirect(routes.home.href())\n      }\n\n      return createHtmlResponse(html`\n        <html>\n          <head>\n            <title>Login - Simple Blog</title>\n            <meta charset=\"utf-8\" />\n          </head>\n          <body>\n            <h1>Login</h1>\n            <p>Enter any username to login (no password required for demo)</p>\n            <form method=\"POST\" action=\"${routes.login.action.href()}\">\n              <div style=\"display: flex; flex-direction: column; gap: 10px; width: 150px;\">\n                <label for=\"username\">Username:</label>\n                <input type=\"text\" id=\"username\" name=\"username\" required />\n                <label for=\"password\">Password:</label>\n                <input type=\"password\" id=\"password\" name=\"password\" required />\n              </div>\n              <br />\n              <button type=\"submit\">Login</button>\n            </form>\n            <p><a href=\"${routes.home.href()}\">← Back to Home</a></p>\n          </body>\n        </html>\n      `)\n    },\n    async action({ get }) {\n      let session = get(Session)\n      let formData = get(FormData)\n      let { username } = s.parse(loginSchema, formData)\n\n      if (username) {\n        session.set('username', username)\n        return redirect(routes.home.href())\n      }\n\n      return redirect(routes.login.index.href())\n    },\n  },\n})\n\nrouter.post(routes.logout, ({ get }) => {\n  let session = get(Session)\n  session.destroy()\n  return redirect(routes.home.href())\n})\n\nrouter.map(routes.posts, {\n  actions: {\n    new: {\n      middleware: [requireAuth()],\n      action() {\n        return createHtmlResponse(`\n          <html>\n            <head>\n              <title>New Post - Simple Blog</title>\n              <meta charset=\"utf-8\" />\n            </head>\n            <body>\n              <h1>New Post</h1>\n              <form method=\"POST\" action=\"${routes.posts.create.href()}\">\n                <div>\n                  <label for=\"title\">Title:</label>\n                  <input type=\"text\" id=\"title\" name=\"title\" required />\n                </div>\n                <div>\n                  <label for=\"content\">Content:</label>\n                  <textarea id=\"content\" name=\"content\" required></textarea>\n                </div>\n                <button type=\"submit\">Create Post</button>\n              </form>\n              <p><a href=\"${routes.home.href()}\">← Back to Home</a></p>\n            </body>\n          </html>\n        `)\n      },\n    },\n    async create({ get }) {\n      let session = get(Session)\n      let formData = get(FormData)\n      let username = session.get('username') as string\n      if (!username) {\n        return redirect(routes.login.index.href())\n      }\n\n      let { content, title } = s.parse(postSchema, formData)\n\n      if (!title || !content) {\n        return redirect(routes.posts.new.href())\n      }\n\n      let post = await data.createPost(env.DB, title, content, username)\n      return redirect(routes.posts.show.href({ id: post.id }))\n    },\n    async show({ params }) {\n      let post = await data.getPost(env.DB, params.id)\n      if (!post) {\n        return new Response('Post not found', { status: 404 })\n      }\n\n      return createHtmlResponse(html`\n        <html>\n          <head>\n            <title>${post.title} - Simple Blog</title>\n            <meta charset=\"utf-8\" />\n          </head>\n          <body>\n            <h1>${post.title}</h1>\n            <div>By ${post.author} on ${post.createdAt.toLocaleDateString()}</div>\n            <div>${post.content.replace(/\\n/g, '<br>')}</div>\n            <p><a href=\"${routes.home.href()}\">← Back to Home</a></p>\n          </body>\n        </html>\n      `)\n    },\n  },\n})\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/app/routes.ts",
    "content": "import { route, form, resources } from '@remix-run/fetch-router/routes'\n\nexport let routes = route({\n  home: '/',\n  login: form('/login'),\n  logout: { method: 'POST', pattern: '/logout' },\n  posts: resources('posts', { only: ['new', 'create', 'show'] }),\n})\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/migrations/0001_initial.sql",
    "content": "CREATE TABLE IF NOT EXISTS posts (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  title TEXT NOT NULL,\n  content TEXT NOT NULL,\n  author TEXT NOT NULL,\n  created_at INTEGER NOT NULL\n);\n\n-- Insert initial demo data\nINSERT INTO posts (title, content, author, created_at) VALUES\n  ('Welcome to the Blog', 'This is a simple blog demo built with fetch-router on Cloudflare Workers.', 'Admin', strftime('%s', '2025-01-01')),\n  ('Getting Started with fetch-router', 'fetch-router is a minimal, composable router built on the web Fetch API.', 'Admin', strftime('%s', '2025-01-02'));\n\n\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/package.json",
    "content": "{\n  \"name\": \"fetch-router-cf-workers-demo\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@remix-run/cookie\": \"workspace:*\",\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/form-data-middleware\": \"workspace:*\",\n    \"@remix-run/html-template\": \"workspace:*\",\n    \"@remix-run/logger-middleware\": \"workspace:*\",\n    \"@remix-run/response\": \"workspace:*\",\n    \"@remix-run/session\": \"workspace:*\",\n    \"@remix-run/session-middleware\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"wrangler\": \"^4.50.0\"\n  },\n  \"scripts\": {\n    \"deploy\": \"wrangler deploy\",\n    \"dev\": \"wrangler dev --port 44100\",\n    \"typecheck\": \"tsgo --noEmit\",\n    \"typegen\": \"wrangler types\",\n    \"db:migrate\": \"wrangler d1 migrations apply fetch-router-blog\"\n  }\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"target\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"allowJs\": true,\n    \"checkJs\": false,\n    \"noEmit\": true,\n    \"isolatedModules\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"./worker-configuration.d.ts\", \"node\"]\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/worker-configuration.d.ts",
    "content": "/* eslint-disable */\n// Generated by Wrangler by running `wrangler types` (hash: e02522ff6c3f8f5e4d18e0c429314dbd)\n// Runtime types generated with workerd@1.20251118.0 2025-11-18 \ndeclare namespace Cloudflare {\n\tinterface GlobalProps {\n\t\tmainModule: typeof import(\"./worker\");\n\t}\n\tinterface Env {\n\t\tDB: D1Database;\n\t}\n}\ninterface Env extends Cloudflare.Env {}\n\n// Begin runtime types\n/*! *****************************************************************************\nCopyright (c) Cloudflare. All rights reserved.\nCopyright (c) Microsoft Corporation. All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use\nthis file except in compliance with the License. You may obtain a copy of the\nLicense at http://www.apache.org/licenses/LICENSE-2.0\nTHIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\nKIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED\nWARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,\nMERCHANTABLITY OR NON-INFRINGEMENT.\nSee the Apache Version 2.0 License for specific language governing permissions\nand limitations under the License.\n***************************************************************************** */\n/* eslint-disable */\n// noinspection JSUnusedGlobalSymbols\ndeclare var onmessage: never;\n/**\n * The **`DOMException`** interface represents an abnormal event (called an **exception**) that occurs as a result of calling a method or accessing a property of a web API.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)\n */\ndeclare class DOMException extends Error {\n    constructor(message?: string, name?: string);\n    /**\n     * The **`message`** read-only property of the a message or description associated with the given error name.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message)\n     */\n    readonly message: string;\n    /**\n     * The **`name`** read-only property of the one of the strings associated with an error name.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name)\n     */\n    readonly name: string;\n    /**\n     * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or `0` if none match.\n     * @deprecated\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)\n     */\n    readonly code: number;\n    static readonly INDEX_SIZE_ERR: number;\n    static readonly DOMSTRING_SIZE_ERR: number;\n    static readonly HIERARCHY_REQUEST_ERR: number;\n    static readonly WRONG_DOCUMENT_ERR: number;\n    static readonly INVALID_CHARACTER_ERR: number;\n    static readonly NO_DATA_ALLOWED_ERR: number;\n    static readonly NO_MODIFICATION_ALLOWED_ERR: number;\n    static readonly NOT_FOUND_ERR: number;\n    static readonly NOT_SUPPORTED_ERR: number;\n    static readonly INUSE_ATTRIBUTE_ERR: number;\n    static readonly INVALID_STATE_ERR: number;\n    static readonly SYNTAX_ERR: number;\n    static readonly INVALID_MODIFICATION_ERR: number;\n    static readonly NAMESPACE_ERR: number;\n    static readonly INVALID_ACCESS_ERR: number;\n    static readonly VALIDATION_ERR: number;\n    static readonly TYPE_MISMATCH_ERR: number;\n    static readonly SECURITY_ERR: number;\n    static readonly NETWORK_ERR: number;\n    static readonly ABORT_ERR: number;\n    static readonly URL_MISMATCH_ERR: number;\n    static readonly QUOTA_EXCEEDED_ERR: number;\n    static readonly TIMEOUT_ERR: number;\n    static readonly INVALID_NODE_TYPE_ERR: number;\n    static readonly DATA_CLONE_ERR: number;\n    get stack(): any;\n    set stack(value: any);\n}\ntype WorkerGlobalScopeEventMap = {\n    fetch: FetchEvent;\n    scheduled: ScheduledEvent;\n    queue: QueueEvent;\n    unhandledrejection: PromiseRejectionEvent;\n    rejectionhandled: PromiseRejectionEvent;\n};\ndeclare abstract class WorkerGlobalScope extends EventTarget<WorkerGlobalScopeEventMap> {\n    EventTarget: typeof EventTarget;\n}\n/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). *\n * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox).\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console)\n */\ninterface Console {\n    \"assert\"(condition?: boolean, ...data: any[]): void;\n    /**\n     * The **`console.clear()`** static method clears the console if possible.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static)\n     */\n    clear(): void;\n    /**\n     * The **`console.count()`** static method logs the number of times that this particular call to `count()` has been called.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static)\n     */\n    count(label?: string): void;\n    /**\n     * The **`console.countReset()`** static method resets counter used with console/count_static.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static)\n     */\n    countReset(label?: string): void;\n    /**\n     * The **`console.debug()`** static method outputs a message to the console at the 'debug' log level.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static)\n     */\n    debug(...data: any[]): void;\n    /**\n     * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)\n     */\n    dir(item?: any, options?: any): void;\n    /**\n     * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static)\n     */\n    dirxml(...data: any[]): void;\n    /**\n     * The **`console.error()`** static method outputs a message to the console at the 'error' log level.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static)\n     */\n    error(...data: any[]): void;\n    /**\n     * The **`console.group()`** static method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console/groupEnd_static is called.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static)\n     */\n    group(...data: any[]): void;\n    /**\n     * The **`console.groupCollapsed()`** static method creates a new inline group in the console.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static)\n     */\n    groupCollapsed(...data: any[]): void;\n    /**\n     * The **`console.groupEnd()`** static method exits the current inline group in the console.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static)\n     */\n    groupEnd(): void;\n    /**\n     * The **`console.info()`** static method outputs a message to the console at the 'info' log level.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static)\n     */\n    info(...data: any[]): void;\n    /**\n     * The **`console.log()`** static method outputs a message to the console.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)\n     */\n    log(...data: any[]): void;\n    /**\n     * The **`console.table()`** static method displays tabular data as a table.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static)\n     */\n    table(tabularData?: any, properties?: string[]): void;\n    /**\n     * The **`console.time()`** static method starts a timer you can use to track how long an operation takes.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static)\n     */\n    time(label?: string): void;\n    /**\n     * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console/time_static.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static)\n     */\n    timeEnd(label?: string): void;\n    /**\n     * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console/time_static.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static)\n     */\n    timeLog(label?: string, ...data: any[]): void;\n    timeStamp(label?: string): void;\n    /**\n     * The **`console.trace()`** static method outputs a stack trace to the console.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static)\n     */\n    trace(...data: any[]): void;\n    /**\n     * The **`console.warn()`** static method outputs a warning message to the console at the 'warning' log level.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static)\n     */\n    warn(...data: any[]): void;\n}\ndeclare const console: Console;\ntype BufferSource = ArrayBufferView | ArrayBuffer;\ntype TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;\ndeclare namespace WebAssembly {\n    class CompileError extends Error {\n        constructor(message?: string);\n    }\n    class RuntimeError extends Error {\n        constructor(message?: string);\n    }\n    type ValueType = \"anyfunc\" | \"externref\" | \"f32\" | \"f64\" | \"i32\" | \"i64\" | \"v128\";\n    interface GlobalDescriptor {\n        value: ValueType;\n        mutable?: boolean;\n    }\n    class Global {\n        constructor(descriptor: GlobalDescriptor, value?: any);\n        value: any;\n        valueOf(): any;\n    }\n    type ImportValue = ExportValue | number;\n    type ModuleImports = Record<string, ImportValue>;\n    type Imports = Record<string, ModuleImports>;\n    type ExportValue = Function | Global | Memory | Table;\n    type Exports = Record<string, ExportValue>;\n    class Instance {\n        constructor(module: Module, imports?: Imports);\n        readonly exports: Exports;\n    }\n    interface MemoryDescriptor {\n        initial: number;\n        maximum?: number;\n        shared?: boolean;\n    }\n    class Memory {\n        constructor(descriptor: MemoryDescriptor);\n        readonly buffer: ArrayBuffer;\n        grow(delta: number): number;\n    }\n    type ImportExportKind = \"function\" | \"global\" | \"memory\" | \"table\";\n    interface ModuleExportDescriptor {\n        kind: ImportExportKind;\n        name: string;\n    }\n    interface ModuleImportDescriptor {\n        kind: ImportExportKind;\n        module: string;\n        name: string;\n    }\n    abstract class Module {\n        static customSections(module: Module, sectionName: string): ArrayBuffer[];\n        static exports(module: Module): ModuleExportDescriptor[];\n        static imports(module: Module): ModuleImportDescriptor[];\n    }\n    type TableKind = \"anyfunc\" | \"externref\";\n    interface TableDescriptor {\n        element: TableKind;\n        initial: number;\n        maximum?: number;\n    }\n    class Table {\n        constructor(descriptor: TableDescriptor, value?: any);\n        readonly length: number;\n        get(index: number): any;\n        grow(delta: number, value?: any): number;\n        set(index: number, value?: any): void;\n    }\n    function instantiate(module: Module, imports?: Imports): Promise<Instance>;\n    function validate(bytes: BufferSource): boolean;\n}\n/**\n * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker.\n * Available only in secure contexts.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope)\n */\ninterface ServiceWorkerGlobalScope extends WorkerGlobalScope {\n    DOMException: typeof DOMException;\n    WorkerGlobalScope: typeof WorkerGlobalScope;\n    btoa(data: string): string;\n    atob(data: string): string;\n    setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;\n    setTimeout<Args extends any[]>(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;\n    clearTimeout(timeoutId: number | null): void;\n    setInterval(callback: (...args: any[]) => void, msDelay?: number): number;\n    setInterval<Args extends any[]>(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;\n    clearInterval(timeoutId: number | null): void;\n    queueMicrotask(task: Function): void;\n    structuredClone<T>(value: T, options?: StructuredSerializeOptions): T;\n    reportError(error: any): void;\n    fetch(input: RequestInfo | URL, init?: RequestInit<RequestInitCfProperties>): Promise<Response>;\n    self: ServiceWorkerGlobalScope;\n    crypto: Crypto;\n    caches: CacheStorage;\n    scheduler: Scheduler;\n    performance: Performance;\n    Cloudflare: Cloudflare;\n    readonly origin: string;\n    Event: typeof Event;\n    ExtendableEvent: typeof ExtendableEvent;\n    CustomEvent: typeof CustomEvent;\n    PromiseRejectionEvent: typeof PromiseRejectionEvent;\n    FetchEvent: typeof FetchEvent;\n    TailEvent: typeof TailEvent;\n    TraceEvent: typeof TailEvent;\n    ScheduledEvent: typeof ScheduledEvent;\n    MessageEvent: typeof MessageEvent;\n    CloseEvent: typeof CloseEvent;\n    ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader;\n    ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader;\n    ReadableStream: typeof ReadableStream;\n    WritableStream: typeof WritableStream;\n    WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter;\n    TransformStream: typeof TransformStream;\n    ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;\n    CountQueuingStrategy: typeof CountQueuingStrategy;\n    ErrorEvent: typeof ErrorEvent;\n    MessageChannel: typeof MessageChannel;\n    MessagePort: typeof MessagePort;\n    EventSource: typeof EventSource;\n    ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;\n    ReadableStreamDefaultController: typeof ReadableStreamDefaultController;\n    ReadableByteStreamController: typeof ReadableByteStreamController;\n    WritableStreamDefaultController: typeof WritableStreamDefaultController;\n    TransformStreamDefaultController: typeof TransformStreamDefaultController;\n    CompressionStream: typeof CompressionStream;\n    DecompressionStream: typeof DecompressionStream;\n    TextEncoderStream: typeof TextEncoderStream;\n    TextDecoderStream: typeof TextDecoderStream;\n    Headers: typeof Headers;\n    Body: typeof Body;\n    Request: typeof Request;\n    Response: typeof Response;\n    WebSocket: typeof WebSocket;\n    WebSocketPair: typeof WebSocketPair;\n    WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair;\n    AbortController: typeof AbortController;\n    AbortSignal: typeof AbortSignal;\n    TextDecoder: typeof TextDecoder;\n    TextEncoder: typeof TextEncoder;\n    navigator: Navigator;\n    Navigator: typeof Navigator;\n    URL: typeof URL;\n    URLSearchParams: typeof URLSearchParams;\n    URLPattern: typeof URLPattern;\n    Blob: typeof Blob;\n    File: typeof File;\n    FormData: typeof FormData;\n    Crypto: typeof Crypto;\n    SubtleCrypto: typeof SubtleCrypto;\n    CryptoKey: typeof CryptoKey;\n    CacheStorage: typeof CacheStorage;\n    Cache: typeof Cache;\n    FixedLengthStream: typeof FixedLengthStream;\n    IdentityTransformStream: typeof IdentityTransformStream;\n    HTMLRewriter: typeof HTMLRewriter;\n}\ndeclare function addEventListener<Type extends keyof WorkerGlobalScopeEventMap>(type: Type, handler: EventListenerOrEventListenerObject<WorkerGlobalScopeEventMap[Type]>, options?: EventTargetAddEventListenerOptions | boolean): void;\ndeclare function removeEventListener<Type extends keyof WorkerGlobalScopeEventMap>(type: Type, handler: EventListenerOrEventListenerObject<WorkerGlobalScopeEventMap[Type]>, options?: EventTargetEventListenerOptions | boolean): void;\n/**\n * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)\n */\ndeclare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */\ndeclare function btoa(data: string): string;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */\ndeclare function atob(data: string): string;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */\ndeclare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */\ndeclare function setTimeout<Args extends any[]>(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */\ndeclare function clearTimeout(timeoutId: number | null): void;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */\ndeclare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */\ndeclare function setInterval<Args extends any[]>(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */\ndeclare function clearInterval(timeoutId: number | null): void;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */\ndeclare function queueMicrotask(task: Function): void;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */\ndeclare function structuredClone<T>(value: T, options?: StructuredSerializeOptions): T;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */\ndeclare function reportError(error: any): void;\n/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */\ndeclare function fetch(input: RequestInfo | URL, init?: RequestInit<RequestInitCfProperties>): Promise<Response>;\ndeclare const self: ServiceWorkerGlobalScope;\n/**\n* The Web Crypto API provides a set of low-level functions for common cryptographic tasks.\n* The Workers runtime implements the full surface of this API, but with some differences in\n* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)\n* compared to those implemented in most browsers.\n*\n* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)\n*/\ndeclare const crypto: Crypto;\n/**\n* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.\n*\n* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)\n*/\ndeclare const caches: CacheStorage;\ndeclare const scheduler: Scheduler;\n/**\n* The Workers runtime supports a subset of the Performance API, used to measure timing and performance,\n* as well as timing of subrequests and other operations.\n*\n* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)\n*/\ndeclare const performance: Performance;\ndeclare const Cloudflare: Cloudflare;\ndeclare const origin: string;\ndeclare const navigator: Navigator;\ninterface TestController {\n}\ninterface ExecutionContext<Props = unknown> {\n    waitUntil(promise: Promise<any>): void;\n    passThroughOnException(): void;\n    readonly exports: Cloudflare.Exports;\n    readonly props: Props;\n}\ntype ExportedHandlerFetchHandler<Env = unknown, CfHostMetadata = unknown> = (request: Request<CfHostMetadata, IncomingRequestCfProperties<CfHostMetadata>>, env: Env, ctx: ExecutionContext) => Response | Promise<Response>;\ntype ExportedHandlerTailHandler<Env = unknown> = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise<void>;\ntype ExportedHandlerTraceHandler<Env = unknown> = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise<void>;\ntype ExportedHandlerTailStreamHandler<Env = unknown> = (event: TailStream.TailEvent<TailStream.Onset>, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise<TailStream.TailEventHandlerType>;\ntype ExportedHandlerScheduledHandler<Env = unknown> = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise<void>;\ntype ExportedHandlerQueueHandler<Env = unknown, Message = unknown> = (batch: MessageBatch<Message>, env: Env, ctx: ExecutionContext) => void | Promise<void>;\ntype ExportedHandlerTestHandler<Env = unknown> = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise<void>;\ninterface ExportedHandler<Env = unknown, QueueHandlerMessage = unknown, CfHostMetadata = unknown> {\n    fetch?: ExportedHandlerFetchHandler<Env, CfHostMetadata>;\n    tail?: ExportedHandlerTailHandler<Env>;\n    trace?: ExportedHandlerTraceHandler<Env>;\n    tailStream?: ExportedHandlerTailStreamHandler<Env>;\n    scheduled?: ExportedHandlerScheduledHandler<Env>;\n    test?: ExportedHandlerTestHandler<Env>;\n    email?: EmailExportedHandler<Env>;\n    queue?: ExportedHandlerQueueHandler<Env, QueueHandlerMessage>;\n}\ninterface StructuredSerializeOptions {\n    transfer?: any[];\n}\ndeclare abstract class Navigator {\n    sendBeacon(url: string, body?: BodyInit): boolean;\n    readonly userAgent: string;\n    readonly hardwareConcurrency: number;\n    readonly language: string;\n    readonly languages: string[];\n}\ninterface AlarmInvocationInfo {\n    readonly isRetry: boolean;\n    readonly retryCount: number;\n}\ninterface Cloudflare {\n    readonly compatibilityFlags: Record<string, boolean>;\n}\ninterface DurableObject {\n    fetch(request: Request): Response | Promise<Response>;\n    alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise<void>;\n    webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise<void>;\n    webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise<void>;\n    webSocketError?(ws: WebSocket, error: unknown): void | Promise<void>;\n}\ntype DurableObjectStub<T extends Rpc.DurableObjectBranded | undefined = undefined> = Fetcher<T, \"alarm\" | \"webSocketMessage\" | \"webSocketClose\" | \"webSocketError\"> & {\n    readonly id: DurableObjectId;\n    readonly name?: string;\n};\ninterface DurableObjectId {\n    toString(): string;\n    equals(other: DurableObjectId): boolean;\n    readonly name?: string;\n}\ndeclare abstract class DurableObjectNamespace<T extends Rpc.DurableObjectBranded | undefined = undefined> {\n    newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId;\n    idFromName(name: string): DurableObjectId;\n    idFromString(id: string): DurableObjectId;\n    get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub<T>;\n    getByName(name: string, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub<T>;\n    jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace<T>;\n}\ntype DurableObjectJurisdiction = \"eu\" | \"fedramp\" | \"fedramp-high\";\ninterface DurableObjectNamespaceNewUniqueIdOptions {\n    jurisdiction?: DurableObjectJurisdiction;\n}\ntype DurableObjectLocationHint = \"wnam\" | \"enam\" | \"sam\" | \"weur\" | \"eeur\" | \"apac\" | \"oc\" | \"afr\" | \"me\";\ninterface DurableObjectNamespaceGetDurableObjectOptions {\n    locationHint?: DurableObjectLocationHint;\n}\ninterface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> {\n}\ninterface DurableObjectState<Props = unknown> {\n    waitUntil(promise: Promise<any>): void;\n    readonly exports: Cloudflare.Exports;\n    readonly props: Props;\n    readonly id: DurableObjectId;\n    readonly storage: DurableObjectStorage;\n    container?: Container;\n    blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>;\n    acceptWebSocket(ws: WebSocket, tags?: string[]): void;\n    getWebSockets(tag?: string): WebSocket[];\n    setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void;\n    getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;\n    getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null;\n    setHibernatableWebSocketEventTimeout(timeoutMs?: number): void;\n    getHibernatableWebSocketEventTimeout(): number | null;\n    getTags(ws: WebSocket): string[];\n    abort(reason?: string): void;\n}\ninterface DurableObjectTransaction {\n    get<T = unknown>(key: string, options?: DurableObjectGetOptions): Promise<T | undefined>;\n    get<T = unknown>(keys: string[], options?: DurableObjectGetOptions): Promise<Map<string, T>>;\n    list<T = unknown>(options?: DurableObjectListOptions): Promise<Map<string, T>>;\n    put<T>(key: string, value: T, options?: DurableObjectPutOptions): Promise<void>;\n    put<T>(entries: Record<string, T>, options?: DurableObjectPutOptions): Promise<void>;\n    delete(key: string, options?: DurableObjectPutOptions): Promise<boolean>;\n    delete(keys: string[], options?: DurableObjectPutOptions): Promise<number>;\n    rollback(): void;\n    getAlarm(options?: DurableObjectGetAlarmOptions): Promise<number | null>;\n    setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise<void>;\n    deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise<void>;\n}\ninterface DurableObjectStorage {\n    get<T = unknown>(key: string, options?: DurableObjectGetOptions): Promise<T | undefined>;\n    get<T = unknown>(keys: string[], options?: DurableObjectGetOptions): Promise<Map<string, T>>;\n    list<T = unknown>(options?: DurableObjectListOptions): Promise<Map<string, T>>;\n    put<T>(key: string, value: T, options?: DurableObjectPutOptions): Promise<void>;\n    put<T>(entries: Record<string, T>, options?: DurableObjectPutOptions): Promise<void>;\n    delete(key: string, options?: DurableObjectPutOptions): Promise<boolean>;\n    delete(keys: string[], options?: DurableObjectPutOptions): Promise<number>;\n    deleteAll(options?: DurableObjectPutOptions): Promise<void>;\n    transaction<T>(closure: (txn: DurableObjectTransaction) => Promise<T>): Promise<T>;\n    getAlarm(options?: DurableObjectGetAlarmOptions): Promise<number | null>;\n    setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise<void>;\n    deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise<void>;\n    sync(): Promise<void>;\n    sql: SqlStorage;\n    kv: SyncKvStorage;\n    transactionSync<T>(closure: () => T): T;\n    getCurrentBookmark(): Promise<string>;\n    getBookmarkForTime(timestamp: number | Date): Promise<string>;\n    onNextSessionRestoreBookmark(bookmark: string): Promise<string>;\n}\ninterface DurableObjectListOptions {\n    start?: string;\n    startAfter?: string;\n    end?: string;\n    prefix?: string;\n    reverse?: boolean;\n    limit?: number;\n    allowConcurrency?: boolean;\n    noCache?: boolean;\n}\ninterface DurableObjectGetOptions {\n    allowConcurrency?: boolean;\n    noCache?: boolean;\n}\ninterface DurableObjectGetAlarmOptions {\n    allowConcurrency?: boolean;\n}\ninterface DurableObjectPutOptions {\n    allowConcurrency?: boolean;\n    allowUnconfirmed?: boolean;\n    noCache?: boolean;\n}\ninterface DurableObjectSetAlarmOptions {\n    allowConcurrency?: boolean;\n    allowUnconfirmed?: boolean;\n}\ndeclare class WebSocketRequestResponsePair {\n    constructor(request: string, response: string);\n    get request(): string;\n    get response(): string;\n}\ninterface AnalyticsEngineDataset {\n    writeDataPoint(event?: AnalyticsEngineDataPoint): void;\n}\ninterface AnalyticsEngineDataPoint {\n    indexes?: ((ArrayBuffer | string) | null)[];\n    doubles?: number[];\n    blobs?: ((ArrayBuffer | string) | null)[];\n}\n/**\n * The **`Event`** interface represents an event which takes place on an `EventTarget`.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event)\n */\ndeclare class Event {\n    constructor(type: string, init?: EventInit);\n    /**\n     * The **`type`** read-only property of the Event interface returns a string containing the event's type.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/type)\n     */\n    get type(): string;\n    /**\n     * The **`eventPhase`** read-only property of the being evaluated.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/eventPhase)\n     */\n    get eventPhase(): number;\n    /**\n     * The read-only **`composed`** property of the or not the event will propagate across the shadow DOM boundary into the standard DOM.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composed)\n     */\n    get composed(): boolean;\n    /**\n     * The **`bubbles`** read-only property of the Event interface indicates whether the event bubbles up through the DOM tree or not.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/bubbles)\n     */\n    get bubbles(): boolean;\n    /**\n     * The **`cancelable`** read-only property of the Event interface indicates whether the event can be canceled, and therefore prevented as if the event never happened.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelable)\n     */\n    get cancelable(): boolean;\n    /**\n     * The **`defaultPrevented`** read-only property of the Event interface returns a boolean value indicating whether or not the call to Event.preventDefault() canceled the event.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/defaultPrevented)\n     */\n    get defaultPrevented(): boolean;\n    /**\n     * The Event property **`returnValue`** indicates whether the default action for this event has been prevented or not.\n     * @deprecated\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/returnValue)\n     */\n    get returnValue(): boolean;\n    /**\n     * The **`currentTarget`** read-only property of the Event interface identifies the element to which the event handler has been attached.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget)\n     */\n    get currentTarget(): EventTarget | undefined;\n    /**\n     * The read-only **`target`** property of the dispatched.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/target)\n     */\n    get target(): EventTarget | undefined;\n    /**\n     * The deprecated **`Event.srcElement`** is an alias for the Event.target property.\n     * @deprecated\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/srcElement)\n     */\n    get srcElement(): EventTarget | undefined;\n    /**\n     * The **`timeStamp`** read-only property of the Event interface returns the time (in milliseconds) at which the event was created.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/timeStamp)\n     */\n    get timeStamp(): number;\n    /**\n     * The **`isTrusted`** read-only property of the when the event was generated by the user agent (including via user actions and programmatic methods such as HTMLElement.focus()), and `false` when the event was dispatched via The only exception is the `click` event, which initializes the `isTrusted` property to `false` in user agents.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/isTrusted)\n     */\n    get isTrusted(): boolean;\n    /**\n     * The **`cancelBubble`** property of the Event interface is deprecated.\n     * @deprecated\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble)\n     */\n    get cancelBubble(): boolean;\n    /**\n     * The **`cancelBubble`** property of the Event interface is deprecated.\n     * @deprecated\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble)\n     */\n    set cancelBubble(value: boolean);\n    /**\n     * The **`stopImmediatePropagation()`** method of the If several listeners are attached to the same element for the same event type, they are called in the order in which they were added.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopImmediatePropagation)\n     */\n    stopImmediatePropagation(): void;\n    /**\n     * The **`preventDefault()`** method of the Event interface tells the user agent that if the event does not get explicitly handled, its default action should not be taken as it normally would be.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)\n     */\n    preventDefault(): void;\n    /**\n     * The **`stopPropagation()`** method of the Event interface prevents further propagation of the current event in the capturing and bubbling phases.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation)\n     */\n    stopPropagation(): void;\n    /**\n     * The **`composedPath()`** method of the Event interface returns the event's path which is an array of the objects on which listeners will be invoked.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composedPath)\n     */\n    composedPath(): EventTarget[];\n    static readonly NONE: number;\n    static readonly CAPTURING_PHASE: number;\n    static readonly AT_TARGET: number;\n    static readonly BUBBLING_PHASE: number;\n}\ninterface EventInit {\n    bubbles?: boolean;\n    cancelable?: boolean;\n    composed?: boolean;\n}\ntype EventListener<EventType extends Event = Event> = (event: EventType) => void;\ninterface EventListenerObject<EventType extends Event = Event> {\n    handleEvent(event: EventType): void;\n}\ntype EventListenerOrEventListenerObject<EventType extends Event = Event> = EventListener<EventType> | EventListenerObject<EventType>;\n/**\n * The **`EventTarget`** interface is implemented by objects that can receive events and may have listeners for them.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget)\n */\ndeclare class EventTarget<EventMap extends Record<string, Event> = Record<string, Event>> {\n    constructor();\n    /**\n     * The **`addEventListener()`** method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)\n     */\n    addEventListener<Type extends keyof EventMap>(type: Type, handler: EventListenerOrEventListenerObject<EventMap[Type]>, options?: EventTargetAddEventListenerOptions | boolean): void;\n    /**\n     * The **`removeEventListener()`** method of the EventTarget interface removes an event listener previously registered with EventTarget.addEventListener() from the target.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener)\n     */\n    removeEventListener<Type extends keyof EventMap>(type: Type, handler: EventListenerOrEventListenerObject<EventMap[Type]>, options?: EventTargetEventListenerOptions | boolean): void;\n    /**\n     * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)\n     */\n    dispatchEvent(event: EventMap[keyof EventMap]): boolean;\n}\ninterface EventTargetEventListenerOptions {\n    capture?: boolean;\n}\ninterface EventTargetAddEventListenerOptions {\n    capture?: boolean;\n    passive?: boolean;\n    once?: boolean;\n    signal?: AbortSignal;\n}\ninterface EventTargetHandlerObject {\n    handleEvent: (event: Event) => any | undefined;\n}\n/**\n * The **`AbortController`** interface represents a controller object that allows you to abort one or more Web requests as and when desired.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController)\n */\ndeclare class AbortController {\n    constructor();\n    /**\n     * The **`signal`** read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal)\n     */\n    get signal(): AbortSignal;\n    /**\n     * The **`abort()`** method of the AbortController interface aborts an asynchronous operation before it has completed.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort)\n     */\n    abort(reason?: any): void;\n}\n/**\n * The **`AbortSignal`** interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal)\n */\ndeclare abstract class AbortSignal extends EventTarget {\n    /**\n     * The **`AbortSignal.abort()`** static method returns an AbortSignal that is already set as aborted (and which does not trigger an AbortSignal/abort_event event).\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_static)\n     */\n    static abort(reason?: any): AbortSignal;\n    /**\n     * The **`AbortSignal.timeout()`** static method returns an AbortSignal that will automatically abort after a specified time.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static)\n     */\n    static timeout(delay: number): AbortSignal;\n    /**\n     * The **`AbortSignal.any()`** static method takes an iterable of abort signals and returns an AbortSignal.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static)\n     */\n    static any(signals: AbortSignal[]): AbortSignal;\n    /**\n     * The **`aborted`** read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (`true`) or not (`false`).\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted)\n     */\n    get aborted(): boolean;\n    /**\n     * The **`reason`** read-only property returns a JavaScript value that indicates the abort reason.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason)\n     */\n    get reason(): any;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */\n    get onabort(): any | null;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */\n    set onabort(value: any | null);\n    /**\n     * The **`throwIfAborted()`** method throws the signal's abort AbortSignal.reason if the signal has been aborted; otherwise it does nothing.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted)\n     */\n    throwIfAborted(): void;\n}\ninterface Scheduler {\n    wait(delay: number, maybeOptions?: SchedulerWaitOptions): Promise<void>;\n}\ninterface SchedulerWaitOptions {\n    signal?: AbortSignal;\n}\n/**\n * The **`ExtendableEvent`** interface extends the lifetime of the `install` and `activate` events dispatched on the global scope as part of the service worker lifecycle.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent)\n */\ndeclare abstract class ExtendableEvent extends Event {\n    /**\n     * The **`ExtendableEvent.waitUntil()`** method tells the event dispatcher that work is ongoing.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil)\n     */\n    waitUntil(promise: Promise<any>): void;\n}\n/**\n * The **`CustomEvent`** interface represents events initialized by an application for any purpose.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent)\n */\ndeclare class CustomEvent<T = any> extends Event {\n    constructor(type: string, init?: CustomEventCustomEventInit);\n    /**\n     * The read-only **`detail`** property of the CustomEvent interface returns any data passed when initializing the event.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail)\n     */\n    get detail(): T;\n}\ninterface CustomEventCustomEventInit {\n    bubbles?: boolean;\n    cancelable?: boolean;\n    composed?: boolean;\n    detail?: any;\n}\n/**\n * The **`Blob`** interface represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob)\n */\ndeclare class Blob {\n    constructor(type?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions);\n    /**\n     * The **`size`** read-only property of the Blob interface returns the size of the Blob or File in bytes.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size)\n     */\n    get size(): number;\n    /**\n     * The **`type`** read-only property of the Blob interface returns the MIME type of the file.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type)\n     */\n    get type(): string;\n    /**\n     * The **`slice()`** method of the Blob interface creates and returns a new `Blob` object which contains data from a subset of the blob on which it's called.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice)\n     */\n    slice(start?: number, end?: number, type?: string): Blob;\n    /**\n     * The **`arrayBuffer()`** method of the Blob interface returns a Promise that resolves with the contents of the blob as binary data contained in an ArrayBuffer.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer)\n     */\n    arrayBuffer(): Promise<ArrayBuffer>;\n    /**\n     * The **`bytes()`** method of the Blob interface returns a Promise that resolves with a Uint8Array containing the contents of the blob as an array of bytes.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes)\n     */\n    bytes(): Promise<Uint8Array>;\n    /**\n     * The **`text()`** method of the string containing the contents of the blob, interpreted as UTF-8.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text)\n     */\n    text(): Promise<string>;\n    /**\n     * The **`stream()`** method of the Blob interface returns a ReadableStream which upon reading returns the data contained within the `Blob`.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/stream)\n     */\n    stream(): ReadableStream;\n}\ninterface BlobOptions {\n    type?: string;\n}\n/**\n * The **`File`** interface provides information about files and allows JavaScript in a web page to access their content.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File)\n */\ndeclare class File extends Blob {\n    constructor(bits: ((ArrayBuffer | ArrayBufferView) | string | Blob)[] | undefined, name: string, options?: FileOptions);\n    /**\n     * The **`name`** read-only property of the File interface returns the name of the file represented by a File object.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name)\n     */\n    get name(): string;\n    /**\n     * The **`lastModified`** read-only property of the File interface provides the last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight).\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified)\n     */\n    get lastModified(): number;\n}\ninterface FileOptions {\n    type?: string;\n    lastModified?: number;\n}\n/**\n* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.\n*\n* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)\n*/\ndeclare abstract class CacheStorage {\n    /**\n     * The **`open()`** method of the the Cache object matching the `cacheName`.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CacheStorage/open)\n     */\n    open(cacheName: string): Promise<Cache>;\n    readonly default: Cache;\n}\n/**\n* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.\n*\n* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)\n*/\ndeclare abstract class Cache {\n    /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#delete) */\n    delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<boolean>;\n    /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#match) */\n    match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined>;\n    /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#put) */\n    put(request: RequestInfo | URL, response: Response): Promise<void>;\n}\ninterface CacheQueryOptions {\n    ignoreMethod?: boolean;\n}\n/**\n* The Web Crypto API provides a set of low-level functions for common cryptographic tasks.\n* The Workers runtime implements the full surface of this API, but with some differences in\n* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)\n* compared to those implemented in most browsers.\n*\n* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)\n*/\ndeclare abstract class Crypto {\n    /**\n     * The **`Crypto.subtle`** read-only property returns a cryptographic operations.\n     * Available only in secure contexts.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle)\n     */\n    get subtle(): SubtleCrypto;\n    /**\n     * The **`Crypto.getRandomValues()`** method lets you get cryptographically strong random values.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues)\n     */\n    getRandomValues<T extends Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | BigInt64Array | BigUint64Array>(buffer: T): T;\n    /**\n     * The **`randomUUID()`** method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator.\n     * Available only in secure contexts.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID)\n     */\n    randomUUID(): string;\n    DigestStream: typeof DigestStream;\n}\n/**\n * The **`SubtleCrypto`** interface of the Web Crypto API provides a number of low-level cryptographic functions.\n * Available only in secure contexts.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto)\n */\ndeclare abstract class SubtleCrypto {\n    /**\n     * The **`encrypt()`** method of the SubtleCrypto interface encrypts data.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt)\n     */\n    encrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, plainText: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;\n    /**\n     * The **`decrypt()`** method of the SubtleCrypto interface decrypts some encrypted data.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt)\n     */\n    decrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, cipherText: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;\n    /**\n     * The **`sign()`** method of the SubtleCrypto interface generates a digital signature.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign)\n     */\n    sign(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, data: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;\n    /**\n     * The **`verify()`** method of the SubtleCrypto interface verifies a digital signature.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify)\n     */\n    verify(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, signature: ArrayBuffer | ArrayBufferView, data: ArrayBuffer | ArrayBufferView): Promise<boolean>;\n    /**\n     * The **`digest()`** method of the SubtleCrypto interface generates a _digest_ of the given data, using the specified hash function.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest)\n     */\n    digest(algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;\n    /**\n     * The **`generateKey()`** method of the SubtleCrypto interface is used to generate a new key (for symmetric algorithms) or key pair (for public-key algorithms).\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey)\n     */\n    generateKey(algorithm: string | SubtleCryptoGenerateKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise<CryptoKey | CryptoKeyPair>;\n    /**\n     * The **`deriveKey()`** method of the SubtleCrypto interface can be used to derive a secret key from a master key.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey)\n     */\n    deriveKey(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, derivedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>;\n    /**\n     * The **`deriveBits()`** method of the key.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits)\n     */\n    deriveBits(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, length?: number | null): Promise<ArrayBuffer>;\n    /**\n     * The **`importKey()`** method of the SubtleCrypto interface imports a key: that is, it takes as input a key in an external, portable format and gives you a CryptoKey object that you can use in the Web Crypto API.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey)\n     */\n    importKey(format: string, keyData: (ArrayBuffer | ArrayBufferView) | JsonWebKey, algorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>;\n    /**\n     * The **`exportKey()`** method of the SubtleCrypto interface exports a key: that is, it takes as input a CryptoKey object and gives you the key in an external, portable format.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey)\n     */\n    exportKey(format: string, key: CryptoKey): Promise<ArrayBuffer | JsonWebKey>;\n    /**\n     * The **`wrapKey()`** method of the SubtleCrypto interface 'wraps' a key.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey)\n     */\n    wrapKey(format: string, key: CryptoKey, wrappingKey: CryptoKey, wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm): Promise<ArrayBuffer>;\n    /**\n     * The **`unwrapKey()`** method of the SubtleCrypto interface 'unwraps' a key.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey)\n     */\n    unwrapKey(format: string, wrappedKey: ArrayBuffer | ArrayBufferView, unwrappingKey: CryptoKey, unwrapAlgorithm: string | SubtleCryptoEncryptAlgorithm, unwrappedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>;\n    timingSafeEqual(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean;\n}\n/**\n * The **`CryptoKey`** interface of the Web Crypto API represents a cryptographic key obtained from one of the SubtleCrypto methods SubtleCrypto.generateKey, SubtleCrypto.deriveKey, SubtleCrypto.importKey, or SubtleCrypto.unwrapKey.\n * Available only in secure contexts.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey)\n */\ndeclare abstract class CryptoKey {\n    /**\n     * The read-only **`type`** property of the CryptoKey interface indicates which kind of key is represented by the object.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/type)\n     */\n    readonly type: string;\n    /**\n     * The read-only **`extractable`** property of the CryptoKey interface indicates whether or not the key may be extracted using `SubtleCrypto.exportKey()` or `SubtleCrypto.wrapKey()`.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable)\n     */\n    readonly extractable: boolean;\n    /**\n     * The read-only **`algorithm`** property of the CryptoKey interface returns an object describing the algorithm for which this key can be used, and any associated extra parameters.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm)\n     */\n    readonly algorithm: CryptoKeyKeyAlgorithm | CryptoKeyAesKeyAlgorithm | CryptoKeyHmacKeyAlgorithm | CryptoKeyRsaKeyAlgorithm | CryptoKeyEllipticKeyAlgorithm | CryptoKeyArbitraryKeyAlgorithm;\n    /**\n     * The read-only **`usages`** property of the CryptoKey interface indicates what can be done with the key.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages)\n     */\n    readonly usages: string[];\n}\ninterface CryptoKeyPair {\n    publicKey: CryptoKey;\n    privateKey: CryptoKey;\n}\ninterface JsonWebKey {\n    kty: string;\n    use?: string;\n    key_ops?: string[];\n    alg?: string;\n    ext?: boolean;\n    crv?: string;\n    x?: string;\n    y?: string;\n    d?: string;\n    n?: string;\n    e?: string;\n    p?: string;\n    q?: string;\n    dp?: string;\n    dq?: string;\n    qi?: string;\n    oth?: RsaOtherPrimesInfo[];\n    k?: string;\n}\ninterface RsaOtherPrimesInfo {\n    r?: string;\n    d?: string;\n    t?: string;\n}\ninterface SubtleCryptoDeriveKeyAlgorithm {\n    name: string;\n    salt?: (ArrayBuffer | ArrayBufferView);\n    iterations?: number;\n    hash?: (string | SubtleCryptoHashAlgorithm);\n    $public?: CryptoKey;\n    info?: (ArrayBuffer | ArrayBufferView);\n}\ninterface SubtleCryptoEncryptAlgorithm {\n    name: string;\n    iv?: (ArrayBuffer | ArrayBufferView);\n    additionalData?: (ArrayBuffer | ArrayBufferView);\n    tagLength?: number;\n    counter?: (ArrayBuffer | ArrayBufferView);\n    length?: number;\n    label?: (ArrayBuffer | ArrayBufferView);\n}\ninterface SubtleCryptoGenerateKeyAlgorithm {\n    name: string;\n    hash?: (string | SubtleCryptoHashAlgorithm);\n    modulusLength?: number;\n    publicExponent?: (ArrayBuffer | ArrayBufferView);\n    length?: number;\n    namedCurve?: string;\n}\ninterface SubtleCryptoHashAlgorithm {\n    name: string;\n}\ninterface SubtleCryptoImportKeyAlgorithm {\n    name: string;\n    hash?: (string | SubtleCryptoHashAlgorithm);\n    length?: number;\n    namedCurve?: string;\n    compressed?: boolean;\n}\ninterface SubtleCryptoSignAlgorithm {\n    name: string;\n    hash?: (string | SubtleCryptoHashAlgorithm);\n    dataLength?: number;\n    saltLength?: number;\n}\ninterface CryptoKeyKeyAlgorithm {\n    name: string;\n}\ninterface CryptoKeyAesKeyAlgorithm {\n    name: string;\n    length: number;\n}\ninterface CryptoKeyHmacKeyAlgorithm {\n    name: string;\n    hash: CryptoKeyKeyAlgorithm;\n    length: number;\n}\ninterface CryptoKeyRsaKeyAlgorithm {\n    name: string;\n    modulusLength: number;\n    publicExponent: ArrayBuffer | ArrayBufferView;\n    hash?: CryptoKeyKeyAlgorithm;\n}\ninterface CryptoKeyEllipticKeyAlgorithm {\n    name: string;\n    namedCurve: string;\n}\ninterface CryptoKeyArbitraryKeyAlgorithm {\n    name: string;\n    hash?: CryptoKeyKeyAlgorithm;\n    namedCurve?: string;\n    length?: number;\n}\ndeclare class DigestStream extends WritableStream<ArrayBuffer | ArrayBufferView> {\n    constructor(algorithm: string | SubtleCryptoHashAlgorithm);\n    readonly digest: Promise<ArrayBuffer>;\n    get bytesWritten(): number | bigint;\n}\n/**\n * The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as `UTF-8`, `ISO-8859-2`, `KOI8-R`, `GBK`, etc.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder)\n */\ndeclare class TextDecoder {\n    constructor(label?: string, options?: TextDecoderConstructorOptions);\n    /**\n     * The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode)\n     */\n    decode(input?: (ArrayBuffer | ArrayBufferView), options?: TextDecoderDecodeOptions): string;\n    get encoding(): string;\n    get fatal(): boolean;\n    get ignoreBOM(): boolean;\n}\n/**\n * The **`TextEncoder`** interface takes a stream of code points as input and emits a stream of UTF-8 bytes.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder)\n */\ndeclare class TextEncoder {\n    constructor();\n    /**\n     * The **`TextEncoder.encode()`** method takes a string as input, and returns a Global_Objects/Uint8Array containing the text given in parameters encoded with the specific method for that TextEncoder object.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode)\n     */\n    encode(input?: string): Uint8Array;\n    /**\n     * The **`TextEncoder.encodeInto()`** method takes a string to encode and a destination Uint8Array to put resulting UTF-8 encoded text into, and returns a dictionary object indicating the progress of the encoding.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto)\n     */\n    encodeInto(input: string, buffer: Uint8Array): TextEncoderEncodeIntoResult;\n    get encoding(): string;\n}\ninterface TextDecoderConstructorOptions {\n    fatal: boolean;\n    ignoreBOM: boolean;\n}\ninterface TextDecoderDecodeOptions {\n    stream: boolean;\n}\ninterface TextEncoderEncodeIntoResult {\n    read: number;\n    written: number;\n}\n/**\n * The **`ErrorEvent`** interface represents events providing information related to errors in scripts or in files.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent)\n */\ndeclare class ErrorEvent extends Event {\n    constructor(type: string, init?: ErrorEventErrorEventInit);\n    /**\n     * The **`filename`** read-only property of the ErrorEvent interface returns a string containing the name of the script file in which the error occurred.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/filename)\n     */\n    get filename(): string;\n    /**\n     * The **`message`** read-only property of the ErrorEvent interface returns a string containing a human-readable error message describing the problem.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/message)\n     */\n    get message(): string;\n    /**\n     * The **`lineno`** read-only property of the ErrorEvent interface returns an integer containing the line number of the script file on which the error occurred.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/lineno)\n     */\n    get lineno(): number;\n    /**\n     * The **`colno`** read-only property of the ErrorEvent interface returns an integer containing the column number of the script file on which the error occurred.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/colno)\n     */\n    get colno(): number;\n    /**\n     * The **`error`** read-only property of the ErrorEvent interface returns a JavaScript value, such as an Error or DOMException, representing the error associated with this event.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/error)\n     */\n    get error(): any;\n}\ninterface ErrorEventErrorEventInit {\n    message?: string;\n    filename?: string;\n    lineno?: number;\n    colno?: number;\n    error?: any;\n}\n/**\n * The **`MessageEvent`** interface represents a message received by a target object.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent)\n */\ndeclare class MessageEvent extends Event {\n    constructor(type: string, initializer: MessageEventInit);\n    /**\n     * The **`data`** read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data)\n     */\n    readonly data: any;\n    /**\n     * The **`origin`** read-only property of the origin of the message emitter.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin)\n     */\n    readonly origin: string | null;\n    /**\n     * The **`lastEventId`** read-only property of the unique ID for the event.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/lastEventId)\n     */\n    readonly lastEventId: string;\n    /**\n     * The **`source`** read-only property of the a WindowProxy, MessagePort, or a `MessageEventSource` (which can be a WindowProxy, message emitter.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/source)\n     */\n    readonly source: MessagePort | null;\n    /**\n     * The **`ports`** read-only property of the containing all MessagePort objects sent with the message, in order.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/ports)\n     */\n    readonly ports: MessagePort[];\n}\ninterface MessageEventInit {\n    data: ArrayBuffer | string;\n}\n/**\n * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent)\n */\ndeclare abstract class PromiseRejectionEvent extends Event {\n    /**\n     * The PromiseRejectionEvent interface's **`promise`** read-only property indicates the JavaScript rejected.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/promise)\n     */\n    readonly promise: Promise<any>;\n    /**\n     * The PromiseRejectionEvent **`reason`** read-only property is any JavaScript value or Object which provides the reason passed into Promise.reject().\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/reason)\n     */\n    readonly reason: any;\n}\n/**\n * The **`FormData`** interface provides a way to construct a set of key/value pairs representing form fields and their values, which can be sent using the Window/fetch, XMLHttpRequest.send() or navigator.sendBeacon() methods.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData)\n */\ndeclare class FormData {\n    constructor();\n    /**\n     * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append)\n     */\n    append(name: string, value: string): void;\n    /**\n     * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append)\n     */\n    append(name: string, value: Blob, filename?: string): void;\n    /**\n     * The **`delete()`** method of the FormData interface deletes a key and its value(s) from a `FormData` object.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/delete)\n     */\n    delete(name: string): void;\n    /**\n     * The **`get()`** method of the FormData interface returns the first value associated with a given key from within a `FormData` object.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/get)\n     */\n    get(name: string): (File | string) | null;\n    /**\n     * The **`getAll()`** method of the FormData interface returns all the values associated with a given key from within a `FormData` object.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/getAll)\n     */\n    getAll(name: string): (File | string)[];\n    /**\n     * The **`has()`** method of the FormData interface returns whether a `FormData` object contains a certain key.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has)\n     */\n    has(name: string): boolean;\n    /**\n     * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set)\n     */\n    set(name: string, value: string): void;\n    /**\n     * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set)\n     */\n    set(name: string, value: Blob, filename?: string): void;\n    /* Returns an array of key, value pairs for every entry in the list. */\n    entries(): IterableIterator<[\n        key: string,\n        value: File | string\n    ]>;\n    /* Returns a list of keys in the list. */\n    keys(): IterableIterator<string>;\n    /* Returns a list of values in the list. */\n    values(): IterableIterator<(File | string)>;\n    forEach<This = unknown>(callback: (this: This, value: File | string, key: string, parent: FormData) => void, thisArg?: This): void;\n    [Symbol.iterator](): IterableIterator<[\n        key: string,\n        value: File | string\n    ]>;\n}\ninterface ContentOptions {\n    html?: boolean;\n}\ndeclare class HTMLRewriter {\n    constructor();\n    on(selector: string, handlers: HTMLRewriterElementContentHandlers): HTMLRewriter;\n    onDocument(handlers: HTMLRewriterDocumentContentHandlers): HTMLRewriter;\n    transform(response: Response): Response;\n}\ninterface HTMLRewriterElementContentHandlers {\n    element?(element: Element): void | Promise<void>;\n    comments?(comment: Comment): void | Promise<void>;\n    text?(element: Text): void | Promise<void>;\n}\ninterface HTMLRewriterDocumentContentHandlers {\n    doctype?(doctype: Doctype): void | Promise<void>;\n    comments?(comment: Comment): void | Promise<void>;\n    text?(text: Text): void | Promise<void>;\n    end?(end: DocumentEnd): void | Promise<void>;\n}\ninterface Doctype {\n    readonly name: string | null;\n    readonly publicId: string | null;\n    readonly systemId: string | null;\n}\ninterface Element {\n    tagName: string;\n    readonly attributes: IterableIterator<string[]>;\n    readonly removed: boolean;\n    readonly namespaceURI: string;\n    getAttribute(name: string): string | null;\n    hasAttribute(name: string): boolean;\n    setAttribute(name: string, value: string): Element;\n    removeAttribute(name: string): Element;\n    before(content: string | ReadableStream | Response, options?: ContentOptions): Element;\n    after(content: string | ReadableStream | Response, options?: ContentOptions): Element;\n    prepend(content: string | ReadableStream | Response, options?: ContentOptions): Element;\n    append(content: string | ReadableStream | Response, options?: ContentOptions): Element;\n    replace(content: string | ReadableStream | Response, options?: ContentOptions): Element;\n    remove(): Element;\n    removeAndKeepContent(): Element;\n    setInnerContent(content: string | ReadableStream | Response, options?: ContentOptions): Element;\n    onEndTag(handler: (tag: EndTag) => void | Promise<void>): void;\n}\ninterface EndTag {\n    name: string;\n    before(content: string | ReadableStream | Response, options?: ContentOptions): EndTag;\n    after(content: string | ReadableStream | Response, options?: ContentOptions): EndTag;\n    remove(): EndTag;\n}\ninterface Comment {\n    text: string;\n    readonly removed: boolean;\n    before(content: string, options?: ContentOptions): Comment;\n    after(content: string, options?: ContentOptions): Comment;\n    replace(content: string, options?: ContentOptions): Comment;\n    remove(): Comment;\n}\ninterface Text {\n    readonly text: string;\n    readonly lastInTextNode: boolean;\n    readonly removed: boolean;\n    before(content: string | ReadableStream | Response, options?: ContentOptions): Text;\n    after(content: string | ReadableStream | Response, options?: ContentOptions): Text;\n    replace(content: string | ReadableStream | Response, options?: ContentOptions): Text;\n    remove(): Text;\n}\ninterface DocumentEnd {\n    append(content: string, options?: ContentOptions): DocumentEnd;\n}\n/**\n * This is the event type for `fetch` events dispatched on the ServiceWorkerGlobalScope.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent)\n */\ndeclare abstract class FetchEvent extends ExtendableEvent {\n    /**\n     * The **`request`** read-only property of the the event handler.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/request)\n     */\n    readonly request: Request;\n    /**\n     * The **`respondWith()`** method of allows you to provide a promise for a Response yourself.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/respondWith)\n     */\n    respondWith(promise: Response | Promise<Response>): void;\n    passThroughOnException(): void;\n}\ntype HeadersInit = Headers | Iterable<Iterable<string>> | Record<string, string>;\n/**\n * The **`Headers`** interface of the Fetch API allows you to perform various actions on HTTP request and response headers.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers)\n */\ndeclare class Headers {\n    constructor(init?: HeadersInit);\n    /**\n     * The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a `Headers` object with a given name.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get)\n     */\n    get(name: string): string | null;\n    getAll(name: string): string[];\n    /**\n     * The **`getSetCookie()`** method of the Headers interface returns an array containing the values of all Set-Cookie headers associated with a response.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie)\n     */\n    getSetCookie(): string[];\n    /**\n     * The **`has()`** method of the Headers interface returns a boolean stating whether a `Headers` object contains a certain header.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has)\n     */\n    has(name: string): boolean;\n    /**\n     * The **`set()`** method of the Headers interface sets a new value for an existing header inside a `Headers` object, or adds the header if it does not already exist.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set)\n     */\n    set(name: string, value: string): void;\n    /**\n     * The **`append()`** method of the Headers interface appends a new value onto an existing header inside a `Headers` object, or adds the header if it does not already exist.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append)\n     */\n    append(name: string, value: string): void;\n    /**\n     * The **`delete()`** method of the Headers interface deletes a header from the current `Headers` object.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete)\n     */\n    delete(name: string): void;\n    forEach<This = unknown>(callback: (this: This, value: string, key: string, parent: Headers) => void, thisArg?: This): void;\n    /* Returns an iterator allowing to go through all key/value pairs contained in this object. */\n    entries(): IterableIterator<[\n        key: string,\n        value: string\n    ]>;\n    /* Returns an iterator allowing to go through all keys of the key/value pairs contained in this object. */\n    keys(): IterableIterator<string>;\n    /* Returns an iterator allowing to go through all values of the key/value pairs contained in this object. */\n    values(): IterableIterator<string>;\n    [Symbol.iterator](): IterableIterator<[\n        key: string,\n        value: string\n    ]>;\n}\ntype BodyInit = ReadableStream<Uint8Array> | string | ArrayBuffer | ArrayBufferView | Blob | URLSearchParams | FormData;\ndeclare abstract class Body {\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */\n    get body(): ReadableStream | null;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */\n    get bodyUsed(): boolean;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */\n    arrayBuffer(): Promise<ArrayBuffer>;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) */\n    bytes(): Promise<Uint8Array>;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) */\n    text(): Promise<string>;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */\n    json<T>(): Promise<T>;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) */\n    formData(): Promise<FormData>;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */\n    blob(): Promise<Blob>;\n}\n/**\n * The **`Response`** interface of the Fetch API represents the response to a request.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)\n */\ndeclare var Response: {\n    prototype: Response;\n    new (body?: BodyInit | null, init?: ResponseInit): Response;\n    error(): Response;\n    redirect(url: string, status?: number): Response;\n    json(any: any, maybeInit?: (ResponseInit | Response)): Response;\n};\n/**\n * The **`Response`** interface of the Fetch API represents the response to a request.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)\n */\ninterface Response extends Body {\n    /**\n     * The **`clone()`** method of the Response interface creates a clone of a response object, identical in every way, but stored in a different variable.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/clone)\n     */\n    clone(): Response;\n    /**\n     * The **`status`** read-only property of the Response interface contains the HTTP status codes of the response.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/status)\n     */\n    status: number;\n    /**\n     * The **`statusText`** read-only property of the Response interface contains the status message corresponding to the HTTP status code in Response.status.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/statusText)\n     */\n    statusText: string;\n    /**\n     * The **`headers`** read-only property of the with the response.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers)\n     */\n    headers: Headers;\n    /**\n     * The **`ok`** read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/ok)\n     */\n    ok: boolean;\n    /**\n     * The **`redirected`** read-only property of the Response interface indicates whether or not the response is the result of a request you made which was redirected.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/redirected)\n     */\n    redirected: boolean;\n    /**\n     * The **`url`** read-only property of the Response interface contains the URL of the response.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/url)\n     */\n    url: string;\n    webSocket: WebSocket | null;\n    cf: any | undefined;\n    /**\n     * The **`type`** read-only property of the Response interface contains the type of the response.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/type)\n     */\n    type: \"default\" | \"error\";\n}\ninterface ResponseInit {\n    status?: number;\n    statusText?: string;\n    headers?: HeadersInit;\n    cf?: any;\n    webSocket?: (WebSocket | null);\n    encodeBody?: \"automatic\" | \"manual\";\n}\ntype RequestInfo<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>> = Request<CfHostMetadata, Cf> | string;\n/**\n * The **`Request`** interface of the Fetch API represents a resource request.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request)\n */\ndeclare var Request: {\n    prototype: Request;\n    new <CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>>(input: RequestInfo<CfProperties> | URL, init?: RequestInit<Cf>): Request<CfHostMetadata, Cf>;\n};\n/**\n * The **`Request`** interface of the Fetch API represents a resource request.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request)\n */\ninterface Request<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>> extends Body {\n    /**\n     * The **`clone()`** method of the Request interface creates a copy of the current `Request` object.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/clone)\n     */\n    clone(): Request<CfHostMetadata, Cf>;\n    /**\n     * The **`method`** read-only property of the `POST`, etc.) A String indicating the method of the request.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/method)\n     */\n    method: string;\n    /**\n     * The **`url`** read-only property of the Request interface contains the URL of the request.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/url)\n     */\n    url: string;\n    /**\n     * The **`headers`** read-only property of the with the request.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers)\n     */\n    headers: Headers;\n    /**\n     * The **`redirect`** read-only property of the Request interface contains the mode for how redirects are handled.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/redirect)\n     */\n    redirect: string;\n    fetcher: Fetcher | null;\n    /**\n     * The read-only **`signal`** property of the Request interface returns the AbortSignal associated with the request.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/signal)\n     */\n    signal: AbortSignal;\n    cf: Cf | undefined;\n    /**\n     * The **`integrity`** read-only property of the Request interface contains the subresource integrity value of the request.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/integrity)\n     */\n    integrity: string;\n    /**\n     * The **`keepalive`** read-only property of the Request interface contains the request's `keepalive` setting (`true` or `false`), which indicates whether the browser will keep the associated request alive if the page that initiated it is unloaded before the request is complete.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/keepalive)\n     */\n    keepalive: boolean;\n    /**\n     * The **`cache`** read-only property of the Request interface contains the cache mode of the request.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/cache)\n     */\n    cache?: \"no-store\" | \"no-cache\";\n}\ninterface RequestInit<Cf = CfProperties> {\n    /* A string to set request's method. */\n    method?: string;\n    /* A Headers object, an object literal, or an array of two-item arrays to set request's headers. */\n    headers?: HeadersInit;\n    /* A BodyInit object or null to set request's body. */\n    body?: BodyInit | null;\n    /* A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. */\n    redirect?: string;\n    fetcher?: (Fetcher | null);\n    cf?: Cf;\n    /* A string indicating how the request will interact with the browser's cache to set request's cache. */\n    cache?: \"no-store\" | \"no-cache\";\n    /* A cryptographic hash of the resource to be fetched by request. Sets request's integrity. */\n    integrity?: string;\n    /* An AbortSignal to set request's signal. */\n    signal?: (AbortSignal | null);\n    encodeResponseBody?: \"automatic\" | \"manual\";\n}\ntype Service<T extends (new (...args: any[]) => Rpc.WorkerEntrypointBranded) | Rpc.WorkerEntrypointBranded | ExportedHandler<any, any, any> | undefined = undefined> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? Fetcher<InstanceType<T>> : T extends Rpc.WorkerEntrypointBranded ? Fetcher<T> : T extends Exclude<Rpc.EntrypointBranded, Rpc.WorkerEntrypointBranded> ? never : Fetcher<undefined>;\ntype Fetcher<T extends Rpc.EntrypointBranded | undefined = undefined, Reserved extends string = never> = (T extends Rpc.EntrypointBranded ? Rpc.Provider<T, Reserved | \"fetch\" | \"connect\"> : unknown) & {\n    fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;\n    connect(address: SocketAddress | string, options?: SocketOptions): Socket;\n};\ninterface KVNamespaceListKey<Metadata, Key extends string = string> {\n    name: Key;\n    expiration?: number;\n    metadata?: Metadata;\n}\ntype KVNamespaceListResult<Metadata, Key extends string = string> = {\n    list_complete: false;\n    keys: KVNamespaceListKey<Metadata, Key>[];\n    cursor: string;\n    cacheStatus: string | null;\n} | {\n    list_complete: true;\n    keys: KVNamespaceListKey<Metadata, Key>[];\n    cacheStatus: string | null;\n};\ninterface KVNamespace<Key extends string = string> {\n    get(key: Key, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<string | null>;\n    get(key: Key, type: \"text\"): Promise<string | null>;\n    get<ExpectedValue = unknown>(key: Key, type: \"json\"): Promise<ExpectedValue | null>;\n    get(key: Key, type: \"arrayBuffer\"): Promise<ArrayBuffer | null>;\n    get(key: Key, type: \"stream\"): Promise<ReadableStream | null>;\n    get(key: Key, options?: KVNamespaceGetOptions<\"text\">): Promise<string | null>;\n    get<ExpectedValue = unknown>(key: Key, options?: KVNamespaceGetOptions<\"json\">): Promise<ExpectedValue | null>;\n    get(key: Key, options?: KVNamespaceGetOptions<\"arrayBuffer\">): Promise<ArrayBuffer | null>;\n    get(key: Key, options?: KVNamespaceGetOptions<\"stream\">): Promise<ReadableStream | null>;\n    get(key: Array<Key>, type: \"text\"): Promise<Map<string, string | null>>;\n    get<ExpectedValue = unknown>(key: Array<Key>, type: \"json\"): Promise<Map<string, ExpectedValue | null>>;\n    get(key: Array<Key>, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<Map<string, string | null>>;\n    get(key: Array<Key>, options?: KVNamespaceGetOptions<\"text\">): Promise<Map<string, string | null>>;\n    get<ExpectedValue = unknown>(key: Array<Key>, options?: KVNamespaceGetOptions<\"json\">): Promise<Map<string, ExpectedValue | null>>;\n    list<Metadata = unknown>(options?: KVNamespaceListOptions): Promise<KVNamespaceListResult<Metadata, Key>>;\n    put(key: Key, value: string | ArrayBuffer | ArrayBufferView | ReadableStream, options?: KVNamespacePutOptions): Promise<void>;\n    getWithMetadata<Metadata = unknown>(key: Key, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>;\n    getWithMetadata<Metadata = unknown>(key: Key, type: \"text\"): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>;\n    getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: Key, type: \"json\"): Promise<KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>;\n    getWithMetadata<Metadata = unknown>(key: Key, type: \"arrayBuffer\"): Promise<KVNamespaceGetWithMetadataResult<ArrayBuffer, Metadata>>;\n    getWithMetadata<Metadata = unknown>(key: Key, type: \"stream\"): Promise<KVNamespaceGetWithMetadataResult<ReadableStream, Metadata>>;\n    getWithMetadata<Metadata = unknown>(key: Key, options: KVNamespaceGetOptions<\"text\">): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>;\n    getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: Key, options: KVNamespaceGetOptions<\"json\">): Promise<KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>;\n    getWithMetadata<Metadata = unknown>(key: Key, options: KVNamespaceGetOptions<\"arrayBuffer\">): Promise<KVNamespaceGetWithMetadataResult<ArrayBuffer, Metadata>>;\n    getWithMetadata<Metadata = unknown>(key: Key, options: KVNamespaceGetOptions<\"stream\">): Promise<KVNamespaceGetWithMetadataResult<ReadableStream, Metadata>>;\n    getWithMetadata<Metadata = unknown>(key: Array<Key>, type: \"text\"): Promise<Map<string, KVNamespaceGetWithMetadataResult<string, Metadata>>>;\n    getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: Array<Key>, type: \"json\"): Promise<Map<string, KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>>;\n    getWithMetadata<Metadata = unknown>(key: Array<Key>, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<Map<string, KVNamespaceGetWithMetadataResult<string, Metadata>>>;\n    getWithMetadata<Metadata = unknown>(key: Array<Key>, options?: KVNamespaceGetOptions<\"text\">): Promise<Map<string, KVNamespaceGetWithMetadataResult<string, Metadata>>>;\n    getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: Array<Key>, options?: KVNamespaceGetOptions<\"json\">): Promise<Map<string, KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>>;\n    delete(key: Key): Promise<void>;\n}\ninterface KVNamespaceListOptions {\n    limit?: number;\n    prefix?: (string | null);\n    cursor?: (string | null);\n}\ninterface KVNamespaceGetOptions<Type> {\n    type: Type;\n    cacheTtl?: number;\n}\ninterface KVNamespacePutOptions {\n    expiration?: number;\n    expirationTtl?: number;\n    metadata?: (any | null);\n}\ninterface KVNamespaceGetWithMetadataResult<Value, Metadata> {\n    value: Value | null;\n    metadata: Metadata | null;\n    cacheStatus: string | null;\n}\ntype QueueContentType = \"text\" | \"bytes\" | \"json\" | \"v8\";\ninterface Queue<Body = unknown> {\n    send(message: Body, options?: QueueSendOptions): Promise<void>;\n    sendBatch(messages: Iterable<MessageSendRequest<Body>>, options?: QueueSendBatchOptions): Promise<void>;\n}\ninterface QueueSendOptions {\n    contentType?: QueueContentType;\n    delaySeconds?: number;\n}\ninterface QueueSendBatchOptions {\n    delaySeconds?: number;\n}\ninterface MessageSendRequest<Body = unknown> {\n    body: Body;\n    contentType?: QueueContentType;\n    delaySeconds?: number;\n}\ninterface QueueRetryOptions {\n    delaySeconds?: number;\n}\ninterface Message<Body = unknown> {\n    readonly id: string;\n    readonly timestamp: Date;\n    readonly body: Body;\n    readonly attempts: number;\n    retry(options?: QueueRetryOptions): void;\n    ack(): void;\n}\ninterface QueueEvent<Body = unknown> extends ExtendableEvent {\n    readonly messages: readonly Message<Body>[];\n    readonly queue: string;\n    retryAll(options?: QueueRetryOptions): void;\n    ackAll(): void;\n}\ninterface MessageBatch<Body = unknown> {\n    readonly messages: readonly Message<Body>[];\n    readonly queue: string;\n    retryAll(options?: QueueRetryOptions): void;\n    ackAll(): void;\n}\ninterface R2Error extends Error {\n    readonly name: string;\n    readonly code: number;\n    readonly message: string;\n    readonly action: string;\n    readonly stack: any;\n}\ninterface R2ListOptions {\n    limit?: number;\n    prefix?: string;\n    cursor?: string;\n    delimiter?: string;\n    startAfter?: string;\n    include?: (\"httpMetadata\" | \"customMetadata\")[];\n}\ndeclare abstract class R2Bucket {\n    head(key: string): Promise<R2Object | null>;\n    get(key: string, options: R2GetOptions & {\n        onlyIf: R2Conditional | Headers;\n    }): Promise<R2ObjectBody | R2Object | null>;\n    get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>;\n    put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions & {\n        onlyIf: R2Conditional | Headers;\n    }): Promise<R2Object | null>;\n    put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions): Promise<R2Object>;\n    createMultipartUpload(key: string, options?: R2MultipartOptions): Promise<R2MultipartUpload>;\n    resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload;\n    delete(keys: string | string[]): Promise<void>;\n    list(options?: R2ListOptions): Promise<R2Objects>;\n}\ninterface R2MultipartUpload {\n    readonly key: string;\n    readonly uploadId: string;\n    uploadPart(partNumber: number, value: ReadableStream | (ArrayBuffer | ArrayBufferView) | string | Blob, options?: R2UploadPartOptions): Promise<R2UploadedPart>;\n    abort(): Promise<void>;\n    complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>;\n}\ninterface R2UploadedPart {\n    partNumber: number;\n    etag: string;\n}\ndeclare abstract class R2Object {\n    readonly key: string;\n    readonly version: string;\n    readonly size: number;\n    readonly etag: string;\n    readonly httpEtag: string;\n    readonly checksums: R2Checksums;\n    readonly uploaded: Date;\n    readonly httpMetadata?: R2HTTPMetadata;\n    readonly customMetadata?: Record<string, string>;\n    readonly range?: R2Range;\n    readonly storageClass: string;\n    readonly ssecKeyMd5?: string;\n    writeHttpMetadata(headers: Headers): void;\n}\ninterface R2ObjectBody extends R2Object {\n    get body(): ReadableStream;\n    get bodyUsed(): boolean;\n    arrayBuffer(): Promise<ArrayBuffer>;\n    bytes(): Promise<Uint8Array>;\n    text(): Promise<string>;\n    json<T>(): Promise<T>;\n    blob(): Promise<Blob>;\n}\ntype R2Range = {\n    offset: number;\n    length?: number;\n} | {\n    offset?: number;\n    length: number;\n} | {\n    suffix: number;\n};\ninterface R2Conditional {\n    etagMatches?: string;\n    etagDoesNotMatch?: string;\n    uploadedBefore?: Date;\n    uploadedAfter?: Date;\n    secondsGranularity?: boolean;\n}\ninterface R2GetOptions {\n    onlyIf?: (R2Conditional | Headers);\n    range?: (R2Range | Headers);\n    ssecKey?: (ArrayBuffer | string);\n}\ninterface R2PutOptions {\n    onlyIf?: (R2Conditional | Headers);\n    httpMetadata?: (R2HTTPMetadata | Headers);\n    customMetadata?: Record<string, string>;\n    md5?: ((ArrayBuffer | ArrayBufferView) | string);\n    sha1?: ((ArrayBuffer | ArrayBufferView) | string);\n    sha256?: ((ArrayBuffer | ArrayBufferView) | string);\n    sha384?: ((ArrayBuffer | ArrayBufferView) | string);\n    sha512?: ((ArrayBuffer | ArrayBufferView) | string);\n    storageClass?: string;\n    ssecKey?: (ArrayBuffer | string);\n}\ninterface R2MultipartOptions {\n    httpMetadata?: (R2HTTPMetadata | Headers);\n    customMetadata?: Record<string, string>;\n    storageClass?: string;\n    ssecKey?: (ArrayBuffer | string);\n}\ninterface R2Checksums {\n    readonly md5?: ArrayBuffer;\n    readonly sha1?: ArrayBuffer;\n    readonly sha256?: ArrayBuffer;\n    readonly sha384?: ArrayBuffer;\n    readonly sha512?: ArrayBuffer;\n    toJSON(): R2StringChecksums;\n}\ninterface R2StringChecksums {\n    md5?: string;\n    sha1?: string;\n    sha256?: string;\n    sha384?: string;\n    sha512?: string;\n}\ninterface R2HTTPMetadata {\n    contentType?: string;\n    contentLanguage?: string;\n    contentDisposition?: string;\n    contentEncoding?: string;\n    cacheControl?: string;\n    cacheExpiry?: Date;\n}\ntype R2Objects = {\n    objects: R2Object[];\n    delimitedPrefixes: string[];\n} & ({\n    truncated: true;\n    cursor: string;\n} | {\n    truncated: false;\n});\ninterface R2UploadPartOptions {\n    ssecKey?: (ArrayBuffer | string);\n}\ndeclare abstract class ScheduledEvent extends ExtendableEvent {\n    readonly scheduledTime: number;\n    readonly cron: string;\n    noRetry(): void;\n}\ninterface ScheduledController {\n    readonly scheduledTime: number;\n    readonly cron: string;\n    noRetry(): void;\n}\ninterface QueuingStrategy<T = any> {\n    highWaterMark?: (number | bigint);\n    size?: (chunk: T) => number | bigint;\n}\ninterface UnderlyingSink<W = any> {\n    type?: string;\n    start?: (controller: WritableStreamDefaultController) => void | Promise<void>;\n    write?: (chunk: W, controller: WritableStreamDefaultController) => void | Promise<void>;\n    abort?: (reason: any) => void | Promise<void>;\n    close?: () => void | Promise<void>;\n}\ninterface UnderlyingByteSource {\n    type: \"bytes\";\n    autoAllocateChunkSize?: number;\n    start?: (controller: ReadableByteStreamController) => void | Promise<void>;\n    pull?: (controller: ReadableByteStreamController) => void | Promise<void>;\n    cancel?: (reason: any) => void | Promise<void>;\n}\ninterface UnderlyingSource<R = any> {\n    type?: \"\" | undefined;\n    start?: (controller: ReadableStreamDefaultController<R>) => void | Promise<void>;\n    pull?: (controller: ReadableStreamDefaultController<R>) => void | Promise<void>;\n    cancel?: (reason: any) => void | Promise<void>;\n    expectedLength?: (number | bigint);\n}\ninterface Transformer<I = any, O = any> {\n    readableType?: string;\n    writableType?: string;\n    start?: (controller: TransformStreamDefaultController<O>) => void | Promise<void>;\n    transform?: (chunk: I, controller: TransformStreamDefaultController<O>) => void | Promise<void>;\n    flush?: (controller: TransformStreamDefaultController<O>) => void | Promise<void>;\n    cancel?: (reason: any) => void | Promise<void>;\n    expectedLength?: number;\n}\ninterface StreamPipeOptions {\n    /**\n     * Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered.\n     *\n     * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader.\n     *\n     * Errors and closures of the source and destination streams propagate as follows:\n     *\n     * An error in this source readable stream will abort destination, unless preventAbort is truthy. The returned promise will be rejected with the source's error, or with any error that occurs during aborting the destination.\n     *\n     * An error in destination will cancel this source readable stream, unless preventCancel is truthy. The returned promise will be rejected with the destination's error, or with any error that occurs during canceling the source.\n     *\n     * When this source readable stream closes, destination will be closed, unless preventClose is truthy. The returned promise will be fulfilled once this process completes, unless an error is encountered while closing the destination, in which case it will be rejected with that error.\n     *\n     * If destination starts out closed or closing, this source readable stream will be canceled, unless preventCancel is true. The returned promise will be rejected with an error indicating piping to a closed stream failed, or with any error that occurs during canceling the source.\n     *\n     * The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set.\n     */\n    preventClose?: boolean;\n    preventAbort?: boolean;\n    preventCancel?: boolean;\n    signal?: AbortSignal;\n}\ntype ReadableStreamReadResult<R = any> = {\n    done: false;\n    value: R;\n} | {\n    done: true;\n    value?: undefined;\n};\n/**\n * The `ReadableStream` interface of the Streams API represents a readable stream of byte data.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream)\n */\ninterface ReadableStream<R = any> {\n    /**\n     * The **`locked`** read-only property of the ReadableStream interface returns whether or not the readable stream is locked to a reader.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/locked)\n     */\n    get locked(): boolean;\n    /**\n     * The **`cancel()`** method of the ReadableStream interface returns a Promise that resolves when the stream is canceled.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/cancel)\n     */\n    cancel(reason?: any): Promise<void>;\n    /**\n     * The **`getReader()`** method of the ReadableStream interface creates a reader and locks the stream to it.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader)\n     */\n    getReader(): ReadableStreamDefaultReader<R>;\n    /**\n     * The **`getReader()`** method of the ReadableStream interface creates a reader and locks the stream to it.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader)\n     */\n    getReader(options: ReadableStreamGetReaderOptions): ReadableStreamBYOBReader;\n    /**\n     * The **`pipeThrough()`** method of the ReadableStream interface provides a chainable way of piping the current stream through a transform stream or any other writable/readable pair.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeThrough)\n     */\n    pipeThrough<T>(transform: ReadableWritablePair<T, R>, options?: StreamPipeOptions): ReadableStream<T>;\n    /**\n     * The **`pipeTo()`** method of the ReadableStream interface pipes the current `ReadableStream` to a given WritableStream and returns a Promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeTo)\n     */\n    pipeTo(destination: WritableStream<R>, options?: StreamPipeOptions): Promise<void>;\n    /**\n     * The **`tee()`** method of the two-element array containing the two resulting branches as new ReadableStream instances.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/tee)\n     */\n    tee(): [\n        ReadableStream<R>,\n        ReadableStream<R>\n    ];\n    values(options?: ReadableStreamValuesOptions): AsyncIterableIterator<R>;\n    [Symbol.asyncIterator](options?: ReadableStreamValuesOptions): AsyncIterableIterator<R>;\n}\n/**\n * The `ReadableStream` interface of the Streams API represents a readable stream of byte data.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream)\n */\ndeclare const ReadableStream: {\n    prototype: ReadableStream;\n    new (underlyingSource: UnderlyingByteSource, strategy?: QueuingStrategy<Uint8Array>): ReadableStream<Uint8Array>;\n    new <R = any>(underlyingSource?: UnderlyingSource<R>, strategy?: QueuingStrategy<R>): ReadableStream<R>;\n};\n/**\n * The **`ReadableStreamDefaultReader`** interface of the Streams API represents a default reader that can be used to read stream data supplied from a network (such as a fetch request).\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader)\n */\ndeclare class ReadableStreamDefaultReader<R = any> {\n    constructor(stream: ReadableStream);\n    get closed(): Promise<void>;\n    cancel(reason?: any): Promise<void>;\n    /**\n     * The **`read()`** method of the ReadableStreamDefaultReader interface returns a Promise providing access to the next chunk in the stream's internal queue.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/read)\n     */\n    read(): Promise<ReadableStreamReadResult<R>>;\n    /**\n     * The **`releaseLock()`** method of the ReadableStreamDefaultReader interface releases the reader's lock on the stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/releaseLock)\n     */\n    releaseLock(): void;\n}\n/**\n * The `ReadableStreamBYOBReader` interface of the Streams API defines a reader for a ReadableStream that supports zero-copy reading from an underlying byte source.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader)\n */\ndeclare class ReadableStreamBYOBReader {\n    constructor(stream: ReadableStream);\n    get closed(): Promise<void>;\n    cancel(reason?: any): Promise<void>;\n    /**\n     * The **`read()`** method of the ReadableStreamBYOBReader interface is used to read data into a view on a user-supplied buffer from an associated readable byte stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/read)\n     */\n    read<T extends ArrayBufferView>(view: T): Promise<ReadableStreamReadResult<T>>;\n    /**\n     * The **`releaseLock()`** method of the ReadableStreamBYOBReader interface releases the reader's lock on the stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/releaseLock)\n     */\n    releaseLock(): void;\n    readAtLeast<T extends ArrayBufferView>(minElements: number, view: T): Promise<ReadableStreamReadResult<T>>;\n}\ninterface ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions {\n    min?: number;\n}\ninterface ReadableStreamGetReaderOptions {\n    /**\n     * Creates a ReadableStreamBYOBReader and locks the stream to the new reader.\n     *\n     * This call behaves the same way as the no-argument variant, except that it only works on readable byte streams, i.e. streams which were constructed specifically with the ability to handle \"bring your own buffer\" reading. The returned BYOB reader provides the ability to directly read individual chunks from the stream via its read() method, into developer-supplied buffers, allowing more precise control over allocation.\n     */\n    mode: \"byob\";\n}\n/**\n * The **`ReadableStreamBYOBRequest`** interface of the Streams API represents a 'pull request' for data from an underlying source that will made as a zero-copy transfer to a consumer (bypassing the stream's internal queues).\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest)\n */\ndeclare abstract class ReadableStreamBYOBRequest {\n    /**\n     * The **`view`** getter property of the ReadableStreamBYOBRequest interface returns the current view.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/view)\n     */\n    get view(): Uint8Array | null;\n    /**\n     * The **`respond()`** method of the ReadableStreamBYOBRequest interface is used to signal to the associated readable byte stream that the specified number of bytes were written into the ReadableStreamBYOBRequest.view.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respond)\n     */\n    respond(bytesWritten: number): void;\n    /**\n     * The **`respondWithNewView()`** method of the ReadableStreamBYOBRequest interface specifies a new view that the consumer of the associated readable byte stream should write to instead of ReadableStreamBYOBRequest.view.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respondWithNewView)\n     */\n    respondWithNewView(view: ArrayBuffer | ArrayBufferView): void;\n    get atLeast(): number | null;\n}\n/**\n * The **`ReadableStreamDefaultController`** interface of the Streams API represents a controller allowing control of a ReadableStream's state and internal queue.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController)\n */\ndeclare abstract class ReadableStreamDefaultController<R = any> {\n    /**\n     * The **`desiredSize`** read-only property of the required to fill the stream's internal queue.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/desiredSize)\n     */\n    get desiredSize(): number | null;\n    /**\n     * The **`close()`** method of the ReadableStreamDefaultController interface closes the associated stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/close)\n     */\n    close(): void;\n    /**\n     * The **`enqueue()`** method of the ```js-nolint enqueue(chunk) ``` - `chunk` - : The chunk to enqueue.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/enqueue)\n     */\n    enqueue(chunk?: R): void;\n    /**\n     * The **`error()`** method of the with the associated stream to error.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/error)\n     */\n    error(reason: any): void;\n}\n/**\n * The **`ReadableByteStreamController`** interface of the Streams API represents a controller for a readable byte stream.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController)\n */\ndeclare abstract class ReadableByteStreamController {\n    /**\n     * The **`byobRequest`** read-only property of the ReadableByteStreamController interface returns the current BYOB request, or `null` if there are no pending requests.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/byobRequest)\n     */\n    get byobRequest(): ReadableStreamBYOBRequest | null;\n    /**\n     * The **`desiredSize`** read-only property of the ReadableByteStreamController interface returns the number of bytes required to fill the stream's internal queue to its 'desired size'.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/desiredSize)\n     */\n    get desiredSize(): number | null;\n    /**\n     * The **`close()`** method of the ReadableByteStreamController interface closes the associated stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/close)\n     */\n    close(): void;\n    /**\n     * The **`enqueue()`** method of the ReadableByteStreamController interface enqueues a given chunk on the associated readable byte stream (the chunk is copied into the stream's internal queues).\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/enqueue)\n     */\n    enqueue(chunk: ArrayBuffer | ArrayBufferView): void;\n    /**\n     * The **`error()`** method of the ReadableByteStreamController interface causes any future interactions with the associated stream to error with the specified reason.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/error)\n     */\n    error(reason: any): void;\n}\n/**\n * The **`WritableStreamDefaultController`** interface of the Streams API represents a controller allowing control of a WritableStream's state.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController)\n */\ndeclare abstract class WritableStreamDefaultController {\n    /**\n     * The read-only **`signal`** property of the WritableStreamDefaultController interface returns the AbortSignal associated with the controller.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/signal)\n     */\n    get signal(): AbortSignal;\n    /**\n     * The **`error()`** method of the with the associated stream to error.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/error)\n     */\n    error(reason?: any): void;\n}\n/**\n * The **`TransformStreamDefaultController`** interface of the Streams API provides methods to manipulate the associated ReadableStream and WritableStream.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController)\n */\ndeclare abstract class TransformStreamDefaultController<O = any> {\n    /**\n     * The **`desiredSize`** read-only property of the TransformStreamDefaultController interface returns the desired size to fill the queue of the associated ReadableStream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/desiredSize)\n     */\n    get desiredSize(): number | null;\n    /**\n     * The **`enqueue()`** method of the TransformStreamDefaultController interface enqueues the given chunk in the readable side of the stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/enqueue)\n     */\n    enqueue(chunk?: O): void;\n    /**\n     * The **`error()`** method of the TransformStreamDefaultController interface errors both sides of the stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/error)\n     */\n    error(reason: any): void;\n    /**\n     * The **`terminate()`** method of the TransformStreamDefaultController interface closes the readable side and errors the writable side of the stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/terminate)\n     */\n    terminate(): void;\n}\ninterface ReadableWritablePair<R = any, W = any> {\n    /**\n     * Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use.\n     *\n     * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader.\n     */\n    writable: WritableStream<W>;\n    readable: ReadableStream<R>;\n}\n/**\n * The **`WritableStream`** interface of the Streams API provides a standard abstraction for writing streaming data to a destination, known as a sink.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream)\n */\ndeclare class WritableStream<W = any> {\n    constructor(underlyingSink?: UnderlyingSink, queuingStrategy?: QueuingStrategy);\n    /**\n     * The **`locked`** read-only property of the WritableStream interface returns a boolean indicating whether the `WritableStream` is locked to a writer.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/locked)\n     */\n    get locked(): boolean;\n    /**\n     * The **`abort()`** method of the WritableStream interface aborts the stream, signaling that the producer can no longer successfully write to the stream and it is to be immediately moved to an error state, with any queued writes discarded.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/abort)\n     */\n    abort(reason?: any): Promise<void>;\n    /**\n     * The **`close()`** method of the WritableStream interface closes the associated stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/close)\n     */\n    close(): Promise<void>;\n    /**\n     * The **`getWriter()`** method of the WritableStream interface returns a new instance of WritableStreamDefaultWriter and locks the stream to that instance.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/getWriter)\n     */\n    getWriter(): WritableStreamDefaultWriter<W>;\n}\n/**\n * The **`WritableStreamDefaultWriter`** interface of the Streams API is the object returned by WritableStream.getWriter() and once created locks the writer to the `WritableStream` ensuring that no other streams can write to the underlying sink.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter)\n */\ndeclare class WritableStreamDefaultWriter<W = any> {\n    constructor(stream: WritableStream);\n    /**\n     * The **`closed`** read-only property of the the stream errors or the writer's lock is released.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/closed)\n     */\n    get closed(): Promise<void>;\n    /**\n     * The **`ready`** read-only property of the that resolves when the desired size of the stream's internal queue transitions from non-positive to positive, signaling that it is no longer applying backpressure.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/ready)\n     */\n    get ready(): Promise<void>;\n    /**\n     * The **`desiredSize`** read-only property of the to fill the stream's internal queue.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/desiredSize)\n     */\n    get desiredSize(): number | null;\n    /**\n     * The **`abort()`** method of the the producer can no longer successfully write to the stream and it is to be immediately moved to an error state, with any queued writes discarded.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/abort)\n     */\n    abort(reason?: any): Promise<void>;\n    /**\n     * The **`close()`** method of the stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/close)\n     */\n    close(): Promise<void>;\n    /**\n     * The **`write()`** method of the operation.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/write)\n     */\n    write(chunk?: W): Promise<void>;\n    /**\n     * The **`releaseLock()`** method of the corresponding stream.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/releaseLock)\n     */\n    releaseLock(): void;\n}\n/**\n * The **`TransformStream`** interface of the Streams API represents a concrete implementation of the pipe chain _transform stream_ concept.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream)\n */\ndeclare class TransformStream<I = any, O = any> {\n    constructor(transformer?: Transformer<I, O>, writableStrategy?: QueuingStrategy<I>, readableStrategy?: QueuingStrategy<O>);\n    /**\n     * The **`readable`** read-only property of the TransformStream interface returns the ReadableStream instance controlled by this `TransformStream`.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/readable)\n     */\n    get readable(): ReadableStream<O>;\n    /**\n     * The **`writable`** read-only property of the TransformStream interface returns the WritableStream instance controlled by this `TransformStream`.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/writable)\n     */\n    get writable(): WritableStream<I>;\n}\ndeclare class FixedLengthStream extends IdentityTransformStream {\n    constructor(expectedLength: number | bigint, queuingStrategy?: IdentityTransformStreamQueuingStrategy);\n}\ndeclare class IdentityTransformStream extends TransformStream<ArrayBuffer | ArrayBufferView, Uint8Array> {\n    constructor(queuingStrategy?: IdentityTransformStreamQueuingStrategy);\n}\ninterface IdentityTransformStreamQueuingStrategy {\n    highWaterMark?: (number | bigint);\n}\ninterface ReadableStreamValuesOptions {\n    preventCancel?: boolean;\n}\n/**\n * The **`CompressionStream`** interface of the Compression Streams API is an API for compressing a stream of data.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CompressionStream)\n */\ndeclare class CompressionStream extends TransformStream<ArrayBuffer | ArrayBufferView, Uint8Array> {\n    constructor(format: \"gzip\" | \"deflate\" | \"deflate-raw\");\n}\n/**\n * The **`DecompressionStream`** interface of the Compression Streams API is an API for decompressing a stream of data.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DecompressionStream)\n */\ndeclare class DecompressionStream extends TransformStream<ArrayBuffer | ArrayBufferView, Uint8Array> {\n    constructor(format: \"gzip\" | \"deflate\" | \"deflate-raw\");\n}\n/**\n * The **`TextEncoderStream`** interface of the Encoding API converts a stream of strings into bytes in the UTF-8 encoding.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoderStream)\n */\ndeclare class TextEncoderStream extends TransformStream<string, Uint8Array> {\n    constructor();\n    get encoding(): string;\n}\n/**\n * The **`TextDecoderStream`** interface of the Encoding API converts a stream of text in a binary encoding, such as UTF-8 etc., to a stream of strings.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoderStream)\n */\ndeclare class TextDecoderStream extends TransformStream<ArrayBuffer | ArrayBufferView, string> {\n    constructor(label?: string, options?: TextDecoderStreamTextDecoderStreamInit);\n    get encoding(): string;\n    get fatal(): boolean;\n    get ignoreBOM(): boolean;\n}\ninterface TextDecoderStreamTextDecoderStreamInit {\n    fatal?: boolean;\n    ignoreBOM?: boolean;\n}\n/**\n * The **`ByteLengthQueuingStrategy`** interface of the Streams API provides a built-in byte length queuing strategy that can be used when constructing streams.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy)\n */\ndeclare class ByteLengthQueuingStrategy implements QueuingStrategy<ArrayBufferView> {\n    constructor(init: QueuingStrategyInit);\n    /**\n     * The read-only **`ByteLengthQueuingStrategy.highWaterMark`** property returns the total number of bytes that can be contained in the internal queue before backpressure is applied.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/highWaterMark)\n     */\n    get highWaterMark(): number;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/size) */\n    get size(): (chunk?: any) => number;\n}\n/**\n * The **`CountQueuingStrategy`** interface of the Streams API provides a built-in chunk counting queuing strategy that can be used when constructing streams.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy)\n */\ndeclare class CountQueuingStrategy implements QueuingStrategy {\n    constructor(init: QueuingStrategyInit);\n    /**\n     * The read-only **`CountQueuingStrategy.highWaterMark`** property returns the total number of chunks that can be contained in the internal queue before backpressure is applied.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/highWaterMark)\n     */\n    get highWaterMark(): number;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/size) */\n    get size(): (chunk?: any) => number;\n}\ninterface QueuingStrategyInit {\n    /**\n     * Creates a new ByteLengthQueuingStrategy with the provided high water mark.\n     *\n     * Note that the provided high water mark will not be validated ahead of time. Instead, if it is negative, NaN, or not a number, the resulting ByteLengthQueuingStrategy will cause the corresponding stream constructor to throw.\n     */\n    highWaterMark: number;\n}\ninterface ScriptVersion {\n    id?: string;\n    tag?: string;\n    message?: string;\n}\ndeclare abstract class TailEvent extends ExtendableEvent {\n    readonly events: TraceItem[];\n    readonly traces: TraceItem[];\n}\ninterface TraceItem {\n    readonly event: (TraceItemFetchEventInfo | TraceItemJsRpcEventInfo | TraceItemScheduledEventInfo | TraceItemAlarmEventInfo | TraceItemQueueEventInfo | TraceItemEmailEventInfo | TraceItemTailEventInfo | TraceItemCustomEventInfo | TraceItemHibernatableWebSocketEventInfo) | null;\n    readonly eventTimestamp: number | null;\n    readonly logs: TraceLog[];\n    readonly exceptions: TraceException[];\n    readonly diagnosticsChannelEvents: TraceDiagnosticChannelEvent[];\n    readonly scriptName: string | null;\n    readonly entrypoint?: string;\n    readonly scriptVersion?: ScriptVersion;\n    readonly dispatchNamespace?: string;\n    readonly scriptTags?: string[];\n    readonly durableObjectId?: string;\n    readonly outcome: string;\n    readonly executionModel: string;\n    readonly truncated: boolean;\n    readonly cpuTime: number;\n    readonly wallTime: number;\n}\ninterface TraceItemAlarmEventInfo {\n    readonly scheduledTime: Date;\n}\ninterface TraceItemCustomEventInfo {\n}\ninterface TraceItemScheduledEventInfo {\n    readonly scheduledTime: number;\n    readonly cron: string;\n}\ninterface TraceItemQueueEventInfo {\n    readonly queue: string;\n    readonly batchSize: number;\n}\ninterface TraceItemEmailEventInfo {\n    readonly mailFrom: string;\n    readonly rcptTo: string;\n    readonly rawSize: number;\n}\ninterface TraceItemTailEventInfo {\n    readonly consumedEvents: TraceItemTailEventInfoTailItem[];\n}\ninterface TraceItemTailEventInfoTailItem {\n    readonly scriptName: string | null;\n}\ninterface TraceItemFetchEventInfo {\n    readonly response?: TraceItemFetchEventInfoResponse;\n    readonly request: TraceItemFetchEventInfoRequest;\n}\ninterface TraceItemFetchEventInfoRequest {\n    readonly cf?: any;\n    readonly headers: Record<string, string>;\n    readonly method: string;\n    readonly url: string;\n    getUnredacted(): TraceItemFetchEventInfoRequest;\n}\ninterface TraceItemFetchEventInfoResponse {\n    readonly status: number;\n}\ninterface TraceItemJsRpcEventInfo {\n    readonly rpcMethod: string;\n}\ninterface TraceItemHibernatableWebSocketEventInfo {\n    readonly getWebSocketEvent: TraceItemHibernatableWebSocketEventInfoMessage | TraceItemHibernatableWebSocketEventInfoClose | TraceItemHibernatableWebSocketEventInfoError;\n}\ninterface TraceItemHibernatableWebSocketEventInfoMessage {\n    readonly webSocketEventType: string;\n}\ninterface TraceItemHibernatableWebSocketEventInfoClose {\n    readonly webSocketEventType: string;\n    readonly code: number;\n    readonly wasClean: boolean;\n}\ninterface TraceItemHibernatableWebSocketEventInfoError {\n    readonly webSocketEventType: string;\n}\ninterface TraceLog {\n    readonly timestamp: number;\n    readonly level: string;\n    readonly message: any;\n}\ninterface TraceException {\n    readonly timestamp: number;\n    readonly message: string;\n    readonly name: string;\n    readonly stack?: string;\n}\ninterface TraceDiagnosticChannelEvent {\n    readonly timestamp: number;\n    readonly channel: string;\n    readonly message: any;\n}\ninterface TraceMetrics {\n    readonly cpuTime: number;\n    readonly wallTime: number;\n}\ninterface UnsafeTraceMetrics {\n    fromTrace(item: TraceItem): TraceMetrics;\n}\n/**\n * The **`URL`** interface is used to parse, construct, normalize, and encode URL.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL)\n */\ndeclare class URL {\n    constructor(url: string | URL, base?: string | URL);\n    /**\n     * The **`origin`** read-only property of the URL interface returns a string containing the Unicode serialization of the origin of the represented URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/origin)\n     */\n    get origin(): string;\n    /**\n     * The **`href`** property of the URL interface is a string containing the whole URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href)\n     */\n    get href(): string;\n    /**\n     * The **`href`** property of the URL interface is a string containing the whole URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href)\n     */\n    set href(value: string);\n    /**\n     * The **`protocol`** property of the URL interface is a string containing the protocol or scheme of the URL, including the final `':'`.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol)\n     */\n    get protocol(): string;\n    /**\n     * The **`protocol`** property of the URL interface is a string containing the protocol or scheme of the URL, including the final `':'`.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol)\n     */\n    set protocol(value: string);\n    /**\n     * The **`username`** property of the URL interface is a string containing the username component of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username)\n     */\n    get username(): string;\n    /**\n     * The **`username`** property of the URL interface is a string containing the username component of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username)\n     */\n    set username(value: string);\n    /**\n     * The **`password`** property of the URL interface is a string containing the password component of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password)\n     */\n    get password(): string;\n    /**\n     * The **`password`** property of the URL interface is a string containing the password component of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password)\n     */\n    set password(value: string);\n    /**\n     * The **`host`** property of the URL interface is a string containing the host, which is the URL.hostname, and then, if the port of the URL is nonempty, a `':'`, followed by the URL.port of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host)\n     */\n    get host(): string;\n    /**\n     * The **`host`** property of the URL interface is a string containing the host, which is the URL.hostname, and then, if the port of the URL is nonempty, a `':'`, followed by the URL.port of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host)\n     */\n    set host(value: string);\n    /**\n     * The **`hostname`** property of the URL interface is a string containing either the domain name or IP address of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname)\n     */\n    get hostname(): string;\n    /**\n     * The **`hostname`** property of the URL interface is a string containing either the domain name or IP address of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname)\n     */\n    set hostname(value: string);\n    /**\n     * The **`port`** property of the URL interface is a string containing the port number of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port)\n     */\n    get port(): string;\n    /**\n     * The **`port`** property of the URL interface is a string containing the port number of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port)\n     */\n    set port(value: string);\n    /**\n     * The **`pathname`** property of the URL interface represents a location in a hierarchical structure.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname)\n     */\n    get pathname(): string;\n    /**\n     * The **`pathname`** property of the URL interface represents a location in a hierarchical structure.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname)\n     */\n    set pathname(value: string);\n    /**\n     * The **`search`** property of the URL interface is a search string, also called a _query string_, that is a string containing a `'?'` followed by the parameters of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search)\n     */\n    get search(): string;\n    /**\n     * The **`search`** property of the URL interface is a search string, also called a _query string_, that is a string containing a `'?'` followed by the parameters of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search)\n     */\n    set search(value: string);\n    /**\n     * The **`hash`** property of the URL interface is a string containing a `'#'` followed by the fragment identifier of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash)\n     */\n    get hash(): string;\n    /**\n     * The **`hash`** property of the URL interface is a string containing a `'#'` followed by the fragment identifier of the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash)\n     */\n    set hash(value: string);\n    /**\n     * The **`searchParams`** read-only property of the access to the [MISSING: httpmethod('GET')] decoded query arguments contained in the URL.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/searchParams)\n     */\n    get searchParams(): URLSearchParams;\n    /**\n     * The **`toJSON()`** method of the URL interface returns a string containing a serialized version of the URL, although in practice it seems to have the same effect as ```js-nolint toJSON() ``` None.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/toJSON)\n     */\n    toJSON(): string;\n    /*function toString() { [native code] }*/\n    toString(): string;\n    /**\n     * The **`URL.canParse()`** static method of the URL interface returns a boolean indicating whether or not an absolute URL, or a relative URL combined with a base URL, are parsable and valid.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/canParse_static)\n     */\n    static canParse(url: string, base?: string): boolean;\n    /**\n     * The **`URL.parse()`** static method of the URL interface returns a newly created URL object representing the URL defined by the parameters.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/parse_static)\n     */\n    static parse(url: string, base?: string): URL | null;\n    /**\n     * The **`createObjectURL()`** static method of the URL interface creates a string containing a URL representing the object given in the parameter.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL_static)\n     */\n    static createObjectURL(object: File | Blob): string;\n    /**\n     * The **`revokeObjectURL()`** static method of the URL interface releases an existing object URL which was previously created by calling Call this method when you've finished using an object URL to let the browser know not to keep the reference to the file any longer.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL_static)\n     */\n    static revokeObjectURL(object_url: string): void;\n}\n/**\n * The **`URLSearchParams`** interface defines utility methods to work with the query string of a URL.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams)\n */\ndeclare class URLSearchParams {\n    constructor(init?: (Iterable<Iterable<string>> | Record<string, string> | string));\n    /**\n     * The **`size`** read-only property of the URLSearchParams interface indicates the total number of search parameter entries.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/size)\n     */\n    get size(): number;\n    /**\n     * The **`append()`** method of the URLSearchParams interface appends a specified key/value pair as a new search parameter.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/append)\n     */\n    append(name: string, value: string): void;\n    /**\n     * The **`delete()`** method of the URLSearchParams interface deletes specified parameters and their associated value(s) from the list of all search parameters.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/delete)\n     */\n    delete(name: string, value?: string): void;\n    /**\n     * The **`get()`** method of the URLSearchParams interface returns the first value associated to the given search parameter.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/get)\n     */\n    get(name: string): string | null;\n    /**\n     * The **`getAll()`** method of the URLSearchParams interface returns all the values associated with a given search parameter as an array.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/getAll)\n     */\n    getAll(name: string): string[];\n    /**\n     * The **`has()`** method of the URLSearchParams interface returns a boolean value that indicates whether the specified parameter is in the search parameters.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/has)\n     */\n    has(name: string, value?: string): boolean;\n    /**\n     * The **`set()`** method of the URLSearchParams interface sets the value associated with a given search parameter to the given value.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/set)\n     */\n    set(name: string, value: string): void;\n    /**\n     * The **`URLSearchParams.sort()`** method sorts all key/value pairs contained in this object in place and returns `undefined`.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/sort)\n     */\n    sort(): void;\n    /* Returns an array of key, value pairs for every entry in the search params. */\n    entries(): IterableIterator<[\n        key: string,\n        value: string\n    ]>;\n    /* Returns a list of keys in the search params. */\n    keys(): IterableIterator<string>;\n    /* Returns a list of values in the search params. */\n    values(): IterableIterator<string>;\n    forEach<This = unknown>(callback: (this: This, value: string, key: string, parent: URLSearchParams) => void, thisArg?: This): void;\n    /*function toString() { [native code] }*/\n    toString(): string;\n    [Symbol.iterator](): IterableIterator<[\n        key: string,\n        value: string\n    ]>;\n}\ndeclare class URLPattern {\n    constructor(input?: (string | URLPatternInit), baseURL?: (string | URLPatternOptions), patternOptions?: URLPatternOptions);\n    get protocol(): string;\n    get username(): string;\n    get password(): string;\n    get hostname(): string;\n    get port(): string;\n    get pathname(): string;\n    get search(): string;\n    get hash(): string;\n    get hasRegExpGroups(): boolean;\n    test(input?: (string | URLPatternInit), baseURL?: string): boolean;\n    exec(input?: (string | URLPatternInit), baseURL?: string): URLPatternResult | null;\n}\ninterface URLPatternInit {\n    protocol?: string;\n    username?: string;\n    password?: string;\n    hostname?: string;\n    port?: string;\n    pathname?: string;\n    search?: string;\n    hash?: string;\n    baseURL?: string;\n}\ninterface URLPatternComponentResult {\n    input: string;\n    groups: Record<string, string>;\n}\ninterface URLPatternResult {\n    inputs: (string | URLPatternInit)[];\n    protocol: URLPatternComponentResult;\n    username: URLPatternComponentResult;\n    password: URLPatternComponentResult;\n    hostname: URLPatternComponentResult;\n    port: URLPatternComponentResult;\n    pathname: URLPatternComponentResult;\n    search: URLPatternComponentResult;\n    hash: URLPatternComponentResult;\n}\ninterface URLPatternOptions {\n    ignoreCase?: boolean;\n}\n/**\n * A `CloseEvent` is sent to clients using WebSockets when the connection is closed.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent)\n */\ndeclare class CloseEvent extends Event {\n    constructor(type: string, initializer?: CloseEventInit);\n    /**\n     * The **`code`** read-only property of the CloseEvent interface returns a WebSocket connection close code indicating the reason the connection was closed.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/code)\n     */\n    readonly code: number;\n    /**\n     * The **`reason`** read-only property of the CloseEvent interface returns the WebSocket connection close reason the server gave for closing the connection; that is, a concise human-readable prose explanation for the closure.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/reason)\n     */\n    readonly reason: string;\n    /**\n     * The **`wasClean`** read-only property of the CloseEvent interface returns `true` if the connection closed cleanly.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/wasClean)\n     */\n    readonly wasClean: boolean;\n}\ninterface CloseEventInit {\n    code?: number;\n    reason?: string;\n    wasClean?: boolean;\n}\ntype WebSocketEventMap = {\n    close: CloseEvent;\n    message: MessageEvent;\n    open: Event;\n    error: ErrorEvent;\n};\n/**\n * The `WebSocket` object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket)\n */\ndeclare var WebSocket: {\n    prototype: WebSocket;\n    new (url: string, protocols?: (string[] | string)): WebSocket;\n    readonly READY_STATE_CONNECTING: number;\n    readonly CONNECTING: number;\n    readonly READY_STATE_OPEN: number;\n    readonly OPEN: number;\n    readonly READY_STATE_CLOSING: number;\n    readonly CLOSING: number;\n    readonly READY_STATE_CLOSED: number;\n    readonly CLOSED: number;\n};\n/**\n * The `WebSocket` object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket)\n */\ninterface WebSocket extends EventTarget<WebSocketEventMap> {\n    accept(): void;\n    /**\n     * The **`WebSocket.send()`** method enqueues the specified data to be transmitted to the server over the WebSocket connection, increasing the value of `bufferedAmount` by the number of bytes needed to contain the data.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/send)\n     */\n    send(message: (ArrayBuffer | ArrayBufferView) | string): void;\n    /**\n     * The **`WebSocket.close()`** method closes the already `CLOSED`, this method does nothing.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/close)\n     */\n    close(code?: number, reason?: string): void;\n    serializeAttachment(attachment: any): void;\n    deserializeAttachment(): any | null;\n    /**\n     * The **`WebSocket.readyState`** read-only property returns the current state of the WebSocket connection.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/readyState)\n     */\n    readyState: number;\n    /**\n     * The **`WebSocket.url`** read-only property returns the absolute URL of the WebSocket as resolved by the constructor.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/url)\n     */\n    url: string | null;\n    /**\n     * The **`WebSocket.protocol`** read-only property returns the name of the sub-protocol the server selected; this will be one of the strings specified in the `protocols` parameter when creating the WebSocket object, or the empty string if no connection is established.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/protocol)\n     */\n    protocol: string | null;\n    /**\n     * The **`WebSocket.extensions`** read-only property returns the extensions selected by the server.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/extensions)\n     */\n    extensions: string | null;\n}\ndeclare const WebSocketPair: {\n    new (): {\n        0: WebSocket;\n        1: WebSocket;\n    };\n};\ninterface SqlStorage {\n    exec<T extends Record<string, SqlStorageValue>>(query: string, ...bindings: any[]): SqlStorageCursor<T>;\n    get databaseSize(): number;\n    Cursor: typeof SqlStorageCursor;\n    Statement: typeof SqlStorageStatement;\n}\ndeclare abstract class SqlStorageStatement {\n}\ntype SqlStorageValue = ArrayBuffer | string | number | null;\ndeclare abstract class SqlStorageCursor<T extends Record<string, SqlStorageValue>> {\n    next(): {\n        done?: false;\n        value: T;\n    } | {\n        done: true;\n        value?: never;\n    };\n    toArray(): T[];\n    one(): T;\n    raw<U extends SqlStorageValue[]>(): IterableIterator<U>;\n    columnNames: string[];\n    get rowsRead(): number;\n    get rowsWritten(): number;\n    [Symbol.iterator](): IterableIterator<T>;\n}\ninterface Socket {\n    get readable(): ReadableStream;\n    get writable(): WritableStream;\n    get closed(): Promise<void>;\n    get opened(): Promise<SocketInfo>;\n    get upgraded(): boolean;\n    get secureTransport(): \"on\" | \"off\" | \"starttls\";\n    close(): Promise<void>;\n    startTls(options?: TlsOptions): Socket;\n}\ninterface SocketOptions {\n    secureTransport?: string;\n    allowHalfOpen: boolean;\n    highWaterMark?: (number | bigint);\n}\ninterface SocketAddress {\n    hostname: string;\n    port: number;\n}\ninterface TlsOptions {\n    expectedServerHostname?: string;\n}\ninterface SocketInfo {\n    remoteAddress?: string;\n    localAddress?: string;\n}\n/**\n * The **`EventSource`** interface is web content's interface to server-sent events.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource)\n */\ndeclare class EventSource extends EventTarget {\n    constructor(url: string, init?: EventSourceEventSourceInit);\n    /**\n     * The **`close()`** method of the EventSource interface closes the connection, if one is made, and sets the ```js-nolint close() ``` None.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/close)\n     */\n    close(): void;\n    /**\n     * The **`url`** read-only property of the URL of the source.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/url)\n     */\n    get url(): string;\n    /**\n     * The **`withCredentials`** read-only property of the the `EventSource` object was instantiated with CORS credentials set.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/withCredentials)\n     */\n    get withCredentials(): boolean;\n    /**\n     * The **`readyState`** read-only property of the connection.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/readyState)\n     */\n    get readyState(): number;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */\n    get onopen(): any | null;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */\n    set onopen(value: any | null);\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */\n    get onmessage(): any | null;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */\n    set onmessage(value: any | null);\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */\n    get onerror(): any | null;\n    /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */\n    set onerror(value: any | null);\n    static readonly CONNECTING: number;\n    static readonly OPEN: number;\n    static readonly CLOSED: number;\n    static from(stream: ReadableStream): EventSource;\n}\ninterface EventSourceEventSourceInit {\n    withCredentials?: boolean;\n    fetcher?: Fetcher;\n}\ninterface Container {\n    get running(): boolean;\n    start(options?: ContainerStartupOptions): void;\n    monitor(): Promise<void>;\n    destroy(error?: any): Promise<void>;\n    signal(signo: number): void;\n    getTcpPort(port: number): Fetcher;\n    setInactivityTimeout(durationMs: number | bigint): Promise<void>;\n}\ninterface ContainerStartupOptions {\n    entrypoint?: string[];\n    enableInternet: boolean;\n    env?: Record<string, string>;\n    hardTimeout?: (number | bigint);\n}\n/**\n * The **`MessagePort`** interface of the Channel Messaging API represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort)\n */\ndeclare abstract class MessagePort extends EventTarget {\n    /**\n     * The **`postMessage()`** method of the transfers ownership of objects to other browsing contexts.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/postMessage)\n     */\n    postMessage(data?: any, options?: (any[] | MessagePortPostMessageOptions)): void;\n    /**\n     * The **`close()`** method of the MessagePort interface disconnects the port, so it is no longer active.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/close)\n     */\n    close(): void;\n    /**\n     * The **`start()`** method of the MessagePort interface starts the sending of messages queued on the port.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/start)\n     */\n    start(): void;\n    get onmessage(): any | null;\n    set onmessage(value: any | null);\n}\n/**\n * The **`MessageChannel`** interface of the Channel Messaging API allows us to create a new message channel and send data through it via its two MessagePort properties.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel)\n */\ndeclare class MessageChannel {\n    constructor();\n    /**\n     * The **`port1`** read-only property of the the port attached to the context that originated the channel.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel/port1)\n     */\n    readonly port1: MessagePort;\n    /**\n     * The **`port2`** read-only property of the the port attached to the context at the other end of the channel, which the message is initially sent to.\n     *\n     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel/port2)\n     */\n    readonly port2: MessagePort;\n}\ninterface MessagePortPostMessageOptions {\n    transfer?: any[];\n}\ntype LoopbackForExport<T extends (new (...args: any[]) => Rpc.EntrypointBranded) | ExportedHandler<any, any, any> | undefined = undefined> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? LoopbackServiceStub<InstanceType<T>> : T extends new (...args: any[]) => Rpc.DurableObjectBranded ? LoopbackDurableObjectClass<InstanceType<T>> : T extends ExportedHandler<any, any, any> ? LoopbackServiceStub<undefined> : undefined;\ntype LoopbackServiceStub<T extends Rpc.WorkerEntrypointBranded | undefined = undefined> = Fetcher<T> & (T extends CloudflareWorkersModule.WorkerEntrypoint<any, infer Props> ? (opts: {\n    props?: Props;\n}) => Fetcher<T> : (opts: {\n    props?: any;\n}) => Fetcher<T>);\ntype LoopbackDurableObjectClass<T extends Rpc.DurableObjectBranded | undefined = undefined> = DurableObjectClass<T> & (T extends CloudflareWorkersModule.DurableObject<any, infer Props> ? (opts: {\n    props?: Props;\n}) => DurableObjectClass<T> : (opts: {\n    props?: any;\n}) => DurableObjectClass<T>);\ninterface SyncKvStorage {\n    get<T = unknown>(key: string): T | undefined;\n    list<T = unknown>(options?: SyncKvListOptions): Iterable<[\n        string,\n        T\n    ]>;\n    put<T>(key: string, value: T): void;\n    delete(key: string): boolean;\n}\ninterface SyncKvListOptions {\n    start?: string;\n    startAfter?: string;\n    end?: string;\n    prefix?: string;\n    reverse?: boolean;\n    limit?: number;\n}\ninterface WorkerStub {\n    getEntrypoint<T extends Rpc.WorkerEntrypointBranded | undefined>(name?: string, options?: WorkerStubEntrypointOptions): Fetcher<T>;\n}\ninterface WorkerStubEntrypointOptions {\n    props?: any;\n}\ninterface WorkerLoader {\n    get(name: string, getCode: () => WorkerLoaderWorkerCode | Promise<WorkerLoaderWorkerCode>): WorkerStub;\n}\ninterface WorkerLoaderModule {\n    js?: string;\n    cjs?: string;\n    text?: string;\n    data?: ArrayBuffer;\n    json?: any;\n    py?: string;\n    wasm?: ArrayBuffer;\n}\ninterface WorkerLoaderWorkerCode {\n    compatibilityDate: string;\n    compatibilityFlags?: string[];\n    allowExperimental?: boolean;\n    mainModule: string;\n    modules: Record<string, WorkerLoaderModule | string>;\n    env?: any;\n    globalOutbound?: (Fetcher | null);\n    tails?: Fetcher[];\n    streamingTails?: Fetcher[];\n}\n/**\n* The Workers runtime supports a subset of the Performance API, used to measure timing and performance,\n* as well as timing of subrequests and other operations.\n*\n* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)\n*/\ndeclare abstract class Performance {\n    /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancetimeorigin) */\n    get timeOrigin(): number;\n    /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */\n    now(): number;\n}\ntype AiImageClassificationInput = {\n    image: number[];\n};\ntype AiImageClassificationOutput = {\n    score?: number;\n    label?: string;\n}[];\ndeclare abstract class BaseAiImageClassification {\n    inputs: AiImageClassificationInput;\n    postProcessedOutputs: AiImageClassificationOutput;\n}\ntype AiImageToTextInput = {\n    image: number[];\n    prompt?: string;\n    max_tokens?: number;\n    temperature?: number;\n    top_p?: number;\n    top_k?: number;\n    seed?: number;\n    repetition_penalty?: number;\n    frequency_penalty?: number;\n    presence_penalty?: number;\n    raw?: boolean;\n    messages?: RoleScopedChatInput[];\n};\ntype AiImageToTextOutput = {\n    description: string;\n};\ndeclare abstract class BaseAiImageToText {\n    inputs: AiImageToTextInput;\n    postProcessedOutputs: AiImageToTextOutput;\n}\ntype AiImageTextToTextInput = {\n    image: string;\n    prompt?: string;\n    max_tokens?: number;\n    temperature?: number;\n    ignore_eos?: boolean;\n    top_p?: number;\n    top_k?: number;\n    seed?: number;\n    repetition_penalty?: number;\n    frequency_penalty?: number;\n    presence_penalty?: number;\n    raw?: boolean;\n    messages?: RoleScopedChatInput[];\n};\ntype AiImageTextToTextOutput = {\n    description: string;\n};\ndeclare abstract class BaseAiImageTextToText {\n    inputs: AiImageTextToTextInput;\n    postProcessedOutputs: AiImageTextToTextOutput;\n}\ntype AiMultimodalEmbeddingsInput = {\n    image: string;\n    text: string[];\n};\ntype AiIMultimodalEmbeddingsOutput = {\n    data: number[][];\n    shape: number[];\n};\ndeclare abstract class BaseAiMultimodalEmbeddings {\n    inputs: AiImageTextToTextInput;\n    postProcessedOutputs: AiImageTextToTextOutput;\n}\ntype AiObjectDetectionInput = {\n    image: number[];\n};\ntype AiObjectDetectionOutput = {\n    score?: number;\n    label?: string;\n}[];\ndeclare abstract class BaseAiObjectDetection {\n    inputs: AiObjectDetectionInput;\n    postProcessedOutputs: AiObjectDetectionOutput;\n}\ntype AiSentenceSimilarityInput = {\n    source: string;\n    sentences: string[];\n};\ntype AiSentenceSimilarityOutput = number[];\ndeclare abstract class BaseAiSentenceSimilarity {\n    inputs: AiSentenceSimilarityInput;\n    postProcessedOutputs: AiSentenceSimilarityOutput;\n}\ntype AiAutomaticSpeechRecognitionInput = {\n    audio: number[];\n};\ntype AiAutomaticSpeechRecognitionOutput = {\n    text?: string;\n    words?: {\n        word: string;\n        start: number;\n        end: number;\n    }[];\n    vtt?: string;\n};\ndeclare abstract class BaseAiAutomaticSpeechRecognition {\n    inputs: AiAutomaticSpeechRecognitionInput;\n    postProcessedOutputs: AiAutomaticSpeechRecognitionOutput;\n}\ntype AiSummarizationInput = {\n    input_text: string;\n    max_length?: number;\n};\ntype AiSummarizationOutput = {\n    summary: string;\n};\ndeclare abstract class BaseAiSummarization {\n    inputs: AiSummarizationInput;\n    postProcessedOutputs: AiSummarizationOutput;\n}\ntype AiTextClassificationInput = {\n    text: string;\n};\ntype AiTextClassificationOutput = {\n    score?: number;\n    label?: string;\n}[];\ndeclare abstract class BaseAiTextClassification {\n    inputs: AiTextClassificationInput;\n    postProcessedOutputs: AiTextClassificationOutput;\n}\ntype AiTextEmbeddingsInput = {\n    text: string | string[];\n};\ntype AiTextEmbeddingsOutput = {\n    shape: number[];\n    data: number[][];\n};\ndeclare abstract class BaseAiTextEmbeddings {\n    inputs: AiTextEmbeddingsInput;\n    postProcessedOutputs: AiTextEmbeddingsOutput;\n}\ntype RoleScopedChatInput = {\n    role: \"user\" | \"assistant\" | \"system\" | \"tool\" | (string & NonNullable<unknown>);\n    content: string;\n    name?: string;\n};\ntype AiTextGenerationToolLegacyInput = {\n    name: string;\n    description: string;\n    parameters?: {\n        type: \"object\" | (string & NonNullable<unknown>);\n        properties: {\n            [key: string]: {\n                type: string;\n                description?: string;\n            };\n        };\n        required: string[];\n    };\n};\ntype AiTextGenerationToolInput = {\n    type: \"function\" | (string & NonNullable<unknown>);\n    function: {\n        name: string;\n        description: string;\n        parameters?: {\n            type: \"object\" | (string & NonNullable<unknown>);\n            properties: {\n                [key: string]: {\n                    type: string;\n                    description?: string;\n                };\n            };\n            required: string[];\n        };\n    };\n};\ntype AiTextGenerationFunctionsInput = {\n    name: string;\n    code: string;\n};\ntype AiTextGenerationResponseFormat = {\n    type: string;\n    json_schema?: any;\n};\ntype AiTextGenerationInput = {\n    prompt?: string;\n    raw?: boolean;\n    stream?: boolean;\n    max_tokens?: number;\n    temperature?: number;\n    top_p?: number;\n    top_k?: number;\n    seed?: number;\n    repetition_penalty?: number;\n    frequency_penalty?: number;\n    presence_penalty?: number;\n    messages?: RoleScopedChatInput[];\n    response_format?: AiTextGenerationResponseFormat;\n    tools?: AiTextGenerationToolInput[] | AiTextGenerationToolLegacyInput[] | (object & NonNullable<unknown>);\n    functions?: AiTextGenerationFunctionsInput[];\n};\ntype AiTextGenerationToolLegacyOutput = {\n    name: string;\n    arguments: unknown;\n};\ntype AiTextGenerationToolOutput = {\n    id: string;\n    type: \"function\";\n    function: {\n        name: string;\n        arguments: string;\n    };\n};\ntype UsageTags = {\n    prompt_tokens: number;\n    completion_tokens: number;\n    total_tokens: number;\n};\ntype AiTextGenerationOutput = {\n    response?: string;\n    tool_calls?: AiTextGenerationToolLegacyOutput[] & AiTextGenerationToolOutput[];\n    usage?: UsageTags;\n};\ndeclare abstract class BaseAiTextGeneration {\n    inputs: AiTextGenerationInput;\n    postProcessedOutputs: AiTextGenerationOutput;\n}\ntype AiTextToSpeechInput = {\n    prompt: string;\n    lang?: string;\n};\ntype AiTextToSpeechOutput = Uint8Array | {\n    audio: string;\n};\ndeclare abstract class BaseAiTextToSpeech {\n    inputs: AiTextToSpeechInput;\n    postProcessedOutputs: AiTextToSpeechOutput;\n}\ntype AiTextToImageInput = {\n    prompt: string;\n    negative_prompt?: string;\n    height?: number;\n    width?: number;\n    image?: number[];\n    image_b64?: string;\n    mask?: number[];\n    num_steps?: number;\n    strength?: number;\n    guidance?: number;\n    seed?: number;\n};\ntype AiTextToImageOutput = ReadableStream<Uint8Array>;\ndeclare abstract class BaseAiTextToImage {\n    inputs: AiTextToImageInput;\n    postProcessedOutputs: AiTextToImageOutput;\n}\ntype AiTranslationInput = {\n    text: string;\n    target_lang: string;\n    source_lang?: string;\n};\ntype AiTranslationOutput = {\n    translated_text?: string;\n};\ndeclare abstract class BaseAiTranslation {\n    inputs: AiTranslationInput;\n    postProcessedOutputs: AiTranslationOutput;\n}\ntype Ai_Cf_Baai_Bge_Base_En_V1_5_Input = {\n    text: string | string[];\n    /**\n     * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy.\n     */\n    pooling?: \"mean\" | \"cls\";\n} | {\n    /**\n     * Batch of the embeddings requests to run using async-queue\n     */\n    requests: {\n        text: string | string[];\n        /**\n         * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy.\n         */\n        pooling?: \"mean\" | \"cls\";\n    }[];\n};\ntype Ai_Cf_Baai_Bge_Base_En_V1_5_Output = {\n    shape?: number[];\n    /**\n     * Embeddings of the requested text values\n     */\n    data?: number[][];\n    /**\n     * The pooling method used in the embedding process.\n     */\n    pooling?: \"mean\" | \"cls\";\n} | AsyncResponse;\ninterface AsyncResponse {\n    /**\n     * The async request id that can be used to obtain the results.\n     */\n    request_id?: string;\n}\ndeclare abstract class Base_Ai_Cf_Baai_Bge_Base_En_V1_5 {\n    inputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Input;\n    postProcessedOutputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Output;\n}\ntype Ai_Cf_Openai_Whisper_Input = string | {\n    /**\n     * An array of integers that represent the audio data constrained to 8-bit unsigned integer values\n     */\n    audio: number[];\n};\ninterface Ai_Cf_Openai_Whisper_Output {\n    /**\n     * The transcription\n     */\n    text: string;\n    word_count?: number;\n    words?: {\n        word?: string;\n        /**\n         * The second this word begins in the recording\n         */\n        start?: number;\n        /**\n         * The ending second when the word completes\n         */\n        end?: number;\n    }[];\n    vtt?: string;\n}\ndeclare abstract class Base_Ai_Cf_Openai_Whisper {\n    inputs: Ai_Cf_Openai_Whisper_Input;\n    postProcessedOutputs: Ai_Cf_Openai_Whisper_Output;\n}\ntype Ai_Cf_Meta_M2M100_1_2B_Input = {\n    /**\n     * The text to be translated\n     */\n    text: string;\n    /**\n     * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified\n     */\n    source_lang?: string;\n    /**\n     * The language code to translate the text into (e.g., 'es' for Spanish)\n     */\n    target_lang: string;\n} | {\n    /**\n     * Batch of the embeddings requests to run using async-queue\n     */\n    requests: {\n        /**\n         * The text to be translated\n         */\n        text: string;\n        /**\n         * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified\n         */\n        source_lang?: string;\n        /**\n         * The language code to translate the text into (e.g., 'es' for Spanish)\n         */\n        target_lang: string;\n    }[];\n};\ntype Ai_Cf_Meta_M2M100_1_2B_Output = {\n    /**\n     * The translated text in the target language\n     */\n    translated_text?: string;\n} | AsyncResponse;\ndeclare abstract class Base_Ai_Cf_Meta_M2M100_1_2B {\n    inputs: Ai_Cf_Meta_M2M100_1_2B_Input;\n    postProcessedOutputs: Ai_Cf_Meta_M2M100_1_2B_Output;\n}\ntype Ai_Cf_Baai_Bge_Small_En_V1_5_Input = {\n    text: string | string[];\n    /**\n     * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy.\n     */\n    pooling?: \"mean\" | \"cls\";\n} | {\n    /**\n     * Batch of the embeddings requests to run using async-queue\n     */\n    requests: {\n        text: string | string[];\n        /**\n         * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy.\n         */\n        pooling?: \"mean\" | \"cls\";\n    }[];\n};\ntype Ai_Cf_Baai_Bge_Small_En_V1_5_Output = {\n    shape?: number[];\n    /**\n     * Embeddings of the requested text values\n     */\n    data?: number[][];\n    /**\n     * The pooling method used in the embedding process.\n     */\n    pooling?: \"mean\" | \"cls\";\n} | AsyncResponse;\ndeclare abstract class Base_Ai_Cf_Baai_Bge_Small_En_V1_5 {\n    inputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Input;\n    postProcessedOutputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Output;\n}\ntype Ai_Cf_Baai_Bge_Large_En_V1_5_Input = {\n    text: string | string[];\n    /**\n     * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy.\n     */\n    pooling?: \"mean\" | \"cls\";\n} | {\n    /**\n     * Batch of the embeddings requests to run using async-queue\n     */\n    requests: {\n        text: string | string[];\n        /**\n         * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy.\n         */\n        pooling?: \"mean\" | \"cls\";\n    }[];\n};\ntype Ai_Cf_Baai_Bge_Large_En_V1_5_Output = {\n    shape?: number[];\n    /**\n     * Embeddings of the requested text values\n     */\n    data?: number[][];\n    /**\n     * The pooling method used in the embedding process.\n     */\n    pooling?: \"mean\" | \"cls\";\n} | AsyncResponse;\ndeclare abstract class Base_Ai_Cf_Baai_Bge_Large_En_V1_5 {\n    inputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Input;\n    postProcessedOutputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Output;\n}\ntype Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input = string | {\n    /**\n     * The input text prompt for the model to generate a response.\n     */\n    prompt?: string;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n    image: number[] | (string & NonNullable<unknown>);\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n};\ninterface Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output {\n    description?: string;\n}\ndeclare abstract class Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M {\n    inputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input;\n    postProcessedOutputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output;\n}\ntype Ai_Cf_Openai_Whisper_Tiny_En_Input = string | {\n    /**\n     * An array of integers that represent the audio data constrained to 8-bit unsigned integer values\n     */\n    audio: number[];\n};\ninterface Ai_Cf_Openai_Whisper_Tiny_En_Output {\n    /**\n     * The transcription\n     */\n    text: string;\n    word_count?: number;\n    words?: {\n        word?: string;\n        /**\n         * The second this word begins in the recording\n         */\n        start?: number;\n        /**\n         * The ending second when the word completes\n         */\n        end?: number;\n    }[];\n    vtt?: string;\n}\ndeclare abstract class Base_Ai_Cf_Openai_Whisper_Tiny_En {\n    inputs: Ai_Cf_Openai_Whisper_Tiny_En_Input;\n    postProcessedOutputs: Ai_Cf_Openai_Whisper_Tiny_En_Output;\n}\ninterface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input {\n    /**\n     * Base64 encoded value of the audio data.\n     */\n    audio: string;\n    /**\n     * Supported tasks are 'translate' or 'transcribe'.\n     */\n    task?: string;\n    /**\n     * The language of the audio being transcribed or translated.\n     */\n    language?: string;\n    /**\n     * Preprocess the audio with a voice activity detection model.\n     */\n    vad_filter?: boolean;\n    /**\n     * A text prompt to help provide context to the model on the contents of the audio.\n     */\n    initial_prompt?: string;\n    /**\n     * The prefix it appended the the beginning of the output of the transcription and can guide the transcription result.\n     */\n    prefix?: string;\n}\ninterface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output {\n    transcription_info?: {\n        /**\n         * The language of the audio being transcribed or translated.\n         */\n        language?: string;\n        /**\n         * The confidence level or probability of the detected language being accurate, represented as a decimal between 0 and 1.\n         */\n        language_probability?: number;\n        /**\n         * The total duration of the original audio file, in seconds.\n         */\n        duration?: number;\n        /**\n         * The duration of the audio after applying Voice Activity Detection (VAD) to remove silent or irrelevant sections, in seconds.\n         */\n        duration_after_vad?: number;\n    };\n    /**\n     * The complete transcription of the audio.\n     */\n    text: string;\n    /**\n     * The total number of words in the transcription.\n     */\n    word_count?: number;\n    segments?: {\n        /**\n         * The starting time of the segment within the audio, in seconds.\n         */\n        start?: number;\n        /**\n         * The ending time of the segment within the audio, in seconds.\n         */\n        end?: number;\n        /**\n         * The transcription of the segment.\n         */\n        text?: string;\n        /**\n         * The temperature used in the decoding process, controlling randomness in predictions. Lower values result in more deterministic outputs.\n         */\n        temperature?: number;\n        /**\n         * The average log probability of the predictions for the words in this segment, indicating overall confidence.\n         */\n        avg_logprob?: number;\n        /**\n         * The compression ratio of the input to the output, measuring how much the text was compressed during the transcription process.\n         */\n        compression_ratio?: number;\n        /**\n         * The probability that the segment contains no speech, represented as a decimal between 0 and 1.\n         */\n        no_speech_prob?: number;\n        words?: {\n            /**\n             * The individual word transcribed from the audio.\n             */\n            word?: string;\n            /**\n             * The starting time of the word within the audio, in seconds.\n             */\n            start?: number;\n            /**\n             * The ending time of the word within the audio, in seconds.\n             */\n            end?: number;\n        }[];\n    }[];\n    /**\n     * The transcription in WebVTT format, which includes timing and text information for use in subtitles.\n     */\n    vtt?: string;\n}\ndeclare abstract class Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo {\n    inputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input;\n    postProcessedOutputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output;\n}\ntype Ai_Cf_Baai_Bge_M3_Input = BGEM3InputQueryAndContexts | BGEM3InputEmbedding | {\n    /**\n     * Batch of the embeddings requests to run using async-queue\n     */\n    requests: (BGEM3InputQueryAndContexts1 | BGEM3InputEmbedding1)[];\n};\ninterface BGEM3InputQueryAndContexts {\n    /**\n     * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts\n     */\n    query?: string;\n    /**\n     * List of provided contexts. Note that the index in this array is important, as the response will refer to it.\n     */\n    contexts: {\n        /**\n         * One of the provided context content\n         */\n        text?: string;\n    }[];\n    /**\n     * When provided with too long context should the model error out or truncate the context to fit?\n     */\n    truncate_inputs?: boolean;\n}\ninterface BGEM3InputEmbedding {\n    text: string | string[];\n    /**\n     * When provided with too long context should the model error out or truncate the context to fit?\n     */\n    truncate_inputs?: boolean;\n}\ninterface BGEM3InputQueryAndContexts1 {\n    /**\n     * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts\n     */\n    query?: string;\n    /**\n     * List of provided contexts. Note that the index in this array is important, as the response will refer to it.\n     */\n    contexts: {\n        /**\n         * One of the provided context content\n         */\n        text?: string;\n    }[];\n    /**\n     * When provided with too long context should the model error out or truncate the context to fit?\n     */\n    truncate_inputs?: boolean;\n}\ninterface BGEM3InputEmbedding1 {\n    text: string | string[];\n    /**\n     * When provided with too long context should the model error out or truncate the context to fit?\n     */\n    truncate_inputs?: boolean;\n}\ntype Ai_Cf_Baai_Bge_M3_Output = BGEM3OuputQuery | BGEM3OutputEmbeddingForContexts | BGEM3OuputEmbedding | AsyncResponse;\ninterface BGEM3OuputQuery {\n    response?: {\n        /**\n         * Index of the context in the request\n         */\n        id?: number;\n        /**\n         * Score of the context under the index.\n         */\n        score?: number;\n    }[];\n}\ninterface BGEM3OutputEmbeddingForContexts {\n    response?: number[][];\n    shape?: number[];\n    /**\n     * The pooling method used in the embedding process.\n     */\n    pooling?: \"mean\" | \"cls\";\n}\ninterface BGEM3OuputEmbedding {\n    shape?: number[];\n    /**\n     * Embeddings of the requested text values\n     */\n    data?: number[][];\n    /**\n     * The pooling method used in the embedding process.\n     */\n    pooling?: \"mean\" | \"cls\";\n}\ndeclare abstract class Base_Ai_Cf_Baai_Bge_M3 {\n    inputs: Ai_Cf_Baai_Bge_M3_Input;\n    postProcessedOutputs: Ai_Cf_Baai_Bge_M3_Output;\n}\ninterface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input {\n    /**\n     * A text description of the image you want to generate.\n     */\n    prompt: string;\n    /**\n     * The number of diffusion steps; higher values can improve quality but take longer.\n     */\n    steps?: number;\n}\ninterface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output {\n    /**\n     * The generated image in Base64 format.\n     */\n    image?: string;\n}\ndeclare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell {\n    inputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input;\n    postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output;\n}\ntype Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input = Prompt | Messages;\ninterface Prompt {\n    /**\n     * The input text prompt for the model to generate a response.\n     */\n    prompt: string;\n    image?: number[] | (string & NonNullable<unknown>);\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n    /**\n     * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model.\n     */\n    lora?: string;\n}\ninterface Messages {\n    /**\n     * An array of message objects representing the conversation history.\n     */\n    messages: {\n        /**\n         * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool').\n         */\n        role?: string;\n        /**\n         * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001\n         */\n        tool_call_id?: string;\n        content?: string | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        }[] | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        };\n    }[];\n    image?: number[] | (string & NonNullable<unknown>);\n    functions?: {\n        name: string;\n        code: string;\n    }[];\n    /**\n     * A list of tools available for the assistant to use.\n     */\n    tools?: ({\n        /**\n         * The name of the tool. More descriptive the better.\n         */\n        name: string;\n        /**\n         * A brief description of what the tool does.\n         */\n        description: string;\n        /**\n         * Schema defining the parameters accepted by the tool.\n         */\n        parameters: {\n            /**\n             * The type of the parameters object (usually 'object').\n             */\n            type: string;\n            /**\n             * List of required parameter names.\n             */\n            required?: string[];\n            /**\n             * Definitions of each parameter.\n             */\n            properties: {\n                [k: string]: {\n                    /**\n                     * The data type of the parameter.\n                     */\n                    type: string;\n                    /**\n                     * A description of the expected parameter.\n                     */\n                    description: string;\n                };\n            };\n        };\n    } | {\n        /**\n         * Specifies the type of tool (e.g., 'function').\n         */\n        type: string;\n        /**\n         * Details of the function tool.\n         */\n        function: {\n            /**\n             * The name of the function.\n             */\n            name: string;\n            /**\n             * A brief description of what the function does.\n             */\n            description: string;\n            /**\n             * Schema defining the parameters accepted by the function.\n             */\n            parameters: {\n                /**\n                 * The type of the parameters object (usually 'object').\n                 */\n                type: string;\n                /**\n                 * List of required parameter names.\n                 */\n                required?: string[];\n                /**\n                 * Definitions of each parameter.\n                 */\n                properties: {\n                    [k: string]: {\n                        /**\n                         * The data type of the parameter.\n                         */\n                        type: string;\n                        /**\n                         * A description of the expected parameter.\n                         */\n                        description: string;\n                    };\n                };\n            };\n        };\n    })[];\n    /**\n     * If true, the response will be streamed back incrementally.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ntype Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output = {\n    /**\n     * The generated text response from the model\n     */\n    response?: string;\n    /**\n     * An array of tool calls requests made during the response generation\n     */\n    tool_calls?: {\n        /**\n         * The arguments passed to be passed to the tool call request\n         */\n        arguments?: object;\n        /**\n         * The name of the tool to be called\n         */\n        name?: string;\n    }[];\n};\ndeclare abstract class Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct {\n    inputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input;\n    postProcessedOutputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output;\n}\ntype Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input = Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt | Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages | AsyncBatch;\ninterface Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt {\n    /**\n     * The input text prompt for the model to generate a response.\n     */\n    prompt: string;\n    /**\n     * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model.\n     */\n    lora?: string;\n    response_format?: JSONMode;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ninterface JSONMode {\n    type?: \"json_object\" | \"json_schema\";\n    json_schema?: unknown;\n}\ninterface Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages {\n    /**\n     * An array of message objects representing the conversation history.\n     */\n    messages: {\n        /**\n         * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool').\n         */\n        role: string;\n        /**\n         * The content of the message as a string.\n         */\n        content: string;\n    }[];\n    functions?: {\n        name: string;\n        code: string;\n    }[];\n    /**\n     * A list of tools available for the assistant to use.\n     */\n    tools?: ({\n        /**\n         * The name of the tool. More descriptive the better.\n         */\n        name: string;\n        /**\n         * A brief description of what the tool does.\n         */\n        description: string;\n        /**\n         * Schema defining the parameters accepted by the tool.\n         */\n        parameters: {\n            /**\n             * The type of the parameters object (usually 'object').\n             */\n            type: string;\n            /**\n             * List of required parameter names.\n             */\n            required?: string[];\n            /**\n             * Definitions of each parameter.\n             */\n            properties: {\n                [k: string]: {\n                    /**\n                     * The data type of the parameter.\n                     */\n                    type: string;\n                    /**\n                     * A description of the expected parameter.\n                     */\n                    description: string;\n                };\n            };\n        };\n    } | {\n        /**\n         * Specifies the type of tool (e.g., 'function').\n         */\n        type: string;\n        /**\n         * Details of the function tool.\n         */\n        function: {\n            /**\n             * The name of the function.\n             */\n            name: string;\n            /**\n             * A brief description of what the function does.\n             */\n            description: string;\n            /**\n             * Schema defining the parameters accepted by the function.\n             */\n            parameters: {\n                /**\n                 * The type of the parameters object (usually 'object').\n                 */\n                type: string;\n                /**\n                 * List of required parameter names.\n                 */\n                required?: string[];\n                /**\n                 * Definitions of each parameter.\n                 */\n                properties: {\n                    [k: string]: {\n                        /**\n                         * The data type of the parameter.\n                         */\n                        type: string;\n                        /**\n                         * A description of the expected parameter.\n                         */\n                        description: string;\n                    };\n                };\n            };\n        };\n    })[];\n    response_format?: JSONMode;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ninterface AsyncBatch {\n    requests?: {\n        /**\n         * User-supplied reference. This field will be present in the response as well it can be used to reference the request and response. It's NOT validated to be unique.\n         */\n        external_reference?: string;\n        /**\n         * Prompt for the text generation model\n         */\n        prompt?: string;\n        /**\n         * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n         */\n        stream?: boolean;\n        /**\n         * The maximum number of tokens to generate in the response.\n         */\n        max_tokens?: number;\n        /**\n         * Controls the randomness of the output; higher values produce more random results.\n         */\n        temperature?: number;\n        /**\n         * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n         */\n        top_p?: number;\n        /**\n         * Random seed for reproducibility of the generation.\n         */\n        seed?: number;\n        /**\n         * Penalty for repeated tokens; higher values discourage repetition.\n         */\n        repetition_penalty?: number;\n        /**\n         * Decreases the likelihood of the model repeating the same lines verbatim.\n         */\n        frequency_penalty?: number;\n        /**\n         * Increases the likelihood of the model introducing new topics.\n         */\n        presence_penalty?: number;\n        response_format?: JSONMode;\n    }[];\n}\ntype Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output = {\n    /**\n     * The generated text response from the model\n     */\n    response: string;\n    /**\n     * Usage statistics for the inference request\n     */\n    usage?: {\n        /**\n         * Total number of tokens in input\n         */\n        prompt_tokens?: number;\n        /**\n         * Total number of tokens in output\n         */\n        completion_tokens?: number;\n        /**\n         * Total number of input and output tokens\n         */\n        total_tokens?: number;\n    };\n    /**\n     * An array of tool calls requests made during the response generation\n     */\n    tool_calls?: {\n        /**\n         * The arguments passed to be passed to the tool call request\n         */\n        arguments?: object;\n        /**\n         * The name of the tool to be called\n         */\n        name?: string;\n    }[];\n} | string | AsyncResponse;\ndeclare abstract class Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast {\n    inputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input;\n    postProcessedOutputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output;\n}\ninterface Ai_Cf_Meta_Llama_Guard_3_8B_Input {\n    /**\n     * An array of message objects representing the conversation history.\n     */\n    messages: {\n        /**\n         * The role of the message sender must alternate between 'user' and 'assistant'.\n         */\n        role: \"user\" | \"assistant\";\n        /**\n         * The content of the message as a string.\n         */\n        content: string;\n    }[];\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Dictate the output format of the generated response.\n     */\n    response_format?: {\n        /**\n         * Set to json_object to process and output generated text as JSON.\n         */\n        type?: string;\n    };\n}\ninterface Ai_Cf_Meta_Llama_Guard_3_8B_Output {\n    response?: string | {\n        /**\n         * Whether the conversation is safe or not.\n         */\n        safe?: boolean;\n        /**\n         * A list of what hazard categories predicted for the conversation, if the conversation is deemed unsafe.\n         */\n        categories?: string[];\n    };\n    /**\n     * Usage statistics for the inference request\n     */\n    usage?: {\n        /**\n         * Total number of tokens in input\n         */\n        prompt_tokens?: number;\n        /**\n         * Total number of tokens in output\n         */\n        completion_tokens?: number;\n        /**\n         * Total number of input and output tokens\n         */\n        total_tokens?: number;\n    };\n}\ndeclare abstract class Base_Ai_Cf_Meta_Llama_Guard_3_8B {\n    inputs: Ai_Cf_Meta_Llama_Guard_3_8B_Input;\n    postProcessedOutputs: Ai_Cf_Meta_Llama_Guard_3_8B_Output;\n}\ninterface Ai_Cf_Baai_Bge_Reranker_Base_Input {\n    /**\n     * A query you wish to perform against the provided contexts.\n     */\n    /**\n     * Number of returned results starting with the best score.\n     */\n    top_k?: number;\n    /**\n     * List of provided contexts. Note that the index in this array is important, as the response will refer to it.\n     */\n    contexts: {\n        /**\n         * One of the provided context content\n         */\n        text?: string;\n    }[];\n}\ninterface Ai_Cf_Baai_Bge_Reranker_Base_Output {\n    response?: {\n        /**\n         * Index of the context in the request\n         */\n        id?: number;\n        /**\n         * Score of the context under the index.\n         */\n        score?: number;\n    }[];\n}\ndeclare abstract class Base_Ai_Cf_Baai_Bge_Reranker_Base {\n    inputs: Ai_Cf_Baai_Bge_Reranker_Base_Input;\n    postProcessedOutputs: Ai_Cf_Baai_Bge_Reranker_Base_Output;\n}\ntype Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input = Qwen2_5_Coder_32B_Instruct_Prompt | Qwen2_5_Coder_32B_Instruct_Messages;\ninterface Qwen2_5_Coder_32B_Instruct_Prompt {\n    /**\n     * The input text prompt for the model to generate a response.\n     */\n    prompt: string;\n    /**\n     * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model.\n     */\n    lora?: string;\n    response_format?: JSONMode;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ninterface Qwen2_5_Coder_32B_Instruct_Messages {\n    /**\n     * An array of message objects representing the conversation history.\n     */\n    messages: {\n        /**\n         * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool').\n         */\n        role: string;\n        /**\n         * The content of the message as a string.\n         */\n        content: string;\n    }[];\n    functions?: {\n        name: string;\n        code: string;\n    }[];\n    /**\n     * A list of tools available for the assistant to use.\n     */\n    tools?: ({\n        /**\n         * The name of the tool. More descriptive the better.\n         */\n        name: string;\n        /**\n         * A brief description of what the tool does.\n         */\n        description: string;\n        /**\n         * Schema defining the parameters accepted by the tool.\n         */\n        parameters: {\n            /**\n             * The type of the parameters object (usually 'object').\n             */\n            type: string;\n            /**\n             * List of required parameter names.\n             */\n            required?: string[];\n            /**\n             * Definitions of each parameter.\n             */\n            properties: {\n                [k: string]: {\n                    /**\n                     * The data type of the parameter.\n                     */\n                    type: string;\n                    /**\n                     * A description of the expected parameter.\n                     */\n                    description: string;\n                };\n            };\n        };\n    } | {\n        /**\n         * Specifies the type of tool (e.g., 'function').\n         */\n        type: string;\n        /**\n         * Details of the function tool.\n         */\n        function: {\n            /**\n             * The name of the function.\n             */\n            name: string;\n            /**\n             * A brief description of what the function does.\n             */\n            description: string;\n            /**\n             * Schema defining the parameters accepted by the function.\n             */\n            parameters: {\n                /**\n                 * The type of the parameters object (usually 'object').\n                 */\n                type: string;\n                /**\n                 * List of required parameter names.\n                 */\n                required?: string[];\n                /**\n                 * Definitions of each parameter.\n                 */\n                properties: {\n                    [k: string]: {\n                        /**\n                         * The data type of the parameter.\n                         */\n                        type: string;\n                        /**\n                         * A description of the expected parameter.\n                         */\n                        description: string;\n                    };\n                };\n            };\n        };\n    })[];\n    response_format?: JSONMode;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ntype Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output = {\n    /**\n     * The generated text response from the model\n     */\n    response: string;\n    /**\n     * Usage statistics for the inference request\n     */\n    usage?: {\n        /**\n         * Total number of tokens in input\n         */\n        prompt_tokens?: number;\n        /**\n         * Total number of tokens in output\n         */\n        completion_tokens?: number;\n        /**\n         * Total number of input and output tokens\n         */\n        total_tokens?: number;\n    };\n    /**\n     * An array of tool calls requests made during the response generation\n     */\n    tool_calls?: {\n        /**\n         * The arguments passed to be passed to the tool call request\n         */\n        arguments?: object;\n        /**\n         * The name of the tool to be called\n         */\n        name?: string;\n    }[];\n};\ndeclare abstract class Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct {\n    inputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input;\n    postProcessedOutputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output;\n}\ntype Ai_Cf_Qwen_Qwq_32B_Input = Qwen_Qwq_32B_Prompt | Qwen_Qwq_32B_Messages;\ninterface Qwen_Qwq_32B_Prompt {\n    /**\n     * The input text prompt for the model to generate a response.\n     */\n    prompt: string;\n    /**\n     * JSON schema that should be fulfilled for the response.\n     */\n    guided_json?: object;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ninterface Qwen_Qwq_32B_Messages {\n    /**\n     * An array of message objects representing the conversation history.\n     */\n    messages: {\n        /**\n         * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool').\n         */\n        role?: string;\n        /**\n         * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001\n         */\n        tool_call_id?: string;\n        content?: string | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        }[] | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        };\n    }[];\n    functions?: {\n        name: string;\n        code: string;\n    }[];\n    /**\n     * A list of tools available for the assistant to use.\n     */\n    tools?: ({\n        /**\n         * The name of the tool. More descriptive the better.\n         */\n        name: string;\n        /**\n         * A brief description of what the tool does.\n         */\n        description: string;\n        /**\n         * Schema defining the parameters accepted by the tool.\n         */\n        parameters: {\n            /**\n             * The type of the parameters object (usually 'object').\n             */\n            type: string;\n            /**\n             * List of required parameter names.\n             */\n            required?: string[];\n            /**\n             * Definitions of each parameter.\n             */\n            properties: {\n                [k: string]: {\n                    /**\n                     * The data type of the parameter.\n                     */\n                    type: string;\n                    /**\n                     * A description of the expected parameter.\n                     */\n                    description: string;\n                };\n            };\n        };\n    } | {\n        /**\n         * Specifies the type of tool (e.g., 'function').\n         */\n        type: string;\n        /**\n         * Details of the function tool.\n         */\n        function: {\n            /**\n             * The name of the function.\n             */\n            name: string;\n            /**\n             * A brief description of what the function does.\n             */\n            description: string;\n            /**\n             * Schema defining the parameters accepted by the function.\n             */\n            parameters: {\n                /**\n                 * The type of the parameters object (usually 'object').\n                 */\n                type: string;\n                /**\n                 * List of required parameter names.\n                 */\n                required?: string[];\n                /**\n                 * Definitions of each parameter.\n                 */\n                properties: {\n                    [k: string]: {\n                        /**\n                         * The data type of the parameter.\n                         */\n                        type: string;\n                        /**\n                         * A description of the expected parameter.\n                         */\n                        description: string;\n                    };\n                };\n            };\n        };\n    })[];\n    /**\n     * JSON schema that should be fufilled for the response.\n     */\n    guided_json?: object;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ntype Ai_Cf_Qwen_Qwq_32B_Output = {\n    /**\n     * The generated text response from the model\n     */\n    response: string;\n    /**\n     * Usage statistics for the inference request\n     */\n    usage?: {\n        /**\n         * Total number of tokens in input\n         */\n        prompt_tokens?: number;\n        /**\n         * Total number of tokens in output\n         */\n        completion_tokens?: number;\n        /**\n         * Total number of input and output tokens\n         */\n        total_tokens?: number;\n    };\n    /**\n     * An array of tool calls requests made during the response generation\n     */\n    tool_calls?: {\n        /**\n         * The arguments passed to be passed to the tool call request\n         */\n        arguments?: object;\n        /**\n         * The name of the tool to be called\n         */\n        name?: string;\n    }[];\n};\ndeclare abstract class Base_Ai_Cf_Qwen_Qwq_32B {\n    inputs: Ai_Cf_Qwen_Qwq_32B_Input;\n    postProcessedOutputs: Ai_Cf_Qwen_Qwq_32B_Output;\n}\ntype Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input = Mistral_Small_3_1_24B_Instruct_Prompt | Mistral_Small_3_1_24B_Instruct_Messages;\ninterface Mistral_Small_3_1_24B_Instruct_Prompt {\n    /**\n     * The input text prompt for the model to generate a response.\n     */\n    prompt: string;\n    /**\n     * JSON schema that should be fulfilled for the response.\n     */\n    guided_json?: object;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ninterface Mistral_Small_3_1_24B_Instruct_Messages {\n    /**\n     * An array of message objects representing the conversation history.\n     */\n    messages: {\n        /**\n         * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool').\n         */\n        role?: string;\n        /**\n         * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001\n         */\n        tool_call_id?: string;\n        content?: string | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        }[] | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        };\n    }[];\n    functions?: {\n        name: string;\n        code: string;\n    }[];\n    /**\n     * A list of tools available for the assistant to use.\n     */\n    tools?: ({\n        /**\n         * The name of the tool. More descriptive the better.\n         */\n        name: string;\n        /**\n         * A brief description of what the tool does.\n         */\n        description: string;\n        /**\n         * Schema defining the parameters accepted by the tool.\n         */\n        parameters: {\n            /**\n             * The type of the parameters object (usually 'object').\n             */\n            type: string;\n            /**\n             * List of required parameter names.\n             */\n            required?: string[];\n            /**\n             * Definitions of each parameter.\n             */\n            properties: {\n                [k: string]: {\n                    /**\n                     * The data type of the parameter.\n                     */\n                    type: string;\n                    /**\n                     * A description of the expected parameter.\n                     */\n                    description: string;\n                };\n            };\n        };\n    } | {\n        /**\n         * Specifies the type of tool (e.g., 'function').\n         */\n        type: string;\n        /**\n         * Details of the function tool.\n         */\n        function: {\n            /**\n             * The name of the function.\n             */\n            name: string;\n            /**\n             * A brief description of what the function does.\n             */\n            description: string;\n            /**\n             * Schema defining the parameters accepted by the function.\n             */\n            parameters: {\n                /**\n                 * The type of the parameters object (usually 'object').\n                 */\n                type: string;\n                /**\n                 * List of required parameter names.\n                 */\n                required?: string[];\n                /**\n                 * Definitions of each parameter.\n                 */\n                properties: {\n                    [k: string]: {\n                        /**\n                         * The data type of the parameter.\n                         */\n                        type: string;\n                        /**\n                         * A description of the expected parameter.\n                         */\n                        description: string;\n                    };\n                };\n            };\n        };\n    })[];\n    /**\n     * JSON schema that should be fufilled for the response.\n     */\n    guided_json?: object;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ntype Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output = {\n    /**\n     * The generated text response from the model\n     */\n    response: string;\n    /**\n     * Usage statistics for the inference request\n     */\n    usage?: {\n        /**\n         * Total number of tokens in input\n         */\n        prompt_tokens?: number;\n        /**\n         * Total number of tokens in output\n         */\n        completion_tokens?: number;\n        /**\n         * Total number of input and output tokens\n         */\n        total_tokens?: number;\n    };\n    /**\n     * An array of tool calls requests made during the response generation\n     */\n    tool_calls?: {\n        /**\n         * The arguments passed to be passed to the tool call request\n         */\n        arguments?: object;\n        /**\n         * The name of the tool to be called\n         */\n        name?: string;\n    }[];\n};\ndeclare abstract class Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct {\n    inputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input;\n    postProcessedOutputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output;\n}\ntype Ai_Cf_Google_Gemma_3_12B_It_Input = Google_Gemma_3_12B_It_Prompt | Google_Gemma_3_12B_It_Messages;\ninterface Google_Gemma_3_12B_It_Prompt {\n    /**\n     * The input text prompt for the model to generate a response.\n     */\n    prompt: string;\n    /**\n     * JSON schema that should be fufilled for the response.\n     */\n    guided_json?: object;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ninterface Google_Gemma_3_12B_It_Messages {\n    /**\n     * An array of message objects representing the conversation history.\n     */\n    messages: {\n        /**\n         * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool').\n         */\n        role?: string;\n        content?: string | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        }[] | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        };\n    }[];\n    functions?: {\n        name: string;\n        code: string;\n    }[];\n    /**\n     * A list of tools available for the assistant to use.\n     */\n    tools?: ({\n        /**\n         * The name of the tool. More descriptive the better.\n         */\n        name: string;\n        /**\n         * A brief description of what the tool does.\n         */\n        description: string;\n        /**\n         * Schema defining the parameters accepted by the tool.\n         */\n        parameters: {\n            /**\n             * The type of the parameters object (usually 'object').\n             */\n            type: string;\n            /**\n             * List of required parameter names.\n             */\n            required?: string[];\n            /**\n             * Definitions of each parameter.\n             */\n            properties: {\n                [k: string]: {\n                    /**\n                     * The data type of the parameter.\n                     */\n                    type: string;\n                    /**\n                     * A description of the expected parameter.\n                     */\n                    description: string;\n                };\n            };\n        };\n    } | {\n        /**\n         * Specifies the type of tool (e.g., 'function').\n         */\n        type: string;\n        /**\n         * Details of the function tool.\n         */\n        function: {\n            /**\n             * The name of the function.\n             */\n            name: string;\n            /**\n             * A brief description of what the function does.\n             */\n            description: string;\n            /**\n             * Schema defining the parameters accepted by the function.\n             */\n            parameters: {\n                /**\n                 * The type of the parameters object (usually 'object').\n                 */\n                type: string;\n                /**\n                 * List of required parameter names.\n                 */\n                required?: string[];\n                /**\n                 * Definitions of each parameter.\n                 */\n                properties: {\n                    [k: string]: {\n                        /**\n                         * The data type of the parameter.\n                         */\n                        type: string;\n                        /**\n                         * A description of the expected parameter.\n                         */\n                        description: string;\n                    };\n                };\n            };\n        };\n    })[];\n    /**\n     * JSON schema that should be fufilled for the response.\n     */\n    guided_json?: object;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ntype Ai_Cf_Google_Gemma_3_12B_It_Output = {\n    /**\n     * The generated text response from the model\n     */\n    response: string;\n    /**\n     * Usage statistics for the inference request\n     */\n    usage?: {\n        /**\n         * Total number of tokens in input\n         */\n        prompt_tokens?: number;\n        /**\n         * Total number of tokens in output\n         */\n        completion_tokens?: number;\n        /**\n         * Total number of input and output tokens\n         */\n        total_tokens?: number;\n    };\n    /**\n     * An array of tool calls requests made during the response generation\n     */\n    tool_calls?: {\n        /**\n         * The arguments passed to be passed to the tool call request\n         */\n        arguments?: object;\n        /**\n         * The name of the tool to be called\n         */\n        name?: string;\n    }[];\n};\ndeclare abstract class Base_Ai_Cf_Google_Gemma_3_12B_It {\n    inputs: Ai_Cf_Google_Gemma_3_12B_It_Input;\n    postProcessedOutputs: Ai_Cf_Google_Gemma_3_12B_It_Output;\n}\ntype Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input = Ai_Cf_Meta_Llama_4_Prompt | Ai_Cf_Meta_Llama_4_Messages | Ai_Cf_Meta_Llama_4_Async_Batch;\ninterface Ai_Cf_Meta_Llama_4_Prompt {\n    /**\n     * The input text prompt for the model to generate a response.\n     */\n    prompt: string;\n    /**\n     * JSON schema that should be fulfilled for the response.\n     */\n    guided_json?: object;\n    response_format?: JSONMode;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ninterface Ai_Cf_Meta_Llama_4_Messages {\n    /**\n     * An array of message objects representing the conversation history.\n     */\n    messages: {\n        /**\n         * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool').\n         */\n        role?: string;\n        /**\n         * The tool call id. If you don't know what to put here you can fall back to 000000001\n         */\n        tool_call_id?: string;\n        content?: string | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        }[] | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        };\n    }[];\n    functions?: {\n        name: string;\n        code: string;\n    }[];\n    /**\n     * A list of tools available for the assistant to use.\n     */\n    tools?: ({\n        /**\n         * The name of the tool. More descriptive the better.\n         */\n        name: string;\n        /**\n         * A brief description of what the tool does.\n         */\n        description: string;\n        /**\n         * Schema defining the parameters accepted by the tool.\n         */\n        parameters: {\n            /**\n             * The type of the parameters object (usually 'object').\n             */\n            type: string;\n            /**\n             * List of required parameter names.\n             */\n            required?: string[];\n            /**\n             * Definitions of each parameter.\n             */\n            properties: {\n                [k: string]: {\n                    /**\n                     * The data type of the parameter.\n                     */\n                    type: string;\n                    /**\n                     * A description of the expected parameter.\n                     */\n                    description: string;\n                };\n            };\n        };\n    } | {\n        /**\n         * Specifies the type of tool (e.g., 'function').\n         */\n        type: string;\n        /**\n         * Details of the function tool.\n         */\n        function: {\n            /**\n             * The name of the function.\n             */\n            name: string;\n            /**\n             * A brief description of what the function does.\n             */\n            description: string;\n            /**\n             * Schema defining the parameters accepted by the function.\n             */\n            parameters: {\n                /**\n                 * The type of the parameters object (usually 'object').\n                 */\n                type: string;\n                /**\n                 * List of required parameter names.\n                 */\n                required?: string[];\n                /**\n                 * Definitions of each parameter.\n                 */\n                properties: {\n                    [k: string]: {\n                        /**\n                         * The data type of the parameter.\n                         */\n                        type: string;\n                        /**\n                         * A description of the expected parameter.\n                         */\n                        description: string;\n                    };\n                };\n            };\n        };\n    })[];\n    response_format?: JSONMode;\n    /**\n     * JSON schema that should be fufilled for the response.\n     */\n    guided_json?: object;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ninterface Ai_Cf_Meta_Llama_4_Async_Batch {\n    requests: (Ai_Cf_Meta_Llama_4_Prompt_Inner | Ai_Cf_Meta_Llama_4_Messages_Inner)[];\n}\ninterface Ai_Cf_Meta_Llama_4_Prompt_Inner {\n    /**\n     * The input text prompt for the model to generate a response.\n     */\n    prompt: string;\n    /**\n     * JSON schema that should be fulfilled for the response.\n     */\n    guided_json?: object;\n    response_format?: JSONMode;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ninterface Ai_Cf_Meta_Llama_4_Messages_Inner {\n    /**\n     * An array of message objects representing the conversation history.\n     */\n    messages: {\n        /**\n         * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool').\n         */\n        role?: string;\n        /**\n         * The tool call id. If you don't know what to put here you can fall back to 000000001\n         */\n        tool_call_id?: string;\n        content?: string | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        }[] | {\n            /**\n             * Type of the content provided\n             */\n            type?: string;\n            text?: string;\n            image_url?: {\n                /**\n                 * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted\n                 */\n                url?: string;\n            };\n        };\n    }[];\n    functions?: {\n        name: string;\n        code: string;\n    }[];\n    /**\n     * A list of tools available for the assistant to use.\n     */\n    tools?: ({\n        /**\n         * The name of the tool. More descriptive the better.\n         */\n        name: string;\n        /**\n         * A brief description of what the tool does.\n         */\n        description: string;\n        /**\n         * Schema defining the parameters accepted by the tool.\n         */\n        parameters: {\n            /**\n             * The type of the parameters object (usually 'object').\n             */\n            type: string;\n            /**\n             * List of required parameter names.\n             */\n            required?: string[];\n            /**\n             * Definitions of each parameter.\n             */\n            properties: {\n                [k: string]: {\n                    /**\n                     * The data type of the parameter.\n                     */\n                    type: string;\n                    /**\n                     * A description of the expected parameter.\n                     */\n                    description: string;\n                };\n            };\n        };\n    } | {\n        /**\n         * Specifies the type of tool (e.g., 'function').\n         */\n        type: string;\n        /**\n         * Details of the function tool.\n         */\n        function: {\n            /**\n             * The name of the function.\n             */\n            name: string;\n            /**\n             * A brief description of what the function does.\n             */\n            description: string;\n            /**\n             * Schema defining the parameters accepted by the function.\n             */\n            parameters: {\n                /**\n                 * The type of the parameters object (usually 'object').\n                 */\n                type: string;\n                /**\n                 * List of required parameter names.\n                 */\n                required?: string[];\n                /**\n                 * Definitions of each parameter.\n                 */\n                properties: {\n                    [k: string]: {\n                        /**\n                         * The data type of the parameter.\n                         */\n                        type: string;\n                        /**\n                         * A description of the expected parameter.\n                         */\n                        description: string;\n                    };\n                };\n            };\n        };\n    })[];\n    response_format?: JSONMode;\n    /**\n     * JSON schema that should be fufilled for the response.\n     */\n    guided_json?: object;\n    /**\n     * If true, a chat template is not applied and you must adhere to the specific model's expected formatting.\n     */\n    raw?: boolean;\n    /**\n     * If true, the response will be streamed back incrementally using SSE, Server Sent Events.\n     */\n    stream?: boolean;\n    /**\n     * The maximum number of tokens to generate in the response.\n     */\n    max_tokens?: number;\n    /**\n     * Controls the randomness of the output; higher values produce more random results.\n     */\n    temperature?: number;\n    /**\n     * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses.\n     */\n    top_p?: number;\n    /**\n     * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises.\n     */\n    top_k?: number;\n    /**\n     * Random seed for reproducibility of the generation.\n     */\n    seed?: number;\n    /**\n     * Penalty for repeated tokens; higher values discourage repetition.\n     */\n    repetition_penalty?: number;\n    /**\n     * Decreases the likelihood of the model repeating the same lines verbatim.\n     */\n    frequency_penalty?: number;\n    /**\n     * Increases the likelihood of the model introducing new topics.\n     */\n    presence_penalty?: number;\n}\ntype Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output = {\n    /**\n     * The generated text response from the model\n     */\n    response: string;\n    /**\n     * Usage statistics for the inference request\n     */\n    usage?: {\n        /**\n         * Total number of tokens in input\n         */\n        prompt_tokens?: number;\n        /**\n         * Total number of tokens in output\n         */\n        completion_tokens?: number;\n        /**\n         * Total number of input and output tokens\n         */\n        total_tokens?: number;\n    };\n    /**\n     * An array of tool calls requests made during the response generation\n     */\n    tool_calls?: {\n        /**\n         * The tool call id.\n         */\n        id?: string;\n        /**\n         * Specifies the type of tool (e.g., 'function').\n         */\n        type?: string;\n        /**\n         * Details of the function tool.\n         */\n        function?: {\n            /**\n             * The name of the tool to be called\n             */\n            name?: string;\n            /**\n             * The arguments passed to be passed to the tool call request\n             */\n            arguments?: object;\n        };\n    }[];\n};\ndeclare abstract class Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct {\n    inputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input;\n    postProcessedOutputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output;\n}\ninterface Ai_Cf_Deepgram_Nova_3_Input {\n    audio: {\n        body: object;\n        contentType: string;\n    };\n    /**\n     * Sets how the model will interpret strings submitted to the custom_topic param. When strict, the model will only return topics submitted using the custom_topic param. When extended, the model will return its own detected topics in addition to those submitted using the custom_topic param.\n     */\n    custom_topic_mode?: \"extended\" | \"strict\";\n    /**\n     * Custom topics you want the model to detect within your input audio or text if present Submit up to 100\n     */\n    custom_topic?: string;\n    /**\n     * Sets how the model will interpret intents submitted to the custom_intent param. When strict, the model will only return intents submitted using the custom_intent param. When extended, the model will return its own detected intents in addition those submitted using the custom_intents param\n     */\n    custom_intent_mode?: \"extended\" | \"strict\";\n    /**\n     * Custom intents you want the model to detect within your input audio if present\n     */\n    custom_intent?: string;\n    /**\n     * Identifies and extracts key entities from content in submitted audio\n     */\n    detect_entities?: boolean;\n    /**\n     * Identifies the dominant language spoken in submitted audio\n     */\n    detect_language?: boolean;\n    /**\n     * Recognize speaker changes. Each word in the transcript will be assigned a speaker number starting at 0\n     */\n    diarize?: boolean;\n    /**\n     * Identify and extract key entities from content in submitted audio\n     */\n    dictation?: boolean;\n    /**\n     * Specify the expected encoding of your submitted audio\n     */\n    encoding?: \"linear16\" | \"flac\" | \"mulaw\" | \"amr-nb\" | \"amr-wb\" | \"opus\" | \"speex\" | \"g729\";\n    /**\n     * Arbitrary key-value pairs that are attached to the API response for usage in downstream processing\n     */\n    extra?: string;\n    /**\n     * Filler Words can help transcribe interruptions in your audio, like 'uh' and 'um'\n     */\n    filler_words?: boolean;\n    /**\n     * Key term prompting can boost or suppress specialized terminology and brands.\n     */\n    keyterm?: string;\n    /**\n     * Keywords can boost or suppress specialized terminology and brands.\n     */\n    keywords?: string;\n    /**\n     * The BCP-47 language tag that hints at the primary spoken language. Depending on the Model and API endpoint you choose only certain languages are available.\n     */\n    language?: string;\n    /**\n     * Spoken measurements will be converted to their corresponding abbreviations.\n     */\n    measurements?: boolean;\n    /**\n     * Opts out requests from the Deepgram Model Improvement Program. Refer to our Docs for pricing impacts before setting this to true. https://dpgr.am/deepgram-mip.\n     */\n    mip_opt_out?: boolean;\n    /**\n     * Mode of operation for the model representing broad area of topic that will be talked about in the supplied audio\n     */\n    mode?: \"general\" | \"medical\" | \"finance\";\n    /**\n     * Transcribe each audio channel independently.\n     */\n    multichannel?: boolean;\n    /**\n     * Numerals converts numbers from written format to numerical format.\n     */\n    numerals?: boolean;\n    /**\n     * Splits audio into paragraphs to improve transcript readability.\n     */\n    paragraphs?: boolean;\n    /**\n     * Profanity Filter looks for recognized profanity and converts it to the nearest recognized non-profane word or removes it from the transcript completely.\n     */\n    profanity_filter?: boolean;\n    /**\n     * Add punctuation and capitalization to the transcript.\n     */\n    punctuate?: boolean;\n    /**\n     * Redaction removes sensitive information from your transcripts.\n     */\n    redact?: string;\n    /**\n     * Search for terms or phrases in submitted audio and replaces them.\n     */\n    replace?: string;\n    /**\n     * Search for terms or phrases in submitted audio.\n     */\n    search?: string;\n    /**\n     * Recognizes the sentiment throughout a transcript or text.\n     */\n    sentiment?: boolean;\n    /**\n     * Apply formatting to transcript output. When set to true, additional formatting will be applied to transcripts to improve readability.\n     */\n    smart_format?: boolean;\n    /**\n     * Detect topics throughout a transcript or text.\n     */\n    topics?: boolean;\n    /**\n     * Segments speech into meaningful semantic units.\n     */\n    utterances?: boolean;\n    /**\n     * Seconds to wait before detecting a pause between words in submitted audio.\n     */\n    utt_split?: number;\n    /**\n     * The number of channels in the submitted audio\n     */\n    channels?: number;\n    /**\n     * Specifies whether the streaming endpoint should provide ongoing transcription updates as more audio is received. When set to true, the endpoint sends continuous updates, meaning transcription results may evolve over time. Note: Supported only for webosockets.\n     */\n    interim_results?: boolean;\n    /**\n     * Indicates how long model will wait to detect whether a speaker has finished speaking or pauses for a significant period of time. When set to a value, the streaming endpoint immediately finalizes the transcription for the processed time range and returns the transcript with a speech_final parameter set to true. Can also be set to false to disable endpointing\n     */\n    endpointing?: string;\n    /**\n     * Indicates that speech has started. You'll begin receiving Speech Started messages upon speech starting. Note: Supported only for webosockets.\n     */\n    vad_events?: boolean;\n    /**\n     * Indicates how long model will wait to send an UtteranceEnd message after a word has been transcribed. Use with interim_results. Note: Supported only for webosockets.\n     */\n    utterance_end_ms?: boolean;\n}\ninterface Ai_Cf_Deepgram_Nova_3_Output {\n    results?: {\n        channels?: {\n            alternatives?: {\n                confidence?: number;\n                transcript?: string;\n                words?: {\n                    confidence?: number;\n                    end?: number;\n                    start?: number;\n                    word?: string;\n                }[];\n            }[];\n        }[];\n        summary?: {\n            result?: string;\n            short?: string;\n        };\n        sentiments?: {\n            segments?: {\n                text?: string;\n                start_word?: number;\n                end_word?: number;\n                sentiment?: string;\n                sentiment_score?: number;\n            }[];\n            average?: {\n                sentiment?: string;\n                sentiment_score?: number;\n            };\n        };\n    };\n}\ndeclare abstract class Base_Ai_Cf_Deepgram_Nova_3 {\n    inputs: Ai_Cf_Deepgram_Nova_3_Input;\n    postProcessedOutputs: Ai_Cf_Deepgram_Nova_3_Output;\n}\ntype Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input = {\n    /**\n     * readable stream with audio data and content-type specified for that data\n     */\n    audio: {\n        body: object;\n        contentType: string;\n    };\n    /**\n     * type of data PCM data that's sent to the inference server as raw array\n     */\n    dtype?: \"uint8\" | \"float32\" | \"float64\";\n} | {\n    /**\n     * base64 encoded audio data\n     */\n    audio: string;\n    /**\n     * type of data PCM data that's sent to the inference server as raw array\n     */\n    dtype?: \"uint8\" | \"float32\" | \"float64\";\n};\ninterface Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output {\n    /**\n     * if true, end-of-turn was detected\n     */\n    is_complete?: boolean;\n    /**\n     * probability of the end-of-turn detection\n     */\n    probability?: number;\n}\ndeclare abstract class Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2 {\n    inputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input;\n    postProcessedOutputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output;\n}\ntype Ai_Cf_Openai_Gpt_Oss_120B_Input = GPT_OSS_120B_Responses | GPT_OSS_120B_Responses_Async;\ninterface GPT_OSS_120B_Responses {\n    /**\n     * Responses API Input messages. Refer to OpenAI Responses API docs to learn more about supported content types\n     */\n    input: string | unknown[];\n    reasoning?: {\n        /**\n         * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.\n         */\n        effort?: \"low\" | \"medium\" | \"high\";\n        /**\n         * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. One of auto, concise, or detailed.\n         */\n        summary?: \"auto\" | \"concise\" | \"detailed\";\n    };\n}\ninterface GPT_OSS_120B_Responses_Async {\n    requests: {\n        /**\n         * Responses API Input messages. Refer to OpenAI Responses API docs to learn more about supported content types\n         */\n        input: string | unknown[];\n        reasoning?: {\n            /**\n             * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.\n             */\n            effort?: \"low\" | \"medium\" | \"high\";\n            /**\n             * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. One of auto, concise, or detailed.\n             */\n            summary?: \"auto\" | \"concise\" | \"detailed\";\n        };\n    }[];\n}\ntype Ai_Cf_Openai_Gpt_Oss_120B_Output = {} | (string & NonNullable<unknown>);\ndeclare abstract class Base_Ai_Cf_Openai_Gpt_Oss_120B {\n    inputs: Ai_Cf_Openai_Gpt_Oss_120B_Input;\n    postProcessedOutputs: Ai_Cf_Openai_Gpt_Oss_120B_Output;\n}\ntype Ai_Cf_Openai_Gpt_Oss_20B_Input = GPT_OSS_20B_Responses | GPT_OSS_20B_Responses_Async;\ninterface GPT_OSS_20B_Responses {\n    /**\n     * Responses API Input messages. Refer to OpenAI Responses API docs to learn more about supported content types\n     */\n    input: string | unknown[];\n    reasoning?: {\n        /**\n         * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.\n         */\n        effort?: \"low\" | \"medium\" | \"high\";\n        /**\n         * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. One of auto, concise, or detailed.\n         */\n        summary?: \"auto\" | \"concise\" | \"detailed\";\n    };\n}\ninterface GPT_OSS_20B_Responses_Async {\n    requests: {\n        /**\n         * Responses API Input messages. Refer to OpenAI Responses API docs to learn more about supported content types\n         */\n        input: string | unknown[];\n        reasoning?: {\n            /**\n             * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.\n             */\n            effort?: \"low\" | \"medium\" | \"high\";\n            /**\n             * A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. One of auto, concise, or detailed.\n             */\n            summary?: \"auto\" | \"concise\" | \"detailed\";\n        };\n    }[];\n}\ntype Ai_Cf_Openai_Gpt_Oss_20B_Output = {} | (string & NonNullable<unknown>);\ndeclare abstract class Base_Ai_Cf_Openai_Gpt_Oss_20B {\n    inputs: Ai_Cf_Openai_Gpt_Oss_20B_Input;\n    postProcessedOutputs: Ai_Cf_Openai_Gpt_Oss_20B_Output;\n}\ninterface Ai_Cf_Leonardo_Phoenix_1_0_Input {\n    /**\n     * A text description of the image you want to generate.\n     */\n    prompt: string;\n    /**\n     * Controls how closely the generated image should adhere to the prompt; higher values make the image more aligned with the prompt\n     */\n    guidance?: number;\n    /**\n     * Random seed for reproducibility of the image generation\n     */\n    seed?: number;\n    /**\n     * The height of the generated image in pixels\n     */\n    height?: number;\n    /**\n     * The width of the generated image in pixels\n     */\n    width?: number;\n    /**\n     * The number of diffusion steps; higher values can improve quality but take longer\n     */\n    num_steps?: number;\n    /**\n     * Specify what to exclude from the generated images\n     */\n    negative_prompt?: string;\n}\n/**\n * The generated image in JPEG format\n */\ntype Ai_Cf_Leonardo_Phoenix_1_0_Output = string;\ndeclare abstract class Base_Ai_Cf_Leonardo_Phoenix_1_0 {\n    inputs: Ai_Cf_Leonardo_Phoenix_1_0_Input;\n    postProcessedOutputs: Ai_Cf_Leonardo_Phoenix_1_0_Output;\n}\ninterface Ai_Cf_Leonardo_Lucid_Origin_Input {\n    /**\n     * A text description of the image you want to generate.\n     */\n    prompt: string;\n    /**\n     * Controls how closely the generated image should adhere to the prompt; higher values make the image more aligned with the prompt\n     */\n    guidance?: number;\n    /**\n     * Random seed for reproducibility of the image generation\n     */\n    seed?: number;\n    /**\n     * The height of the generated image in pixels\n     */\n    height?: number;\n    /**\n     * The width of the generated image in pixels\n     */\n    width?: number;\n    /**\n     * The number of diffusion steps; higher values can improve quality but take longer\n     */\n    num_steps?: number;\n    /**\n     * The number of diffusion steps; higher values can improve quality but take longer\n     */\n    steps?: number;\n}\ninterface Ai_Cf_Leonardo_Lucid_Origin_Output {\n    /**\n     * The generated image in Base64 format.\n     */\n    image?: string;\n}\ndeclare abstract class Base_Ai_Cf_Leonardo_Lucid_Origin {\n    inputs: Ai_Cf_Leonardo_Lucid_Origin_Input;\n    postProcessedOutputs: Ai_Cf_Leonardo_Lucid_Origin_Output;\n}\ninterface Ai_Cf_Deepgram_Aura_1_Input {\n    /**\n     * Speaker used to produce the audio.\n     */\n    speaker?: \"angus\" | \"asteria\" | \"arcas\" | \"orion\" | \"orpheus\" | \"athena\" | \"luna\" | \"zeus\" | \"perseus\" | \"helios\" | \"hera\" | \"stella\";\n    /**\n     * Encoding of the output audio.\n     */\n    encoding?: \"linear16\" | \"flac\" | \"mulaw\" | \"alaw\" | \"mp3\" | \"opus\" | \"aac\";\n    /**\n     * Container specifies the file format wrapper for the output audio. The available options depend on the encoding type..\n     */\n    container?: \"none\" | \"wav\" | \"ogg\";\n    /**\n     * The text content to be converted to speech\n     */\n    text: string;\n    /**\n     * Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable\n     */\n    sample_rate?: number;\n    /**\n     * The bitrate of the audio in bits per second. Choose from predefined ranges or specific values based on the encoding type.\n     */\n    bit_rate?: number;\n}\n/**\n * The generated audio in MP3 format\n */\ntype Ai_Cf_Deepgram_Aura_1_Output = string;\ndeclare abstract class Base_Ai_Cf_Deepgram_Aura_1 {\n    inputs: Ai_Cf_Deepgram_Aura_1_Input;\n    postProcessedOutputs: Ai_Cf_Deepgram_Aura_1_Output;\n}\ninterface AiModels {\n    \"@cf/huggingface/distilbert-sst-2-int8\": BaseAiTextClassification;\n    \"@cf/stabilityai/stable-diffusion-xl-base-1.0\": BaseAiTextToImage;\n    \"@cf/runwayml/stable-diffusion-v1-5-inpainting\": BaseAiTextToImage;\n    \"@cf/runwayml/stable-diffusion-v1-5-img2img\": BaseAiTextToImage;\n    \"@cf/lykon/dreamshaper-8-lcm\": BaseAiTextToImage;\n    \"@cf/bytedance/stable-diffusion-xl-lightning\": BaseAiTextToImage;\n    \"@cf/myshell-ai/melotts\": BaseAiTextToSpeech;\n    \"@cf/google/embeddinggemma-300m\": BaseAiTextEmbeddings;\n    \"@cf/microsoft/resnet-50\": BaseAiImageClassification;\n    \"@cf/meta/llama-2-7b-chat-int8\": BaseAiTextGeneration;\n    \"@cf/mistral/mistral-7b-instruct-v0.1\": BaseAiTextGeneration;\n    \"@cf/meta/llama-2-7b-chat-fp16\": BaseAiTextGeneration;\n    \"@hf/thebloke/llama-2-13b-chat-awq\": BaseAiTextGeneration;\n    \"@hf/thebloke/mistral-7b-instruct-v0.1-awq\": BaseAiTextGeneration;\n    \"@hf/thebloke/zephyr-7b-beta-awq\": BaseAiTextGeneration;\n    \"@hf/thebloke/openhermes-2.5-mistral-7b-awq\": BaseAiTextGeneration;\n    \"@hf/thebloke/neural-chat-7b-v3-1-awq\": BaseAiTextGeneration;\n    \"@hf/thebloke/llamaguard-7b-awq\": BaseAiTextGeneration;\n    \"@hf/thebloke/deepseek-coder-6.7b-base-awq\": BaseAiTextGeneration;\n    \"@hf/thebloke/deepseek-coder-6.7b-instruct-awq\": BaseAiTextGeneration;\n    \"@cf/deepseek-ai/deepseek-math-7b-instruct\": BaseAiTextGeneration;\n    \"@cf/defog/sqlcoder-7b-2\": BaseAiTextGeneration;\n    \"@cf/openchat/openchat-3.5-0106\": BaseAiTextGeneration;\n    \"@cf/tiiuae/falcon-7b-instruct\": BaseAiTextGeneration;\n    \"@cf/thebloke/discolm-german-7b-v1-awq\": BaseAiTextGeneration;\n    \"@cf/qwen/qwen1.5-0.5b-chat\": BaseAiTextGeneration;\n    \"@cf/qwen/qwen1.5-7b-chat-awq\": BaseAiTextGeneration;\n    \"@cf/qwen/qwen1.5-14b-chat-awq\": BaseAiTextGeneration;\n    \"@cf/tinyllama/tinyllama-1.1b-chat-v1.0\": BaseAiTextGeneration;\n    \"@cf/microsoft/phi-2\": BaseAiTextGeneration;\n    \"@cf/qwen/qwen1.5-1.8b-chat\": BaseAiTextGeneration;\n    \"@cf/mistral/mistral-7b-instruct-v0.2-lora\": BaseAiTextGeneration;\n    \"@hf/nousresearch/hermes-2-pro-mistral-7b\": BaseAiTextGeneration;\n    \"@hf/nexusflow/starling-lm-7b-beta\": BaseAiTextGeneration;\n    \"@hf/google/gemma-7b-it\": BaseAiTextGeneration;\n    \"@cf/meta-llama/llama-2-7b-chat-hf-lora\": BaseAiTextGeneration;\n    \"@cf/google/gemma-2b-it-lora\": BaseAiTextGeneration;\n    \"@cf/google/gemma-7b-it-lora\": BaseAiTextGeneration;\n    \"@hf/mistral/mistral-7b-instruct-v0.2\": BaseAiTextGeneration;\n    \"@cf/meta/llama-3-8b-instruct\": BaseAiTextGeneration;\n    \"@cf/fblgit/una-cybertron-7b-v2-bf16\": BaseAiTextGeneration;\n    \"@cf/meta/llama-3-8b-instruct-awq\": BaseAiTextGeneration;\n    \"@hf/meta-llama/meta-llama-3-8b-instruct\": BaseAiTextGeneration;\n    \"@cf/meta/llama-3.1-8b-instruct-fp8\": BaseAiTextGeneration;\n    \"@cf/meta/llama-3.1-8b-instruct-awq\": BaseAiTextGeneration;\n    \"@cf/meta/llama-3.2-3b-instruct\": BaseAiTextGeneration;\n    \"@cf/meta/llama-3.2-1b-instruct\": BaseAiTextGeneration;\n    \"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\": BaseAiTextGeneration;\n    \"@cf/facebook/bart-large-cnn\": BaseAiSummarization;\n    \"@cf/llava-hf/llava-1.5-7b-hf\": BaseAiImageToText;\n    \"@cf/baai/bge-base-en-v1.5\": Base_Ai_Cf_Baai_Bge_Base_En_V1_5;\n    \"@cf/openai/whisper\": Base_Ai_Cf_Openai_Whisper;\n    \"@cf/meta/m2m100-1.2b\": Base_Ai_Cf_Meta_M2M100_1_2B;\n    \"@cf/baai/bge-small-en-v1.5\": Base_Ai_Cf_Baai_Bge_Small_En_V1_5;\n    \"@cf/baai/bge-large-en-v1.5\": Base_Ai_Cf_Baai_Bge_Large_En_V1_5;\n    \"@cf/unum/uform-gen2-qwen-500m\": Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M;\n    \"@cf/openai/whisper-tiny-en\": Base_Ai_Cf_Openai_Whisper_Tiny_En;\n    \"@cf/openai/whisper-large-v3-turbo\": Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo;\n    \"@cf/baai/bge-m3\": Base_Ai_Cf_Baai_Bge_M3;\n    \"@cf/black-forest-labs/flux-1-schnell\": Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell;\n    \"@cf/meta/llama-3.2-11b-vision-instruct\": Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct;\n    \"@cf/meta/llama-3.3-70b-instruct-fp8-fast\": Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast;\n    \"@cf/meta/llama-guard-3-8b\": Base_Ai_Cf_Meta_Llama_Guard_3_8B;\n    \"@cf/baai/bge-reranker-base\": Base_Ai_Cf_Baai_Bge_Reranker_Base;\n    \"@cf/qwen/qwen2.5-coder-32b-instruct\": Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct;\n    \"@cf/qwen/qwq-32b\": Base_Ai_Cf_Qwen_Qwq_32B;\n    \"@cf/mistralai/mistral-small-3.1-24b-instruct\": Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct;\n    \"@cf/google/gemma-3-12b-it\": Base_Ai_Cf_Google_Gemma_3_12B_It;\n    \"@cf/meta/llama-4-scout-17b-16e-instruct\": Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct;\n    \"@cf/deepgram/nova-3\": Base_Ai_Cf_Deepgram_Nova_3;\n    \"@cf/pipecat-ai/smart-turn-v2\": Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2;\n    \"@cf/openai/gpt-oss-120b\": Base_Ai_Cf_Openai_Gpt_Oss_120B;\n    \"@cf/openai/gpt-oss-20b\": Base_Ai_Cf_Openai_Gpt_Oss_20B;\n    \"@cf/leonardo/phoenix-1.0\": Base_Ai_Cf_Leonardo_Phoenix_1_0;\n    \"@cf/leonardo/lucid-origin\": Base_Ai_Cf_Leonardo_Lucid_Origin;\n    \"@cf/deepgram/aura-1\": Base_Ai_Cf_Deepgram_Aura_1;\n}\ntype AiOptions = {\n    /**\n     * Send requests as an asynchronous batch job, only works for supported models\n     * https://developers.cloudflare.com/workers-ai/features/batch-api\n     */\n    queueRequest?: boolean;\n    /**\n     * Establish websocket connections, only works for supported models\n     */\n    websocket?: boolean;\n    gateway?: GatewayOptions;\n    returnRawResponse?: boolean;\n    prefix?: string;\n    extraHeaders?: object;\n};\ntype AiModelsSearchParams = {\n    author?: string;\n    hide_experimental?: boolean;\n    page?: number;\n    per_page?: number;\n    search?: string;\n    source?: number;\n    task?: string;\n};\ntype AiModelsSearchObject = {\n    id: string;\n    source: number;\n    name: string;\n    description: string;\n    task: {\n        id: string;\n        name: string;\n        description: string;\n    };\n    tags: string[];\n    properties: {\n        property_id: string;\n        value: string;\n    }[];\n};\ninterface InferenceUpstreamError extends Error {\n}\ninterface AiInternalError extends Error {\n}\ntype AiModelListType = Record<string, any>;\ndeclare abstract class Ai<AiModelList extends AiModelListType = AiModels> {\n    aiGatewayLogId: string | null;\n    gateway(gatewayId: string): AiGateway;\n    autorag(autoragId: string): AutoRAG;\n    run<Name extends keyof AiModelList, Options extends AiOptions, InputOptions extends AiModelList[Name][\"inputs\"]>(model: Name, inputs: InputOptions, options?: Options): Promise<Options extends {\n        returnRawResponse: true;\n    } | {\n        websocket: true;\n    } ? Response : InputOptions extends {\n        stream: true;\n    } ? ReadableStream : AiModelList[Name][\"postProcessedOutputs\"]>;\n    models(params?: AiModelsSearchParams): Promise<AiModelsSearchObject[]>;\n    toMarkdown(): ToMarkdownService;\n    toMarkdown(files: MarkdownDocument[], options?: ConversionRequestOptions): Promise<ConversionResponse[]>;\n    toMarkdown(files: MarkdownDocument, options?: ConversionRequestOptions): Promise<ConversionResponse>;\n}\ntype GatewayRetries = {\n    maxAttempts?: 1 | 2 | 3 | 4 | 5;\n    retryDelayMs?: number;\n    backoff?: 'constant' | 'linear' | 'exponential';\n};\ntype GatewayOptions = {\n    id: string;\n    cacheKey?: string;\n    cacheTtl?: number;\n    skipCache?: boolean;\n    metadata?: Record<string, number | string | boolean | null | bigint>;\n    collectLog?: boolean;\n    eventId?: string;\n    requestTimeoutMs?: number;\n    retries?: GatewayRetries;\n};\ntype UniversalGatewayOptions = Exclude<GatewayOptions, 'id'> & {\n    /**\n     ** @deprecated\n     */\n    id?: string;\n};\ntype AiGatewayPatchLog = {\n    score?: number | null;\n    feedback?: -1 | 1 | null;\n    metadata?: Record<string, number | string | boolean | null | bigint> | null;\n};\ntype AiGatewayLog = {\n    id: string;\n    provider: string;\n    model: string;\n    model_type?: string;\n    path: string;\n    duration: number;\n    request_type?: string;\n    request_content_type?: string;\n    status_code: number;\n    response_content_type?: string;\n    success: boolean;\n    cached: boolean;\n    tokens_in?: number;\n    tokens_out?: number;\n    metadata?: Record<string, number | string | boolean | null | bigint>;\n    step?: number;\n    cost?: number;\n    custom_cost?: boolean;\n    request_size: number;\n    request_head?: string;\n    request_head_complete: boolean;\n    response_size: number;\n    response_head?: string;\n    response_head_complete: boolean;\n    created_at: Date;\n};\ntype AIGatewayProviders = 'workers-ai' | 'anthropic' | 'aws-bedrock' | 'azure-openai' | 'google-vertex-ai' | 'huggingface' | 'openai' | 'perplexity-ai' | 'replicate' | 'groq' | 'cohere' | 'google-ai-studio' | 'mistral' | 'grok' | 'openrouter' | 'deepseek' | 'cerebras' | 'cartesia' | 'elevenlabs' | 'adobe-firefly';\ntype AIGatewayHeaders = {\n    'cf-aig-metadata': Record<string, number | string | boolean | null | bigint> | string;\n    'cf-aig-custom-cost': {\n        per_token_in?: number;\n        per_token_out?: number;\n    } | {\n        total_cost?: number;\n    } | string;\n    'cf-aig-cache-ttl': number | string;\n    'cf-aig-skip-cache': boolean | string;\n    'cf-aig-cache-key': string;\n    'cf-aig-event-id': string;\n    'cf-aig-request-timeout': number | string;\n    'cf-aig-max-attempts': number | string;\n    'cf-aig-retry-delay': number | string;\n    'cf-aig-backoff': string;\n    'cf-aig-collect-log': boolean | string;\n    Authorization: string;\n    'Content-Type': string;\n    [key: string]: string | number | boolean | object;\n};\ntype AIGatewayUniversalRequest = {\n    provider: AIGatewayProviders | string; // eslint-disable-line\n    endpoint: string;\n    headers: Partial<AIGatewayHeaders>;\n    query: unknown;\n};\ninterface AiGatewayInternalError extends Error {\n}\ninterface AiGatewayLogNotFound extends Error {\n}\ndeclare abstract class AiGateway {\n    patchLog(logId: string, data: AiGatewayPatchLog): Promise<void>;\n    getLog(logId: string): Promise<AiGatewayLog>;\n    run(data: AIGatewayUniversalRequest | AIGatewayUniversalRequest[], options?: {\n        gateway?: UniversalGatewayOptions;\n        extraHeaders?: object;\n    }): Promise<Response>;\n    getUrl(provider?: AIGatewayProviders | string): Promise<string>; // eslint-disable-line\n}\ninterface AutoRAGInternalError extends Error {\n}\ninterface AutoRAGNotFoundError extends Error {\n}\ninterface AutoRAGUnauthorizedError extends Error {\n}\ninterface AutoRAGNameNotSetError extends Error {\n}\ntype ComparisonFilter = {\n    key: string;\n    type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte';\n    value: string | number | boolean;\n};\ntype CompoundFilter = {\n    type: 'and' | 'or';\n    filters: ComparisonFilter[];\n};\ntype AutoRagSearchRequest = {\n    query: string;\n    filters?: CompoundFilter | ComparisonFilter;\n    max_num_results?: number;\n    ranking_options?: {\n        ranker?: string;\n        score_threshold?: number;\n    };\n    reranking?: {\n        enabled?: boolean;\n        model?: string;\n    };\n    rewrite_query?: boolean;\n};\ntype AutoRagAiSearchRequest = AutoRagSearchRequest & {\n    stream?: boolean;\n    system_prompt?: string;\n};\ntype AutoRagAiSearchRequestStreaming = Omit<AutoRagAiSearchRequest, 'stream'> & {\n    stream: true;\n};\ntype AutoRagSearchResponse = {\n    object: 'vector_store.search_results.page';\n    search_query: string;\n    data: {\n        file_id: string;\n        filename: string;\n        score: number;\n        attributes: Record<string, string | number | boolean | null>;\n        content: {\n            type: 'text';\n            text: string;\n        }[];\n    }[];\n    has_more: boolean;\n    next_page: string | null;\n};\ntype AutoRagListResponse = {\n    id: string;\n    enable: boolean;\n    type: string;\n    source: string;\n    vectorize_name: string;\n    paused: boolean;\n    status: string;\n}[];\ntype AutoRagAiSearchResponse = AutoRagSearchResponse & {\n    response: string;\n};\ndeclare abstract class AutoRAG {\n    list(): Promise<AutoRagListResponse>;\n    search(params: AutoRagSearchRequest): Promise<AutoRagSearchResponse>;\n    aiSearch(params: AutoRagAiSearchRequestStreaming): Promise<Response>;\n    aiSearch(params: AutoRagAiSearchRequest): Promise<AutoRagAiSearchResponse>;\n    aiSearch(params: AutoRagAiSearchRequest): Promise<AutoRagAiSearchResponse | Response>;\n}\ninterface BasicImageTransformations {\n    /**\n     * Maximum width in image pixels. The value must be an integer.\n     */\n    width?: number;\n    /**\n     * Maximum height in image pixels. The value must be an integer.\n     */\n    height?: number;\n    /**\n     * Resizing mode as a string. It affects interpretation of width and height\n     * options:\n     *  - scale-down: Similar to contain, but the image is never enlarged. If\n     *    the image is larger than given width or height, it will be resized.\n     *    Otherwise its original size will be kept.\n     *  - contain: Resizes to maximum size that fits within the given width and\n     *    height. If only a single dimension is given (e.g. only width), the\n     *    image will be shrunk or enlarged to exactly match that dimension.\n     *    Aspect ratio is always preserved.\n     *  - cover: Resizes (shrinks or enlarges) to fill the entire area of width\n     *    and height. If the image has an aspect ratio different from the ratio\n     *    of width and height, it will be cropped to fit.\n     *  - crop: The image will be shrunk and cropped to fit within the area\n     *    specified by width and height. The image will not be enlarged. For images\n     *    smaller than the given dimensions it's the same as scale-down. For\n     *    images larger than the given dimensions, it's the same as cover.\n     *    See also trim.\n     *  - pad: Resizes to the maximum size that fits within the given width and\n     *    height, and then fills the remaining area with a background color\n     *    (white by default). Use of this mode is not recommended, as the same\n     *    effect can be more efficiently achieved with the contain mode and the\n     *    CSS object-fit: contain property.\n     *  - squeeze: Stretches and deforms to the width and height given, even if it\n     *    breaks aspect ratio\n     */\n    fit?: \"scale-down\" | \"contain\" | \"cover\" | \"crop\" | \"pad\" | \"squeeze\";\n    /**\n     * Image segmentation using artificial intelligence models. Sets pixels not\n     * within selected segment area to transparent e.g \"foreground\" sets every\n     * background pixel as transparent.\n     */\n    segment?: \"foreground\";\n    /**\n     * When cropping with fit: \"cover\", this defines the side or point that should\n     * be left uncropped. The value is either a string\n     * \"left\", \"right\", \"top\", \"bottom\", \"auto\", or \"center\" (the default),\n     * or an object {x, y} containing focal point coordinates in the original\n     * image expressed as fractions ranging from 0.0 (top or left) to 1.0\n     * (bottom or right), 0.5 being the center. {fit: \"cover\", gravity: \"top\"} will\n     * crop bottom or left and right sides as necessary, but won’t crop anything\n     * from the top. {fit: \"cover\", gravity: {x:0.5, y:0.2}} will crop each side to\n     * preserve as much as possible around a point at 20% of the height of the\n     * source image.\n     */\n    gravity?: 'face' | 'left' | 'right' | 'top' | 'bottom' | 'center' | 'auto' | 'entropy' | BasicImageTransformationsGravityCoordinates;\n    /**\n     * Background color to add underneath the image. Applies only to images with\n     * transparency (such as PNG). Accepts any CSS color (#RRGGBB, rgba(…),\n     * hsl(…), etc.)\n     */\n    background?: string;\n    /**\n     * Number of degrees (90, 180, 270) to rotate the image by. width and height\n     * options refer to axes after rotation.\n     */\n    rotate?: 0 | 90 | 180 | 270 | 360;\n}\ninterface BasicImageTransformationsGravityCoordinates {\n    x?: number;\n    y?: number;\n    mode?: 'remainder' | 'box-center';\n}\n/**\n * In addition to the properties you can set in the RequestInit dict\n * that you pass as an argument to the Request constructor, you can\n * set certain properties of a `cf` object to control how Cloudflare\n * features are applied to that new Request.\n *\n * Note: Currently, these properties cannot be tested in the\n * playground.\n */\ninterface RequestInitCfProperties extends Record<string, unknown> {\n    cacheEverything?: boolean;\n    /**\n     * A request's cache key is what determines if two requests are\n     * \"the same\" for caching purposes. If a request has the same cache key\n     * as some previous request, then we can serve the same cached response for\n     * both. (e.g. 'some-key')\n     *\n     * Only available for Enterprise customers.\n     */\n    cacheKey?: string;\n    /**\n     * This allows you to append additional Cache-Tag response headers\n     * to the origin response without modifications to the origin server.\n     * This will allow for greater control over the Purge by Cache Tag feature\n     * utilizing changes only in the Workers process.\n     *\n     * Only available for Enterprise customers.\n     */\n    cacheTags?: string[];\n    /**\n     * Force response to be cached for a given number of seconds. (e.g. 300)\n     */\n    cacheTtl?: number;\n    /**\n     * Force response to be cached for a given number of seconds based on the Origin status code.\n     * (e.g. { '200-299': 86400, '404': 1, '500-599': 0 })\n     */\n    cacheTtlByStatus?: Record<string, number>;\n    scrapeShield?: boolean;\n    apps?: boolean;\n    image?: RequestInitCfPropertiesImage;\n    minify?: RequestInitCfPropertiesImageMinify;\n    mirage?: boolean;\n    polish?: \"lossy\" | \"lossless\" | \"off\";\n    r2?: RequestInitCfPropertiesR2;\n    /**\n     * Redirects the request to an alternate origin server. You can use this,\n     * for example, to implement load balancing across several origins.\n     * (e.g.us-east.example.com)\n     *\n     * Note - For security reasons, the hostname set in resolveOverride must\n     * be proxied on the same Cloudflare zone of the incoming request.\n     * Otherwise, the setting is ignored. CNAME hosts are allowed, so to\n     * resolve to a host under a different domain or a DNS only domain first\n     * declare a CNAME record within your own zone’s DNS mapping to the\n     * external hostname, set proxy on Cloudflare, then set resolveOverride\n     * to point to that CNAME record.\n     */\n    resolveOverride?: string;\n}\ninterface RequestInitCfPropertiesImageDraw extends BasicImageTransformations {\n    /**\n     * Absolute URL of the image file to use for the drawing. It can be any of\n     * the supported file formats. For drawing of watermarks or non-rectangular\n     * overlays we recommend using PNG or WebP images.\n     */\n    url: string;\n    /**\n     * Floating-point number between 0 (transparent) and 1 (opaque).\n     * For example, opacity: 0.5 makes overlay semitransparent.\n     */\n    opacity?: number;\n    /**\n     * - If set to true, the overlay image will be tiled to cover the entire\n     *   area. This is useful for stock-photo-like watermarks.\n     * - If set to \"x\", the overlay image will be tiled horizontally only\n     *   (form a line).\n     * - If set to \"y\", the overlay image will be tiled vertically only\n     *   (form a line).\n     */\n    repeat?: true | \"x\" | \"y\";\n    /**\n     * Position of the overlay image relative to a given edge. Each property is\n     * an offset in pixels. 0 aligns exactly to the edge. For example, left: 10\n     * positions left side of the overlay 10 pixels from the left edge of the\n     * image it's drawn over. bottom: 0 aligns bottom of the overlay with bottom\n     * of the background image.\n     *\n     * Setting both left & right, or both top & bottom is an error.\n     *\n     * If no position is specified, the image will be centered.\n     */\n    top?: number;\n    left?: number;\n    bottom?: number;\n    right?: number;\n}\ninterface RequestInitCfPropertiesImage extends BasicImageTransformations {\n    /**\n     * Device Pixel Ratio. Default 1. Multiplier for width/height that makes it\n     * easier to specify higher-DPI sizes in <img srcset>.\n     */\n    dpr?: number;\n    /**\n     * Allows you to trim your image. Takes dpr into account and is performed before\n     * resizing or rotation.\n     *\n     * It can be used as:\n     * - left, top, right, bottom - it will specify the number of pixels to cut\n     *   off each side\n     * - width, height - the width/height you'd like to end up with - can be used\n     *   in combination with the properties above\n     * - border - this will automatically trim the surroundings of an image based on\n     *   it's color. It consists of three properties:\n     *    - color: rgb or hex representation of the color you wish to trim (todo: verify the rgba bit)\n     *    - tolerance: difference from color to treat as color\n     *    - keep: the number of pixels of border to keep\n     */\n    trim?: \"border\" | {\n        top?: number;\n        bottom?: number;\n        left?: number;\n        right?: number;\n        width?: number;\n        height?: number;\n        border?: boolean | {\n            color?: string;\n            tolerance?: number;\n            keep?: number;\n        };\n    };\n    /**\n     * Quality setting from 1-100 (useful values are in 60-90 range). Lower values\n     * make images look worse, but load faster. The default is 85. It applies only\n     * to JPEG and WebP images. It doesn’t have any effect on PNG.\n     */\n    quality?: number | \"low\" | \"medium-low\" | \"medium-high\" | \"high\";\n    /**\n     * Output format to generate. It can be:\n     *  - avif: generate images in AVIF format.\n     *  - webp: generate images in Google WebP format. Set quality to 100 to get\n     *    the WebP-lossless format.\n     *  - json: instead of generating an image, outputs information about the\n     *    image, in JSON format. The JSON object will contain image size\n     *    (before and after resizing), source image’s MIME type, file size, etc.\n     * - jpeg: generate images in JPEG format.\n     * - png: generate images in PNG format.\n     */\n    format?: \"avif\" | \"webp\" | \"json\" | \"jpeg\" | \"png\" | \"baseline-jpeg\" | \"png-force\" | \"svg\";\n    /**\n     * Whether to preserve animation frames from input files. Default is true.\n     * Setting it to false reduces animations to still images. This setting is\n     * recommended when enlarging images or processing arbitrary user content,\n     * because large GIF animations can weigh tens or even hundreds of megabytes.\n     * It is also useful to set anim:false when using format:\"json\" to get the\n     * response quicker without the number of frames.\n     */\n    anim?: boolean;\n    /**\n     * What EXIF data should be preserved in the output image. Note that EXIF\n     * rotation and embedded color profiles are always applied (\"baked in\" into\n     * the image), and aren't affected by this option. Note that if the Polish\n     * feature is enabled, all metadata may have been removed already and this\n     * option may have no effect.\n     *  - keep: Preserve most of EXIF metadata, including GPS location if there's\n     *    any.\n     *  - copyright: Only keep the copyright tag, and discard everything else.\n     *    This is the default behavior for JPEG files.\n     *  - none: Discard all invisible EXIF metadata. Currently WebP and PNG\n     *    output formats always discard metadata.\n     */\n    metadata?: \"keep\" | \"copyright\" | \"none\";\n    /**\n     * Strength of sharpening filter to apply to the image. Floating-point\n     * number between 0 (no sharpening, default) and 10 (maximum). 1.0 is a\n     * recommended value for downscaled images.\n     */\n    sharpen?: number;\n    /**\n     * Radius of a blur filter (approximate gaussian). Maximum supported radius\n     * is 250.\n     */\n    blur?: number;\n    /**\n     * Overlays are drawn in the order they appear in the array (last array\n     * entry is the topmost layer).\n     */\n    draw?: RequestInitCfPropertiesImageDraw[];\n    /**\n     * Fetching image from authenticated origin. Setting this property will\n     * pass authentication headers (Authorization, Cookie, etc.) through to\n     * the origin.\n     */\n    \"origin-auth\"?: \"share-publicly\";\n    /**\n     * Adds a border around the image. The border is added after resizing. Border\n     * width takes dpr into account, and can be specified either using a single\n     * width property, or individually for each side.\n     */\n    border?: {\n        color: string;\n        width: number;\n    } | {\n        color: string;\n        top: number;\n        right: number;\n        bottom: number;\n        left: number;\n    };\n    /**\n     * Increase brightness by a factor. A value of 1.0 equals no change, a value\n     * of 0.5 equals half brightness, and a value of 2.0 equals twice as bright.\n     * 0 is ignored.\n     */\n    brightness?: number;\n    /**\n     * Increase contrast by a factor. A value of 1.0 equals no change, a value of\n     * 0.5 equals low contrast, and a value of 2.0 equals high contrast. 0 is\n     * ignored.\n     */\n    contrast?: number;\n    /**\n     * Increase exposure by a factor. A value of 1.0 equals no change, a value of\n     * 0.5 darkens the image, and a value of 2.0 lightens the image. 0 is ignored.\n     */\n    gamma?: number;\n    /**\n     * Increase contrast by a factor. A value of 1.0 equals no change, a value of\n     * 0.5 equals low contrast, and a value of 2.0 equals high contrast. 0 is\n     * ignored.\n     */\n    saturation?: number;\n    /**\n     * Flips the images horizontally, vertically, or both. Flipping is applied before\n     * rotation, so if you apply flip=h,rotate=90 then the image will be flipped\n     * horizontally, then rotated by 90 degrees.\n     */\n    flip?: 'h' | 'v' | 'hv';\n    /**\n     * Slightly reduces latency on a cache miss by selecting a\n     * quickest-to-compress file format, at a cost of increased file size and\n     * lower image quality. It will usually override the format option and choose\n     * JPEG over WebP or AVIF. We do not recommend using this option, except in\n     * unusual circumstances like resizing uncacheable dynamically-generated\n     * images.\n     */\n    compression?: \"fast\";\n}\ninterface RequestInitCfPropertiesImageMinify {\n    javascript?: boolean;\n    css?: boolean;\n    html?: boolean;\n}\ninterface RequestInitCfPropertiesR2 {\n    /**\n     * Colo id of bucket that an object is stored in\n     */\n    bucketColoId?: number;\n}\n/**\n * Request metadata provided by Cloudflare's edge.\n */\ntype IncomingRequestCfProperties<HostMetadata = unknown> = IncomingRequestCfPropertiesBase & IncomingRequestCfPropertiesBotManagementEnterprise & IncomingRequestCfPropertiesCloudflareForSaaSEnterprise<HostMetadata> & IncomingRequestCfPropertiesGeographicInformation & IncomingRequestCfPropertiesCloudflareAccessOrApiShield;\ninterface IncomingRequestCfPropertiesBase extends Record<string, unknown> {\n    /**\n     * [ASN](https://www.iana.org/assignments/as-numbers/as-numbers.xhtml) of the incoming request.\n     *\n     * @example 395747\n     */\n    asn?: number;\n    /**\n     * The organization which owns the ASN of the incoming request.\n     *\n     * @example \"Google Cloud\"\n     */\n    asOrganization?: string;\n    /**\n     * The original value of the `Accept-Encoding` header if Cloudflare modified it.\n     *\n     * @example \"gzip, deflate, br\"\n     */\n    clientAcceptEncoding?: string;\n    /**\n     * The number of milliseconds it took for the request to reach your worker.\n     *\n     * @example 22\n     */\n    clientTcpRtt?: number;\n    /**\n     * The three-letter [IATA](https://en.wikipedia.org/wiki/IATA_airport_code)\n     * airport code of the data center that the request hit.\n     *\n     * @example \"DFW\"\n     */\n    colo: string;\n    /**\n     * Represents the upstream's response to a\n     * [TCP `keepalive` message](https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html)\n     * from cloudflare.\n     *\n     * For workers with no upstream, this will always be `1`.\n     *\n     * @example 3\n     */\n    edgeRequestKeepAliveStatus: IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus;\n    /**\n     * The HTTP Protocol the request used.\n     *\n     * @example \"HTTP/2\"\n     */\n    httpProtocol: string;\n    /**\n     * The browser-requested prioritization information in the request object.\n     *\n     * If no information was set, defaults to the empty string `\"\"`\n     *\n     * @example \"weight=192;exclusive=0;group=3;group-weight=127\"\n     * @default \"\"\n     */\n    requestPriority: string;\n    /**\n     * The TLS version of the connection to Cloudflare.\n     * In requests served over plaintext (without TLS), this property is the empty string `\"\"`.\n     *\n     * @example \"TLSv1.3\"\n     */\n    tlsVersion: string;\n    /**\n     * The cipher for the connection to Cloudflare.\n     * In requests served over plaintext (without TLS), this property is the empty string `\"\"`.\n     *\n     * @example \"AEAD-AES128-GCM-SHA256\"\n     */\n    tlsCipher: string;\n    /**\n     * Metadata containing the [`HELLO`](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2) and [`FINISHED`](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.9) messages from this request's TLS handshake.\n     *\n     * If the incoming request was served over plaintext (without TLS) this field is undefined.\n     */\n    tlsExportedAuthenticator?: IncomingRequestCfPropertiesExportedAuthenticatorMetadata;\n}\ninterface IncomingRequestCfPropertiesBotManagementBase {\n    /**\n     * Cloudflare’s [level of certainty](https://developers.cloudflare.com/bots/concepts/bot-score/) that a request comes from a bot,\n     * represented as an integer percentage between `1` (almost certainly a bot) and `99` (almost certainly human).\n     *\n     * @example 54\n     */\n    score: number;\n    /**\n     * A boolean value that is true if the request comes from a good bot, like Google or Bing.\n     * Most customers choose to allow this traffic. For more details, see [Traffic from known bots](https://developers.cloudflare.com/firewall/known-issues-and-faq/#how-does-firewall-rules-handle-traffic-from-known-bots).\n     */\n    verifiedBot: boolean;\n    /**\n     * A boolean value that is true if the request originates from a\n     * Cloudflare-verified proxy service.\n     */\n    corporateProxy: boolean;\n    /**\n     * A boolean value that's true if the request matches [file extensions](https://developers.cloudflare.com/bots/reference/static-resources/) for many types of static resources.\n     */\n    staticResource: boolean;\n    /**\n     * List of IDs that correlate to the Bot Management heuristic detections made on a request (you can have multiple heuristic detections on the same request).\n     */\n    detectionIds: number[];\n}\ninterface IncomingRequestCfPropertiesBotManagement {\n    /**\n     * Results of Cloudflare's Bot Management analysis\n     */\n    botManagement: IncomingRequestCfPropertiesBotManagementBase;\n    /**\n     * Duplicate of `botManagement.score`.\n     *\n     * @deprecated\n     */\n    clientTrustScore: number;\n}\ninterface IncomingRequestCfPropertiesBotManagementEnterprise extends IncomingRequestCfPropertiesBotManagement {\n    /**\n     * Results of Cloudflare's Bot Management analysis\n     */\n    botManagement: IncomingRequestCfPropertiesBotManagementBase & {\n        /**\n         * A [JA3 Fingerprint](https://developers.cloudflare.com/bots/concepts/ja3-fingerprint/) to help profile specific SSL/TLS clients\n         * across different destination IPs, Ports, and X509 certificates.\n         */\n        ja3Hash: string;\n    };\n}\ninterface IncomingRequestCfPropertiesCloudflareForSaaSEnterprise<HostMetadata> {\n    /**\n     * Custom metadata set per-host in [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/).\n     *\n     * This field is only present if you have Cloudflare for SaaS enabled on your account\n     * and you have followed the [required steps to enable it]((https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/custom-metadata/)).\n     */\n    hostMetadata?: HostMetadata;\n}\ninterface IncomingRequestCfPropertiesCloudflareAccessOrApiShield {\n    /**\n     * Information about the client certificate presented to Cloudflare.\n     *\n     * This is populated when the incoming request is served over TLS using\n     * either Cloudflare Access or API Shield (mTLS)\n     * and the presented SSL certificate has a valid\n     * [Certificate Serial Number](https://ldapwiki.com/wiki/Certificate%20Serial%20Number)\n     * (i.e., not `null` or `\"\"`).\n     *\n     * Otherwise, a set of placeholder values are used.\n     *\n     * The property `certPresented` will be set to `\"1\"` when\n     * the object is populated (i.e. the above conditions were met).\n     */\n    tlsClientAuth: IncomingRequestCfPropertiesTLSClientAuth | IncomingRequestCfPropertiesTLSClientAuthPlaceholder;\n}\n/**\n * Metadata about the request's TLS handshake\n */\ninterface IncomingRequestCfPropertiesExportedAuthenticatorMetadata {\n    /**\n     * The client's [`HELLO` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2), encoded in hexadecimal\n     *\n     * @example \"44372ba35fa1270921d318f34c12f155dc87b682cf36a790cfaa3ba8737a1b5d\"\n     */\n    clientHandshake: string;\n    /**\n     * The server's [`HELLO` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2), encoded in hexadecimal\n     *\n     * @example \"44372ba35fa1270921d318f34c12f155dc87b682cf36a790cfaa3ba8737a1b5d\"\n     */\n    serverHandshake: string;\n    /**\n     * The client's [`FINISHED` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.9), encoded in hexadecimal\n     *\n     * @example \"084ee802fe1348f688220e2a6040a05b2199a761f33cf753abb1b006792d3f8b\"\n     */\n    clientFinished: string;\n    /**\n     * The server's [`FINISHED` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.9), encoded in hexadecimal\n     *\n     * @example \"084ee802fe1348f688220e2a6040a05b2199a761f33cf753abb1b006792d3f8b\"\n     */\n    serverFinished: string;\n}\n/**\n * Geographic data about the request's origin.\n */\ninterface IncomingRequestCfPropertiesGeographicInformation {\n    /**\n     * The [ISO 3166-1 Alpha 2](https://www.iso.org/iso-3166-country-codes.html) country code the request originated from.\n     *\n     * If your worker is [configured to accept TOR connections](https://support.cloudflare.com/hc/en-us/articles/203306930-Understanding-Cloudflare-Tor-support-and-Onion-Routing), this may also be `\"T1\"`, indicating a request that originated over TOR.\n     *\n     * If Cloudflare is unable to determine where the request originated this property is omitted.\n     *\n     * The country code `\"T1\"` is used for requests originating on TOR.\n     *\n     * @example \"GB\"\n     */\n    country?: Iso3166Alpha2Code | \"T1\";\n    /**\n     * If present, this property indicates that the request originated in the EU\n     *\n     * @example \"1\"\n     */\n    isEUCountry?: \"1\";\n    /**\n     * A two-letter code indicating the continent the request originated from.\n     *\n     * @example \"AN\"\n     */\n    continent?: ContinentCode;\n    /**\n     * The city the request originated from\n     *\n     * @example \"Austin\"\n     */\n    city?: string;\n    /**\n     * Postal code of the incoming request\n     *\n     * @example \"78701\"\n     */\n    postalCode?: string;\n    /**\n     * Latitude of the incoming request\n     *\n     * @example \"30.27130\"\n     */\n    latitude?: string;\n    /**\n     * Longitude of the incoming request\n     *\n     * @example \"-97.74260\"\n     */\n    longitude?: string;\n    /**\n     * Timezone of the incoming request\n     *\n     * @example \"America/Chicago\"\n     */\n    timezone?: string;\n    /**\n     * If known, the ISO 3166-2 name for the first level region associated with\n     * the IP address of the incoming request\n     *\n     * @example \"Texas\"\n     */\n    region?: string;\n    /**\n     * If known, the ISO 3166-2 code for the first-level region associated with\n     * the IP address of the incoming request\n     *\n     * @example \"TX\"\n     */\n    regionCode?: string;\n    /**\n     * Metro code (DMA) of the incoming request\n     *\n     * @example \"635\"\n     */\n    metroCode?: string;\n}\n/** Data about the incoming request's TLS certificate */\ninterface IncomingRequestCfPropertiesTLSClientAuth {\n    /** Always `\"1\"`, indicating that the certificate was presented */\n    certPresented: \"1\";\n    /**\n     * Result of certificate verification.\n     *\n     * @example \"FAILED:self signed certificate\"\n     */\n    certVerified: Exclude<CertVerificationStatus, \"NONE\">;\n    /** The presented certificate's revokation status.\n     *\n     * - A value of `\"1\"` indicates the certificate has been revoked\n     * - A value of `\"0\"` indicates the certificate has not been revoked\n     */\n    certRevoked: \"1\" | \"0\";\n    /**\n     * The certificate issuer's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html)\n     *\n     * @example \"CN=cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare\"\n     */\n    certIssuerDN: string;\n    /**\n     * The certificate subject's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html)\n     *\n     * @example \"CN=*.cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare\"\n     */\n    certSubjectDN: string;\n    /**\n     * The certificate issuer's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) ([RFC 2253](https://www.rfc-editor.org/rfc/rfc2253.html) formatted)\n     *\n     * @example \"CN=cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare\"\n     */\n    certIssuerDNRFC2253: string;\n    /**\n     * The certificate subject's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) ([RFC 2253](https://www.rfc-editor.org/rfc/rfc2253.html) formatted)\n     *\n     * @example \"CN=*.cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare\"\n     */\n    certSubjectDNRFC2253: string;\n    /** The certificate issuer's distinguished name (legacy policies) */\n    certIssuerDNLegacy: string;\n    /** The certificate subject's distinguished name (legacy policies) */\n    certSubjectDNLegacy: string;\n    /**\n     * The certificate's serial number\n     *\n     * @example \"00936EACBE07F201DF\"\n     */\n    certSerial: string;\n    /**\n     * The certificate issuer's serial number\n     *\n     * @example \"2489002934BDFEA34\"\n     */\n    certIssuerSerial: string;\n    /**\n     * The certificate's Subject Key Identifier\n     *\n     * @example \"BB:AF:7E:02:3D:FA:A6:F1:3C:84:8E:AD:EE:38:98:EC:D9:32:32:D4\"\n     */\n    certSKI: string;\n    /**\n     * The certificate issuer's Subject Key Identifier\n     *\n     * @example \"BB:AF:7E:02:3D:FA:A6:F1:3C:84:8E:AD:EE:38:98:EC:D9:32:32:D4\"\n     */\n    certIssuerSKI: string;\n    /**\n     * The certificate's SHA-1 fingerprint\n     *\n     * @example \"6b9109f323999e52259cda7373ff0b4d26bd232e\"\n     */\n    certFingerprintSHA1: string;\n    /**\n     * The certificate's SHA-256 fingerprint\n     *\n     * @example \"acf77cf37b4156a2708e34c4eb755f9b5dbbe5ebb55adfec8f11493438d19e6ad3f157f81fa3b98278453d5652b0c1fd1d71e5695ae4d709803a4d3f39de9dea\"\n     */\n    certFingerprintSHA256: string;\n    /**\n     * The effective starting date of the certificate\n     *\n     * @example \"Dec 22 19:39:00 2018 GMT\"\n     */\n    certNotBefore: string;\n    /**\n     * The effective expiration date of the certificate\n     *\n     * @example \"Dec 22 19:39:00 2018 GMT\"\n     */\n    certNotAfter: string;\n}\n/** Placeholder values for TLS Client Authorization */\ninterface IncomingRequestCfPropertiesTLSClientAuthPlaceholder {\n    certPresented: \"0\";\n    certVerified: \"NONE\";\n    certRevoked: \"0\";\n    certIssuerDN: \"\";\n    certSubjectDN: \"\";\n    certIssuerDNRFC2253: \"\";\n    certSubjectDNRFC2253: \"\";\n    certIssuerDNLegacy: \"\";\n    certSubjectDNLegacy: \"\";\n    certSerial: \"\";\n    certIssuerSerial: \"\";\n    certSKI: \"\";\n    certIssuerSKI: \"\";\n    certFingerprintSHA1: \"\";\n    certFingerprintSHA256: \"\";\n    certNotBefore: \"\";\n    certNotAfter: \"\";\n}\n/** Possible outcomes of TLS verification */\ndeclare type CertVerificationStatus = \n/** Authentication succeeded */\n\"SUCCESS\"\n/** No certificate was presented */\n | \"NONE\"\n/** Failed because the certificate was self-signed */\n | \"FAILED:self signed certificate\"\n/** Failed because the certificate failed a trust chain check */\n | \"FAILED:unable to verify the first certificate\"\n/** Failed because the certificate not yet valid */\n | \"FAILED:certificate is not yet valid\"\n/** Failed because the certificate is expired */\n | \"FAILED:certificate has expired\"\n/** Failed for another unspecified reason */\n | \"FAILED\";\n/**\n * An upstream endpoint's response to a TCP `keepalive` message from Cloudflare.\n */\ndeclare type IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus = 0 /** Unknown */ | 1 /** no keepalives (not found) */ | 2 /** no connection re-use, opening keepalive connection failed */ | 3 /** no connection re-use, keepalive accepted and saved */ | 4 /** connection re-use, refused by the origin server (`TCP FIN`) */ | 5; /** connection re-use, accepted by the origin server */\n/** ISO 3166-1 Alpha-2 codes */\ndeclare type Iso3166Alpha2Code = \"AD\" | \"AE\" | \"AF\" | \"AG\" | \"AI\" | \"AL\" | \"AM\" | \"AO\" | \"AQ\" | \"AR\" | \"AS\" | \"AT\" | \"AU\" | \"AW\" | \"AX\" | \"AZ\" | \"BA\" | \"BB\" | \"BD\" | \"BE\" | \"BF\" | \"BG\" | \"BH\" | \"BI\" | \"BJ\" | \"BL\" | \"BM\" | \"BN\" | \"BO\" | \"BQ\" | \"BR\" | \"BS\" | \"BT\" | \"BV\" | \"BW\" | \"BY\" | \"BZ\" | \"CA\" | \"CC\" | \"CD\" | \"CF\" | \"CG\" | \"CH\" | \"CI\" | \"CK\" | \"CL\" | \"CM\" | \"CN\" | \"CO\" | \"CR\" | \"CU\" | \"CV\" | \"CW\" | \"CX\" | \"CY\" | \"CZ\" | \"DE\" | \"DJ\" | \"DK\" | \"DM\" | \"DO\" | \"DZ\" | \"EC\" | \"EE\" | \"EG\" | \"EH\" | \"ER\" | \"ES\" | \"ET\" | \"FI\" | \"FJ\" | \"FK\" | \"FM\" | \"FO\" | \"FR\" | \"GA\" | \"GB\" | \"GD\" | \"GE\" | \"GF\" | \"GG\" | \"GH\" | \"GI\" | \"GL\" | \"GM\" | \"GN\" | \"GP\" | \"GQ\" | \"GR\" | \"GS\" | \"GT\" | \"GU\" | \"GW\" | \"GY\" | \"HK\" | \"HM\" | \"HN\" | \"HR\" | \"HT\" | \"HU\" | \"ID\" | \"IE\" | \"IL\" | \"IM\" | \"IN\" | \"IO\" | \"IQ\" | \"IR\" | \"IS\" | \"IT\" | \"JE\" | \"JM\" | \"JO\" | \"JP\" | \"KE\" | \"KG\" | \"KH\" | \"KI\" | \"KM\" | \"KN\" | \"KP\" | \"KR\" | \"KW\" | \"KY\" | \"KZ\" | \"LA\" | \"LB\" | \"LC\" | \"LI\" | \"LK\" | \"LR\" | \"LS\" | \"LT\" | \"LU\" | \"LV\" | \"LY\" | \"MA\" | \"MC\" | \"MD\" | \"ME\" | \"MF\" | \"MG\" | \"MH\" | \"MK\" | \"ML\" | \"MM\" | \"MN\" | \"MO\" | \"MP\" | \"MQ\" | \"MR\" | \"MS\" | \"MT\" | \"MU\" | \"MV\" | \"MW\" | \"MX\" | \"MY\" | \"MZ\" | \"NA\" | \"NC\" | \"NE\" | \"NF\" | \"NG\" | \"NI\" | \"NL\" | \"NO\" | \"NP\" | \"NR\" | \"NU\" | \"NZ\" | \"OM\" | \"PA\" | \"PE\" | \"PF\" | \"PG\" | \"PH\" | \"PK\" | \"PL\" | \"PM\" | \"PN\" | \"PR\" | \"PS\" | \"PT\" | \"PW\" | \"PY\" | \"QA\" | \"RE\" | \"RO\" | \"RS\" | \"RU\" | \"RW\" | \"SA\" | \"SB\" | \"SC\" | \"SD\" | \"SE\" | \"SG\" | \"SH\" | \"SI\" | \"SJ\" | \"SK\" | \"SL\" | \"SM\" | \"SN\" | \"SO\" | \"SR\" | \"SS\" | \"ST\" | \"SV\" | \"SX\" | \"SY\" | \"SZ\" | \"TC\" | \"TD\" | \"TF\" | \"TG\" | \"TH\" | \"TJ\" | \"TK\" | \"TL\" | \"TM\" | \"TN\" | \"TO\" | \"TR\" | \"TT\" | \"TV\" | \"TW\" | \"TZ\" | \"UA\" | \"UG\" | \"UM\" | \"US\" | \"UY\" | \"UZ\" | \"VA\" | \"VC\" | \"VE\" | \"VG\" | \"VI\" | \"VN\" | \"VU\" | \"WF\" | \"WS\" | \"YE\" | \"YT\" | \"ZA\" | \"ZM\" | \"ZW\";\n/** The 2-letter continent codes Cloudflare uses */\ndeclare type ContinentCode = \"AF\" | \"AN\" | \"AS\" | \"EU\" | \"NA\" | \"OC\" | \"SA\";\ntype CfProperties<HostMetadata = unknown> = IncomingRequestCfProperties<HostMetadata> | RequestInitCfProperties;\ninterface D1Meta {\n    duration: number;\n    size_after: number;\n    rows_read: number;\n    rows_written: number;\n    last_row_id: number;\n    changed_db: boolean;\n    changes: number;\n    /**\n     * The region of the database instance that executed the query.\n     */\n    served_by_region?: string;\n    /**\n     * True if-and-only-if the database instance that executed the query was the primary.\n     */\n    served_by_primary?: boolean;\n    timings?: {\n        /**\n         * The duration of the SQL query execution by the database instance. It doesn't include any network time.\n         */\n        sql_duration_ms: number;\n    };\n    /**\n     * Number of total attempts to execute the query, due to automatic retries.\n     * Note: All other fields in the response like `timings` only apply to the last attempt.\n     */\n    total_attempts?: number;\n}\ninterface D1Response {\n    success: true;\n    meta: D1Meta & Record<string, unknown>;\n    error?: never;\n}\ntype D1Result<T = unknown> = D1Response & {\n    results: T[];\n};\ninterface D1ExecResult {\n    count: number;\n    duration: number;\n}\ntype D1SessionConstraint = \n// Indicates that the first query should go to the primary, and the rest queries\n// using the same D1DatabaseSession will go to any replica that is consistent with\n// the bookmark maintained by the session (returned by the first query).\n'first-primary'\n// Indicates that the first query can go anywhere (primary or replica), and the rest queries\n// using the same D1DatabaseSession will go to any replica that is consistent with\n// the bookmark maintained by the session (returned by the first query).\n | 'first-unconstrained';\ntype D1SessionBookmark = string;\ndeclare abstract class D1Database {\n    prepare(query: string): D1PreparedStatement;\n    batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;\n    exec(query: string): Promise<D1ExecResult>;\n    /**\n     * Creates a new D1 Session anchored at the given constraint or the bookmark.\n     * All queries executed using the created session will have sequential consistency,\n     * meaning that all writes done through the session will be visible in subsequent reads.\n     *\n     * @param constraintOrBookmark Either the session constraint or the explicit bookmark to anchor the created session.\n     */\n    withSession(constraintOrBookmark?: D1SessionBookmark | D1SessionConstraint): D1DatabaseSession;\n    /**\n     * @deprecated dump() will be removed soon, only applies to deprecated alpha v1 databases.\n     */\n    dump(): Promise<ArrayBuffer>;\n}\ndeclare abstract class D1DatabaseSession {\n    prepare(query: string): D1PreparedStatement;\n    batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;\n    /**\n     * @returns The latest session bookmark across all executed queries on the session.\n     *          If no query has been executed yet, `null` is returned.\n     */\n    getBookmark(): D1SessionBookmark | null;\n}\ndeclare abstract class D1PreparedStatement {\n    bind(...values: unknown[]): D1PreparedStatement;\n    first<T = unknown>(colName: string): Promise<T | null>;\n    first<T = Record<string, unknown>>(): Promise<T | null>;\n    run<T = Record<string, unknown>>(): Promise<D1Result<T>>;\n    all<T = Record<string, unknown>>(): Promise<D1Result<T>>;\n    raw<T = unknown[]>(options: {\n        columnNames: true;\n    }): Promise<[\n        string[],\n        ...T[]\n    ]>;\n    raw<T = unknown[]>(options?: {\n        columnNames?: false;\n    }): Promise<T[]>;\n}\n// `Disposable` was added to TypeScript's standard lib types in version 5.2.\n// To support older TypeScript versions, define an empty `Disposable` interface.\n// Users won't be able to use `using`/`Symbol.dispose` without upgrading to 5.2,\n// but this will ensure type checking on older versions still passes.\n// TypeScript's interface merging will ensure our empty interface is effectively\n// ignored when `Disposable` is included in the standard lib.\ninterface Disposable {\n}\n/**\n * An email message that can be sent from a Worker.\n */\ninterface EmailMessage {\n    /**\n     * Envelope From attribute of the email message.\n     */\n    readonly from: string;\n    /**\n     * Envelope To attribute of the email message.\n     */\n    readonly to: string;\n}\n/**\n * An email message that is sent to a consumer Worker and can be rejected/forwarded.\n */\ninterface ForwardableEmailMessage extends EmailMessage {\n    /**\n     * Stream of the email message content.\n     */\n    readonly raw: ReadableStream<Uint8Array>;\n    /**\n     * An [Headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers).\n     */\n    readonly headers: Headers;\n    /**\n     * Size of the email message content.\n     */\n    readonly rawSize: number;\n    /**\n     * Reject this email message by returning a permanent SMTP error back to the connecting client including the given reason.\n     * @param reason The reject reason.\n     * @returns void\n     */\n    setReject(reason: string): void;\n    /**\n     * Forward this email message to a verified destination address of the account.\n     * @param rcptTo Verified destination address.\n     * @param headers A [Headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers).\n     * @returns A promise that resolves when the email message is forwarded.\n     */\n    forward(rcptTo: string, headers?: Headers): Promise<void>;\n    /**\n     * Reply to the sender of this email message with a new EmailMessage object.\n     * @param message The reply message.\n     * @returns A promise that resolves when the email message is replied.\n     */\n    reply(message: EmailMessage): Promise<void>;\n}\n/**\n * A binding that allows a Worker to send email messages.\n */\ninterface SendEmail {\n    send(message: EmailMessage): Promise<void>;\n}\ndeclare abstract class EmailEvent extends ExtendableEvent {\n    readonly message: ForwardableEmailMessage;\n}\ndeclare type EmailExportedHandler<Env = unknown> = (message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) => void | Promise<void>;\ndeclare module \"cloudflare:email\" {\n    let _EmailMessage: {\n        prototype: EmailMessage;\n        new (from: string, to: string, raw: ReadableStream | string): EmailMessage;\n    };\n    export { _EmailMessage as EmailMessage };\n}\n/**\n * Hello World binding to serve as an explanatory example. DO NOT USE\n */\ninterface HelloWorldBinding {\n    /**\n     * Retrieve the current stored value\n     */\n    get(): Promise<{\n        value: string;\n        ms?: number;\n    }>;\n    /**\n     * Set a new stored value\n     */\n    set(value: string): Promise<void>;\n}\ninterface Hyperdrive {\n    /**\n     * Connect directly to Hyperdrive as if it's your database, returning a TCP socket.\n     *\n     * Calling this method returns an idential socket to if you call\n     * `connect(\"host:port\")` using the `host` and `port` fields from this object.\n     * Pick whichever approach works better with your preferred DB client library.\n     *\n     * Note that this socket is not yet authenticated -- it's expected that your\n     * code (or preferably, the client library of your choice) will authenticate\n     * using the information in this class's readonly fields.\n     */\n    connect(): Socket;\n    /**\n     * A valid DB connection string that can be passed straight into the typical\n     * client library/driver/ORM. This will typically be the easiest way to use\n     * Hyperdrive.\n     */\n    readonly connectionString: string;\n    /*\n     * A randomly generated hostname that is only valid within the context of the\n     * currently running Worker which, when passed into `connect()` function from\n     * the \"cloudflare:sockets\" module, will connect to the Hyperdrive instance\n     * for your database.\n     */\n    readonly host: string;\n    /*\n     * The port that must be paired the the host field when connecting.\n     */\n    readonly port: number;\n    /*\n     * The username to use when authenticating to your database via Hyperdrive.\n     * Unlike the host and password, this will be the same every time\n     */\n    readonly user: string;\n    /*\n     * The randomly generated password to use when authenticating to your\n     * database via Hyperdrive. Like the host field, this password is only valid\n     * within the context of the currently running Worker instance from which\n     * it's read.\n     */\n    readonly password: string;\n    /*\n     * The name of the database to connect to.\n     */\n    readonly database: string;\n}\n// Copyright (c) 2024 Cloudflare, Inc.\n// Licensed under the Apache 2.0 license found in the LICENSE file or at:\n//     https://opensource.org/licenses/Apache-2.0\ntype ImageInfoResponse = {\n    format: 'image/svg+xml';\n} | {\n    format: string;\n    fileSize: number;\n    width: number;\n    height: number;\n};\ntype ImageTransform = {\n    width?: number;\n    height?: number;\n    background?: string;\n    blur?: number;\n    border?: {\n        color?: string;\n        width?: number;\n    } | {\n        top?: number;\n        bottom?: number;\n        left?: number;\n        right?: number;\n    };\n    brightness?: number;\n    contrast?: number;\n    fit?: 'scale-down' | 'contain' | 'pad' | 'squeeze' | 'cover' | 'crop';\n    flip?: 'h' | 'v' | 'hv';\n    gamma?: number;\n    segment?: 'foreground';\n    gravity?: 'face' | 'left' | 'right' | 'top' | 'bottom' | 'center' | 'auto' | 'entropy' | {\n        x?: number;\n        y?: number;\n        mode: 'remainder' | 'box-center';\n    };\n    rotate?: 0 | 90 | 180 | 270;\n    saturation?: number;\n    sharpen?: number;\n    trim?: 'border' | {\n        top?: number;\n        bottom?: number;\n        left?: number;\n        right?: number;\n        width?: number;\n        height?: number;\n        border?: boolean | {\n            color?: string;\n            tolerance?: number;\n            keep?: number;\n        };\n    };\n};\ntype ImageDrawOptions = {\n    opacity?: number;\n    repeat?: boolean | string;\n    top?: number;\n    left?: number;\n    bottom?: number;\n    right?: number;\n};\ntype ImageInputOptions = {\n    encoding?: 'base64';\n};\ntype ImageOutputOptions = {\n    format: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | 'image/avif' | 'rgb' | 'rgba';\n    quality?: number;\n    background?: string;\n    anim?: boolean;\n};\ninterface ImagesBinding {\n    /**\n     * Get image metadata (type, width and height)\n     * @throws {@link ImagesError} with code 9412 if input is not an image\n     * @param stream The image bytes\n     */\n    info(stream: ReadableStream<Uint8Array>, options?: ImageInputOptions): Promise<ImageInfoResponse>;\n    /**\n     * Begin applying a series of transformations to an image\n     * @param stream The image bytes\n     * @returns A transform handle\n     */\n    input(stream: ReadableStream<Uint8Array>, options?: ImageInputOptions): ImageTransformer;\n}\ninterface ImageTransformer {\n    /**\n     * Apply transform next, returning a transform handle.\n     * You can then apply more transformations, draw, or retrieve the output.\n     * @param transform\n     */\n    transform(transform: ImageTransform): ImageTransformer;\n    /**\n     * Draw an image on this transformer, returning a transform handle.\n     * You can then apply more transformations, draw, or retrieve the output.\n     * @param image The image (or transformer that will give the image) to draw\n     * @param options The options configuring how to draw the image\n     */\n    draw(image: ReadableStream<Uint8Array> | ImageTransformer, options?: ImageDrawOptions): ImageTransformer;\n    /**\n     * Retrieve the image that results from applying the transforms to the\n     * provided input\n     * @param options Options that apply to the output e.g. output format\n     */\n    output(options: ImageOutputOptions): Promise<ImageTransformationResult>;\n}\ntype ImageTransformationOutputOptions = {\n    encoding?: 'base64';\n};\ninterface ImageTransformationResult {\n    /**\n     * The image as a response, ready to store in cache or return to users\n     */\n    response(): Response;\n    /**\n     * The content type of the returned image\n     */\n    contentType(): string;\n    /**\n     * The bytes of the response\n     */\n    image(options?: ImageTransformationOutputOptions): ReadableStream<Uint8Array>;\n}\ninterface ImagesError extends Error {\n    readonly code: number;\n    readonly message: string;\n    readonly stack?: string;\n}\n/**\n * Media binding for transforming media streams.\n * Provides the entry point for media transformation operations.\n */\ninterface MediaBinding {\n    /**\n     * Creates a media transformer from an input stream.\n     * @param media - The input media bytes\n     * @returns A MediaTransformer instance for applying transformations\n     */\n    input(media: ReadableStream<Uint8Array>): MediaTransformer;\n}\n/**\n * Media transformer for applying transformation operations to media content.\n * Handles sizing, fitting, and other input transformation parameters.\n */\ninterface MediaTransformer {\n    /**\n     * Applies transformation options to the media content.\n     * @param transform - Configuration for how the media should be transformed\n     * @returns A generator for producing the transformed media output\n     */\n    transform(transform: MediaTransformationInputOptions): MediaTransformationGenerator;\n}\n/**\n * Generator for producing media transformation results.\n * Configures the output format and parameters for the transformed media.\n */\ninterface MediaTransformationGenerator {\n    /**\n     * Generates the final media output with specified options.\n     * @param output - Configuration for the output format and parameters\n     * @returns The final transformation result containing the transformed media\n     */\n    output(output: MediaTransformationOutputOptions): MediaTransformationResult;\n}\n/**\n * Result of a media transformation operation.\n * Provides multiple ways to access the transformed media content.\n */\ninterface MediaTransformationResult {\n    /**\n     * Returns the transformed media as a readable stream of bytes.\n     * @returns A stream containing the transformed media data\n     */\n    media(): ReadableStream<Uint8Array>;\n    /**\n     * Returns the transformed media as an HTTP response object.\n     * @returns The transformed media as a Response, ready to store in cache or return to users\n     */\n    response(): Response;\n    /**\n     * Returns the MIME type of the transformed media.\n     * @returns The content type string (e.g., 'image/jpeg', 'video/mp4')\n     */\n    contentType(): string;\n}\n/**\n * Configuration options for transforming media input.\n * Controls how the media should be resized and fitted.\n */\ntype MediaTransformationInputOptions = {\n    /** How the media should be resized to fit the specified dimensions */\n    fit?: 'contain' | 'cover' | 'scale-down';\n    /** Target width in pixels */\n    width?: number;\n    /** Target height in pixels */\n    height?: number;\n};\n/**\n * Configuration options for Media Transformations output.\n * Controls the format, timing, and type of the generated output.\n */\ntype MediaTransformationOutputOptions = {\n    /**\n     * Output mode determining the type of media to generate\n     */\n    mode?: 'video' | 'spritesheet' | 'frame' | 'audio';\n    /** Whether to include audio in the output */\n    audio?: boolean;\n    /**\n     * Starting timestamp for frame extraction or start time for clips. (e.g. '2s').\n     */\n    time?: string;\n    /**\n     * Duration for video clips, audio extraction, and spritesheet generation (e.g. '5s').\n     */\n    duration?: string;\n    /**\n     * Number of frames in the spritesheet.\n     */\n    imageCount?: number;\n    /**\n     * Output format for the generated media.\n     */\n    format?: 'jpg' | 'png' | 'm4a';\n};\n/**\n * Error object for media transformation operations.\n * Extends the standard Error interface with additional media-specific information.\n */\ninterface MediaError extends Error {\n    readonly code: number;\n    readonly message: string;\n    readonly stack?: string;\n}\ndeclare module 'cloudflare:node' {\n    interface NodeStyleServer {\n        listen(...args: unknown[]): this;\n        address(): {\n            port?: number | null | undefined;\n        };\n    }\n    export function httpServerHandler(port: number): ExportedHandler;\n    export function httpServerHandler(options: {\n        port: number;\n    }): ExportedHandler;\n    export function httpServerHandler(server: NodeStyleServer): ExportedHandler;\n}\ntype Params<P extends string = any> = Record<P, string | string[]>;\ntype EventContext<Env, P extends string, Data> = {\n    request: Request<unknown, IncomingRequestCfProperties<unknown>>;\n    functionPath: string;\n    waitUntil: (promise: Promise<any>) => void;\n    passThroughOnException: () => void;\n    next: (input?: Request | string, init?: RequestInit) => Promise<Response>;\n    env: Env & {\n        ASSETS: {\n            fetch: typeof fetch;\n        };\n    };\n    params: Params<P>;\n    data: Data;\n};\ntype PagesFunction<Env = unknown, Params extends string = any, Data extends Record<string, unknown> = Record<string, unknown>> = (context: EventContext<Env, Params, Data>) => Response | Promise<Response>;\ntype EventPluginContext<Env, P extends string, Data, PluginArgs> = {\n    request: Request<unknown, IncomingRequestCfProperties<unknown>>;\n    functionPath: string;\n    waitUntil: (promise: Promise<any>) => void;\n    passThroughOnException: () => void;\n    next: (input?: Request | string, init?: RequestInit) => Promise<Response>;\n    env: Env & {\n        ASSETS: {\n            fetch: typeof fetch;\n        };\n    };\n    params: Params<P>;\n    data: Data;\n    pluginArgs: PluginArgs;\n};\ntype PagesPluginFunction<Env = unknown, Params extends string = any, Data extends Record<string, unknown> = Record<string, unknown>, PluginArgs = unknown> = (context: EventPluginContext<Env, Params, Data, PluginArgs>) => Response | Promise<Response>;\ndeclare module \"assets:*\" {\n    export const onRequest: PagesFunction;\n}\n// Copyright (c) 2022-2023 Cloudflare, Inc.\n// Licensed under the Apache 2.0 license found in the LICENSE file or at:\n//     https://opensource.org/licenses/Apache-2.0\ndeclare module \"cloudflare:pipelines\" {\n    export abstract class PipelineTransformationEntrypoint<Env = unknown, I extends PipelineRecord = PipelineRecord, O extends PipelineRecord = PipelineRecord> {\n        protected env: Env;\n        protected ctx: ExecutionContext;\n        constructor(ctx: ExecutionContext, env: Env);\n        /**\n         * run recieves an array of PipelineRecord which can be\n         * transformed and returned to the pipeline\n         * @param records Incoming records from the pipeline to be transformed\n         * @param metadata Information about the specific pipeline calling the transformation entrypoint\n         * @returns A promise containing the transformed PipelineRecord array\n         */\n        public run(records: I[], metadata: PipelineBatchMetadata): Promise<O[]>;\n    }\n    export type PipelineRecord = Record<string, unknown>;\n    export type PipelineBatchMetadata = {\n        pipelineId: string;\n        pipelineName: string;\n    };\n    export interface Pipeline<T extends PipelineRecord = PipelineRecord> {\n        /**\n         * The Pipeline interface represents the type of a binding to a Pipeline\n         *\n         * @param records The records to send to the pipeline\n         */\n        send(records: T[]): Promise<void>;\n    }\n}\n// PubSubMessage represents an incoming PubSub message.\n// The message includes metadata about the broker, the client, and the payload\n// itself.\n// https://developers.cloudflare.com/pub-sub/\ninterface PubSubMessage {\n    // Message ID\n    readonly mid: number;\n    // MQTT broker FQDN in the form mqtts://BROKER.NAMESPACE.cloudflarepubsub.com:PORT\n    readonly broker: string;\n    // The MQTT topic the message was sent on.\n    readonly topic: string;\n    // The client ID of the client that published this message.\n    readonly clientId: string;\n    // The unique identifier (JWT ID) used by the client to authenticate, if token\n    // auth was used.\n    readonly jti?: string;\n    // A Unix timestamp (seconds from Jan 1, 1970), set when the Pub/Sub Broker\n    // received the message from the client.\n    readonly receivedAt: number;\n    // An (optional) string with the MIME type of the payload, if set by the\n    // client.\n    readonly contentType: string;\n    // Set to 1 when the payload is a UTF-8 string\n    // https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901063\n    readonly payloadFormatIndicator: number;\n    // Pub/Sub (MQTT) payloads can be UTF-8 strings, or byte arrays.\n    // You can use payloadFormatIndicator to inspect this before decoding.\n    payload: string | Uint8Array;\n}\n// JsonWebKey extended by kid parameter\ninterface JsonWebKeyWithKid extends JsonWebKey {\n    // Key Identifier of the JWK\n    readonly kid: string;\n}\ninterface RateLimitOptions {\n    key: string;\n}\ninterface RateLimitOutcome {\n    success: boolean;\n}\ninterface RateLimit {\n    /**\n     * Rate limit a request based on the provided options.\n     * @see https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/\n     * @returns A promise that resolves with the outcome of the rate limit.\n     */\n    limit(options: RateLimitOptions): Promise<RateLimitOutcome>;\n}\n// Namespace for RPC utility types. Unfortunately, we can't use a `module` here as these types need\n// to referenced by `Fetcher`. This is included in the \"importable\" version of the types which\n// strips all `module` blocks.\ndeclare namespace Rpc {\n    // Branded types for identifying `WorkerEntrypoint`/`DurableObject`/`Target`s.\n    // TypeScript uses *structural* typing meaning anything with the same shape as type `T` is a `T`.\n    // For the classes exported by `cloudflare:workers` we want *nominal* typing (i.e. we only want to\n    // accept `WorkerEntrypoint` from `cloudflare:workers`, not any other class with the same shape)\n    export const __RPC_STUB_BRAND: '__RPC_STUB_BRAND';\n    export const __RPC_TARGET_BRAND: '__RPC_TARGET_BRAND';\n    export const __WORKER_ENTRYPOINT_BRAND: '__WORKER_ENTRYPOINT_BRAND';\n    export const __DURABLE_OBJECT_BRAND: '__DURABLE_OBJECT_BRAND';\n    export const __WORKFLOW_ENTRYPOINT_BRAND: '__WORKFLOW_ENTRYPOINT_BRAND';\n    export interface RpcTargetBranded {\n        [__RPC_TARGET_BRAND]: never;\n    }\n    export interface WorkerEntrypointBranded {\n        [__WORKER_ENTRYPOINT_BRAND]: never;\n    }\n    export interface DurableObjectBranded {\n        [__DURABLE_OBJECT_BRAND]: never;\n    }\n    export interface WorkflowEntrypointBranded {\n        [__WORKFLOW_ENTRYPOINT_BRAND]: never;\n    }\n    export type EntrypointBranded = WorkerEntrypointBranded | DurableObjectBranded | WorkflowEntrypointBranded;\n    // Types that can be used through `Stub`s\n    export type Stubable = RpcTargetBranded | ((...args: any[]) => any);\n    // Types that can be passed over RPC\n    // The reason for using a generic type here is to build a serializable subset of structured\n    //   cloneable composite types. This allows types defined with the \"interface\" keyword to pass the\n    //   serializable check as well. Otherwise, only types defined with the \"type\" keyword would pass.\n    type Serializable<T> = \n    // Structured cloneables\n    BaseType\n    // Structured cloneable composites\n     | Map<T extends Map<infer U, unknown> ? Serializable<U> : never, T extends Map<unknown, infer U> ? Serializable<U> : never> | Set<T extends Set<infer U> ? Serializable<U> : never> | ReadonlyArray<T extends ReadonlyArray<infer U> ? Serializable<U> : never> | {\n        [K in keyof T]: K extends number | string ? Serializable<T[K]> : never;\n    }\n    // Special types\n     | Stub<Stubable>\n    // Serialized as stubs, see `Stubify`\n     | Stubable;\n    // Base type for all RPC stubs, including common memory management methods.\n    // `T` is used as a marker type for unwrapping `Stub`s later.\n    interface StubBase<T extends Stubable> extends Disposable {\n        [__RPC_STUB_BRAND]: T;\n        dup(): this;\n    }\n    export type Stub<T extends Stubable> = Provider<T> & StubBase<T>;\n    // This represents all the types that can be sent as-is over an RPC boundary\n    type BaseType = void | undefined | null | boolean | number | bigint | string | TypedArray | ArrayBuffer | DataView | Date | Error | RegExp | ReadableStream<Uint8Array> | WritableStream<Uint8Array> | Request | Response | Headers;\n    // Recursively rewrite all `Stubable` types with `Stub`s\n    // prettier-ignore\n    type Stubify<T> = T extends Stubable ? Stub<T> : T extends Map<infer K, infer V> ? Map<Stubify<K>, Stubify<V>> : T extends Set<infer V> ? Set<Stubify<V>> : T extends Array<infer V> ? Array<Stubify<V>> : T extends ReadonlyArray<infer V> ? ReadonlyArray<Stubify<V>> : T extends BaseType ? T : T extends {\n        [key: string | number]: any;\n    } ? {\n        [K in keyof T]: Stubify<T[K]>;\n    } : T;\n    // Recursively rewrite all `Stub<T>`s with the corresponding `T`s.\n    // Note we use `StubBase` instead of `Stub` here to avoid circular dependencies:\n    // `Stub` depends on `Provider`, which depends on `Unstubify`, which would depend on `Stub`.\n    // prettier-ignore\n    type Unstubify<T> = T extends StubBase<infer V> ? V : T extends Map<infer K, infer V> ? Map<Unstubify<K>, Unstubify<V>> : T extends Set<infer V> ? Set<Unstubify<V>> : T extends Array<infer V> ? Array<Unstubify<V>> : T extends ReadonlyArray<infer V> ? ReadonlyArray<Unstubify<V>> : T extends BaseType ? T : T extends {\n        [key: string | number]: unknown;\n    } ? {\n        [K in keyof T]: Unstubify<T[K]>;\n    } : T;\n    type UnstubifyAll<A extends any[]> = {\n        [I in keyof A]: Unstubify<A[I]>;\n    };\n    // Utility type for adding `Provider`/`Disposable`s to `object` types only.\n    // Note `unknown & T` is equivalent to `T`.\n    type MaybeProvider<T> = T extends object ? Provider<T> : unknown;\n    type MaybeDisposable<T> = T extends object ? Disposable : unknown;\n    // Type for method return or property on an RPC interface.\n    // - Stubable types are replaced by stubs.\n    // - Serializable types are passed by value, with stubable types replaced by stubs\n    //   and a top-level `Disposer`.\n    // Everything else can't be passed over PRC.\n    // Technically, we use custom thenables here, but they quack like `Promise`s.\n    // Intersecting with `(Maybe)Provider` allows pipelining.\n    // prettier-ignore\n    type Result<R> = R extends Stubable ? Promise<Stub<R>> & Provider<R> : R extends Serializable<R> ? Promise<Stubify<R> & MaybeDisposable<R>> & MaybeProvider<R> : never;\n    // Type for method or property on an RPC interface.\n    // For methods, unwrap `Stub`s in parameters, and rewrite returns to be `Result`s.\n    // Unwrapping `Stub`s allows calling with `Stubable` arguments.\n    // For properties, rewrite types to be `Result`s.\n    // In each case, unwrap `Promise`s.\n    type MethodOrProperty<V> = V extends (...args: infer P) => infer R ? (...args: UnstubifyAll<P>) => Result<Awaited<R>> : Result<Awaited<V>>;\n    // Type for the callable part of an `Provider` if `T` is callable.\n    // This is intersected with methods/properties.\n    type MaybeCallableProvider<T> = T extends (...args: any[]) => any ? MethodOrProperty<T> : unknown;\n    // Base type for all other types providing RPC-like interfaces.\n    // Rewrites all methods/properties to be `MethodOrProperty`s, while preserving callable types.\n    // `Reserved` names (e.g. stub method names like `dup()`) and symbols can't be accessed over RPC.\n    export type Provider<T extends object, Reserved extends string = never> = MaybeCallableProvider<T> & {\n        [K in Exclude<keyof T, Reserved | symbol | keyof StubBase<never>>]: MethodOrProperty<T[K]>;\n    };\n}\ndeclare namespace Cloudflare {\n    // Type of `env`.\n    //\n    // The specific project can extend `Env` by redeclaring it in project-specific files. Typescript\n    // will merge all declarations.\n    //\n    // You can use `wrangler types` to generate the `Env` type automatically.\n    interface Env {\n    }\n    // Project-specific parameters used to inform types.\n    //\n    // This interface is, again, intended to be declared in project-specific files, and then that\n    // declaration will be merged with this one.\n    //\n    // A project should have a declaration like this:\n    //\n    //     interface GlobalProps {\n    //       // Declares the main module's exports. Used to populate Cloudflare.Exports aka the type\n    //       // of `ctx.exports`.\n    //       mainModule: typeof import(\"my-main-module\");\n    //\n    //       // Declares which of the main module's exports are configured with durable storage, and\n    //       // thus should behave as Durable Object namsepace bindings.\n    //       durableNamespaces: \"MyDurableObject\" | \"AnotherDurableObject\";\n    //     }\n    //\n    // You can use `wrangler types` to generate `GlobalProps` automatically.\n    interface GlobalProps {\n    }\n    // Evaluates to the type of a property in GlobalProps, defaulting to `Default` if it is not\n    // present.\n    type GlobalProp<K extends string, Default> = K extends keyof GlobalProps ? GlobalProps[K] : Default;\n    // The type of the program's main module exports, if known. Requires `GlobalProps` to declare the\n    // `mainModule` property.\n    type MainModule = GlobalProp<\"mainModule\", {}>;\n    // The type of ctx.exports, which contains loopback bindings for all top-level exports.\n    type Exports = {\n        [K in keyof MainModule]: LoopbackForExport<MainModule[K]>\n        // If the export is listed in `durableNamespaces`, then it is also a\n        // DurableObjectNamespace.\n         & (K extends GlobalProp<\"durableNamespaces\", never> ? MainModule[K] extends new (...args: any[]) => infer DoInstance ? DoInstance extends Rpc.DurableObjectBranded ? DurableObjectNamespace<DoInstance> : DurableObjectNamespace<undefined> : DurableObjectNamespace<undefined> : {});\n    };\n}\ndeclare namespace CloudflareWorkersModule {\n    export type RpcStub<T extends Rpc.Stubable> = Rpc.Stub<T>;\n    export const RpcStub: {\n        new <T extends Rpc.Stubable>(value: T): Rpc.Stub<T>;\n    };\n    export abstract class RpcTarget implements Rpc.RpcTargetBranded {\n        [Rpc.__RPC_TARGET_BRAND]: never;\n    }\n    // `protected` fields don't appear in `keyof`s, so can't be accessed over RPC\n    export abstract class WorkerEntrypoint<Env = Cloudflare.Env, Props = {}> implements Rpc.WorkerEntrypointBranded {\n        [Rpc.__WORKER_ENTRYPOINT_BRAND]: never;\n        protected ctx: ExecutionContext<Props>;\n        protected env: Env;\n        constructor(ctx: ExecutionContext, env: Env);\n        email?(message: ForwardableEmailMessage): void | Promise<void>;\n        fetch?(request: Request): Response | Promise<Response>;\n        queue?(batch: MessageBatch<unknown>): void | Promise<void>;\n        scheduled?(controller: ScheduledController): void | Promise<void>;\n        tail?(events: TraceItem[]): void | Promise<void>;\n        tailStream?(event: TailStream.TailEvent<TailStream.Onset>): TailStream.TailEventHandlerType | Promise<TailStream.TailEventHandlerType>;\n        test?(controller: TestController): void | Promise<void>;\n        trace?(traces: TraceItem[]): void | Promise<void>;\n    }\n    export abstract class DurableObject<Env = Cloudflare.Env, Props = {}> implements Rpc.DurableObjectBranded {\n        [Rpc.__DURABLE_OBJECT_BRAND]: never;\n        protected ctx: DurableObjectState<Props>;\n        protected env: Env;\n        constructor(ctx: DurableObjectState, env: Env);\n        alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise<void>;\n        fetch?(request: Request): Response | Promise<Response>;\n        webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise<void>;\n        webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise<void>;\n        webSocketError?(ws: WebSocket, error: unknown): void | Promise<void>;\n    }\n    export type WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';\n    export type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number;\n    export type WorkflowDelayDuration = WorkflowSleepDuration;\n    export type WorkflowTimeoutDuration = WorkflowSleepDuration;\n    export type WorkflowRetentionDuration = WorkflowSleepDuration;\n    export type WorkflowBackoff = 'constant' | 'linear' | 'exponential';\n    export type WorkflowStepConfig = {\n        retries?: {\n            limit: number;\n            delay: WorkflowDelayDuration | number;\n            backoff?: WorkflowBackoff;\n        };\n        timeout?: WorkflowTimeoutDuration | number;\n    };\n    export type WorkflowEvent<T> = {\n        payload: Readonly<T>;\n        timestamp: Date;\n        instanceId: string;\n    };\n    export type WorkflowStepEvent<T> = {\n        payload: Readonly<T>;\n        timestamp: Date;\n        type: string;\n    };\n    export abstract class WorkflowStep {\n        do<T extends Rpc.Serializable<T>>(name: string, callback: () => Promise<T>): Promise<T>;\n        do<T extends Rpc.Serializable<T>>(name: string, config: WorkflowStepConfig, callback: () => Promise<T>): Promise<T>;\n        sleep: (name: string, duration: WorkflowSleepDuration) => Promise<void>;\n        sleepUntil: (name: string, timestamp: Date | number) => Promise<void>;\n        waitForEvent<T extends Rpc.Serializable<T>>(name: string, options: {\n            type: string;\n            timeout?: WorkflowTimeoutDuration | number;\n        }): Promise<WorkflowStepEvent<T>>;\n    }\n    export abstract class WorkflowEntrypoint<Env = unknown, T extends Rpc.Serializable<T> | unknown = unknown> implements Rpc.WorkflowEntrypointBranded {\n        [Rpc.__WORKFLOW_ENTRYPOINT_BRAND]: never;\n        protected ctx: ExecutionContext;\n        protected env: Env;\n        constructor(ctx: ExecutionContext, env: Env);\n        run(event: Readonly<WorkflowEvent<T>>, step: WorkflowStep): Promise<unknown>;\n    }\n    export function waitUntil(promise: Promise<unknown>): void;\n    export const env: Cloudflare.Env;\n}\ndeclare module 'cloudflare:workers' {\n    export = CloudflareWorkersModule;\n}\ninterface SecretsStoreSecret {\n    /**\n     * Get a secret from the Secrets Store, returning a string of the secret value\n     * if it exists, or throws an error if it does not exist\n     */\n    get(): Promise<string>;\n}\ndeclare module \"cloudflare:sockets\" {\n    function _connect(address: string | SocketAddress, options?: SocketOptions): Socket;\n    export { _connect as connect };\n}\ntype MarkdownDocument = {\n    name: string;\n    blob: Blob;\n};\ntype ConversionResponse = {\n    name: string;\n    mimeType: string;\n    format: 'markdown';\n    tokens: number;\n    data: string;\n} | {\n    name: string;\n    mimeType: string;\n    format: 'error';\n    error: string;\n};\ntype ImageConversionOptions = {\n    descriptionLanguage?: 'en' | 'es' | 'fr' | 'it' | 'pt' | 'de';\n};\ntype EmbeddedImageConversionOptions = ImageConversionOptions & {\n    convert?: boolean;\n    maxConvertedImages?: number;\n};\ntype ConversionOptions = {\n    html?: {\n        images?: EmbeddedImageConversionOptions & {\n            convertOGImage?: boolean;\n        };\n    };\n    docx?: {\n        images?: EmbeddedImageConversionOptions;\n    };\n    image?: ImageConversionOptions;\n    pdf?: {\n        images?: EmbeddedImageConversionOptions;\n        metadata?: boolean;\n    };\n};\ntype ConversionRequestOptions = {\n    gateway?: GatewayOptions;\n    extraHeaders?: object;\n    conversionOptions?: ConversionOptions;\n};\ntype SupportedFileFormat = {\n    mimeType: string;\n    extension: string;\n};\ndeclare abstract class ToMarkdownService {\n    transform(files: MarkdownDocument[], options?: ConversionRequestOptions): Promise<ConversionResponse[]>;\n    transform(files: MarkdownDocument, options?: ConversionRequestOptions): Promise<ConversionResponse>;\n    supported(): Promise<SupportedFileFormat[]>;\n}\ndeclare namespace TailStream {\n    interface Header {\n        readonly name: string;\n        readonly value: string;\n    }\n    interface FetchEventInfo {\n        readonly type: \"fetch\";\n        readonly method: string;\n        readonly url: string;\n        readonly cfJson?: object;\n        readonly headers: Header[];\n    }\n    interface JsRpcEventInfo {\n        readonly type: \"jsrpc\";\n    }\n    interface ScheduledEventInfo {\n        readonly type: \"scheduled\";\n        readonly scheduledTime: Date;\n        readonly cron: string;\n    }\n    interface AlarmEventInfo {\n        readonly type: \"alarm\";\n        readonly scheduledTime: Date;\n    }\n    interface QueueEventInfo {\n        readonly type: \"queue\";\n        readonly queueName: string;\n        readonly batchSize: number;\n    }\n    interface EmailEventInfo {\n        readonly type: \"email\";\n        readonly mailFrom: string;\n        readonly rcptTo: string;\n        readonly rawSize: number;\n    }\n    interface TraceEventInfo {\n        readonly type: \"trace\";\n        readonly traces: (string | null)[];\n    }\n    interface HibernatableWebSocketEventInfoMessage {\n        readonly type: \"message\";\n    }\n    interface HibernatableWebSocketEventInfoError {\n        readonly type: \"error\";\n    }\n    interface HibernatableWebSocketEventInfoClose {\n        readonly type: \"close\";\n        readonly code: number;\n        readonly wasClean: boolean;\n    }\n    interface HibernatableWebSocketEventInfo {\n        readonly type: \"hibernatableWebSocket\";\n        readonly info: HibernatableWebSocketEventInfoClose | HibernatableWebSocketEventInfoError | HibernatableWebSocketEventInfoMessage;\n    }\n    interface CustomEventInfo {\n        readonly type: \"custom\";\n    }\n    interface FetchResponseInfo {\n        readonly type: \"fetch\";\n        readonly statusCode: number;\n    }\n    type EventOutcome = \"ok\" | \"canceled\" | \"exception\" | \"unknown\" | \"killSwitch\" | \"daemonDown\" | \"exceededCpu\" | \"exceededMemory\" | \"loadShed\" | \"responseStreamDisconnected\" | \"scriptNotFound\";\n    interface ScriptVersion {\n        readonly id: string;\n        readonly tag?: string;\n        readonly message?: string;\n    }\n    interface Onset {\n        readonly type: \"onset\";\n        readonly attributes: Attribute[];\n        // id for the span being opened by this Onset event.\n        readonly spanId: string;\n        readonly dispatchNamespace?: string;\n        readonly entrypoint?: string;\n        readonly executionModel: string;\n        readonly scriptName?: string;\n        readonly scriptTags?: string[];\n        readonly scriptVersion?: ScriptVersion;\n        readonly info: FetchEventInfo | JsRpcEventInfo | ScheduledEventInfo | AlarmEventInfo | QueueEventInfo | EmailEventInfo | TraceEventInfo | HibernatableWebSocketEventInfo | CustomEventInfo;\n    }\n    interface Outcome {\n        readonly type: \"outcome\";\n        readonly outcome: EventOutcome;\n        readonly cpuTime: number;\n        readonly wallTime: number;\n    }\n    interface SpanOpen {\n        readonly type: \"spanOpen\";\n        readonly name: string;\n        // id for the span being opened by this SpanOpen event.\n        readonly spanId: string;\n        readonly info?: FetchEventInfo | JsRpcEventInfo | Attributes;\n    }\n    interface SpanClose {\n        readonly type: \"spanClose\";\n        readonly outcome: EventOutcome;\n    }\n    interface DiagnosticChannelEvent {\n        readonly type: \"diagnosticChannel\";\n        readonly channel: string;\n        readonly message: any;\n    }\n    interface Exception {\n        readonly type: \"exception\";\n        readonly name: string;\n        readonly message: string;\n        readonly stack?: string;\n    }\n    interface Log {\n        readonly type: \"log\";\n        readonly level: \"debug\" | \"error\" | \"info\" | \"log\" | \"warn\";\n        readonly message: object;\n    }\n    // This marks the worker handler return information.\n    // This is separate from Outcome because the worker invocation can live for a long time after\n    // returning. For example - Websockets that return an http upgrade response but then continue\n    // streaming information or SSE http connections.\n    interface Return {\n        readonly type: \"return\";\n        readonly info?: FetchResponseInfo;\n    }\n    interface Attribute {\n        readonly name: string;\n        readonly value: string | string[] | boolean | boolean[] | number | number[] | bigint | bigint[];\n    }\n    interface Attributes {\n        readonly type: \"attributes\";\n        readonly info: Attribute[];\n    }\n    type EventType = Onset | Outcome | SpanOpen | SpanClose | DiagnosticChannelEvent | Exception | Log | Return | Attributes;\n    // Context in which this trace event lives.\n    interface SpanContext {\n        // Single id for the entire top-level invocation\n        // This should be a new traceId for the first worker stage invoked in the eyeball request and then\n        // same-account service-bindings should reuse the same traceId but cross-account service-bindings\n        // should use a new traceId.\n        readonly traceId: string;\n        // spanId in which this event is handled\n        // for Onset and SpanOpen events this would be the parent span id\n        // for Outcome and SpanClose these this would be the span id of the opening Onset and SpanOpen events\n        // For Hibernate and Mark this would be the span under which they were emitted.\n        // spanId is not set ONLY if:\n        //  1. This is an Onset event\n        //  2. We are not inherting any SpanContext. (e.g. this is a cross-account service binding or a new top-level invocation)\n        readonly spanId?: string;\n    }\n    interface TailEvent<Event extends EventType> {\n        // invocation id of the currently invoked worker stage.\n        // invocation id will always be unique to every Onset event and will be the same until the Outcome event.\n        readonly invocationId: string;\n        // Inherited spanContext for this event.\n        readonly spanContext: SpanContext;\n        readonly timestamp: Date;\n        readonly sequence: number;\n        readonly event: Event;\n    }\n    type TailEventHandler<Event extends EventType = EventType> = (event: TailEvent<Event>) => void | Promise<void>;\n    type TailEventHandlerObject = {\n        outcome?: TailEventHandler<Outcome>;\n        spanOpen?: TailEventHandler<SpanOpen>;\n        spanClose?: TailEventHandler<SpanClose>;\n        diagnosticChannel?: TailEventHandler<DiagnosticChannelEvent>;\n        exception?: TailEventHandler<Exception>;\n        log?: TailEventHandler<Log>;\n        return?: TailEventHandler<Return>;\n        attributes?: TailEventHandler<Attributes>;\n    };\n    type TailEventHandlerType = TailEventHandler | TailEventHandlerObject;\n}\n// Copyright (c) 2022-2023 Cloudflare, Inc.\n// Licensed under the Apache 2.0 license found in the LICENSE file or at:\n//     https://opensource.org/licenses/Apache-2.0\n/**\n * Data types supported for holding vector metadata.\n */\ntype VectorizeVectorMetadataValue = string | number | boolean | string[];\n/**\n * Additional information to associate with a vector.\n */\ntype VectorizeVectorMetadata = VectorizeVectorMetadataValue | Record<string, VectorizeVectorMetadataValue>;\ntype VectorFloatArray = Float32Array | Float64Array;\ninterface VectorizeError {\n    code?: number;\n    error: string;\n}\n/**\n * Comparison logic/operation to use for metadata filtering.\n *\n * This list is expected to grow as support for more operations are released.\n */\ntype VectorizeVectorMetadataFilterOp = '$eq' | '$ne' | '$lt' | '$lte' | '$gt' | '$gte';\ntype VectorizeVectorMetadataFilterCollectionOp = '$in' | '$nin';\n/**\n * Filter criteria for vector metadata used to limit the retrieved query result set.\n */\ntype VectorizeVectorMetadataFilter = {\n    [field: string]: Exclude<VectorizeVectorMetadataValue, string[]> | null | {\n        [Op in VectorizeVectorMetadataFilterOp]?: Exclude<VectorizeVectorMetadataValue, string[]> | null;\n    } | {\n        [Op in VectorizeVectorMetadataFilterCollectionOp]?: Exclude<VectorizeVectorMetadataValue, string[]>[];\n    };\n};\n/**\n * Supported distance metrics for an index.\n * Distance metrics determine how other \"similar\" vectors are determined.\n */\ntype VectorizeDistanceMetric = \"euclidean\" | \"cosine\" | \"dot-product\";\n/**\n * Metadata return levels for a Vectorize query.\n *\n * Default to \"none\".\n *\n * @property all      Full metadata for the vector return set, including all fields (including those un-indexed) without truncation. This is a more expensive retrieval, as it requires additional fetching & reading of un-indexed data.\n * @property indexed  Return all metadata fields configured for indexing in the vector return set. This level of retrieval is \"free\" in that no additional overhead is incurred returning this data. However, note that indexed metadata is subject to truncation (especially for larger strings).\n * @property none     No indexed metadata will be returned.\n */\ntype VectorizeMetadataRetrievalLevel = \"all\" | \"indexed\" | \"none\";\ninterface VectorizeQueryOptions {\n    topK?: number;\n    namespace?: string;\n    returnValues?: boolean;\n    returnMetadata?: boolean | VectorizeMetadataRetrievalLevel;\n    filter?: VectorizeVectorMetadataFilter;\n}\n/**\n * Information about the configuration of an index.\n */\ntype VectorizeIndexConfig = {\n    dimensions: number;\n    metric: VectorizeDistanceMetric;\n} | {\n    preset: string; // keep this generic, as we'll be adding more presets in the future and this is only in a read capacity\n};\n/**\n * Metadata about an existing index.\n *\n * This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released.\n * See {@link VectorizeIndexInfo} for its post-beta equivalent.\n */\ninterface VectorizeIndexDetails {\n    /** The unique ID of the index */\n    readonly id: string;\n    /** The name of the index. */\n    name: string;\n    /** (optional) A human readable description for the index. */\n    description?: string;\n    /** The index configuration, including the dimension size and distance metric. */\n    config: VectorizeIndexConfig;\n    /** The number of records containing vectors within the index. */\n    vectorsCount: number;\n}\n/**\n * Metadata about an existing index.\n */\ninterface VectorizeIndexInfo {\n    /** The number of records containing vectors within the index. */\n    vectorCount: number;\n    /** Number of dimensions the index has been configured for. */\n    dimensions: number;\n    /** ISO 8601 datetime of the last processed mutation on in the index. All changes before this mutation will be reflected in the index state. */\n    processedUpToDatetime: number;\n    /** UUIDv4 of the last mutation processed by the index. All changes before this mutation will be reflected in the index state. */\n    processedUpToMutation: number;\n}\n/**\n * Represents a single vector value set along with its associated metadata.\n */\ninterface VectorizeVector {\n    /** The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents. */\n    id: string;\n    /** The vector values */\n    values: VectorFloatArray | number[];\n    /** The namespace this vector belongs to. */\n    namespace?: string;\n    /** Metadata associated with the vector. Includes the values of other fields and potentially additional details. */\n    metadata?: Record<string, VectorizeVectorMetadata>;\n}\n/**\n * Represents a matched vector for a query along with its score and (if specified) the matching vector information.\n */\ntype VectorizeMatch = Pick<Partial<VectorizeVector>, \"values\"> & Omit<VectorizeVector, \"values\"> & {\n    /** The score or rank for similarity, when returned as a result */\n    score: number;\n};\n/**\n * A set of matching {@link VectorizeMatch} for a particular query.\n */\ninterface VectorizeMatches {\n    matches: VectorizeMatch[];\n    count: number;\n}\n/**\n * Results of an operation that performed a mutation on a set of vectors.\n * Here, `ids` is a list of vectors that were successfully processed.\n *\n * This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released.\n * See {@link VectorizeAsyncMutation} for its post-beta equivalent.\n */\ninterface VectorizeVectorMutation {\n    /* List of ids of vectors that were successfully processed. */\n    ids: string[];\n    /* Total count of the number of processed vectors. */\n    count: number;\n}\n/**\n * Result type indicating a mutation on the Vectorize Index.\n * Actual mutations are processed async where the `mutationId` is the unique identifier for the operation.\n */\ninterface VectorizeAsyncMutation {\n    /** The unique identifier for the async mutation operation containing the changeset. */\n    mutationId: string;\n}\n/**\n * A Vectorize Vector Search Index for querying vectors/embeddings.\n *\n * This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released.\n * See {@link Vectorize} for its new implementation.\n */\ndeclare abstract class VectorizeIndex {\n    /**\n     * Get information about the currently bound index.\n     * @returns A promise that resolves with information about the current index.\n     */\n    public describe(): Promise<VectorizeIndexDetails>;\n    /**\n     * Use the provided vector to perform a similarity search across the index.\n     * @param vector Input vector that will be used to drive the similarity search.\n     * @param options Configuration options to massage the returned data.\n     * @returns A promise that resolves with matched and scored vectors.\n     */\n    public query(vector: VectorFloatArray | number[], options?: VectorizeQueryOptions): Promise<VectorizeMatches>;\n    /**\n     * Insert a list of vectors into the index dataset. If a provided id exists, an error will be thrown.\n     * @param vectors List of vectors that will be inserted.\n     * @returns A promise that resolves with the ids & count of records that were successfully processed.\n     */\n    public insert(vectors: VectorizeVector[]): Promise<VectorizeVectorMutation>;\n    /**\n     * Upsert a list of vectors into the index dataset. If a provided id exists, it will be replaced with the new values.\n     * @param vectors List of vectors that will be upserted.\n     * @returns A promise that resolves with the ids & count of records that were successfully processed.\n     */\n    public upsert(vectors: VectorizeVector[]): Promise<VectorizeVectorMutation>;\n    /**\n     * Delete a list of vectors with a matching id.\n     * @param ids List of vector ids that should be deleted.\n     * @returns A promise that resolves with the ids & count of records that were successfully processed (and thus deleted).\n     */\n    public deleteByIds(ids: string[]): Promise<VectorizeVectorMutation>;\n    /**\n     * Get a list of vectors with a matching id.\n     * @param ids List of vector ids that should be returned.\n     * @returns A promise that resolves with the raw unscored vectors matching the id set.\n     */\n    public getByIds(ids: string[]): Promise<VectorizeVector[]>;\n}\n/**\n * A Vectorize Vector Search Index for querying vectors/embeddings.\n *\n * Mutations in this version are async, returning a mutation id.\n */\ndeclare abstract class Vectorize {\n    /**\n     * Get information about the currently bound index.\n     * @returns A promise that resolves with information about the current index.\n     */\n    public describe(): Promise<VectorizeIndexInfo>;\n    /**\n     * Use the provided vector to perform a similarity search across the index.\n     * @param vector Input vector that will be used to drive the similarity search.\n     * @param options Configuration options to massage the returned data.\n     * @returns A promise that resolves with matched and scored vectors.\n     */\n    public query(vector: VectorFloatArray | number[], options?: VectorizeQueryOptions): Promise<VectorizeMatches>;\n    /**\n     * Use the provided vector-id to perform a similarity search across the index.\n     * @param vectorId Id for a vector in the index against which the index should be queried.\n     * @param options Configuration options to massage the returned data.\n     * @returns A promise that resolves with matched and scored vectors.\n     */\n    public queryById(vectorId: string, options?: VectorizeQueryOptions): Promise<VectorizeMatches>;\n    /**\n     * Insert a list of vectors into the index dataset. If a provided id exists, an error will be thrown.\n     * @param vectors List of vectors that will be inserted.\n     * @returns A promise that resolves with a unique identifier of a mutation containing the insert changeset.\n     */\n    public insert(vectors: VectorizeVector[]): Promise<VectorizeAsyncMutation>;\n    /**\n     * Upsert a list of vectors into the index dataset. If a provided id exists, it will be replaced with the new values.\n     * @param vectors List of vectors that will be upserted.\n     * @returns A promise that resolves with a unique identifier of a mutation containing the upsert changeset.\n     */\n    public upsert(vectors: VectorizeVector[]): Promise<VectorizeAsyncMutation>;\n    /**\n     * Delete a list of vectors with a matching id.\n     * @param ids List of vector ids that should be deleted.\n     * @returns A promise that resolves with a unique identifier of a mutation containing the delete changeset.\n     */\n    public deleteByIds(ids: string[]): Promise<VectorizeAsyncMutation>;\n    /**\n     * Get a list of vectors with a matching id.\n     * @param ids List of vector ids that should be returned.\n     * @returns A promise that resolves with the raw unscored vectors matching the id set.\n     */\n    public getByIds(ids: string[]): Promise<VectorizeVector[]>;\n}\n/**\n * The interface for \"version_metadata\" binding\n * providing metadata about the Worker Version using this binding.\n */\ntype WorkerVersionMetadata = {\n    /** The ID of the Worker Version using this binding */\n    id: string;\n    /** The tag of the Worker Version using this binding */\n    tag: string;\n    /** The timestamp of when the Worker Version was uploaded */\n    timestamp: string;\n};\ninterface DynamicDispatchLimits {\n    /**\n     * Limit CPU time in milliseconds.\n     */\n    cpuMs?: number;\n    /**\n     * Limit number of subrequests.\n     */\n    subRequests?: number;\n}\ninterface DynamicDispatchOptions {\n    /**\n     * Limit resources of invoked Worker script.\n     */\n    limits?: DynamicDispatchLimits;\n    /**\n     * Arguments for outbound Worker script, if configured.\n     */\n    outbound?: {\n        [key: string]: any;\n    };\n}\ninterface DispatchNamespace {\n    /**\n    * @param name Name of the Worker script.\n    * @param args Arguments to Worker script.\n    * @param options Options for Dynamic Dispatch invocation.\n    * @returns A Fetcher object that allows you to send requests to the Worker script.\n    * @throws If the Worker script does not exist in this dispatch namespace, an error will be thrown.\n    */\n    get(name: string, args?: {\n        [key: string]: any;\n    }, options?: DynamicDispatchOptions): Fetcher;\n}\ndeclare module 'cloudflare:workflows' {\n    /**\n     * NonRetryableError allows for a user to throw a fatal error\n     * that makes a Workflow instance fail immediately without triggering a retry\n     */\n    export class NonRetryableError extends Error {\n        public constructor(message: string, name?: string);\n    }\n}\ndeclare abstract class Workflow<PARAMS = unknown> {\n    /**\n     * Get a handle to an existing instance of the Workflow.\n     * @param id Id for the instance of this Workflow\n     * @returns A promise that resolves with a handle for the Instance\n     */\n    public get(id: string): Promise<WorkflowInstance>;\n    /**\n     * Create a new instance and return a handle to it. If a provided id exists, an error will be thrown.\n     * @param options Options when creating an instance including id and params\n     * @returns A promise that resolves with a handle for the Instance\n     */\n    public create(options?: WorkflowInstanceCreateOptions<PARAMS>): Promise<WorkflowInstance>;\n    /**\n     * Create a batch of instances and return handle for all of them. If a provided id exists, an error will be thrown.\n     * `createBatch` is limited at 100 instances at a time or when the RPC limit for the batch (1MiB) is reached.\n     * @param batch List of Options when creating an instance including name and params\n     * @returns A promise that resolves with a list of handles for the created instances.\n     */\n    public createBatch(batch: WorkflowInstanceCreateOptions<PARAMS>[]): Promise<WorkflowInstance[]>;\n}\ntype WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';\ntype WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number;\ntype WorkflowRetentionDuration = WorkflowSleepDuration;\ninterface WorkflowInstanceCreateOptions<PARAMS = unknown> {\n    /**\n     * An id for your Workflow instance. Must be unique within the Workflow.\n     */\n    id?: string;\n    /**\n     * The event payload the Workflow instance is triggered with\n     */\n    params?: PARAMS;\n    /**\n     * The retention policy for Workflow instance.\n     * Defaults to the maximum retention period available for the owner's account.\n     */\n    retention?: {\n        successRetention?: WorkflowRetentionDuration;\n        errorRetention?: WorkflowRetentionDuration;\n    };\n}\ntype InstanceStatus = {\n    status: 'queued' // means that instance is waiting to be started (see concurrency limits)\n     | 'running' | 'paused' | 'errored' | 'terminated' // user terminated the instance while it was running\n     | 'complete' | 'waiting' // instance is hibernating and waiting for sleep or event to finish\n     | 'waitingForPause' // instance is finishing the current work to pause\n     | 'unknown';\n    error?: string;\n    output?: object;\n};\ninterface WorkflowError {\n    code?: number;\n    message: string;\n}\ndeclare abstract class WorkflowInstance {\n    public id: string;\n    /**\n     * Pause the instance.\n     */\n    public pause(): Promise<void>;\n    /**\n     * Resume the instance. If it is already running, an error will be thrown.\n     */\n    public resume(): Promise<void>;\n    /**\n     * Terminate the instance. If it is errored, terminated or complete, an error will be thrown.\n     */\n    public terminate(): Promise<void>;\n    /**\n     * Restart the instance.\n     */\n    public restart(): Promise<void>;\n    /**\n     * Returns the current status of the instance.\n     */\n    public status(): Promise<InstanceStatus>;\n    /**\n     * Send an event to this instance.\n     */\n    public sendEvent({ type, payload, }: {\n        type: string;\n        payload: unknown;\n    }): Promise<void>;\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/worker.ts",
    "content": "import { router } from './app/router.ts'\n\nexport default {\n  async fetch(request): Promise<Response> {\n    try {\n      return await router.fetch(request)\n    } catch (error) {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  },\n} satisfies ExportedHandler<Env>\n"
  },
  {
    "path": "packages/fetch-router/demos/cf-workers/wrangler.jsonc",
    "content": "/**\n * For more details on how to configure Wrangler, refer to:\n * https://developers.cloudflare.com/workers/wrangler/configuration/\n */\n{\n  \"$schema\": \"node_modules/wrangler/config-schema.json\",\n  \"name\": \"fetch-router-cf-workers-demo\",\n  \"main\": \"worker.ts\",\n  \"compatibility_date\": \"2025-11-18\",\n  \"observability\": {\n    \"enabled\": true,\n  },\n  \"d1_databases\": [\n    {\n      \"binding\": \"DB\",\n      \"database_name\": \"fetch-router-blog\",\n      \"database_id\": \"local\",\n      \"migrations_dir\": \"migrations\",\n    },\n  ],\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/node/README.md",
    "content": "# fetch-router Node Example\n\nThis example is a [Node.js server](https://nodejs.org/) that handles routing using `@remix-run/fetch-router`.\n"
  },
  {
    "path": "packages/fetch-router/demos/node/app/data.ts",
    "content": "export interface Post {\n  id: string\n  title: string\n  content: string\n  author: string\n  createdAt: Date\n}\n\nlet posts: Post[] = [\n  {\n    id: '1',\n    title: 'Welcome to the Blog',\n    content: 'This is a simple blog demo built with fetch-router on Node.js.',\n    author: 'Admin',\n    createdAt: new Date('2025-01-01'),\n  },\n  {\n    id: '2',\n    title: 'Getting Started with fetch-router',\n    content: 'fetch-router is a minimal, composable router built on the web Fetch API.',\n    author: 'Admin',\n    createdAt: new Date('2025-01-02'),\n  },\n]\n\nexport function getPosts() {\n  return posts.toSorted((a, b) => b.createdAt.getTime() - a.createdAt.getTime())\n}\n\nexport function getPost(id: string) {\n  return posts.find((p) => p.id === id)\n}\n\nexport function createPost(title: string, content: string, author: string) {\n  let post: Post = {\n    id: String(posts.length + 1),\n    title,\n    content,\n    author,\n    createdAt: new Date(),\n  }\n  posts.push(post)\n  return post\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/node/app/router.ts",
    "content": "import { createRouter } from '@remix-run/fetch-router'\nimport { createCookie } from '@remix-run/cookie'\nimport * as s from '@remix-run/data-schema'\nimport * as f from '@remix-run/data-schema/form-data'\nimport { Session } from '@remix-run/session'\nimport { createCookieSessionStorage } from '@remix-run/session/cookie-storage'\nimport { formData } from '@remix-run/form-data-middleware'\nimport { logger } from '@remix-run/logger-middleware'\nimport { session } from '@remix-run/session-middleware'\nimport { html } from '@remix-run/html-template'\nimport { createHtmlResponse } from '@remix-run/response/html'\nimport { createRedirectResponse as redirect } from '@remix-run/response/redirect'\nimport type { Middleware } from '@remix-run/fetch-router'\n\nimport { routes } from './routes.ts'\nimport * as data from './data.ts'\n\nconst textField = f.field(s.defaulted(s.string(), ''))\nconst loginSchema = f.object({\n  username: textField,\n})\nconst postSchema = f.object({\n  title: textField,\n  content: textField,\n})\n\nlet sessionCookie = createCookie('__sess', {\n  secrets: ['s3cr3t'],\n})\n\nlet sessionStorage = createCookieSessionStorage()\n\nfunction requireAuth(): Middleware {\n  return ({ get }, next) => {\n    let session = get(Session)\n    let username = session.get('username')\n    if (!username) {\n      return redirect(routes.login.index.href())\n    }\n    return next()\n  }\n}\n\nexport let router = createRouter({\n  middleware: [logger(), formData(), session(sessionCookie, sessionStorage)],\n})\n\nrouter.map(routes.home, ({ get }) => {\n  let session = get(Session)\n  let posts = data.getPosts()\n  let username = session.get('username') as string | undefined\n\n  return createHtmlResponse(html`\n    <html>\n      <head>\n        <title>Simple Blog - fetch-router Demo</title>\n        <meta charset=\"utf-8\" />\n      </head>\n      <body>\n        <nav>\n          <h1>Simple Blog</h1>\n          <div>\n            ${username\n              ? html`\n                  <span>Hello, ${username}!</span>\n                  <form\n                    method=\"POST\"\n                    action=\"${routes.logout.href()}\"\n                    style=\"display: inline; margin-left: 10px;\"\n                  >\n                    <button type=\"submit\">Logout</button>\n                  </form>\n                  <a href=\"${routes.posts.new.href()}\" style=\"margin-left: 10px;\">New Post</a>\n                `\n              : html`<a href=\"${routes.login.index.href()}\">Login</a>`}\n          </div>\n        </nav>\n        <main>\n          ${posts.length === 0 ? html`<p>No posts yet.</p>` : null}\n          ${posts.map(\n            (post) => html`\n              <article>\n                <h2><a href=\"${routes.posts.show.href({ id: post.id })}\">${post.title}</a></h2>\n                <p>${post.content.substring(0, 150)}${post.content.length > 150 ? '...' : ''}</p>\n                <div>By ${post.author} on ${post.createdAt.toLocaleDateString()}</div>\n              </article>\n            `,\n          )}\n        </main>\n      </body>\n    </html>\n  `)\n})\n\nrouter.map(routes.login, {\n  actions: {\n    index({ get }) {\n      let session = get(Session)\n      let username = session.get('username') as string | undefined\n      if (username) {\n        return redirect(routes.home.href())\n      }\n\n      return createHtmlResponse(`\n        <html>\n          <head>\n            <title>Login - Simple Blog</title>\n            <meta charset=\"utf-8\" />\n          </head>\n          <body>\n            <h1>Login</h1>\n            <p>Enter any username to login (no password required for demo)</p>\n            <form method=\"POST\" action=\"${routes.login.action.href()}\">\n              <div style=\"display: flex; flex-direction: column; gap: 10px; width: 150px;\">\n                <label for=\"username\">Username:</label>\n                <input type=\"text\" id=\"username\" name=\"username\" required />\n                <label for=\"password\">Password:</label>\n                <input type=\"password\" id=\"password\" name=\"password\" required />\n              </div>\n              <br />\n              <button type=\"submit\">Login</button>\n            </form>\n            <p><a href=\"${routes.home.href()}\">← Back to Home</a></p>\n          </body>\n        </html>\n      `)\n    },\n    async action({ get }) {\n      let session = get(Session)\n      let formData = get(FormData)\n      let { username } = s.parse(loginSchema, formData)\n      if (!username) {\n        return redirect(routes.login.index.href())\n      }\n      session.set('username', username)\n      return redirect(routes.home.href())\n    },\n  },\n})\n\nrouter.post(routes.logout, ({ get }) => {\n  let session = get(Session)\n  session.destroy()\n  return redirect(routes.home.href())\n})\n\nrouter.map(routes.posts, {\n  actions: {\n    new: {\n      middleware: [requireAuth()],\n      action() {\n        return createHtmlResponse(`\n          <html>\n            <head>\n              <title>New Post - Simple Blog</title>\n              <meta charset=\"utf-8\" />\n            </head>\n            <body>\n              <h1>New Post</h1>\n              <form method=\"POST\" action=\"${routes.posts.create.href()}\">\n                <div>\n                  <label for=\"title\">Title:</label>\n                  <input type=\"text\" id=\"title\" name=\"title\" required />\n                </div>\n                <div>\n                  <label for=\"content\">Content:</label>\n                  <textarea id=\"content\" name=\"content\" required></textarea>\n                </div>\n                <button type=\"submit\">Create Post</button>\n              </form>\n              <p><a href=\"${routes.home.href()}\">← Back to Home</a></p>\n            </body>\n          </html>\n        `)\n      },\n    },\n    async create({ get }) {\n      let session = get(Session)\n      let formData = get(FormData)\n      let username = session.get('username') as string\n      if (!username) {\n        return redirect(routes.login.index.href())\n      }\n\n      let { content, title } = s.parse(postSchema, formData)\n\n      if (!title || !content) {\n        return redirect(routes.posts.new.href())\n      }\n\n      let post = data.createPost(title, content, username)\n      return redirect(routes.posts.show.href({ id: post.id }))\n    },\n    show({ params }) {\n      let post = data.getPost(params.id)\n      if (!post) {\n        return new Response('Post not found', { status: 404 })\n      }\n\n      return createHtmlResponse(html`\n        <html>\n          <head>\n            <title>${post.title} - Simple Blog</title>\n            <meta charset=\"utf-8\" />\n          </head>\n          <body>\n            <h1>${post.title}</h1>\n            <div>By ${post.author} on ${post.createdAt.toLocaleDateString()}</div>\n            <div>${post.content.replace(/\\n/g, '<br>')}</div>\n            <p><a href=\"${routes.home.href()}\">← Back to Home</a></p>\n          </body>\n        </html>\n      `)\n    },\n  },\n})\n"
  },
  {
    "path": "packages/fetch-router/demos/node/app/routes.ts",
    "content": "import { route, form, resources } from '@remix-run/fetch-router/routes'\n\nexport let routes = route({\n  home: '/',\n  login: form('/login'),\n  logout: { method: 'POST', pattern: '/logout' },\n  posts: resources('posts', { only: ['new', 'create', 'show'] }),\n})\n"
  },
  {
    "path": "packages/fetch-router/demos/node/package.json",
    "content": "{\n  \"name\": \"fetch-router-node-demo\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@remix-run/cookie\": \"workspace:*\",\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/form-data-middleware\": \"workspace:*\",\n    \"@remix-run/html-template\": \"workspace:*\",\n    \"@remix-run/logger-middleware\": \"workspace:*\",\n    \"@remix-run/node-fetch-server\": \"workspace:*\",\n    \"@remix-run/response\": \"workspace:*\",\n    \"@remix-run/session\": \"workspace:*\",\n    \"@remix-run/session-middleware\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"dev\": \"node --watch server.ts\",\n    \"start\": \"node server.ts\",\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "packages/fetch-router/demos/node/server.ts",
    "content": "import * as http from 'node:http'\nimport { createRequestListener } from '@remix-run/node-fetch-server'\n\nimport { router } from './app/router.ts'\n\nconst PORT = 44100\n\nlet server = http.createServer(\n  createRequestListener(async (request) => {\n    try {\n      return await router.fetch(request)\n    } catch (error) {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  }),\n)\n\nserver.listen(PORT, () => {\n  console.log(`Server running at http://localhost:${PORT}`)\n})\n"
  },
  {
    "path": "packages/fetch-router/demos/node/tsconfig.json",
    "content": "{\n  \"include\": [\"server.ts\", \"../../global.d.ts\"],\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/fetch-router/package.json",
    "content": "{\n  \"name\": \"@remix-run/fetch-router\",\n  \"version\": \"0.17.0\",\n  \"description\": \"A minimal, composable router for the web Fetch API\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/fetch-router\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/fetch-router#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"SPEC.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./routes\": \"./src/routes.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./routes\": {\n        \"types\": \"./dist/routes.d.ts\",\n        \"default\": \"./dist/routes.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/data-schema\": \"workspace:^\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/route-pattern\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"routing\",\n    \"middleware\",\n    \"koa\",\n    \"url\",\n    \"pattern\",\n    \"match\"\n  ]\n}\n"
  },
  {
    "path": "packages/fetch-router/src/index.ts",
    "content": "export { createContextKey } from './lib/request-context.ts'\n\nexport type { Controller, Action, BuildAction, RequestHandler } from './lib/controller.ts'\n\nexport type { Middleware, NextFunction } from './lib/middleware.ts'\n\nexport { RequestContext } from './lib/request-context.ts'\n\nexport { RequestMethods } from './lib/request-methods.ts'\nexport type { RequestMethod } from './lib/request-methods.ts'\n\nexport { createRouter } from './lib/router.ts'\nexport type { MatchData, Router, RouterOptions } from './lib/router.ts'\n"
  },
  {
    "path": "packages/fetch-router/src/lib/controller.ts",
    "content": "import type { Params, RoutePattern } from '@remix-run/route-pattern'\n\nimport type { Middleware } from './middleware.ts'\nimport type { RequestContext } from './request-context.ts'\nimport type { RequestMethod } from './request-methods.ts'\nimport type { Route, RouteMap } from './route-map.ts'\n\n/**\n * Controller object that mirrors a route map with matching action handlers.\n */\nexport type Controller<routes extends RouteMap> = {\n  actions: ControllerActions<routes>\n  middleware?: Middleware[]\n}\n\n// prettier-ignore\ntype ControllerActions<routes extends RouteMap> = routes extends any ?\n  {\n    [name in keyof routes]: (\n      routes[name] extends Route<infer method extends RequestMethod | 'ANY', infer pattern extends string> ? Action<method, pattern> :\n      routes[name] extends RouteMap ? Controller<routes[name]> :\n      never\n    )\n  } :\n  never\n\n/**\n * An individual route action.\n */\nexport type Action<method extends RequestMethod | 'ANY', pattern extends string> =\n  | RequestHandlerObject<method, Params<pattern>>\n  | RequestHandler<method, Params<pattern>>\n\ntype RequestHandlerObject<\n  method extends RequestMethod | 'ANY',\n  params extends Record<string, any>,\n> = {\n  middleware?: Middleware<method, params>[]\n  action: RequestHandler<method, params>\n}\n\n/**\n * Build an {@link Action} type from a string, {@link RoutePattern}, or {@link Route}.\n */\n// prettier-ignore\nexport type BuildAction<method extends RequestMethod | 'ANY', route extends string | RoutePattern | Route> =\n  route extends string ? Action<method, route> :\n  route extends RoutePattern<infer pattern> ? Action<method, pattern> :\n  route extends Route<infer _, infer pattern> ? Action<method, pattern> :\n  never\n\n/**\n * A request handler function that returns some kind of response.\n *\n * @param context The request context\n * @returns The response\n */\nexport interface RequestHandler<\n  method extends RequestMethod | 'ANY' = RequestMethod | 'ANY',\n  params extends Record<string, any> = {},\n> {\n  /**\n   * Handles a matched request and returns the response.\n   */\n  (context: RequestContext<params>): Response | Promise<Response>\n}\n\n/**\n * Runtime shape for a controller.\n */\nexport interface ControllerShape {\n  actions: Record<string, unknown>\n  middleware?: Middleware[]\n}\n\n/**\n * Check if an object has an `actions` property (controller).\n *\n * @param obj The object to check\n * @returns `true` if the object is a controller\n */\nexport function isController(obj: unknown): obj is ControllerShape {\n  return typeof obj === 'object' && obj != null && 'actions' in obj\n}\n\n/**\n * Runtime shape for an action object.\n */\nexport interface ActionObjectShape {\n  middleware?: Middleware[]\n  action: RequestHandler<any, any>\n}\n\n/**\n * Check if an object has an `action` property (action object).\n *\n * @param obj The object to check\n * @returns `true` if the object is an action object\n */\nexport function isActionObject(obj: unknown): obj is ActionObjectShape {\n  return typeof obj === 'object' && obj != null && 'action' in obj\n}\n"
  },
  {
    "path": "packages/fetch-router/src/lib/middleware.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { runMiddleware } from './middleware.ts'\nimport type { NextFunction } from './middleware.ts'\nimport { RequestContext } from './request-context.ts'\n\nfunction mockContext(input: string | Request, params: Record<string, any> = {}): RequestContext {\n  let context =\n    input instanceof Request ? new RequestContext(input) : new RequestContext(new Request(input))\n  context.params = params\n  return context\n}\n\ndescribe('runMiddleware', () => {\n  it('runs middleware and returns a response', async () => {\n    let middleware = [() => new Response('Hello, world!')]\n    let context = mockContext('https://remix.run')\n    let handler = () => new Response('Hello, world!')\n\n    let response = await runMiddleware(middleware, context, handler)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, world!')\n  })\n\n  it('runs middleware in order from left to right', async () => {\n    let requestLog: string[] = []\n\n    let middleware = [\n      (_: any, next: NextFunction) => {\n        requestLog.push('one')\n        return next()\n      },\n      (_: any, next: NextFunction) => {\n        requestLog.push('two')\n        return next()\n      },\n    ]\n    let context = mockContext('https://remix.run')\n    let handler = () => new Response('Hello, world!')\n\n    let response = await runMiddleware(middleware, context, handler)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, world!')\n    assert.deepEqual(requestLog, ['one', 'two'])\n  })\n\n  it('short-circuits the chain when a middleware returns a response', async () => {\n    let requestLog: string[] = []\n\n    let middleware = [\n      () => {\n        requestLog.push('one')\n        return new Response('Hello, middleware!')\n      },\n      (_: any, next: NextFunction) => {\n        requestLog.push('two') // we never get here\n        return next()\n      },\n    ]\n    let context = mockContext('https://remix.run')\n    let handler = () => new Response('Hello, world!')\n\n    let response = await runMiddleware(middleware, context, handler)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, middleware!')\n\n    assert.deepEqual(requestLog, ['one'])\n  })\n\n  it('invokes downstream automatically when a middleware does not call next()', async () => {\n    let requestLog: string[] = []\n\n    let middleware = [\n      () => {\n        requestLog.push('one')\n        // no next()\n      },\n      () => {\n        requestLog.push('two')\n        // no next()\n      },\n    ]\n    let context = mockContext('https://remix.run')\n    let handler = () => new Response('Hello, world!')\n\n    let response = await runMiddleware(middleware, context, handler)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, world!')\n    assert.deepEqual(requestLog, ['one', 'two'])\n  })\n\n  it('rejects when a middleware calls next() multiple times', async () => {\n    let middleware = [\n      async (_: any, next: NextFunction) => {\n        await next()\n        await next() // error\n      },\n    ]\n    let context = mockContext('https://remix.run')\n    let handler = () => new Response('Hello, world!')\n\n    await assert.rejects(async () => {\n      await runMiddleware(middleware, context, handler)\n    }, new Error('next() called multiple times'))\n  })\n\n  it('rejects when a handler throws an error', async () => {\n    let middleware = [() => {}]\n    let context = mockContext('https://remix.run')\n    let handler = () => {\n      throw new Error('Handler error!')\n    }\n\n    await assert.rejects(async () => {\n      await runMiddleware(middleware, context, handler)\n    }, new Error('Handler error!'))\n  })\n\n  it('rejects when a middleware throws an error', async () => {\n    let middleware = [\n      () => {\n        throw new Error('Middleware error!')\n      },\n    ]\n    let context = mockContext('https://remix.run')\n    let handler = () => new Response('Hello, world!')\n\n    await assert.rejects(async () => {\n      await runMiddleware(middleware, context, handler)\n    }, new Error('Middleware error!'))\n  })\n})\n"
  },
  {
    "path": "packages/fetch-router/src/lib/middleware.ts",
    "content": "import type { RequestHandler } from './controller.ts'\nimport { raceRequestAbort } from './request-abort.ts'\nimport type { RequestContext } from './request-context.ts'\nimport type { RequestMethod } from './request-methods.ts'\n\n/**\n * A special kind of request handler that either returns a response or passes control\n * to the next middleware or request handler in the chain.\n *\n * @param context The request context\n * @param next A function that invokes the next middleware or handler in the chain\n * @returns A response to short-circuit the chain, or `undefined`/`void` to continue\n */\nexport interface Middleware<\n  method extends RequestMethod | 'ANY' = RequestMethod | 'ANY',\n  params extends Record<string, any> = {},\n> {\n  /**\n   * Handles a request and optionally delegates to the next middleware or handler.\n   */\n  (\n    context: RequestContext<params>,\n    next: NextFunction,\n  ): Response | undefined | void | Promise<Response | undefined | void>\n}\n\n/**\n * A function that invokes the next middleware or handler in the chain.\n *\n * @returns The response from the downstream handler\n */\nexport type NextFunction = () => Promise<Response>\n\nexport function runMiddleware<\n  method extends RequestMethod | 'ANY' = RequestMethod | 'ANY',\n  params extends Record<string, any> = {},\n>(\n  middleware: Middleware<method, params>[],\n  context: RequestContext<params>,\n  handler: RequestHandler<method, params>,\n): Promise<Response> {\n  let index = -1\n\n  let dispatch = async (i: number): Promise<Response> => {\n    if (i <= index) throw new Error('next() called multiple times')\n    index = i\n\n    if (context.request.signal.aborted) {\n      throw context.request.signal.reason\n    }\n\n    let fn = middleware[i]\n    if (!fn) {\n      return await raceRequestAbort(Promise.resolve(handler(context)), context.request)\n    }\n\n    let nextPromise: Promise<Response> | undefined\n    let next: NextFunction = () => {\n      nextPromise = dispatch(i + 1)\n      return nextPromise\n    }\n\n    let response = await raceRequestAbort(Promise.resolve(fn(context, next)), context.request)\n\n    // If a response was returned, short-circuit the chain\n    if (response instanceof Response) {\n      return response\n    }\n\n    // If the middleware called next(), use the downstream response\n    if (nextPromise != null) {\n      return nextPromise\n    }\n\n    // If it did not call next(), invoke downstream automatically\n    return next()\n  }\n\n  return dispatch(0)\n}\n"
  },
  {
    "path": "packages/fetch-router/src/lib/request-abort.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { raceRequestAbort } from './request-abort.ts'\n\ndescribe('raceRequestAbort', () => {\n  it('resolves when promise resolves before abort', async () => {\n    let controller = new AbortController()\n    let request = new Request('https://remix.run', { signal: controller.signal })\n    let promise = Promise.resolve('success')\n\n    let result = await raceRequestAbort(promise, request)\n\n    assert.equal(result, 'success')\n  })\n\n  it('rejects when promise rejects before abort', async () => {\n    let controller = new AbortController()\n    let request = new Request('https://remix.run', { signal: controller.signal })\n    let promise = Promise.reject(new Error('failed'))\n\n    await assert.rejects(\n      async () => {\n        await raceRequestAbort(promise, request)\n      },\n      (error: any) => {\n        assert.equal(error.message, 'failed')\n        return true\n      },\n    )\n  })\n\n  it('throws AbortError when signal is already aborted', async () => {\n    let controller = new AbortController()\n    let request = new Request('https://remix.run', { signal: controller.signal })\n    let promise = Promise.resolve('success')\n\n    controller.abort()\n\n    await assert.rejects(\n      async () => {\n        await raceRequestAbort(promise, request)\n      },\n      (error: any) => {\n        assert.equal(error.name, 'AbortError')\n        assert.ok(error instanceof DOMException)\n        return true\n      },\n    )\n  })\n\n  it('throws AbortError when signal is aborted during promise execution', async () => {\n    let controller = new AbortController()\n    let request = new Request('https://remix.run', { signal: controller.signal })\n    let promise = new Promise((resolve) => setTimeout(() => resolve('success'), 100))\n\n    setTimeout(() => controller.abort(), 10)\n\n    await assert.rejects(\n      async () => {\n        await raceRequestAbort(promise, request)\n      },\n      (error: any) => {\n        assert.equal(error.name, 'AbortError')\n        return true\n      },\n    )\n  })\n})\n"
  },
  {
    "path": "packages/fetch-router/src/lib/request-abort.ts",
    "content": "export function raceRequestAbort<T>(promise: Promise<T>, request: Request): Promise<T> {\n  let signal = request.signal\n\n  if (signal.aborted) {\n    throw signal.reason\n  }\n\n  return new Promise<T>((resolve, reject) => {\n    let onAbort = () => reject(signal.reason)\n\n    signal.addEventListener('abort', onAbort, { once: true })\n\n    promise.then(\n      (value) => {\n        signal.removeEventListener('abort', onAbort)\n        resolve(value)\n      },\n      (error) => {\n        signal.removeEventListener('abort', onAbort)\n        reject(error)\n      },\n    )\n  })\n}\n"
  },
  {
    "path": "packages/fetch-router/src/lib/request-context.test.ts",
    "content": "import { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { createContextKey, RequestContext } from './request-context.ts'\n\ndescribe('new RequestContext()', () => {\n  it('provides access to request headers', () => {\n    let req = new Request('https://remix.run/test', {\n      headers: {\n        'Content-Type': 'application/json',\n      },\n    })\n    let context = new RequestContext(req)\n\n    assert.equal(context.headers.get('content-type'), 'application/json')\n  })\n\n  it('provides a copy of request headers that can be mutated independently', () => {\n    let req = new Request('https://remix.run/test', {\n      headers: { 'X-Original': 'value' },\n    })\n    let context = new RequestContext(req)\n\n    context.headers.set('X-New', 'new-value')\n    context.headers.delete('X-Original')\n\n    // context.headers was mutated\n    assert.equal(context.headers.get('X-New'), 'new-value')\n    assert.equal(context.headers.get('X-Original'), null)\n\n    // original request.headers unchanged\n    assert.equal(req.headers.get('X-Original'), 'value')\n    assert.equal(req.headers.get('X-New'), null)\n  })\n\n  it('stores and reads FormData using the FormData constructor as a context key', () => {\n    let context = new RequestContext(new Request('https://remix.run/test', { method: 'POST' }))\n    let formData = new FormData()\n    let file = new File(['hello'], 'hello.txt', { type: 'text/plain' })\n\n    assert.equal(context.has(FormData), false)\n\n    formData.set('avatar', file)\n    formData.set('name', 'Jane')\n    context.set(FormData, formData)\n\n    assert.equal(context.has(FormData), true)\n    assert.equal(context.get(FormData).get('name'), 'Jane')\n    assert.equal(context.get(FormData).get('avatar'), file)\n  })\n\n  it('sets and gets values in request context', () => {\n    let key = createContextKey('hello')\n    let context = new RequestContext(new Request('https://remix.run/test'))\n\n    context.set(key, 'world')\n\n    assert.equal(context.get(key), 'world')\n  })\n\n  it('gets a default value from request context when one is available', () => {\n    let key = createContextKey('hello')\n    let context = new RequestContext(new Request('https://remix.run/test'))\n\n    assert.equal(context.get(key), 'hello')\n  })\n\n  it('allows `null` as a valid default value in request context', () => {\n    let key = createContextKey(null)\n    let context = new RequestContext(new Request('https://remix.run/test'))\n\n    assert.equal(context.get(key), null)\n  })\n\n  it('throws if a context value is not set and no default value exists', () => {\n    let key = createContextKey()\n    let context = new RequestContext(new Request('https://remix.run/test'))\n\n    assert.throws(() => context.get(key), Error)\n  })\n\n  it('checks if a key has a context value', () => {\n    let key = createContextKey('default')\n    let context = new RequestContext(new Request('https://remix.run/test'))\n\n    assert.equal(context.has(key), false)\n    context.set(key, 'value')\n    assert.equal(context.has(key), true)\n  })\n\n  it('supports destructuring get/set/has from request context', () => {\n    let key = createContextKey('default')\n    let context = new RequestContext(new Request('https://remix.run/test'))\n    let { get, has, set } = context\n\n    assert.equal(has(key), false)\n    assert.equal(get(key), 'default')\n\n    set(key, 'value')\n\n    assert.equal(has(key), true)\n    assert.equal(get(key), 'value')\n  })\n\n  it('supports class constructors as context keys', () => {\n    class Value {\n      text: string\n\n      constructor(text: string) {\n        this.text = text\n      }\n    }\n\n    let context = new RequestContext(new Request('https://remix.run/test'))\n    let value = new Value('hello')\n\n    context.set(Value, value)\n\n    assert.equal(context.has(Value), true)\n    assert.equal(context.get(Value), value)\n  })\n})\n"
  },
  {
    "path": "packages/fetch-router/src/lib/request-context.ts",
    "content": "import type { Router } from './router.ts'\n\nimport type { RequestMethod } from './request-methods.ts'\n\n/**\n * Create a request context key with an optional default value.\n *\n * @param defaultValue The default value for the context key\n * @returns The new context key\n */\nexport function createContextKey<value>(defaultValue?: value): ContextKey<value> {\n  return { defaultValue }\n}\n\n/**\n * A type-safe key for storing and retrieving values from {@link RequestContext}.\n */\nexport interface ContextKey<value> {\n  /**\n   * The default value for this key if no value has been set.\n   */\n  defaultValue?: value\n}\n\nexport type ContextValue<key> =\n  key extends ContextKey<infer value>\n    ? value\n    : key extends abstract new (...args: any[]) => infer instance\n      ? instance\n      : never\n\n/**\n * A context object that contains information about the current request. Every request\n * handler or middleware in the lifecycle of a request receives the same context object.\n */\nexport class RequestContext<params extends Record<string, any> = {}> {\n  /**\n   * @param request The incoming request\n   */\n  constructor(request: Request) {\n    this.headers = new Headers(request.headers)\n    this.method = request.method.toUpperCase() as RequestMethod\n    this.params = {} as params\n    this.request = request\n    this.url = new URL(request.url)\n  }\n\n  /**\n   * The headers of the request.\n   */\n  headers: Headers\n\n  /**\n   * The request method. This may differ from `request.method` when using the `methodOverride`\n   * middleware, which allows HTML forms to simulate RESTful API request methods like `PUT` and\n   * `DELETE` using a hidden input field.\n   */\n  method: RequestMethod\n\n  /**\n   * Params that were parsed from the URL.\n   */\n  params: params\n\n  /**\n   * The original request that was dispatched to the router.\n   *\n   * Note: Various properties of the original request may not be available or may have been\n   * modified by middleware. For example, the request's body may already have been consumed by the\n   * `formData` middleware (available as `context.get(FormData)`), or its method may have been\n   * overridden by the `methodOverride` middleware (available as `context.method`). You should\n   * default to using properties of the `context` object instead of the original request.\n   * However, the original request is made available in case you need it for some edge case.\n   */\n  request: Request\n\n  #contextMap: Map<object, unknown> = new Map()\n\n  /**\n   * Get a value from request context.\n   *\n   * @param key The key to read\n   * @returns The value for the given key\n   */\n  get = <key extends object>(key: key): ContextValue<key> => {\n    if (!this.#contextMap.has(key)) {\n      let contextKey = key as ContextKey<ContextValue<key>>\n      if (contextKey.defaultValue === undefined) {\n        throw new Error(`Missing default value in context for key ${key}`)\n      }\n\n      return contextKey.defaultValue\n    }\n\n    return this.#contextMap.get(key) as ContextValue<key>\n  }\n\n  /**\n   * Check whether a value exists in request context.\n   *\n   * @param key The key to check\n   * @returns `true` if a value has been set for the key\n   */\n  has = <key extends object>(key: key): boolean => this.#contextMap.has(key)\n\n  /**\n   * Set a value in request context.\n   *\n   * @param key The key to write\n   * @param value The value to write\n   */\n  set = <key extends object>(key: key, value: ContextValue<key>): void => {\n    this.#contextMap.set(key, value)\n  }\n\n  #router?: Router\n\n  /**\n   * The router handling this request.\n   */\n  get router(): Router {\n    if (this.#router == null) {\n      throw new Error('No router found in request context.')\n    }\n\n    return this.#router\n  }\n\n  set router(router: Router) {\n    this.#router = router\n  }\n\n  /**\n   * The URL that was matched by the route.\n   */\n  url: URL\n}\n"
  },
  {
    "path": "packages/fetch-router/src/lib/request-methods.ts",
    "content": "export type RequestBodyMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'\n\n/**\n * All HTTP request methods for requests that may have a body.\n */\nexport const RequestBodyMethods = ['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as const\n\n/**\n * All HTTP request methods supported by the router.\n */\nexport type RequestMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'\n\n/**\n * All HTTP request methods that are supported by the router.\n */\nexport const RequestMethods = ['GET', 'HEAD', ...RequestBodyMethods] as const\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-helpers/form.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createFormRoutes as form } from './form.ts'\nimport { createRoutes as route, Route } from '../route-map.ts'\nimport type { Assert, IsEqual } from '../type-utils.ts'\n\ndescribe('form routes helper', () => {\n  it('creates a route map with index and action routes', () => {\n    let login = form('login')\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof login,\n          {\n            index: Route<'GET', '/login'>\n            action: Route<'POST', '/login'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(login.index, new Route('GET', '/login'))\n    assert.deepEqual(login.action, new Route('POST', '/login'))\n  })\n\n  it('supports a custom form method', () => {\n    let settings = form('settings', { formMethod: 'PUT' })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof settings,\n          {\n            index: Route<'GET', '/settings'>\n            action: Route<'PUT', '/settings'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(settings.index, new Route('GET', '/settings'))\n    assert.deepEqual(settings.action, new Route('PUT', '/settings'))\n  })\n\n  it('supports a custom index name', () => {\n    let profile = form('profile', { names: { index: 'show' } })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof profile,\n          {\n            show: Route<'GET', '/profile'>\n            action: Route<'POST', '/profile'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(profile.show, new Route('GET', '/profile'))\n    assert.deepEqual(profile.action, new Route('POST', '/profile'))\n  })\n\n  it('supports a custom action name', () => {\n    let signup = form('signup', { names: { action: 'register' } })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof signup,\n          {\n            index: Route<'GET', '/signup'>\n            register: Route<'POST', '/signup'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(signup.index, new Route('GET', '/signup'))\n    assert.deepEqual(signup.register, new Route('POST', '/signup'))\n  })\n\n  it('supports custom names for both index and action', () => {\n    let contact = form('contact', {\n      names: {\n        index: 'show',\n        action: 'submit',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof contact,\n          {\n            show: Route<'GET', '/contact'>\n            submit: Route<'POST', '/contact'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(contact.show, new Route('GET', '/contact'))\n    assert.deepEqual(contact.submit, new Route('POST', '/contact'))\n  })\n\n  it('supports custom names with custom form method', () => {\n    let account = form('account', {\n      formMethod: 'PATCH',\n      names: {\n        index: 'edit',\n        action: 'update',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof account,\n          {\n            edit: Route<'GET', '/account'>\n            update: Route<'PATCH', '/account'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(account.edit, new Route('GET', '/account'))\n    assert.deepEqual(account.update, new Route('PATCH', '/account'))\n  })\n\n  it('creates nested forms', () => {\n    let routes = route({\n      account: {\n        ...form('account'),\n        settings: form('account/settings'),\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof routes.account,\n          {\n            index: Route<'GET', '/account'>\n            action: Route<'POST', '/account'>\n            settings: {\n              index: Route<'GET', '/account/settings'>\n              action: Route<'POST', '/account/settings'>\n            }\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(routes.account.index, new Route('GET', '/account'))\n    assert.deepEqual(routes.account.action, new Route('POST', '/account'))\n\n    assert.deepEqual(routes.account.settings.index, new Route('GET', '/account/settings'))\n    assert.deepEqual(routes.account.settings.action, new Route('POST', '/account/settings'))\n  })\n})\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-helpers/form.ts",
    "content": "import type { RoutePattern } from '@remix-run/route-pattern'\n\nimport type { RequestMethod } from '../request-methods.ts'\nimport { createRoutes } from '../route-map.ts'\nimport type { BuildRouteMap } from '../route-map.ts'\n\n/**\n * Options for generating a paired `index`/`action` form route map.\n */\nexport interface FormOptions {\n  /**\n   * The method the `<form>` uses to submit the action.\n   *\n   * @default 'POST'\n   */\n  formMethod?: RequestMethod\n  /**\n   * Custom names to use for the `index` and `action` routes.\n   */\n  names?: {\n    index?: string\n    action?: string\n  }\n}\n\n/**\n * Create a route map with `index` (`GET`) and `action` (`POST`) routes, suitable\n * for showing a standard HTML `<form>` and handling its submit action at the same\n * URL.\n *\n * @param pattern The route pattern to use for the form and its submit action\n * @param options Options to configure the form action routes\n * @returns The route map with `index` and `action` routes\n */\nexport function createFormRoutes<pattern extends string, const options extends FormOptions>(\n  pattern: pattern | RoutePattern<pattern>,\n  options?: options,\n): BuildFormMap<pattern, options> {\n  let formMethod = options?.formMethod ?? 'POST'\n  let indexName = options?.names?.index ?? 'index'\n  let actionName = options?.names?.action ?? 'action'\n\n  return createRoutes(pattern, {\n    [indexName]: { method: 'GET', pattern: '/' },\n    [actionName]: { method: formMethod, pattern: '/' },\n  }) as BuildFormMap<pattern, options>\n}\n\n// prettier-ignore\ntype BuildFormMap<pattern extends string, options extends FormOptions> = BuildRouteMap<\n  pattern,\n  {\n    [\n      key in options extends { names: { index: infer indexName extends string } } ? indexName : 'index'\n    ]: {\n      method: 'GET'\n      pattern: '/'\n    }\n  } & {\n    [\n      key in options extends { names: { action: infer actionName extends string } } ? actionName : 'action'\n    ]: {\n      method: options extends { formMethod: infer formMethod extends RequestMethod } ? formMethod : 'POST'\n      pattern: '/'\n    }\n  }\n>\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-helpers/method.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\nimport { RoutePattern } from '@remix-run/route-pattern'\n\nimport type { Assert, IsEqual } from '../type-utils.ts'\nimport { Route, createRoutes as route } from '../route-map.ts'\nimport {\n  createDeleteRoute as del,\n  createGetRoute as get,\n  createHeadRoute as head,\n  createOptionsRoute as options,\n  createPatchRoute as patch,\n  createPostRoute as post,\n  createPutRoute as put,\n} from './method.ts'\n\ndescribe('route helpers composition', () => {\n  it('composes route helpers in a route map', () => {\n    let routes = route({\n      home: '/',\n      posts: {\n        index: get('/posts'),\n        create: post('/posts'),\n        show: get('/posts/:id'),\n        update: put('/posts/:id'),\n        patch: patch('/posts/:id'),\n        destroy: del('/posts/:id'),\n      },\n      api: {\n        health: head('/api/health'),\n        options: options('/api/settings'),\n      },\n    })\n\n    assert.deepEqual(routes.home, new Route('ANY', '/'))\n    assert.deepEqual(routes.posts.index, new Route('GET', '/posts'))\n    assert.deepEqual(routes.posts.create, new Route('POST', '/posts'))\n    assert.deepEqual(routes.posts.show, new Route('GET', '/posts/:id'))\n    assert.deepEqual(routes.posts.update, new Route('PUT', '/posts/:id'))\n    assert.deepEqual(routes.posts.patch, new Route('PATCH', '/posts/:id'))\n    assert.deepEqual(routes.posts.destroy, new Route('DELETE', '/posts/:id'))\n    assert.deepEqual(routes.api.health, new Route('HEAD', '/api/health'))\n    assert.deepEqual(routes.api.options, new Route('OPTIONS', '/api/settings'))\n  })\n\n  it('composes route helpers with base paths', () => {\n    let apiRoutes = route('api/v1', {\n      users: {\n        index: get('/'),\n        create: post('/'),\n        show: get('/:id'),\n        update: put('/:id'),\n        destroy: del('/:id'),\n      },\n    })\n\n    let routes = route({\n      home: '/',\n      api: apiRoutes,\n    })\n\n    assert.deepEqual(routes.api.users.index, new Route('GET', '/api/v1'))\n    assert.deepEqual(routes.api.users.create, new Route('POST', '/api/v1'))\n    assert.deepEqual(routes.api.users.show, new Route('GET', '/api/v1/:id'))\n    assert.deepEqual(routes.api.users.update, new Route('PUT', '/api/v1/:id'))\n    assert.deepEqual(routes.api.users.destroy, new Route('DELETE', '/api/v1/:id'))\n  })\n\n  it('mixes helper methods with string patterns', () => {\n    let routes = route({\n      home: '/',\n      about: '/about',\n      contact: get('/contact'),\n      login: post('/auth/login'),\n      logout: del('/auth/logout'),\n      profile: {\n        show: '/profile',\n        edit: get('/profile/edit'),\n        update: patch('/profile'),\n      },\n    })\n\n    assert.deepEqual(routes.home, new Route('ANY', '/'))\n    assert.deepEqual(routes.about, new Route('ANY', '/about'))\n    assert.deepEqual(routes.contact, new Route('GET', '/contact'))\n    assert.deepEqual(routes.login, new Route('POST', '/auth/login'))\n    assert.deepEqual(routes.logout, new Route('DELETE', '/auth/logout'))\n    assert.deepEqual(routes.profile.show, new Route('ANY', '/profile'))\n    assert.deepEqual(routes.profile.edit, new Route('GET', '/profile/edit'))\n    assert.deepEqual(routes.profile.update, new Route('PATCH', '/profile'))\n  })\n\n  it('uses helper methods with complex patterns', () => {\n    let routes = route({\n      api: {\n        posts: get('/api/posts(/:lang)'),\n        createPost: post('/api/posts'),\n        updatePost: put('/api/posts/:id'),\n        deletePost: del('/api/posts/:id'),\n      },\n      healthCheck: head('/health'),\n    })\n\n    assert.deepEqual(routes.api.posts, new Route('GET', '/api/posts(/:lang)'))\n    assert.deepEqual(routes.api.createPost, new Route('POST', '/api/posts'))\n    assert.deepEqual(routes.api.updatePost, new Route('PUT', '/api/posts/:id'))\n    assert.deepEqual(routes.api.deletePost, new Route('DELETE', '/api/posts/:id'))\n    assert.deepEqual(routes.healthCheck, new Route('HEAD', '/health'))\n  })\n})\n\nlet composedRoutes = route({\n  home: '/',\n  ...route('posts', {\n    posts: get('/'),\n    createPost: post('/'),\n    showPost: get(':id'),\n    updatePost: put(':id'),\n    deletePost: del(':id'),\n  }),\n  api: {\n    health: head('/api/health'),\n    options: options('/api/settings'),\n  },\n  patch: patch(new RoutePattern('/patch')),\n  put: put(new RoutePattern('/misc/put')),\n})\n\ntype Tests = [\n  Assert<IsEqual<typeof composedRoutes.posts, Route<'GET', '/posts'>>>,\n  Assert<IsEqual<typeof composedRoutes.createPost, Route<'POST', '/posts'>>>,\n  Assert<IsEqual<typeof composedRoutes.showPost, Route<'GET', '/posts/:id'>>>,\n  Assert<IsEqual<typeof composedRoutes.updatePost, Route<'PUT', '/posts/:id'>>>,\n  Assert<IsEqual<typeof composedRoutes.deletePost, Route<'DELETE', '/posts/:id'>>>,\n  Assert<IsEqual<typeof composedRoutes.api.health, Route<'HEAD', '/api/health'>>>,\n  Assert<IsEqual<typeof composedRoutes.api.options, Route<'OPTIONS', '/api/settings'>>>,\n  Assert<IsEqual<typeof composedRoutes.patch, Route<'PATCH', '/patch'>>>,\n  Assert<IsEqual<typeof composedRoutes.put, Route<'PUT', '/misc/put'>>>,\n]\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-helpers/method.ts",
    "content": "import type { RoutePattern } from '@remix-run/route-pattern'\n\nimport { Route } from '../route-map.ts'\n\n/**\n * Shorthand for a DELETE route.\n *\n * @alias del\n * @param pattern The route pattern string or {@link RoutePattern} object\n * @returns A Route configured for DELETE requests\n */\nexport function createDeleteRoute<source extends string>(\n  pattern: source | RoutePattern<source>,\n): Route<'DELETE', source> {\n  return new Route('DELETE', pattern)\n}\n\n/**\n * Shorthand for a GET route.\n *\n * @alias get\n * @param pattern The route pattern string or {@link RoutePattern} object\n * @returns A Route configured for GET requests\n */\nexport function createGetRoute<source extends string>(\n  pattern: source | RoutePattern<source>,\n): Route<'GET', source> {\n  return new Route('GET', pattern)\n}\n\n/**\n * Shorthand for a HEAD route.\n *\n * @alias head\n * @param pattern The route pattern string or {@link RoutePattern} object\n * @returns A Route configured for HEAD requests\n */\nexport function createHeadRoute<source extends string>(\n  pattern: source | RoutePattern<source>,\n): Route<'HEAD', source> {\n  return new Route('HEAD', pattern)\n}\n\n/**\n * Shorthand for a OPTIONS route.\n *\n * @alias options\n * @param pattern The route pattern string or {@link RoutePattern} object\n * @returns A Route configured for OPTIONS requests\n */\nexport function createOptionsRoute<source extends string>(\n  pattern: source | RoutePattern<source>,\n): Route<'OPTIONS', source> {\n  return new Route('OPTIONS', pattern)\n}\n\n/**\n * Shorthand for a PATCH route.\n *\n * @alias patch\n * @param pattern The route pattern string or {@link RoutePattern} object\n * @returns A Route configured for PATCH requests\n */\nexport function createPatchRoute<source extends string>(\n  pattern: source | RoutePattern<source>,\n): Route<'PATCH', source> {\n  return new Route('PATCH', pattern)\n}\n\n/**\n * Shorthand for a POST route.\n *\n * @alias post\n * @param pattern The route pattern string or {@link RoutePattern} object\n * @returns A Route configured for POST requests\n */\nexport function createPostRoute<source extends string>(\n  pattern: source | RoutePattern<source>,\n): Route<'POST', source> {\n  return new Route('POST', pattern)\n}\n\n/**\n * Shorthand for a PUT route.\n *\n * @alias put\n * @param pattern The route pattern string or {@link RoutePattern} object\n * @returns A Route configured for PUT requests\n */\nexport function createPutRoute<source extends string>(\n  pattern: source | RoutePattern<source>,\n): Route<'PUT', source> {\n  return new Route('PUT', pattern)\n}\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-helpers/resource.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { Route } from '../route-map.ts'\nimport type { Assert, IsEqual } from '../type-utils.ts'\nimport { ResourceMethods, createResourceRoutes as resource } from './resource.ts'\n\ndescribe('resource routes helper', () => {\n  it('creates a resource', () => {\n    let book = resource('book')\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof book,\n          {\n            show: Route<'GET', '/book'>\n            new: Route<'GET', '/book/new'>\n            create: Route<'POST', '/book'>\n            edit: Route<'GET', '/book/edit'>\n            update: Route<'PUT', '/book'>\n            destroy: Route<'DELETE', '/book'>\n          }\n        >\n      >,\n    ]\n\n    // Key order is important. new must come before show.\n    assert.deepEqual(Object.keys(book), ResourceMethods)\n\n    assert.deepEqual(book.show, new Route('GET', '/book'))\n    assert.deepEqual(book.new, new Route('GET', '/book/new'))\n    assert.deepEqual(book.create, new Route('POST', '/book'))\n    assert.deepEqual(book.edit, new Route('GET', '/book/edit'))\n    assert.deepEqual(book.update, new Route('PUT', '/book'))\n    assert.deepEqual(book.destroy, new Route('DELETE', '/book'))\n  })\n\n  it('creates a resource with only option', () => {\n    let book = resource('book', { only: ['show', 'update'] })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof book,\n          {\n            show: Route<'GET', '/book'>\n            update: Route<'PUT', '/book'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(book.show, new Route('GET', '/book'))\n    assert.deepEqual(book.update, new Route('PUT', '/book'))\n    // Other routes are excluded from the type\n    assert.equal((book as any).new, undefined)\n    assert.equal((book as any).create, undefined)\n  })\n\n  it('creates a resource with exclude option', () => {\n    let book = resource('book', { exclude: ['new', 'create', 'edit', 'destroy'] })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof book,\n          {\n            show: Route<'GET', '/book'>\n            update: Route<'PUT', '/book'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(book.show, new Route('GET', '/book'))\n    assert.deepEqual(book.update, new Route('PUT', '/book'))\n    // Other routes are excluded from the type\n    assert.equal((book as any).new, undefined)\n    assert.equal((book as any).create, undefined)\n    assert.equal((book as any).edit, undefined)\n    assert.equal((book as any).destroy, undefined)\n  })\n\n  it('creates a resource with custom route names', () => {\n    let book = resource('book', {\n      names: {\n        show: 'view',\n        new: 'newForm',\n        create: 'store',\n        edit: 'editForm',\n        update: 'save',\n        destroy: 'delete',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof book,\n          {\n            view: Route<'GET', '/book'>\n            newForm: Route<'GET', '/book/new'>\n            store: Route<'POST', '/book'>\n            editForm: Route<'GET', '/book/edit'>\n            save: Route<'PUT', '/book'>\n            delete: Route<'DELETE', '/book'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(book.view, new Route('GET', '/book'))\n    assert.deepEqual(book.newForm, new Route('GET', '/book/new'))\n    assert.deepEqual(book.store, new Route('POST', '/book'))\n    assert.deepEqual(book.editForm, new Route('GET', '/book/edit'))\n    assert.deepEqual(book.save, new Route('PUT', '/book'))\n    assert.deepEqual(book.delete, new Route('DELETE', '/book'))\n    // Old route names should not exist\n    assert.equal((book as any).show, undefined)\n    assert.equal((book as any).new, undefined)\n    assert.equal((book as any).create, undefined)\n  })\n\n  it('creates a resource with partial custom route names', () => {\n    let book = resource('book', {\n      names: {\n        show: 'view',\n        create: 'store',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof book,\n          {\n            view: Route<'GET', '/book'>\n            new: Route<'GET', '/book/new'>\n            store: Route<'POST', '/book'>\n            edit: Route<'GET', '/book/edit'>\n            update: Route<'PUT', '/book'>\n            destroy: Route<'DELETE', '/book'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(book.view, new Route('GET', '/book'))\n    assert.deepEqual(book.new, new Route('GET', '/book/new'))\n    assert.deepEqual(book.store, new Route('POST', '/book'))\n    assert.deepEqual(book.edit, new Route('GET', '/book/edit'))\n    assert.deepEqual(book.update, new Route('PUT', '/book'))\n    assert.deepEqual(book.destroy, new Route('DELETE', '/book'))\n  })\n\n  it('creates a resource with custom route names and only option', () => {\n    let book = resource('book', {\n      only: ['show', 'create'],\n      names: {\n        show: 'view',\n        create: 'store',\n        update: 'save',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof book,\n          {\n            view: Route<'GET', '/book'>\n            store: Route<'POST', '/book'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(book.view, new Route('GET', '/book'))\n    assert.deepEqual(book.store, new Route('POST', '/book'))\n    // Other routes are excluded from the type\n    assert.equal((book as any).save, undefined)\n    assert.equal((book as any).new, undefined)\n    assert.equal((book as any).edit, undefined)\n    assert.equal((book as any).destroy, undefined)\n  })\n\n  it('creates a resource with custom route names and exclude option', () => {\n    let book = resource('book', {\n      exclude: ['new', 'edit', 'destroy'],\n      names: {\n        show: 'view',\n        create: 'store',\n        update: 'save',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof book,\n          {\n            view: Route<'GET', '/book'>\n            store: Route<'POST', '/book'>\n            save: Route<'PUT', '/book'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(book.view, new Route('GET', '/book'))\n    assert.deepEqual(book.store, new Route('POST', '/book'))\n    assert.deepEqual(book.save, new Route('PUT', '/book'))\n    // Other routes are excluded from the type\n    assert.equal((book as any).new, undefined)\n    assert.equal((book as any).edit, undefined)\n    assert.equal((book as any).destroy, undefined)\n  })\n\n  it('throws an error if both only and exclude are specified', () => {\n    assert.throws(\n      () => resource('book', { only: ['show'], exclude: ['destroy'] } as any),\n      /Cannot specify both \"only\" and \"exclude\" options/,\n    )\n  })\n})\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-helpers/resource.ts",
    "content": "import type { RoutePattern } from '@remix-run/route-pattern'\n\nimport { type BuildRouteMap, createRoutes } from '../route-map.ts'\n\n/**\n * Named CRUD routes available for singleton resources.\n */\nexport type ResourceMethod = 'new' | 'show' | 'create' | 'edit' | 'update' | 'destroy'\n\n// prettier-ignore\nexport const ResourceMethods = ['new', 'show', 'create', 'edit', 'update', 'destroy'] as const\n\n/**\n * Options for generating singleton resource routes.\n */\nexport type ResourceOptions = {\n  /**\n   * Custom names to use for the resource routes.\n   */\n  names?: {\n    new?: string\n    show?: string\n    create?: string\n    edit?: string\n    update?: string\n    destroy?: string\n  }\n} & (\n  | {\n      /**\n       * The resource methods to include in the route map. If not provided, all\n       * methods (`show`, `new`, `create`, `edit`, `update`, and `destroy`) will be\n       * included.\n       * Cannot be used together with `exclude`.\n       */\n      only?: ResourceMethod[]\n      exclude?: never\n    }\n  | {\n      /**\n       * The resource methods to exclude from the route map.\n       * Cannot be used together with `only`.\n       */\n      exclude?: ResourceMethod[]\n      only?: never\n    }\n)\n\n/**\n * Create a route map with standard CRUD routes for a singleton resource.\n *\n * @param base The base route pattern to use for the resource\n * @param options Options to configure the resource routes\n * @returns The route map with CRUD routes\n */\nexport function createResourceRoutes<base extends string, const options extends ResourceOptions>(\n  base: base | RoutePattern<base>,\n  options?: options,\n): BuildResourceMap<base, options> {\n  // Runtime validation\n  if (options?.only && options?.exclude) {\n    throw new Error('Cannot specify both \"only\" and \"exclude\" options')\n  }\n\n  // Resolve which methods to include\n  let only: readonly ResourceMethod[]\n  if (options?.only) {\n    only = options.only\n  } else if (options?.exclude) {\n    only = ResourceMethods.filter((m) => !options.exclude!.includes(m))\n  } else {\n    only = ResourceMethods\n  }\n\n  let newName = options?.names?.new ?? 'new'\n  let showName = options?.names?.show ?? 'show'\n  let createName = options?.names?.create ?? 'create'\n  let editName = options?.names?.edit ?? 'edit'\n  let updateName = options?.names?.update ?? 'update'\n  let destroyName = options?.names?.destroy ?? 'destroy'\n\n  let routes: any = {}\n\n  if (only.includes('new')) {\n    routes[newName] = { method: 'GET', pattern: `/new` }\n  }\n  if (only.includes('show')) {\n    routes[showName] = { method: 'GET', pattern: `/` }\n  }\n  if (only.includes('create')) {\n    routes[createName] = { method: 'POST', pattern: `/` }\n  }\n  if (only.includes('edit')) {\n    routes[editName] = { method: 'GET', pattern: `/edit` }\n  }\n  if (only.includes('update')) {\n    routes[updateName] = { method: 'PUT', pattern: `/` }\n  }\n  if (only.includes('destroy')) {\n    routes[destroyName] = { method: 'DELETE', pattern: `/` }\n  }\n\n  return createRoutes(base, routes) as BuildResourceMap<base, options>\n}\n\ntype BuildResourceMap<base extends string, options extends ResourceOptions> = BuildRouteMap<\n  base,\n  BuildResourceRoutes<\n    options,\n    options extends { only: readonly ResourceMethod[] }\n      ? options['only'][number]\n      : options extends { exclude: readonly ResourceMethod[] }\n        ? Exclude<ResourceMethod, options['exclude'][number]>\n        : ResourceMethod\n  >\n>\n\ntype BuildResourceRoutes<options extends ResourceOptions, method extends ResourceMethod> = {\n  [methodName in method as GetRouteName<options, methodName>]: ResourceRoutes[methodName]\n}\n\ntype GetRouteName<\n  options extends ResourceOptions,\n  method extends ResourceMethod,\n> = method extends ResourceMethod\n  ? options extends { names: { [methodName in method]: infer customName extends string } }\n    ? customName\n    : method\n  : never\n\ntype ResourceRoutes = {\n  new: { method: 'GET'; pattern: `/new` }\n  show: { method: 'GET'; pattern: `/` }\n  create: { method: 'POST'; pattern: `/` }\n  edit: { method: 'GET'; pattern: `/edit` }\n  update: { method: 'PUT'; pattern: `/` }\n  destroy: { method: 'DELETE'; pattern: `/` }\n}\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-helpers/resources.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { Route, createRoutes as route } from '../route-map.ts'\nimport type { Assert, IsEqual } from '../type-utils.ts'\nimport { ResourcesMethods, createResourcesRoutes as resources } from './resources.ts'\n\ndescribe('resources routes helper', () => {\n  it('creates resources with index route', () => {\n    let books = resources('books')\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof books,\n          {\n            index: Route<'GET', '/books'>\n            show: Route<'GET', '/books/:id'>\n            new: Route<'GET', '/books/new'>\n            create: Route<'POST', '/books'>\n            edit: Route<'GET', '/books/:id/edit'>\n            update: Route<'PUT', '/books/:id'>\n            destroy: Route<'DELETE', '/books/:id'>\n          }\n        >\n      >,\n    ]\n\n    // Key order is important. new must come before show.\n    assert.deepEqual(Object.keys(books), ResourcesMethods)\n\n    assert.deepEqual(books.index, new Route('GET', '/books'))\n    assert.deepEqual(books.new, new Route('GET', '/books/new'))\n    assert.deepEqual(books.show, new Route('GET', '/books/:id'))\n    assert.deepEqual(books.create, new Route('POST', '/books'))\n    assert.deepEqual(books.edit, new Route('GET', '/books/:id/edit'))\n    assert.deepEqual(books.update, new Route('PUT', '/books/:id'))\n    assert.deepEqual(books.destroy, new Route('DELETE', '/books/:id'))\n  })\n\n  it('creates resources with custom param', () => {\n    let posts = resources('posts', { param: 'slug' })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof posts,\n          {\n            index: Route<'GET', '/posts'>\n            new: Route<'GET', '/posts/new'>\n            show: Route<'GET', '/posts/:slug'>\n            create: Route<'POST', '/posts'>\n            edit: Route<'GET', '/posts/:slug/edit'>\n            update: Route<'PUT', '/posts/:slug'>\n            destroy: Route<'DELETE', '/posts/:slug'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(posts.index, new Route('GET', '/posts'))\n    assert.deepEqual(posts.new, new Route('GET', '/posts/new'))\n    assert.deepEqual(posts.show, new Route('GET', '/posts/:slug'))\n    assert.deepEqual(posts.create, new Route('POST', '/posts'))\n    assert.deepEqual(posts.edit, new Route('GET', '/posts/:slug/edit'))\n    assert.deepEqual(posts.update, new Route('PUT', '/posts/:slug'))\n    assert.deepEqual(posts.destroy, new Route('DELETE', '/posts/:slug'))\n  })\n\n  it('creates resources with only option', () => {\n    let books = resources('books', { only: ['index', 'show', 'create'] })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof books,\n          {\n            index: Route<'GET', '/books'>\n            show: Route<'GET', '/books/:id'>\n            create: Route<'POST', '/books'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(books.index, new Route('GET', '/books'))\n    assert.deepEqual(books.show, new Route('GET', '/books/:id'))\n    assert.deepEqual(books.create, new Route('POST', '/books'))\n    // Other routes are excluded from the type\n    assert.equal((books as any).new, undefined)\n    assert.equal((books as any).edit, undefined)\n    assert.equal((books as any).update, undefined)\n    assert.equal((books as any).destroy, undefined)\n  })\n\n  it('creates resources with exclude option', () => {\n    let books = resources('books', { exclude: ['new', 'edit', 'update', 'destroy'] })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof books,\n          {\n            index: Route<'GET', '/books'>\n            show: Route<'GET', '/books/:id'>\n            create: Route<'POST', '/books'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(books.index, new Route('GET', '/books'))\n    assert.deepEqual(books.show, new Route('GET', '/books/:id'))\n    assert.deepEqual(books.create, new Route('POST', '/books'))\n    // Other routes are excluded from the type\n    assert.equal((books as any).new, undefined)\n    assert.equal((books as any).edit, undefined)\n    assert.equal((books as any).update, undefined)\n    assert.equal((books as any).destroy, undefined)\n  })\n\n  it('creates resources with custom param and only option', () => {\n    let articles = resources('articles', {\n      only: ['index', 'show'],\n      param: 'slug',\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof articles,\n          {\n            index: Route<'GET', '/articles'>\n            show: Route<'GET', '/articles/:slug'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(articles.index, new Route('GET', '/articles'))\n    assert.deepEqual(articles.show, new Route('GET', '/articles/:slug'))\n    // Other routes are excluded from the type\n    assert.equal((articles as any).new, undefined)\n    assert.equal((articles as any).create, undefined)\n  })\n\n  it('creates resources with custom route names', () => {\n    let books = resources('books', {\n      names: {\n        index: 'list',\n        new: 'newForm',\n        show: 'view',\n        create: 'store',\n        edit: 'editForm',\n        update: 'save',\n        destroy: 'delete',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof books,\n          {\n            list: Route<'GET', '/books'>\n            newForm: Route<'GET', '/books/new'>\n            view: Route<'GET', '/books/:id'>\n            store: Route<'POST', '/books'>\n            editForm: Route<'GET', '/books/:id/edit'>\n            save: Route<'PUT', '/books/:id'>\n            delete: Route<'DELETE', '/books/:id'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(books.list, new Route('GET', '/books'))\n    assert.deepEqual(books.newForm, new Route('GET', '/books/new'))\n    assert.deepEqual(books.view, new Route('GET', '/books/:id'))\n    assert.deepEqual(books.store, new Route('POST', '/books'))\n    assert.deepEqual(books.editForm, new Route('GET', '/books/:id/edit'))\n    assert.deepEqual(books.save, new Route('PUT', '/books/:id'))\n    assert.deepEqual(books.delete, new Route('DELETE', '/books/:id'))\n    // Old route names should not exist\n    assert.equal((books as any).index, undefined)\n    assert.equal((books as any).show, undefined)\n    assert.equal((books as any).create, undefined)\n  })\n\n  it('creates resources with partial custom route names', () => {\n    let books = resources('books', {\n      names: {\n        index: 'list',\n        show: 'view',\n        create: 'store',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof books,\n          {\n            list: Route<'GET', '/books'>\n            new: Route<'GET', '/books/new'>\n            view: Route<'GET', '/books/:id'>\n            store: Route<'POST', '/books'>\n            edit: Route<'GET', '/books/:id/edit'>\n            update: Route<'PUT', '/books/:id'>\n            destroy: Route<'DELETE', '/books/:id'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(books.list, new Route('GET', '/books'))\n    assert.deepEqual(books.new, new Route('GET', '/books/new'))\n    assert.deepEqual(books.view, new Route('GET', '/books/:id'))\n    assert.deepEqual(books.store, new Route('POST', '/books'))\n    assert.deepEqual(books.edit, new Route('GET', '/books/:id/edit'))\n    assert.deepEqual(books.update, new Route('PUT', '/books/:id'))\n    assert.deepEqual(books.destroy, new Route('DELETE', '/books/:id'))\n  })\n\n  it('creates resources with custom route names and only option', () => {\n    let books = resources('books', {\n      only: ['index', 'show'],\n      names: {\n        index: 'list',\n        show: 'view',\n        create: 'store',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof books,\n          {\n            list: Route<'GET', '/books'>\n            view: Route<'GET', '/books/:id'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(books.list, new Route('GET', '/books'))\n    assert.deepEqual(books.view, new Route('GET', '/books/:id'))\n    // Other routes are excluded from the type\n    assert.equal((books as any).store, undefined)\n    assert.equal((books as any).new, undefined)\n    assert.equal((books as any).edit, undefined)\n    assert.equal((books as any).update, undefined)\n    assert.equal((books as any).destroy, undefined)\n  })\n\n  it('creates resources with custom route names and exclude option', () => {\n    let books = resources('books', {\n      exclude: ['new', 'edit', 'update', 'destroy'],\n      names: {\n        index: 'list',\n        show: 'view',\n        create: 'store',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof books,\n          {\n            list: Route<'GET', '/books'>\n            view: Route<'GET', '/books/:id'>\n            store: Route<'POST', '/books'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(books.list, new Route('GET', '/books'))\n    assert.deepEqual(books.view, new Route('GET', '/books/:id'))\n    assert.deepEqual(books.store, new Route('POST', '/books'))\n    // Other routes are excluded from the type\n    assert.equal((books as any).new, undefined)\n    assert.equal((books as any).edit, undefined)\n    assert.equal((books as any).update, undefined)\n    assert.equal((books as any).destroy, undefined)\n  })\n\n  it('creates resources with custom route names and custom param', () => {\n    let posts = resources('posts', {\n      param: 'slug',\n      names: {\n        index: 'list',\n        show: 'view',\n        edit: 'editForm',\n        update: 'save',\n        destroy: 'delete',\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof posts,\n          {\n            list: Route<'GET', '/posts'>\n            new: Route<'GET', '/posts/new'>\n            view: Route<'GET', '/posts/:slug'>\n            create: Route<'POST', '/posts'>\n            editForm: Route<'GET', '/posts/:slug/edit'>\n            save: Route<'PUT', '/posts/:slug'>\n            delete: Route<'DELETE', '/posts/:slug'>\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(posts.list, new Route('GET', '/posts'))\n    assert.deepEqual(posts.new, new Route('GET', '/posts/new'))\n    assert.deepEqual(posts.view, new Route('GET', '/posts/:slug'))\n    assert.deepEqual(posts.create, new Route('POST', '/posts'))\n    assert.deepEqual(posts.editForm, new Route('GET', '/posts/:slug/edit'))\n    assert.deepEqual(posts.save, new Route('PUT', '/posts/:slug'))\n    assert.deepEqual(posts.delete, new Route('DELETE', '/posts/:slug'))\n  })\n\n  it('creates nested resources', () => {\n    let routes = route({\n      brands: {\n        ...resources('brands'),\n        products: resources('brands/:brandId/products'),\n      },\n    })\n\n    type T = [\n      Assert<\n        IsEqual<\n          typeof routes.brands,\n          {\n            index: Route<'GET', '/brands'>\n            new: Route<'GET', '/brands/new'>\n            show: Route<'GET', '/brands/:id'>\n            create: Route<'POST', '/brands'>\n            edit: Route<'GET', '/brands/:id/edit'>\n            update: Route<'PUT', '/brands/:id'>\n            destroy: Route<'DELETE', '/brands/:id'>\n            products: {\n              index: Route<'GET', '/brands/:brandId/products'>\n              new: Route<'GET', '/brands/:brandId/products/new'>\n              show: Route<'GET', '/brands/:brandId/products/:id'>\n              create: Route<'POST', '/brands/:brandId/products'>\n              edit: Route<'GET', '/brands/:brandId/products/:id/edit'>\n              update: Route<'PUT', '/brands/:brandId/products/:id'>\n              destroy: Route<'DELETE', '/brands/:brandId/products/:id'>\n            }\n          }\n        >\n      >,\n    ]\n\n    assert.deepEqual(routes.brands.index, new Route('GET', '/brands'))\n    assert.deepEqual(routes.brands.new, new Route('GET', '/brands/new'))\n    assert.deepEqual(routes.brands.show, new Route('GET', '/brands/:id'))\n    assert.deepEqual(routes.brands.create, new Route('POST', '/brands'))\n    assert.deepEqual(routes.brands.edit, new Route('GET', '/brands/:id/edit'))\n    assert.deepEqual(routes.brands.update, new Route('PUT', '/brands/:id'))\n    assert.deepEqual(routes.brands.destroy, new Route('DELETE', '/brands/:id'))\n\n    assert.deepEqual(routes.brands.products.index, new Route('GET', '/brands/:brandId/products'))\n    assert.deepEqual(routes.brands.products.new, new Route('GET', '/brands/:brandId/products/new'))\n    assert.deepEqual(routes.brands.products.show, new Route('GET', '/brands/:brandId/products/:id'))\n    assert.deepEqual(routes.brands.products.create, new Route('POST', '/brands/:brandId/products'))\n    assert.deepEqual(\n      routes.brands.products.edit,\n      new Route('GET', '/brands/:brandId/products/:id/edit'),\n    )\n    assert.deepEqual(\n      routes.brands.products.update,\n      new Route('PUT', '/brands/:brandId/products/:id'),\n    )\n    assert.deepEqual(\n      routes.brands.products.destroy,\n      new Route('DELETE', '/brands/:brandId/products/:id'),\n    )\n  })\n\n  it('throws an error if both only and exclude are specified', () => {\n    assert.throws(\n      () => resources('books', { only: ['index'], exclude: ['destroy'] } as any),\n      new Error('Cannot specify both \"only\" and \"exclude\" options'),\n    )\n  })\n})\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-helpers/resources.ts",
    "content": "import type { RoutePattern } from '@remix-run/route-pattern'\n\nimport { type BuildRouteMap, createRoutes } from '../route-map.ts'\n\n/**\n * Named CRUD routes available for resource collections.\n */\nexport type ResourcesMethod = 'index' | 'new' | 'show' | 'create' | 'edit' | 'update' | 'destroy'\n\n// prettier-ignore\nexport const ResourcesMethods = ['index', 'new', 'show', 'create', 'edit', 'update', 'destroy'] as const\n\n/**\n * Options for generating collection resource routes.\n */\nexport type ResourcesOptions = {\n  /**\n   * The parameter name to use for the resource.\n   *\n   * @default 'id'\n   */\n  param?: string\n  /**\n   * Custom names to use for the resource routes.\n   */\n  names?: {\n    index?: string\n    new?: string\n    show?: string\n    create?: string\n    edit?: string\n    update?: string\n    destroy?: string\n  }\n} & (\n  | {\n      /**\n       * The resource methods to include in the route map. If not provided, all\n       * methods (`index`, `show`, `new`, `create`, `edit`, `update`, and `destroy`)\n       * will be included.\n       * Cannot be used together with `exclude`.\n       */\n      only?: ResourcesMethod[]\n      exclude?: never\n    }\n  | {\n      /**\n       * The resource methods to exclude from the route map.\n       * Cannot be used together with `only`.\n       */\n      exclude?: ResourcesMethod[]\n      only?: never\n    }\n)\n\n/**\n * Create a route map with standard CRUD routes for a resource collection.\n *\n * @param base The base route pattern to use for the resources\n * @param options Options to configure the resource routes\n * @returns The route map with CRUD routes\n */\nexport function createResourcesRoutes<base extends string, const options extends ResourcesOptions>(\n  base: base | RoutePattern<base>,\n  options?: options,\n): BuildResourcesMap<base, options> {\n  // Runtime validation\n  if (options?.only && options?.exclude) {\n    throw new Error('Cannot specify both \"only\" and \"exclude\" options')\n  }\n\n  // Resolve which methods to include\n  let only: readonly ResourcesMethod[]\n  if (options?.only) {\n    only = options.only\n  } else if (options?.exclude) {\n    only = ResourcesMethods.filter((m) => !options.exclude!.includes(m))\n  } else {\n    only = ResourcesMethods\n  }\n\n  let param = options?.param ?? 'id'\n  let indexName = options?.names?.index ?? 'index'\n  let newName = options?.names?.new ?? 'new'\n  let showName = options?.names?.show ?? 'show'\n  let createName = options?.names?.create ?? 'create'\n  let editName = options?.names?.edit ?? 'edit'\n  let updateName = options?.names?.update ?? 'update'\n  let destroyName = options?.names?.destroy ?? 'destroy'\n\n  let routes: any = {}\n\n  if (only.includes('index')) {\n    routes[indexName] = { method: 'GET', pattern: `/` }\n  }\n  if (only.includes('new')) {\n    routes[newName] = { method: 'GET', pattern: `/new` }\n  }\n  if (only.includes('show')) {\n    routes[showName] = { method: 'GET', pattern: `/:${param}` }\n  }\n  if (only.includes('create')) {\n    routes[createName] = { method: 'POST', pattern: `/` }\n  }\n  if (only.includes('edit')) {\n    routes[editName] = { method: 'GET', pattern: `/:${param}/edit` }\n  }\n  if (only.includes('update')) {\n    routes[updateName] = { method: 'PUT', pattern: `/:${param}` }\n  }\n  if (only.includes('destroy')) {\n    routes[destroyName] = { method: 'DELETE', pattern: `/:${param}` }\n  }\n\n  return createRoutes(base, routes) as BuildResourcesMap<base, options>\n}\n\ntype BuildResourcesMap<base extends string, options extends ResourcesOptions> = BuildRouteMap<\n  base,\n  BuildResourcesRoutes<\n    options,\n    options extends { only: readonly ResourcesMethod[] }\n      ? options['only'][number]\n      : options extends { exclude: readonly ResourcesMethod[] }\n        ? Exclude<ResourcesMethod, options['exclude'][number]>\n        : ResourcesMethod,\n    GetParam<options>\n  >\n>\n\n// prettier-ignore\ntype BuildResourcesRoutes<\n  options extends ResourcesOptions,\n  method extends ResourcesMethod,\n  param extends string,\n> = {\n  [methodName in method as GetResourcesRouteName<options, methodName>]: ResourcesRoutes<param>[methodName]\n}\n\ntype GetResourcesRouteName<\n  options extends ResourcesOptions,\n  method extends ResourcesMethod,\n> = method extends ResourcesMethod\n  ? options extends { names: { [methodName in method]: infer customName extends string } }\n    ? customName\n    : method\n  : never\n\ntype ResourcesRoutes<param extends string> = {\n  index: { method: 'GET'; pattern: `/` }\n  new: { method: 'GET'; pattern: `/new` }\n  show: { method: 'GET'; pattern: `/:${param}` }\n  create: { method: 'POST'; pattern: `/` }\n  edit: { method: 'GET'; pattern: `/:${param}/edit` }\n  update: { method: 'PUT'; pattern: `/:${param}` }\n  destroy: { method: 'DELETE'; pattern: `/:${param}` }\n}\n\n// prettier-ignore\ntype GetParam<options extends ResourcesOptions> =\n  options extends { param: infer param extends string } ? param : 'id'\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-map.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport type { Assert, IsEqual } from './type-utils.ts'\nimport { Route, createRoutes as route } from './route-map.ts'\n\ndescribe('createRoutes', () => {\n  it('creates a route map', () => {\n    let routes = route({\n      home: '/',\n      about: {\n        index: 'about',\n        company: 'about/company',\n      },\n    })\n\n    assert.deepEqual(routes.home, new Route('ANY', '/'))\n    assert.deepEqual(routes.about.index, new Route('ANY', '/about'))\n    assert.deepEqual(routes.about.company, new Route('ANY', '/about/company'))\n  })\n\n  it('creates a route map with a base', () => {\n    let routes = route('categories', {\n      index: '/',\n      new: '/new',\n      show: '/:slug',\n      edit: '/:slug/edit',\n    })\n\n    assert.deepEqual(routes.index, new Route('ANY', '/categories'))\n    assert.deepEqual(routes.new, new Route('ANY', '/categories/new'))\n    assert.deepEqual(routes.show, new Route('ANY', '/categories/:slug'))\n    assert.deepEqual(routes.edit, new Route('ANY', '/categories/:slug/edit'))\n  })\n\n  it('creates a route map with a nested route map', () => {\n    let categoriesRoutes = route('categories', {\n      index: '/',\n      new: '/new',\n      show: '/:slug',\n      edit: '/:slug/edit',\n    })\n\n    let routes = route({\n      home: '/',\n      about: '/about',\n      // nested route map\n      categories: categoriesRoutes,\n    })\n\n    assert.deepEqual(routes.home, new Route('ANY', '/'))\n    assert.deepEqual(routes.about, new Route('ANY', '/about'))\n    assert.deepEqual(routes.categories.index, new Route('ANY', '/categories'))\n    assert.deepEqual(routes.categories.new, new Route('ANY', '/categories/new'))\n    assert.deepEqual(routes.categories.show, new Route('ANY', '/categories/:slug'))\n    assert.deepEqual(routes.categories.edit, new Route('ANY', '/categories/:slug/edit'))\n  })\n\n  it('creates nested routes using object spread syntax', () => {\n    let routes = route({\n      home: '/',\n      ...route('posts', {\n        posts: '/',\n        editPost: '/:slug/edit',\n      }),\n    })\n\n    assert.deepEqual(routes.home, new Route('ANY', '/'))\n    assert.deepEqual(routes.posts, new Route('ANY', '/posts'))\n    assert.deepEqual(routes.editPost, new Route('ANY', '/posts/:slug/edit'))\n  })\n})\n\nlet categoriesRoutes = route('categories', {\n  index: '/',\n  create: { method: 'POST', pattern: '/:slug/edit' },\n  products: {\n    index: '/:slug/products',\n  },\n})\n\nlet routes = route({\n  home: '/',\n  promo: '(/:lang)/promo',\n  about: {\n    index: 'about',\n    company: 'about/company',\n  },\n  blog: {\n    index: '/blog',\n    show: '/blog(/:lang)/:slug',\n  },\n  category: '/categories/:slug',\n  categories: categoriesRoutes,\n})\n\ntype Tests = [\n  Assert<IsEqual<typeof categoriesRoutes.index, Route<'ANY', '/categories'>>>,\n  Assert<IsEqual<typeof categoriesRoutes.create, Route<'POST', '/categories/:slug/edit'>>>,\n  Assert<\n    IsEqual<typeof categoriesRoutes.products.index, Route<'ANY', '/categories/:slug/products'>>\n  >,\n\n  Assert<IsEqual<typeof routes.home, Route<'ANY', '/'>>>,\n  Assert<IsEqual<typeof routes.promo, Route<'ANY', '(/:lang)/promo'>>>,\n  Assert<IsEqual<typeof routes.about.index, Route<'ANY', '/about'>>>,\n  Assert<IsEqual<typeof routes.about.company, Route<'ANY', '/about/company'>>>,\n  Assert<IsEqual<typeof routes.blog.index, Route<'ANY', '/blog'>>>,\n  Assert<IsEqual<typeof routes.blog.show, Route<'ANY', '/blog(/:lang)/:slug'>>>,\n  Assert<IsEqual<typeof routes.category, Route<'ANY', '/categories/:slug'>>>,\n  Assert<IsEqual<typeof routes.categories.index, Route<'ANY', '/categories'>>>,\n  Assert<IsEqual<typeof routes.categories.create, Route<'POST', '/categories/:slug/edit'>>>,\n  Assert<\n    IsEqual<typeof routes.categories.products.index, Route<'ANY', '/categories/:slug/products'>>\n  >,\n]\n"
  },
  {
    "path": "packages/fetch-router/src/lib/route-map.ts",
    "content": "import { RoutePattern } from '@remix-run/route-pattern'\nimport type { HrefArgs, Join, RoutePatternMatch } from '@remix-run/route-pattern'\n\nimport type { RequestMethod } from './request-methods.ts'\nimport type { Simplify } from './type-utils.ts'\n\n/**\n * A map of route names to {@link Route} objects or nested route maps.\n */\nexport interface RouteMap<pattern extends string = string> {\n  /**\n   * Named route or nested route map.\n   */\n  [name: string]: Route<RequestMethod | 'ANY', pattern> | RouteMap<pattern>\n}\n\n/**\n * A route definition that includes a request method and pattern.\n */\nexport class Route<\n  method extends RequestMethod | 'ANY' = RequestMethod | 'ANY',\n  pattern extends string = string,\n> {\n  /**\n   * The HTTP method this route matches.\n   */\n  readonly method: method | 'ANY'\n\n  /**\n   * The pattern this route matches.\n   */\n  readonly pattern: RoutePattern<pattern>\n\n  /**\n   * @param method The HTTP method this route matches\n   * @param pattern The pattern this route matches\n   */\n  constructor(method: method | 'ANY', pattern: pattern | RoutePattern<pattern>) {\n    this.method = method\n    this.pattern = typeof pattern === 'string' ? new RoutePattern(pattern) : pattern\n  }\n\n  /**\n   * Build a URL href for this route using the given parameters.\n   *\n   * @param args The parameters to use for building the href\n   * @returns The built URL href\n   */\n  href(...args: HrefArgs<pattern>): string {\n    return this.pattern.href(...args)\n  }\n\n  /**\n   * Match a URL against this route's pattern.\n   *\n   * @param url The URL to match\n   * @returns The match result, or `null` if the URL doesn't match\n   */\n  match(url: string | URL): RoutePatternMatch<pattern> | null {\n    return this.pattern.match(url)\n  }\n}\n\n/**\n * Build a {@link Route} type from a request method and pattern.\n */\n// prettier-ignore\nexport type BuildRoute<method extends RequestMethod | 'ANY', pattern extends string | RoutePattern> =\n  pattern extends string ? Route<method, pattern> :\n  pattern extends RoutePattern<infer source extends string> ? Route<method, source> :\n  never\n\n/**\n * Create a route map from a set of route definitions.\n *\n * @param defs The route definitions\n * @returns The route map\n */\nexport function createRoutes<const defs extends RouteDefs>(defs: defs): BuildRouteMap<'/', defs>\n/**\n * Create a route map from a set of route definitions with a base pattern.\n *\n * @param base The base pattern for all routes\n * @param defs The route definitions\n * @returns The route map\n */\nexport function createRoutes<base extends string, const defs extends RouteDefs>(\n  base: base | RoutePattern<base>,\n  defs: defs,\n): BuildRouteMap<base, defs>\nexport function createRoutes(baseOrDefs: any, defs?: RouteDefs): RouteMap {\n  return typeof baseOrDefs === 'string' || baseOrDefs instanceof RoutePattern\n    ? buildRouteMap(\n        typeof baseOrDefs === 'string' ? new RoutePattern(baseOrDefs) : baseOrDefs,\n        defs!,\n      )\n    : buildRouteMap(new RoutePattern('/'), baseOrDefs)\n}\n\nfunction buildRouteMap<base extends string, defs extends RouteDefs>(\n  base: RoutePattern<base>,\n  defs: defs,\n): BuildRouteMap<base, defs> {\n  let routes: any = {}\n\n  for (let key in defs) {\n    let def = defs[key]\n\n    if (def instanceof Route) {\n      routes[key] = new Route(def.method, base.join(def.pattern))\n    } else if (typeof def === 'string' || def instanceof RoutePattern) {\n      routes[key] = new Route('ANY', base.join(def))\n    } else if (typeof def === 'object' && def != null && 'pattern' in def) {\n      routes[key] = new Route((def as any).method ?? 'ANY', base.join((def as any).pattern))\n    } else {\n      routes[key] = buildRouteMap(base, def as any)\n    }\n  }\n\n  return routes\n}\n\n// prettier-ignore\nexport type BuildRouteMap<base extends string, defs extends RouteDefs> = Simplify<{\n  -readonly [name in keyof defs]: (\n    defs[name] extends Route<infer method extends RequestMethod | 'ANY', infer pattern extends string> ? Route<method, Join<base, pattern>> :\n    defs[name] extends RouteDef ? BuildRouteWithBase<base, defs[name]> :\n    defs[name] extends RouteDefs ? BuildRouteMap<base, defs[name]> :\n    never\n  )\n}>\n\n// prettier-ignore\ntype BuildRouteWithBase<base extends string, def extends RouteDef> =\n  def extends string ? Route<'ANY', Join<base, def>> :\n  def extends RoutePattern<infer pattern extends string> ? Route<'ANY', Join<base, pattern>> :\n  def extends { method: infer method extends RequestMethod | 'ANY', pattern: infer pattern } ? (\n    pattern extends string ? Route<method, Join<base, pattern>> :\n    pattern extends RoutePattern<infer source extends string> ? Route<method, Join<base, source>> :\n    never\n  ) :\n  never\n\n/**\n * A map of route names to route definitions.\n */\nexport interface RouteDefs {\n  /**\n   * Named route definition or nested route definition map.\n   */\n  [name: string]: Route | RouteDef | RouteDefs\n}\n\n/**\n * A route definition that can be a string pattern, `RoutePattern`, or an object with method and\n * pattern.\n */\nexport type RouteDef<source extends string = string> =\n  | source\n  | RoutePattern<source>\n  | { method?: RequestMethod; pattern: source | RoutePattern<source> }\n"
  },
  {
    "path": "packages/fetch-router/src/lib/router-abort.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createRouter } from './router.ts'\n\nfunction sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\ndescribe('abort signal support', () => {\n  it('throws AbortError when signal is already aborted', async () => {\n    let router = createRouter()\n    router.get('/', () => new Response('Home'))\n\n    let controller = new AbortController()\n    controller.abort()\n\n    await assert.rejects(\n      async () => {\n        await router.fetch('https://remix.run', { signal: controller.signal })\n      },\n      (error: any) => {\n        assert.equal(error.name, 'AbortError')\n        assert.ok(error instanceof DOMException)\n        return true\n      },\n    )\n  })\n\n  it('throws AbortError when signal is aborted during request processing', async () => {\n    let router = createRouter()\n    let controller = new AbortController()\n\n    router.get('/', async () => {\n      // Abort while handler is running\n      controller.abort()\n      // Simulate some async work\n      await sleep(10)\n      return new Response('Home')\n    })\n\n    await assert.rejects(\n      async () => {\n        await router.fetch('https://remix.run', { signal: controller.signal })\n      },\n      (error: any) => {\n        assert.equal(error.name, 'AbortError')\n        assert.ok(error instanceof DOMException)\n        return true\n      },\n    )\n  })\n\n  it('handles signal from Request object passed to fetch()', async () => {\n    let router = createRouter()\n    router.get('/', () => new Response('Home'))\n\n    let controller = new AbortController()\n    let request = new Request('https://remix.run', { signal: controller.signal })\n    controller.abort()\n\n    await assert.rejects(\n      async () => {\n        await router.fetch(request)\n      },\n      (error: any) => {\n        assert.equal(error.name, 'AbortError')\n        assert.ok(error instanceof DOMException)\n        return true\n      },\n    )\n  })\n\n  it('handles signal from init object', async () => {\n    let router = createRouter()\n    router.get('/', () => new Response('Home'))\n\n    let controller = new AbortController()\n    controller.abort()\n\n    await assert.rejects(\n      async () => {\n        await router.fetch('https://remix.run', { signal: controller.signal })\n      },\n      (error: any) => {\n        assert.equal(error.name, 'AbortError')\n        assert.ok(error instanceof DOMException)\n        return true\n      },\n    )\n  })\n\n  it('allows middleware to catch and handle abort errors', async () => {\n    let controller = new AbortController()\n    let errorCaught = false\n\n    let router = createRouter({\n      middleware: [\n        async (_, next) => {\n          try {\n            return await next()\n          } catch (error) {\n            if ((error as Error).name === 'AbortError') {\n              errorCaught = true\n            }\n            throw error\n          }\n        },\n      ],\n    })\n\n    router.get('/', async () => {\n      // Simulate some async work that gets aborted\n      await sleep(50)\n      return new Response('Home')\n    })\n\n    // Abort while handler is running\n    setTimeout(() => controller.abort(), 10)\n\n    await assert.rejects(\n      async () => {\n        await router.fetch('https://remix.run', { signal: controller.signal })\n      },\n      (error: any) => {\n        assert.equal(error.name, 'AbortError')\n        return true\n      },\n    )\n\n    // Middleware should have caught the error\n    assert.equal(errorCaught, true)\n  })\n\n  it('completes successfully if not aborted', async () => {\n    let controller = new AbortController()\n    let router = createRouter()\n\n    router.get('/', () => new Response('Home'))\n\n    let response = await router.fetch('https://remix.run', { signal: controller.signal })\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n  })\n\n  it('throws AbortError when aborted before middleware completes', async () => {\n    let controller = new AbortController()\n\n    let router = createRouter({\n      middleware: [\n        async () => {\n          controller.abort()\n          await sleep(10)\n        },\n      ],\n    })\n\n    router.get('/', () => new Response('Home'))\n\n    await assert.rejects(\n      async () => {\n        await router.fetch('https://remix.run', { signal: controller.signal })\n      },\n      (error: any) => {\n        assert.equal(error.name, 'AbortError')\n        return true\n      },\n    )\n  })\n\n  it('does not call downstream middleware or handler when aborted in upstream middleware', async () => {\n    let controller = new AbortController()\n    let downstreamMiddlewareCalled = false\n    let handlerCalled = false\n\n    let router = createRouter({\n      middleware: [\n        // Upstream middleware that aborts\n        async () => {\n          controller.abort()\n          await sleep(10)\n        },\n        // Downstream middleware that should NOT be called\n        async () => {\n          downstreamMiddlewareCalled = true\n        },\n      ],\n    })\n\n    // Handler that should NOT be called\n    router.get('/', () => {\n      handlerCalled = true\n      return new Response('Home')\n    })\n\n    await assert.rejects(\n      async () => {\n        await router.fetch('https://remix.run', { signal: controller.signal })\n      },\n      (error: any) => {\n        assert.equal(error.name, 'AbortError')\n        return true\n      },\n    )\n\n    assert.equal(downstreamMiddlewareCalled, false)\n    assert.equal(handlerCalled, false)\n  })\n})\n"
  },
  {
    "path": "packages/fetch-router/src/lib/router.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\nimport { ArrayMatcher, RoutePattern } from '@remix-run/route-pattern'\n\nimport type { BuildAction } from './controller.ts'\nimport type { RequestContext } from './request-context.ts'\nimport { createRoutes as route } from './route-map.ts'\nimport type { MatchData } from './router.ts'\nimport { createRouter } from './router.ts'\n\ndescribe('router.fetch()', () => {\n  it('fetches a route', async () => {\n    let router = createRouter()\n    router.get('/', () => new Response('Home'))\n\n    let response = await router.fetch('https://remix.run')\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n  })\n\n  it('fetches a route with middleware', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    router.get(routes.home, {\n      middleware: [\n        () => {\n          requestLog.push('middleware')\n        },\n      ],\n      action() {\n        return new Response('Home')\n      },\n    })\n\n    let response = await router.fetch('https://remix.run')\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n\n    assert.equal(requestLog.length, 1)\n    assert.deepEqual(requestLog, ['middleware'])\n  })\n\n  it('runs router middleware before fetching a route', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let requestLog: string[] = []\n    let router = createRouter({\n      middleware: [\n        () => {\n          requestLog.push('router middleware')\n        },\n      ],\n    })\n\n    router.get(routes.home, {\n      middleware: [\n        () => {\n          requestLog.push('route middleware')\n        },\n      ],\n      action() {\n        return new Response('Home')\n      },\n    })\n\n    let response = await router.fetch('https://remix.run')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n    assert.deepEqual(requestLog, ['router middleware', 'route middleware'])\n  })\n\n  it('fetches a route with specific method actions', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let router = createRouter()\n\n    router.get(routes.home, () => new Response('GET'))\n    router.head(routes.home, () => new Response('HEAD'))\n    router.post(routes.home, () => new Response('POST'))\n    router.put(routes.home, () => new Response('PUT'))\n    router.patch(routes.home, () => new Response('PATCH'))\n    router.delete(routes.home, () => new Response('DELETE'))\n    router.options(routes.home, () => new Response('OPTIONS'))\n\n    for (let method of ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) {\n      let response = await router.fetch('https://remix.run', { method })\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), method)\n    }\n  })\n\n  it('replaces any corresponding options set in the original `Request` when options are provided', async () => {\n    let requestLog: Array<string | null> = []\n    let router = createRouter()\n\n    router.get('/', {\n      middleware: [\n        ({ headers }) => {\n          requestLog.push(headers.get('From'))\n        },\n      ],\n      action() {\n        return new Response('Home')\n      },\n    })\n\n    await router.fetch('https://remix.run')\n    assert.deepEqual(requestLog, [null])\n\n    requestLog = []\n\n    await router.fetch('https://remix.run', { headers: { From: 'admin@remix.run' } })\n    assert.deepEqual(requestLog, ['admin@remix.run'])\n  })\n\n  it('runs router middleware even when there are no routes', async () => {\n    let requestLog: string[] = []\n    let router = createRouter({\n      middleware: [\n        () => {\n          requestLog.push('middleware')\n        },\n      ],\n    })\n\n    let response = await router.fetch('https://remix.run/nonexistent')\n    assert.equal(response.status, 404)\n    assert.equal(await response.text(), 'Not Found: /nonexistent')\n    assert.deepEqual(requestLog, ['middleware'])\n  })\n\n  it('runs router middleware even when no route matches', async () => {\n    let requestLog: string[] = []\n    let router = createRouter({\n      middleware: [\n        () => {\n          requestLog.push('middleware')\n        },\n      ],\n    })\n\n    router.get('/', () => new Response('Home'))\n\n    let response = await router.fetch('https://remix.run/nonexistent')\n    assert.equal(response.status, 404)\n    assert.equal(await response.text(), 'Not Found: /nonexistent')\n    assert.deepEqual(requestLog, ['middleware'])\n  })\n})\n\ndescribe('router.map() with single routes', () => {\n  it('maps a single route to a request handler', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let router = createRouter()\n\n    router.map(routes.home, () => new Response('Home'))\n\n    let response = await router.fetch('https://remix.run')\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n  })\n\n  it('maps a single route to an action with middleware', async () => {\n    let routes = route({\n      profile: '/profile/:id',\n    })\n\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    function middleware(context: RequestContext<{ id: string }>) {\n      requestLog.push(`middleware ${context.params.id}`)\n    }\n\n    router.map(routes.profile, {\n      middleware: [middleware],\n      action() {\n        requestLog.push('action')\n        return new Response('OK')\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/profile/1')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'OK')\n\n    assert.deepEqual(requestLog, ['middleware 1', 'action'])\n  })\n\n  it('matches any request method', async () => {\n    let router = createRouter()\n\n    router.map('/', ({ method }) => new Response(method))\n\n    for (let method of ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) {\n      let response = await router.fetch('https://remix.run', { method })\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), method)\n    }\n  })\n})\n\ndescribe('router.map()', () => {\n  it('maps a route map to a map of actions', async () => {\n    let routes = route({\n      home: '/',\n      blog: {\n        index: { method: 'GET', pattern: '/blog' },\n        create: { method: 'POST', pattern: '/blog' },\n        show: '/blog/:id',\n      },\n    })\n\n    let router = createRouter()\n\n    router.map(routes, {\n      actions: {\n        home() {\n          return new Response('Home')\n        },\n        blog: {\n          actions: {\n            index() {\n              return new Response('Blog')\n            },\n            create() {\n              return new Response('Blog Post Created')\n            },\n            show({ params }) {\n              return new Response(`Blog Post ${params.id}`)\n            },\n          },\n        },\n      },\n    })\n\n    let response = await router.fetch('https://remix.run')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n\n    response = await router.fetch('https://remix.run/blog')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Blog')\n\n    response = await router.fetch('https://remix.run/blog', { method: 'POST' })\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Blog Post Created')\n\n    response = await router.fetch('https://remix.run/blog/1')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Blog Post 1')\n  })\n\n  it('maps a route map to actions with middleware', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    function middleware() {\n      requestLog.push('middleware')\n    }\n\n    router.map(routes, {\n      middleware: [middleware],\n      actions: {\n        home() {\n          requestLog.push('action')\n          return new Response('OK')\n        },\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'OK')\n\n    assert.deepEqual(requestLog, ['middleware', 'action'])\n  })\n\n  it('requires controllers to define actions under `actions`', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let router = createRouter()\n\n    // This is a compile-time type check only - we use a never-executed block\n    // to verify that TypeScript catches the error without running the code.\n    if (false as boolean) {\n      router.map(routes, {\n        // @ts-expect-error - controllers must define actions under `actions`\n        home() {\n          return new Response('OK')\n        },\n      })\n    }\n  })\n\n  it('supports middleware in nested controllers', async () => {\n    let routes = route({\n      blog: {\n        index: '/blog',\n        show: '/blog/:id',\n      },\n    })\n\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    router.map(routes, {\n      middleware: [\n        () => {\n          requestLog.push('outer middleware')\n        },\n      ],\n      actions: {\n        blog: {\n          middleware: [\n            () => {\n              requestLog.push('inner middleware')\n            },\n          ],\n          actions: {\n            index() {\n              requestLog.push('blog-index')\n              return new Response('Blog')\n            },\n            show() {\n              requestLog.push('blog-show')\n              return new Response('Blog Post')\n            },\n          },\n        },\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/blog')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Blog')\n    assert.deepEqual(requestLog, ['outer middleware', 'inner middleware', 'blog-index'])\n\n    requestLog = []\n\n    response = await router.fetch('https://remix.run/blog/1')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Blog Post')\n    assert.deepEqual(requestLog, ['outer middleware', 'inner middleware', 'blog-show'])\n  })\n\n  it('allows selective middleware by mapping specific nested route subtrees separately', async () => {\n    let routes = route({\n      public: '/public',\n      admin: {\n        dashboard: '/admin/dashboard',\n        users: '/admin/users',\n      },\n    })\n\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    // Public route - no middleware\n    router.map(routes.public, () => {\n      requestLog.push('public')\n      return new Response('Public')\n    })\n\n    // Admin routes - with auth middleware\n    router.map(routes.admin, {\n      middleware: [\n        () => {\n          requestLog.push('auth')\n        },\n      ],\n      actions: {\n        dashboard() {\n          requestLog.push('dashboard')\n          return new Response('Dashboard')\n        },\n        users() {\n          requestLog.push('users')\n          return new Response('Users')\n        },\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/public')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Public')\n    assert.deepEqual(requestLog, ['public'])\n\n    requestLog = []\n    response = await router.fetch('https://remix.run/admin/dashboard')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Dashboard')\n    assert.deepEqual(requestLog, ['auth', 'dashboard'])\n\n    requestLog = []\n    response = await router.fetch('https://remix.run/admin/users')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Users')\n    assert.deepEqual(requestLog, ['auth', 'users'])\n  })\n\n  it('runs both global and inline middleware', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let requestLog: string[] = []\n    let router = createRouter({\n      middleware: [\n        () => {\n          requestLog.push('global')\n        },\n      ],\n    })\n\n    router.map(routes, {\n      middleware: [\n        () => {\n          requestLog.push('inline')\n        },\n      ],\n      actions: {\n        home() {\n          requestLog.push('action')\n          return new Response('OK')\n        },\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'OK')\n\n    assert.deepEqual(requestLog, ['global', 'inline', 'action'])\n  })\n})\n\ndescribe('router.get()', () => {\n  it('maps a single route to a request handler', async () => {\n    let router = createRouter()\n    router.get('/', () => new Response('Home'))\n\n    let response = await router.fetch('https://remix.run')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n  })\n\n  it('maps a single route to an action with middleware', async () => {\n    let requestLog: string[] = []\n    let router = createRouter()\n    router.get('/', {\n      middleware: [\n        () => {\n          requestLog.push('middleware')\n        },\n      ],\n      action() {\n        return new Response('Home')\n      },\n    })\n    let response = await router.fetch('https://remix.run')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n    assert.deepEqual(requestLog, ['middleware'])\n  })\n})\n\ndescribe('inline middleware', () => {\n  it('runs after global middleware', async () => {\n    let requestLog: string[] = []\n    let router = createRouter({\n      middleware: [\n        () => {\n          requestLog.push('global')\n        },\n      ],\n    })\n\n    router.get('/', {\n      middleware: [\n        () => {\n          requestLog.push('inline-1')\n        },\n        () => {\n          requestLog.push('inline-2')\n        },\n      ],\n      action() {\n        requestLog.push('action')\n        return new Response('OK')\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/')\n    assert.equal(response.status, 200)\n    assert.deepEqual(requestLog, ['global', 'inline-1', 'inline-2', 'action'])\n  })\n\n  it('runs only on the route it is defined on', async () => {\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    router.get('/a', {\n      middleware: [\n        () => {\n          requestLog.push('inline-a')\n        },\n      ],\n      action() {\n        requestLog.push('action-a')\n        return new Response('A')\n      },\n    })\n\n    router.get('/b', () => {\n      requestLog.push('action-b')\n      return new Response('B')\n    })\n\n    let response = await router.fetch('https://remix.run/a')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'A')\n    assert.deepEqual(requestLog, ['inline-a', 'action-a'])\n\n    requestLog = []\n    response = await router.fetch('https://remix.run/b')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'B')\n    assert.deepEqual(requestLog, ['action-b'])\n  })\n\n  it('works with empty middleware array', async () => {\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    router.get('/', {\n      middleware: [],\n      action() {\n        requestLog.push('action')\n        return new Response('OK')\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/')\n    assert.equal(response.status, 200)\n    assert.deepEqual(requestLog, ['action'])\n  })\n\n  it('works with action objects that omit middleware', async () => {\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    router.get('/', {\n      action() {\n        requestLog.push('action')\n        return new Response('OK')\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/')\n    assert.equal(response.status, 200)\n    assert.deepEqual(requestLog, ['action'])\n  })\n\n  it('handles middleware that returns a response (short-circuits)', async () => {\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    router.get('/', {\n      middleware: [\n        () => {\n          requestLog.push('m1')\n        },\n        () => {\n          requestLog.push('m2-short-circuit')\n          return new Response('Blocked', { status: 403 })\n        },\n        () => {\n          requestLog.push('m3')\n        },\n      ],\n      action() {\n        requestLog.push('action')\n        return new Response('OK')\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/')\n    assert.equal(response.status, 403)\n    assert.equal(await response.text(), 'Blocked')\n    assert.deepEqual(requestLog, ['m1', 'm2-short-circuit'])\n  })\n\n  it('runs both global and inline middleware', async () => {\n    let routes = route({\n      admin: {\n        dashboard: '/admin/dashboard',\n        users: '/admin/users',\n      },\n    })\n\n    let requestLog: string[] = []\n    let router = createRouter()\n\n    router.map(routes.admin, {\n      middleware: [\n        () => {\n          requestLog.push('auth')\n        },\n        () => {\n          requestLog.push('admin')\n        },\n      ],\n      actions: {\n        dashboard() {\n          requestLog.push('dashboard-action')\n          return new Response('Dashboard')\n        },\n        users: {\n          middleware: [\n            () => {\n              requestLog.push('users-middleware')\n            },\n          ],\n          action() {\n            requestLog.push('users-action')\n            return new Response('Users')\n          },\n        },\n      },\n    })\n\n    let response1 = await router.fetch('https://remix.run/admin/dashboard')\n    assert.equal(response1.status, 200)\n    assert.equal(await response1.text(), 'Dashboard')\n    assert.deepEqual(requestLog, ['auth', 'admin', 'dashboard-action'])\n\n    requestLog = []\n\n    let response2 = await router.fetch('https://remix.run/admin/users')\n    assert.equal(response2.status, 200)\n    assert.equal(await response2.text(), 'Users')\n    assert.deepEqual(requestLog, ['auth', 'admin', 'users-middleware', 'users-action'])\n  })\n\n  it('supports route-map action objects that omit middleware', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let router = createRouter()\n\n    router.map(routes.home, {\n      action() {\n        return new Response('Home')\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n  })\n\n  it('allows BuildAction object form without middleware', () => {\n    let routes = route({\n      home: '/',\n    })\n\n    if (false as boolean) {\n      let action: BuildAction<'GET', typeof routes.home> = {\n        action() {\n          return new Response('Home')\n        },\n      }\n\n      void action\n    }\n  })\n})\n\ndescribe('404 handling', () => {\n  it('returns a 404 response when no route matches', async () => {\n    let router = createRouter()\n    router.get('/home', () => new Response('Home'))\n\n    let response = await router.fetch('https://remix.run/nonexistent')\n\n    assert.equal(response.status, 404)\n    assert.equal(await response.text(), 'Not Found: /nonexistent')\n  })\n\n  it('supports a custom defaultHandler', async () => {\n    let router = createRouter({\n      defaultHandler({ url }) {\n        return new Response(`Custom 404: ${url.pathname}`, {\n          status: 404,\n          headers: { 'X-Custom': 'true' },\n        })\n      },\n    })\n\n    router.get('/home', () => new Response('Home'))\n\n    let response = await router.fetch('https://remix.run/missing')\n\n    assert.equal(response.status, 404)\n    assert.equal(await response.text(), 'Custom 404: /missing')\n    assert.equal(response.headers.get('X-Custom'), 'true')\n  })\n\n  it('calls defaultHandler only when no routes match', async () => {\n    let defaultCalls = 0\n    let router = createRouter({\n      defaultHandler() {\n        defaultCalls++\n        return new Response('Not Found', { status: 404 })\n      },\n    })\n\n    router.get('/', () => new Response('Home'))\n    router.get('/about', () => new Response('About'))\n\n    await router.fetch('https://remix.run/')\n    assert.equal(defaultCalls, 0)\n\n    await router.fetch('https://remix.run/about')\n    assert.equal(defaultCalls, 0)\n\n    await router.fetch('https://remix.run/missing')\n    assert.equal(defaultCalls, 1)\n  })\n})\n\ndescribe('error handling', () => {\n  it('propagates errors thrown in request handlers', async () => {\n    let router = createRouter()\n    router.get('/', () => {\n      throw new Error('Action error')\n    })\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/')\n    }, new Error('Action error'))\n  })\n\n  it('propagates async errors thrown in route actions', async () => {\n    let router = createRouter()\n    router.get('/', async () => {\n      await Promise.resolve()\n      throw new Error('Async action error')\n    })\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/')\n    }, new Error('Async action error'))\n  })\n\n  it('propagates errors thrown in router middleware', async () => {\n    let router = createRouter({\n      middleware: [\n        () => {\n          throw new Error('Router middleware error')\n        },\n      ],\n    })\n\n    router.get('/', () => new Response('OK'))\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/')\n    }, new Error('Router middleware error'))\n  })\n\n  it('propagates errors thrown in route middleware', async () => {\n    let router = createRouter()\n\n    router.get('/', {\n      middleware: [\n        () => {\n          throw new Error('Route middleware error')\n        },\n      ],\n      action() {\n        return new Response('OK')\n      },\n    })\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/')\n    }, new Error('Route middleware error'))\n  })\n\n  it('propagates errors thrown in the default handler', async () => {\n    let router = createRouter({\n      defaultHandler() {\n        throw new Error('Default handler error')\n      },\n    })\n\n    router.get('/home', () => new Response('Home'))\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/missing')\n    }, new Error('Default handler error'))\n  })\n\n  it('allows middleware to catch and handle errors from downstream', async () => {\n    let router = createRouter({\n      middleware: [\n        async (_, next) => {\n          try {\n            return await next()\n          } catch (error) {\n            return new Response(`Caught: ${(error as Error).message}`, { status: 500 })\n          }\n        },\n      ],\n    })\n\n    router.get('/', () => {\n      throw new Error('Action error')\n    })\n\n    let response = await router.fetch('https://remix.run/')\n    assert.equal(response.status, 500)\n    assert.equal(await response.text(), 'Caught: Action error')\n  })\n})\n\ndescribe('trailing slash handling', () => {\n  it('matches routes with and without trailing slashes for single-path routes', async () => {\n    let router = createRouter()\n    router.get('/about', () => new Response('About'))\n    router.get('/contact/', () => new Response('Contact'))\n\n    // Route defined without trailing slash\n    let response1 = await router.fetch('https://remix.run/about')\n    assert.equal(response1.status, 200)\n    assert.equal(await response1.text(), 'About')\n\n    let response2 = await router.fetch('https://remix.run/about/')\n    assert.equal(response2.status, 404) // Trailing slash doesn't match\n\n    // Route defined with trailing slash\n    let response3 = await router.fetch('https://remix.run/contact/')\n    assert.equal(response3.status, 200)\n    assert.equal(await response3.text(), 'Contact')\n\n    let response4 = await router.fetch('https://remix.run/contact')\n    assert.equal(response4.status, 404) // Without trailing slash doesn't match\n  })\n\n  it('matches routes with and without trailing slashes for createRoutes', async () => {\n    let routes = route('api', {\n      users: '/users',\n      posts: '/posts/',\n    })\n\n    let router = createRouter()\n    router.get(routes.users, () => new Response('Users'))\n    router.get(routes.posts, () => new Response('Posts'))\n\n    // Route defined without trailing slash in createRoutes\n    let response1 = await router.fetch('https://remix.run/api/users')\n    assert.equal(response1.status, 200)\n    assert.equal(await response1.text(), 'Users')\n\n    let response2 = await router.fetch('https://remix.run/api/users/')\n    assert.equal(response2.status, 404) // Trailing slash doesn't match\n\n    // Route defined with trailing slash in createRoutes\n    let response3 = await router.fetch('https://remix.run/api/posts/')\n    assert.equal(response3.status, 200)\n    assert.equal(await response3.text(), 'Posts')\n\n    let response4 = await router.fetch('https://remix.run/api/posts')\n    assert.equal(response4.status, 404) // Without trailing slash doesn't match\n  })\n\n  it('handles root path with and without trailing slash', async () => {\n    let router = createRouter()\n    router.get('/', () => new Response('Home'))\n\n    // Root with trailing slash\n    let response1 = await router.fetch('https://remix.run/')\n    assert.equal(response1.status, 200)\n    assert.equal(await response1.text(), 'Home')\n\n    // Root without trailing slash (edge case)\n    let response2 = await router.fetch('https://remix.run')\n    assert.equal(response2.status, 200)\n    assert.equal(await response2.text(), 'Home')\n  })\n\n  it('handles nested routes with trailing slash combinations', async () => {\n    let routes = route('admin', {\n      dashboard: '/',\n      users: {\n        index: '/users',\n        show: '/users/:id',\n      },\n    })\n\n    let router = createRouter()\n    router.get(routes.dashboard, () => new Response('Admin Dashboard'))\n    router.get(routes.users.index, () => new Response('Users List'))\n    router.get(routes.users.show, ({ params }) => new Response(`User ${params.id}`))\n\n    // Dashboard (base path - createRoutes('admin', { dashboard: '/' }) produces '/admin')\n    let response1 = await router.fetch('https://remix.run/admin')\n    assert.equal(response1.status, 200)\n    assert.equal(await response1.text(), 'Admin Dashboard')\n\n    let response2 = await router.fetch('https://remix.run/admin/')\n    assert.equal(response2.status, 404) // Trailing slash doesn't match '/admin'\n\n    // Nested users index\n    let response3 = await router.fetch('https://remix.run/admin/users')\n    assert.equal(response3.status, 200)\n    assert.equal(await response3.text(), 'Users List')\n\n    let response4 = await router.fetch('https://remix.run/admin/users/')\n    assert.equal(response4.status, 404) // Trailing slash doesn't match\n\n    // Nested users show\n    let response5 = await router.fetch('https://remix.run/admin/users/123')\n    assert.equal(response5.status, 200)\n    assert.equal(await response5.text(), 'User 123')\n\n    let response6 = await router.fetch('https://remix.run/admin/users/123/')\n    assert.equal(response6.status, 404) // Trailing slash doesn't match\n  })\n})\n\ndescribe('custom matcher', () => {\n  it('uses a custom matcher when provided', async () => {\n    let matchAllCalls = 0\n\n    // Create a custom matcher that tracks calls\n    class CustomMatcher extends ArrayMatcher<MatchData> {\n      matchAll(url: string | URL) {\n        matchAllCalls++\n        return super.matchAll(url)\n      }\n    }\n\n    let customMatcher = new CustomMatcher()\n    let router = createRouter({ matcher: customMatcher })\n    router.get('/', () => new Response('Home'))\n\n    await router.fetch('https://remix.run/')\n\n    assert.ok(matchAllCalls > 0, 'Custom matcher should be called')\n  })\n\n  it('adds routes to the custom matcher', async () => {\n    let addedPatterns: string[] = []\n\n    class CustomMatcher extends ArrayMatcher<MatchData> {\n      add<P extends string>(pattern: P | RoutePattern<P>, data: MatchData): void {\n        let routePattern = typeof pattern === 'string' ? new RoutePattern(pattern) : pattern\n        addedPatterns.push(routePattern.source)\n        super.add(pattern, data)\n      }\n    }\n\n    let customMatcher = new CustomMatcher()\n    let router = createRouter({ matcher: customMatcher })\n    router.get('/home', () => new Response('Home'))\n    router.get('/about', () => new Response('About'))\n\n    assert.deepEqual(addedPatterns, ['/home', '/about'])\n  })\n})\n"
  },
  {
    "path": "packages/fetch-router/src/lib/router.ts",
    "content": "import { type Matcher, ArrayMatcher, RoutePattern } from '@remix-run/route-pattern'\n\nimport { type Middleware, runMiddleware } from './middleware.ts'\nimport { raceRequestAbort } from './request-abort.ts'\nimport { RequestContext } from './request-context.ts'\nimport type { RequestMethod } from './request-methods.ts'\nimport {\n  type Controller,\n  type Action,\n  type ControllerShape,\n  type RequestHandler,\n  isController,\n  isActionObject,\n} from './controller.ts'\nimport { type RouteMap, Route } from './route-map.ts'\n\n/**\n * Normalized route match payload stored in the router matcher.\n */\nexport type MatchData = {\n  handler: RequestHandler<any>\n  method: RequestMethod | 'ANY'\n  middleware: Middleware<any>[] | undefined\n}\n\ntype NormalizedAction = {\n  handler: RequestHandler<any, any>\n  middleware: Middleware<any, any>[] | undefined\n}\n\n/**\n * The valid types for the first argument to `router.map()`.\n */\nexport type MapTarget =\n  | string\n  | RoutePattern<string>\n  | Route<RequestMethod | 'ANY', string>\n  | RouteMap\n\n/**\n * Infer the correct handler type (Action or Controller) based on the map target.\n */\n// prettier-ignore\nexport type MapHandler<target extends MapTarget> =\n  target extends string ? Action<RequestMethod | 'ANY', target> :\n  target extends RoutePattern<infer pattern extends string> ? Action<RequestMethod | 'ANY', pattern> :\n  target extends Route<RequestMethod | 'ANY', infer pattern extends string> ? Action<RequestMethod | 'ANY', pattern> :\n  target extends RouteMap ? Controller<target> :\n  never\n\n/**\n * Options for creating a router.\n */\nexport interface RouterOptions {\n  /**\n   * The default request handler that runs when no route matches.\n   *\n   * @default A 404 \"Not Found\" response\n   */\n  defaultHandler?: RequestHandler\n  /**\n   * The matcher to use for matching routes.\n   *\n   * @default `new ArrayMatcher()`\n   */\n  matcher?: Matcher<MatchData>\n  /**\n   * Global middleware to run for all routes. This middleware runs on every request before any\n   * routes are matched.\n   */\n  middleware?: Middleware[]\n}\n\n/**\n * A router maps incoming requests to request handlers and middleware.\n */\nexport interface Router {\n  /**\n   * Fetch a response from the router.\n   *\n   * @param input The request input to fetch\n   * @param init The request init options\n   * @returns The response from the route that matched the request\n   */\n  fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>\n  /**\n   * Add a route to the router.\n   *\n   * @param method The request method to match\n   * @param pattern The pattern to match\n   * @param action The action to invoke when the route matches\n   */\n  route<method extends RequestMethod | 'ANY', pattern extends string>(\n    method: method,\n    pattern: pattern | RoutePattern<pattern> | Route<method | 'ANY', pattern>,\n    action: Action<method, pattern>,\n  ): void\n  /**\n   * Map a route or route map to an action or controller.\n   *\n   * @param target The route/pattern or route map to match\n   * @param handler The action or controller to invoke when the route(s) match\n   */\n  map<target extends MapTarget>(target: target, handler: MapHandler<target>): void\n  /**\n   * Map a `GET` route/pattern to an action.\n   *\n   * @param route The route/pattern to match\n   * @param action The action to invoke when the route matches\n   */\n  get<pattern extends string>(\n    route: pattern | RoutePattern<pattern> | Route<'GET' | 'ANY', pattern>,\n    action: Action<'GET', pattern>,\n  ): void\n  /**\n   * Map a `HEAD` route/pattern to an action.\n   *\n   * @param route The route/pattern to match\n   * @param action The action to invoke when the route matches\n   */\n  head<pattern extends string>(\n    route: pattern | RoutePattern<pattern> | Route<'HEAD' | 'ANY', pattern>,\n    action: Action<'HEAD', pattern>,\n  ): void\n  /**\n   * Map a `POST` route/pattern to an action.\n   *\n   * @param route The route/pattern to match\n   * @param action The action to invoke when the route matches\n   */\n  post<pattern extends string>(\n    route: pattern | RoutePattern<pattern> | Route<'POST' | 'ANY', pattern>,\n    action: Action<'POST', pattern>,\n  ): void\n  /**\n   * Map a `PUT` route/pattern to an action.\n   *\n   * @param route The route/pattern to match\n   * @param action The action to invoke when the route matches\n   */\n  put<pattern extends string>(\n    route: pattern | RoutePattern<pattern> | Route<'PUT' | 'ANY', pattern>,\n    action: Action<'PUT', pattern>,\n  ): void\n  /**\n   * Map a `PATCH` route/pattern to an action.\n   *\n   * @param route The route/pattern to match\n   * @param action The action to invoke when the route matches\n   */\n  patch<pattern extends string>(\n    route: pattern | RoutePattern<pattern> | Route<'PATCH' | 'ANY', pattern>,\n    action: Action<'PATCH', pattern>,\n  ): void\n  /**\n   * Map a `DELETE` route/pattern to an action.\n   *\n   * @param route The route/pattern to match\n   * @param action The action to invoke when the route matches\n   */\n  delete<pattern extends string>(\n    route: pattern | RoutePattern<pattern> | Route<'DELETE' | 'ANY', pattern>,\n    action: Action<'DELETE', pattern>,\n  ): void\n  /**\n   * Map an `OPTIONS` route/pattern to an action.\n   *\n   * @param route The route/pattern to match\n   * @param action The action to invoke when the route matches\n   */\n  options<pattern extends string>(\n    route: pattern | RoutePattern<pattern> | Route<'OPTIONS' | 'ANY', pattern>,\n    action: Action<'OPTIONS', pattern>,\n  ): void\n}\n\nfunction noMatchHandler({ url }: RequestContext): Response {\n  return new Response(`Not Found: ${url.pathname}`, { status: 404 })\n}\n\n/**\n * Create a new router.\n *\n * @param options Options to configure the router\n * @returns The new router\n */\nexport function createRouter(options?: RouterOptions): Router {\n  let defaultHandler = options?.defaultHandler ?? noMatchHandler\n  let matcher = options?.matcher ?? new ArrayMatcher<MatchData>()\n  let globalMiddleware = options?.middleware\n\n  function normalizeAction<method extends RequestMethod | 'ANY', pattern extends string>(\n    action: Action<method, pattern>,\n  ): NormalizedAction\n  function normalizeAction(action: Action<any, any>): NormalizedAction {\n    if (isActionObject(action)) {\n      return {\n        handler: action.action,\n        middleware:\n          action.middleware && action.middleware.length > 0 ? action.middleware : undefined,\n      }\n    }\n\n    return {\n      handler: action as RequestHandler<any, any>,\n      middleware: undefined,\n    }\n  }\n\n  function mergeMiddleware(\n    routeMiddleware: Middleware<any, any>[] | undefined,\n    actionMiddleware: Middleware<any, any>[] | undefined,\n  ): Middleware<any, any>[] | undefined {\n    if (!routeMiddleware || routeMiddleware.length === 0) {\n      return actionMiddleware\n    }\n\n    if (!actionMiddleware || actionMiddleware.length === 0) {\n      return routeMiddleware\n    }\n\n    return routeMiddleware.concat(actionMiddleware)\n  }\n\n  function createRequestContext(input: string | URL | Request, init?: RequestInit): RequestContext {\n    let request = new Request(input, init)\n\n    if (request.signal.aborted) {\n      throw request.signal.reason\n    }\n\n    return new RequestContext(request)\n  }\n\n  function dispatch(context: RequestContext): Promise<Response> {\n    for (let match of matcher.matchAll(context.url)) {\n      let { handler, method, middleware } = match.data\n\n      if (method !== context.method && method !== 'ANY') {\n        // Request method does not match, continue to next match\n        continue\n      }\n\n      context.params = match.params\n\n      if (middleware) {\n        return runMiddleware(middleware, context, handler)\n      }\n\n      return raceRequestAbort(Promise.resolve(handler(context)), context.request)\n    }\n\n    return raceRequestAbort(Promise.resolve(defaultHandler(context)), context.request)\n  }\n\n  function registerRoute<method extends RequestMethod | 'ANY', pattern extends string>(\n    method: method,\n    route: pattern | RoutePattern<pattern> | Route<method | 'ANY', pattern>,\n    action: NormalizedAction,\n  ): void {\n    matcher.add(route instanceof Route ? route.pattern : route, {\n      handler: action.handler,\n      method,\n      middleware: action.middleware,\n    })\n  }\n\n  function addRoute<method extends RequestMethod | 'ANY', pattern extends string>(\n    method: method,\n    route: pattern | RoutePattern<pattern> | Route<method | 'ANY', pattern>,\n    action: Action<method, pattern>,\n  ): void {\n    registerRoute(method, route, normalizeAction(action))\n  }\n\n  function mapRoutes(target: MapTarget, handler: unknown): void {\n    // Single route: string, RoutePattern, or Route\n    if (typeof target === 'string' || target instanceof RoutePattern || target instanceof Route) {\n      addRoute('ANY', target as any, handler as Action<any, any>)\n      return\n    }\n\n    // Route map\n    if (!isController(handler)) {\n      throw new TypeError('Expected a controller with an `actions` property')\n    }\n\n    mapController(target, handler)\n  }\n\n  function mapController(\n    routes: RouteMap,\n    controller: ControllerShape,\n    parentMiddleware: Middleware[] = [],\n  ): void {\n    let middleware = controller.middleware\n      ? parentMiddleware.concat(controller.middleware)\n      : parentMiddleware\n\n    for (let key in routes) {\n      let route = routes[key]\n      let action = controller.actions[key]\n\n      if (route instanceof Route) {\n        let normalizedAction = normalizeAction(action as Action<any, any>)\n        let routeMiddleware = middleware.length > 0 ? middleware : undefined\n        registerRoute(route.method, route.pattern, {\n          handler: normalizedAction.handler,\n          middleware: mergeMiddleware(routeMiddleware, normalizedAction.middleware),\n        })\n      } else {\n        if (!isController(action)) {\n          throw new TypeError(\n            `Expected a nested controller with an \\`actions\\` property at \\`${key}\\``,\n          )\n        }\n\n        mapController(route as RouteMap, action, middleware)\n      }\n    }\n  }\n\n  let router: Router = {\n    fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {\n      let context = createRequestContext(input, init)\n      context.router = router\n\n      if (globalMiddleware) {\n        return runMiddleware(globalMiddleware, context, dispatch)\n      }\n\n      return dispatch(context)\n    },\n    route: addRoute,\n    map: mapRoutes,\n    get<pattern extends string>(\n      route: pattern | RoutePattern<pattern> | Route<'GET' | 'ANY', pattern>,\n      action: Action<'GET', pattern>,\n    ): void {\n      addRoute('GET', route, action)\n    },\n    head<pattern extends string>(\n      route: pattern | RoutePattern<pattern> | Route<'HEAD' | 'ANY', pattern>,\n      action: Action<'HEAD', pattern>,\n    ): void {\n      addRoute('HEAD', route, action)\n    },\n    post<pattern extends string>(\n      route: pattern | RoutePattern<pattern> | Route<'POST' | 'ANY', pattern>,\n      action: Action<'POST', pattern>,\n    ): void {\n      addRoute('POST', route, action)\n    },\n    put<pattern extends string>(\n      route: pattern | RoutePattern<pattern> | Route<'PUT' | 'ANY', pattern>,\n      action: Action<'PUT', pattern>,\n    ): void {\n      addRoute('PUT', route, action)\n    },\n    patch<pattern extends string>(\n      route: pattern | RoutePattern<pattern> | Route<'PATCH' | 'ANY', pattern>,\n      action: Action<'PATCH', pattern>,\n    ): void {\n      addRoute('PATCH', route, action)\n    },\n    delete<pattern extends string>(\n      route: pattern | RoutePattern<pattern> | Route<'DELETE' | 'ANY', pattern>,\n      action: Action<'DELETE', pattern>,\n    ): void {\n      addRoute('DELETE', route, action)\n    },\n    options<pattern extends string>(\n      route: pattern | RoutePattern<pattern> | Route<'OPTIONS' | 'ANY', pattern>,\n      action: Action<'OPTIONS', pattern>,\n    ): void {\n      addRoute('OPTIONS', route, action)\n    },\n  }\n\n  return router\n}\n"
  },
  {
    "path": "packages/fetch-router/src/lib/type-utils.ts",
    "content": "export type Assert<T extends true> = T\n\nexport type IsEqual<A, B> =\n  (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false\n\nexport type Simplify<T> = { [K in keyof T]: T[K] } & {}\n"
  },
  {
    "path": "packages/fetch-router/src/routes.ts",
    "content": "export {\n  Route,\n  createRoutes,\n  createRoutes as route, // shorthand\n} from './lib/route-map.ts'\nexport type { BuildRoute, RouteMap, RouteDefs, RouteDef } from './lib/route-map.ts'\n\n// Route helpers\nexport {\n  createDeleteRoute,\n  createDeleteRoute as del, // shorthand\n  createGetRoute,\n  createGetRoute as get, // shorthand\n  createHeadRoute,\n  createHeadRoute as head, // shorthand\n  createOptionsRoute,\n  createOptionsRoute as options, // shorthand\n  createPatchRoute,\n  createPatchRoute as patch, // shorthand\n  createPostRoute,\n  createPostRoute as post, // shorthand\n  createPutRoute,\n  createPutRoute as put, // shorthand\n} from './lib/route-helpers/method.ts'\n\nexport {\n  createFormRoutes,\n  createFormRoutes as form, // shorthand\n} from './lib/route-helpers/form.ts'\nexport type { FormOptions } from './lib/route-helpers/form.ts'\n\nexport {\n  createResourceRoutes,\n  createResourceRoutes as resource, // shorthand\n} from './lib/route-helpers/resource.ts'\nexport type { ResourceMethod, ResourceOptions } from './lib/route-helpers/resource.ts'\n\nexport {\n  createResourcesRoutes,\n  createResourcesRoutes as resources, // shorthand\n} from './lib/route-helpers/resources.ts'\nexport type { ResourcesMethod, ResourcesOptions } from './lib/route-helpers/resources.ts'\n"
  },
  {
    "path": "packages/fetch-router/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"global.d.ts\", \"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/fetch-router/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"demos\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/file-storage/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/file-storage/CHANGELOG.md",
    "content": "# `file-storage` CHANGELOG\n\nThis is the changelog for [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage). It follows [semantic versioning](https://semver.org/).\n\n## v0.13.3\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`fs@0.4.2`](https://github.com/remix-run/remix/releases/tag/fs@0.4.2)\n  - [`lazy-file@5.0.2`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.2)\n\n## v0.13.2\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.13.1\n\n### Patch Changes\n\n- Update `@remix-run/fs` peer dependency to use new `openLazyFile()` API\n\n## v0.13.0 (2025-11-25)\n\n- BREAKING CHANGE: `LocalFileStorage` class has been replaced with `createFsFileStorage(directory)`\n- BREAKING CHANGE: `MemoryFileStorage` class has been replaced with `createMemoryFileStorage()`\n\n  ```ts\n  // before\n  import { LocalFileStorage } from '@remix-run/file-storage/local'\n  import { MemoryFileStorage } from '@remix-run/file-storage/memory'\n  let fsStorage = new LocalFileStorage('./files')\n  let memoryStorage = new MemoryFileStorage()\n\n  // after\n  import { createFsFileStorage } from '@remix-run/file-storage/fs'\n  import { createMemoryFileStorage } from '@remix-run/file-storage/memory'\n  let fsStorage = createFsFileStorage('./files')\n  let memoryStorage = createMemoryFileStorage()\n  ```\n\n## v0.12.0 (2025-11-20)\n\n- Add `@remix-run/fs` as a peer dependency. This package now imports from `@remix-run/fs` instead of `@remix-run/lazy-file/fs`.\n\n## v0.11.0 (2025-11-05)\n\n- Move `@remix-run/lazy-file` to `peerDependencies`\n- Build using `tsc` instead of `esbuild`. This means modules in the `dist` directory now mirror the layout of modules in the `src` directory.\n\n## v0.10.0 (2025-10-22)\n\n- 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()`.\n\n## v0.9.0 (2025-07-25)\n\n- Remove hash directories when they are empty in `LocalFileStorage`\n\n## v0.8.0 (2025-07-21)\n\n- Renamed package from `@mjackson/file-storage` to `@remix-run/file-storage`\n\n## v0.7.0 (2025-06-10)\n\n- Add `/src` to npm package, so \"go to definition\" goes to the actual source\n- Use one set of types for all built files, instead of separate types for ESM and CJS\n- Build using esbuild directly instead of tsup\n\n## v0.6.1 (2025-02-06)\n\n- Fix regression when using `LocalFileStorage` together with `form-data-parser` (see #53)\n\n## v0.6.0 (2025-02-04)\n\n- BREAKING CHANGE: `LocalFileStorage` now uses 2 characters for shard directory names instead of 8.\n- Buffer contents of files stored in `MemoryFileStorage`.\n- Add `storage.list(options)` for listing files in storage.\n\nThe following `options` are available:\n\n- `cursor`: An opaque string that allows you to paginate over the keys in storage\n- `includeMetadata`: If `true`, include file metadata in the result\n- `limit`: The maximum number of files to return\n- `prefix`: Only return keys that start with this string\n\nFor example, to list all files under keys that start with `user123/`:\n\n```ts\nlet result = await storage.list({ prefix: 'user123/' })\nconsole.log(result.files)\n// [\n//   { key: \"user123/...\" },\n//   { key: \"user123/...\" },\n//   ...\n// ]\n```\n\n`result.files` will be an array of `{ key: string }` objects. To include metadata about each file, use `includeMetadata: true`.\n\n```ts\nlet result = await storage.list({ prefix: 'user123/', includeMetadata: true })\nconsole.log(result.files)\n// [\n//   {\n//     key: \"user123/...\",\n//     lastModified: 1737955705270,\n//     name: \"hello.txt\",\n//     size: 16,\n//     type: \"text/plain\"\n//   },\n//   ...\n// ]\n```\n\nPagination 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:\n\n```ts\nlet result = await storage.list()\nconsole.log(result.files)\n\nwhile (result.cursor !== undefined) {\n  result = await storage.list({ cursor: result.cursor })\n  console.log(result.files)\n}\n```\n\nUse the `limit` option to limit how many results you get back in the `files` array.\n\n## v0.5.0 (2025-01-25)\n\n- 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\n\n```ts\n// before\nawait storage.set(key, file)\nlet newFile = await storage.get(key)!\n\n// after\nlet newFile = await storage.put(key, file)\n```\n\n## v0.4.1 (2025-01-10)\n\n- Fix missing types for `file-storage/local` in npm package\n\n## v0.4.0 (2025-01-08)\n\n- Fixes race conditions with concurrent calls to `set`\n- Shards storage directories for more scalable file systems\n\n## v0.3.0 (2024-11-14)\n\n- Added CommonJS build\n- Upgrade to lazy-file@3.1.0\n\n## v0.2.1 (2024-09-04)\n\n- Automatically clean up old files in `LocalFileStorage` when new files are stored with the same key\n\n## v0.2.0 (2024-08-26)\n\n- Moved `LocalFileStorage` to `file-storage/local` export\n- Moved `MemoryFileStorage` to `file-storage/memory` export\n\n## v0.1.0 (2024-08-24)\n\n- Initial release\n"
  },
  {
    "path": "packages/file-storage/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/file-storage/README.md",
    "content": "# file-storage\n\nKey/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.\n\n## Features\n\n- **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)\n- **Multiple Backends** - Built-in filesystem and memory backends\n- **Streaming Support** - Stream file content to and from storage\n- **Metadata Preservation** - Preserves all `File` metadata including `file.name`, `file.type`, and `file.lastModified`\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n### File System\n\n```ts\nimport { createFsFileStorage } from 'remix/file-storage/fs'\n\nlet storage = createFsFileStorage('./user/files')\n\nlet file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })\nlet key = 'hello-key'\n\n// Put the file in storage.\nawait storage.set(key, file)\n\n// Then, sometime later...\nlet fileFromStorage = await storage.get(key)\n// All of the original file's metadata is intact\nfileFromStorage.name // 'hello.txt'\nfileFromStorage.type // 'text/plain'\n\n// To remove from storage\nawait storage.remove(key)\n```\n\n## Related Packages\n\n- [`file-storage-s3`](https://github.com/remix-run/remix/tree/main/packages/file-storage-s3) - S3 backend for `file-storage`\n- [`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\n- [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) - The streaming `File` implementation used internally to stream files from storage\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/file-storage/package.json",
    "content": "{\n  \"name\": \"@remix-run/file-storage\",\n  \"version\": \"0.13.3\",\n  \"description\": \"Key/value storage for JavaScript File objects\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/file-storage\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/file-storage#readme\",\n  \"license\": \"MIT\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./fs\": \"./src/fs.ts\",\n    \"./memory\": \"./src/memory.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./fs\": {\n        \"types\": \"./dist/fs.d.ts\",\n        \"default\": \"./dist/fs.js\"\n      },\n      \"./memory\": {\n        \"types\": \"./dist/memory.d.ts\",\n        \"default\": \"./dist/memory.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"dependencies\": {\n    \"@remix-run/fs\": \"workspace:^\",\n    \"@remix-run/lazy-file\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@remix-run/form-data-parser\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"file\",\n    \"storage\",\n    \"stream\",\n    \"fs\"\n  ]\n}\n"
  },
  {
    "path": "packages/file-storage/src/fs.ts",
    "content": "export { createFsFileStorage } from './lib/backends/fs.ts'\n"
  },
  {
    "path": "packages/file-storage/src/index.ts",
    "content": "export type {\n  FileStorage,\n  FileKey,\n  FileMetadata,\n  ListOptions,\n  ListResult,\n} from './lib/file-storage.ts'\n"
  },
  {
    "path": "packages/file-storage/src/lib/backends/fs.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { afterEach, beforeEach, describe, it } from 'node:test'\nimport * as fs from 'node:fs'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\nimport { parseFormData } from '@remix-run/form-data-parser'\n\nimport { createFsFileStorage } from './fs.ts'\n\ndescribe('fs file storage', () => {\n  let tmpDir: string\n  beforeEach(() => {\n    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fs-file-storage-test-'))\n  })\n\n  afterEach(() => {\n    fs.rmSync(tmpDir, { recursive: true, force: true })\n  })\n\n  it('stores and retrieves files', async () => {\n    let storage = createFsFileStorage(tmpDir)\n    let lastModified = Date.now()\n    let file = new File(['Hello, world!'], 'hello.txt', {\n      type: 'text/plain',\n      lastModified,\n    })\n\n    await storage.set('hello', file)\n\n    assert.ok(await storage.has('hello'))\n\n    let retrieved = await storage.get('hello')\n\n    assert.ok(retrieved)\n    assert.equal(retrieved.name, 'hello.txt')\n    assert.equal(retrieved.type, 'text/plain')\n    assert.equal(retrieved.lastModified, lastModified)\n    assert.equal(retrieved.size, 13)\n\n    let text = await retrieved.text()\n\n    assert.equal(text, 'Hello, world!')\n\n    await storage.remove('hello')\n\n    assert.ok(!(await storage.has('hello')))\n    assert.equal(await storage.get('hello'), null)\n  })\n\n  it('removes empty hash directories after removing files', async () => {\n    let storage = createFsFileStorage(tmpDir)\n    let file = new File(['Test content'], 'test.txt', { type: 'text/plain' })\n\n    // Set a file\n    await storage.set('test-key', file)\n\n    // Verify subdirectories exist\n    let subdirs = fs\n      .readdirSync(tmpDir)\n      .filter((name) => fs.statSync(path.join(tmpDir, name)).isDirectory())\n    assert.ok(subdirs.length > 0)\n\n    // Remove the file\n    await storage.remove('test-key')\n\n    // Verify no subdirectories remain\n    let subdirsAfter = fs\n      .readdirSync(tmpDir)\n      .filter((name) => fs.statSync(path.join(tmpDir, name)).isDirectory())\n    assert.equal(subdirsAfter.length, 0)\n  })\n\n  it('lists files with pagination', async () => {\n    let storage = createFsFileStorage(tmpDir)\n    let allKeys = ['a', 'b', 'c', 'd', 'e']\n\n    await Promise.all(\n      allKeys.map((key) =>\n        storage.set(key, new File([`Hello ${key}!`], `hello.txt`, { type: 'text/plain' })),\n      ),\n    )\n\n    let { cursor, files } = await storage.list()\n    assert.equal(cursor, undefined)\n    assert.equal(files.length, 5)\n    assert.deepEqual(files.map((f) => f.key).sort(), allKeys)\n\n    let { cursor: cursor1, files: files1 } = await storage.list({ limit: 0 })\n    assert.equal(cursor1, undefined)\n    assert.equal(files1.length, 0)\n\n    let { cursor: cursor2, files: files2 } = await storage.list({ limit: 2 })\n    assert.notEqual(cursor2, undefined)\n    assert.equal(files2.length, 2)\n\n    let { cursor: cursor3, files: files3 } = await storage.list({ cursor: cursor2 })\n    assert.equal(cursor3, undefined)\n    assert.equal(files3.length, 3)\n\n    assert.deepEqual([...files2, ...files3].map((f) => f.key).sort(), allKeys)\n  })\n\n  it('lists files by key prefix', async () => {\n    let storage = createFsFileStorage(tmpDir)\n    let allKeys = ['a', 'b', 'b/c', 'c', 'd']\n\n    await Promise.all(\n      allKeys.map((key) =>\n        storage.set(key, new File([`Hello ${key}!`], `hello.txt`, { type: 'text/plain' })),\n      ),\n    )\n\n    let { cursor, files } = await storage.list({ prefix: 'b' })\n    assert.equal(cursor, undefined)\n    assert.equal(files.length, 2)\n    assert.deepEqual(files.map((f) => f.key).sort(), ['b', 'b/c'])\n  })\n\n  it('lists files with metadata', async () => {\n    let storage = createFsFileStorage(tmpDir)\n    let allKeys = ['a', 'b', 'c', 'd', 'e']\n\n    await Promise.all(\n      allKeys.map((key) =>\n        storage.set(key, new File([`Hello ${key}!`], `hello.txt`, { type: 'text/plain' })),\n      ),\n    )\n\n    let { cursor, files } = await storage.list({ includeMetadata: true })\n    assert.equal(cursor, undefined)\n    assert.equal(files.length, 5)\n    assert.deepEqual(files.map((f) => f.key).sort(), allKeys)\n    files.forEach((f) => assert.ok('lastModified' in f))\n    files.forEach((f) => assert.ok('name' in f))\n    files.forEach((f) => assert.ok('size' in f))\n    files.forEach((f) => assert.ok('type' in f))\n  })\n\n  it('handles race conditions', async () => {\n    let storage = createFsFileStorage(tmpDir)\n    let lastModified = Date.now()\n\n    let file1 = new File(['Hello, world!'], 'hello1.txt', {\n      type: 'text/plain',\n      lastModified,\n    })\n\n    let file2 = new File(['Hello, universe!'], 'hello2.txt', {\n      type: 'text/plain',\n      lastModified,\n    })\n\n    let setOnePromise = storage.set('one', file1)\n    let setTwoPromise = storage.set('two', file2)\n    await Promise.all([setOnePromise, setTwoPromise])\n\n    let retrieved1 = await storage.get('one')\n    assert.ok(retrieved1)\n    assert.equal(await retrieved1.text(), 'Hello, world!')\n\n    let retrieved2 = await storage.get('two')\n    assert.ok(retrieved2)\n    assert.equal(await retrieved2.text(), 'Hello, universe!')\n  })\n\n  it('throws if directory is a file', () => {\n    fs.mkdirSync(tmpDir, { recursive: true })\n    let filePath = path.join(tmpDir, 'not-a-directory')\n    fs.writeFileSync(filePath, 'I am a file')\n\n    assert.throws(\n      () => {\n        createFsFileStorage(filePath)\n      },\n      new Error(`Path \"${filePath}\" is not a directory`),\n    )\n  })\n\n  it('puts files', async () => {\n    let storage = createFsFileStorage(tmpDir)\n    let lastModified = Date.now()\n    let file = new File(['Hello, world!'], 'hello.txt', {\n      type: 'text/plain',\n      lastModified,\n    })\n\n    let retrieved = await storage.put('hello', file)\n\n    assert.ok(await storage.has('hello'))\n    assert.ok(retrieved)\n    assert.equal(retrieved.name, 'hello.txt')\n    assert.equal(retrieved.type, 'text/plain')\n    assert.equal(retrieved.lastModified, lastModified)\n    assert.equal(retrieved.size, 13)\n  })\n\n  describe('integration with form-data-parser', () => {\n    it('stores and lists file uploads', async () => {\n      let storage = createFsFileStorage(tmpDir)\n\n      let boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'\n      let request = new Request('http://example.com', {\n        method: 'POST',\n        headers: {\n          'Content-Type': `multipart/form-data; boundary=${boundary}`,\n        },\n        body: [\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"hello\"; filename=\"hello.txt\"',\n          'Content-Type: text/plain',\n          '',\n          'Hello, world!',\n          `--${boundary}--`,\n        ].join('\\r\\n'),\n      })\n\n      await parseFormData(request, async (file) => {\n        await storage.set('hello', file)\n      })\n\n      assert.ok(await storage.has('hello'))\n\n      let { files } = await storage.list({ includeMetadata: true })\n\n      assert.equal(files.length, 1)\n      assert.equal(files[0].key, 'hello')\n      assert.equal(files[0].name, 'hello.txt')\n      assert.equal(files[0].size, 13)\n      assert.equal(files[0].type, 'text/plain')\n      assert.ok(files[0].lastModified)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/file-storage/src/lib/backends/fs.ts",
    "content": "import * as fs from 'node:fs'\nimport * as fsp from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { openLazyFile, writeFile } from '@remix-run/fs'\n\nimport type { FileStorage, FileMetadata, ListOptions, ListResult } from '../file-storage.ts'\n\ntype MetadataJson = Omit<FileMetadata, 'size'>\n\n/**\n * Creates a {@link FileStorage} that is backed by a filesystem directory using `node:fs`.\n *\n * Important: No attempt is made to avoid overwriting existing files, so the directory used should\n * be a new directory solely dedicated to this storage object.\n *\n * Note: Keys have no correlation to file names on disk, so they may be any string including\n * characters that are not valid in file names. Additionally, individual `File` names have no\n * correlation to names of files on disk, so multiple files with the same name may be stored in the\n * same storage object.\n *\n * @param directory The directory where files are stored\n * @returns A new {@link FileStorage} backed by a filesystem directory\n */\nexport function createFsFileStorage(directory: string): FileStorage {\n  let rootDir = path.resolve(directory)\n\n  try {\n    let stats = fs.statSync(rootDir)\n\n    if (!stats.isDirectory()) {\n      throw new Error(`Path \"${rootDir}\" is not a directory`)\n    }\n  } catch (error) {\n    if (!isNoEntityError(error)) {\n      throw error\n    }\n\n    fs.mkdirSync(rootDir, { recursive: true })\n  }\n\n  async function getPaths(\n    key: string,\n  ): Promise<{ directory: string; filePath: string; metaPath: string }> {\n    let hash = await computeHash(key)\n    let directory = path.join(rootDir, hash.slice(0, 2))\n\n    return {\n      directory,\n      filePath: path.join(directory, `${hash}.dat`),\n      metaPath: path.join(directory, `${hash}.meta.json`),\n    }\n  }\n\n  async function putFile(key: string, file: File): Promise<File> {\n    let { directory, filePath, metaPath } = await getPaths(key)\n\n    // Ensure directory exists\n    await fsp.mkdir(directory, { recursive: true })\n\n    await writeFile(filePath, file)\n\n    let meta: MetadataJson = {\n      key,\n      lastModified: file.lastModified,\n      name: file.name,\n      type: file.type,\n    }\n    await fsp.writeFile(metaPath, JSON.stringify(meta))\n\n    let metaData = await readMetadata(metaPath)\n\n    return openLazyFile(filePath, {\n      lastModified: metaData.lastModified,\n      name: metaData.name,\n      type: metaData.type,\n    })\n  }\n\n  return {\n    async get(key: string): Promise<File | null> {\n      let { filePath, metaPath } = await getPaths(key)\n\n      try {\n        let meta = await readMetadata(metaPath)\n\n        return openLazyFile(filePath, {\n          lastModified: meta.lastModified,\n          name: meta.name,\n          type: meta.type,\n        })\n      } catch (error) {\n        if (!isNoEntityError(error)) {\n          throw error\n        }\n\n        return null\n      }\n    },\n    async has(key: string): Promise<boolean> {\n      let { metaPath } = await getPaths(key)\n\n      try {\n        await fsp.access(metaPath)\n        return true\n      } catch {\n        return false\n      }\n    },\n    async list<opts extends ListOptions>(options?: opts): Promise<ListResult<opts>> {\n      let { cursor, includeMetadata = false, limit = 32, prefix } = options ?? {}\n\n      let files: any[] = []\n      let foundCursor = cursor === undefined\n      let nextCursor: string | undefined\n      let lastHash: string | undefined\n\n      outerLoop: for await (let subdir of await fsp.opendir(rootDir)) {\n        if (!subdir.isDirectory()) continue\n\n        for await (let file of await fsp.opendir(path.join(rootDir, subdir.name))) {\n          if (!file.isFile() || !file.name.endsWith('.meta.json')) continue\n\n          let hash = file.name.slice(0, -10) // Remove \".meta.json\"\n\n          if (foundCursor) {\n            let meta = await readMetadata(path.join(rootDir, subdir.name, file.name))\n\n            if (prefix != null && !meta.key.startsWith(prefix)) {\n              continue\n            }\n\n            if (files.length >= limit) {\n              nextCursor = lastHash\n              break outerLoop\n            }\n\n            if (includeMetadata) {\n              let size = (await fsp.stat(path.join(rootDir, subdir.name, `${hash}.dat`))).size\n              files.push({ ...meta, size })\n            } else {\n              files.push({ key: meta.key })\n            }\n          } else if (hash === cursor) {\n            foundCursor = true\n          }\n\n          lastHash = hash\n        }\n      }\n\n      return {\n        cursor: nextCursor,\n        files,\n      }\n    },\n    put(key: string, file: File): Promise<File> {\n      return putFile(key, file)\n    },\n    async remove(key: string): Promise<void> {\n      let { directory, filePath, metaPath } = await getPaths(key)\n\n      try {\n        await Promise.all([fsp.unlink(filePath), fsp.unlink(metaPath)])\n\n        // Check if directory is empty and remove it if so\n        let files = await fsp.readdir(directory)\n        if (files.length === 0) {\n          await fsp.rmdir(directory)\n        }\n      } catch (error) {\n        if (!isNoEntityError(error)) {\n          throw error\n        }\n      }\n    },\n    async set(key: string, file: File): Promise<void> {\n      await putFile(key, file)\n    },\n  }\n}\n\nasync function readMetadata(metaPath: string): Promise<MetadataJson> {\n  return JSON.parse(await fsp.readFile(metaPath, 'utf-8'))\n}\n\nasync function computeHash(key: string, algorithm = 'SHA-256'): Promise<string> {\n  let digest = await crypto.subtle.digest(algorithm, new TextEncoder().encode(key))\n  return Array.from(new Uint8Array(digest))\n    .map((b) => b.toString(16).padStart(2, '0'))\n    .join('')\n}\n\nfunction isNoEntityError(obj: unknown): obj is NodeJS.ErrnoException & { code: 'ENOENT' } {\n  return obj instanceof Error && 'code' in obj && (obj as NodeJS.ErrnoException).code === 'ENOENT'\n}\n"
  },
  {
    "path": "packages/file-storage/src/lib/backends/memory.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\nimport { parseFormData } from '@remix-run/form-data-parser'\n\nimport { createMemoryFileStorage } from './memory.ts'\n\ndescribe('createMemoryFileStorage', () => {\n  it('stores and retrieves files', async () => {\n    let storage = createMemoryFileStorage()\n    let file = new File(['Hello, world!'], 'hello.txt', { type: 'text/plain' })\n\n    await storage.set('hello', file)\n\n    assert.ok(storage.has('hello'))\n\n    let retrieved = await storage.get('hello')\n\n    assert.ok(retrieved)\n    assert.equal(retrieved!.name, 'hello.txt')\n    assert.equal(retrieved!.type, 'text/plain')\n    assert.equal(retrieved!.size, 13)\n\n    let text = await retrieved.text()\n\n    assert.equal(text, 'Hello, world!')\n\n    storage.remove('hello')\n\n    assert.ok(!storage.has('hello'))\n    assert.equal(storage.get('hello'), null)\n  })\n\n  it('lists files with pagination', async () => {\n    let storage = createMemoryFileStorage()\n    let allKeys = ['a', 'b', 'c', 'd', 'e']\n\n    await Promise.all(\n      allKeys.map((key) =>\n        storage.set(key, new File([`Hello ${key}!`], `hello.txt`, { type: 'text/plain' })),\n      ),\n    )\n\n    let { cursor, files } = await storage.list()\n    assert.equal(cursor, undefined)\n    assert.equal(files.length, 5)\n    assert.deepEqual(files.map((f) => f.key).sort(), allKeys)\n\n    let { cursor: cursor1, files: files1 } = await storage.list({ limit: 0 })\n    assert.equal(cursor1, undefined)\n    assert.equal(files1.length, 0)\n\n    let { cursor: cursor2, files: files2 } = await storage.list({ limit: 2 })\n    assert.notEqual(cursor2, undefined)\n    assert.equal(files2.length, 2)\n\n    let { cursor: cursor3, files: files3 } = await storage.list({ cursor: cursor2 })\n    assert.equal(cursor3, undefined)\n    assert.equal(files3.length, 3)\n\n    assert.deepEqual([...files2, ...files3].map((f) => f.key).sort(), allKeys)\n  })\n\n  it('lists files by key prefix', async () => {\n    let storage = createMemoryFileStorage()\n    let allKeys = ['a', 'b', 'b/c', 'c', 'd']\n\n    await Promise.all(\n      allKeys.map((key) =>\n        storage.set(key, new File([`Hello ${key}!`], `hello.txt`, { type: 'text/plain' })),\n      ),\n    )\n\n    let { cursor, files } = await storage.list({ prefix: 'b' })\n\n    assert.equal(cursor, undefined)\n    assert.equal(files.length, 2)\n    assert.equal(files[0].key, 'b')\n    assert.equal(files[1].key, 'b/c')\n  })\n\n  it('lists files with metadata', async () => {\n    let storage = createMemoryFileStorage()\n    let allKeys = ['a', 'b', 'c', 'd', 'e']\n\n    await Promise.all(\n      allKeys.map((key) =>\n        storage.set(key, new File([`Hello ${key}!`], `hello.txt`, { type: 'text/plain' })),\n      ),\n    )\n\n    let { cursor, files } = await storage.list({ includeMetadata: true })\n\n    assert.equal(cursor, undefined)\n    assert.equal(files.length, 5)\n    assert.deepEqual(files.map((f) => f.key).sort(), allKeys)\n    files.forEach((f) => assert.ok('lastModified' in f))\n    files.forEach((f) => assert.ok('name' in f))\n    files.forEach((f) => assert.ok('size' in f))\n    files.forEach((f) => assert.ok('type' in f))\n  })\n\n  describe('integration with form-data-parser', () => {\n    it('stores and lists file uploads', async () => {\n      let storage = createMemoryFileStorage()\n\n      let boundary = '----WebKitFormBoundaryzv5f5B8XUeVl7e0A'\n      let request = new Request('http://example.com', {\n        method: 'POST',\n        headers: {\n          'Content-Type': `multipart/form-data; boundary=${boundary}`,\n        },\n        body: [\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"a\"; filename=\"hello.txt\"',\n          'Content-Type: text/plain',\n          '',\n          'Hello, world!',\n          `--${boundary}--`,\n        ].join('\\r\\n'),\n      })\n\n      await parseFormData(request, async (file) => {\n        await storage.put('hello', file)\n      })\n\n      assert.ok(storage.has('hello'))\n\n      let { files } = await storage.list({ includeMetadata: true })\n\n      assert.equal(files.length, 1)\n      assert.equal(files[0].key, 'hello')\n      assert.equal(files[0].name, 'hello.txt')\n      assert.equal(files[0].size, 13)\n      assert.equal(files[0].type, 'text/plain')\n      assert.ok(files[0].lastModified)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/file-storage/src/lib/backends/memory.ts",
    "content": "import type { FileStorage, ListOptions, ListResult } from '../file-storage.ts'\n\n/**\n * Creates a simple, in-memory implementation of the {@link FileStorage} interface.\n *\n * @returns A new in-memory {@link FileStorage} instance\n */\nexport function createMemoryFileStorage(): FileStorage {\n  let map = new Map<string, File>()\n\n  async function putFile(key: string, file: File): Promise<File> {\n    let buffer = await file.arrayBuffer()\n    let newFile = new File([buffer], file.name, {\n      lastModified: file.lastModified,\n      type: file.type,\n    })\n    map.set(key, newFile)\n    return newFile\n  }\n\n  return {\n    get(key: string): File | null {\n      return map.get(key) ?? null\n    },\n    has(key: string): boolean {\n      return map.has(key)\n    },\n    list<opts extends ListOptions>(options?: opts): ListResult<opts> {\n      let { cursor, includeMetadata = false, limit = Infinity, prefix } = options ?? {}\n\n      let files: any[] = []\n      let foundCursor = cursor === undefined\n      let nextCursor: string | undefined\n\n      for (let [key, file] of map.entries()) {\n        if (foundCursor) {\n          if (prefix != null && !key.startsWith(prefix)) {\n            continue\n          }\n\n          if (files.length >= limit) {\n            nextCursor = files[files.length - 1]?.key\n            break\n          }\n\n          if (includeMetadata) {\n            files.push({\n              key,\n              lastModified: file.lastModified,\n              name: file.name,\n              size: file.size,\n              type: file.type,\n            })\n          } else {\n            files.push({ key })\n          }\n        } else if (key === cursor) {\n          foundCursor = true\n        }\n      }\n\n      return {\n        cursor: nextCursor,\n        files,\n      }\n    },\n    put(key: string, file: File): Promise<File> {\n      return putFile(key, file)\n    },\n    remove(key: string): void {\n      map.delete(key)\n    },\n    async set(key: string, file: File): Promise<void> {\n      await putFile(key, file)\n    },\n  }\n}\n"
  },
  {
    "path": "packages/file-storage/src/lib/file-storage.ts",
    "content": "/**\n * A key/value interface for storing `File` objects.\n */\nexport interface FileStorage {\n  /**\n   * Get a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) at the given key.\n   *\n   * @param key The key to look up\n   * @returns The file with the given key, or `null` if no such key exists\n   */\n  get(key: string): File | null | Promise<File | null>\n  /**\n   * Check if a file with the given key exists.\n   *\n   * @param key The key to look up\n   * @returns `true` if a file with the given key exists, `false` otherwise\n   */\n  has(key: string): boolean | Promise<boolean>\n  /**\n   * List the files in storage.\n   *\n   * The following `options` are available:\n   *\n   * - `cursor`: An opaque string that allows you to paginate over the keys in storage\n   * - `includeMetadata`: If `true`, include file metadata in the result\n   * - `limit`: The maximum number of files to return\n   * - `prefix`: Only return keys that start with this string\n   *\n   * For example, to list all files under keys that start with `user123/`:\n   *\n   * ```ts\n   * let result = await storage.list({ prefix: 'user123/' });\n   * console.log(result.files);\n   * // [\n   * //   { key: \"user123/...\" },\n   * //   { key: \"user123/...\" },\n   * //   ...\n   * // ]\n   * ```\n   *\n   * `result.files` will be an array of `{ key: string }` objects. To include metadata about each\n   * file, use `includeMetadata: true`.\n   *\n   * ```ts\n   * let result = await storage.list({ prefix: 'user123/', includeMetadata: true });\n   * console.log(result.files);\n   * // [\n   * //   {\n   * //     key: \"user123/...\",\n   * //     lastModified: 1737955705270,\n   * //     name: \"hello.txt\",\n   * //     size: 16,\n   * //     type: \"text/plain\"\n   * //   },\n   * //   ...\n   * // ]\n   * ```\n   *\n   * Pagination is done via an opaque `cursor` property in the list result object. If it is not\n   * `undefined`, there are more files to list. You can list them by passing the `cursor` back in\n   * the `options` object on the next call.\n   *\n   * ```ts\n   * let result = await storage.list();\n   *\n   * console.log(result.files);\n   *\n   * if (result.cursor !== undefined) {\n   *   let result2 = await storage.list({ cursor: result.cursor });\n   * }\n   * ```\n   *\n   * Use the `limit` option to limit how many results you get back in the `files` array.\n   *\n   * @param options Options for the list operation\n   * @returns An object with an array of `files` and an optional `cursor` property\n   */\n  list<T extends ListOptions>(options?: T): ListResult<T> | Promise<ListResult<T>>\n  /**\n   * Put a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) in storage and return a\n   * new file backed by this storage.\n   *\n   * @param key The key to store the file under\n   * @param file The file to store\n   * @returns A new `File` object backed by this storage\n   */\n  put(key: string, file: File): File | Promise<File>\n  /**\n   * Remove the file with the given key from storage.\n   *\n   * @param key The key to remove\n   */\n  remove(key: string): void | Promise<void>\n  /**\n   * Put a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) in storage at the given\n   * key.\n   *\n   * @param key The key to store the file under\n   * @param file The file to store\n   */\n  set(key: string, file: File): void | Promise<void>\n}\n\n/**\n * A reference to a file in storage by its key.\n */\nexport interface FileKey {\n  /**\n   * The key of the file in storage.\n   */\n  key: string\n}\n\n/**\n * Metadata about a file in storage.\n */\nexport interface FileMetadata extends FileKey {\n  /**\n   * The last modified time of the file (in ms since the Unix epoch).\n   */\n  lastModified: number\n  /**\n   * The name of the file.\n   */\n  name: string\n  /**\n   * The size of the file in bytes.\n   */\n  size: number\n  /**\n   * The MIME type of the file.\n   */\n  type: string\n}\n\n/**\n * Options for listing files in storage.\n */\nexport interface ListOptions {\n  /**\n   * An opaque string that allows you to paginate over the keys in storage.\n   */\n  cursor?: string\n  /**\n   * If `true`, include file metadata in the result.\n   */\n  includeMetadata?: boolean\n  /**\n   * The maximum number of files to return.\n   */\n  limit?: number\n  /**\n   * Only return files with keys that start with this prefix.\n   */\n  prefix?: string\n}\n\n/**\n * The result of listing files in storage.\n */\nexport interface ListResult<T extends ListOptions> {\n  /**\n   * An opaque string that allows you to paginate over the keys in storage. Pass this back in the\n   * `options` object on the next `list()` call to get the next page of results.\n   */\n  cursor?: string\n  /**\n   * A list of the files in storage.\n   */\n  files: (T extends { includeMetadata: true } ? FileMetadata : FileKey)[]\n}\n"
  },
  {
    "path": "packages/file-storage/src/memory.ts",
    "content": "export { createMemoryFileStorage } from './lib/backends/memory.ts'\n"
  },
  {
    "path": "packages/file-storage/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"global.d.ts\", \"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/file-storage/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/file-storage-s3/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/file-storage-s3/CHANGELOG.md",
    "content": "# `file-storage-s3` CHANGELOG\n\nThis 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/).\n\n## v0.1.0\n\n### Minor Changes\n\n- #### Unreleased\n\n  Initial release of `@remix-run/file-storage-s3`.\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`file-storage@0.13.3`](https://github.com/remix-run/remix/releases/tag/file-storage@0.13.3)\n\n## Unreleased\n\n### Minor Changes\n\n- Initial release of `@remix-run/file-storage-s3`.\n"
  },
  {
    "path": "packages/file-storage-s3/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/file-storage-s3/README.md",
    "content": "# file-storage-s3\n\nS3 backend for [`remix/file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage).\nUse this package when you want the `FileStorage` API backed by AWS S3 or an S3-compatible provider.\n\n## Features\n\n- **S3-Compatible API** - Works with AWS S3 and S3-compatible APIs (e.g. MinIO, LocalStack)\n- **Metadata Preservation** - Preserves `File` metadata (`name`, `type`, `lastModified`)\n- **Runtime-Agnostic Signing** - Uses [`aws4fetch`](https://github.com/mhart/aws4fetch) for SigV4 signing\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```ts\nimport { createS3FileStorage } from 'remix/file-storage-s3'\n\nlet storage = createS3FileStorage({\n  accessKeyId: process.env.AWS_ACCESS_KEY_ID!,\n  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,\n  bucket: 'my-app-uploads',\n  region: 'us-east-1',\n})\n\nawait storage.set(\n  'uploads/hello.txt',\n  new File(['hello world'], 'hello.txt', { type: 'text/plain' }),\n)\nlet file = await storage.get('uploads/hello.txt')\nawait storage.remove('uploads/hello.txt')\n```\n\nFor S3-compatible providers such as MinIO and LocalStack, set `endpoint` and `forcePathStyle: true`.\n\n## Related Packages\n\n- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) - Core `FileStorage` interface and filesystem/memory backends\n- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - Parses `multipart/form-data` uploads into `FileUpload` objects\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/file-storage-s3/package.json",
    "content": "{\n  \"name\": \"@remix-run/file-storage-s3\",\n  \"version\": \"0.1.0\",\n  \"description\": \"S3 backend for remix/file-storage\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/file-storage-s3\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/file-storage-s3#readme\",\n  \"license\": \"MIT\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"dependencies\": {\n    \"@remix-run/file-storage\": \"workspace:^\",\n    \"aws4fetch\": \"^1.0.20\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"file\",\n    \"storage\",\n    \"s3\",\n    \"aws\"\n  ]\n}\n"
  },
  {
    "path": "packages/file-storage-s3/src/index.ts",
    "content": "export { type S3FileStorageOptions, createS3FileStorage } from './lib/s3.ts'\n"
  },
  {
    "path": "packages/file-storage-s3/src/lib/s3.integration.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { before, beforeEach, describe, it } from 'node:test'\nimport { AwsClient } from 'aws4fetch'\n\nimport { createS3FileStorage } from './s3.ts'\n\nlet integrationEnabled =\n  process.env.FILE_STORAGE_S3_INTEGRATION === '1' &&\n  typeof process.env.FILE_STORAGE_S3_ENDPOINT === 'string' &&\n  typeof process.env.FILE_STORAGE_S3_BUCKET === 'string' &&\n  typeof process.env.FILE_STORAGE_S3_REGION === 'string' &&\n  typeof process.env.FILE_STORAGE_S3_ACCESS_KEY_ID === 'string' &&\n  typeof process.env.FILE_STORAGE_S3_SECRET_ACCESS_KEY === 'string'\n\ndescribe('s3 file storage integration', () => {\n  let storage: ReturnType<typeof createS3FileStorage>\n  let bucketUrl: URL\n  let client: AwsClient\n\n  before(async () => {\n    if (!integrationEnabled) {\n      return\n    }\n\n    let endpoint = process.env.FILE_STORAGE_S3_ENDPOINT!\n    let bucket = process.env.FILE_STORAGE_S3_BUCKET!\n    let forcePathStyle = process.env.FILE_STORAGE_S3_FORCE_PATH_STYLE === '1'\n\n    storage = createS3FileStorage({\n      accessKeyId: process.env.FILE_STORAGE_S3_ACCESS_KEY_ID!,\n      secretAccessKey: process.env.FILE_STORAGE_S3_SECRET_ACCESS_KEY!,\n      sessionToken: process.env.FILE_STORAGE_S3_SESSION_TOKEN,\n      bucket,\n      endpoint,\n      region: process.env.FILE_STORAGE_S3_REGION!,\n      forcePathStyle,\n    })\n\n    client = new AwsClient({\n      accessKeyId: process.env.FILE_STORAGE_S3_ACCESS_KEY_ID!,\n      secretAccessKey: process.env.FILE_STORAGE_S3_SECRET_ACCESS_KEY!,\n      sessionToken: process.env.FILE_STORAGE_S3_SESSION_TOKEN,\n      service: 's3',\n      region: process.env.FILE_STORAGE_S3_REGION!,\n    })\n\n    bucketUrl = createBucketUrl(endpoint, bucket, forcePathStyle)\n\n    await ensureBucketExists(client, bucketUrl)\n  })\n\n  beforeEach(async () => {\n    if (!integrationEnabled) {\n      return\n    }\n\n    await clearStorage(storage)\n  })\n\n  it('stores and retrieves files', { skip: !integrationEnabled }, async () => {\n    let lastModified = Date.now()\n    let file = new File(['Hello, world!'], 'hello.txt', {\n      type: 'text/plain',\n      lastModified,\n    })\n\n    await storage.set('hello', file)\n\n    assert.ok(await storage.has('hello'))\n\n    let retrieved = await storage.get('hello')\n\n    assert.ok(retrieved)\n    assert.equal(retrieved.name, 'hello.txt')\n    assert.equal(retrieved.type, 'text/plain')\n    assert.equal(retrieved.lastModified, lastModified)\n    assert.equal(retrieved.size, 13)\n    assert.equal(await retrieved.text(), 'Hello, world!')\n\n    await storage.remove('hello')\n\n    assert.ok(!(await storage.has('hello')))\n    assert.equal(await storage.get('hello'), null)\n  })\n\n  it(\n    'lists files with pagination and prefix filtering',\n    { skip: !integrationEnabled },\n    async () => {\n      let allKeys = ['a', 'b', 'b/c', 'c', 'd']\n\n      await Promise.all(\n        allKeys.map((key) =>\n          storage.set(key, new File([`Hello ${key}!`], `${key}.txt`, { type: 'text/plain' })),\n        ),\n      )\n\n      let { cursor, files } = await storage.list({ limit: 2 })\n      assert.notEqual(cursor, undefined)\n      assert.equal(files.length, 2)\n\n      let { files: files2 } = await storage.list({ cursor })\n      assert.equal(files2.length, 3)\n\n      let { files: prefixedFiles } = await storage.list({ prefix: 'b' })\n      assert.equal(prefixedFiles.length, 2)\n      assert.deepEqual(\n        prefixedFiles.map((file) => file.key),\n        ['b', 'b/c'],\n      )\n    },\n  )\n\n  it('lists files with metadata', { skip: !integrationEnabled }, async () => {\n    let lastModified = Date.now()\n    let file = new File(['Hello, world!'], 'hello.txt', {\n      type: 'text/plain',\n      lastModified,\n    })\n\n    await storage.set('hello', file)\n\n    let { files } = await storage.list({ includeMetadata: true })\n\n    assert.equal(files.length, 1)\n    assert.equal(files[0].key, 'hello')\n    assert.equal(files[0].name, 'hello.txt')\n    assert.equal(files[0].type, 'text/plain')\n    assert.equal(files[0].lastModified, lastModified)\n    assert.equal(files[0].size, 13)\n  })\n})\n\nasync function ensureBucketExists(client: AwsClient, bucketUrl: URL): Promise<void> {\n  let response = await client.fetch(bucketUrl, { method: 'PUT' })\n\n  if (response.ok || response.status === 409) {\n    return\n  }\n\n  throw new Error(`Unable to create integration bucket: ${response.status} ${response.statusText}`)\n}\n\nasync function clearStorage(storage: ReturnType<typeof createS3FileStorage>): Promise<void> {\n  let cursor: string | undefined\n\n  do {\n    let result = await storage.list({ cursor, limit: 100 })\n    cursor = result.cursor\n\n    await Promise.all(result.files.map((file) => storage.remove(file.key)))\n  } while (cursor != null)\n}\n\nfunction createBucketUrl(endpoint: string, bucket: string, forcePathStyle: boolean): URL {\n  let endpointUrl = new URL(endpoint)\n\n  if (forcePathStyle) {\n    endpointUrl.pathname = joinPath(endpointUrl.pathname, bucket)\n  } else {\n    endpointUrl.hostname = `${bucket}.${endpointUrl.hostname}`\n    endpointUrl.pathname = joinPath(endpointUrl.pathname)\n  }\n\n  return endpointUrl\n}\n\nfunction joinPath(basePath: string, ...parts: (string | undefined)[]): string {\n  let normalizedBasePath = basePath.replace(/\\/+$/g, '')\n  let normalizedParts = parts\n    .filter((part): part is string => part != null && part !== '')\n    .map((part) => part.replace(/^\\/+|\\/+$/g, ''))\n\n  let joined = [normalizedBasePath, ...normalizedParts].filter((part) => part !== '').join('/')\n  if (joined === '') {\n    return '/'\n  }\n\n  return joined.startsWith('/') ? joined : `/${joined}`\n}\n"
  },
  {
    "path": "packages/file-storage-s3/src/lib/s3.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createS3FileStorage } from './s3.ts'\n\nconst BUCKET = 'test-bucket'\nconst ENDPOINT = 'https://s3.us-east-1.amazonaws.com'\n\ntype StoredObject = {\n  body: ArrayBuffer\n  contentType: string\n  metadata: Record<string, string>\n  updatedAt: number\n}\n\ndescribe('s3 file storage', () => {\n  it('stores and retrieves files', async () => {\n    let storage = createS3FileStorage({\n      accessKeyId: 'test',\n      secretAccessKey: 'test',\n      bucket: BUCKET,\n      endpoint: ENDPOINT,\n      region: 'us-east-1',\n      fetch: createMockS3Fetch(),\n    })\n    let lastModified = Date.now()\n    let file = new File(['Hello, world!'], 'hello.txt', {\n      type: 'text/plain',\n      lastModified,\n    })\n\n    await storage.set('hello', file)\n\n    assert.ok(await storage.has('hello'))\n\n    let retrieved = await storage.get('hello')\n\n    assert.ok(retrieved)\n    assert.equal(retrieved.name, 'hello.txt')\n    assert.equal(retrieved.type, 'text/plain')\n    assert.equal(retrieved.lastModified, lastModified)\n    assert.equal(retrieved.size, 13)\n    assert.equal(await retrieved.text(), 'Hello, world!')\n\n    await storage.remove('hello')\n\n    assert.ok(!(await storage.has('hello')))\n    assert.equal(await storage.get('hello'), null)\n  })\n\n  it('lists files with pagination', async () => {\n    let storage = createS3FileStorage({\n      accessKeyId: 'test',\n      secretAccessKey: 'test',\n      bucket: BUCKET,\n      endpoint: ENDPOINT,\n      region: 'us-east-1',\n      fetch: createMockS3Fetch(),\n    })\n    let allKeys = ['a', 'b', 'c', 'd', 'e']\n\n    await Promise.all(\n      allKeys.map((key) =>\n        storage.set(key, new File([`Hello ${key}!`], `${key}.txt`, { type: 'text/plain' })),\n      ),\n    )\n\n    let { cursor, files } = await storage.list()\n    assert.equal(cursor, undefined)\n    assert.equal(files.length, 5)\n    assert.deepEqual(\n      files.map((file) => file.key),\n      allKeys,\n    )\n\n    let { cursor: cursor1, files: files1 } = await storage.list({ limit: 0 })\n    assert.equal(cursor1, undefined)\n    assert.equal(files1.length, 0)\n\n    let { cursor: cursor2, files: files2 } = await storage.list({ limit: 2 })\n    assert.notEqual(cursor2, undefined)\n    assert.equal(files2.length, 2)\n\n    let { cursor: cursor3, files: files3 } = await storage.list({ cursor: cursor2 })\n    assert.equal(cursor3, undefined)\n    assert.equal(files3.length, 3)\n    assert.deepEqual(\n      [...files2, ...files3].map((file) => file.key),\n      allKeys,\n    )\n  })\n\n  it('lists files by key prefix', async () => {\n    let storage = createS3FileStorage({\n      accessKeyId: 'test',\n      secretAccessKey: 'test',\n      bucket: BUCKET,\n      endpoint: ENDPOINT,\n      region: 'us-east-1',\n      fetch: createMockS3Fetch(),\n    })\n    let allKeys = ['a', 'b', 'b/c', 'c', 'd']\n\n    await Promise.all(\n      allKeys.map((key) =>\n        storage.set(key, new File([`Hello ${key}!`], `${key}.txt`, { type: 'text/plain' })),\n      ),\n    )\n\n    let { cursor, files } = await storage.list({ prefix: 'b' })\n    assert.equal(cursor, undefined)\n    assert.equal(files.length, 2)\n    assert.deepEqual(\n      files.map((file) => file.key),\n      ['b', 'b/c'],\n    )\n  })\n\n  it('lists files with metadata', async () => {\n    let storage = createS3FileStorage({\n      accessKeyId: 'test',\n      secretAccessKey: 'test',\n      bucket: BUCKET,\n      endpoint: ENDPOINT,\n      region: 'us-east-1',\n      fetch: createMockS3Fetch(),\n    })\n    let lastModified = Date.now()\n    let file = new File(['Hello, world!'], 'hello.txt', {\n      type: 'text/plain',\n      lastModified,\n    })\n\n    await storage.set('hello', file)\n\n    let { cursor, files } = await storage.list({ includeMetadata: true })\n\n    assert.equal(cursor, undefined)\n    assert.equal(files.length, 1)\n    assert.equal(files[0].key, 'hello')\n    assert.equal(files[0].name, 'hello.txt')\n    assert.equal(files[0].type, 'text/plain')\n    assert.equal(files[0].lastModified, lastModified)\n    assert.equal(files[0].size, 13)\n  })\n\n  it('supports virtual-host style URLs', async () => {\n    let storage = createS3FileStorage({\n      accessKeyId: 'test',\n      secretAccessKey: 'test',\n      bucket: BUCKET,\n      endpoint: ENDPOINT,\n      region: 'us-east-1',\n      forcePathStyle: false,\n      fetch: createMockS3Fetch(),\n    })\n\n    await storage.set(\n      'dir/hello.txt',\n      new File(['Hello, world!'], 'hello.txt', { type: 'text/plain' }),\n    )\n\n    assert.ok(await storage.has('dir/hello.txt'))\n    let retrieved = await storage.get('dir/hello.txt')\n    assert.ok(retrieved)\n    assert.equal(await retrieved.text(), 'Hello, world!')\n  })\n})\n\nfunction createMockS3Fetch(): typeof globalThis.fetch {\n  let buckets = new Set([BUCKET])\n  let objects = new Map<string, StoredObject>()\n\n  return async function fetch(input, init) {\n    let request = new Request(input, init)\n    let url = new URL(request.url)\n    let method = request.method.toUpperCase()\n    let { bucket, key } = parseBucketAndKey(url)\n\n    if (bucket == null) {\n      return createErrorResponse(400, 'InvalidBucketName', 'Could not determine bucket')\n    }\n\n    if (method === 'PUT' && key === '') {\n      buckets.add(bucket)\n      return new Response(null, { status: 200 })\n    }\n\n    if (!buckets.has(bucket)) {\n      return createErrorResponse(404, 'NoSuchBucket', 'The specified bucket does not exist')\n    }\n\n    if (method === 'GET' && key === '' && url.searchParams.get('list-type') === '2') {\n      return listObjects(url, bucket, objects)\n    }\n\n    if (key === '') {\n      return createErrorResponse(405, 'MethodNotAllowed', 'Method not allowed for bucket')\n    }\n\n    let objectKey = `${bucket}:${key}`\n\n    if (method === 'PUT') {\n      let body = await request.arrayBuffer()\n      let metadata: Record<string, string> = {}\n\n      request.headers.forEach((value, header) => {\n        if (header.startsWith('x-amz-meta-')) {\n          metadata[header] = value\n        }\n      })\n\n      objects.set(objectKey, {\n        body,\n        metadata,\n        contentType: request.headers.get('content-type') ?? '',\n        updatedAt: Date.now(),\n      })\n\n      return new Response(null, { status: 200 })\n    }\n\n    let object = objects.get(objectKey)\n\n    if (object == null) {\n      return createErrorResponse(404, 'NoSuchKey', 'The specified key does not exist')\n    }\n\n    if (method === 'GET') {\n      return createObjectResponse(object, false)\n    }\n\n    if (method === 'HEAD') {\n      return createObjectResponse(object, true)\n    }\n\n    if (method === 'DELETE') {\n      objects.delete(objectKey)\n      return new Response(null, { status: 204 })\n    }\n\n    return createErrorResponse(405, 'MethodNotAllowed', 'Unsupported method')\n  }\n}\n\nfunction listObjects(url: URL, bucket: string, objects: Map<string, StoredObject>): Response {\n  let prefix = url.searchParams.get('prefix') ?? ''\n  let continuationToken = url.searchParams.get('continuation-token')\n  let maxKeys = Number(url.searchParams.get('max-keys') ?? '1000')\n  let safeMaxKeys = Number.isFinite(maxKeys) ? Math.max(0, maxKeys) : 1000\n  let useUrlEncoding = url.searchParams.get('encoding-type') === 'url'\n\n  let keys = Array.from(objects.keys())\n    .filter((key) => key.startsWith(`${bucket}:`))\n    .map((key) => key.slice(bucket.length + 1))\n    .filter((key) => key.startsWith(prefix))\n    .sort()\n\n  let startIndex =\n    continuationToken == null ? 0 : keys.findIndex((key) => key === continuationToken) + 1\n  if (startIndex < 0) {\n    startIndex = 0\n  }\n\n  let page = keys.slice(startIndex, startIndex + safeMaxKeys)\n  let isTruncated = startIndex + safeMaxKeys < keys.length\n  let nextContinuationToken = isTruncated ? page[page.length - 1] : undefined\n\n  let contents = page\n    .map((key) => {\n      let object = objects.get(`${bucket}:${key}`)\n\n      if (object == null) {\n        return ''\n      }\n\n      let outputKey = useUrlEncoding ? encodeURIComponent(key) : key\n\n      return [\n        '<Contents>',\n        `<Key>${escapeXml(outputKey)}</Key>`,\n        `<LastModified>${new Date(object.updatedAt).toISOString()}</LastModified>`,\n        `<Size>${object.body.byteLength}</Size>`,\n        '</Contents>',\n      ].join('')\n    })\n    .join('')\n\n  let xml = [\n    '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n    '<ListBucketResult>',\n    `<IsTruncated>${String(isTruncated)}</IsTruncated>`,\n    nextContinuationToken == null\n      ? ''\n      : `<NextContinuationToken>${escapeXml(nextContinuationToken)}</NextContinuationToken>`,\n    contents,\n    '</ListBucketResult>',\n  ].join('')\n\n  return new Response(xml, {\n    status: 200,\n    headers: {\n      'content-type': 'application/xml',\n    },\n  })\n}\n\nfunction createObjectResponse(object: StoredObject, headOnly: boolean): Response {\n  let headers = new Headers({\n    'content-type': object.contentType,\n    'content-length': String(object.body.byteLength),\n    'last-modified': new Date(object.updatedAt).toUTCString(),\n  })\n\n  for (let [header, value] of Object.entries(object.metadata)) {\n    headers.set(header, value)\n  }\n\n  let body = headOnly ? null : object.body\n\n  return new Response(body, {\n    status: 200,\n    headers,\n  })\n}\n\nfunction parseBucketAndKey(url: URL): { bucket: string | undefined; key: string } {\n  let hostStyleBucket = url.hostname.startsWith(`${BUCKET}.`) ? BUCKET : undefined\n  let pathSegments = url.pathname\n    .replace(/^\\/+/, '')\n    .split('/')\n    .filter((segment) => segment !== '')\n\n  if (hostStyleBucket != null) {\n    return {\n      bucket: hostStyleBucket,\n      key: decodePathKey(pathSegments.join('/')),\n    }\n  }\n\n  let bucket = pathSegments[0] != null ? decodeURIComponent(pathSegments[0]) : undefined\n  let key = decodePathKey(pathSegments.slice(1).join('/'))\n\n  return { bucket, key }\n}\n\nfunction decodePathKey(path: string): string {\n  if (path === '') {\n    return ''\n  }\n\n  return path\n    .split('/')\n    .map((segment) => decodeURIComponent(segment))\n    .join('/')\n}\n\nfunction createErrorResponse(status: number, code: string, message: string): Response {\n  let xml = [\n    '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n    '<Error>',\n    `<Code>${escapeXml(code)}</Code>`,\n    `<Message>${escapeXml(message)}</Message>`,\n    '</Error>',\n  ].join('')\n\n  return new Response(xml, {\n    status,\n    headers: {\n      'content-type': 'application/xml',\n    },\n  })\n}\n\nfunction escapeXml(value: string): string {\n  return value\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&apos;')\n}\n"
  },
  {
    "path": "packages/file-storage-s3/src/lib/s3.ts",
    "content": "import { AwsClient } from 'aws4fetch'\n\nimport type { FileMetadata, FileStorage, ListOptions, ListResult } from '@remix-run/file-storage'\n\nconst CONTENTS_PATTERN = /<Contents>([\\s\\S]*?)<\\/Contents>/g\n\ntype ListedObject = {\n  key: string\n  lastModified: number\n  size: number\n}\n\n/**\n * Configuration for an S3-backed `FileStorage` implementation.\n */\nexport interface S3FileStorageOptions {\n  /**\n   * AWS access key ID used to sign S3 requests.\n   */\n  accessKeyId: string\n  /**\n   * AWS secret access key used to sign S3 requests.\n   */\n  secretAccessKey: string\n  /**\n   * Bucket name used for all file storage operations.\n   */\n  bucket: string\n  /**\n   * AWS region for request signing.\n   */\n  region: string\n  /**\n   * Custom S3-compatible endpoint URL. Defaults to AWS S3 for the given region.\n   */\n  endpoint?: string\n  /**\n   * Whether to use path-style bucket URLs (`/bucket/key`). Defaults to `true` when `endpoint` is\n   * provided and `false` otherwise.\n   */\n  forcePathStyle?: boolean\n  /**\n   * Optional session token for temporary credentials.\n   */\n  sessionToken?: string\n  /**\n   * Optional fetch implementation.\n   */\n  fetch?: typeof globalThis.fetch\n}\n\n/**\n * Creates an S3-backed implementation of `FileStorage`.\n *\n * This works with AWS S3 and S3-compatible providers (for example MinIO or LocalStack) by\n * overriding the `endpoint` option.\n *\n * @param options Configuration for the S3 backend\n * @returns A `FileStorage` implementation backed by S3\n */\nexport function createS3FileStorage(options: S3FileStorageOptions): FileStorage {\n  let endpoint = new URL(options.endpoint ?? `https://s3.${options.region}.amazonaws.com`)\n  let forcePathStyle = options.forcePathStyle ?? options.endpoint != null\n\n  let aws = new AwsClient({\n    accessKeyId: options.accessKeyId,\n    secretAccessKey: options.secretAccessKey,\n    sessionToken: options.sessionToken,\n    service: 's3',\n    region: options.region,\n  })\n\n  async function s3Fetch(url: URL, init?: RequestInit): Promise<Response> {\n    let request = await aws.sign(url, init)\n\n    if (options.fetch != null) {\n      return options.fetch(request)\n    }\n\n    return fetch(request)\n  }\n\n  function getBucketUrl(): URL {\n    return createBucketUrl(endpoint, options.bucket, forcePathStyle)\n  }\n\n  function getObjectUrl(key: string): URL {\n    return createObjectUrl(endpoint, options.bucket, forcePathStyle, key)\n  }\n\n  async function putFile(key: string, file: File): Promise<File> {\n    let body = await file.arrayBuffer()\n    let headers = new Headers()\n\n    if (file.type !== '') {\n      headers.set('content-type', file.type)\n    }\n\n    headers.set('x-amz-meta-file-name', encodeMetadataValue(file.name))\n    headers.set('x-amz-meta-file-last-modified', String(file.lastModified))\n\n    let response = await s3Fetch(getObjectUrl(key), {\n      method: 'PUT',\n      headers,\n      body,\n    })\n    await assertOk(response, `PUT \"${key}\"`)\n\n    return new File([body], file.name, {\n      lastModified: file.lastModified,\n      type: file.type,\n    })\n  }\n\n  async function getFileMetadata(object: ListedObject): Promise<FileMetadata> {\n    let response = await s3Fetch(getObjectUrl(object.key), { method: 'HEAD' })\n\n    if (response.status === 404) {\n      return {\n        key: object.key,\n        lastModified: object.lastModified,\n        name: getDefaultFileName(object.key),\n        size: object.size,\n        type: '',\n      }\n    }\n\n    await assertOk(response, `HEAD \"${object.key}\"`)\n\n    return {\n      key: object.key,\n      lastModified:\n        parseEpochMillis(response.headers.get('x-amz-meta-file-last-modified')) ??\n        object.lastModified,\n      name:\n        decodeMetadataValue(response.headers.get('x-amz-meta-file-name')) ??\n        getDefaultFileName(object.key),\n      size: parseInteger(response.headers.get('content-length')) ?? object.size,\n      type: response.headers.get('content-type') ?? '',\n    }\n  }\n\n  return {\n    async get(key: string): Promise<File | null> {\n      let response = await s3Fetch(getObjectUrl(key), { method: 'GET' })\n\n      if (response.status === 404) {\n        return null\n      }\n\n      await assertOk(response, `GET \"${key}\"`)\n\n      let body = await response.arrayBuffer()\n\n      return new File(\n        [body],\n        decodeMetadataValue(response.headers.get('x-amz-meta-file-name')) ??\n          getDefaultFileName(key),\n        {\n          lastModified:\n            parseEpochMillis(response.headers.get('x-amz-meta-file-last-modified')) ??\n            parseHttpDate(response.headers.get('last-modified')) ??\n            0,\n          type: response.headers.get('content-type') ?? '',\n        },\n      )\n    },\n    async has(key: string): Promise<boolean> {\n      let response = await s3Fetch(getObjectUrl(key), { method: 'HEAD' })\n\n      if (response.status === 404) {\n        return false\n      }\n\n      await assertOk(response, `HEAD \"${key}\"`)\n\n      return true\n    },\n    async list<opts extends ListOptions>(options?: opts): Promise<ListResult<opts>> {\n      let { cursor, includeMetadata = false, limit = 32, prefix } = options ?? {}\n\n      if (limit <= 0) {\n        return {\n          files: [] as ListResult<opts>['files'],\n        }\n      }\n\n      let url = getBucketUrl()\n      url.searchParams.set('encoding-type', 'url')\n      url.searchParams.set('list-type', '2')\n      url.searchParams.set('max-keys', String(limit))\n\n      if (cursor !== undefined) {\n        url.searchParams.set('continuation-token', cursor)\n      }\n      if (prefix !== undefined) {\n        url.searchParams.set('prefix', prefix)\n      }\n\n      let response = await s3Fetch(url, { method: 'GET' })\n      await assertOk(response, 'LIST')\n\n      let xml = await response.text()\n      let objects = parseListedObjects(xml)\n      let nextCursor = parseNextCursor(xml)\n\n      if (!includeMetadata) {\n        return {\n          cursor: nextCursor,\n          files: objects.map((object) => ({ key: object.key })) as ListResult<opts>['files'],\n        }\n      }\n\n      let files = await Promise.all(objects.map((object) => getFileMetadata(object)))\n\n      return {\n        cursor: nextCursor,\n        files: files as ListResult<opts>['files'],\n      }\n    },\n    put(key: string, file: File): Promise<File> {\n      return putFile(key, file)\n    },\n    async remove(key: string): Promise<void> {\n      let response = await s3Fetch(getObjectUrl(key), { method: 'DELETE' })\n\n      if (response.status === 404) {\n        return\n      }\n\n      await assertOk(response, `DELETE \"${key}\"`)\n    },\n    async set(key: string, file: File): Promise<void> {\n      await putFile(key, file)\n    },\n  }\n}\n\nfunction createBucketUrl(endpoint: URL, bucket: string, forcePathStyle: boolean): URL {\n  let url = new URL(endpoint.toString())\n\n  if (forcePathStyle) {\n    url.pathname = joinPath(endpoint.pathname, encodeURIComponent(bucket))\n  } else {\n    url.hostname = `${bucket}.${endpoint.hostname}`\n    url.pathname = joinPath(endpoint.pathname)\n  }\n\n  return url\n}\n\nfunction createObjectUrl(endpoint: URL, bucket: string, forcePathStyle: boolean, key: string): URL {\n  let url = new URL(endpoint.toString())\n\n  if (forcePathStyle) {\n    url.pathname = joinPath(endpoint.pathname, encodeURIComponent(bucket), encodeS3Key(key))\n  } else {\n    url.hostname = `${bucket}.${endpoint.hostname}`\n    url.pathname = joinPath(endpoint.pathname, encodeS3Key(key))\n  }\n\n  return url\n}\n\nfunction encodeS3Key(key: string): string {\n  return key\n    .split('/')\n    .map((segment) => encodeURIComponent(segment))\n    .join('/')\n}\n\nfunction encodeMetadataValue(value: string): string {\n  return encodeURIComponent(value)\n}\n\nfunction decodeMetadataValue(value: string | null): string | undefined {\n  if (value == null || value === '') {\n    return undefined\n  }\n\n  try {\n    return decodeURIComponent(value)\n  } catch {\n    return value\n  }\n}\n\nfunction getDefaultFileName(key: string): string {\n  let lastSlashIndex = key.lastIndexOf('/')\n  let fileName = lastSlashIndex >= 0 ? key.slice(lastSlashIndex + 1) : key\n  return fileName === '' ? key : fileName\n}\n\nfunction parseListedObjects(xml: string): ListedObject[] {\n  CONTENTS_PATTERN.lastIndex = 0\n\n  let objects: ListedObject[] = []\n\n  for (let match of xml.matchAll(CONTENTS_PATTERN)) {\n    let entry = match[1] ?? ''\n    let encodedKey = readXmlTag(entry, 'Key')\n\n    if (encodedKey == null) {\n      continue\n    }\n\n    let key = decodeS3Key(encodedKey)\n    let size = parseInteger(readXmlTag(entry, 'Size')) ?? 0\n    let lastModified = parseHttpDate(readXmlTag(entry, 'LastModified')) ?? 0\n\n    objects.push({\n      key,\n      lastModified,\n      size,\n    })\n  }\n\n  return objects\n}\n\nfunction parseNextCursor(xml: string): string | undefined {\n  if (readXmlTag(xml, 'IsTruncated') !== 'true') {\n    return undefined\n  }\n\n  return readXmlTag(xml, 'NextContinuationToken')\n}\n\nfunction decodeS3Key(value: string): string {\n  try {\n    return decodeURIComponent(value)\n  } catch {\n    return value\n  }\n}\n\nfunction readXmlTag(xml: string, tagName: string): string | undefined {\n  let pattern = new RegExp(`<${tagName}>([\\\\s\\\\S]*?)</${tagName}>`)\n  let match = xml.match(pattern)\n\n  if (match == null || match[1] == null) {\n    return undefined\n  }\n\n  return decodeXmlEntities(match[1])\n}\n\nfunction decodeXmlEntities(value: string): string {\n  return value\n    .replace(/&amp;/g, '&')\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>')\n    .replace(/&quot;/g, '\"')\n    .replace(/&#39;/g, \"'\")\n    .replace(/&apos;/g, \"'\")\n}\n\nfunction parseInteger(value: string | null | undefined): number | undefined {\n  if (value == null || value === '') {\n    return undefined\n  }\n\n  let parsed = Number(value)\n  return Number.isFinite(parsed) ? parsed : undefined\n}\n\nfunction parseEpochMillis(value: string | null): number | undefined {\n  let parsed = parseInteger(value)\n  return parsed != null ? parsed : undefined\n}\n\nfunction parseHttpDate(value: string | null | undefined): number | undefined {\n  if (value == null || value === '') {\n    return undefined\n  }\n\n  let parsed = Date.parse(value)\n  return Number.isFinite(parsed) ? parsed : undefined\n}\n\nfunction joinPath(basePath: string, ...parts: (string | undefined)[]): string {\n  let normalizedBasePath = basePath.replace(/\\/+$/g, '')\n  let normalizedParts = parts\n    .filter((part): part is string => part != null && part !== '')\n    .map((part) => part.replace(/^\\/+|\\/+$/g, ''))\n\n  let joined = [normalizedBasePath, ...normalizedParts].filter((part) => part !== '').join('/')\n  if (joined === '') {\n    return '/'\n  }\n\n  return joined.startsWith('/') ? joined : `/${joined}`\n}\n\nasync function assertOk(response: Response, operation: string): Promise<void> {\n  if (response.ok) {\n    return\n  }\n\n  let message = `${response.status} ${response.statusText}`\n\n  try {\n    let body = await response.text()\n    let s3Message = readXmlTag(body, 'Message')\n\n    if (s3Message != null && s3Message !== '') {\n      message = `${message} (${s3Message})`\n    }\n  } catch {\n    // Ignore body parse errors and keep the status-only message.\n  }\n\n  throw new Error(`S3 request failed for ${operation}: ${message}`)\n}\n"
  },
  {
    "path": "packages/file-storage-s3/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/file-storage-s3/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/form-data-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/form-data-middleware/.changes/minor.context-form-data-key.md",
    "content": "BREAKING CHANGE: Form data middleware no longer reads/writes `context.formData` or `context.files`.\n\nParsed `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(...)`.\n"
  },
  {
    "path": "packages/form-data-middleware/.changes/patch.no-op-if-already-parsed.md",
    "content": "`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.\n"
  },
  {
    "path": "packages/form-data-middleware/CHANGELOG.md",
    "content": "# `form-data-middleware` CHANGELOG\n\nThis 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/).\n\n## v0.1.4\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0)\n\n## v0.1.3\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0)\n  - [`@remix-run/form-data-parser@0.15.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.15.0)\n\n## v0.1.2\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.1.1 (2025-12-06)\n\n- Explicitly set `context.formData` in all `POST` cases, even when the request body is invalid\n\n## v0.1.0 (2025-11-19)\n\nInitial release extracted from `@remix-run/fetch-router` v0.9.0.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/form-data-middleware/README.md) for more details.\n"
  },
  {
    "path": "packages/form-data-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/form-data-middleware/README.md",
    "content": "# form-data-middleware\n\nForm 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)`.\n\n## Features\n\n- **Request Form Parsing** - Parses request body form data once per request\n- **File Access** - Uploaded files are available from `context.get(FormData)`\n- **Custom Upload Handling** - Supports pluggable upload handlers for file processing\n- **Error Control** - Optional suppression for malformed form data\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nUse 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)`.\n\nUploaded 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)`.\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { formData } from 'remix/form-data-middleware'\n\nlet router = createRouter({\n  middleware: [formData()],\n})\n\nrouter.post('/users', async (context) => {\n  let formData = context.get(FormData)\n  let name = formData.get('name')\n  let email = formData.get('email')\n\n  // Handle file uploads\n  let avatar = formData.get('avatar')\n\n  return Response.json({ name, email, hasAvatar: avatar instanceof File })\n})\n```\n\n### Custom File Upload Handler\n\nYou 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.\n\n```ts\nimport { formData } from 'remix/form-data-middleware'\nimport { writeFile } from 'node:fs/promises'\n\nlet router = createRouter({\n  middleware: [\n    formData({\n      async uploadHandler(upload) {\n        // Save to disk and return path\n        let path = `./uploads/${upload.name}`\n        await writeFile(path, Buffer.from(await upload.arrayBuffer()))\n        return path\n      },\n    }),\n  ],\n})\n```\n\n### Limit Multipart Growth\n\n`formData()` forwards multipart limit options to `parseFormData()`, so you can cap uploads with\n`maxHeaderSize`, `maxFiles`, `maxFileSize`, `maxParts`, and `maxTotalSize`.\n\n```ts\nlet router = createRouter({\n  middleware: [\n    formData({\n      maxFiles: 5,\n      maxFileSize: 10 * 1024 * 1024,\n      maxParts: 25,\n      maxTotalSize: 12 * 1024 * 1024,\n    }),\n  ],\n})\n```\n\n### Suppress Parse Errors\n\nSome 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.\n\n```ts\nlet router = createRouter({\n  middleware: [\n    formData({\n      suppressErrors: true, // Invalid form data won't throw\n    }),\n  ],\n})\n```\n\n## Related Packages\n\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - The underlying form data parser\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/form-data-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/form-data-middleware\",\n  \"version\": \"0.1.4\",\n  \"description\": \"Middleware for parsing FormData from request bodies\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/form-data-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/form-data-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/form-data-parser\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\",\n    \"@remix-run/form-data-parser\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"form-data\",\n    \"multipart\",\n    \"file-upload\"\n  ]\n}\n"
  },
  {
    "path": "packages/form-data-middleware/src/index.ts",
    "content": "export { FormDataParseError } from '@remix-run/form-data-parser'\nexport type { FileUpload, FileUploadHandler } from '@remix-run/form-data-parser'\n\nexport { type FormDataOptions, formData } from './lib/form-data.ts'\n"
  },
  {
    "path": "packages/form-data-middleware/src/lib/form-data.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it, mock } from 'node:test'\n\nimport {\n  FormDataParseError,\n  MaxFilesExceededError,\n  MaxFileSizeExceededError,\n  MaxHeaderSizeExceededError,\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n  type FileUploadHandler,\n} from '@remix-run/form-data-parser'\nimport { createRouter } from '@remix-run/fetch-router'\n\nimport { formData } from './form-data.ts'\n\ndescribe('formData middleware', () => {\n  it('parses application/x-www-form-urlencoded form data from the request body', async () => {\n    let router = createRouter({\n      middleware: [formData()],\n    })\n\n    router.post('/', (context) => {\n      let entries = Object.fromEntries(context.get(FormData).entries())\n      return Response.json(entries)\n    })\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: 'name=test',\n    })\n\n    assert.equal(response.status, 200)\n    assert.deepEqual(await response.json(), { name: 'test' })\n  })\n\n  it('parses multipart/form-data form data from the request body', async () => {\n    let router = createRouter({\n      middleware: [formData()],\n    })\n\n    router.post('/', (context) => {\n      let entries = Object.fromEntries(context.get(FormData).entries())\n      return Response.json(entries)\n    })\n\n    let boundary = '----WebKitFormBoundary1234567890'\n    let response = await router.fetch('https://remix.run/', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: [\n        `--${boundary}`,\n        'Content-Disposition: form-data; name=\"name\"',\n        '',\n        'test',\n        `--${boundary}--`,\n      ].join('\\r\\n'),\n    })\n\n    assert.equal(response.status, 200)\n    assert.deepEqual(await response.json(), { name: 'test' })\n  })\n\n  it('stores uploaded files in context.get(FormData) on a multipart/form-data request', async () => {\n    let router = createRouter({\n      middleware: [formData()],\n    })\n\n    router.post('/', (context) => {\n      let file1 = context.get(FormData).get('file1')\n      let file2 = context.get(FormData).get('file2')\n\n      return Response.json({\n        file1: {\n          isFile: file1 instanceof File,\n          name: file1 instanceof File ? file1.name : null,\n          type: file1 instanceof File ? file1.type : null,\n        },\n        file2: {\n          isFile: file2 instanceof File,\n          name: file2 instanceof File ? file2.name : null,\n          type: file2 instanceof File ? file2.type : null,\n        },\n      })\n    })\n\n    let boundary = '----WebKitFormBoundary1234567890'\n    let response = await router.fetch('https://remix.run/', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: [\n        `--${boundary}`,\n        'Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'test 1',\n        `--${boundary}`,\n        'Content-Disposition: form-data; name=\"file2\"; filename=\"test2.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'test 2',\n        `--${boundary}--`,\n      ].join('\\r\\n'),\n    })\n\n    assert.equal(response.status, 200)\n    assert.deepEqual(await response.json(), {\n      file1: {\n        isFile: true,\n        name: 'test1.txt',\n        type: 'text/plain',\n      },\n      file2: {\n        isFile: true,\n        name: 'test2.txt',\n        type: 'text/plain',\n      },\n    })\n  })\n\n  it('throws when the request body is malformed multipart/form-data', async () => {\n    let router = createRouter({\n      middleware: [formData()],\n    })\n\n    router.post('/', (context) => Response.json(context.get(FormData)))\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'multipart/form-data',\n        },\n        body: 'invalid',\n      })\n    }, FormDataParseError)\n  })\n\n  it('suppresses parse errors when suppressErrors is true', async () => {\n    let router = createRouter({\n      middleware: [formData({ suppressErrors: true })],\n    })\n\n    router.post('/', (context) => {\n      let entries = Object.fromEntries(context.get(FormData).entries())\n      return Response.json(entries)\n    })\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data',\n      },\n      body: 'invalid',\n    })\n\n    assert.equal(response.status, 200)\n    assert.deepEqual(await response.json(), {})\n  })\n\n  it('sets context.get(FormData) to an empty FormData when parse errors are suppressed', async () => {\n    let router = createRouter({\n      middleware: [formData({ suppressErrors: true })],\n    })\n\n    router.post('/', (context) =>\n      // Explicitly check that FormData exists in request context\n      Response.json({\n        isDefined: context.has(FormData),\n        isFormData: context.get(FormData) instanceof FormData,\n        isEmpty: context.get(FormData).entries().next().done,\n      }),\n    )\n\n    let response = await router.fetch('https://remix.run/', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data',\n      },\n      body: 'invalid',\n    })\n\n    assert.equal(response.status, 200)\n    assert.deepEqual(await response.json(), {\n      isDefined: true,\n      isFormData: true,\n      isEmpty: true,\n    })\n  })\n\n  it('does not suppress maxFiles errors when suppressErrors is true', async () => {\n    let actionCalled = false\n    let router = createRouter({\n      middleware: [formData({ suppressErrors: true, maxFiles: 1 })],\n    })\n\n    router.post('/', () => {\n      actionCalled = true\n      return new Response('ok')\n    })\n\n    let boundary = '----WebKitFormBoundary1234567890'\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/', {\n        method: 'POST',\n        headers: {\n          'Content-Type': `multipart/form-data; boundary=${boundary}`,\n        },\n        body: [\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"',\n          'Content-Type: text/plain',\n          '',\n          'test1',\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"file2\"; filename=\"test2.txt\"',\n          'Content-Type: text/plain',\n          '',\n          'test2',\n          `--${boundary}--`,\n        ].join('\\r\\n'),\n      })\n    }, MaxFilesExceededError)\n\n    assert.equal(actionCalled, false)\n  })\n\n  it('does not suppress maxHeaderSize errors when suppressErrors is true', async () => {\n    let actionCalled = false\n    let router = createRouter({\n      middleware: [formData({ suppressErrors: true, maxHeaderSize: 4 * 1024 })],\n    })\n\n    router.post('/', () => {\n      actionCalled = true\n      return new Response('ok')\n    })\n\n    let boundary = '----WebKitFormBoundary1234567890'\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/', {\n        method: 'POST',\n        headers: {\n          'Content-Type': `multipart/form-data; boundary=${boundary}`,\n        },\n        body: [\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"field1\"',\n          'X-Large-Header: ' + 'X'.repeat(6 * 1024),\n          '',\n          'value1',\n          `--${boundary}--`,\n        ].join('\\r\\n'),\n      })\n    }, MaxHeaderSizeExceededError)\n\n    assert.equal(actionCalled, false)\n  })\n\n  it('does not suppress maxFileSize errors when suppressErrors is true', async () => {\n    let actionCalled = false\n    let router = createRouter({\n      middleware: [formData({ suppressErrors: true, maxFileSize: 4 })],\n    })\n\n    router.post('/', () => {\n      actionCalled = true\n      return new Response('ok')\n    })\n\n    let boundary = '----WebKitFormBoundary1234567890'\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/', {\n        method: 'POST',\n        headers: {\n          'Content-Type': `multipart/form-data; boundary=${boundary}`,\n        },\n        body: [\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"',\n          'Content-Type: text/plain',\n          '',\n          'hello',\n          `--${boundary}--`,\n        ].join('\\r\\n'),\n      })\n    }, MaxFileSizeExceededError)\n\n    assert.equal(actionCalled, false)\n  })\n\n  it('does not suppress maxParts errors when suppressErrors is true', async () => {\n    let actionCalled = false\n    let router = createRouter({\n      middleware: [formData({ suppressErrors: true, maxParts: 2 })],\n    })\n\n    router.post('/', () => {\n      actionCalled = true\n      return new Response('ok')\n    })\n\n    let boundary = '----WebKitFormBoundary1234567890'\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/', {\n        method: 'POST',\n        headers: {\n          'Content-Type': `multipart/form-data; boundary=${boundary}`,\n        },\n        body: [\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"field1\"',\n          '',\n          'value1',\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"',\n          'Content-Type: text/plain',\n          '',\n          'test1',\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"field2\"',\n          '',\n          'value2',\n          `--${boundary}--`,\n        ].join('\\r\\n'),\n      })\n    }, MaxPartsExceededError)\n\n    assert.equal(actionCalled, false)\n  })\n\n  it('does not suppress maxTotalSize errors when suppressErrors is true', async () => {\n    let actionCalled = false\n    let router = createRouter({\n      middleware: [formData({ suppressErrors: true, maxTotalSize: 9 })],\n    })\n\n    router.post('/', () => {\n      actionCalled = true\n      return new Response('ok')\n    })\n\n    let boundary = '----WebKitFormBoundary1234567890'\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run/', {\n        method: 'POST',\n        headers: {\n          'Content-Type': `multipart/form-data; boundary=${boundary}`,\n        },\n        body: [\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"field1\"',\n          '',\n          'hello',\n          `--${boundary}`,\n          'Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"',\n          'Content-Type: text/plain',\n          '',\n          'world',\n          `--${boundary}--`,\n        ].join('\\r\\n'),\n      })\n    }, MaxTotalSizeExceededError)\n\n    assert.equal(actionCalled, false)\n  })\n\n  it('invokes a custom `uploadHandler` for file uploads', async () => {\n    let uploadHandler = mock.fn<FileUploadHandler>()\n\n    let router = createRouter({\n      middleware: [formData({ uploadHandler })],\n    })\n\n    router.post('/', () => new Response('home'))\n\n    let boundary = '----WebKitFormBoundary1234567890'\n    let response = await router.fetch('https://remix.run/', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: [\n        `--${boundary}`,\n        'Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'test 1',\n        `--${boundary}`,\n        'Content-Disposition: form-data; name=\"file2\"; filename=\"test2.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'test 2',\n        `--${boundary}--`,\n      ].join('\\r\\n'),\n    })\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'home')\n\n    assert.equal(uploadHandler.mock.calls.length, 2)\n\n    let call0 = uploadHandler.mock.calls[0]\n    let upload1 = call0.arguments[0]\n    assert.equal(upload1.fieldName, 'file1')\n    assert.equal(upload1.name, 'test1.txt')\n    assert.equal(upload1.type, 'text/plain')\n    assert.equal(await upload1.text(), 'test 1')\n\n    let call1 = uploadHandler.mock.calls[1]\n    let upload2 = call1.arguments[0]\n    assert.equal(upload2.fieldName, 'file2')\n    assert.equal(upload2.name, 'test2.txt')\n    assert.equal(upload2.type, 'text/plain')\n    assert.equal(await upload2.text(), 'test 2')\n  })\n\n  it('is a no-op when FormData has already been parsed by an earlier middleware', async () => {\n    let firstUploadHandler = mock.fn<FileUploadHandler>((upload) => `first:${upload.name}`)\n    let secondUploadHandler = mock.fn<FileUploadHandler>((upload) => `second:${upload.name}`)\n\n    let router = createRouter({\n      middleware: [\n        formData({ uploadHandler: firstUploadHandler }),\n        formData({ uploadHandler: secondUploadHandler }),\n      ],\n    })\n\n    router.post('/', (context) =>\n      Response.json({\n        file: context.get(FormData).get('file'),\n      }),\n    )\n\n    let boundary = '----WebKitFormBoundary1234567890'\n    let response = await router.fetch('https://remix.run/', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: [\n        `--${boundary}`,\n        'Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'test',\n        `--${boundary}--`,\n      ].join('\\r\\n'),\n    })\n\n    assert.equal(response.status, 200)\n    assert.deepEqual(await response.json(), {\n      file: 'first:test.txt',\n    })\n    assert.equal(firstUploadHandler.mock.calls.length, 1)\n    assert.equal(secondUploadHandler.mock.calls.length, 0)\n  })\n\n  it('is a no-op when FormData has already been parsed by earlier request pipeline middleware', async () => {\n    let globalUploadHandler = mock.fn<FileUploadHandler>((upload) => `global:${upload.name}`)\n    let routeUploadHandler = mock.fn<FileUploadHandler>((upload) => `route:${upload.name}`)\n\n    let router = createRouter({\n      middleware: [formData({ uploadHandler: globalUploadHandler })],\n    })\n\n    router.post('/', {\n      middleware: [formData({ uploadHandler: routeUploadHandler })],\n      action(context) {\n        return Response.json({\n          file: context.get(FormData).get('file'),\n        })\n      },\n    })\n\n    let boundary = '----WebKitFormBoundary1234567890'\n    let response = await router.fetch('https://remix.run/', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: [\n        `--${boundary}`,\n        'Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'test',\n        `--${boundary}--`,\n      ].join('\\r\\n'),\n    })\n\n    assert.equal(response.status, 200)\n    assert.deepEqual(await response.json(), {\n      file: 'global:test.txt',\n    })\n    assert.equal(globalUploadHandler.mock.calls.length, 1)\n    assert.equal(routeUploadHandler.mock.calls.length, 0)\n  })\n})\n"
  },
  {
    "path": "packages/form-data-middleware/src/lib/form-data.ts",
    "content": "import {\n  MaxFilesExceededError,\n  MaxFileSizeExceededError,\n  MaxHeaderSizeExceededError,\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n  parseFormData,\n  type FileUploadHandler,\n  type ParseFormDataOptions,\n} from '@remix-run/form-data-parser'\nimport type { Middleware } from '@remix-run/fetch-router'\n\nfunction isMultipartLimitError(error: unknown): boolean {\n  return (\n    error instanceof MaxFilesExceededError ||\n    error instanceof MaxHeaderSizeExceededError ||\n    error instanceof MaxFileSizeExceededError ||\n    error instanceof MaxPartsExceededError ||\n    error instanceof MaxTotalSizeExceededError\n  )\n}\n\n/**\n * Options for the {@link formData} middleware.\n */\nexport interface FormDataOptions extends ParseFormDataOptions {\n  /**\n   * Set `true` to suppress malformed form-data parse errors. Multipart limit violations always\n   * surface as errors even when suppression is enabled.\n   *\n   * @default false\n   */\n  suppressErrors?: boolean\n  /**\n   * A function that handles file uploads. It receives a `FileUpload` object and may return any\n   * value that is a valid `FormData` value. Default is `undefined`, which means file uploads are\n   * stored in memory.\n   */\n  uploadHandler?: FileUploadHandler\n}\n\n/**\n * Middleware that parses `FormData` from the request body and populates request context.\n *\n * @param options Options for parsing form data\n * @returns A middleware function that parses form data\n */\nexport function formData(options?: FormDataOptions): Middleware {\n  let suppressErrors = options?.suppressErrors ?? false\n  let uploadHandler = options?.uploadHandler\n\n  return async (context) => {\n    if (context.has(FormData)) {\n      return\n    }\n\n    if (context.method === 'GET' || context.method === 'HEAD') {\n      return\n    }\n\n    let contentType = context.headers.get('Content-Type')\n    if (\n      contentType == null ||\n      (!contentType.startsWith('multipart/') &&\n        !contentType.startsWith('application/x-www-form-urlencoded'))\n    ) {\n      context.set(FormData, new FormData())\n      return\n    }\n\n    try {\n      context.set(FormData, await parseFormData(context.request, options, uploadHandler))\n    } catch (error) {\n      if (!suppressErrors || isMultipartLimitError(error)) {\n        throw error\n      }\n\n      context.set(FormData, new FormData())\n    }\n  }\n}\n"
  },
  {
    "path": "packages/form-data-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/form-data-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/form-data-parser/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/form-data-parser/.changes/minor.aggregate-multipart-limits.md",
    "content": "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.\n\nApps that intentionally accept large multipart submissions may need to raise these limits explicitly.\n"
  },
  {
    "path": "packages/form-data-parser/CHANGELOG.md",
    "content": "# `form-data-parser` CHANGELOG\n\nThis 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/).\n\n## v0.15.0\n\n### Minor Changes\n\n- Bump multipart-parser dependency to 0.14.2\n\n## v0.14.0 (2025-11-05)\n\n- Build using `tsc` instead of `esbuild`. This means modules in the `dist` directory now mirror the layout of modules in the `src` directory.\n\n## v0.13.0 (2025-11-04)\n\n- Throw `FormDataParseError` when the request body is malformed multipart/form-data. The underlying `MultipartParseError` is its `cause`.\n\n## v0.12.0 (2025-10-22)\n\n- 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()`.\n\n## v0.11.0 (2025-10-05)\n\n- Make `options` optional in `parseFormData` signature\n- Export `ParseFormDataOptions` type\n\n## v0.10.1 (2025-07-24)\n\n- Update to `@remix-run/multipart-parser` v0.11.0\n\n## v0.10.0 (2025-07-24)\n\n- Renamed package from `@mjackson/form-data-parser` to `@remix-run/form-data-parser`\n\n## v0.9.1 (2025-06-13)\n\n- Export `FormDataParserError` and `MaxFilesExceededError`\n- Re-export `MultipartParseError`, `MaxHeaderSizeExceededError`, and `MaxFileSizeExceededError` from multipart parser\n\n## v0.9.0 (2025-06-13)\n\nThis release updates to `multipart-parser` 0.10.0 and removes the restrictions on checking the `size` and/or `slice`ing `FileUpload` objects.\n\n- `FileUpload` is now a normal subclass of `File` with all the same functionality (instead of just implementing the same interface)\n- Add `maxFiles` option to `parseFormData` to allow limiting the number of files uploaded in a single request\n\n```ts\nlet formData = await parseFormData(request, { maxFiles: 5 })\nlet file = formData.get('file-upload')\nlet size = file.size // This is ok now!\n```\n\n## v0.8.0 (2025-06-10)\n\n- Add `/src` to npm package, so \"go to definition\" goes to the actual source\n- Use one set of types for all built files, instead of separate types for ESM and CJS\n- Build using esbuild directly instead of tsup\n\n## v0.7.0 (2025-01-25)\n\n- BREAKING CHANGE: Override `parseFormData` signature so the upload handler is always last in the argument list. `parserOptions` are now an optional 2nd arg.\n\n```ts\nimport { parseFormData } from '@remix-run/form-data-parser'\n\n// before\nawait parseFormData(\n  request,\n  (fileUpload) => {\n    // ...\n  },\n  { maxFileSize },\n)\n\n// after\nawait parseFormData(request, { maxFileSize }, (fileUpload) => {\n  // ...\n})\n```\n\n- 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)\n- 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\n- Expand `FileUploadHandler` interface to support returning `Blob` from the upload handler, which is the superclass of `File`\n\n## v0.6.0 (2025-01-15)\n\n- Allow upload handlers to run in parallel. Fixes #44\n\n## v0.5.1 (2024-12-12)\n\n- Fix dependency on `headers` in package.json\n\n## v0.5.0 (2024-11-14)\n\n- Added CommonJS build\n\n## v0.4.0 (2024-09-05)\n\n- Allow passing `MultipartParserOptions` as optional 3rd arg to `parseFormData()`\n\n## v0.3.0 (2024-09-05)\n\n- Make `FileUpload` implement the `File` interface instead of extending `File` (fixes https://github.com/mjackson/form-data-parser/issues/4)\n- Allow returning `null` from an upload handler, so it allows `return fileStorage.get(key)` without type errors\n\n## v0.2.0 (2024-08-28)\n\n- Add missing `FileUpload` export 🤦‍♂️\n\n## v0.1.0 (2024-08-24)\n\n- Initial release\n"
  },
  {
    "path": "packages/form-data-parser/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/form-data-parser/README.md",
    "content": "# form-data-parser\n\nA 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.\n\n## Features\n\n- **Drop-in replacement** for `request.formData()` with streaming file upload support\n- **Minimal buffering** - processes file upload streams with minimal memory footprint\n- **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)\n- **Smart fallback** - automatically uses native `request.formData()` for non-`multipart/form-data` requests\n- **Storage agnostic** - works with any storage backend (local disk, S3, R2, etc.)\n\n## Why You Need This\n\nThe native [`request.formData()` method](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData) has a few major flaws in server environments:\n\n- It buffers all file uploads in memory\n- It does not provide fine-grained control over file upload handling\n- It does not prevent DoS attacks from malicious requests\n\nIn 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.\n\nFor attackers, this creates an attack vector where malicious actors can overwhelm your server's memory by sending large payloads with many files.\n\n`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.\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nThe `parseFormData` interface allows you to define an \"upload handler\" function for fine-grained control of handling file uploads.\n\n```ts\nimport * as fsp from 'node:fs/promises'\nimport type { FileUpload } from 'remix/form-data-parser'\nimport { parseFormData } from 'remix/form-data-parser'\n\n// Define how to handle incoming file uploads\nasync function uploadHandler(fileUpload: FileUpload) {\n  // Is this file upload from the <input type=\"file\" name=\"user-avatar\"> field?\n  if (fileUpload.fieldName === 'user-avatar') {\n    let filename = `/uploads/user-${user.id}-avatar.bin`\n\n    // Store the file safely on disk\n    await fsp.writeFile(filename, fileUpload.bytes)\n\n    // Return the file name to use in the FormData object so we don't\n    // keep the file contents around in memory.\n    return filename\n  }\n\n  // Ignore unrecognized fields\n}\n\n// Handle form submissions with file uploads\nasync function requestHandler(request: Request) {\n  // Parse the form data from the request.body stream, passing any files\n  // through your upload handler as they are parsed from the stream\n  let formData = await parseFormData(request, uploadHandler)\n\n  let avatarFilename = formData.get('user-avatar')\n\n  if (avatarFilename != null) {\n    console.log(`User avatar uploaded to ${avatarFilename}`)\n  } else {\n    console.log(`No user avatar file was uploaded`)\n  }\n}\n```\n\nTo validate the resulting `FormData` object with `remix/data-schema`, use the\n`remix/data-schema/form-data` helpers.\n\nTo limit the overall shape of multipart requests, use the `maxHeaderSize`, `maxFileSize`, `maxFiles`,\n`maxParts`, and `maxTotalSize` options. By default, `parseFormData()` uses `maxFiles = 20`,\n`maxParts = 1000`, and `maxTotalSize = maxFiles * maxFileSize + 1 MiB`.\n\n```ts\nimport {\n  MaxFilesExceededError,\n  MaxFileSizeExceededError,\n  MaxHeaderSizeExceededError,\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n} from 'remix/form-data-parser'\n\nconst oneKb = 1024\nconst oneMb = 1024 * oneKb\n\ntry {\n  let formData = await parseFormData(request, {\n    maxFiles: 5,\n    maxFileSize: 10 * oneMb,\n    maxParts: 25,\n    maxTotalSize: 12 * oneMb,\n  })\n} catch (error) {\n  if (error instanceof MaxFilesExceededError) {\n    console.error(`Request may not contain more than 5 files`)\n  } else if (error instanceof MaxHeaderSizeExceededError) {\n    console.error(`Multipart headers may not exceed the configured size limit`)\n  } else if (error instanceof MaxFileSizeExceededError) {\n    console.error(`Files may not be larger than 10 MiB`)\n  } else if (error instanceof MaxPartsExceededError) {\n    console.error(`Request may not contain more than 25 multipart parts`)\n  } else if (error instanceof MaxTotalSizeExceededError) {\n    console.error(`Multipart request may not exceed 12 MiB of total content`)\n  } else {\n    console.error(`An unknown error occurred:`, error)\n  }\n}\n```\n\nIf 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.\n\n```ts\nimport { LocalFileStorage } from 'remix/file-storage/local'\nimport type { FileUpload } from 'remix/form-data-parser'\nimport { parseFormData } from 'remix/form-data-parser'\n\n// Set up storage for uploaded files\nconst fileStorage = new LocalFileStorage('/uploads/user-avatars')\n\n// Define how to handle incoming file uploads\nasync function uploadHandler(fileUpload: FileUpload) {\n  // Is this file upload from the <input type=\"file\" name=\"user-avatar\"> field?\n  if (fileUpload.fieldName === 'user-avatar') {\n    let storageKey = `user-${user.id}-avatar`\n\n    // Put the file in storage\n    await fileStorage.set(storageKey, fileUpload)\n\n    // Return a lazy File object that can access the stored file when needed\n    return fileStorage.get(storageKey)\n  }\n\n  // Ignore unrecognized fields\n}\n```\n\n## Demos\n\nThe [`demos` directory](https://github.com/remix-run/remix/tree/main/packages/form-data-parser/demos) contains working demos:\n\n- [`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\n\n## Related Packages\n\n- [`data-schema`](https://github.com/remix-run/remix/tree/main/packages/data-schema) - Tiny,\n  standards-aligned validation with a `form-data` export for `FormData` and `URLSearchParams`\n- [`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\n- [`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser) - The parser used internally for parsing `multipart/form-data` HTTP messages\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/form-data-parser/demos/node/README.md",
    "content": "# form-data-parser Node Example\n\nThis example is a [Node.js server](https://nodejs.org/) that handles file uploads and streams them to a tmp file on disk.\n"
  },
  {
    "path": "packages/form-data-parser/demos/node/package.json",
    "content": "{\n  \"name\": \"form-data-parser-node-example\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@remix-run/file-storage\": \"workspace:^\",\n    \"@remix-run/form-data-parser\": \"workspace:^\",\n    \"@remix-run/multipart-parser\": \"workspace:^\",\n    \"@remix-run/node-fetch-server\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"start\": \"node server.js\",\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "packages/form-data-parser/demos/node/server.js",
    "content": "import * as fsp from 'node:fs/promises'\nimport * as http from 'node:http'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\n\nimport * as s from '@remix-run/data-schema'\nimport * as f from '@remix-run/data-schema/form-data'\nimport { createFsFileStorage } from '@remix-run/file-storage/fs'\nimport {\n  MultipartParseError,\n  MaxFileSizeExceededError,\n  parseFormData,\n} from '@remix-run/form-data-parser'\nimport { createRequestListener } from '@remix-run/node-fetch-server'\n\nconst PORT = 44100\n\nconst oneMb = 1024 * 1024\nconst maxFileSize = 10 * oneMb\nconst submittedDataSchema = f.object({\n  text1: f.field(s.optional(s.string())),\n  image1: f.file(s.optional(s.instanceof_(File))),\n})\n\nconst fileStorage = createFsFileStorage(await fsp.mkdtemp(path.join(os.tmpdir(), 'uploads-')))\n\n/** @type (file: File) => Promise<string> */\nasync function getDataUrl(file) {\n  let buffer = Buffer.from(await file.arrayBuffer())\n  return `data:${file.type};base64,${buffer.toString('base64')}`\n}\n\nconst server = http.createServer(\n  createRequestListener(async (request) => {\n    if (request.method === 'GET') {\n      return new Response(\n        `<!DOCTYPE html>\n<html>\n  <head>\n    <title>form-data-parser Node Example</title>\n  </head>\n  <body>\n    <h1>form-data-parser Node Example</h1>\n    <form method=\"post\" enctype=\"multipart/form-data\">\n      <p>Enter some text: <input name=\"text1\" type=\"text\" /></p>\n      <p>Select an image (max size 10MB): <input name=\"image1\" type=\"file\" accept=\"image/*\" /></p>\n      <p><button type=\"submit\">Submit</button></p>\n    </form>\n  </body>\n</html>`,\n        {\n          headers: {\n            'Content-Type': 'text/html',\n          },\n        },\n      )\n    }\n\n    if (request.method === 'POST') {\n      try {\n        let formData = await parseFormData(request, { maxFileSize }, async (upload) => {\n          let file = await fileStorage.put('image-upload', upload)\n          return file.size === 0 ? null : file\n        })\n\n        let { image1: image, text1: text } = s.parse(submittedDataSchema, formData)\n\n        return new Response(\n          `<!DOCTYPE html>\n<html>\n  <head>\n    <title>form-data-parser Submitted Data</title>\n  </head>\n  <body>\n    <h1>form-data-parser Submitted Data</h1>\n    ${text ? `<p>You entered this text: ${text}</p>` : '<p>You did not enter any text.</p>'}\n    ${image ? `<p>You uploaded this image:</p><p><img src=\"${await getDataUrl(image)}\" /></p>` : '<p>You did not upload an image.</p>'}\n  </body>\n</html>`,\n          {\n            headers: {\n              'Content-Type': 'text/html',\n            },\n          },\n        )\n      } catch (error) {\n        if (error instanceof MaxFileSizeExceededError) {\n          return new Response(error.message, { status: 413 })\n        }\n\n        if (error instanceof MultipartParseError) {\n          return new Response(error.message, { status: 400 })\n        }\n\n        console.error(error)\n\n        return new Response('Internal Server Error', { status: 500 })\n      }\n    }\n\n    return new Response('Method Not Allowed', { status: 405 })\n  }),\n)\n\nserver.listen(PORT, () => {\n  console.log(`Server listening on http://localhost:${PORT} ...`)\n})\n"
  },
  {
    "path": "packages/form-data-parser/demos/node/tsconfig.json",
    "content": "{\n  \"include\": [\"server.js\"],\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\"],\n    \"noEmit\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/form-data-parser/package.json",
    "content": "{\n  \"name\": \"@remix-run/form-data-parser\",\n  \"version\": \"0.15.0\",\n  \"description\": \"A request.formData() wrapper with streaming file upload handling\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/form-data-parser\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/form-data-parser#readme\",\n  \"license\": \"MIT\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"dependencies\": {\n    \"@remix-run/multipart-parser\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@remix-run/data-schema\": \"workspace:^\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"form-data\",\n    \"FormData\",\n    \"multipart\",\n    \"parser\"\n  ]\n}\n"
  },
  {
    "path": "packages/form-data-parser/src/index.ts",
    "content": "export {\n  type FileUploadHandler,\n  type ParseFormDataOptions,\n  FormDataParseError,\n  MaxFilesExceededError,\n  FileUpload,\n  parseFormData,\n} from './lib/form-data.ts'\n\n// Re-export errors that may be thrown by the parser.\nexport {\n  MultipartParseError,\n  MaxHeaderSizeExceededError,\n  MaxFileSizeExceededError,\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n} from '@remix-run/multipart-parser'\n"
  },
  {
    "path": "packages/form-data-parser/src/lib/form-data.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it, mock } from 'node:test'\n\nimport {\n  type FileUploadHandler,\n  FormDataParseError,\n  MaxFilesExceededError,\n  parseFormData,\n} from './form-data.ts'\nimport { MaxPartsExceededError, MaxTotalSizeExceededError } from '../index.ts'\n\ndescribe('parseFormData', () => {\n  it('parses a application/x-www-form-urlencoded request', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: 'text=Hello%2C%20World!',\n    })\n\n    let formData = await parseFormData(request)\n\n    assert.equal(formData.get('text'), 'Hello, World!')\n  })\n\n  it('parses a multipart/form-data request', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',\n      },\n      body: [\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"text\"',\n        '',\n        'Hello, World!',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file\"; filename=\"example.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is an example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW--',\n      ].join('\\r\\n'),\n    })\n\n    let formData = await parseFormData(request)\n\n    assert.equal(formData.get('text'), 'Hello, World!')\n\n    let file = formData.get('file')\n    assert.ok(file instanceof File)\n    assert.equal(file.name, 'example.txt')\n    assert.equal(file.type, 'text/plain')\n    assert.equal(await file.text(), 'This is an example file.')\n  })\n\n  it('calls the file upload handler for each file part', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',\n      },\n      body: [\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file1\"; filename=\"example.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is an example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file2\"; filename=\"example.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is another example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW--',\n      ].join('\\r\\n'),\n    })\n\n    let fileUploadHandler = mock.fn<FileUploadHandler>()\n\n    await parseFormData(request, fileUploadHandler)\n\n    assert.equal(fileUploadHandler.mock.calls.length, 2)\n  })\n\n  it('allows returning `null` from the upload handler', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',\n      },\n      body: [\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file\"; filename=\"example.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is an example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW--',\n      ].join('\\r\\n'),\n    })\n\n    let formData = await parseFormData(request, () => null)\n\n    assert.equal(formData.get('file'), null)\n  })\n\n  it('allows returning strings from the upload handler', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',\n      },\n      body: [\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file\"; filename=\"example.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is an example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW--',\n      ].join('\\r\\n'),\n    })\n\n    let formData = await parseFormData(request, (upload) => upload.text())\n\n    assert.equal(formData.get('file'), 'This is an example file.')\n  })\n\n  it('allows returning files from the upload handler', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',\n      },\n      body: [\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file\"; filename=\"example.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is an example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW--',\n      ].join('\\r\\n'),\n    })\n\n    let formData = await parseFormData(\n      request,\n      async (upload) => new File([await upload.text()], 'example.txt', { type: 'text/plain' }),\n    )\n\n    let file = formData.get('file')\n\n    assert.ok(file instanceof File)\n    assert.equal(file.name, 'example.txt')\n    assert.equal(file.type, 'text/plain')\n    assert.equal(await file.text(), 'This is an example file.')\n  })\n\n  it('throws MaxFilesExceededError when the number of files exceeds the limit', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',\n      },\n      body: [\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file1\"; filename=\"example1.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is the first example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file2\"; filename=\"example2.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is the second example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file3\"; filename=\"example3.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is the third example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW--',\n      ].join('\\r\\n'),\n    })\n\n    await assert.rejects(\n      async () => await parseFormData(request, { maxFiles: 2 }),\n      MaxFilesExceededError,\n    )\n  })\n\n  it('throws when the number of multipart parts exceeds maxParts', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',\n      },\n      body: [\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"field1\"',\n        '',\n        'value1',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file1\"; filename=\"example1.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'This is the first example file.',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"field2\"',\n        '',\n        'value2',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW--',\n      ].join('\\r\\n'),\n    })\n\n    await assert.rejects(\n      async () => await parseFormData(request, { maxParts: 2 }),\n      MaxPartsExceededError,\n    )\n  })\n\n  it('throws when aggregate multipart content size exceeds maxTotalSize', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',\n      },\n      body: [\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"field1\"',\n        '',\n        'hello',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW',\n        'Content-Disposition: form-data; name=\"file1\"; filename=\"example1.txt\"',\n        'Content-Type: text/plain',\n        '',\n        'world',\n        '------WebKitFormBoundary7MA4YWxkTrZu0gW--',\n      ].join('\\r\\n'),\n    })\n\n    await assert.rejects(\n      async () => await parseFormData(request, { maxTotalSize: 9 }),\n      MaxTotalSizeExceededError,\n    )\n  })\n\n  it('throws when the request does not contain parseable content', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'text/plain',\n      },\n      body: 'Hello, World!',\n    })\n\n    await assert.rejects(async () => {\n      await parseFormData(request)\n    }, FormDataParseError)\n  })\n\n  it('throws when the request contains malformed multipart/form-data', async () => {\n    let boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: 'invalid',\n    })\n\n    await assert.rejects(async () => {\n      await parseFormData(request)\n    }, FormDataParseError)\n  })\n\n  it('parses a multipart file without a media type', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data; boundary=----BOUNDARY',\n      },\n      body: [\n        '------BOUNDARY',\n        'Content-Disposition: form-data; name=\"file\"; filename=\"example.txt\"',\n        '',\n        'This is an example file.',\n        '------BOUNDARY--',\n      ].join('\\r\\n'),\n    })\n\n    let formData = await parseFormData(request)\n    let file = formData.get('file')\n    assert.ok(file instanceof File)\n    assert.equal(file.name, 'example.txt')\n    assert.equal(file.type, 'application/octet-stream')\n    assert.equal(await file.text(), 'This is an example file.')\n  })\n})\n"
  },
  {
    "path": "packages/form-data-parser/src/lib/form-data.ts",
    "content": "import {\n  type MultipartParserOptions,\n  type MultipartPart,\n  MaxFileSizeExceededError,\n  MaxHeaderSizeExceededError,\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n  isMultipartRequest,\n  parseMultipartRequest,\n} from '@remix-run/multipart-parser'\n\n/**\n * The base class for errors thrown by the form data parser.\n */\nexport class FormDataParseError extends Error {\n  constructor(message: string, options?: ErrorOptions) {\n    super(message, options)\n    this.name = 'FormDataParseError'\n  }\n}\n\n/**\n * An error thrown when the maximum number of files allowed in a request is exceeded.\n */\nexport class MaxFilesExceededError extends FormDataParseError {\n  constructor(maxFiles: number) {\n    super(`Maximum number of files exceeded: ${maxFiles}`)\n    this.name = 'MaxFilesExceededError'\n  }\n}\n\n/**\n * A file that was uploaded as part of a `multipart/form-data` request.\n */\nexport class FileUpload extends File {\n  /**\n   * The name of the `<input>` field used to upload the file.\n   */\n  readonly fieldName: string\n\n  constructor(part: MultipartPart, fieldName: string) {\n    super(part.content as BlobPart[], part.filename ?? 'file-upload', {\n      type: part.mediaType ?? 'application/octet-stream',\n    })\n\n    this.fieldName = fieldName\n  }\n}\n\n/**\n * A function used for handling file uploads.\n *\n * @param file The uploaded file\n * @returns A value to store in `FormData`, or `void`/`null` to skip\n */\nexport interface FileUploadHandler {\n  /**\n   * Transforms an uploaded file into the value stored in the parsed {@link FormData}.\n   */\n  (file: FileUpload): void | null | string | Blob | Promise<void | null | string | Blob>\n}\n\nfunction defaultFileUploadHandler(file: FileUpload): File {\n  // By default just keep the file around in memory.\n  return file\n}\n\nconst oneKb = 1024\nconst oneMb = oneKb * oneKb\nconst defaultMaxFiles = 20\nconst defaultMaxFileSize = 2 * oneMb\nconst defaultMaxParts = 1000\n\nfunction isMultipartLimitError(error: unknown): boolean {\n  return (\n    error instanceof MaxHeaderSizeExceededError ||\n    error instanceof MaxFileSizeExceededError ||\n    error instanceof MaxPartsExceededError ||\n    error instanceof MaxTotalSizeExceededError\n  )\n}\n\n/**\n * Options for parsing form data.\n */\nexport interface ParseFormDataOptions extends MultipartParserOptions {\n  /**\n   * The maximum number of files that can be uploaded in a single request. If this limit is\n   * exceeded, a `MaxFilesExceededError` will be thrown.\n   *\n   * @default 20\n   */\n  maxFiles?: number\n}\n\n/**\n * Parses a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) body into a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)\n * object. This is useful when accessing the data contained in a HTTP `multipart/form-data` request\n * generated by a HTML `<form>` element.\n *\n * This is a drop-in replacement for [the built-in `request.formData()` API](https://developer.mozilla.org/en-US/docs/Web/API/Request/formData)\n * with the main difference being the ability to customize the handling of file uploads. Instead of\n * keeping all files in memory, the `uploadHandler` allows you to store the file on disk or a\n * cloud storage service.\n *\n * @param request The `Request` object to parse\n * @param uploadHandler A function that handles file uploads. It receives a `File` object and may return any value that is valid in a `FormData` object\n * @returns A `Promise` that resolves to a `FormData` object containing the parsed data\n */\nexport async function parseFormData(\n  request: Request,\n  uploadHandler?: FileUploadHandler,\n): Promise<FormData>\n/**\n * @param request The `Request` object to parse\n * @param options Options for the parser\n * @param uploadHandler A function that handles file uploads. It receives a `File` object and may return any value that is valid in a `FormData` object\n */\nexport async function parseFormData(\n  request: Request,\n  options?: ParseFormDataOptions,\n  uploadHandler?: FileUploadHandler,\n): Promise<FormData>\nexport async function parseFormData(\n  request: Request,\n  optionsOrUploadHandler?: ParseFormDataOptions | FileUploadHandler,\n  uploadHandler?: FileUploadHandler,\n): Promise<FormData> {\n  if (typeof optionsOrUploadHandler === 'function') {\n    uploadHandler = optionsOrUploadHandler\n    optionsOrUploadHandler = {}\n  } else if (optionsOrUploadHandler == null) {\n    optionsOrUploadHandler = {}\n  }\n  if (uploadHandler == null) {\n    uploadHandler = defaultFileUploadHandler\n  }\n\n  if (!isMultipartRequest(request)) {\n    try {\n      return await request.formData()\n    } catch (error) {\n      throw new FormDataParseError('Cannot parse form data', { cause: error })\n    }\n  }\n\n  let {\n    maxFiles = defaultMaxFiles,\n    maxHeaderSize,\n    maxFileSize = defaultMaxFileSize,\n    maxParts = defaultMaxParts,\n    maxTotalSize = maxFiles * maxFileSize + oneMb,\n  } = optionsOrUploadHandler\n\n  let parserOptions: MultipartParserOptions = {\n    maxHeaderSize,\n    maxFileSize,\n    maxParts,\n    maxTotalSize,\n  }\n\n  let formData = new FormData()\n  let fileCount = 0\n\n  try {\n    for await (let part of parseMultipartRequest(request, parserOptions)) {\n      let fieldName = part.name\n      if (!fieldName) continue\n\n      if (part.isFile) {\n        if (++fileCount > maxFiles) {\n          throw new MaxFilesExceededError(maxFiles)\n        }\n\n        let value = await uploadHandler(new FileUpload(part, fieldName))\n        if (value != null) {\n          formData.append(fieldName, value)\n        }\n      } else {\n        formData.append(fieldName, part.text)\n      }\n    }\n  } catch (error) {\n    if (error instanceof FormDataParseError || isMultipartLimitError(error)) {\n      throw error\n    }\n\n    throw new FormDataParseError('Cannot parse form data', { cause: error })\n  }\n\n  return formData\n}\n"
  },
  {
    "path": "packages/form-data-parser/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/form-data-parser/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"demos\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/fs/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/fs/CHANGELOG.md",
    "content": "# `fs` CHANGELOG\n\nThis is the changelog for [`fs`](https://github.com/remix-run/remix/tree/main/packages/fs). It follows [semantic versioning](https://semver.org/).\n\n## v0.4.2\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`lazy-file@5.0.2`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.2)\n  - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0)\n\n## v0.4.1\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.4.0\n\n### Minor Changes\n\n- BREAKING CHANGE: Renamed `openFile()` to `openLazyFile()`, removed `getFile()`\n\n  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.\n\n  **Migration:**\n\n  ```ts\n  import { openLazyFile } from '@remix-run/fs'\n\n  let lazyFile = openLazyFile('./document.pdf')\n\n  // Streaming\n  let response = new Response(lazyFile.stream())\n\n  // For non-streaming APIs that require a complete File (e.g. FormData)\n  formData.append('file', await lazyFile.toFile())\n  ```\n\n  **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.\n\n## v0.3.0 (2025-11-26)\n\n- Move `@remix-run/lazy-file` and `@remix-run/mime` to `peerDependencies`\n\n## v0.2.0 (2025-11-25)\n\n- Replaced `mrmime` dependency with `@remix-run/mime` for MIME type detection\n\n## v0.1.0 (2025-11-20)\n\nInitial release with filesystem utilities extracted from `@remix-run/lazy-file/fs`.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/fs/README.md) for more details.\n"
  },
  {
    "path": "packages/fs/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/fs/README.md",
    "content": "# fs\n\nLazy, 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.\n\n## Features\n\n- **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.\n- **Seamless Node.js Compat** - Works seamlessly with Node.js file descriptors and handles\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n### Opening Lazy Files\n\n```ts\nimport { openLazyFile } from 'remix/fs'\n\n// Open a file from the filesystem\nlet lazyFile = openLazyFile('./path/to/file.json')\n\n// The file is lazy - no data is read until you call lazyFile.text(), lazyFile.bytes(), etc.\nlet json = JSON.parse(await lazyFile.text())\n\n// You can override file metadata\nlet customLazyFile = openLazyFile('./image.jpg', {\n  name: 'custom-name.jpg',\n  type: 'image/jpeg',\n  lastModified: Date.now(),\n})\n```\n\n### Writing Files\n\n```ts\nimport { openLazyFile, writeFile } from 'remix/fs'\n\n// Read a file and write it elsewhere\nlet lazyFile = openLazyFile('./source.txt')\nawait writeFile('./destination.txt', lazyFile)\n\n// Write to an open file handle\nimport * as fsp from 'node:fs/promises'\nlet handle = await fsp.open('./destination.txt', 'w')\nawait writeFile(handle, lazyFile)\nawait handle.close()\n```\n\n## Related Packages\n\n- [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) - Lazy, streaming `Blob`/`File` implementation\n- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) - Storage abstraction for files\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/fs/package.json",
    "content": "{\n  \"name\": \"@remix-run/fs\",\n  \"version\": \"0.4.2\",\n  \"description\": \"Filesystem utilities using the Web File API\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/fs\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/fs#readme\",\n  \"license\": \"MIT\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"dependencies\": {\n    \"@remix-run/lazy-file\": \"workspace:^\",\n    \"@remix-run/mime\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"file\",\n    \"filesystem\",\n    \"fs\"\n  ]\n}\n"
  },
  {
    "path": "packages/fs/src/index.ts",
    "content": "export type { OpenLazyFileOptions } from './lib/fs.ts'\nexport { openLazyFile, writeFile } from './lib/fs.ts'\n"
  },
  {
    "path": "packages/fs/src/lib/fs.test.ts",
    "content": "import * as assert from 'node:assert'\nimport * as fs from 'node:fs'\nimport * as fsp from 'node:fs/promises'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\nimport { beforeEach, afterEach, describe, it } from 'node:test'\n\nimport { openLazyFile, writeFile } from './fs.ts'\n\ndescribe('openLazyFile', () => {\n  let tmpDir: string\n\n  beforeEach(() => {\n    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fs-test-'))\n  })\n\n  afterEach(() => {\n    if (tmpDir) {\n      fs.rmSync(tmpDir, { recursive: true, force: true })\n    }\n  })\n\n  function createTestFile(filename: string, content: string = 'test content'): string {\n    let filePath = path.join(tmpDir, filename)\n    let dir = path.dirname(filePath)\n    if (!fs.existsSync(dir)) {\n      fs.mkdirSync(dir, { recursive: true })\n    }\n    fs.writeFileSync(filePath, content)\n    return filePath\n  }\n\n  it('opens a file and reads content', async () => {\n    let filePath = createTestFile('test.txt', 'hello world')\n\n    let lazyFile = openLazyFile(filePath)\n\n    assert.equal(lazyFile.name, filePath)\n    assert.equal(lazyFile.size, 11)\n    assert.equal(await lazyFile.text(), 'hello world')\n  })\n\n  it('sets MIME type based on file extension', () => {\n    let htmlPath = createTestFile('test.html', '<html></html>')\n    let jsonPath = createTestFile('test.json', '{}')\n    let txtPath = createTestFile('test.txt', 'text')\n\n    assert.equal(openLazyFile(htmlPath).type, 'text/html')\n    assert.equal(openLazyFile(jsonPath).type, 'application/json')\n    assert.equal(openLazyFile(txtPath).type, 'text/plain')\n  })\n\n  it('sets lastModified from file stats', () => {\n    let filePath = createTestFile('test.txt', 'content')\n    let stats = fs.statSync(filePath)\n\n    let lazyFile = openLazyFile(filePath)\n\n    assert.equal(lazyFile.lastModified, stats.mtimeMs)\n  })\n\n  it('overrides file name with options.name', () => {\n    let filePath = createTestFile('test.txt', 'content')\n\n    let lazyFile = openLazyFile(filePath, { name: 'custom.txt' })\n\n    assert.equal(lazyFile.name, 'custom.txt')\n  })\n\n  it('overrides MIME type with options.type', () => {\n    let filePath = createTestFile('test.txt', 'content')\n\n    let lazyFile = openLazyFile(filePath, { type: 'application/custom' })\n\n    assert.equal(lazyFile.type, 'application/custom')\n  })\n\n  it('overrides lastModified with options.lastModified', () => {\n    let filePath = createTestFile('test.txt', 'content')\n    let customTime = Date.now() - 1000000\n\n    let lazyFile = openLazyFile(filePath, { lastModified: customTime })\n\n    assert.equal(lazyFile.lastModified, customTime)\n  })\n\n  it('reads file as ArrayBuffer', async () => {\n    let filePath = createTestFile('test.txt', 'hello')\n\n    let lazyFile = openLazyFile(filePath)\n    let buffer = await lazyFile.arrayBuffer()\n\n    assert.equal(buffer.byteLength, 5)\n    assert.equal(new TextDecoder().decode(buffer), 'hello')\n  })\n\n  it('streams file content', async () => {\n    let filePath = createTestFile('test.txt', 'streaming content')\n\n    let lazyFile = openLazyFile(filePath)\n    let chunks: Uint8Array[] = []\n\n    for await (let chunk of lazyFile.stream()) {\n      chunks.push(chunk)\n    }\n\n    let combined = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0))\n    let offset = 0\n    for (let chunk of chunks) {\n      combined.set(chunk, offset)\n      offset += chunk.length\n    }\n\n    assert.equal(new TextDecoder().decode(combined), 'streaming content')\n  })\n\n  it('handles empty files', async () => {\n    let filePath = createTestFile('empty.txt', '')\n\n    let lazyFile = openLazyFile(filePath)\n\n    assert.equal(lazyFile.size, 0)\n    assert.equal(await lazyFile.text(), '')\n  })\n\n  it('handles large files', async () => {\n    let largeContent = 'x'.repeat(10000)\n    let filePath = createTestFile('large.txt', largeContent)\n\n    let lazyFile = openLazyFile(filePath)\n\n    assert.equal(lazyFile.size, 10000)\n    assert.equal(await lazyFile.text(), largeContent)\n  })\n\n  it('throws error for non-existent files', () => {\n    let nonExistentPath = path.join(tmpDir, 'nonexistent.txt')\n\n    assert.throws(\n      () => openLazyFile(nonExistentPath),\n      (error: Error) => error.message.includes('ENOENT'),\n    )\n  })\n\n  it('throws error when opening a directory', () => {\n    let dirPath = path.join(tmpDir, 'testdir')\n    fs.mkdirSync(dirPath)\n\n    assert.throws(\n      () => openLazyFile(dirPath),\n      (error: Error) => error.message.includes('is not a file'),\n    )\n  })\n})\n\ndescribe('writeFile', () => {\n  let tmpDir: string\n\n  beforeEach(() => {\n    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fs-test-'))\n  })\n\n  afterEach(() => {\n    if (tmpDir) {\n      fs.rmSync(tmpDir, { recursive: true, force: true })\n    }\n  })\n\n  it('writes file to a path', async () => {\n    let sourcePath = path.join(tmpDir, 'source.txt')\n    let destPath = path.join(tmpDir, 'dest.txt')\n    fs.writeFileSync(sourcePath, 'test content')\n\n    let lazyFile = openLazyFile(sourcePath)\n    await writeFile(destPath, lazyFile)\n\n    assert.equal(fs.readFileSync(destPath, 'utf-8'), 'test content')\n  })\n\n  it('writes file using a file descriptor', async () => {\n    let sourcePath = path.join(tmpDir, 'source.txt')\n    let destPath = path.join(tmpDir, 'dest.txt')\n    fs.writeFileSync(sourcePath, 'test content')\n\n    let lazyFile = openLazyFile(sourcePath)\n    let fd = fs.openSync(destPath, 'w')\n\n    await writeFile(fd, lazyFile)\n    // Note: fd is automatically closed by the write stream\n\n    assert.equal(fs.readFileSync(destPath, 'utf-8'), 'test content')\n  })\n\n  it('writes file using a FileHandle', async () => {\n    let sourcePath = path.join(tmpDir, 'source.txt')\n    let destPath = path.join(tmpDir, 'dest.txt')\n    fs.writeFileSync(sourcePath, 'test content')\n\n    let lazyFile = openLazyFile(sourcePath)\n    let handle = await fsp.open(destPath, 'w')\n\n    await writeFile(handle, lazyFile)\n    await handle.close()\n\n    assert.equal(fs.readFileSync(destPath, 'utf-8'), 'test content')\n  })\n\n  it('writes empty files', async () => {\n    let sourcePath = path.join(tmpDir, 'empty.txt')\n    let destPath = path.join(tmpDir, 'dest.txt')\n    fs.writeFileSync(sourcePath, '')\n\n    let lazyFile = openLazyFile(sourcePath)\n    await writeFile(destPath, lazyFile)\n\n    assert.equal(fs.readFileSync(destPath, 'utf-8'), '')\n  })\n\n  it('writes large files', async () => {\n    let largeContent = 'x'.repeat(100000)\n    let sourcePath = path.join(tmpDir, 'large.txt')\n    let destPath = path.join(tmpDir, 'dest.txt')\n    fs.writeFileSync(sourcePath, largeContent)\n\n    let lazyFile = openLazyFile(sourcePath)\n    await writeFile(destPath, lazyFile)\n\n    assert.equal(fs.readFileSync(destPath, 'utf-8'), largeContent)\n  })\n\n  it('creates parent directories when writing to path', async () => {\n    let sourcePath = path.join(tmpDir, 'source.txt')\n    let destPath = path.join(tmpDir, 'nested', 'dir', 'dest.txt')\n    fs.writeFileSync(sourcePath, 'content')\n    fs.mkdirSync(path.dirname(destPath), { recursive: true })\n\n    let lazyFile = openLazyFile(sourcePath)\n    await writeFile(destPath, lazyFile)\n\n    assert.ok(fs.existsSync(destPath))\n    assert.equal(fs.readFileSync(destPath, 'utf-8'), 'content')\n  })\n\n  it('preserves file content exactly', async () => {\n    let binaryData = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd])\n    let sourcePath = path.join(tmpDir, 'binary.dat')\n    let destPath = path.join(tmpDir, 'dest.dat')\n    fs.writeFileSync(sourcePath, binaryData)\n\n    let lazyFile = openLazyFile(sourcePath)\n    await writeFile(destPath, lazyFile)\n\n    let written = fs.readFileSync(destPath)\n    assert.deepEqual(written, binaryData)\n  })\n})\n"
  },
  {
    "path": "packages/fs/src/lib/fs.ts",
    "content": "import * as fs from 'node:fs'\nimport { detectMimeType } from '@remix-run/mime'\n\nimport { type LazyContent, LazyFile } from '@remix-run/lazy-file'\n\n/**\n * Options for opening a lazy file from the local filesystem.\n */\nexport interface OpenLazyFileOptions {\n  /**\n   * Overrides the name of the file.\n   *\n   * @default the filename argument as provided\n   */\n  name?: string\n  /**\n   * Overrides the MIME type of the file.\n   *\n   * @default determined by the file extension\n   */\n  type?: string\n  /**\n   * Overrides the last modified timestamp of the file.\n   *\n   * @default the file's last modified time\n   */\n  lastModified?: number\n}\n\n/**\n * Returns a `LazyFile` from the local filesystem.\n *\n * The returned file's `name` property will be set to the `filename` argument as provided,\n * unless overridden via `options.name`.\n *\n * @param filename The path to the file\n * @param options Options to override the file's metadata\n * @returns A `LazyFile` object\n */\nexport function openLazyFile(filename: string, options?: OpenLazyFileOptions): LazyFile {\n  let stats = fs.statSync(filename)\n\n  if (!stats.isFile()) {\n    throw new Error(`Path \"${filename}\" is not a file`)\n  }\n\n  let content: LazyContent = {\n    byteLength: stats.size,\n    stream(start, end) {\n      return streamFile(filename, start, end)\n    },\n  }\n\n  return new LazyFile(content, options?.name ?? filename, {\n    type: options?.type ?? detectMimeType(filename) ?? '',\n    lastModified: options?.lastModified ?? stats.mtimeMs,\n  })\n}\n\nfunction streamFile(\n  filename: string,\n  start = 0,\n  end = Infinity,\n): ReadableStream<Uint8Array<ArrayBuffer>> {\n  let readStream = fs.createReadStream(filename, { start, end: end - 1 }).iterator()\n\n  return new ReadableStream({\n    async pull(controller) {\n      let { done, value } = await readStream.next()\n\n      if (done) {\n        controller.close()\n      } else {\n        controller.enqueue(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))\n      }\n    },\n  })\n}\n\n/**\n * Writes a file-like object to the local filesystem and resolves when the stream is finished.\n *\n * Accepts any object with a `stream()` method, including native `File`, `Blob`, and `LazyFile`.\n *\n * [MDN `File` Reference](https://developer.mozilla.org/en-US/docs/Web/API/File)\n *\n * @param to The path to write the file to, or an open file descriptor\n * @param file The file to write (any object with a `stream()` method)\n * @param file.stream Method that returns a readable stream of the file's contents\n * @returns A promise that resolves when the file is written\n */\nexport function writeFile(\n  to: string | number | fs.promises.FileHandle,\n  file: { stream(): ReadableStream<Uint8Array> },\n): Promise<void> {\n  return new Promise(async (resolve, reject) => {\n    let writeStream =\n      typeof to === 'string'\n        ? fs.createWriteStream(to)\n        : fs.createWriteStream('ignored', { fd: to })\n\n    try {\n      for await (let chunk of file.stream()) {\n        writeStream.write(chunk)\n      }\n\n      writeStream.end(() => {\n        resolve()\n      })\n    } catch (error) {\n      writeStream.end(() => {\n        reject(error)\n      })\n    }\n  })\n}\n"
  },
  {
    "path": "packages/fs/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"global.d.ts\", \"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/fs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/headers/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/headers/CHANGELOG.md",
    "content": "# `headers` CHANGELOG\n\nThis is the changelog for [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers). It follows [semantic versioning](https://semver.org/).\n\n## v0.19.0\n\n### Minor Changes\n\n- BREAKING CHANGE: Removed `Headers`/`SuperHeaders` class and default export. Use the native `Headers` class with the static `from()` method on each header class instead.\n\n  New individual header `.from()` methods:\n\n  - `Accept.from()`\n  - `AcceptEncoding.from()`\n  - `AcceptLanguage.from()`\n  - `CacheControl.from()`\n  - `ContentDisposition.from()`\n  - `ContentRange.from()`\n  - `ContentType.from()`\n  - `Cookie.from()`\n  - `IfMatch.from()`\n  - `IfNoneMatch.from()`\n  - `IfRange.from()`\n  - `Range.from()`\n  - `SetCookie.from()`\n  - `Vary.from()`\n\n  New raw header utilities added:\n\n  - `parse()`\n  - `stringify()`\n\n  Migration example:\n\n  ```ts\n  // Before:\n  import SuperHeaders from '@remix-run/headers'\n  let headers = new SuperHeaders(request.headers)\n  let mediaType = headers.contentType.mediaType\n\n  // After:\n  import { ContentType } from '@remix-run/headers'\n  let contentType = ContentType.from(request.headers.get('content-type'))\n  let mediaType = contentType.mediaType\n  ```\n\n  If you were using the `Headers` constructor to parse raw HTTP header strings, use `parse()` instead:\n\n  ```ts\n  // Before:\n  import SuperHeaders from '@remix-run/headers'\n  let headers = new SuperHeaders('Content-Type: text/html\\r\\nCache-Control: no-cache')\n\n  // After:\n  import { parse } from '@remix-run/headers'\n  let headers = parse('Content-Type: text/html\\r\\nCache-Control: no-cache')\n  ```\n\n  If you were using `headers.toString()` to convert headers to raw format, use `stringify()` instead:\n\n  ```ts\n  // Before:\n  import SuperHeaders from '@remix-run/headers'\n  let headers = new SuperHeaders()\n  headers.set('Content-Type', 'text/html')\n  let rawHeaders = headers.toString()\n\n  // After:\n  import { stringify } from '@remix-run/headers'\n  let headers = new Headers()\n  headers.set('Content-Type', 'text/html')\n  let rawHeaders = stringify(headers)\n  ```\n\n## v0.18.0 (2025-11-25)\n\n- Add `Vary` support\n\n```ts\nimport { Vary } from '@remix-run/headers'\n\nlet header = new Vary('Accept-Encoding')\nheader.add('Accept-Language')\nheader.headerNames // ['accept-encoding', 'accept-language']\nheader.toString() // 'accept-encoding, accept-language'\n```\n\n- `Accept.getPreferred()`, `AcceptEncoding.getPreferred()`, and `AcceptLanguage.getPreferred()` are now generic, preserving the union type of the input array in the return type\n\n## v0.17.2 (2025-11-25)\n\n- Fix `secure` property type in `SetCookie` to accept `boolean` instead of only `true`, making it consistent with `httpOnly` and `partitioned`\n\n## v0.17.1 (2025-11-21)\n\n- Fix bug where `Max-Age=0` did not show up in `SetCookie` header\n\n## v0.17.0 (2025-11-18)\n\n- Add `Range` support\n\n```ts\nimport { Range } from '@remix-run/headers'\n\nlet header = new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] })\nheader.toString() // \"bytes=0-999\"\n\n// Parse from string\nlet header = new Range('bytes=0-999,2000-2999')\nheader.ranges // [{ start: 0, end: 999 }, { start: 2000, end: 2999 }]\n\n// Check if range is satisfiable for a given file size\nheader.isSatisfiable(5000) // true\n\n// Normalize ranges to concrete start/end values for a given file size\nlet header = new Range('bytes=0-')\nheader.normalize(5000) // [{ start: 0, end: 4999 }]\n```\n\n- Add `Content-Range` support\n\n```ts\nimport { ContentRange } from '@remix-run/headers'\n\nlet header = new ContentRange({\n  unit: 'bytes',\n  start: 0,\n  end: 999,\n  size: 5000,\n})\nheader.toString() // \"bytes 0-999/5000\"\n\n// Parse from string\nlet header = new ContentRange('bytes 200-1000/67589')\nheader.start // 200\nheader.end // 1000\nheader.size // 67589\n```\n\n- Add `If-Match` support\n\n```ts\nimport { IfMatch } from '@remix-run/headers'\n\nlet header = new IfMatch(['\"abc123\"', '\"def456\"'])\nheader.has('\"abc123\"') // true\n\n// Check if precondition passes\nheader.matches('\"abc123\"') // true\nheader.matches('\"xyz789\"') // false\nheader.matches('W/\"abc123\"') // false (weak ETags never match)\n```\n\n- Add `If-Range` support\n\n```ts\nimport { IfRange } from '@remix-run/headers'\n\n// With ETag\nlet header = new IfRange('\"abc123\"')\nheader.matches({ etag: '\"abc123\"' }) // true\nheader.matches({ etag: 'W/\"abc123\"' }) // false (weak ETags never match)\n\n// With Last-Modified date\nlet header = new IfRange(new Date('2025-10-21T07:28:00Z'))\nheader.matches({ lastModified: new Date('2025-10-21T07:28:00Z') }) // true\n```\n\n- Add `Allow` support\n\n```ts\nimport { SuperHeaders } from '@remix-run/headers'\n\nlet headers = new SuperHeaders({ allow: ['GET', 'POST', 'OPTIONS'] })\nheaders.get('Allow') // \"GET, POST, OPTIONS\"\n```\n\n## v0.16.0 (2025-11-05)\n\n- Build using `tsc` instead of `esbuild`. This means modules in the `dist` directory now mirror the layout of modules in the `src` directory.\n\n## v0.15.0 (2025-11-04)\n\n- Add support for `httpOnly: false` in `SetCookie` constructor\n- Export `CookieProperties` type with all cookie properties\n- Add `Partitioned` support to `SetCookie`\n\n## v0.14.0 (2025-10-22)\n\n- 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()`.\n\n## v0.13.0 (2025-10-04)\n\n- Drop support for TypeScript < 5.7\n\n## v0.12.0 (2025-07-18)\n\n- Rename package from `@mjackson/headers` to `@remix-run/headers`\n\n## v0.11.1 (2025-06-06)\n\n- Do not minify builds\n- Remove some test files from the build\n\n## v0.11.0 (2025-06-06)\n\n- Add `/src` to npm package, so \"go to definition\" goes to the actual source\n- Use one set of types for all built files, instead of separate types for ESM and CJS\n- Build using esbuild directly instead of tsup\n\n## v0.10.0 (2025-01-27)\n\nThis release contains several improvements to `Cookie` that bring it more in line with other headers like `Accept`, `AcceptEncoding`, and `AcceptLanguage`.\n\n- BREAKING CHANGE: `cookie.names()` and `cookie.values()` are now getters that return `string[]` instead of methods that return `IterableIterator<string>`\n- BREAKING CHANGE: `cookie.forEach()` calls its callback with `(name, value, cookie)` instead of `(value, name, map)`\n- BREAKING CHANGE: `cookie.delete(name)` returns `void` instead of `boolean`\n\n```ts\n// before\nlet cookieNames = Array.from(headers.cookie.names())\n\n// after\nlet cookieNames = headers.cookie.names\n```\n\nAdditionally, 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.\n\n```ts\nimport { SuperHeaders } from '@remix-run/headers'\n\nfunction requestHandler(request: Request): Promise<Response> {\n  let response = await callDownstreamService(request)\n\n  if (request.method === 'GET' && response.headers.has('ETag')) {\n    let headers = new SuperHeaders(request.headers)\n    if (headers.ifNoneMatch.matches(response.headers.get('ETag'))) {\n      return new Response(null, { status: 304 })\n    }\n  }\n\n  return response\n}\n```\n\n## v0.9.0 (2024-12-20)\n\nThis release tightens up the type safety and brings `SuperHeaders` more in line with the built-in `Headers` interface.\n\n- 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.\n\n```ts\n// before\nlet headers = new SuperHeaders()\nheaders.set('Content-Type', { mediaType: 'text/html' })\n\n// after\nheaders.set('Content-Type', 'text/html')\n\n// if you need the previous behavior, use the setter instead of set()\nheaders.contentType = { mediaType: 'text/html' }\n```\n\nSimilarly, the constructor no longer accepts non-string values in an array init value.\n\n```ts\n// before\nlet headers = new SuperHeaders([['Content-Type', { mediaType: 'text/html' }]])\n\n// if you need the previous behavior, use the object init instead\nlet headers = new SuperHeaders({ contentType: { mediaType: 'text/html' } })\n```\n\n- BREAKING CHANGE: `headers.get()` returns `null` for uninitialized custom header values instead of `undefined`. This follows the native `Headers` interface more closely.\n\n```ts\n// before\nlet headers = new SuperHeaders()\nheaders.get('Host') // null\nheaders.get('Content-Type') // undefined\n\n// after\nheaders.get('Host') // null\nheaders.get('Content-Type') // null\n```\n\n- BREAKING CHANGE: Removed ability to initialize `AcceptLanguage` with `undefined` weight values.\n\n```ts\n// before\nlet h1 = new AcceptLanguage({ 'en-US': undefined })\nlet h2 = new AcceptLanguage([['en-US', undefined]])\n\n// after\nlet h3 = new AcceptLanguage({ 'en-US': 1 })\n```\n\n- 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()`.\n\n```ts\nlet headers = new SuperHeaders({ contentType: 'text/html' })\nheaders.get('Content-Type') // 'text/html'\n\nheaders.contentType = null // same as headers.delete('Content-Type');\nheaders.get('Content-Type') // null\n```\n\n- Allow setting date headers (`date`, `expires`, `ifModifiedSince`, `ifUnmodifiedSince`, and `lastModified`) using numbers.\n\n```ts\nlet ms = new Date().getTime()\nlet headers = new SuperHeaders({ lastModified: ms })\nheaders.date = ms\n```\n\n- Added `AcceptLanguage.prototype.accepts(language)`, `AcceptLanguage.prototype.getWeight(language)`,\n  `AcceptLanguage.prototype.getPreferred(languages)`\n\n```ts\nimport { AcceptLanguage } from '@remix-run/headers'\n\nlet header = new AcceptLanguage({ 'en-US': 1, en: 0.9 })\n\nheader.accepts('en-US') // true\nheader.accepts('en-GB') // true\nheader.accepts('en') // true\nheader.accepts('fr') // false\n\nheader.getWeight('en-US') // 1\nheader.getWeight('en-GB') // 0.9\n\nheader.getPreferred(['en-GB', 'en-US']) // 'en-US'\n```\n\n- Added `Accept` support\n\n```ts\nimport { Accept } from '@remix-run/headers'\n\nlet header = new Accept({ 'text/html': 1, 'text/*': 0.9 })\n\nheader.accepts('text/html') // true\nheader.accepts('text/plain') // true\nheader.accepts('text/*') // true\nheader.accepts('image/jpeg') // false\n\nheader.getWeight('text/html') // 1\nheader.getWeight('text/plain') // 0.9\n\nheader.getPreferred(['text/html', 'text/plain']) // 'text/html'\n```\n\n- Added `Accept-Encoding` support\n\n```ts\nimport { AcceptEncoding } from '@remix-run/headers'\n\nlet header = new AcceptEncoding({ gzip: 1, deflate: 0.9 })\n\nheader.accepts('gzip') // true\nheader.accepts('deflate') // true\nheader.accepts('identity') // true\nheader.accepts('br') // false\n\nheader.getWeight('gzip') // 1\nheader.getWeight('deflate') // 0.9\n\nheader.getPreferred(['gzip', 'deflate']) // 'gzip'\n```\n\n- Added `SuperHeaders.prototype` (getters and setters) for:\n  - `accept`\n  - `acceptEncoding`\n  - `acceptRanges`\n  - `connection`\n  - `contentEncoding`\n  - `contentLanguage`\n  - `etag`\n  - `host`\n  - `location`\n  - `referer`\n\n## v0.8.0 (2024-11-14)\n\n- Added CommonJS build\n\n## 0.7.2 (2024-08-29)\n\n- Treat `Headers` as iterable in the constructor\n\n## v0.7.1 (2024-08-28)\n\n- Added `string` init type to `new Headers({ acceptLanguage })`\n\n## v0.7.0 (2024-08-27)\n\n- Added support for the `Accept-Language` header (https://github.com/remix-run/remix/pull/8, thanks [@ArnoSaine](https://github.com/ArnoSaine))\n\n## v0.6.1 (2024-08-13)\n\n- Associate `CacheControl` doc comments with the class instead of the constructor function\n\n## v0.6.0 (2024-08-13)\n\n- Added support for `Cache-Control` header (https://github.com/mjackson/headers/pull/7, thanks [@alexanderson1993](https://github.com/alexanderson1993))\n\n## v0.5.1 (2024-08-6)\n\n- Added `CookieInit` support to `headers.cookie=` setter\n\n## v0.5.0 (2024-08-6)\n\n- Added the ability to initialize a `SuperHeaders` instance with object config instead of just strings or header object instances.\n\n```ts\nlet headers = new Headers({\n  contentType: { mediaType: 'text/html' },\n  cookies: [\n    ['session_id', 'abc'],\n    ['theme', 'dark'],\n  ],\n})\n```\n\n- Changed package name from `fetch-super-headers` to `@remix-run/headers`. Eventual goal is to get the `headers` npm package name.\n"
  },
  {
    "path": "packages/headers/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/headers/README.md",
    "content": "# headers\n\nTyped utilities for parsing, manipulating, and serializing HTTP header values. `headers` provides focused classes for common HTTP headers.\n\n## Features\n\n- **Header-Specific Classes** - Purpose-built APIs for `Accept`, `Cache-Control`, `Content-Type`, and more\n- **Round-Trip Safety** - Parse from raw values and serialize back with `.toString()`\n- **Typed Operations** - Work with structured values instead of manual string parsing\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Individual Header Utilities\n\nEach 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.\n\nThe following headers are currently supported:\n\n- [Accept](./README.md#accept)\n- [Accept-Encoding](./README.md#accept-encoding)\n- [Accept-Language](./README.md#accept-language)\n- [Cache-Control](./README.md#cache-control)\n- [Content-Disposition](./README.md#content-disposition)\n- [Content-Range](./README.md#content-range)\n- [Content-Type](./README.md#content-type)\n- [Cookie](./README.md#cookie)\n- [If-Match](./README.md#if-match)\n- [If-None-Match](./README.md#if-none-match)\n- [If-Range](./README.md#if-range)\n- [Range](./README.md#range)\n- [Set-Cookie](./README.md#set-cookie)\n- [Vary](./README.md#vary)\n\n### Accept\n\nParse, manipulate and stringify [`Accept` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept).\n\nImplements `Map<mediaType, quality>`.\n\n```ts\nimport { Accept } from 'remix/headers'\n\n// Parse from headers\nlet accept = Accept.from(request.headers.get('accept'))\n\naccept.mediaTypes // ['text/html', 'text/*']\naccept.weights // [1, 0.9]\naccept.accepts('text/html') // true\naccept.accepts('text/plain') // true (matches text/*)\naccept.accepts('image/jpeg') // false\naccept.getWeight('text/plain') // 1 (matches text/*)\naccept.getPreferred(['text/html', 'text/plain']) // 'text/html'\n\n// Iterate\nfor (let [mediaType, quality] of accept) {\n  // ...\n}\n\n// Modify and set header\naccept.set('application/json', 0.8)\naccept.delete('text/*')\nheaders.set('Accept', accept)\n\n// Construct directly\nnew Accept('text/html, text/*;q=0.9')\nnew Accept({ 'text/html': 1, 'text/*': 0.9 })\nnew Accept(['text/html', ['text/*', 0.9]])\n\n// Use class for type safety when setting Headers values\n// via Accept's `.toString()` method\nlet headers = new Headers({\n  Accept: new Accept({ 'text/html': 1, 'application/json': 0.8 }),\n})\nheaders.set('Accept', new Accept({ 'text/html': 1, 'application/json': 0.8 }))\n```\n\n### Accept-Encoding\n\nParse, manipulate and stringify [`Accept-Encoding` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding).\n\nImplements `Map<encoding, quality>`.\n\n```ts\nimport { AcceptEncoding } from 'remix/headers'\n\n// Parse from headers\nlet acceptEncoding = AcceptEncoding.from(request.headers.get('accept-encoding'))\n\nacceptEncoding.encodings // ['gzip', 'deflate']\nacceptEncoding.weights // [1, 0.8]\nacceptEncoding.accepts('gzip') // true\nacceptEncoding.accepts('br') // false\nacceptEncoding.getWeight('gzip') // 1\nacceptEncoding.getPreferred(['gzip', 'deflate', 'br']) // 'gzip'\n\n// Modify and set header\nacceptEncoding.set('br', 1)\nacceptEncoding.delete('deflate')\nheaders.set('Accept-Encoding', acceptEncoding)\n\n// Construct directly\nnew AcceptEncoding('gzip, deflate;q=0.8')\nnew AcceptEncoding({ gzip: 1, deflate: 0.8 })\n\n// Use class for type safety when setting Headers values\n// via AcceptEncoding's `.toString()` method\nlet headers = new Headers({\n  'Accept-Encoding': new AcceptEncoding({ gzip: 1, br: 0.9 }),\n})\nheaders.set('Accept-Encoding', new AcceptEncoding({ gzip: 1, br: 0.9 }))\n```\n\n### Accept-Language\n\nParse, manipulate and stringify [`Accept-Language` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language).\n\nImplements `Map<language, quality>`.\n\n```ts\nimport { AcceptLanguage } from 'remix/headers'\n\n// Parse from headers\nlet acceptLanguage = AcceptLanguage.from(request.headers.get('accept-language'))\n\nacceptLanguage.languages // ['en-us', 'en']\nacceptLanguage.weights // [1, 0.9]\nacceptLanguage.accepts('en-US') // true\nacceptLanguage.accepts('en-GB') // true (matches en)\nacceptLanguage.getWeight('en-GB') // 1 (matches en)\nacceptLanguage.getPreferred(['en-US', 'en-GB', 'fr']) // 'en-US'\n\n// Modify and set header\nacceptLanguage.set('fr', 0.5)\nacceptLanguage.delete('en')\nheaders.set('Accept-Language', acceptLanguage)\n\n// Construct directly\nnew AcceptLanguage('en-US, en;q=0.9')\nnew AcceptLanguage({ 'en-US': 1, en: 0.9 })\n\n// Use class for type safety when setting Headers values\n// via AcceptLanguage's `.toString()` method\nlet headers = new Headers({\n  'Accept-Language': new AcceptLanguage({ 'en-US': 1, fr: 0.5 }),\n})\nheaders.set('Accept-Language', new AcceptLanguage({ 'en-US': 1, fr: 0.5 }))\n```\n\n### Cache-Control\n\nParse, manipulate and stringify [`Cache-Control` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).\n\n```ts\nimport { CacheControl } from 'remix/headers'\n\n// Parse from headers\nlet cacheControl = CacheControl.from(response.headers.get('cache-control'))\n\ncacheControl.public // true\ncacheControl.maxAge // 3600\ncacheControl.sMaxage // 7200\ncacheControl.noCache // undefined\ncacheControl.noStore // undefined\ncacheControl.noTransform // undefined\ncacheControl.mustRevalidate // undefined\ncacheControl.immutable // undefined\n\n// Modify and set header\ncacheControl.maxAge = 7200\ncacheControl.immutable = true\nheaders.set('Cache-Control', cacheControl)\n\n// Construct directly\nnew CacheControl('public, max-age=3600')\nnew CacheControl({ public: true, maxAge: 3600 })\n\n// Use class for type safety when setting Headers values\n// via CacheControl's `.toString()` method\nlet headers = new Headers({\n  'Cache-Control': new CacheControl({ public: true, maxAge: 3600 }),\n})\nheaders.set('Cache-Control', new CacheControl({ public: true, maxAge: 3600 }))\n```\n\n### Content-Disposition\n\nParse, manipulate and stringify [`Content-Disposition` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition).\n\n```ts\nimport { ContentDisposition } from 'remix/headers'\n\n// Parse from headers\nlet contentDisposition = ContentDisposition.from(response.headers.get('content-disposition'))\n\ncontentDisposition.type // 'attachment'\ncontentDisposition.filename // 'example.pdf'\ncontentDisposition.filenameSplat // \"UTF-8''%E4%BE%8B%E5%AD%90.pdf\"\ncontentDisposition.preferredFilename // '例子.pdf' (decoded from filename*)\n\n// Modify and set header\ncontentDisposition.filename = 'download.pdf'\nheaders.set('Content-Disposition', contentDisposition)\n\n// Construct directly\nnew ContentDisposition('attachment; filename=\"example.pdf\"')\nnew ContentDisposition({ type: 'attachment', filename: 'example.pdf' })\n\n// Use class for type safety when setting Headers values\n// via ContentDisposition's `.toString()` method\nlet headers = new Headers({\n  'Content-Disposition': new ContentDisposition({ type: 'attachment', filename: 'example.pdf' }),\n})\nheaders.set(\n  'Content-Disposition',\n  new ContentDisposition({ type: 'attachment', filename: 'example.pdf' }),\n)\n```\n\n### Content-Range\n\nParse, manipulate and stringify [`Content-Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range).\n\n```ts\nimport { ContentRange } from 'remix/headers'\n\n// Parse from headers\nlet contentRange = ContentRange.from(response.headers.get('content-range'))\n\ncontentRange.unit // \"bytes\"\ncontentRange.start // 200\ncontentRange.end // 1000\ncontentRange.size // 67589\n\n// Unsatisfied range\nlet unsatisfied = ContentRange.from('bytes */67589')\nunsatisfied.start // null\nunsatisfied.end // null\nunsatisfied.size // 67589\n\n// Construct directly\nnew ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 })\n\n// Use class for type safety when setting Headers values\n// via ContentRange's `.toString()` method\nlet headers = new Headers({\n  'Content-Range': new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 }),\n})\nheaders.set('Content-Range', new ContentRange({ unit: 'bytes', start: 0, end: 499, size: 1000 }))\n```\n\n### Content-Type\n\nParse, manipulate and stringify [`Content-Type` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type).\n\n```ts\nimport { ContentType } from 'remix/headers'\n\n// Parse from headers\nlet contentType = ContentType.from(request.headers.get('content-type'))\n\ncontentType.mediaType // \"text/html\"\ncontentType.charset // \"utf-8\"\ncontentType.boundary // undefined (or boundary string for multipart)\n\n// Modify and set header\ncontentType.charset = 'iso-8859-1'\nheaders.set('Content-Type', contentType)\n\n// Construct directly\nnew ContentType('text/html; charset=utf-8')\nnew ContentType({ mediaType: 'text/html', charset: 'utf-8' })\n\n// Use class for type safety when setting Headers values\n// via ContentType's `.toString()` method\nlet headers = new Headers({\n  'Content-Type': new ContentType({ mediaType: 'text/html', charset: 'utf-8' }),\n})\nheaders.set('Content-Type', new ContentType({ mediaType: 'text/html', charset: 'utf-8' }))\n```\n\n### Cookie\n\nParse, manipulate and stringify [`Cookie` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie).\n\nImplements `Map<name, value>`.\n\n```ts\nimport { Cookie } from 'remix/headers'\n\n// Parse from headers\nlet cookie = Cookie.from(request.headers.get('cookie'))\n\ncookie.get('session_id') // 'abc123'\ncookie.get('theme') // 'dark'\ncookie.has('session_id') // true\ncookie.size // 2\n\n// Iterate\nfor (let [name, value] of cookie) {\n  // ...\n}\n\n// Modify and set header\ncookie.set('theme', 'light')\ncookie.delete('session_id')\nheaders.set('Cookie', cookie)\n\n// Construct directly\nnew Cookie('session_id=abc123; theme=dark')\nnew Cookie({ session_id: 'abc123', theme: 'dark' })\nnew Cookie([\n  ['session_id', 'abc123'],\n  ['theme', 'dark'],\n])\n\n// Use class for type safety when setting Headers values\n// via Cookie's `.toString()` method\nlet headers = new Headers({\n  Cookie: new Cookie({ session_id: 'abc123', theme: 'dark' }),\n})\nheaders.set('Cookie', new Cookie({ session_id: 'abc123', theme: 'dark' }))\n```\n\n### If-Match\n\nParse, manipulate and stringify [`If-Match` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match).\n\nImplements `Set<etag>`.\n\n```ts\nimport { IfMatch } from 'remix/headers'\n\n// Parse from headers\nlet ifMatch = IfMatch.from(request.headers.get('if-match'))\n\nifMatch.tags // ['\"67ab43\"', '\"54ed21\"']\nifMatch.has('\"67ab43\"') // true\nifMatch.matches('\"67ab43\"') // true (checks precondition)\nifMatch.matches('\"abc123\"') // false\n\n// Note: Uses strong comparison only (weak ETags never match)\nlet weak = IfMatch.from('W/\"67ab43\"')\nweak.matches('W/\"67ab43\"') // false\n\n// Modify and set header\nifMatch.add('\"newetag\"')\nifMatch.delete('\"67ab43\"')\nheaders.set('If-Match', ifMatch)\n\n// Construct directly\nnew IfMatch(['abc123', 'def456'])\n\n// Use class for type safety when setting Headers values\n// via IfMatch's `.toString()` method\nlet headers = new Headers({\n  'If-Match': new IfMatch(['\"abc123\"', '\"def456\"']),\n})\nheaders.set('If-Match', new IfMatch(['\"abc123\"', '\"def456\"']))\n```\n\n### If-None-Match\n\nParse, manipulate and stringify [`If-None-Match` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match).\n\nImplements `Set<etag>`.\n\n```ts\nimport { IfNoneMatch } from 'remix/headers'\n\n// Parse from headers\nlet ifNoneMatch = IfNoneMatch.from(request.headers.get('if-none-match'))\n\nifNoneMatch.tags // ['\"67ab43\"', '\"54ed21\"']\nifNoneMatch.has('\"67ab43\"') // true\nifNoneMatch.matches('\"67ab43\"') // true\n\n// Supports weak comparison (unlike If-Match)\nlet weak = IfNoneMatch.from('W/\"67ab43\"')\nweak.matches('W/\"67ab43\"') // true\n\n// Modify and set header\nifNoneMatch.add('\"newetag\"')\nifNoneMatch.delete('\"67ab43\"')\nheaders.set('If-None-Match', ifNoneMatch)\n\n// Construct directly\nnew IfNoneMatch(['abc123'])\n\n// Use class for type safety when setting Headers values\n// via IfNoneMatch's `.toString()` method\nlet headers = new Headers({\n  'If-None-Match': new IfNoneMatch(['\"abc123\"']),\n})\nheaders.set('If-None-Match', new IfNoneMatch(['\"abc123\"']))\n```\n\n### If-Range\n\nParse, manipulate and stringify [`If-Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range).\n\n```ts\nimport { IfRange } from 'remix/headers'\n\n// Parse from headers\nlet ifRange = IfRange.from(request.headers.get('if-range'))\n\n// With HTTP date\nifRange.matches({ lastModified: 1609459200000 }) // true\nifRange.matches({ lastModified: new Date('2021-01-01') }) // true\n\n// With ETag\nlet etagHeader = IfRange.from('\"67ab43\"')\netagHeader.matches({ etag: '\"67ab43\"' }) // true\n\n// Empty/null returns empty instance (range proceeds unconditionally)\nlet empty = IfRange.from(null)\nempty.matches({ etag: '\"any\"' }) // true\n\n// Construct directly\nnew IfRange('\"abc123\"')\n\n// Use class for type safety when setting Headers values\n// via IfRange's `.toString()` method\nlet headers = new Headers({\n  'If-Range': new IfRange('\"abc123\"'),\n})\nheaders.set('If-Range', new IfRange('\"abc123\"'))\n```\n\n### Range\n\nParse, manipulate and stringify [`Range` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range).\n\n```ts\nimport { Range } from 'remix/headers'\n\n// Parse from headers\nlet range = Range.from(request.headers.get('range'))\n\nrange.unit // \"bytes\"\nrange.ranges // [{ start: 200, end: 1000 }]\nrange.canSatisfy(2000) // true\nrange.canSatisfy(500) // false\nrange.normalize(2000) // [{ start: 200, end: 1000 }]\n\n// Multiple ranges\nlet multi = Range.from('bytes=0-499, 1000-1499')\nmulti.ranges.length // 2\n\n// Suffix range (last N bytes)\nlet suffix = Range.from('bytes=-500')\nsuffix.normalize(2000) // [{ start: 1500, end: 1999 }]\n\n// Construct directly\nnew Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] })\n\n// Use class for type safety when setting Headers values\n// via Range's `.toString()` method\nlet headers = new Headers({\n  Range: new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }),\n})\nheaders.set('Range', new Range({ unit: 'bytes', ranges: [{ start: 0, end: 999 }] }))\n```\n\n### Set-Cookie\n\nParse, manipulate and stringify [`Set-Cookie` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).\n\n```ts\nimport { SetCookie } from 'remix/headers'\n\n// Parse from headers\nlet setCookie = SetCookie.from(response.headers.get('set-cookie'))\n\nsetCookie.name // \"session_id\"\nsetCookie.value // \"abc\"\nsetCookie.path // \"/\"\nsetCookie.httpOnly // true\nsetCookie.secure // true\nsetCookie.domain // undefined\nsetCookie.maxAge // undefined\nsetCookie.expires // undefined\nsetCookie.sameSite // undefined\n\n// Modify and set header\nsetCookie.maxAge = 3600\nsetCookie.sameSite = 'Strict'\nheaders.set('Set-Cookie', setCookie)\n\n// Construct directly\nnew SetCookie('session_id=abc; Path=/; HttpOnly; Secure')\nnew SetCookie({\n  name: 'session_id',\n  value: 'abc',\n  path: '/',\n  httpOnly: true,\n  secure: true,\n})\n\n// Use class for type safety when setting Headers values\n// via SetCookie's `.toString()` method\nlet headers = new Headers({\n  'Set-Cookie': new SetCookie({ name: 'session_id', value: 'abc', httpOnly: true }),\n})\nheaders.set('Set-Cookie', new SetCookie({ name: 'session_id', value: 'abc', httpOnly: true }))\n```\n\n### Vary\n\nParse, manipulate and stringify [`Vary` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary).\n\nImplements `Set<headerName>`.\n\n```ts\nimport { Vary } from 'remix/headers'\n\n// Parse from headers\nlet vary = Vary.from(response.headers.get('vary'))\n\nvary.headerNames // ['accept-encoding', 'accept-language']\nvary.has('Accept-Encoding') // true (case-insensitive)\nvary.size // 2\n\n// Modify and set header\nvary.add('User-Agent')\nvary.delete('Accept-Language')\nheaders.set('Vary', vary)\n\n// Construct directly\nnew Vary('Accept-Encoding, Accept-Language')\nnew Vary(['Accept-Encoding', 'Accept-Language'])\nnew Vary({ headerNames: ['Accept-Encoding', 'Accept-Language'] })\n\n// Use class for type safety when setting Headers values\n// via Vary's `.toString()` method\nlet headers = new Headers({\n  Vary: new Vary(['Accept-Encoding', 'Accept-Language']),\n})\nheaders.set('Vary', new Vary(['Accept-Encoding', 'Accept-Language']))\n```\n\n## Raw Headers\n\nParse and stringify raw HTTP header strings.\n\n```ts\nimport { parse, stringify } from 'remix/headers'\n\nlet headers = parse('Content-Type: text/html\\r\\nCache-Control: no-cache')\nheaders.get('content-type') // 'text/html'\nheaders.get('cache-control') // 'no-cache'\n\nstringify(headers)\n// 'Content-Type: text/html\\r\\nCache-Control: no-cache'\n```\n\n## Related Packages\n\n- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) - Build HTTP proxy servers using the web fetch API\n- [`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\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/headers/package.json",
    "content": "{\n  \"name\": \"@remix-run/headers\",\n  \"version\": \"0.19.0\",\n  \"description\": \"A toolkit for working with HTTP headers in JavaScript\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/headers\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/headers#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"http\",\n    \"header\",\n    \"headers\",\n    \"http-headers\",\n    \"request-headers\",\n    \"response-headers\",\n    \"content-negotiation\",\n    \"cookies\",\n    \"set-cookie\",\n    \"cache-control\",\n    \"content-type\",\n    \"accept\",\n    \"accept-encoding\",\n    \"accept-language\",\n    \"content-disposition\",\n    \"if-none-match\",\n    \"etag\",\n    \"user-agent\",\n    \"host\",\n    \"last-modified\"\n  ]\n}\n"
  },
  {
    "path": "packages/headers/src/index.ts",
    "content": "export { type AcceptInit, Accept } from './lib/accept.ts'\nexport { type AcceptEncodingInit, AcceptEncoding } from './lib/accept-encoding.ts'\nexport { type AcceptLanguageInit, AcceptLanguage } from './lib/accept-language.ts'\nexport { type CacheControlInit, CacheControl } from './lib/cache-control.ts'\nexport { type ContentDispositionInit, ContentDisposition } from './lib/content-disposition.ts'\nexport { type ContentRangeInit, ContentRange } from './lib/content-range.ts'\nexport { type ContentTypeInit, ContentType } from './lib/content-type.ts'\nexport { type CookieInit, Cookie } from './lib/cookie.ts'\nexport { type IfMatchInit, IfMatch } from './lib/if-match.ts'\nexport { type IfNoneMatchInit, IfNoneMatch } from './lib/if-none-match.ts'\nexport { IfRange } from './lib/if-range.ts'\nexport { type RangeInit, Range } from './lib/range.ts'\nexport { type CookieProperties, type SetCookieInit, SetCookie } from './lib/set-cookie.ts'\nexport { type VaryInit, Vary } from './lib/vary.ts'\nexport { parse, stringify } from './lib/raw-headers.ts'\n"
  },
  {
    "path": "packages/headers/src/lib/accept-encoding.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { AcceptEncoding } from './accept-encoding.ts'\n\ndescribe('Accept-Encoding', () => {\n  it('initializes with an empty string', () => {\n    let header = new AcceptEncoding('')\n    assert.equal(header.size, 0)\n  })\n\n  it('initializes with a string', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with an array', () => {\n    let header = new AcceptEncoding(['gzip', ['deflate', 0.9]])\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with an object', () => {\n    let header = new AcceptEncoding({ gzip: 1, deflate: 0.9 })\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with another AcceptEncoding', () => {\n    let header = new AcceptEncoding(new AcceptEncoding('gzip, deflate;q=0.9'))\n    assert.equal(header.size, 2)\n  })\n\n  it('handles whitespace in initial value', () => {\n    let header = new AcceptEncoding(' gzip ,  deflate;q=  0.9  ')\n    assert.equal(header.size, 2)\n  })\n\n  it('gets all encodings', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    assert.deepEqual(header.encodings, ['gzip', 'deflate'])\n  })\n\n  it('gets all weights', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    assert.deepEqual(header.weights, [1, 0.9])\n  })\n\n  it('checks if an encoding is acceptable', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9,br;q=0.8')\n    assert.equal(header.accepts('gzip'), true)\n    assert.equal(header.accepts('deflate'), true)\n    assert.equal(header.accepts('br'), true)\n    assert.equal(header.accepts('compress'), false)\n    assert.equal(header.accepts('identity'), true) // special case\n  })\n\n  it('gets the correct weights', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9,*;q=0.8')\n    assert.equal(header.getWeight('gzip'), 1)\n    assert.equal(header.getWeight('deflate'), 0.9)\n    assert.equal(header.getWeight('br'), 0.8)\n  })\n\n  it('gets the preferred encoding', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9,*;q=0.8')\n    assert.equal(header.getPreferred(['gzip', 'deflate']), 'gzip')\n    assert.equal(header.getPreferred(['deflate', 'br']), 'deflate')\n  })\n\n  it('sets and gets encodings', () => {\n    let header = new AcceptEncoding()\n    header.set('gzip', 1)\n    assert.equal(header.get('gzip'), 1)\n  })\n\n  it('deletes encodings', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    assert.equal(header.has('gzip'), true)\n    header.delete('gzip')\n    assert.equal(header.has('gzip'), false)\n  })\n\n  it('clears all encodings', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    assert.equal(header.size, 2)\n    header.clear()\n    assert.equal(header.size, 0)\n  })\n\n  it('iterates over entries', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    assert.deepEqual(Array.from(header.entries()), [\n      ['gzip', 1],\n      ['deflate', 0.9],\n    ])\n  })\n\n  it('is directly iterable', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    assert.deepEqual(Array.from(header), [\n      ['gzip', 1],\n      ['deflate', 0.9],\n    ])\n  })\n\n  it('uses forEach correctly', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    let result: [string, number][] = []\n    header.forEach((encoding, weight) => {\n      result.push([encoding, weight])\n    })\n    assert.deepEqual(result, [\n      ['gzip', 1],\n      ['deflate', 0.9],\n    ])\n  })\n\n  it('returns correct size', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    assert.equal(header.size, 2)\n  })\n\n  it('converts to a string', () => {\n    let header = new AcceptEncoding('gzip, deflate;q=0.9')\n    assert.equal(header.toString(), 'gzip,deflate;q=0.9')\n  })\n\n  it('handles setting empty weights', () => {\n    let header = new AcceptEncoding()\n    header.set('deflate')\n    assert.equal(header.get('deflate'), 1)\n  })\n\n  it('handles setting wildcard value', () => {\n    let header = new AcceptEncoding()\n    header.set('*', 0.8)\n    assert.equal(header.get('*'), 0.8)\n  })\n\n  it('sorts initial value', () => {\n    let header = new AcceptEncoding('deflate;q=0.9,gzip')\n    assert.equal(header.toString(), 'gzip,deflate;q=0.9')\n  })\n\n  it('sorts updated value', () => {\n    let header = new AcceptEncoding('gzip;q=0.8,deflate')\n    header.set('br')\n    assert.equal(header.toString(), 'deflate,br,gzip;q=0.8')\n    header.set('deflate', 0.9)\n    assert.equal(header.toString(), 'br,deflate;q=0.9,gzip;q=0.8')\n  })\n})\n\ndescribe('AcceptEncoding.from', () => {\n  it('parses a string value', () => {\n    let result = AcceptEncoding.from('gzip, deflate;q=0.5')\n    assert.ok(result instanceof AcceptEncoding)\n    assert.equal(result.size, 2)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/accept-encoding.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { parseParams } from './param-values.ts'\nimport { isIterable } from './utils.ts'\n\n/**\n * Initializer for an `Accept-Encoding` header value.\n */\nexport type AcceptEncodingInit = Iterable<string | [string, number]> | Record<string, number>\n\n/**\n * The value of a `Accept-Encoding` HTTP header.\n *\n * [MDN `Accept-Encoding` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4)\n */\nexport class AcceptEncoding implements HeaderValue, Iterable<[string, number]> {\n  #map!: Map<string, number>\n\n  constructor(init?: string | AcceptEncodingInit) {\n    if (init) return AcceptEncoding.from(init)\n    this.#map = new Map()\n  }\n\n  #sort() {\n    this.#map = new Map([...this.#map].sort((a, b) => b[1] - a[1]))\n  }\n\n  /**\n   * An array of all encodings in the header.\n   */\n  get encodings(): string[] {\n    return Array.from(this.#map.keys())\n  }\n\n  /**\n   * An array of all weights (q values) in the header.\n   */\n  get weights(): number[] {\n    return Array.from(this.#map.values())\n  }\n\n  /**\n   * The number of encodings in the header.\n   */\n  get size(): number {\n    return this.#map.size\n  }\n\n  /**\n   * Returns `true` if the header matches the given encoding (i.e. it is \"acceptable\").\n   *\n   * @param encoding The encoding to check\n   * @returns `true` if the encoding is acceptable, `false` otherwise\n   */\n  accepts(encoding: string): boolean {\n    return encoding.toLowerCase() === 'identity' || this.getWeight(encoding) > 0\n  }\n\n  /**\n   * Gets the weight an encoding. Performs wildcard matching so `*` matches all encodings.\n   *\n   * @param encoding The encoding to get\n   * @returns The weight of the encoding, or `0` if it is not in the header\n   */\n  getWeight(encoding: string): number {\n    let lower = encoding.toLowerCase()\n\n    for (let [enc, weight] of this) {\n      if (enc === lower || enc === '*' || lower === '*') {\n        return weight\n      }\n    }\n\n    return 0\n  }\n\n  /**\n   * Returns the most preferred encoding from the given list of encodings.\n   *\n   * @param encodings The encodings to choose from\n   * @returns The most preferred encoding or `null` if none match\n   */\n  getPreferred<encoding extends string>(encodings: readonly encoding[]): encoding | null {\n    let sorted = encodings\n      .map((encoding) => [encoding, this.getWeight(encoding)] as const)\n      .sort((a, b) => b[1] - a[1])\n\n    let first = sorted[0]\n\n    return first !== undefined && first[1] > 0 ? first[0] : null\n  }\n\n  /**\n   * Gets the weight of an encoding. If it is not in the header verbatim, this returns `null`.\n   *\n   * @param encoding The encoding to get\n   * @returns The weight of the encoding, or `null` if it is not in the header\n   */\n  get(encoding: string): number | null {\n    return this.#map.get(encoding.toLowerCase()) ?? null\n  }\n\n  /**\n   * Sets an encoding with the given weight.\n   *\n   * @param encoding The encoding to set\n   * @param weight The weight of the encoding (default: `1`)\n   */\n  set(encoding: string, weight = 1): void {\n    this.#map.set(encoding.toLowerCase(), weight)\n    this.#sort()\n  }\n\n  /**\n   * Removes the given encoding from the header.\n   *\n   * @param encoding The encoding to remove\n   */\n  delete(encoding: string): void {\n    this.#map.delete(encoding.toLowerCase())\n  }\n\n  /**\n   * Checks if the header contains a given encoding.\n   *\n   * @param encoding The encoding to check\n   * @returns `true` if the encoding is in the header, `false` otherwise\n   */\n  has(encoding: string): boolean {\n    return this.#map.has(encoding.toLowerCase())\n  }\n\n  /**\n   * Removes all encodings from the header.\n   */\n  clear(): void {\n    this.#map.clear()\n  }\n\n  /**\n   * Returns an iterator of all encoding and weight pairs.\n   *\n   * @returns An iterator of `[encoding, weight]` tuples\n   */\n  entries(): IterableIterator<[string, number]> {\n    return this.#map.entries()\n  }\n\n  /**\n   * Iterates over encoding and weight pairs in preference order.\n   *\n   * @returns An iterator of `[encoding, weight]` tuples.\n   */\n  [Symbol.iterator](): IterableIterator<[string, number]> {\n    return this.entries()\n  }\n\n  /**\n   * Invokes the callback for each encoding and weight pair.\n   *\n   * @param callback The function to call for each pair\n   * @param thisArg The value to use as `this` when calling the callback\n   */\n  forEach(\n    callback: (encoding: string, weight: number, header: AcceptEncoding) => void,\n    thisArg?: any,\n  ): void {\n    for (let [encoding, weight] of this) {\n      callback.call(thisArg, encoding, weight, this)\n    }\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    let pairs: string[] = []\n\n    for (let [encoding, weight] of this.#map) {\n      pairs.push(`${encoding}${weight === 1 ? '' : `;q=${weight}`}`)\n    }\n\n    return pairs.join(',')\n  }\n\n  /**\n   * Parse an Accept-Encoding header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns An AcceptEncoding instance (empty if null)\n   */\n  static from(value: string | AcceptEncodingInit | null): AcceptEncoding {\n    let header = new AcceptEncoding()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        for (let piece of value.split(/\\s*,\\s*/)) {\n          let params = parseParams(piece)\n          if (params.length < 1) continue\n\n          let encoding = params[0][0]\n          let weight = 1\n\n          for (let i = 1; i < params.length; i++) {\n            let [key, val] = params[i]\n            if (key === 'q') {\n              weight = Number(val)\n              break\n            }\n          }\n\n          header.#map.set(encoding.toLowerCase(), weight)\n        }\n      } else if (isIterable(value)) {\n        for (let item of value) {\n          if (Array.isArray(item)) {\n            header.#map.set(item[0].toLowerCase(), item[1])\n          } else {\n            header.#map.set(item.toLowerCase(), 1)\n          }\n        }\n      } else {\n        for (let encoding of Object.getOwnPropertyNames(value)) {\n          header.#map.set(encoding.toLowerCase(), value[encoding])\n        }\n      }\n\n      header.#sort()\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/accept-language.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { AcceptLanguage } from './accept-language.ts'\n\ndescribe('Accept-Language', () => {\n  it('initializes with an empty string', () => {\n    let header = new AcceptLanguage('')\n    assert.equal(header.size, 0)\n  })\n\n  it('initializes with a string', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with an array', () => {\n    let header = new AcceptLanguage(['en-US', ['en', 0.9]])\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with an object', () => {\n    let header = new AcceptLanguage({ 'en-US': 1, en: 0.9 })\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with another AcceptLanguage', () => {\n    let header = new AcceptLanguage(new AcceptLanguage('en-US,en;q=0.9'))\n    assert.equal(header.size, 2)\n  })\n\n  it('handles whitespace in initial value', () => {\n    let header = new AcceptLanguage(' en-US ,  en;q=  0.9  ')\n    assert.equal(header.size, 2)\n  })\n\n  it('gets all languages', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    assert.deepEqual(header.languages, ['en-us', 'en'])\n  })\n\n  it('gets all weights', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    assert.deepEqual(header.weights, [1, 0.9])\n  })\n\n  it('checks if a language is acceptable', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9,fr;q=0.8')\n    assert.equal(header.accepts('en-US'), true)\n    assert.equal(header.accepts('en'), true)\n    assert.equal(header.accepts('en-GB'), true)\n    assert.equal(header.accepts('fr'), true)\n    assert.equal(header.accepts('fi'), false)\n  })\n\n  it('gets the correct weight values', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9,fr;q=0.8')\n    assert.equal(header.getWeight('en-US'), 1)\n    assert.equal(header.getWeight('*'), 1)\n    assert.equal(header.getWeight('en'), 1)\n    assert.equal(header.getWeight('en-GB'), 0.9)\n    assert.equal(header.getWeight('fr'), 0.8)\n    assert.equal(header.getWeight('fi'), 0)\n  })\n\n  it('gets the preferred language', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    assert.equal(header.getPreferred(['en-GB', 'en-US']), 'en-US')\n    assert.equal(header.getPreferred(['en-GB', 'en']), 'en')\n    assert.equal(header.getPreferred(['fr', 'en-GB']), 'en-GB')\n    assert.equal(header.getPreferred(['fi', 'ja']), null)\n  })\n\n  it('sets and gets languages', () => {\n    let header = new AcceptLanguage()\n    header.set('en', 0.9)\n    assert.equal(header.get('en'), 0.9)\n  })\n\n  it('deletes languages', () => {\n    let header = new AcceptLanguage('en-US')\n    assert.equal(header.has('en-US'), true)\n    header.delete('en-US')\n    assert.equal(header.has('en-US'), false)\n  })\n\n  it('clears all languages', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    assert.equal(header.size, 2)\n    header.clear()\n    assert.equal(header.size, 0)\n  })\n\n  it('iterates over entries', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    let entries = Array.from(header.entries())\n    assert.deepEqual(entries, [\n      ['en-us', 1],\n      ['en', 0.9],\n    ])\n  })\n\n  it('is directly iterable', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    let entries = Array.from(header)\n    assert.deepEqual(entries, [\n      ['en-us', 1],\n      ['en', 0.9],\n    ])\n  })\n\n  it('uses forEach correctly', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    let result: [string, number][] = []\n    header.forEach((language, weight) => {\n      result.push([language, weight])\n    })\n    assert.deepEqual(result, [\n      ['en-us', 1],\n      ['en', 0.9],\n    ])\n  })\n\n  it('returns correct size', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    assert.equal(header.size, 2)\n  })\n\n  it('converts to string', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    assert.equal(header.toString(), 'en-us,en;q=0.9')\n  })\n\n  it('handles setting empty weight values', () => {\n    let header = new AcceptLanguage()\n    header.set('en-US')\n    assert.equal(header.get('en-US'), 1)\n  })\n\n  it('overwrites existing weight values', () => {\n    let header = new AcceptLanguage('en;q=0.9')\n    header.set('en', 1)\n    assert.equal(header.get('en'), 1)\n  })\n\n  it('handles setting wildcard value', () => {\n    let header = new AcceptLanguage()\n    header.set('*')\n    assert.equal(header.get('*'), 1)\n  })\n\n  it('sorts initial value', () => {\n    let header = new AcceptLanguage('en;q=0.9,en-US')\n    assert.equal(header.toString(), 'en-us,en;q=0.9')\n  })\n\n  it('sorts updated value', () => {\n    let header = new AcceptLanguage('en-US,en;q=0.9')\n    header.set('fi')\n    assert.equal(header.toString(), 'en-us,fi,en;q=0.9')\n    header.set('en-US', 0.8)\n    assert.equal(header.toString(), 'fi,en;q=0.9,en-us;q=0.8')\n  })\n})\n\ndescribe('AcceptLanguage.from', () => {\n  it('parses a string value', () => {\n    let result = AcceptLanguage.from('en-US, en;q=0.9')\n    assert.ok(result instanceof AcceptLanguage)\n    assert.equal(result.size, 2)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/accept-language.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { parseParams } from './param-values.ts'\nimport { isIterable } from './utils.ts'\n\n/**\n * Initializer for an `Accept-Language` header value.\n */\nexport type AcceptLanguageInit = Iterable<string | [string, number]> | Record<string, number>\n\n/**\n * The value of a `Accept-Language` HTTP header.\n *\n * [MDN `Accept-Language` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5)\n */\nexport class AcceptLanguage implements HeaderValue, Iterable<[string, number]> {\n  #map!: Map<string, number>\n\n  constructor(init?: string | AcceptLanguageInit) {\n    if (init) return AcceptLanguage.from(init)\n    this.#map = new Map()\n  }\n\n  #sort() {\n    this.#map = new Map([...this.#map].sort((a, b) => b[1] - a[1]))\n  }\n\n  /**\n   * An array of all languages in the header.\n   */\n  get languages(): string[] {\n    return Array.from(this.#map.keys())\n  }\n\n  /**\n   * An array of all weights (q values) in the header.\n   */\n  get weights(): number[] {\n    return Array.from(this.#map.values())\n  }\n\n  /**\n   * The number of languages in the header.\n   */\n  get size(): number {\n    return this.#map.size\n  }\n\n  /**\n   * Returns `true` if the header matches the given language (i.e. it is \"acceptable\").\n   *\n   * @param language The locale identifier of the language to check\n   * @returns `true` if the language is acceptable, `false` otherwise\n   */\n  accepts(language: string): boolean {\n    return this.getWeight(language) > 0\n  }\n\n  /**\n   * Gets the weight of a language with the given locale identifier. Performs wildcard and subtype\n   * matching, so `en` matches `en-US` and `en-GB`, and `*` matches all languages.\n   *\n   * @param language The locale identifier of the language to get\n   * @returns The weight of the language, or `0` if it is not in the header\n   */\n  getWeight(language: string): number {\n    let [base, subtype] = language.toLowerCase().split('-')\n\n    for (let [key, value] of this) {\n      let [b, s] = key.split('-')\n      if (\n        (b === base || b === '*' || base === '*') &&\n        (s === subtype || s === undefined || subtype === undefined)\n      ) {\n        return value\n      }\n    }\n\n    return 0\n  }\n\n  /**\n   * Returns the most preferred language from the given list of languages.\n   *\n   * @param languages The locale identifiers of the languages to choose from\n   * @returns The most preferred language or `null` if none match\n   */\n  getPreferred<language extends string>(languages: readonly language[]): language | null {\n    let sorted = languages\n      .map((language) => [language, this.getWeight(language)] as const)\n      .sort((a, b) => b[1] - a[1])\n\n    let first = sorted[0]\n\n    return first !== undefined && first[1] > 0 ? first[0] : null\n  }\n\n  /**\n   * Gets the weight of a language with the given locale identifier. If it is not in the header\n   * verbatim, this returns `null`.\n   *\n   * @param language The locale identifier of the language to get\n   * @returns The weight of the language, or `null` if it is not in the header\n   */\n  get(language: string): number | null {\n    return this.#map.get(language.toLowerCase()) ?? null\n  }\n\n  /**\n   * Sets a language with the given weight.\n   *\n   * @param language The locale identifier of the language to set\n   * @param weight The weight of the language (default: `1`)\n   */\n  set(language: string, weight = 1): void {\n    this.#map.set(language.toLowerCase(), weight)\n    this.#sort()\n  }\n\n  /**\n   * Removes a language with the given locale identifier.\n   *\n   * @param language The locale identifier of the language to remove\n   */\n  delete(language: string): void {\n    this.#map.delete(language.toLowerCase())\n  }\n\n  /**\n   * Checks if the header contains a language with the given locale identifier.\n   *\n   * @param language The locale identifier of the language to check\n   * @returns `true` if the language is in the header, `false` otherwise\n   */\n  has(language: string): boolean {\n    return this.#map.has(language.toLowerCase())\n  }\n\n  /**\n   * Removes all languages from the header.\n   */\n  clear(): void {\n    this.#map.clear()\n  }\n\n  /**\n   * Returns an iterator of all language and weight pairs.\n   *\n   * @returns An iterator of `[language, weight]` tuples\n   */\n  entries(): IterableIterator<[string, number]> {\n    return this.#map.entries()\n  }\n\n  /**\n   * Iterates over language and weight pairs in preference order.\n   *\n   * @returns An iterator of `[language, weight]` tuples.\n   */\n  [Symbol.iterator](): IterableIterator<[string, number]> {\n    return this.entries()\n  }\n\n  /**\n   * Invokes the callback for each language and weight pair.\n   *\n   * @param callback The function to call for each pair\n   * @param thisArg The value to use as `this` when calling the callback\n   */\n  forEach(\n    callback: (language: string, weight: number, header: AcceptLanguage) => void,\n    thisArg?: any,\n  ): void {\n    for (let [language, weight] of this) {\n      callback.call(thisArg, language, weight, this)\n    }\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    let pairs: string[] = []\n\n    for (let [language, weight] of this.#map) {\n      pairs.push(`${language}${weight === 1 ? '' : `;q=${weight}`}`)\n    }\n\n    return pairs.join(',')\n  }\n\n  /**\n   * Parse an Accept-Language header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns An AcceptLanguage instance (empty if null)\n   */\n  static from(value: string | AcceptLanguageInit | null): AcceptLanguage {\n    let header = new AcceptLanguage()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        for (let piece of value.split(/\\s*,\\s*/)) {\n          let params = parseParams(piece)\n          if (params.length < 1) continue\n\n          let language = params[0][0]\n          let weight = 1\n\n          for (let i = 1; i < params.length; i++) {\n            let [key, val] = params[i]\n            if (key === 'q') {\n              weight = Number(val)\n              break\n            }\n          }\n\n          header.#map.set(language.toLowerCase(), weight)\n        }\n      } else if (isIterable(value)) {\n        for (let item of value) {\n          if (Array.isArray(item)) {\n            header.#map.set(item[0].toLowerCase(), item[1])\n          } else {\n            header.#map.set(item.toLowerCase(), 1)\n          }\n        }\n      } else {\n        for (let language of Object.getOwnPropertyNames(value)) {\n          header.#map.set(language.toLowerCase(), value[language])\n        }\n      }\n\n      header.#sort()\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/accept.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { Accept } from './accept.ts'\n\ndescribe('Accept', () => {\n  it('initializes with an empty string', () => {\n    let header = new Accept('')\n    assert.equal(header.size, 0)\n  })\n\n  it('initializes with a string', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with an array', () => {\n    let header = new Accept(['text/html', ['application/json', 0.9]])\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with an object', () => {\n    let header = new Accept({ 'text/html': 1, 'application/json': 0.9 })\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with another Accept', () => {\n    let header = new Accept(new Accept('text/html,application/json;q=0.9'))\n    assert.equal(header.size, 2)\n  })\n\n  it('handles whitespace in initial value', () => {\n    let header = new Accept(' text/html ,  application/json;q=  0.9  ')\n    assert.equal(header.size, 2)\n  })\n\n  it('gets all media types', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    assert.deepEqual(header.mediaTypes, ['text/html', 'application/json'])\n  })\n\n  it('gets all weights', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    assert.deepEqual(header.weights, [1, 0.9])\n  })\n\n  it('checks if a media type is acceptable', () => {\n    let header = new Accept('text/html,text/*;q=0.9,application/json;q=0.8')\n    assert.equal(header.accepts('text/html'), true)\n    assert.equal(header.accepts('text/*'), true)\n    assert.equal(header.accepts('text/plain'), true)\n    assert.equal(header.accepts('application/json'), true)\n    assert.equal(header.accepts('image/jpeg'), false)\n  })\n\n  it('gets the correct weight values', () => {\n    let header = new Accept('text/html,text/*;q=0.9,application/json;q=0.8')\n    assert.equal(header.getWeight('text/html'), 1)\n    assert.equal(header.getWeight('*/*'), 1)\n    assert.equal(header.getWeight('text/*'), 1)\n    assert.equal(header.getWeight('text/plain'), 0.9)\n    assert.equal(header.getWeight('application/json'), 0.8)\n    assert.equal(header.getWeight('image/jpeg'), 0)\n  })\n\n  it('gets the preferred media type', () => {\n    let header = new Accept('text/html,text/*;q=0.9,application/json;q=0.8')\n    assert.equal(header.getPreferred(['text/html', 'application/json']), 'text/html')\n    assert.equal(header.getPreferred(['text/plain', 'text/html']), 'text/html')\n    assert.equal(header.getPreferred(['image/jpeg']), null)\n  })\n\n  it('sets and gets media types', () => {\n    let header = new Accept()\n    header.set('application/json', 0.9)\n    assert.equal(header.get('application/json'), 0.9)\n  })\n\n  it('deletes media types', () => {\n    let header = new Accept('text/html')\n    assert.equal(header.has('text/html'), true)\n    header.delete('text/html')\n    assert.equal(header.has('text/html'), false)\n  })\n\n  it('clears all media types', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    header.clear()\n    assert.equal(header.size, 0)\n  })\n\n  it('iterates over entries', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    let entries = Array.from(header.entries())\n    assert.deepEqual(entries, [\n      ['text/html', 1],\n      ['application/json', 0.9],\n    ])\n  })\n\n  it('is directly iterable', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    let mediaTypes = Array.from(header)\n    assert.deepEqual(mediaTypes, [\n      ['text/html', 1],\n      ['application/json', 0.9],\n    ])\n  })\n\n  it('uses forEach correctly', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    let result: [string, number][] = []\n    header.forEach((mediaType, weight) => {\n      result.push([mediaType, weight])\n    })\n    assert.deepEqual(result, [\n      ['text/html', 1],\n      ['application/json', 0.9],\n    ])\n  })\n\n  it('returns correct size', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    assert.equal(header.size, 2)\n  })\n\n  it('converts to string correctly', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    assert.equal(header.toString(), 'text/html,application/json;q=0.9')\n  })\n\n  it('handles setting empty weight values', () => {\n    let header = new Accept()\n    header.set('text/html')\n    assert.equal(header.get('text/html'), 1)\n  })\n\n  it('overwrites existing weight values', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    header.set('application/json', 0.8)\n    assert.equal(header.get('application/json'), 0.8)\n  })\n\n  it('handles setting wildcard media types', () => {\n    let header = new Accept()\n    header.set('*/*')\n    assert.equal(header.get('*/*'), 1)\n  })\n\n  it('sorts initial value', () => {\n    let header = new Accept('application/json;q=0.9,text/html')\n    assert.equal(header.toString(), 'text/html,application/json;q=0.9')\n    assert.deepEqual(header.mediaTypes, ['text/html', 'application/json'])\n  })\n\n  it('sorts updated value', () => {\n    let header = new Accept('text/html,application/json;q=0.9')\n    header.set('application/json', 0.8)\n    assert.equal(header.toString(), 'text/html,application/json;q=0.8')\n    assert.deepEqual(header.mediaTypes, ['text/html', 'application/json'])\n  })\n})\n\ndescribe('Accept.from', () => {\n  it('parses a string value', () => {\n    let result = Accept.from('text/html, application/json;q=0.9')\n    assert.ok(result instanceof Accept)\n    assert.equal(result.size, 2)\n    assert.equal(result.getWeight('text/html'), 1)\n    assert.equal(result.getWeight('application/json'), 0.9)\n  })\n\n  it('returns empty instance for null', () => {\n    let result = Accept.from(null)\n    assert.ok(result instanceof Accept)\n    assert.equal(result.size, 0)\n  })\n\n  it('accepts init object', () => {\n    let result = Accept.from({ 'text/html': 1 })\n    assert.ok(result instanceof Accept)\n    assert.equal(result.size, 1)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/accept.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { parseParams } from './param-values.ts'\nimport { isIterable } from './utils.ts'\n\n/**\n * Initializer for an {@link Accept} header value.\n */\nexport type AcceptInit = Iterable<string | [string, number]> | Record<string, number>\n\n/**\n * The value of a `Accept` HTTP header.\n *\n * [MDN `Accept` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)\n */\nexport class Accept implements HeaderValue, Iterable<[string, number]> {\n  #map!: Map<string, number>\n\n  constructor(init?: string | AcceptInit) {\n    if (init) return Accept.from(init)\n    this.#map = new Map()\n  }\n\n  #sort() {\n    this.#map = new Map([...this.#map].sort((a, b) => b[1] - a[1]))\n  }\n\n  /**\n   * An array of all media types in the header.\n   */\n  get mediaTypes(): string[] {\n    return Array.from(this.#map.keys())\n  }\n\n  /**\n   * An array of all weights (q values) in the header.\n   */\n  get weights(): number[] {\n    return Array.from(this.#map.values())\n  }\n\n  /**\n   * The number of media types in the `Accept` header.\n   */\n  get size(): number {\n    return this.#map.size\n  }\n\n  /**\n   * Returns `true` if the header matches the given media type (i.e. it is \"acceptable\").\n   *\n   * @param mediaType The media type to check\n   * @returns `true` if the media type is acceptable, `false` otherwise\n   */\n  accepts(mediaType: string): boolean {\n    return this.getWeight(mediaType) > 0\n  }\n\n  /**\n   * Gets the weight of a given media type. Also supports wildcards, so e.g. `text/*` will match `text/html`.\n   *\n   * @param mediaType The media type to get the weight of\n   * @returns The weight of the media type\n   */\n  getWeight(mediaType: string): number {\n    let [type, subtype] = mediaType.toLowerCase().split('/')\n\n    for (let [key, value] of this) {\n      let [t, s] = key.split('/')\n      if (\n        (t === type || t === '*' || type === '*') &&\n        (s === subtype || s === '*' || subtype === '*')\n      ) {\n        return value\n      }\n    }\n\n    return 0\n  }\n\n  /**\n   * Returns the most preferred media type from the given list of media types.\n   *\n   * @param mediaTypes The list of media types to choose from\n   * @returns The most preferred media type or `null` if none match\n   */\n  getPreferred<mediaType extends string>(mediaTypes: readonly mediaType[]): mediaType | null {\n    let sorted = mediaTypes\n      .map((mediaType) => [mediaType, this.getWeight(mediaType)] as const)\n      .sort((a, b) => b[1] - a[1])\n\n    let first = sorted[0]\n\n    return first !== undefined && first[1] > 0 ? first[0] : null\n  }\n\n  /**\n   * Returns the weight of a media type. If it is not in the header verbatim, this returns `null`.\n   *\n   * @param mediaType The media type to get the weight of\n   * @returns The weight of the media type, or `null` if it is not in the header\n   */\n  get(mediaType: string): number | null {\n    return this.#map.get(mediaType.toLowerCase()) ?? null\n  }\n\n  /**\n   * Sets a media type with the given weight.\n   *\n   * @param mediaType The media type to set\n   * @param weight The weight of the media type (default: `1`)\n   */\n  set(mediaType: string, weight = 1): void {\n    this.#map.set(mediaType.toLowerCase(), weight)\n    this.#sort()\n  }\n\n  /**\n   * Removes the given media type from the header.\n   *\n   * @param mediaType The media type to remove\n   */\n  delete(mediaType: string): void {\n    this.#map.delete(mediaType.toLowerCase())\n  }\n\n  /**\n   * Checks if a media type is in the header.\n   *\n   * @param mediaType The media type to check\n   * @returns `true` if the media type is in the header (verbatim), `false` otherwise\n   */\n  has(mediaType: string): boolean {\n    return this.#map.has(mediaType.toLowerCase())\n  }\n\n  /**\n   * Removes all media types from the header.\n   */\n  clear(): void {\n    this.#map.clear()\n  }\n\n  /**\n   * Returns an iterator of all media type and weight pairs.\n   *\n   * @returns An iterator of `[mediaType, weight]` tuples\n   */\n  entries(): IterableIterator<[string, number]> {\n    return this.#map.entries()\n  }\n\n  /**\n   * Iterates over media type and weight pairs in preference order.\n   *\n   * @returns An iterator of `[mediaType, weight]` tuples.\n   */\n  [Symbol.iterator](): IterableIterator<[string, number]> {\n    return this.entries()\n  }\n\n  /**\n   * Invokes the callback for each media type and weight pair.\n   *\n   * @param callback The function to call for each pair\n   * @param thisArg The value to use as `this` when calling the callback\n   */\n  forEach(\n    callback: (mediaType: string, weight: number, header: Accept) => void,\n    thisArg?: any,\n  ): void {\n    for (let [mediaType, weight] of this) {\n      callback.call(thisArg, mediaType, weight, this)\n    }\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    let pairs: string[] = []\n\n    for (let [mediaType, weight] of this.#map) {\n      pairs.push(`${mediaType}${weight === 1 ? '' : `;q=${weight}`}`)\n    }\n\n    return pairs.join(',')\n  }\n\n  /**\n   * Parse an Accept header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns An Accept instance (empty if null)\n   */\n  static from(value: string | AcceptInit | null): Accept {\n    let header = new Accept()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        for (let piece of value.split(/\\s*,\\s*/)) {\n          let params = parseParams(piece)\n          if (params.length < 1) continue\n\n          let mediaType = params[0][0]\n          let weight = 1\n\n          for (let i = 1; i < params.length; i++) {\n            let [key, val] = params[i]\n            if (key === 'q') {\n              weight = Number(val)\n              break\n            }\n          }\n\n          header.#map.set(mediaType.toLowerCase(), weight)\n        }\n      } else if (isIterable(value)) {\n        for (let mediaType of value) {\n          if (Array.isArray(mediaType)) {\n            header.#map.set(mediaType[0].toLowerCase(), mediaType[1])\n          } else {\n            header.#map.set(mediaType.toLowerCase(), 1)\n          }\n        }\n      } else {\n        for (let mediaType of Object.getOwnPropertyNames(value)) {\n          header.#map.set(mediaType.toLowerCase(), value[mediaType])\n        }\n      }\n\n      header.#sort()\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/cache-control.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { CacheControl } from './cache-control.ts'\n\nconst paramTestCases: Array<[string, keyof CacheControl, string, unknown]> = [\n  ['max-age', 'maxAge', '3600', 3600],\n  ['max-stale', 'maxStale', '7200', 7200],\n  ['min-fresh', 'minFresh', '1800', 1800],\n  ['s-maxage', 'sMaxage', '3600', 3600],\n  ['no-cache', 'noCache', '', true],\n  ['no-store', 'noStore', '', true],\n  ['no-transform', 'noTransform', '', true],\n  ['only-if-cached', 'onlyIfCached', '', true],\n  ['must-revalidate', 'mustRevalidate', '', true],\n  ['proxy-revalidate', 'proxyRevalidate', '', true],\n  ['must-understand', 'mustUnderstand', '', true],\n  ['private', 'private', '', true],\n  ['public', 'public', '', true],\n  ['immutable', 'immutable', '', true],\n  ['stale-while-revalidate', 'staleWhileRevalidate', '60', 60],\n  ['stale-if-error', 'staleIfError', '120', 120],\n]\n\ndescribe('CacheControl', () => {\n  it('initializes with an empty string', () => {\n    let header = new CacheControl('')\n    assert.equal(header.maxAge, undefined)\n    assert.equal(header.public, undefined)\n    assert.equal(`${header}`, '')\n  })\n\n  it('initializes with a string', () => {\n    let header = new CacheControl('public, max-age=3600, s-maxage=3600')\n    assert.equal(header.maxAge, 3600)\n    assert.equal(header.sMaxage, 3600)\n    assert.equal(header.public, true)\n  })\n\n  for (let [param, prop, value, expected] of paramTestCases) {\n    it(`initializes parameter: ${param}=${value}`, () => {\n      let header = new CacheControl(`${param}=${value}`)\n      assert.equal(header[prop], expected)\n    })\n\n    it(`coverts parameter to string: ${param}=${value}`, () => {\n      let header = new CacheControl('')\n      header[prop] = expected as never\n      let expectedString = value ? `${param}=${value}` : param\n      assert.equal(header.toString(), expectedString)\n    })\n  }\n\n  it('initializes with an object', () => {\n    let header = new CacheControl({ public: true, maxAge: 3600, sMaxage: 3600 })\n    assert.equal(header.maxAge, 3600)\n    assert.equal(header.sMaxage, 3600)\n    assert.equal(header.public, true)\n  })\n\n  it('initializes with another CacheControl', () => {\n    let header = new CacheControl(new CacheControl('public, max-age=3600, s-maxage=3600'))\n    assert.equal(header.maxAge, 3600)\n    assert.equal(header.sMaxage, 3600)\n    assert.equal(header.public, true)\n  })\n\n  it('handles whitespace in initial value', () => {\n    let header = new CacheControl(' public , max-age = 3600, s-maxage=3600 ')\n    assert.equal(header.maxAge, 3600)\n    assert.equal(header.sMaxage, 3600)\n    assert.equal(header.public, true)\n  })\n\n  it('sets and gets attributes', () => {\n    let header = new CacheControl('')\n    header.maxAge = 3600\n    header.sMaxage = 3600\n    header.public = true\n    assert.equal(header.maxAge, 3600)\n    assert.equal(header.sMaxage, 3600)\n    assert.equal(header.public, true)\n  })\n\n  it('converts to a string properly', () => {\n    let header = new CacheControl('public, max-age=3600, s-maxage=3600')\n    assert.equal(header.toString(), 'public, max-age=3600, s-maxage=3600')\n  })\n\n  it('sets numerical values to 0 instead of omitting them', () => {\n    let header = new CacheControl()\n    header.maxAge = 0\n    assert.equal(header.toString(), 'max-age=0')\n  })\n})\n\ndescribe('CacheControl.from', () => {\n  it('parses a string value', () => {\n    let result = CacheControl.from('max-age=3600, public')\n    assert.ok(result instanceof CacheControl)\n    assert.equal(result.maxAge, 3600)\n    assert.equal(result.public, true)\n  })\n\n  it('accepts init object', () => {\n    let result = CacheControl.from({ maxAge: 3600, public: true })\n    assert.ok(result instanceof CacheControl)\n    assert.equal(result.maxAge, 3600)\n    assert.equal(result.public, true)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/cache-control.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { parseParams } from './param-values.ts'\n\n// Taken from https://github.com/jjenzz/pretty-cache-header by jjenzz\n// License: MIT https://github.com/jjenzz/pretty-cache-header/blob/main/LICENSE\n\n/**\n * Initializer for a `Cache-Control` header value.\n */\nexport interface CacheControlInit {\n  /**\n   * The `max-age=N` **request directive** indicates that the client allows a stored response that\n   * is generated on the origin server within _N_ seconds — where _N_ may be any non-negative\n   * integer (including `0`).\n   *\n   * The `max-age=N` **response directive** indicates that the response remains\n   * [fresh](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age)\n   * until _N_ seconds after the response is generated.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#max-age)\n   */\n  maxAge?: number\n  /**\n   * The `max-stale=N` **request directive** indicates that the client allows a stored response\n   * that is [stale](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age)\n   * within _N_ seconds.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#max-stale)\n   */\n  maxStale?: number\n  /**\n   * The `min-fresh=N` **request directive** indicates that the client allows a stored response\n   * that is [fresh](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age)\n   * for at least _N_ seconds.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#min-fresh)\n   */\n  minFresh?: number\n  /**\n   * The `s-maxage` **response directive** also indicates how long the response is\n   * [fresh](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age) for (similar to `max-age`) —\n   * but it is specific to shared caches, and they will ignore `max-age` when it is present.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#s-maxage)\n   */\n  sMaxage?: number\n  /**\n   * The `no-cache` **request directive** asks caches to validate the response with the origin\n   * server before reuse. If you want caches to always check for content updates while reusing\n   * stored content, `no-cache` is the directive to use.\n   *\n   * The `no-cache` **response directive** indicates that the response can be stored in caches, but\n   * the response must be validated with the origin server before each reuse, even when the cache\n   * is disconnected from the origin server.\n   *\n   * `no-cache` allows clients to request the most up-to-date response even if the cache has a\n   * [fresh](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age)\n   * response.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#no-cache)\n   */\n  noCache?: true\n  /**\n   * The `no-store` **request directive** allows a client to request that caches refrain from\n   * storing the request and corresponding response — even if the origin server's response could\n   * be stored.\n   *\n   * The `no-store` **response directive** indicates that any caches of any kind (private or shared)\n   * should not store this response.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#no-store)\n   */\n  noStore?: true\n  /**\n   * `no-transform` indicates that any intermediary (regardless of whether it implements a cache)\n   * shouldn't transform the response contents.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#no-transform)\n   */\n  noTransform?: true\n  /**\n   * The client indicates that cache should obtain an already-cached response. If a cache has\n   * stored a response, it's reused.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#only-if-cached)\n   */\n  onlyIfCached?: true\n  /**\n   * The `must-revalidate` **response directive** indicates that the response can be stored in\n   * caches and can be reused while [fresh](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age).\n   * If the response becomes [stale](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age),\n   * it must be validated with the origin server before reuse.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#must-revalidate)\n   */\n  mustRevalidate?: true\n  /**\n   * The `proxy-revalidate` **response directive** is the equivalent of `must-revalidate`, but\n   * specifically for shared caches only.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#proxy-revalidate)\n   */\n  proxyRevalidate?: true\n  /**\n   * The `must-understand` **response directive** indicates that a cache should store the response\n   * only if it understands the requirements for caching based on status code.\n   *\n   * `must-understand` should be coupled with `no-store` for fallback behavior.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#must-understand)\n   */\n  mustUnderstand?: true\n  /**\n   * The `private` **response directive** indicates that the response can be stored only in a\n   * private cache (e.g. local caches in browsers).\n   *\n   * You should add the `private` directive for user-personalized content, especially for responses\n   * received after login and for sessions managed via cookies.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#private)\n   */\n  private?: true\n  /**\n   * The `public` **response directive** indicates that the response can be stored in a shared\n   * cache. Responses for requests with `Authorization` header fields must not be stored in a\n   * shared cache; however, the `public` directive will cause such responses to be stored in a\n   * shared cache.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#public)\n   */\n  public?: true\n  /**\n   * The `immutable` **response directive** indicates that the response will not be updated while\n   * it's [fresh](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age).\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#public)\n   */\n  immutable?: true\n  /**\n   * The `stale-while-revalidate` **response directive** indicates that the cache could reuse a\n   * stale response while it revalidates it to a cache.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-while-revalidate)\n   */\n  staleWhileRevalidate?: number\n  /**\n   * The `stale-if-error` **response directive** indicates that the cache can reuse a\n   * [stale response](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age)\n   * when an upstream server generates an error, or when the error is generated locally. Here, an\n   * error is considered any response with a status code of 500, 502, 503, or 504.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-if-error)\n   */\n  staleIfError?: number\n}\n\n/**\n * The value of a `Cache-Control` HTTP header.\n *\n * [MDN `Cache-Control` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2)\n */\nexport class CacheControl implements HeaderValue, CacheControlInit {\n  /**\n   * The configured `max-age` directive value in seconds.\n   */\n  maxAge?: number\n\n  /**\n   * The configured `max-stale` directive value in seconds.\n   */\n  maxStale?: number\n\n  /**\n   * The configured `min-fresh` directive value in seconds.\n   */\n  minFresh?: number\n\n  /**\n   * The configured `s-maxage` directive value in seconds.\n   */\n  sMaxage?: number\n\n  /**\n   * Whether the `no-cache` directive is present.\n   */\n  noCache?: true\n\n  /**\n   * Whether the `no-store` directive is present.\n   */\n  noStore?: true\n\n  /**\n   * Whether the `no-transform` directive is present.\n   */\n  noTransform?: true\n\n  /**\n   * Whether the `only-if-cached` directive is present.\n   */\n  onlyIfCached?: true\n\n  /**\n   * Whether the `must-revalidate` directive is present.\n   */\n  mustRevalidate?: true\n\n  /**\n   * Whether the `proxy-revalidate` directive is present.\n   */\n  proxyRevalidate?: true\n\n  /**\n   * Whether the `must-understand` directive is present.\n   */\n  mustUnderstand?: true\n\n  /**\n   * Whether the `private` directive is present.\n   */\n  private?: true\n\n  /**\n   * Whether the `public` directive is present.\n   */\n  public?: true\n\n  /**\n   * Whether the `immutable` directive is present.\n   */\n  immutable?: true\n\n  /**\n   * The configured `stale-while-revalidate` directive value in seconds.\n   */\n  staleWhileRevalidate?: number\n\n  /**\n   * The configured `stale-if-error` directive value in seconds.\n   */\n  staleIfError?: number\n\n  constructor(init?: string | CacheControlInit) {\n    if (init) return CacheControl.from(init)\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    let parts = []\n\n    if (this.public) {\n      parts.push('public')\n    }\n    if (this.private) {\n      parts.push('private')\n    }\n    if (typeof this.maxAge === 'number') {\n      parts.push(`max-age=${this.maxAge}`)\n    }\n    if (typeof this.sMaxage === 'number') {\n      parts.push(`s-maxage=${this.sMaxage}`)\n    }\n    if (this.noCache) {\n      parts.push('no-cache')\n    }\n    if (this.noStore) {\n      parts.push('no-store')\n    }\n    if (this.noTransform) {\n      parts.push('no-transform')\n    }\n    if (this.onlyIfCached) {\n      parts.push('only-if-cached')\n    }\n    if (this.mustRevalidate) {\n      parts.push('must-revalidate')\n    }\n    if (this.proxyRevalidate) {\n      parts.push('proxy-revalidate')\n    }\n    if (this.mustUnderstand) {\n      parts.push('must-understand')\n    }\n    if (this.immutable) {\n      parts.push('immutable')\n    }\n    if (typeof this.staleWhileRevalidate === 'number') {\n      parts.push(`stale-while-revalidate=${this.staleWhileRevalidate}`)\n    }\n    if (typeof this.staleIfError === 'number') {\n      parts.push(`stale-if-error=${this.staleIfError}`)\n    }\n    if (typeof this.maxStale === 'number') {\n      parts.push(`max-stale=${this.maxStale}`)\n    }\n    if (typeof this.minFresh === 'number') {\n      parts.push(`min-fresh=${this.minFresh}`)\n    }\n\n    return parts.join(', ')\n  }\n\n  /**\n   * Parse a Cache-Control header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns A CacheControl instance (empty if null)\n   */\n  static from(value: string | CacheControlInit | null): CacheControl {\n    let header = new CacheControl()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        let params = parseParams(value, ',')\n        if (params.length > 0) {\n          for (let [name, val] of params) {\n            switch (name) {\n              case 'max-age':\n                header.maxAge = Number(val)\n                break\n              case 'max-stale':\n                header.maxStale = Number(val)\n                break\n              case 'min-fresh':\n                header.minFresh = Number(val)\n                break\n              case 's-maxage':\n                header.sMaxage = Number(val)\n                break\n              case 'no-cache':\n                header.noCache = true\n                break\n              case 'no-store':\n                header.noStore = true\n                break\n              case 'no-transform':\n                header.noTransform = true\n                break\n              case 'only-if-cached':\n                header.onlyIfCached = true\n                break\n              case 'must-revalidate':\n                header.mustRevalidate = true\n                break\n              case 'proxy-revalidate':\n                header.proxyRevalidate = true\n                break\n              case 'must-understand':\n                header.mustUnderstand = true\n                break\n              case 'private':\n                header.private = true\n                break\n              case 'public':\n                header.public = true\n                break\n              case 'immutable':\n                header.immutable = true\n                break\n              case 'stale-while-revalidate':\n                header.staleWhileRevalidate = Number(val)\n                break\n              case 'stale-if-error':\n                header.staleIfError = Number(val)\n                break\n            }\n          }\n        }\n      } else {\n        header.maxAge = value.maxAge\n        header.maxStale = value.maxStale\n        header.minFresh = value.minFresh\n        header.sMaxage = value.sMaxage\n        header.noCache = value.noCache\n        header.noStore = value.noStore\n        header.noTransform = value.noTransform\n        header.onlyIfCached = value.onlyIfCached\n        header.mustRevalidate = value.mustRevalidate\n        header.proxyRevalidate = value.proxyRevalidate\n        header.mustUnderstand = value.mustUnderstand\n        header.private = value.private\n        header.public = value.public\n        header.immutable = value.immutable\n        header.staleWhileRevalidate = value.staleWhileRevalidate\n        header.staleIfError = value.staleIfError\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/content-disposition.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { ContentDisposition } from './content-disposition.ts'\n\ndescribe('ContentDisposition', () => {\n  it('initializes with an empty string', () => {\n    let header = new ContentDisposition('')\n    assert.equal(header.type, undefined)\n    assert.equal(header.filename, undefined)\n  })\n\n  it('initializes with a string', () => {\n    let header = new ContentDisposition('attachment; filename=\"example.txt\"')\n    assert.equal(header.type, 'attachment')\n    assert.equal(header.filename, 'example.txt')\n  })\n\n  it('initializes with an object', () => {\n    let header = new ContentDisposition({ type: 'attachment', filename: 'example.txt' })\n    assert.equal(header.type, 'attachment')\n    assert.equal(header.filename, 'example.txt')\n  })\n\n  it('initializes with another ContentDisposition', () => {\n    let header = new ContentDisposition(\n      new ContentDisposition('attachment; filename=\"example.txt\"'),\n    )\n    assert.equal(header.type, 'attachment')\n    assert.equal(header.filename, 'example.txt')\n  })\n\n  it('handles whitespace in initial value', () => {\n    let header = new ContentDisposition(' inline ;  filename = \"document.pdf\" ')\n    assert.equal(header.type, 'inline')\n    assert.equal(header.filename, 'document.pdf')\n  })\n\n  it('sets and gets type', () => {\n    let header = new ContentDisposition('attachment')\n    header.type = 'inline'\n    assert.equal(header.type, 'inline')\n  })\n\n  it('sets and gets filename', () => {\n    let header = new ContentDisposition('attachment')\n    header.filename = 'example.txt'\n    assert.equal(header.filename, 'example.txt')\n  })\n\n  it('sets and gets name', () => {\n    let header = new ContentDisposition('form-data')\n    header.name = 'field1'\n    assert.equal(header.name, 'field1')\n  })\n\n  it('sets and gets filenameSplat', () => {\n    let header = new ContentDisposition('attachment')\n    header.filenameSplat = \"UTF-8''%E6%96%87%E4%BB%B6.txt\"\n    assert.equal(header.filenameSplat, \"UTF-8''%E6%96%87%E4%BB%B6.txt\")\n  })\n\n  it('handles quoted attribute values', () => {\n    let header = new ContentDisposition('attachment; filename=\"file with spaces.txt\"')\n    assert.equal(header.filename, 'file with spaces.txt')\n  })\n\n  it('converts to string correctly', () => {\n    let header = new ContentDisposition('attachment; filename=\"example.txt\"')\n    assert.equal(header.toString(), 'attachment; filename=example.txt')\n  })\n\n  it('converts to an empty string when type is not set', () => {\n    let header = new ContentDisposition()\n    header.filename = 'example.txt'\n    assert.equal(header.toString(), '')\n  })\n\n  it('handles multiple attributes', () => {\n    let header = new ContentDisposition('form-data; name=\"field1\"; filename=\"example.txt\"')\n    assert.equal(header.type, 'form-data')\n    assert.equal(header.name, 'field1')\n    assert.equal(header.filename, 'example.txt')\n  })\n\n  it('preserves case for type', () => {\n    let header = new ContentDisposition('Attachment')\n    assert.equal(header.type, 'Attachment')\n  })\n\n  it('handles attribute values with special characters', () => {\n    let header = new ContentDisposition(\n      'attachment; filename=\"file with spaces and (parentheses).txt\"',\n    )\n    assert.equal(header.filename, 'file with spaces and (parentheses).txt')\n  })\n\n  it('correctly quotes attribute values in toString()', () => {\n    let header = new ContentDisposition('attachment')\n    header.filename = 'file \"with\" quotes.txt'\n    assert.equal(header.toString(), 'attachment; filename=\"file \\\\\"with\\\\\" quotes.txt\"')\n  })\n\n  it('handles empty attribute values', () => {\n    let header = new ContentDisposition('form-data; name=')\n    assert.equal(header.name, '')\n  })\n\n  it('ignores attributes without values', () => {\n    let header = new ContentDisposition('attachment; filename')\n    assert.equal(header.filename, undefined)\n  })\n\n  it('preserves order of attributes in toString()', () => {\n    let header = new ContentDisposition('form-data; name=\"field1\"; filename=\"example.txt\"')\n    assert.equal(header.toString(), 'form-data; name=field1; filename=example.txt')\n  })\n\n  it('handles filename* (RFC 5987) correctly', () => {\n    let header = new ContentDisposition(\"attachment; filename*=UTF-8''%E6%96%87%E4%BB%B6.txt\")\n    assert.equal(header.filenameSplat, \"UTF-8''%E6%96%87%E4%BB%B6.txt\")\n  })\n\n  it('prioritizes filename* over filename when both are present', () => {\n    let header = new ContentDisposition(\n      'attachment; filename=\"fallback.txt\"; filename*=UTF-8\\'\\'%E6%96%87%E4%BB%B6.txt',\n    )\n    assert.equal(header.filename, 'fallback.txt')\n    assert.equal(header.filenameSplat, \"UTF-8''%E6%96%87%E4%BB%B6.txt\")\n  })\n\n  it('handles form-data disposition type correctly', () => {\n    let header = new ContentDisposition('form-data; name=\"uploadedfile\"; filename=\"example.txt\"')\n    assert.equal(header.type, 'form-data')\n    assert.equal(header.name, 'uploadedfile')\n    assert.equal(header.filename, 'example.txt')\n  })\n\n  describe('preferredFilename', () => {\n    it('returns filename* when both filename and filename* are present', () => {\n      let header = new ContentDisposition(\n        'attachment; filename=\"old.txt\"; filename*=UTF-8\\'\\'new.txt',\n      )\n      assert.equal(header.preferredFilename, 'new.txt')\n    })\n\n    it('returns filename when only filename is present', () => {\n      let header = new ContentDisposition('attachment; filename=\"document.pdf\"')\n      assert.equal(header.preferredFilename, 'document.pdf')\n    })\n\n    it('returns filename* when only filename* is present', () => {\n      let header = new ContentDisposition(\"attachment; filename*=UTF-8''special%20file.txt\")\n      assert.equal(header.preferredFilename, 'special file.txt')\n    })\n\n    it('handles UTF-8 encoded filename* with special characters', () => {\n      let header = new ContentDisposition(\"attachment; filename*=UTF-8''%E6%96%87%E4%BB%B6.txt\")\n      assert.equal(header.preferredFilename, '文件.txt')\n    })\n\n    it('handles ISO-8859-1 encoded filename* with special characters', () => {\n      let header = new ContentDisposition(\"attachment; filename*=ISO-8859-1''f%F6o.txt\")\n      assert.equal(header.preferredFilename, 'föo.txt')\n    })\n\n    it('handles filename* with spaces and other special characters', () => {\n      let header = new ContentDisposition(\n        \"attachment; filename*=UTF-8''hello%20world%21%20%26%20goodbye.txt\",\n      )\n      assert.equal(header.preferredFilename, 'hello world! & goodbye.txt')\n    })\n\n    it('returns undefined when no filename or filename* is present', () => {\n      let header = new ContentDisposition('attachment')\n      assert.equal(header.preferredFilename, undefined)\n    })\n\n    it('falls back to filename when filename* is invalid', () => {\n      let header = new ContentDisposition('attachment; filename=\"fallback.txt\"; filename*=invalid')\n      assert.equal(header.preferredFilename, 'fallback.txt')\n    })\n\n    it('correctly decodes ISO-8859-1 encoded filename', () => {\n      let header = new ContentDisposition(\"attachment; filename*=ISO-8859-1''f%F6o.txt\")\n      assert.equal(header.preferredFilename, 'föo.txt')\n    })\n\n    it('correctly decodes ISO-8859-15 encoded filename', () => {\n      let header = new ContentDisposition(\"attachment; filename*=ISO-8859-15''file%A4.txt\")\n      assert.equal(header.preferredFilename, 'file€.txt')\n    })\n\n    it('correctly decodes windows-1252 encoded filename', () => {\n      let header = new ContentDisposition(\"attachment; filename*=windows-1252''file%80.txt\")\n      assert.equal(header.preferredFilename, 'file€.txt')\n    })\n\n    it('handles UTF-8 encoded filename correctly', () => {\n      let header = new ContentDisposition(\"attachment; filename*=UTF-8''%E6%96%87%E4%BB%B6.txt\")\n      assert.equal(header.preferredFilename, '文件.txt')\n    })\n\n    it('falls back gracefully with a warning for unknown charsets', () => {\n      let warn = console.warn\n      let warnWasCalled = false\n      console.warn = () => {\n        warnWasCalled = true\n      }\n\n      let header = new ContentDisposition(\"attachment; filename*=unknown-charset''file%FF.txt\")\n\n      assert.equal(header.preferredFilename, 'fileÿ.txt')\n      assert.ok(warnWasCalled, 'console.warn should have been called')\n\n      console.warn = warn\n    })\n  })\n})\n\ndescribe('ContentDisposition.from', () => {\n  it('parses a string value', () => {\n    let result = ContentDisposition.from('attachment; filename=\"test.txt\"')\n    assert.ok(result instanceof ContentDisposition)\n    assert.equal(result.type, 'attachment')\n    assert.equal(result.filename, 'test.txt')\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/content-disposition.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { parseParams, quote } from './param-values.ts'\n\n/**\n * Initializer for a `Content-Disposition` header value.\n */\nexport interface ContentDispositionInit {\n  /**\n   * For file uploads, the name of the file that the user selected.\n   */\n  filename?: string\n  /**\n   * For file uploads, the name of the file that the user selected, encoded as a [RFC 8187](https://tools.ietf.org/html/rfc8187) `filename*` parameter.\n   * This parameter allows non-ASCII characters in filenames, and specifies the character encoding.\n   */\n  filenameSplat?: string\n  /**\n   * For `multipart/form-data` requests, the name of the `<input>` field associated with this content.\n   */\n  name?: string\n  /**\n   * The disposition type of the content, such as `attachment` or `inline`.\n   */\n  type?: string\n}\n\n/**\n * The value of a `Content-Disposition` HTTP header.\n *\n * [MDN `Content-Disposition` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)\n *\n * [RFC 6266](https://tools.ietf.org/html/rfc6266)\n */\nexport class ContentDisposition implements HeaderValue, ContentDispositionInit {\n  /**\n   * The `filename` parameter value.\n   */\n  filename?: string\n\n  /**\n   * The RFC 8187-encoded `filename*` parameter value.\n   */\n  filenameSplat?: string\n\n  /**\n   * The associated multipart field name.\n   */\n  name?: string\n\n  /**\n   * The disposition type such as `attachment` or `inline`.\n   */\n  type?: string\n\n  constructor(init?: string | ContentDispositionInit) {\n    if (init) return ContentDisposition.from(init)\n  }\n\n  /**\n   * The preferred filename for the content, using the `filename*` parameter if present, falling back to the `filename` parameter.\n   *\n   * From [RFC 6266](https://tools.ietf.org/html/rfc6266):\n   *\n   * Many user agent implementations predating this specification do not understand the \"filename*\" parameter.\n   * Therefore, when both \"filename\" and \"filename*\" are present in a single header field value, recipients SHOULD\n   * pick \"filename*\" and ignore \"filename\". This way, senders can avoid special-casing specific user agents by\n   * sending both the more expressive \"filename*\" parameter, and the \"filename\" parameter as fallback for legacy recipients.\n   */\n  get preferredFilename(): string | undefined {\n    let filenameSplat = this.filenameSplat\n    if (filenameSplat) {\n      let decodedFilename = decodeFilenameSplat(filenameSplat)\n      if (decodedFilename) return decodedFilename\n    }\n\n    return this.filename\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    if (!this.type) {\n      return ''\n    }\n\n    let parts = [this.type]\n\n    if (this.name) {\n      parts.push(`name=${quote(this.name)}`)\n    }\n    if (this.filename) {\n      parts.push(`filename=${quote(this.filename)}`)\n    }\n    if (this.filenameSplat) {\n      parts.push(`filename*=${quote(this.filenameSplat)}`)\n    }\n\n    return parts.join('; ')\n  }\n\n  /**\n   * Parse a Content-Disposition header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns A ContentDisposition instance (empty if null)\n   */\n  static from(value: string | ContentDispositionInit | null): ContentDisposition {\n    let header = new ContentDisposition()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        let params = parseParams(value)\n        if (params.length > 0) {\n          header.type = params[0][0]\n          for (let [name, val] of params.slice(1)) {\n            if (name === 'filename') {\n              header.filename = val\n            } else if (name === 'filename*') {\n              header.filenameSplat = val\n            } else if (name === 'name') {\n              header.name = val\n            }\n          }\n        }\n      } else {\n        header.filename = value.filename\n        header.filenameSplat = value.filenameSplat\n        header.name = value.name\n        header.type = value.type\n      }\n    }\n\n    return header\n  }\n}\n\nfunction decodeFilenameSplat(value: string): string | null {\n  let match = value.match(/^([\\w-]+)'([^']*)'(.+)$/)\n  if (!match) return null\n\n  let [, charset, , encodedFilename] = match\n\n  let decodedFilename = percentDecode(encodedFilename)\n\n  try {\n    let decoder = new TextDecoder(charset)\n    let bytes = new Uint8Array(decodedFilename.split('').map((char) => char.charCodeAt(0)))\n    return decoder.decode(bytes)\n  } catch (error) {\n    console.warn(`Failed to decode filename from charset ${charset}:`, error)\n    return decodedFilename\n  }\n}\n\nfunction percentDecode(value: string): string {\n  return value\n    .replace(/\\+/g, ' ')\n    .replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))\n}\n"
  },
  {
    "path": "packages/headers/src/lib/content-range.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { ContentRange } from './content-range.ts'\n\ndescribe('ContentRange', () => {\n  it('initializes with an empty string', () => {\n    let contentRange = new ContentRange('')\n    assert.equal(contentRange.unit, '')\n    assert.equal(contentRange.start, null)\n    assert.equal(contentRange.end, null)\n    assert.equal(contentRange.size, undefined)\n  })\n\n  it('initializes with a string (satisfied range)', () => {\n    let contentRange = new ContentRange('bytes 200-1000/67589')\n    assert.equal(contentRange.unit, 'bytes')\n    assert.equal(contentRange.start, 200)\n    assert.equal(contentRange.end, 1000)\n    assert.equal(contentRange.size, 67589)\n  })\n\n  it('initializes with a string (unsatisfied range)', () => {\n    let contentRange = new ContentRange('bytes */67589')\n    assert.equal(contentRange.unit, 'bytes')\n    assert.equal(contentRange.start, null)\n    assert.equal(contentRange.end, null)\n    assert.equal(contentRange.size, 67589)\n  })\n\n  it('initializes with a string (unknown size)', () => {\n    let contentRange = new ContentRange('bytes 0-999/*')\n    assert.equal(contentRange.unit, 'bytes')\n    assert.equal(contentRange.start, 0)\n    assert.equal(contentRange.end, 999)\n    assert.equal(contentRange.size, '*')\n  })\n\n  it('initializes with an object', () => {\n    let contentRange = new ContentRange({\n      unit: 'bytes',\n      start: 200,\n      end: 1000,\n      size: 67589,\n    })\n    assert.equal(contentRange.unit, 'bytes')\n    assert.equal(contentRange.start, 200)\n    assert.equal(contentRange.end, 1000)\n    assert.equal(contentRange.size, 67589)\n  })\n\n  it('initializes with another ContentRange', () => {\n    let contentRange1 = new ContentRange({\n      unit: 'bytes',\n      start: 200,\n      end: 1000,\n      size: 67589,\n    })\n    let contentRange2 = new ContentRange(contentRange1)\n    assert.equal(contentRange2.unit, 'bytes')\n    assert.equal(contentRange2.start, 200)\n    assert.equal(contentRange2.end, 1000)\n    assert.equal(contentRange2.size, 67589)\n  })\n\n  it('sets and gets unit', () => {\n    let contentRange = new ContentRange()\n    contentRange.unit = 'items'\n    assert.equal(contentRange.unit, 'items')\n  })\n\n  it('sets and gets start', () => {\n    let contentRange = new ContentRange()\n    contentRange.start = 100\n    assert.equal(contentRange.start, 100)\n  })\n\n  it('sets and gets end', () => {\n    let contentRange = new ContentRange()\n    contentRange.end = 500\n    assert.equal(contentRange.end, 500)\n  })\n\n  it('sets and gets size', () => {\n    let contentRange = new ContentRange()\n    contentRange.size = 1000\n    assert.equal(contentRange.size, 1000)\n  })\n\n  it('converts to string correctly (satisfied range)', () => {\n    let contentRange = new ContentRange({\n      unit: 'bytes',\n      start: 200,\n      end: 1000,\n      size: 67589,\n    })\n    assert.equal(contentRange.toString(), 'bytes 200-1000/67589')\n  })\n\n  it('converts to string correctly (unsatisfied range)', () => {\n    let contentRange = new ContentRange({\n      unit: 'bytes',\n      start: null,\n      end: null,\n      size: 67589,\n    })\n    assert.equal(contentRange.toString(), 'bytes */67589')\n  })\n\n  it('converts to string correctly (unknown size)', () => {\n    let contentRange = new ContentRange({\n      unit: 'bytes',\n      start: 0,\n      end: 999,\n      size: '*',\n    })\n    assert.equal(contentRange.toString(), 'bytes 0-999/*')\n  })\n\n  it('converts to an empty string when unit is not set', () => {\n    let contentRange = new ContentRange()\n    contentRange.unit = ''\n    assert.equal(contentRange.toString(), '')\n  })\n\n  it('converts to an empty string when size is not set', () => {\n    let contentRange = new ContentRange()\n    contentRange.unit = 'bytes'\n    contentRange.start = 0\n    contentRange.end = 999\n    assert.equal(contentRange.toString(), '')\n  })\n\n  it('handles partial range with start only', () => {\n    let contentRange = new ContentRange({\n      unit: 'bytes',\n      start: 500,\n      end: null,\n      size: 1000,\n    })\n    assert.equal(contentRange.toString(), 'bytes */1000')\n  })\n\n  it('handles partial range with end only', () => {\n    let contentRange = new ContentRange({\n      unit: 'bytes',\n      start: null,\n      end: 999,\n      size: 1000,\n    })\n    assert.equal(contentRange.toString(), 'bytes */1000')\n  })\n\n  it('handles zero-based ranges', () => {\n    let contentRange = new ContentRange('bytes 0-0/1')\n    assert.equal(contentRange.start, 0)\n    assert.equal(contentRange.end, 0)\n    assert.equal(contentRange.size, 1)\n    assert.equal(contentRange.toString(), 'bytes 0-0/1')\n  })\n})\n\ndescribe('ContentRange.from', () => {\n  it('parses a string value', () => {\n    let result = ContentRange.from('bytes 0-499/1234')\n    assert.ok(result instanceof ContentRange)\n    assert.equal(result.unit, 'bytes')\n    assert.equal(result.start, 0)\n    assert.equal(result.end, 499)\n    assert.equal(result.size, 1234)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/content-range.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\n\n/**\n * Initializer for a `Content-Range` header value.\n */\nexport interface ContentRangeInit {\n  /**\n   * The unit of the range, typically \"bytes\"\n   */\n  unit?: string\n  /**\n   * The start position of the range (inclusive)\n   * Set to null for unsatisfied ranges\n   */\n  start?: number | null\n  /**\n   * The end position of the range (inclusive)\n   * Set to null for unsatisfied ranges\n   */\n  end?: number | null\n  /**\n   * The total size of the resource\n   * Set to '*' for unknown size\n   */\n  size?: number | '*'\n}\n\n/**\n * The value of a `Content-Range` HTTP header.\n *\n * [MDN `Content-Range` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range)\n *\n * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.content-range)\n */\nexport class ContentRange implements HeaderValue, ContentRangeInit {\n  /**\n   * The range unit, typically `bytes`.\n   */\n  unit: string = ''\n\n  /**\n   * The inclusive start offset, or `null` for unsatisfied ranges.\n   */\n  start: number | null = null\n\n  /**\n   * The inclusive end offset, or `null` for unsatisfied ranges.\n   */\n  end: number | null = null\n\n  /**\n   * The total resource size, or `'*'` when unknown.\n   */\n  size?: number | '*'\n\n  constructor(init?: string | ContentRangeInit) {\n    if (init) return ContentRange.from(init)\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    if (!this.unit || this.size === undefined) return ''\n\n    let range = this.start !== null && this.end !== null ? `${this.start}-${this.end}` : '*'\n\n    return `${this.unit} ${range}/${this.size}`\n  }\n\n  /**\n   * Parse a Content-Range header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns A ContentRange instance (empty if null)\n   */\n  static from(value: string | ContentRangeInit | null): ContentRange {\n    let header = new ContentRange()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        // Parse: \"bytes 200-1000/67589\" or \"bytes */67589\" or \"bytes 200-1000/*\"\n        let match = value.match(/^(\\w+)\\s+(?:(\\d+)-(\\d+)|\\*)\\/((?:\\d+|\\*))$/)\n        if (match) {\n          header.unit = match[1]\n          header.start = match[2] ? parseInt(match[2], 10) : null\n          header.end = match[3] ? parseInt(match[3], 10) : null\n          header.size = match[4] === '*' ? '*' : parseInt(match[4], 10)\n        }\n      } else {\n        if (value.unit !== undefined) header.unit = value.unit\n        if (value.start !== undefined) header.start = value.start\n        if (value.end !== undefined) header.end = value.end\n        if (value.size !== undefined) header.size = value.size\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/content-type.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { ContentType } from './content-type.ts'\n\ndescribe('ContentType', () => {\n  it('initializes with an empty string', () => {\n    let header = new ContentType('')\n    assert.equal(header.mediaType, undefined)\n    assert.equal(header.charset, undefined)\n  })\n\n  it('initializes with a string', () => {\n    let header = new ContentType('text/plain; charset=utf-8')\n    assert.equal(header.mediaType, 'text/plain')\n    assert.equal(header.charset, 'utf-8')\n  })\n\n  it('initializes with an object', () => {\n    let header = new ContentType({ mediaType: 'text/plain', charset: 'utf-8' })\n    assert.equal(header.mediaType, 'text/plain')\n    assert.equal(header.charset, 'utf-8')\n  })\n\n  it('initializes with another ContentType', () => {\n    let header = new ContentType(new ContentType('text/plain; charset=utf-8'))\n    assert.equal(header.mediaType, 'text/plain')\n    assert.equal(header.charset, 'utf-8')\n  })\n\n  it('handles whitespace in initial value', () => {\n    let header = new ContentType(' text/html ;  charset = iso-8859-1 ')\n    assert.equal(header.mediaType, 'text/html')\n    assert.equal(header.charset, 'iso-8859-1')\n  })\n\n  it('sets and gets media type', () => {\n    let header = new ContentType('text/plain')\n    header.mediaType = 'application/json'\n    assert.equal(header.mediaType, 'application/json')\n  })\n\n  it('sets and gets charset', () => {\n    let header = new ContentType('text/plain')\n    header.charset = 'utf-8'\n    assert.equal(header.charset, 'utf-8')\n  })\n\n  it('sets and gets boundary', () => {\n    let header = new ContentType('multipart/form-data')\n    header.boundary = 'abc123'\n    assert.equal(header.boundary, 'abc123')\n  })\n\n  it('handles quoted attribute values', () => {\n    let header = new ContentType('text/plain; charset=\"us-ascii\"')\n    assert.equal(header.charset, 'us-ascii')\n  })\n\n  it('converts to string correctly', () => {\n    let header = new ContentType('text/plain; charset=utf-8')\n    assert.equal(header.toString(), 'text/plain; charset=utf-8')\n  })\n\n  it('converts to an empty string when media type is not set', () => {\n    let header = new ContentType()\n    header.charset = 'utf-8'\n    assert.equal(header.toString(), '')\n  })\n\n  it('handles multiple attributes', () => {\n    let header = new ContentType('multipart/form-data; boundary=\"abc123\"; charset=utf-8')\n    assert.equal(header.mediaType, 'multipart/form-data')\n    assert.equal(header.boundary, 'abc123')\n    assert.equal(header.charset, 'utf-8')\n  })\n\n  it('preserves case for media type', () => {\n    let header = new ContentType('Text/HTML')\n    assert.equal(header.mediaType, 'Text/HTML')\n  })\n\n  it('handles attribute values with special characters', () => {\n    let header = new ContentType('multipart/form-data; boundary=\"---=_Part_0_1234567.89\"')\n    assert.equal(header.boundary, '---=_Part_0_1234567.89')\n  })\n\n  it('correctly quotes attribute values in toString()', () => {\n    let header = new ContentType('multipart/form-data')\n    header.boundary = 'abc 123'\n    assert.equal(header.toString(), 'multipart/form-data; boundary=\"abc 123\"')\n  })\n\n  it('handles empty attribute values', () => {\n    let header = new ContentType('text/plain; charset=')\n    assert.equal(header.charset, '')\n  })\n\n  it('ignores attributes without values', () => {\n    let header = new ContentType('text/plain; charset')\n    assert.equal(header.charset, undefined)\n  })\n\n  it('preserves order of attributes in toString()', () => {\n    let header = new ContentType('multipart/form-data; charset=utf-8; boundary=abc123')\n    assert.equal(header.toString(), 'multipart/form-data; charset=utf-8; boundary=abc123')\n  })\n})\n\ndescribe('ContentType.from', () => {\n  it('parses a string value', () => {\n    let result = ContentType.from('text/html; charset=utf-8')\n    assert.ok(result instanceof ContentType)\n    assert.equal(result.mediaType, 'text/html')\n    assert.equal(result.charset, 'utf-8')\n  })\n\n  it('accepts init object', () => {\n    let result = ContentType.from({ mediaType: 'text/html', charset: 'utf-8' })\n    assert.ok(result instanceof ContentType)\n    assert.equal(result.mediaType, 'text/html')\n    assert.equal(result.charset, 'utf-8')\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/content-type.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { parseParams, quote } from './param-values.ts'\n\n/**\n * Initializer for a `Content-Type` header value.\n */\nexport interface ContentTypeInit {\n  /**\n   * For multipart entities, the boundary that separates the different parts of the message.\n   */\n  boundary?: string\n  /**\n   * Indicates the [character encoding](https://developer.mozilla.org/en-US/docs/Glossary/Character_encoding) of the content.\n   *\n   * For example, `utf-8`, `iso-8859-1`.\n   */\n  charset?: string\n  /**\n   * The media type (or MIME type) of the content. This consists of a type and subtype, separated by a slash.\n   *\n   * For example, `text/html`, `application/json`, `image/png`.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)\n   */\n  mediaType?: string\n}\n\n/**\n * The value of a `Content-Type` HTTP header.\n *\n * [MDN `Content-Type` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5)\n */\nexport class ContentType implements HeaderValue, ContentTypeInit {\n  /**\n   * Multipart boundary parameter value.\n   */\n  boundary?: string\n\n  /**\n   * Character set parameter value.\n   */\n  charset?: string\n\n  /**\n   * Media type such as `text/html` or `application/json`.\n   */\n  mediaType?: string\n\n  constructor(init?: string | ContentTypeInit) {\n    if (init) return ContentType.from(init)\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    if (!this.mediaType) {\n      return ''\n    }\n\n    let parts = [this.mediaType]\n\n    if (this.charset) {\n      parts.push(`charset=${quote(this.charset)}`)\n    }\n    if (this.boundary) {\n      parts.push(`boundary=${quote(this.boundary)}`)\n    }\n\n    return parts.join('; ')\n  }\n\n  /**\n   * Parse a Content-Type header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns A ContentType instance (empty if null)\n   */\n  static from(value: string | ContentTypeInit | null): ContentType {\n    let header = new ContentType()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        let params = parseParams(value)\n        if (params.length > 0) {\n          header.mediaType = params[0][0]\n          for (let [name, val] of params.slice(1)) {\n            if (name === 'boundary') {\n              header.boundary = val\n            } else if (name === 'charset') {\n              header.charset = val\n            }\n          }\n        }\n      } else {\n        header.boundary = value.boundary\n        header.charset = value.charset\n        header.mediaType = value.mediaType\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/cookie.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { Cookie } from './cookie.ts'\n\ndescribe('Cookie', () => {\n  it('initializes with an empty string', () => {\n    let header = new Cookie('')\n    assert.equal(header.size, 0)\n  })\n\n  it('initializes with a string', () => {\n    let header = new Cookie('name1=value1; name2=value2')\n    assert.equal(header.get('name1'), 'value1')\n    assert.equal(header.get('name2'), 'value2')\n  })\n\n  it('initializes with an array', () => {\n    let header = new Cookie([\n      ['name1', 'value1'],\n      ['name2', 'value2'],\n    ])\n    assert.equal(header.get('name1'), 'value1')\n    assert.equal(header.get('name2'), 'value2')\n  })\n\n  it('initializes with an object', () => {\n    let header = new Cookie({ name1: 'value1', name2: 'value2' })\n    assert.equal(header.get('name1'), 'value1')\n    assert.equal(header.get('name2'), 'value2')\n  })\n\n  it('initializes with another Cookie', () => {\n    let header = new Cookie(new Cookie('name1=value1; name2=value2'))\n    assert.equal(header.get('name1'), 'value1')\n    assert.equal(header.get('name2'), 'value2')\n  })\n\n  it('handles whitespace in initial value', () => {\n    let header = new Cookie(' name1 = value1 ;  name2  =  value2 ')\n    assert.equal(header.get('name1'), 'value1')\n    assert.equal(header.get('name2'), 'value2')\n  })\n\n  it('gets all names', () => {\n    let header = new Cookie('name1=value1; name2=value2')\n    assert.deepEqual(header.names, ['name1', 'name2'])\n  })\n\n  it('gets all values', () => {\n    let header = new Cookie('name1=value1; name2=value2')\n    assert.deepEqual(header.values, ['value1', 'value2'])\n  })\n\n  it('sets and gets values', () => {\n    let header = new Cookie()\n    header.set('name', 'value')\n    assert.equal(header.get('name'), 'value')\n  })\n\n  it('returns `null` for nonexistent values', () => {\n    let header = new Cookie()\n    assert.equal(header.get('name'), null)\n  })\n\n  it('deletes values', () => {\n    let header = new Cookie('name=value')\n    assert.equal(header.has('name'), true)\n    header.delete('name')\n    assert.equal(header.has('name'), false)\n  })\n\n  it('checks if value exists', () => {\n    let header = new Cookie('name=value')\n    assert.equal(header.has('name'), true)\n    assert.equal(header.has('nonexistent'), false)\n  })\n\n  it('clears all values', () => {\n    let header = new Cookie('name1=value1; name2=value2')\n    assert.equal(header.size, 2)\n    header.clear()\n    assert.equal(header.size, 0)\n  })\n\n  it('iterates over entries', () => {\n    let header = new Cookie('name1=value1; name2=value2')\n    let entries = Array.from(header.entries())\n    assert.deepEqual(entries, [\n      ['name1', 'value1'],\n      ['name2', 'value2'],\n    ])\n  })\n\n  it('uses forEach correctly', () => {\n    let header = new Cookie('name1=value1; name2=value2')\n    let result: [string, string][] = []\n    header.forEach((name, value) => {\n      result.push([name, value])\n    })\n    assert.deepEqual(result, [\n      ['name1', 'value1'],\n      ['name2', 'value2'],\n    ])\n  })\n\n  it('returns correct size', () => {\n    let header = new Cookie('name1=value1; name2=value2')\n    assert.equal(header.size, 2)\n  })\n\n  it('converts to string correctly', () => {\n    let header = new Cookie('name1=value1; name2=value2')\n    assert.equal(header.toString(), 'name1=value1; name2=value2')\n  })\n\n  it('is directly iterable', () => {\n    let header = new Cookie('name1=value1; name2=value2')\n    let entries = Array.from(header)\n    assert.deepEqual(entries, [\n      ['name1', 'value1'],\n      ['name2', 'value2'],\n    ])\n  })\n\n  it('handles cookies without values', () => {\n    let header = new Cookie('name1=value1; name2')\n    assert.equal(header.get('name1'), 'value1')\n    assert.equal(header.get('name2'), '')\n  })\n\n  it('handles setting empty values', () => {\n    let header = new Cookie('')\n    header.set('name', '')\n    assert.equal(header.get('name'), '')\n    assert.equal(header.toString(), 'name=')\n  })\n\n  it('overwrites existing values', () => {\n    let header = new Cookie('name=value1')\n    header.set('name', 'value2')\n    assert.equal(header.get('name'), 'value2')\n  })\n})\n\ndescribe('Cookie.from', () => {\n  it('parses a string value', () => {\n    let result = Cookie.from('session=abc123; user=john')\n    assert.ok(result instanceof Cookie)\n    assert.equal(result.get('session'), 'abc123')\n    assert.equal(result.get('user'), 'john')\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/cookie.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { parseParams, quote } from './param-values.ts'\nimport { isIterable } from './utils.ts'\n\n/**\n * Initializer for a {@link Cookie} header value.\n */\nexport type CookieInit = Iterable<[string, string]> | Record<string, string>\n\n/**\n * The value of a `Cookie` HTTP header.\n *\n * [MDN `Cookie` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc6265#section-4.2)\n */\nexport class Cookie implements HeaderValue, Iterable<[string, string]> {\n  #map!: Map<string, string>\n\n  constructor(init?: string | CookieInit) {\n    if (init) return Cookie.from(init)\n    this.#map = new Map()\n  }\n\n  /**\n   * An array of the names of the cookies in the header.\n   */\n  get names(): string[] {\n    return Array.from(this.#map.keys())\n  }\n\n  /**\n   * An array of the values of the cookies in the header.\n   */\n  get values(): string[] {\n    return Array.from(this.#map.values())\n  }\n\n  /**\n   * The number of cookies in the header.\n   */\n  get size(): number {\n    return this.#map.size\n  }\n\n  /**\n   * Gets the value of a cookie with the given name from the header.\n   *\n   * @param name The name of the cookie\n   * @returns The value of the cookie, or `null` if the cookie does not exist\n   */\n  get(name: string): string | null {\n    return this.#map.get(name) ?? null\n  }\n\n  /**\n   * Sets a cookie with the given name and value in the header.\n   *\n   * @param name The name of the cookie\n   * @param value The value of the cookie\n   */\n  set(name: string, value: string): void {\n    this.#map.set(name, value)\n  }\n\n  /**\n   * Removes a cookie with the given name from the header.\n   *\n   * @param name The name of the cookie\n   */\n  delete(name: string): void {\n    this.#map.delete(name)\n  }\n\n  /**\n   * True if a cookie with the given name exists in the header.\n   *\n   * @param name The name of the cookie\n   * @returns `true` if a cookie with the given name exists in the header\n   */\n  has(name: string): boolean {\n    return this.#map.has(name)\n  }\n\n  /**\n   * Removes all cookies from the header.\n   */\n  clear(): void {\n    this.#map.clear()\n  }\n\n  /**\n   * Returns an iterator of all cookie name and value pairs.\n   *\n   * @returns An iterator of `[name, value]` tuples\n   */\n  entries(): IterableIterator<[string, string]> {\n    return this.#map.entries()\n  }\n\n  /**\n   * Iterates over cookie name and value pairs.\n   *\n   * @returns An iterator of `[name, value]` tuples.\n   */\n  [Symbol.iterator](): IterableIterator<[string, string]> {\n    return this.entries()\n  }\n\n  /**\n   * Invokes the callback for each cookie name and value pair.\n   *\n   * @param callback The function to call for each pair\n   * @param thisArg The value to use as `this` when calling the callback\n   */\n  forEach(callback: (name: string, value: string, header: Cookie) => void, thisArg?: any): void {\n    for (let [name, value] of this) {\n      callback.call(thisArg, name, value, this)\n    }\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    let pairs: string[] = []\n\n    for (let [name, value] of this.#map) {\n      pairs.push(`${name}=${quote(value)}`)\n    }\n\n    return pairs.join('; ')\n  }\n\n  /**\n   * Parse a Cookie header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns A Cookie instance (empty if null)\n   */\n  static from(value: string | CookieInit | null): Cookie {\n    let header = new Cookie()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        let params = parseParams(value)\n        for (let [name, val] of params) {\n          header.#map.set(name, val ?? '')\n        }\n      } else if (isIterable(value)) {\n        for (let [name, val] of value) {\n          header.#map.set(name, val)\n        }\n      } else {\n        for (let name of Object.getOwnPropertyNames(value)) {\n          header.#map.set(name, value[name])\n        }\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/header-names.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { canonicalHeaderName } from './header-names.ts'\n\ndescribe('normalizeHeaderName', () => {\n  it('handles common headers correctly', () => {\n    assert.equal(canonicalHeaderName('content-type'), 'Content-Type')\n    assert.equal(canonicalHeaderName('content-length'), 'Content-Length')\n    assert.equal(canonicalHeaderName('user-agent'), 'User-Agent')\n    assert.equal(canonicalHeaderName('accept'), 'Accept')\n  })\n\n  it('handles special case headers correctly', () => {\n    assert.equal(canonicalHeaderName('etag'), 'ETag')\n    assert.equal(canonicalHeaderName('www-authenticate'), 'WWW-Authenticate')\n    assert.equal(canonicalHeaderName('x-forwarded-for'), 'X-Forwarded-For')\n    assert.equal(canonicalHeaderName('x-xss-protection'), 'X-XSS-Protection')\n    assert.equal(canonicalHeaderName('te'), 'TE')\n    assert.equal(canonicalHeaderName('expect-ct'), 'Expect-CT')\n  })\n\n  it('normalizes mixed-case input', () => {\n    assert.equal(canonicalHeaderName('CoNtEnT-TyPe'), 'Content-Type')\n    assert.equal(canonicalHeaderName('x-FoRwArDeD-fOr'), 'X-Forwarded-For')\n  })\n\n  it('handles single-word headers', () => {\n    assert.equal(canonicalHeaderName('authorization'), 'Authorization')\n    assert.equal(canonicalHeaderName('host'), 'Host')\n  })\n\n  it('normalizes other common HTTP headers', () => {\n    assert.equal(canonicalHeaderName('accept-charset'), 'Accept-Charset')\n    assert.equal(canonicalHeaderName('accept-encoding'), 'Accept-Encoding')\n    assert.equal(canonicalHeaderName('accept-language'), 'Accept-Language')\n    assert.equal(canonicalHeaderName('cache-control'), 'Cache-Control')\n    assert.equal(canonicalHeaderName('connection'), 'Connection')\n    assert.equal(canonicalHeaderName('cookie'), 'Cookie')\n    assert.equal(canonicalHeaderName('date'), 'Date')\n    assert.equal(canonicalHeaderName('expect'), 'Expect')\n    assert.equal(canonicalHeaderName('forwarded'), 'Forwarded')\n    assert.equal(canonicalHeaderName('from'), 'From')\n    assert.equal(canonicalHeaderName('if-match'), 'If-Match')\n    assert.equal(canonicalHeaderName('if-modified-since'), 'If-Modified-Since')\n    assert.equal(canonicalHeaderName('if-none-match'), 'If-None-Match')\n    assert.equal(canonicalHeaderName('if-range'), 'If-Range')\n    assert.equal(canonicalHeaderName('if-unmodified-since'), 'If-Unmodified-Since')\n    assert.equal(canonicalHeaderName('max-forwards'), 'Max-Forwards')\n    assert.equal(canonicalHeaderName('origin'), 'Origin')\n    assert.equal(canonicalHeaderName('pragma'), 'Pragma')\n    assert.equal(canonicalHeaderName('proxy-authorization'), 'Proxy-Authorization')\n    assert.equal(canonicalHeaderName('range'), 'Range')\n    assert.equal(canonicalHeaderName('referer'), 'Referer')\n    assert.equal(canonicalHeaderName('server'), 'Server')\n    assert.equal(canonicalHeaderName('transfer-encoding'), 'Transfer-Encoding')\n    assert.equal(canonicalHeaderName('upgrade'), 'Upgrade')\n    assert.equal(canonicalHeaderName('via'), 'Via')\n    assert.equal(canonicalHeaderName('warning'), 'Warning')\n    assert.equal(canonicalHeaderName('alt-svc'), 'Alt-Svc')\n    assert.equal(canonicalHeaderName('content-disposition'), 'Content-Disposition')\n    assert.equal(canonicalHeaderName('content-encoding'), 'Content-Encoding')\n    assert.equal(canonicalHeaderName('content-language'), 'Content-Language')\n    assert.equal(canonicalHeaderName('content-location'), 'Content-Location')\n    assert.equal(canonicalHeaderName('content-range'), 'Content-Range')\n    assert.equal(canonicalHeaderName('link'), 'Link')\n    assert.equal(canonicalHeaderName('location'), 'Location')\n    assert.equal(canonicalHeaderName('retry-after'), 'Retry-After')\n    assert.equal(canonicalHeaderName('strict-transport-security'), 'Strict-Transport-Security')\n    assert.equal(canonicalHeaderName('vary'), 'Vary')\n  })\n\n  it('handles custom X- headers', () => {\n    assert.equal(canonicalHeaderName('x-custom-header'), 'X-Custom-Header')\n    assert.equal(canonicalHeaderName('x-requested-with'), 'X-Requested-With')\n  })\n\n  it('preserves casing for unknown acronyms', () => {\n    assert.equal(canonicalHeaderName('x-csrf-token'), 'X-Csrf-Token')\n    assert.equal(canonicalHeaderName('x-api-key'), 'X-Api-Key')\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/header-names.ts",
    "content": "const HeaderWordCasingExceptions: Record<string, string> = {\n  ct: 'CT',\n  etag: 'ETag',\n  te: 'TE',\n  www: 'WWW',\n  x: 'X',\n  xss: 'XSS',\n}\n\nexport function canonicalHeaderName(name: string): string {\n  return name\n    .toLowerCase()\n    .split('-')\n    .map((word) => HeaderWordCasingExceptions[word] || word.charAt(0).toUpperCase() + word.slice(1))\n    .join('-')\n}\n"
  },
  {
    "path": "packages/headers/src/lib/header-value.ts",
    "content": "export interface HeaderValue {\n  toString(): string\n}\n"
  },
  {
    "path": "packages/headers/src/lib/if-match.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { IfMatch } from './if-match.ts'\n\ndescribe('IfMatch', () => {\n  it('initializes with an empty string', () => {\n    let header = new IfMatch('')\n    assert.deepEqual(header.tags, [])\n  })\n\n  it('initializes with a string with a single tag', () => {\n    let header = new IfMatch('67ab43')\n    assert.deepEqual(header.tags, ['\"67ab43\"'])\n\n    let header2 = new IfMatch('\"67ab43\"')\n    assert.deepEqual(header2.tags, ['\"67ab43\"'])\n\n    let header3 = new IfMatch('W/\"67ab43\"')\n    assert.deepEqual(header3.tags, ['W/\"67ab43\"'])\n  })\n\n  it('initializes with a string with multiple tags', () => {\n    let header = new IfMatch('67ab43, 54ed21')\n    assert.deepEqual(header.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header2 = new IfMatch('\"67ab43\", \"54ed21\"')\n    assert.deepEqual(header2.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header3 = new IfMatch('W/\"67ab43\", \"54ed21\"')\n    assert.deepEqual(header3.tags, ['W/\"67ab43\"', '\"54ed21\"'])\n  })\n\n  it('initializes with an array of tags', () => {\n    let header = new IfMatch(['67ab43', '54ed21'])\n    assert.deepEqual(header.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header2 = new IfMatch(['\"67ab43\"', '\"54ed21\"'])\n    assert.deepEqual(header2.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header3 = new IfMatch(['W/\"67ab43\"', '\"54ed21\"'])\n    assert.deepEqual(header3.tags, ['W/\"67ab43\"', '\"54ed21\"'])\n  })\n\n  it('initializes with an object', () => {\n    let header = new IfMatch({ tags: ['67ab43', '54ed21'] })\n    assert.deepEqual(header.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header2 = new IfMatch({ tags: ['\"67ab43\"', '\"54ed21\"'] })\n    assert.deepEqual(header2.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header3 = new IfMatch({ tags: ['W/\"67ab43\"', '\"54ed21\"'] })\n    assert.deepEqual(header3.tags, ['W/\"67ab43\"', '\"54ed21\"'])\n  })\n\n  it('initializes with another IfMatch', () => {\n    let header = new IfMatch(new IfMatch('67ab43, 54ed21'))\n    assert.deepEqual(header.tags, ['\"67ab43\"', '\"54ed21\"'])\n  })\n\n  it('converts to a string', () => {\n    let header = new IfMatch('W/\"67ab43\", \"54ed21\"')\n    assert.equal(header.toString(), 'W/\"67ab43\", \"54ed21\"')\n  })\n\n  describe('has()', () => {\n    it('checks if a tag is present', () => {\n      let header = new IfMatch('67ab43, 54ed21')\n      assert.ok(header.has('\"67ab43\"'))\n      assert.ok(header.has('\"54ed21\"'))\n      assert.ok(!header.has('\"7892dd\"'))\n      assert.ok(!header.has('*'))\n\n      let header2 = new IfMatch('W/\"67ab43\", \"54ed21\"')\n      assert.ok(header2.has('W/\"67ab43\"'))\n      assert.ok(header2.has('\"54ed21\"'))\n      assert.ok(!header2.has('\"7892dd\"'))\n    })\n  })\n\n  describe('matches()', () => {\n    it('returns true when header is not present', () => {\n      let emptyHeader = new IfMatch()\n      assert.ok(emptyHeader.matches('\"67ab43\"'))\n    })\n\n    it('returns true when header is present and matches', () => {\n      let matchingHeader = new IfMatch('67ab43, 54ed21')\n      assert.ok(matchingHeader.matches('\"67ab43\"'))\n      assert.ok(matchingHeader.matches('\"54ed21\"'))\n    })\n\n    it('returns false when header is present but does not match', () => {\n      let matchingHeader = new IfMatch('67ab43, 54ed21')\n      assert.ok(!matchingHeader.matches('\"7892dd\"'))\n    })\n\n    it('returns true when wildcard is present', () => {\n      let wildcardHeader = new IfMatch('*')\n      assert.ok(wildcardHeader.matches('\"67ab43\"'))\n      assert.ok(wildcardHeader.matches('\"anything\"'))\n    })\n\n    describe('ETag handling', () => {\n      it('returns false when resource has weak tag', () => {\n        let header = new IfMatch('67ab43')\n        assert.ok(!header.matches('W/\"67ab43\"'))\n      })\n\n      it('returns false when If-Match header has weak tag', () => {\n        let header = new IfMatch('W/\"67ab43\"')\n        assert.ok(!header.matches('\"67ab43\"'))\n      })\n\n      it('returns false when both resource and If-Match header have weak tags', () => {\n        let header = new IfMatch('W/\"67ab43\"')\n        assert.ok(!header.matches('W/\"67ab43\"'))\n      })\n\n      it('returns true when both resource and If-Match header have strong tags', () => {\n        let header = new IfMatch('\"67ab43\"')\n        assert.ok(header.matches('\"67ab43\"'))\n      })\n\n      it('returns false when If-Match has mix of weak and strong tags and resource is weak', () => {\n        let header = new IfMatch('W/\"67ab43\", \"54ed21\"')\n        assert.ok(!header.matches('W/\"67ab43\"'))\n      })\n\n      it('returns true when If-Match has mix of weak and strong tags and resource matches strong tag', () => {\n        let header = new IfMatch('W/\"67ab43\", \"54ed21\"')\n        assert.ok(header.matches('\"54ed21\"'))\n      })\n    })\n  })\n})\n\ndescribe('IfMatch.from', () => {\n  it('parses a string value', () => {\n    let result = IfMatch.from('\"abc\", \"def\"')\n    assert.ok(result instanceof IfMatch)\n    assert.equal(result.tags.length, 2)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/if-match.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { quoteEtag } from './utils.ts'\n\n/**\n * Initializer for an `If-Match` header value.\n */\nexport interface IfMatchInit {\n  /**\n   * The entity tags to compare against the current entity.\n   */\n  tags: string[]\n}\n\n/**\n * The value of an `If-Match` HTTP header.\n *\n * [MDN `If-Match` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.1)\n */\nexport class IfMatch implements HeaderValue, IfMatchInit {\n  /**\n   * Entity tags carried by the header.\n   */\n  tags: string[] = []\n\n  constructor(init?: string | string[] | IfMatchInit) {\n    if (init) return IfMatch.from(init)\n  }\n\n  /**\n   * Checks if the header contains the given entity tag.\n   *\n   * Note: This method checks only for exact matches and does not consider wildcards.\n   *\n   * @param tag The entity tag to check for\n   * @returns `true` if the tag is present in the header, `false` otherwise\n   */\n  has(tag: string): boolean {\n    return this.tags.includes(quoteEtag(tag))\n  }\n\n  /**\n   * Checks if the precondition passes for the given entity tag.\n   *\n   * This method always returns `true` if the `If-Match` header is not present\n   * since the precondition passes regardless of the entity tag being checked.\n   *\n   * Uses strong comparison as per RFC 9110, meaning weak entity tags (prefixed with `W/`)\n   * will never match.\n   *\n   * @param tag The entity tag to check against\n   * @returns `true` if the precondition passes, `false` if it fails (should return 412)\n   */\n  matches(tag: string): boolean {\n    if (this.tags.length === 0) {\n      return true\n    }\n\n    // Wildcard always matches (regardless of weak/strong)\n    if (this.tags.includes('*')) {\n      return true\n    }\n\n    let normalizedTag = quoteEtag(tag)\n\n    // Weak tags never match in If-Match (strong comparison only)\n    if (normalizedTag.startsWith('W/')) {\n      return false\n    }\n\n    // Only match against strong tags in the header\n    for (let headerTag of this.tags) {\n      if (!headerTag.startsWith('W/') && headerTag === normalizedTag) {\n        return true\n      }\n    }\n\n    return false\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString() {\n    return this.tags.join(', ')\n  }\n\n  /**\n   * Parse an If-Match header value.\n   *\n   * @param value The header value (string, string[], init object, or null)\n   * @returns An IfMatch instance (empty if null)\n   */\n  static from(value: string | string[] | IfMatchInit | null): IfMatch {\n    let header = new IfMatch()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        header.tags.push(...value.split(/\\s*,\\s*/).map(quoteEtag))\n      } else if (Array.isArray(value)) {\n        header.tags.push(...value.map(quoteEtag))\n      } else {\n        header.tags.push(...value.tags.map(quoteEtag))\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/if-none-match.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { IfNoneMatch } from './if-none-match.ts'\n\ndescribe('IfNoneMatch', () => {\n  it('initializes with an empty string', () => {\n    let header = new IfNoneMatch('')\n    assert.deepEqual(header.tags, [])\n  })\n\n  it('initializes with a string with a single tag', () => {\n    let header = new IfNoneMatch('67ab43')\n    assert.deepEqual(header.tags, ['\"67ab43\"'])\n\n    let header2 = new IfNoneMatch('\"67ab43\"')\n    assert.deepEqual(header2.tags, ['\"67ab43\"'])\n\n    let header3 = new IfNoneMatch('W/\"67ab43\"')\n    assert.deepEqual(header3.tags, ['W/\"67ab43\"'])\n  })\n\n  it('initializes with a string with multiple tags', () => {\n    let header = new IfNoneMatch('67ab43, 54ed21')\n    assert.deepEqual(header.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header2 = new IfNoneMatch('\"67ab43\", \"54ed21\"')\n    assert.deepEqual(header2.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header3 = new IfNoneMatch('W/\"67ab43\", \"54ed21\"')\n    assert.deepEqual(header3.tags, ['W/\"67ab43\"', '\"54ed21\"'])\n  })\n\n  it('initializes with an array of tags', () => {\n    let header = new IfNoneMatch(['67ab43', '54ed21'])\n    assert.deepEqual(header.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header2 = new IfNoneMatch(['\"67ab43\"', '\"54ed21\"'])\n    assert.deepEqual(header2.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header3 = new IfNoneMatch(['W/\"67ab43\"', '\"54ed21\"'])\n    assert.deepEqual(header3.tags, ['W/\"67ab43\"', '\"54ed21\"'])\n  })\n\n  it('initializes with an object', () => {\n    let header = new IfNoneMatch({ tags: ['67ab43', '54ed21'] })\n    assert.deepEqual(header.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header2 = new IfNoneMatch({ tags: ['\"67ab43\"', '\"54ed21\"'] })\n    assert.deepEqual(header2.tags, ['\"67ab43\"', '\"54ed21\"'])\n\n    let header3 = new IfNoneMatch({ tags: ['W/\"67ab43\"', '\"54ed21\"'] })\n    assert.deepEqual(header3.tags, ['W/\"67ab43\"', '\"54ed21\"'])\n  })\n\n  it('initializes with another IfNoneMatch', () => {\n    let header = new IfNoneMatch(new IfNoneMatch('67ab43, 54ed21'))\n    assert.deepEqual(header.tags, ['\"67ab43\"', '\"54ed21\"'])\n  })\n\n  it('checks if a tag is present', () => {\n    let header = new IfNoneMatch('67ab43, 54ed21')\n    assert.ok(header.has('\"67ab43\"'))\n    assert.ok(header.has('\"54ed21\"'))\n    assert.ok(!header.has('\"7892dd\"'))\n    assert.ok(!header.has('*'))\n\n    let header2 = new IfNoneMatch('W/\"67ab43\", \"54ed21\"')\n    assert.ok(header2.has('W/\"67ab43\"'))\n    assert.ok(header2.has('\"54ed21\"'))\n    assert.ok(!header2.has('\"7892dd\"'))\n  })\n\n  it('checks if a tag matches', () => {\n    let header = new IfNoneMatch('67ab43, 54ed21')\n    assert.ok(header.matches('\"67ab43\"'))\n    assert.ok(header.matches('\"54ed21\"'))\n    assert.ok(!header.matches('\"7892dd\"'))\n\n    let header2 = new IfNoneMatch('W/\"67ab43\", \"54ed21\"')\n    assert.ok(header2.matches('W/\"67ab43\"'))\n    assert.ok(header2.matches('\"54ed21\"'))\n    assert.ok(!header2.matches('\"7892dd\"'))\n\n    let header3 = new IfNoneMatch('*')\n    assert.ok(header3.matches('\"67ab43\"'))\n    assert.ok(header3.matches('\"54ed21\"'))\n  })\n\n  it('converts to a string', () => {\n    let header = new IfNoneMatch('W/\"67ab43\", \"54ed21\"')\n    assert.equal(header.toString(), 'W/\"67ab43\", \"54ed21\"')\n  })\n})\n\ndescribe('IfNoneMatch.from', () => {\n  it('parses a string value', () => {\n    let result = IfNoneMatch.from('\"abc\", \"def\"')\n    assert.ok(result instanceof IfNoneMatch)\n    assert.equal(result.tags.length, 2)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/if-none-match.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { quoteEtag } from './utils.ts'\n\n/**\n * Initializer for an `If-None-Match` header value.\n */\nexport interface IfNoneMatchInit {\n  /**\n   * The entity tags to compare against the current entity.\n   */\n  tags: string[]\n}\n\n/**\n * The value of an `If-None-Match` HTTP header.\n *\n * [MDN `If-None-Match` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7232#section-3.2)\n */\nexport class IfNoneMatch implements HeaderValue, IfNoneMatchInit {\n  /**\n   * Entity tags carried by the header.\n   */\n  tags: string[] = []\n\n  constructor(init?: string | string[] | IfNoneMatchInit) {\n    if (init) return IfNoneMatch.from(init)\n  }\n\n  /**\n   * Checks if the header contains the given entity tag.\n   *\n   * Note: This method checks only for exact matches and does not consider wildcards.\n   *\n   * @param tag The entity tag to check for\n   * @returns `true` if the tag is present in the header, `false` otherwise\n   */\n  has(tag: string): boolean {\n    return this.tags.includes(quoteEtag(tag))\n  }\n\n  /**\n   * Checks if this header matches the given entity tag.\n   *\n   * @param tag The entity tag to check for\n   * @returns `true` if the tag is present in the header (or the header contains a wildcard), `false` otherwise\n   */\n  matches(tag: string): boolean {\n    return this.has(tag) || this.tags.includes('*')\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString() {\n    return this.tags.join(', ')\n  }\n\n  /**\n   * Parse an If-None-Match header value.\n   *\n   * @param value The header value (string, string[], init object, or null)\n   * @returns An IfNoneMatch instance (empty if null)\n   */\n  static from(value: string | string[] | IfNoneMatchInit | null): IfNoneMatch {\n    let header = new IfNoneMatch()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        header.tags.push(...value.split(/\\s*,\\s*/).map(quoteEtag))\n      } else if (Array.isArray(value)) {\n        header.tags.push(...value.map(quoteEtag))\n      } else {\n        header.tags.push(...value.tags.map(quoteEtag))\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/if-range.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { IfRange } from './if-range.ts'\n\ndescribe('IfRange', () => {\n  let testDate = new Date('2021-01-01T00:00:00Z')\n  let testDateString = testDate.toUTCString() // 'Fri, 01 Jan 2021 00:00:00 GMT'\n  let testTimestamp = testDate.getTime() // 1609459200000\n\n  it('initializes with an empty string', () => {\n    let header = new IfRange('')\n    assert.equal(header.value, '')\n  })\n\n  it('initializes with a string (HTTP date)', () => {\n    let header = new IfRange(testDateString)\n    assert.equal(header.value, testDateString)\n  })\n\n  it('initializes with a string (ETag)', () => {\n    let header = new IfRange('\"67ab43\"')\n    assert.equal(header.value, '\"67ab43\"')\n  })\n\n  it('initializes with a Date', () => {\n    let header = new IfRange(testDate)\n    assert.equal(header.value, testDateString)\n  })\n\n  it('converts to a string', () => {\n    let header = new IfRange(testDateString)\n    assert.equal(header.toString(), testDateString)\n\n    let header2 = new IfRange('\"67ab43\"')\n    assert.equal(header2.toString(), '\"67ab43\"')\n  })\n\n  describe('matches()', () => {\n    describe('with HTTP dates', () => {\n      it('matches when lastModified matches the date (timestamp)', () => {\n        let header = new IfRange(testDateString)\n        assert.ok(header.matches({ lastModified: testTimestamp }))\n      })\n\n      it('matches when lastModified matches the date (Date object)', () => {\n        let header = new IfRange(testDateString)\n        assert.ok(header.matches({ lastModified: testDate }))\n      })\n\n      it('does not match when lastModified is later', () => {\n        let header = new IfRange(testDateString)\n        assert.ok(!header.matches({ lastModified: testTimestamp + 1000 }))\n      })\n\n      it('does not match when lastModified is earlier', () => {\n        let header = new IfRange(testDateString)\n        assert.ok(!header.matches({ lastModified: testTimestamp - 1000 }))\n      })\n\n      it('handles fractional seconds (rounds to second)', () => {\n        let header = new IfRange(testDateString)\n        // Same second, different milliseconds\n        assert.ok(header.matches({ lastModified: testTimestamp + 123 }))\n        assert.ok(header.matches({ lastModified: testTimestamp + 999 }))\n      })\n\n      it('does not match when lastModified is null', () => {\n        let header = new IfRange(testDateString)\n        assert.ok(!header.matches({ lastModified: null }))\n      })\n\n      it('does not match when lastModified is missing', () => {\n        let header = new IfRange(testDateString)\n        assert.ok(!header.matches({}))\n      })\n\n      it('does not match invalid HTTP dates', () => {\n        let header = new IfRange('not-a-date')\n        assert.ok(!header.matches({ lastModified: testTimestamp }))\n      })\n    })\n\n    describe('with ETags', () => {\n      it('matches when etag matches (strong ETag)', () => {\n        let header = new IfRange('\"67ab43\"')\n        assert.ok(header.matches({ etag: '\"67ab43\"' }))\n      })\n\n      it('matches when etag matches (without quotes)', () => {\n        let header = new IfRange('67ab43')\n        assert.ok(header.matches({ etag: '67ab43' }))\n      })\n\n      it('does not match when etag differs', () => {\n        let header = new IfRange('\"67ab43\"')\n        assert.ok(!header.matches({ etag: '\"54ed21\"' }))\n      })\n\n      it('does not match when etag is null', () => {\n        let header = new IfRange('\"67ab43\"')\n        assert.ok(!header.matches({ etag: null }))\n      })\n\n      it('does not match when etag is missing', () => {\n        let header = new IfRange('\"67ab43\"')\n        assert.ok(!header.matches({}))\n      })\n\n      it('does not match weak ETags (per RFC 7233)', () => {\n        let header = new IfRange('W/\"67ab43\"')\n        assert.ok(!header.matches({ etag: 'W/\"67ab43\"' }))\n      })\n\n      it('does not match when resource ETag is weak', () => {\n        let header = new IfRange('\"67ab43\"')\n        assert.ok(!header.matches({ etag: 'W/\"67ab43\"' }))\n      })\n\n      it('does not match when If-Range is weak and resource is strong', () => {\n        let header = new IfRange('W/\"67ab43\"')\n        assert.ok(!header.matches({ etag: '\"67ab43\"' }))\n      })\n    })\n\n    describe('with both etag and lastModified', () => {\n      it('matches date when value is a date', () => {\n        let header = new IfRange(testDateString)\n        assert.ok(\n          header.matches({\n            etag: '\"67ab43\"',\n            lastModified: testTimestamp,\n          }),\n        )\n      })\n\n      it('matches etag when value is an ETag', () => {\n        let header = new IfRange('\"67ab43\"')\n        assert.ok(\n          header.matches({\n            etag: '\"67ab43\"',\n            lastModified: testTimestamp,\n          }),\n        )\n      })\n    })\n\n    describe('with empty value', () => {\n      it('matches unconditionally (condition passes when header is not present)', () => {\n        let header = new IfRange('')\n        assert.ok(header.matches({ etag: '\"67ab43\"' }))\n        assert.ok(header.matches({ lastModified: testTimestamp }))\n        assert.ok(header.matches({ etag: '\"67ab43\"', lastModified: testTimestamp }))\n        assert.ok(header.matches({}))\n      })\n    })\n  })\n})\n\ndescribe('IfRange.from', () => {\n  it('parses a string value', () => {\n    let result = IfRange.from('\"abc\"')\n    assert.ok(result instanceof IfRange)\n    assert.equal(result.value, '\"abc\"')\n  })\n\n  it('parses a Date value', () => {\n    let date = new Date('2024-01-01T00:00:00.000Z')\n    let result = IfRange.from(date)\n    assert.ok(result instanceof IfRange)\n    assert.equal(result.value, date.toUTCString())\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/if-range.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { parseHttpDate, removeMilliseconds } from './utils.ts'\nimport { quoteEtag } from './utils.ts'\n\n/**\n * The value of an `If-Range` HTTP header.\n *\n * The `If-Range` header can contain either an entity tag (ETag) or an HTTP date.\n *\n * [MDN `If-Range` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2)\n */\nexport class IfRange implements HeaderValue {\n  /**\n   * Raw header value, either an entity tag or an HTTP date.\n   */\n  value: string = ''\n\n  constructor(init?: string | Date) {\n    if (init) return IfRange.from(init)\n  }\n\n  /**\n   * Checks if the `If-Range` condition is satisfied for the current resource state.\n   *\n   * This method always returns `true` if the `If-Range` header is not present,\n   * meaning the range request should proceed unconditionally.\n   *\n   * The `If-Range` header can contain either:\n   * - An HTTP date (RFC 7231 IMF-fixdate format)\n   * - An entity tag (ETag)\n   *\n   * When comparing ETags, only strong entity tags are matched as per RFC 7233.\n   * Weak entity tags (prefixed with `W/`) are never considered a match.\n   *\n   * @param resource The current resource state to compare against\n   * @param resource.etag The resource's ETag value\n   * @param resource.lastModified The resource's last modified timestamp\n   * @returns `true` if the condition is satisfied, `false` otherwise\n   *\n   * @example\n   * ```ts\n   * let ifRange = new IfRange('Wed, 21 Oct 2015 07:28:00 GMT')\n   * ifRange.matches({ lastModified: 1445412480000 }) // true if dates match\n   * ifRange.matches({ lastModified: new Date('2015-10-21T07:28:00Z') }) // true\n   *\n   * let ifRange2 = new IfRange('\"abc123\"')\n   * ifRange2.matches({ etag: '\"abc123\"' }) // true\n   * ifRange2.matches({ etag: 'W/\"abc123\"' }) // false (weak ETag)\n   * ```\n   */\n  matches(resource: { etag?: string | null; lastModified?: number | Date | null }): boolean {\n    if (!this.value) {\n      return true\n    }\n\n    // Try parsing as HTTP date first\n    let dateTimestamp = parseHttpDate(this.value)\n    if (dateTimestamp !== null && resource.lastModified != null) {\n      return removeMilliseconds(dateTimestamp) === removeMilliseconds(resource.lastModified)\n    }\n\n    // Otherwise treat as ETag\n    if (resource.etag != null) {\n      let normalizedTag = quoteEtag(this.value)\n      let normalizedResourceTag = quoteEtag(resource.etag)\n\n      // Weak tags never match in If-Range (strong comparison only, per RFC 7233)\n      if (normalizedTag.startsWith('W/') || normalizedResourceTag.startsWith('W/')) {\n        return false\n      }\n\n      return normalizedTag === normalizedResourceTag\n    }\n\n    return false\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString() {\n    return this.value\n  }\n\n  /**\n   * Parse an If-Range header value.\n   *\n   * @param value The header value (string, Date, or null)\n   * @returns An IfRange instance (empty if null)\n   */\n  static from(value: string | Date | null): IfRange {\n    let header = new IfRange()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        header.value = value\n      } else {\n        header.value = value.toUTCString()\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/param-values.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { parseParams } from './param-values.ts'\n\ndescribe('parseParams', () => {\n  it('correctly parses a string of parameters for a Content-Type header', () => {\n    assert.deepEqual(parseParams('text/html; charset=utf-8'), [\n      ['text/html', undefined],\n      ['charset', 'utf-8'],\n    ])\n    assert.deepEqual(parseParams('application/json'), [['application/json', undefined]])\n    assert.deepEqual(parseParams('multipart/form-data; boundary=----WebKitFormBoundaryABC123'), [\n      ['multipart/form-data', undefined],\n      ['boundary', '----WebKitFormBoundaryABC123'],\n    ])\n  })\n\n  it('correctly parses a string of parameters for a Content-Disposition header', () => {\n    assert.deepEqual(parseParams('form-data; name=fieldName'), [\n      ['form-data', undefined],\n      ['name', 'fieldName'],\n    ])\n    assert.deepEqual(parseParams('form-data; name=\"fieldName\"; filename=\"filename.jpg\"'), [\n      ['form-data', undefined],\n      ['name', 'fieldName'],\n      ['filename', 'filename.jpg'],\n    ])\n    assert.deepEqual(\n      parseParams(\"attachment; filename=photo.jpg; filename*=UTF-8''%E7%85%A7%E7%89%87.jpg\"),\n      [\n        ['attachment', undefined],\n        ['filename', 'photo.jpg'],\n        ['filename*', \"UTF-8''%E7%85%A7%E7%89%87.jpg\"],\n      ],\n    )\n    assert.deepEqual(\n      parseParams('attachment; filename=\"photo.jpg\"; filename*=\"UTF-8\\'\\'%E7%85%A7%E7%89%87.jpg\"'),\n      [\n        ['attachment', undefined],\n        ['filename', 'photo.jpg'],\n        ['filename*', \"UTF-8''%E7%85%A7%E7%89%87.jpg\"],\n      ],\n    )\n  })\n\n  it('correctly parses a string of parameters for a Set-Cookie header', () => {\n    assert.deepEqual(parseParams('session_id=abc123; Path=/; HttpOnly; Secure'), [\n      ['session_id', 'abc123'],\n      ['Path', '/'],\n      ['HttpOnly', undefined],\n      ['Secure', undefined],\n    ])\n    assert.deepEqual(parseParams('user_pref=\"dark_mode\"; Max-Age=31536000; SameSite=Lax'), [\n      ['user_pref', 'dark_mode'],\n      ['Max-Age', '31536000'],\n      ['SameSite', 'Lax'],\n    ])\n    assert.deepEqual(\n      parseParams(\n        'preferences={\"font\":\"Arial\",\"size\":\"12pt\"}; Expires=Fri, 31 Dec 2023 23:59:59 GMT',\n      ),\n      [\n        ['preferences', '{\"font\":\"Arial\",\"size\":\"12pt\"}'],\n        ['Expires', 'Fri, 31 Dec 2023 23:59:59 GMT'],\n      ],\n    )\n    assert.deepEqual(parseParams('cart_items=\"[\\\\\"item1\\\\\",\\\\\"item2\\\\\"]\"; Path=/cart; HttpOnly'), [\n      ['cart_items', '[\"item1\",\"item2\"]'],\n      ['Path', '/cart'],\n      ['HttpOnly', undefined],\n    ])\n    assert.deepEqual(parseParams('account_type=\"premium,\\\\\"gold\\\\\"\"; Domain=example.com; Secure'), [\n      ['account_type', 'premium,\"gold\"'],\n      ['Domain', 'example.com'],\n      ['Secure', undefined],\n    ])\n    assert.deepEqual(\n      parseParams('a2f_token=987654; Path=/2fa; Secure; HttpOnly; SameSite=Strict; Max-Age=300'),\n      [\n        ['a2f_token', '987654'],\n        ['Path', '/2fa'],\n        ['Secure', undefined],\n        ['HttpOnly', undefined],\n        ['SameSite', 'Strict'],\n        ['Max-Age', '300'],\n      ],\n    )\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/param-values.ts",
    "content": "export function parseParams(\n  input: string,\n  delimiter: ';' | ',' = ';',\n): [string, string | undefined][] {\n  // This parser splits on the delimiter and unquotes any quoted values\n  // like `filename=\"the\\\\ filename.txt\"`.\n  let parser =\n    delimiter === ';'\n      ? /(?:^|;)\\s*([^=;\\s]+)(\\s*=\\s*(?:\"((?:[^\"\\\\]|\\\\.)*)\"|((?:[^;]|\\\\\\;)+))?)?/g\n      : /(?:^|,)\\s*([^=,\\s]+)(\\s*=\\s*(?:\"((?:[^\"\\\\]|\\\\.)*)\"|((?:[^,]|\\\\\\,)+))?)?/g\n\n  let params: [string, string | undefined][] = []\n\n  let match\n  while ((match = parser.exec(input)) !== null) {\n    let key = match[1].trim()\n\n    let value: string | undefined\n    if (match[2]) {\n      value = (match[3] || match[4] || '').replace(/\\\\(.)/g, '$1').trim()\n    }\n\n    params.push([key, value])\n  }\n\n  return params\n}\n\nexport function quote(value: string): string {\n  if (value.includes('\"') || value.includes(';') || value.includes(' ')) {\n    return `\"${value.replace(/\"/g, '\\\\\"')}\"`\n  }\n  return value\n}\n"
  },
  {
    "path": "packages/headers/src/lib/range.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { Range } from './range.ts'\n\ndescribe('Range', () => {\n  it('initializes with an empty string', () => {\n    let range = new Range('')\n    assert.equal(range.unit, '')\n    assert.deepEqual(range.ranges, [])\n  })\n\n  describe('parsing from string', () => {\n    it('parses a simple range', () => {\n      let range = new Range('bytes=0-99')\n      assert.equal(range.unit, 'bytes')\n      assert.equal(range.ranges.length, 1)\n      assert.equal(range.ranges[0].start, 0)\n      assert.equal(range.ranges[0].end, 99)\n    })\n\n    it('parses a range with only start', () => {\n      let range = new Range('bytes=100-')\n      assert.equal(range.unit, 'bytes')\n      assert.equal(range.ranges.length, 1)\n      assert.equal(range.ranges[0].start, 100)\n      assert.equal(range.ranges[0].end, undefined)\n    })\n\n    it('parses a suffix range (only end)', () => {\n      let range = new Range('bytes=-500')\n      assert.equal(range.unit, 'bytes')\n      assert.equal(range.ranges.length, 1)\n      assert.equal(range.ranges[0].start, undefined)\n      assert.equal(range.ranges[0].end, 500)\n    })\n\n    it('parses multiple ranges', () => {\n      let range = new Range('bytes=0-99,200-299,400-')\n      assert.equal(range.unit, 'bytes')\n      assert.equal(range.ranges.length, 3)\n      assert.equal(range.ranges[0].start, 0)\n      assert.equal(range.ranges[0].end, 99)\n      assert.equal(range.ranges[1].start, 200)\n      assert.equal(range.ranges[1].end, 299)\n      assert.equal(range.ranges[2].start, 400)\n      assert.equal(range.ranges[2].end, undefined)\n    })\n\n    it('handles malformed range with no bounds', () => {\n      let range = new Range('bytes=-')\n      assert.equal(range.ranges.length, 0)\n    })\n\n    it('handles completely invalid syntax', () => {\n      let range = new Range('not-a-range')\n      assert.equal(range.ranges.length, 0)\n    })\n\n    it('handles whitespace in ranges', () => {\n      let range = new Range('bytes=0-99, 200-299')\n      assert.equal(range.ranges.length, 2)\n      assert.equal(range.ranges[0].start, 0)\n      assert.equal(range.ranges[0].end, 99)\n      assert.equal(range.ranges[1].start, 200)\n      assert.equal(range.ranges[1].end, 299)\n    })\n  })\n\n  describe('construction from object', () => {\n    it('creates range from object init', () => {\n      let range = new Range({\n        unit: 'bytes',\n        ranges: [{ start: 0, end: 99 }],\n      })\n      assert.equal(range.unit, 'bytes')\n      assert.equal(range.ranges.length, 1)\n      assert.equal(range.ranges[0].start, 0)\n      assert.equal(range.ranges[0].end, 99)\n    })\n\n    it('uses empty unit if not specified', () => {\n      let range = new Range({\n        ranges: [{ start: 0, end: 99 }],\n      })\n      assert.equal(range.unit, '')\n    })\n\n    it('initializes with another Range', () => {\n      let range1 = new Range('bytes=0-99')\n      let range2 = new Range({\n        unit: range1.unit,\n        ranges: range1.ranges,\n      })\n      assert.equal(range2.unit, 'bytes')\n      assert.equal(range2.ranges.length, 1)\n      assert.equal(range2.ranges[0].start, 0)\n      assert.equal(range2.ranges[0].end, 99)\n    })\n  })\n\n  describe('canSatisfy', () => {\n    it('returns true when range is within resource', () => {\n      let range = new Range('bytes=0-99')\n      assert.equal(range.canSatisfy(1000), true)\n    })\n\n    it('returns true when range starts within resource', () => {\n      let range = new Range('bytes=100-')\n      assert.equal(range.canSatisfy(1000), true)\n    })\n\n    it('returns true for suffix range', () => {\n      let range = new Range('bytes=-500')\n      assert.equal(range.canSatisfy(1000), true)\n    })\n\n    it('returns false when range starts beyond resource', () => {\n      let range = new Range('bytes=1000-')\n      assert.equal(range.canSatisfy(500), false)\n    })\n\n    it('returns false for empty ranges', () => {\n      let range = new Range({ ranges: [] })\n      assert.equal(range.canSatisfy(1000), false)\n    })\n\n    it('returns false when start > end', () => {\n      let range = new Range({\n        ranges: [{ start: 100, end: 50 }],\n      })\n      assert.equal(range.canSatisfy(1000), false)\n    })\n\n    it('returns false when range has no bounds', () => {\n      let range = new Range({\n        ranges: [{}],\n      })\n      assert.equal(range.canSatisfy(1000), false)\n    })\n\n    it('returns false for malformed range string', () => {\n      let range = new Range('bytes=-')\n      assert.equal(range.canSatisfy(1000), false)\n    })\n\n    it('returns true when at least one range is satisfiable', () => {\n      let range = new Range('bytes=1000-,0-99')\n      assert.equal(range.canSatisfy(500), true)\n    })\n  })\n\n  describe('normalize', () => {\n    it('normalizes simple range', () => {\n      let range = new Range('bytes=0-99')\n      let normalized = range.normalize(1000)\n      assert.equal(normalized.length, 1)\n      assert.equal(normalized[0].start, 0)\n      assert.equal(normalized[0].end, 99)\n    })\n\n    it('normalizes start-only range', () => {\n      let range = new Range('bytes=100-')\n      let normalized = range.normalize(1000)\n      assert.equal(normalized.length, 1)\n      assert.equal(normalized[0].start, 100)\n      assert.equal(normalized[0].end, 999)\n    })\n\n    it('normalizes suffix range', () => {\n      let range = new Range('bytes=-500')\n      let normalized = range.normalize(1000)\n      assert.equal(normalized.length, 1)\n      assert.equal(normalized[0].start, 500)\n      assert.equal(normalized[0].end, 999)\n    })\n\n    it('clamps end to file size', () => {\n      let range = new Range('bytes=0-5000')\n      let normalized = range.normalize(1000)\n      assert.equal(normalized.length, 1)\n      assert.equal(normalized[0].start, 0)\n      assert.equal(normalized[0].end, 999)\n    })\n\n    it('normalizes multiple ranges', () => {\n      let range = new Range('bytes=0-99,200-299')\n      let normalized = range.normalize(1000)\n      assert.equal(normalized.length, 2)\n      assert.equal(normalized[0].start, 0)\n      assert.equal(normalized[0].end, 99)\n      assert.equal(normalized[1].start, 200)\n      assert.equal(normalized[1].end, 299)\n    })\n\n    it('returns empty array for unsatisfiable range', () => {\n      let range = new Range('bytes=1000-')\n      let normalized = range.normalize(500)\n      assert.equal(normalized.length, 0)\n    })\n\n    it('returns empty array for malformed range', () => {\n      let range = new Range('bytes=-')\n      let normalized = range.normalize(1000)\n      assert.equal(normalized.length, 0)\n    })\n\n    it('handles suffix larger than file size', () => {\n      let range = new Range('bytes=-5000')\n      let normalized = range.normalize(1000)\n      assert.equal(normalized.length, 1)\n      assert.equal(normalized[0].start, 0)\n      assert.equal(normalized[0].end, 999)\n    })\n  })\n\n  describe('toString', () => {\n    it('converts simple range to string', () => {\n      let range = new Range('bytes=0-99')\n      assert.equal(range.toString(), 'bytes=0-99')\n    })\n\n    it('converts start-only range to string', () => {\n      let range = new Range('bytes=100-')\n      assert.equal(range.toString(), 'bytes=100-')\n    })\n\n    it('converts suffix range to string', () => {\n      let range = new Range('bytes=-500')\n      assert.equal(range.toString(), 'bytes=-500')\n    })\n\n    it('converts multiple ranges to string', () => {\n      let range = new Range('bytes=0-99,200-299')\n      assert.equal(range.toString(), 'bytes=0-99,200-299')\n    })\n\n    it('returns empty string for empty ranges', () => {\n      let range = new Range({ ranges: [] })\n      assert.equal(range.toString(), '')\n    })\n\n    it('returns empty string when unit is not set', () => {\n      let range = new Range()\n      range.unit = ''\n      range.ranges = [{ start: 0, end: 99 }]\n      assert.equal(range.toString(), '')\n    })\n  })\n})\n\ndescribe('Range.from', () => {\n  it('parses a string value', () => {\n    let result = Range.from('bytes=0-499')\n    assert.ok(result instanceof Range)\n    assert.equal(result.unit, 'bytes')\n    assert.equal(result.ranges.length, 1)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/range.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\n\n/**\n * Initializer for a {@link Range} header value.\n */\nexport interface RangeInit {\n  /**\n   * The unit of the range, typically \"bytes\"\n   */\n  unit?: string\n  /**\n   * The ranges requested. Each range has optional start and end values.\n   * - {start: 0, end: 99} = bytes 0-99\n   * - {start: 100} = bytes 100- (from 100 to end)\n   * - {end: 500} = bytes -500 (last 500 bytes)\n   */\n  ranges?: Array<{ start?: number; end?: number }>\n}\n\n/**\n * The value of a `Range` HTTP header.\n *\n * [MDN `Range` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)\n *\n * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.range)\n */\nexport class Range implements HeaderValue, RangeInit {\n  /**\n   * The range unit, typically `bytes`.\n   */\n  unit: string = ''\n\n  /**\n   * Requested byte ranges from the header.\n   */\n  ranges: Array<{ start?: number; end?: number }> = []\n\n  constructor(init?: string | RangeInit) {\n    if (init) return Range.from(init)\n  }\n\n  /**\n   * Checks if this range can be satisfied for a resource of the given size.\n   *\n   * @param resourceSize The size of the resource in bytes\n   * @returns `false` if the range is malformed or all ranges are beyond the resource size\n   */\n  canSatisfy(resourceSize: number): boolean {\n    // No unit or no ranges means header was malformed or empty\n    if (!this.unit || this.ranges.length === 0) return false\n\n    // Validate all ranges first\n    for (let range of this.ranges) {\n      // At least one bound must be specified\n      if (range.start === undefined && range.end === undefined) {\n        return false\n      }\n      // If both are specified, start must be <= end\n      if (range.start !== undefined && range.end !== undefined && range.start > range.end) {\n        return false\n      }\n    }\n\n    // Check if at least one range is within the resource\n    for (let range of this.ranges) {\n      if (range.start === undefined) {\n        // Suffix range (e.g., \"-500\") is always satisfiable\n        return true\n      }\n      if (range.start < resourceSize) {\n        // At least one range starts within the resource\n        return true\n      }\n    }\n\n    return false\n  }\n\n  /**\n   * Normalizes the ranges for a resource of the given size.\n   * Returns an array of ranges with resolved start and end values.\n   * Returns an empty array if the range cannot be satisfied.\n   *\n   * @param resourceSize The size of the resource in bytes\n   * @returns An array of ranges with resolved start and end values\n   */\n  normalize(resourceSize: number): Array<{ start: number; end: number }> {\n    if (!this.canSatisfy(resourceSize)) {\n      return []\n    }\n\n    return this.ranges.map((range) => {\n      if (range.start !== undefined && range.end !== undefined) {\n        // Both bounds specified (e.g., \"0-99\")\n        return {\n          start: range.start,\n          end: Math.min(range.end, resourceSize - 1),\n        }\n      } else if (range.start !== undefined) {\n        // Only start specified (e.g., \"100-\")\n        return {\n          start: range.start,\n          end: resourceSize - 1,\n        }\n      } else {\n        // Only end specified (e.g., \"-500\" means last 500 bytes)\n        let suffix = range.end!\n        return {\n          start: Math.max(0, resourceSize - suffix),\n          end: resourceSize - 1,\n        }\n      }\n    })\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    if (!this.unit || this.ranges.length === 0) return ''\n\n    let rangeParts = this.ranges.map((range) => {\n      if (range.start !== undefined && range.end !== undefined) {\n        return `${range.start}-${range.end}`\n      } else if (range.start !== undefined) {\n        return `${range.start}-`\n      } else if (range.end !== undefined) {\n        return `-${range.end}`\n      }\n      return ''\n    })\n\n    return `${this.unit}=${rangeParts.join(',')}`\n  }\n\n  /**\n   * Parse a Range header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns A Range instance (empty if null)\n   */\n  static from(value: string | RangeInit | null): Range {\n    let header = new Range()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        // Parse: \"bytes=200-1000\" or \"bytes=200-\" or \"bytes=-500\" or \"bytes=0-99,200-299\"\n        let match = value.match(/^(\\w+)=(.+)$/)\n        if (match) {\n          header.unit = match[1]\n          let rangeParts = match[2].split(',')\n\n          // Track if any range part is invalid to mark the entire header as malformed\n          let hasInvalidPart = false\n\n          for (let part of rangeParts) {\n            let rangeMatch = part.trim().match(/^(\\d*)-(\\d*)$/)\n            if (!rangeMatch) {\n              // Invalid syntax for this range part\n              hasInvalidPart = true\n              continue\n            }\n\n            let [, startStr, endStr] = rangeMatch\n            // At least one bound must be specified\n            if (!startStr && !endStr) {\n              hasInvalidPart = true\n              continue\n            }\n\n            let start = startStr ? parseInt(startStr, 10) : undefined\n            let end = endStr ? parseInt(endStr, 10) : undefined\n\n            // If both bounds are specified, start must be <= end\n            if (start !== undefined && end !== undefined && start > end) {\n              hasInvalidPart = true\n              continue\n            }\n\n            header.ranges.push({ start, end })\n          }\n\n          // If any part was invalid, mark as malformed by clearing ranges\n          if (hasInvalidPart) {\n            header.ranges = []\n          }\n        }\n      } else {\n        if (value.unit !== undefined) header.unit = value.unit\n        if (value.ranges !== undefined) header.ranges = value.ranges\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/raw-headers.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { parse as parseRawHeaders, stringify as stringifyRawHeaders } from './raw-headers.ts'\n\ndescribe('parseRawHeaders', () => {\n  it('parses a single header', () => {\n    let headers = parseRawHeaders('Content-Type: text/html')\n    assert.equal(headers.get('content-type'), 'text/html')\n  })\n\n  it('parses multiple headers', () => {\n    let headers = parseRawHeaders('Content-Type: text/html\\r\\nCache-Control: no-cache')\n    assert.equal(headers.get('content-type'), 'text/html')\n    assert.equal(headers.get('cache-control'), 'no-cache')\n  })\n\n  it('trims whitespace from header names and values', () => {\n    let headers = parseRawHeaders('  Content-Type  :  text/html  ')\n    assert.equal(headers.get('content-type'), 'text/html')\n  })\n\n  it('handles multiple values for the same header', () => {\n    let headers = parseRawHeaders('Set-Cookie: a=1\\r\\nSet-Cookie: b=2')\n    assert.equal(headers.get('set-cookie'), 'a=1, b=2')\n  })\n\n  it('ignores malformed lines', () => {\n    let headers = parseRawHeaders(\n      'Content-Type: text/html\\r\\nmalformed line\\r\\nCache-Control: no-cache',\n    )\n    assert.equal(headers.get('content-type'), 'text/html')\n    assert.equal(headers.get('cache-control'), 'no-cache')\n  })\n\n  it('returns empty Headers for empty string', () => {\n    let headers = parseRawHeaders('')\n    assert.equal([...headers].length, 0)\n  })\n\n  it('handles headers with colons in values', () => {\n    let headers = parseRawHeaders('Location: https://example.com:8080/path')\n    assert.equal(headers.get('location'), 'https://example.com:8080/path')\n  })\n})\n\ndescribe('stringifyRawHeaders', () => {\n  it('stringifies a single header', () => {\n    let headers = new Headers({ 'Content-Type': 'text/html' })\n    assert.equal(stringifyRawHeaders(headers), 'Content-Type: text/html')\n  })\n\n  it('stringifies multiple headers', () => {\n    let headers = new Headers()\n    headers.set('Content-Type', 'text/html')\n    headers.set('Cache-Control', 'no-cache')\n    let result = stringifyRawHeaders(headers)\n    assert.ok(result.includes('Content-Type: text/html'))\n    assert.ok(result.includes('Cache-Control: no-cache'))\n    assert.ok(result.includes('\\r\\n'))\n  })\n\n  it('returns empty string for empty Headers', () => {\n    let headers = new Headers()\n    assert.equal(stringifyRawHeaders(headers), '')\n  })\n\n  it('handles headers with colons in values', () => {\n    let headers = new Headers({ Location: 'https://example.com:8080/path' })\n    assert.equal(stringifyRawHeaders(headers), 'Location: https://example.com:8080/path')\n  })\n\n  it('uses canonical header name casing', () => {\n    let headers = new Headers()\n    headers.set('etag', '\"abc\"')\n    headers.set('www-authenticate', 'Basic')\n    headers.set('x-custom-header', 'value')\n    let result = stringifyRawHeaders(headers)\n    assert.ok(result.includes('ETag: \"abc\"'))\n    assert.ok(result.includes('WWW-Authenticate: Basic'))\n    assert.ok(result.includes('X-Custom-Header: value'))\n  })\n\n  it('round-trips with parseRawHeaders', () => {\n    let original = new Headers()\n    original.set('Content-Type', 'text/html')\n    original.set('Cache-Control', 'no-cache')\n\n    let stringified = stringifyRawHeaders(original)\n    let parsed = parseRawHeaders(stringified)\n\n    assert.equal(parsed.get('content-type'), 'text/html')\n    assert.equal(parsed.get('cache-control'), 'no-cache')\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/raw-headers.ts",
    "content": "import { canonicalHeaderName } from './header-names.ts'\n\nconst CRLF = '\\r\\n'\n\n/**\n * Parses a raw HTTP header string into a `Headers` object.\n *\n * @param raw A raw HTTP header string with headers separated by CRLF (`\\r\\n`)\n * @returns A `Headers` object containing the parsed headers\n *\n * @example\n * let headers = parse('Content-Type: text/html\\r\\nCache-Control: no-cache')\n * headers.get('content-type') // 'text/html'\n * headers.get('cache-control') // 'no-cache'\n */\nexport function parse(raw: string): Headers {\n  let headers = new Headers()\n\n  for (let line of raw.split(CRLF)) {\n    let match = line.match(/^([^:]+):(.*)/)\n    if (match) {\n      headers.append(match[1].trim(), match[2].trim())\n    }\n  }\n\n  return headers\n}\n\n/**\n * Converts a `Headers` object to a raw HTTP header string.\n *\n * @param headers A `Headers` object to stringify\n * @returns A raw HTTP header string with headers separated by CRLF (`\\r\\n`)\n *\n * @example\n * let headers = new Headers({ 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' })\n * stringify(headers) // 'Content-Type: text/html\\r\\nCache-Control: no-cache'\n */\nexport function stringify(headers: Headers): string {\n  let result = ''\n\n  for (let [name, value] of headers) {\n    if (result) result += CRLF\n    result += `${canonicalHeaderName(name)}: ${value}`\n  }\n\n  return result\n}\n"
  },
  {
    "path": "packages/headers/src/lib/set-cookie.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { SetCookie } from './set-cookie.ts'\n\ndescribe('SetCookie', () => {\n  it('initializes with an empty string', () => {\n    let header = new SetCookie('')\n    assert.equal(header.name, undefined)\n    assert.equal(header.value, undefined)\n  })\n\n  it('initializes with a string', () => {\n    let header = new SetCookie(\n      'session=abc123; Domain=example.com; Path=/; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly',\n    )\n    assert.equal(header.name, 'session')\n    assert.equal(header.value, 'abc123')\n    assert.equal(header.domain, 'example.com')\n    assert.equal(header.path, '/')\n    assert.equal(header.expires?.toUTCString(), 'Wed, 21 Oct 2015 07:28:00 GMT')\n    assert.equal(header.secure, true)\n    assert.equal(header.httpOnly, true)\n  })\n\n  it('initializes with an object', () => {\n    let header = new SetCookie({\n      name: 'session',\n      value: 'abc123',\n      domain: 'example.com',\n      path: '/',\n      expires: new Date('Wed, 21 Oct 2015 07:28:00 GMT'),\n      secure: true,\n      httpOnly: true,\n    })\n    assert.equal(header.name, 'session')\n    assert.equal(header.value, 'abc123')\n    assert.equal(header.domain, 'example.com')\n    assert.equal(header.path, '/')\n    assert.equal(header.expires?.toUTCString(), 'Wed, 21 Oct 2015 07:28:00 GMT')\n    assert.equal(header.secure, true)\n    assert.equal(header.httpOnly, true)\n  })\n\n  it('initializes with httpOnly: false', () => {\n    let header = new SetCookie({\n      name: 'session',\n      value: 'abc123',\n      httpOnly: false,\n    })\n    assert.equal(header.name, 'session')\n    assert.equal(header.value, 'abc123')\n    assert.equal(header.httpOnly, false)\n  })\n\n  it('initializes with secure: false', () => {\n    let header = new SetCookie({\n      name: 'session',\n      value: 'abc123',\n      secure: false,\n    })\n    assert.equal(header.name, 'session')\n    assert.equal(header.value, 'abc123')\n    assert.equal(header.secure, false)\n  })\n\n  it('initializes with another SetCookie', () => {\n    let header = new SetCookie(\n      new SetCookie('session=abc123; Domain=example.com; Path=/; Secure; HttpOnly'),\n    )\n    assert.equal(header.name, 'session')\n    assert.equal(header.value, 'abc123')\n    assert.equal(header.domain, 'example.com')\n    assert.equal(header.path, '/')\n    assert.equal(header.secure, true)\n    assert.equal(header.httpOnly, true)\n  })\n\n  it('handles cookies without attributes', () => {\n    let header = new SetCookie('user=john')\n    assert.equal(header.name, 'user')\n    assert.equal(header.value, 'john')\n  })\n\n  it('handles cookie values with commas', () => {\n    let header = new SetCookie('list=apple,banana,cherry; Domain=example.com')\n    assert.equal(header.name, 'list')\n    assert.equal(header.value, 'apple,banana,cherry')\n    assert.equal(header.domain, 'example.com')\n  })\n\n  it('handles cookie values with semicolons', () => {\n    let header = new SetCookie('complex=\"value; with; semicolons\"; Path=/')\n    assert.equal(header.name, 'complex')\n    assert.equal(header.value, 'value; with; semicolons')\n    assert.equal(header.path, '/')\n  })\n\n  it('handles cookie values with equals signs', () => {\n    let header = new SetCookie('equation=\"1+1=2\"; Secure')\n    assert.equal(header.name, 'equation')\n    assert.equal(header.value, '1+1=2')\n    assert.equal(header.secure, true)\n  })\n\n  it('sets and gets attributes', () => {\n    let header = new SetCookie('test=value')\n    header.domain = 'example.org'\n    header.path = '/api'\n    header.maxAge = 3600\n    header.secure = true\n    header.httpOnly = true\n    header.sameSite = 'Strict'\n\n    assert.equal(header.domain, 'example.org')\n    assert.equal(header.path, '/api')\n    assert.equal(header.maxAge, 3600)\n    assert.equal(header.secure, true)\n    assert.equal(header.httpOnly, true)\n    assert.equal(header.sameSite, 'Strict')\n  })\n\n  it('converts to string correctly', () => {\n    let header = new SetCookie('session=abc123')\n    header.domain = 'example.com'\n    header.path = '/'\n    header.secure = true\n    header.httpOnly = true\n    header.sameSite = 'Lax'\n    header.maxAge = 0\n\n    assert.equal(\n      header.toString(),\n      'session=abc123; Domain=example.com; HttpOnly; Max-Age=0; Path=/; SameSite=Lax; Secure',\n    )\n  })\n\n  it('converts to an empty string when name is not set', () => {\n    let header = new SetCookie()\n    header.value = 'test'\n    assert.equal(header.toString(), '')\n  })\n\n  it('handles quoted values', () => {\n    let header = new SetCookie('complex=\"quoted value; with semicolon\"')\n    assert.equal(header.name, 'complex')\n    assert.equal(header.value, 'quoted value; with semicolon')\n  })\n\n  it('parses and formats expires attribute correctly', () => {\n    let expiresDate = new Date('Wed, 21 Oct 2015 07:28:00 GMT')\n    let header = new SetCookie(`test=value; Expires=${expiresDate.toUTCString()}`)\n    assert.equal(header.expires?.toUTCString(), expiresDate.toUTCString())\n\n    header.expires = new Date('Thu, 22 Oct 2015 07:28:00 GMT')\n    assert.equal(header.toString(), 'test=value; Expires=Thu, 22 Oct 2015 07:28:00 GMT')\n  })\n\n  it('handles SameSite attribute case-insensitively', () => {\n    let header = new SetCookie('test=value; SameSite=lax')\n    assert.equal(header.sameSite, 'Lax')\n\n    header = new SetCookie('test=value; SameSite=STRICT')\n    assert.equal(header.sameSite, 'Strict')\n\n    header = new SetCookie('test=value; SameSite=NoNe')\n    assert.equal(header.sameSite, 'None')\n  })\n\n  it('handles cookies with empty value', () => {\n    let header = new SetCookie('name=')\n    assert.equal(header.name, 'name')\n    assert.equal(header.value, '')\n  })\n\n  it('handles multiple identical attributes', () => {\n    let header = new SetCookie('test=value; Path=/; Path=/api')\n    assert.equal(header.path, '/api')\n  })\n\n  it('ignores unknown attributes', () => {\n    let header = new SetCookie('test=value; Unknown=something')\n    assert.equal(header.toString(), 'test=value')\n  })\n\n  it('handles Max-Age as a number', () => {\n    let header = new SetCookie('test=value; Max-Age=3600')\n    assert.equal(header.maxAge, 3600)\n  })\n\n  it('ignores invalid Max-Age', () => {\n    let header = new SetCookie('test=value; Max-Age=invalid')\n    assert.equal(header.maxAge, undefined)\n  })\n\n  it('handles missing value in attributes', () => {\n    let header = new SetCookie('test=value; Domain=; Path')\n    assert.equal(header.domain, '')\n    assert.equal(header.path, undefined)\n  })\n\n  it('preserves the case of the cookie name and value', () => {\n    let header = new SetCookie('TestName=TestValue')\n    assert.equal(header.name, 'TestName')\n    assert.equal(header.value, 'TestValue')\n  })\n\n  it('handles setting new name and value', () => {\n    let header = new SetCookie('old=value')\n    header.name = 'new'\n    header.value = 'newvalue'\n    assert.equal(header.toString(), 'new=newvalue')\n  })\n\n  it('correctly quotes values when necessary', () => {\n    let header = new SetCookie('test=value')\n    header.value = 'need; quotes'\n    assert.equal(header.toString(), 'test=\"need; quotes\"')\n  })\n})\n\ndescribe('SetCookie.from', () => {\n  it('parses a string value', () => {\n    let result = SetCookie.from('session=abc123; Path=/; HttpOnly')\n    assert.ok(result instanceof SetCookie)\n    assert.equal(result.name, 'session')\n    assert.equal(result.value, 'abc123')\n    assert.equal(result.path, '/')\n    assert.equal(result.httpOnly, true)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/set-cookie.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\nimport { parseParams, quote } from './param-values.ts'\nimport { capitalize, isValidDate } from './utils.ts'\n\ntype SameSiteValue = 'Strict' | 'Lax' | 'None'\n\n/**\n * Properties for a `Set-Cookie` header value.\n */\nexport interface CookieProperties {\n  /**\n   * The domain of the cookie. For example, `example.com`.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value)\n   */\n  domain?: string\n  /**\n   * The expiration date of the cookie. If not specified, the cookie is a session cookie that is\n   * removed when the browser is closed.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#expiresdate)\n   */\n  expires?: Date\n  /**\n   * Indicates this cookie should not be accessible via JavaScript.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#httponly)\n   */\n  httpOnly?: boolean\n  /**\n   * The maximum age of the cookie in seconds.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#max-age)\n   */\n  maxAge?: number\n  /**\n   * Indicates this cookie is a partitioned cookie.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#partitioned)\n   */\n  partitioned?: boolean\n  /**\n   * The path of the cookie. For example, `/` or `/admin`.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value)\n   */\n  path?: string\n  /**\n   * The `SameSite` attribute of the cookie. This attribute lets servers require that a cookie shouldn't be sent with\n   * cross-site requests, which provides some protection against cross-site request forgery attacks.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value)\n   */\n  sameSite?: SameSiteValue\n  /**\n   * Indicates the cookie should only be sent over HTTPS.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#secure)\n   */\n  secure?: boolean\n}\n\n/**\n * Initializer for a `Set-Cookie` header value.\n */\nexport interface SetCookieInit extends CookieProperties {\n  /**\n   * The name of the cookie.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie-namecookie-value)\n   */\n  name?: string\n  /**\n   * The value of the cookie.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie-namecookie-value)\n   */\n  value?: string\n}\n\n/**\n * The value of a `Set-Cookie` HTTP header.\n *\n * [MDN `Set-Cookie` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)\n *\n * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1)\n */\nexport class SetCookie implements HeaderValue, SetCookieInit {\n  /**\n   * The cookie domain attribute.\n   */\n  domain?: string\n\n  /**\n   * The cookie expiration date.\n   */\n  expires?: Date\n\n  /**\n   * Whether the `HttpOnly` attribute is present.\n   */\n  httpOnly?: boolean\n\n  /**\n   * The `Max-Age` attribute value in seconds.\n   */\n  maxAge?: number\n\n  /**\n   * The cookie name.\n   */\n  name?: string\n\n  /**\n   * Whether the `Partitioned` attribute is present.\n   */\n  partitioned?: boolean\n\n  /**\n   * The cookie path attribute.\n   */\n  path?: string\n\n  /**\n   * The `SameSite` attribute value.\n   */\n  sameSite?: SameSiteValue\n\n  /**\n   * Whether the `Secure` attribute is present.\n   */\n  secure?: boolean\n\n  /**\n   * The cookie value.\n   */\n  value?: string\n\n  constructor(init?: string | SetCookieInit) {\n    if (init) return SetCookie.from(init)\n  }\n\n  /**\n   * Returns the string representation of the header value.\n   *\n   * @returns The header value as a string\n   */\n  toString(): string {\n    if (!this.name) {\n      return ''\n    }\n\n    let parts = [`${this.name}=${quote(this.value || '')}`]\n\n    if (this.domain) {\n      parts.push(`Domain=${this.domain}`)\n    }\n    if (this.expires) {\n      parts.push(`Expires=${this.expires.toUTCString()}`)\n    }\n    if (this.httpOnly) {\n      parts.push('HttpOnly')\n    }\n    if (this.maxAge != null) {\n      parts.push(`Max-Age=${this.maxAge}`)\n    }\n    if (this.partitioned) {\n      parts.push('Partitioned')\n    }\n    if (this.path) {\n      parts.push(`Path=${this.path}`)\n    }\n    if (this.sameSite) {\n      parts.push(`SameSite=${this.sameSite}`)\n    }\n    if (this.secure) {\n      parts.push('Secure')\n    }\n\n    return parts.join('; ')\n  }\n\n  /**\n   * Parse a Set-Cookie header value.\n   *\n   * @param value The header value (string, init object, or null)\n   * @returns A SetCookie instance (empty if null)\n   */\n  static from(value: string | SetCookieInit | null): SetCookie {\n    let header = new SetCookie()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        let params = parseParams(value)\n        if (params.length > 0) {\n          header.name = params[0][0]\n          header.value = params[0][1]\n\n          for (let [key, val] of params.slice(1)) {\n            switch (key.toLowerCase()) {\n              case 'domain':\n                header.domain = val\n                break\n              case 'expires': {\n                if (typeof val === 'string') {\n                  let date = new Date(val)\n                  if (isValidDate(date)) {\n                    header.expires = date\n                  }\n                }\n                break\n              }\n              case 'httponly':\n                header.httpOnly = true\n                break\n              case 'max-age': {\n                if (typeof val === 'string') {\n                  let v = parseInt(val, 10)\n                  if (!isNaN(v)) header.maxAge = v\n                }\n                break\n              }\n              case 'partitioned':\n                header.partitioned = true\n                break\n              case 'path':\n                header.path = val\n                break\n              case 'samesite':\n                if (typeof val === 'string' && /strict|lax|none/i.test(val)) {\n                  header.sameSite = capitalize(val) as SameSiteValue\n                }\n                break\n              case 'secure':\n                header.secure = true\n                break\n            }\n          }\n        }\n      } else {\n        header.domain = value.domain\n        header.expires = value.expires\n        header.httpOnly = value.httpOnly\n        header.maxAge = value.maxAge\n        header.name = value.name\n        header.partitioned = value.partitioned\n        header.path = value.path\n        header.sameSite = value.sameSite\n        header.secure = value.secure\n        header.value = value.value\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/src/lib/utils.ts",
    "content": "export function capitalize(str: string): string {\n  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()\n}\n\nexport function isIterable<T>(value: any): value is Iterable<T> {\n  return value != null && typeof value[Symbol.iterator] === 'function'\n}\n\nexport function isValidDate(date: unknown): boolean {\n  return date instanceof Date && !isNaN(date.getTime())\n}\n\nexport function quoteEtag(tag: string): string {\n  return tag === '*' ? tag : /^(W\\/)?\".*\"$/.test(tag) ? tag : `\"${tag}\"`\n}\n\n/**\n * Removes milliseconds from a timestamp, returning seconds.\n * HTTP dates only have second precision, so this is useful for date comparisons.\n *\n * @param time The timestamp or Date to truncate\n * @returns The timestamp in seconds (milliseconds removed)\n */\nexport function removeMilliseconds(time: number | Date): number {\n  let timestamp = time instanceof Date ? time.getTime() : time\n  return Math.floor(timestamp / 1000)\n}\n\nconst imfFixdatePattern =\n  /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\\d{4}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$/\n\n/**\n * Parses an HTTP date header value.\n *\n * HTTP dates must follow RFC 7231 IMF-fixdate format:\n * \"Day, DD Mon YYYY HH:MM:SS GMT\" (e.g., \"Wed, 21 Oct 2015 07:28:00 GMT\")\n *\n * [RFC 7231 Section 7.1.1.1](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.1)\n *\n * @param dateString The HTTP date string to parse\n * @returns The timestamp in milliseconds, or null if invalid\n */\nexport function parseHttpDate(dateString: string): number | null {\n  if (!imfFixdatePattern.test(dateString)) {\n    return null\n  }\n\n  let timestamp = Date.parse(dateString)\n  if (isNaN(timestamp)) {\n    return null\n  }\n\n  return timestamp\n}\n"
  },
  {
    "path": "packages/headers/src/lib/vary.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { Vary } from './vary.ts'\n\ndescribe('Vary', () => {\n  it('initializes with an empty string', () => {\n    let header = new Vary('')\n    assert.equal(header.size, 0)\n    assert.deepEqual(header.headerNames, [])\n  })\n\n  it('initializes with a string', () => {\n    let header = new Vary('Accept-Encoding, Accept-Language')\n    assert.deepEqual(header.headerNames, ['accept-encoding', 'accept-language'])\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with an array', () => {\n    let header = new Vary(['Accept-Encoding', 'Accept-Language'])\n    assert.deepEqual(header.headerNames, ['accept-encoding', 'accept-language'])\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with an object', () => {\n    let header = new Vary({ headerNames: ['Accept-Encoding', 'Accept-Language'] })\n    assert.deepEqual(header.headerNames, ['accept-encoding', 'accept-language'])\n    assert.equal(header.size, 2)\n  })\n\n  it('initializes with another Vary', () => {\n    let header = new Vary(new Vary('Accept-Encoding, Accept-Language'))\n    assert.deepEqual(header.headerNames, ['accept-encoding', 'accept-language'])\n    assert.equal(header.size, 2)\n  })\n\n  it('handles whitespace in initial value', () => {\n    let header = new Vary('  Accept-Encoding  ,   Accept-Language  ')\n    assert.deepEqual(header.headerNames, ['accept-encoding', 'accept-language'])\n  })\n\n  it('normalizes header names to lowercase', () => {\n    let header = new Vary(['Accept-Encoding', 'ACCEPT-LANGUAGE', 'user-agent'])\n    assert.deepEqual(header.headerNames, ['accept-encoding', 'accept-language', 'user-agent'])\n  })\n\n  it('gets all header names', () => {\n    let header = new Vary('Accept-Encoding, Accept-Language')\n    assert.deepEqual(header.headerNames, ['accept-encoding', 'accept-language'])\n  })\n\n  it('returns size', () => {\n    let header = new Vary('Accept-Encoding, Accept-Language, User-Agent')\n    assert.equal(header.size, 3)\n  })\n\n  it('checks if a header name exists (case-insensitive)', () => {\n    let header = new Vary('Accept-Encoding, Accept-Language')\n    assert.equal(header.has('accept-encoding'), true)\n    assert.equal(header.has('Accept-Encoding'), true)\n    assert.equal(header.has('ACCEPT-ENCODING'), true)\n    assert.equal(header.has('user-agent'), false)\n  })\n\n  it('adds a header name', () => {\n    let header = new Vary()\n    header.add('Accept-Encoding')\n    assert.equal(header.has('accept-encoding'), true)\n    assert.equal(header.size, 1)\n  })\n\n  it('adds multiple header names', () => {\n    let header = new Vary()\n    header.add('Accept-Encoding')\n    header.add('Accept-Language')\n    header.add('User-Agent')\n    assert.equal(header.size, 3)\n    assert.deepEqual(header.headerNames, ['accept-encoding', 'accept-language', 'user-agent'])\n  })\n\n  it('does not add duplicate header names (case-insensitive)', () => {\n    let header = new Vary('Accept-Encoding')\n    header.add('accept-encoding')\n    header.add('ACCEPT-ENCODING')\n    header.add('Accept-Encoding')\n    assert.equal(header.size, 1)\n    assert.deepEqual(header.headerNames, ['accept-encoding'])\n  })\n\n  it('handles empty header names', () => {\n    let header = new Vary()\n    header.add('')\n    header.add('  ')\n    assert.equal(header.size, 0)\n  })\n\n  it('deletes a header name', () => {\n    let header = new Vary(['Accept-Encoding', 'Accept-Language'])\n    header.delete('Accept-Encoding')\n    assert.equal(header.has('Accept-Encoding'), false)\n    assert.equal(header.size, 1)\n    assert.deepEqual(header.headerNames, ['accept-language'])\n  })\n\n  it('deletes a header name (case-insensitive)', () => {\n    let header = new Vary(['Accept-Encoding', 'Accept-Language'])\n    header.delete('accept-encoding')\n    assert.equal(header.has('Accept-Encoding'), false)\n    assert.equal(header.size, 1)\n  })\n\n  it('clears all header names', () => {\n    let header = new Vary(['Accept-Encoding', 'Accept-Language', 'User-Agent'])\n    header.clear()\n    assert.equal(header.size, 0)\n    assert.deepEqual(header.headerNames, [])\n  })\n\n  it('converts to string', () => {\n    let header = new Vary(['Accept-Encoding', 'Accept-Language', 'User-Agent'])\n    assert.equal(header.toString(), 'accept-encoding, accept-language, user-agent')\n  })\n\n  it('converts empty header to empty string', () => {\n    let header = new Vary()\n    assert.equal(header.toString(), '')\n  })\n\n  it('is directly iterable', () => {\n    let header = new Vary(['Accept-Encoding', 'Accept-Language', 'User-Agent'])\n    let names = []\n    for (let name of header) {\n      names.push(name)\n    }\n    assert.deepEqual(names, ['accept-encoding', 'accept-language', 'user-agent'])\n  })\n\n  it('supports forEach', () => {\n    let header = new Vary(['Accept-Encoding', 'Accept-Language'])\n    let names: string[] = []\n    header.forEach((name) => {\n      names.push(name)\n    })\n    assert.deepEqual(names, ['accept-encoding', 'accept-language'])\n  })\n})\n\ndescribe('Vary.from', () => {\n  it('parses a string value', () => {\n    let result = Vary.from('Accept-Encoding, Accept-Language')\n    assert.ok(result instanceof Vary)\n    assert.equal(result.size, 2)\n    assert.equal(result.has('Accept-Encoding'), true)\n    assert.equal(result.has('Accept-Language'), true)\n  })\n\n  it('parses an array value', () => {\n    let result = Vary.from(['Accept-Encoding', 'Accept-Language'])\n    assert.ok(result instanceof Vary)\n    assert.equal(result.size, 2)\n  })\n})\n"
  },
  {
    "path": "packages/headers/src/lib/vary.ts",
    "content": "import { type HeaderValue } from './header-value.ts'\n\n/**\n * Object form for constructing a {@link Vary} header value.\n */\nexport interface VaryInit {\n  /**\n   * The request header names that determine cache eligibility.\n   */\n  headerNames: string[]\n}\n\n/**\n * The value of a `Vary` HTTP header.\n *\n * The `Vary` header indicates which request headers affect whether a cached\n * response can be used, enabling proper content negotiation caching.\n *\n * Header names are normalized to lowercase for case-insensitive comparison.\n *\n * [MDN `Vary` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary)\n *\n * [HTTP/1.1 Specification](https://httpwg.org/specs/rfc9110.html#field.vary)\n */\nexport class Vary implements HeaderValue, VaryInit, Iterable<string> {\n  #set!: Set<string>\n\n  constructor(init?: string | string[] | VaryInit) {\n    if (init) return Vary.from(init)\n    this.#set = new Set()\n  }\n\n  /**\n   * An array of the header names (normalized to lowercase).\n   */\n  get headerNames(): string[] {\n    return Array.from(this.#set)\n  }\n\n  /**\n   * The number of header names in the Vary header.\n   */\n  get size(): number {\n    return this.#set.size\n  }\n\n  /**\n   * Checks if the Vary header includes the given header name (case-insensitive).\n   * @param headerName The header name to check for.\n   * @returns `true` if the header name is present, `false` otherwise.\n   */\n  has(headerName: string): boolean {\n    return this.#set.has(headerName.toLowerCase())\n  }\n\n  /**\n   * Adds a header name to the Vary header (case-insensitive).\n   * If the header name already exists, this is a no-op.\n   * @param headerName The header name to add.\n   */\n  add(headerName: string): void {\n    let trimmed = headerName.trim()\n    if (trimmed) {\n      this.#set.add(trimmed.toLowerCase())\n    }\n  }\n\n  /**\n   * Removes a header name from the Vary header (case-insensitive).\n   * @param headerName The header name to remove.\n   */\n  delete(headerName: string): void {\n    this.#set.delete(headerName.toLowerCase())\n  }\n\n  /**\n   * Removes all header names from the Vary header.\n   */\n  clear(): void {\n    this.#set.clear()\n  }\n\n  /**\n   * Calls a callback function for each header name in the Vary header.\n   * @param callback The callback function to call for each header name.\n   * @param thisArg Optional value to use as `this` when executing the callback.\n   */\n  forEach(callback: (headerName: string, vary: Vary) => void, thisArg?: any): void {\n    for (let headerName of this) {\n      callback.call(thisArg, headerName, this)\n    }\n  }\n\n  /**\n   * Iterates over normalized header names in the `Vary` set.\n   *\n   * @returns An iterator of header names.\n   */\n  [Symbol.iterator](): IterableIterator<string> {\n    return this.#set.values()\n  }\n\n  /**\n   * Returns the serialized `Vary` header value.\n   *\n   * @returns The header value as a comma-separated string.\n   */\n  toString() {\n    return Array.from(this.#set).join(', ')\n  }\n\n  /**\n   * Parse a Vary header value.\n   *\n   * @param value The header value (string, string[], init object, or null)\n   * @returns A Vary instance (empty if null)\n   */\n  static from(value: string | string[] | VaryInit | null): Vary {\n    let header = new Vary()\n\n    if (value !== null) {\n      if (typeof value === 'string') {\n        for (let headerName of value.split(',')) {\n          let trimmed = headerName.trim()\n          if (trimmed) {\n            header.#set.add(trimmed.toLowerCase())\n          }\n        }\n      } else if (Array.isArray(value)) {\n        for (let headerName of value) {\n          let trimmed = headerName.trim()\n          if (trimmed) {\n            header.#set.add(trimmed.toLowerCase())\n          }\n        }\n      } else {\n        for (let headerName of value.headerNames) {\n          let trimmed = headerName.trim()\n          if (trimmed) {\n            header.#set.add(trimmed.toLowerCase())\n          }\n        }\n      }\n    }\n\n    return header\n  }\n}\n"
  },
  {
    "path": "packages/headers/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/headers/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/html-template/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/html-template/CHANGELOG.md",
    "content": "# `html-template` CHANGELOG\n\nThis is the changelog for [`html-template`](https://github.com/remix-run/remix/tree/main/packages/html-template). It follows [semantic versioning](https://semver.org/).\n\n## v0.3.0 (2025-11-05)\n\n- Build using `tsc` instead of `esbuild`. This means modules in the `dist` directory now mirror the layout of modules in the `src` directory.\n\n## v0.2.0 (2025-10-31)\n\n- No real changes, just testing a new release process.\n\n## 0.1.0 (2025-10-25)\n\nThis is the initial release of the `@remix-run/html-template` package.\n\n- `html` tagged template function for HTML string construction with automatic escaping\n- `html.raw` for explicitly marking HTML as safe (no escaping)\n- `isSafeHtml` type guard function\n- `SafeHtml` branded type for type-safe HTML strings\n- Support for composable HTML fragments without double-escaping\n- Support for arrays, primitives, and falsy values in interpolations\n"
  },
  {
    "path": "packages/html-template/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/html-template/README.md",
    "content": "# html-template\n\nSafe HTML template literals for Remix. `html-template` automatically escapes interpolated values to prevent XSS while still supporting explicit trusted HTML insertion.\n\n## Features\n\n- **Automatic HTML escaping** - All interpolated values are escaped by default\n- **Explicit raw HTML** - Use `html.raw` when you need unescaped HTML from trusted sources\n- **Composable** - SafeHtml values can be nested without double-escaping\n- **Type-safe** - Full TypeScript support with branded types\n- **Zero dependencies** - Lightweight and self-contained\n- **Runtime agnostic** - Works in Node.js, Bun, Deno, browsers, and edge runtimes\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```ts\nimport { html } from 'remix/html-template'\n\nlet userInput = '<script>alert(\"XSS\")</script>'\nlet greeting = html`<h1>Hello ${userInput}!</h1>`\n\nconsole.log(String(greeting))\n// Output: <h1>Hello &lt;script&gt;alert(\"XSS\")&lt;/script&gt;!</h1>\n```\n\nBy default, all interpolated values are automatically escaped to prevent XSS attacks.\n\nIf you have trusted HTML that should not be escaped, use `html.raw`:\n\n```ts\nimport { html } from 'remix/html-template'\n\nlet trustedIcon = '<svg>...</svg>'\nlet button = html.raw`<button>${trustedIcon} Click me</button>`\n\nconsole.log(String(button))\n// => <button><svg>...</svg> Click me</button>\n```\n\n**Warning**: Only use `html.raw` with content you trust. Never use it with user input.\n\n### Composing HTML Fragments\n\nSafeHtml values can be nested without double-escaping:\n\n```ts\nimport { html } from 'remix/html-template'\n\nlet title = html`<h1>My Title</h1>`\nlet content = html`<p>Some content with ${userInput}</p>`\n\nlet page = html`\n  <!doctype html>\n  <html>\n    <body>\n      ${title} ${content}\n    </body>\n  </html>\n`\n```\n\n### Working with Arrays\n\nYou can interpolate arrays of values, which will be flattened and joined:\n\n```ts\nimport { html } from 'remix/html-template'\n\nlet items = ['Apple', 'Banana', 'Cherry']\nlet list = html`\n  <ul>\n    ${items.map((item) => html`<li>${item}</li>`)}\n  </ul>\n`\n```\n\n### Conditional Rendering\n\nUse `null` or `undefined` to render nothing:\n\n```ts\nimport { html } from 'remix/html-template'\n\nlet showError = false\nlet errorMessage = 'Something went wrong'\nlet page = html`<div>${showError ? html`<div class=\"error\">${errorMessage}</div>` : null}</div>`\n```\n\n## Related Packages\n\n- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - HTTP router that works great with html-template\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/html-template/package.json",
    "content": "{\n  \"name\": \"@remix-run/html-template\",\n  \"version\": \"0.3.0\",\n  \"description\": \"HTML template tag with auto-escaping for JavaScript\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/html-template\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/html-template#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"html\",\n    \"template\",\n    \"tagged-template\",\n    \"template-literal\",\n    \"safe-html\",\n    \"xss\",\n    \"escaping\",\n    \"sanitize\",\n    \"security\"\n  ]\n}\n"
  },
  {
    "path": "packages/html-template/src/index.ts",
    "content": "export { html, isSafeHtml } from './lib/safe-html.ts'\nexport type { SafeHtml } from './lib/safe-html.ts'\n"
  },
  {
    "path": "packages/html-template/src/lib/safe-html.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { html } from './safe-html.ts'\n\ndescribe('html', () => {\n  it('returns a SafeHtml value', () => {\n    let value = html`<h1>Hello</h1>`\n    assert.equal(String(value), '<h1>Hello</h1>')\n  })\n\n  it('escapes special characters', () => {\n    let value = html`<h1>${'<script>alert(1)</script>'}</h1>`\n    assert.equal(String(value), '<h1>&lt;script&gt;alert(1)&lt;/script&gt;</h1>')\n  })\n\n  it('preserves nested SafeHtml fragments', () => {\n    let value = html`<h1>${html.raw`<b>Hello</b>`} World</h1>`\n    assert.equal(String(value), '<h1><b>Hello</b> World</h1>')\n  })\n\n  it('flattens arrays and preserves nested escapes', () => {\n    // prettier-ignore\n    let value = html`<ul>${['<li>', html.raw`<b>Hello</b>`, '</li>']}</ul>`\n    assert.equal(String(value), '<ul>&lt;li&gt;<b>Hello</b>&lt;/li&gt;</ul>')\n  })\n\n  it('handles numbers and booleans', () => {\n    let value = html`<div>${42} ${true} ${false}</div>`\n    assert.equal(String(value), '<div>42 true false</div>')\n  })\n\n  it('handles null and undefined', () => {\n    let value = html`<div>${null}${undefined}</div>`\n    assert.equal(String(value), '<div></div>')\n  })\n\n  it('throws when not used as a template tag', () => {\n    assert.throws(\n      () => {\n        // @ts-expect-error - Testing runtime behavior\n        html('<div>test</div>')\n      },\n      { message: 'html must be used as a template tag' },\n    )\n  })\n})\n\ndescribe('html.raw', () => {\n  it('does not escape interpolated strings', () => {\n    let rawHtml = '<b>Bold</b>'\n    let value = html.raw`<div>${rawHtml}</div>`\n    assert.equal(String(value), '<div><b>Bold</b></div>')\n  })\n\n  it('does not escape numbers', () => {\n    let num = 42\n    let value = html.raw`<div>${num}</div>`\n    assert.equal(String(value), '<div>42</div>')\n  })\n\n  it('does not escape boolean values', () => {\n    let bool = true\n    let value = html.raw`<div>${bool}</div>`\n    assert.equal(String(value), '<div>true</div>')\n  })\n\n  it('handles null and undefined', () => {\n    let value = html.raw`<div>${null}${undefined}</div>`\n    assert.equal(String(value), '<div></div>')\n  })\n\n  it('handles false', () => {\n    let value = html.raw`<div>${false}</div>`\n    assert.equal(String(value), '<div>false</div>')\n  })\n\n  it('preserves SafeHtml fragments', () => {\n    let icon = html.raw`<i>icon</i>`\n    let value = html.raw`<div>${icon}</div>`\n    assert.equal(String(value), '<div><i>icon</i></div>')\n  })\n\n  it('does not escape dangerous HTML characters', () => {\n    let dangerous = '<script>alert(\"XSS\")</script>'\n    let value = html.raw`<div>${dangerous}</div>`\n    assert.equal(String(value), '<div><script>alert(\"XSS\")</script></div>')\n  })\n\n  it('flattens arrays without escaping', () => {\n    let items = ['<li>A</li>', '<li>B</li>', '<li>C</li>']\n    let value = html.raw`<ul>${items}</ul>`\n    assert.equal(String(value), '<ul><li>A</li><li>B</li><li>C</li></ul>')\n  })\n\n  it('handles nested arrays', () => {\n    let nested = [\n      ['<a>', '<b>'],\n      ['</b>', '</a>'],\n    ]\n    let value = html.raw`<div>${nested}</div>`\n    assert.equal(String(value), '<div><a><b></b></a></div>')\n  })\n\n  it('can be used to build pre-escaped HTML snippets', () => {\n    let userInput = '<script>alert(1)</script>'\n    let escaped = userInput.replace(/</g, '&lt;').replace(/>/g, '&gt;')\n    let value = html.raw`<div class=\"content\">${escaped}</div>`\n    assert.equal(String(value), '<div class=\"content\">&lt;script&gt;alert(1)&lt;/script&gt;</div>')\n  })\n\n  it('works with multiple interpolations', () => {\n    let title = '<h1>Title</h1>'\n    let body = '<p>Content</p>'\n    let footer = '<footer>Footer</footer>'\n    let value = html.raw`<div>${title}${body}${footer}</div>`\n    assert.equal(String(value), '<div><h1>Title</h1><p>Content</p><footer>Footer</footer></div>')\n  })\n\n  it('throws when not used as a template tag', () => {\n    assert.throws(\n      () => {\n        // @ts-expect-error - Testing runtime behavior\n        html.raw('<div>test</div>')\n      },\n      { message: 'html.raw must be used as a template tag' },\n    )\n  })\n})\n"
  },
  {
    "path": "packages/html-template/src/lib/safe-html.ts",
    "content": "// Safe HTML branding\nconst kSafeHtml: unique symbol = Symbol('safeHtml')\n\n/**\n * A string that is safe to render as HTML without escaping.\n */\nexport type SafeHtml = String & { readonly [kSafeHtml]: true }\n\nfunction createSafeHtml(value: string): SafeHtml {\n  let s = new String(value) as SafeHtml\n  ;(s as any)[kSafeHtml] = true\n  return s\n}\n\n/**\n * Checks if a value is a {@link SafeHtml} string.\n *\n * @param value The value to check\n * @returns `true` if the value is a {@link SafeHtml} string\n */\nexport function isSafeHtml(value: unknown): value is SafeHtml {\n  return typeof value === 'object' && value != null && (value as any)[kSafeHtml] === true\n}\n\nconst escapeRe = /[&<>\"']/g\nconst escapeMap = {\n  '&': '&amp;',\n  '<': '&lt;',\n  '>': '&gt;',\n  '\"': '&quot;',\n  \"'\": '&#39;',\n} as const\n\nfunction escapeHtml(text: string): string {\n  return text.replace(escapeRe, (c) => escapeMap[c as keyof typeof escapeMap])\n}\n\ntype Interpolation = SafeHtml | string | number | boolean | null | undefined | Array<Interpolation>\n\nfunction stringifyInterpolation(value: Interpolation): string {\n  if (value == null) return ''\n  if (Array.isArray(value)) return value.map(stringifyInterpolation).join('')\n  if (isSafeHtml(value)) return String(value)\n  if (typeof value === 'string') return escapeHtml(value)\n  if (typeof value === 'number' || typeof value === 'boolean') return escapeHtml(String(value))\n  return escapeHtml(String(value))\n}\n\nfunction stringifyRawInterpolation(value: Interpolation): string {\n  if (value == null) return ''\n  if (Array.isArray(value)) return value.map(stringifyRawInterpolation).join('')\n  if (isSafeHtml(value)) return String(value)\n  if (typeof value === 'string') return value\n  if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n  return String(value)\n}\n\nfunction isTemplateStringsArray(obj: any): obj is TemplateStringsArray {\n  return Array.isArray(obj) && 'raw' in obj\n}\n\n/**\n * Use this helper to escape HTML and create a safe HTML string.\n *\n * ```ts\n * let unsafe = '<script>alert(1)</script>'\n * let safe = html`<h1>${unsafe}</h1>`\n * assert.equal(String(safe), '<h1>&lt;script&gt;alert(1)&lt;/script&gt;</h1>')\n * ```\n *\n * To interpolate raw HTML without escaping, use `html.raw` as a template tag:\n *\n * ```ts\n * let icon = '<b>OK</b>'\n * let safe = html.raw`<div>${icon}</div>`\n * assert.equal(String(safe), '<div><b>Bold</b></div>')\n * ```\n *\n * This has the same semantics as `String.raw` but for HTML snippets that have\n * already been escaped or are from trusted sources.\n */\ntype SafeHtmlHelper = {\n  /**\n   * A tagged template function that escapes interpolated values as HTML.\n   *\n   * @param strings The template strings\n   * @param values The values to interpolate\n   * @returns A `SafeHtml` value\n   */\n  (strings: TemplateStringsArray, ...values: Interpolation[]): SafeHtml\n  /**\n   * A tagged template function that does not escape interpolated values.\n   *\n   * Similar to `String.raw`, this preserves the raw values without escaping.\n   * Only use with trusted content or pre-escaped HTML.\n   *\n   * @param strings The template strings\n   * @param values The values to interpolate\n   * @returns A `SafeHtml` value\n   */\n  raw(strings: TemplateStringsArray, ...values: Interpolation[]): SafeHtml\n}\n\nfunction htmlHelper(strings: TemplateStringsArray, ...values: Interpolation[]): SafeHtml {\n  if (!isTemplateStringsArray(strings)) {\n    throw new TypeError('html must be used as a template tag')\n  }\n\n  let out = ''\n  for (let i = 0; i < strings.length; i++) {\n    out += strings[i]\n    if (i < values.length) out += stringifyInterpolation(values[i])\n  }\n\n  return createSafeHtml(out)\n}\n\n/**\n * Tagged template helper for creating {@link SafeHtml} values.\n */\nexport const html = htmlHelper as SafeHtmlHelper\n\nhtml.raw = (strings, ...values) => {\n  if (!isTemplateStringsArray(strings)) {\n    throw new TypeError('html.raw must be used as a template tag')\n  }\n\n  let out = ''\n  for (let i = 0; i < strings.length; i++) {\n    out += strings[i]\n    if (i < values.length) out += stringifyRawInterpolation(values[i])\n  }\n\n  return createSafeHtml(out)\n}\n"
  },
  {
    "path": "packages/html-template/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/html-template/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/lazy-file/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/lazy-file/CHANGELOG.md",
    "content": "# `lazy-file` CHANGELOG\n\nThis is the changelog for [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file). It follows [semantic versioning](https://semver.org/).\n\n## v5.0.2\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0)\n\n## v5.0.1\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v5.0.0\n\n### Major Changes\n\n- `LazyFile` and `LazyBlob` no longer extend native `File` and `Blob`\n\n  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.\n\n  As a result:\n\n  - `lazyFile instanceof File` now returns `false`\n  - You cannot pass `LazyFile`/`LazyBlob` directly to `new Response(file)` or `formData.append('file', file)`\n  - Passing a `LazyFile`/`LazyBlob` directly to `Response` will throw an error with guidance on correct usage\n\n  **Migration:**\n\n  ```ts\n  // Before\n  let response = new Response(lazyFile)\n\n  // After - streaming\n  let response = new Response(lazyFile.stream())\n\n  // After - for non-streaming APIs that require a complete File (e.g. FormData)\n  formData.append('file', await lazyFile.toFile())\n  ```\n\n  **New methods added:**\n\n  - `LazyFile.toFile()`\n  - `LazyFile.toBlob()`\n  - `LazyBlob.toBlob()`\n\n  **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.\n\n## v4.2.0 (2025-11-26)\n\n- Move `@remix-run/mime` to `peerDependencies`\n\n## v4.1.0 (2025-11-25)\n\n- Replaced `mrmime` dependency with `@remix-run/mime` for MIME type detection\n\n## v4.0.0 (2025-11-20)\n\n- BREAKING CHANGE: Removed `lazy-file/fs` export. Use `@remix-run/fs` package instead.\n\n  ```ts\n  // before\n  import { openFile, writeFile } from '@remix-run/lazy-file/fs'\n\n  // after\n  import { openFile, writeFile } from '@remix-run/fs'\n  ```\n\n## v3.8.0 (2025-11-18)\n\n- 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`.\n\n```ts\n// before\nlet file = openFile('./public/assets/favicon.ico')\nfile.name // \"favicon.ico\"\n\n// after\nlet file = openFile('./public/assets/favicon.ico')\nfile.name // \"./public/assets/favicon.ico\"\n\n// You can still override the name\nlet file = openFile('./public/assets/favicon.ico', { name: 'favicon.ico' })\nfile.name // \"favicon.ico\"\n```\n\n## v3.7.0 (2025-11-04)\n\n- Build using `tsc` instead of `esbuild`. This means modules in the `dist` directory now mirror the layout of modules in the `src` directory.\n- Fix type errors in TypeScript 5.7+ when using typed arrays\n\n## v3.6.0 (2025-10-22)\n\n- 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()`.\n\n## v3.5.0 (2025-07-21)\n\n- Renamed package from `@mjackson/lazy-file` to `@remix-run/lazy-file`\n\n## v3.4.0 (2025-06-10)\n\n- Add `/src` to npm package, so \"go to definition\" goes to the actual source\n- Use one set of types for all built files, instead of separate types for ESM and CJS\n- Build using esbuild directly instead of tsup\n\n## v3.3.1 (2025-01-25)\n\n- 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.\n\n## v3.3.0 (2024-11-14)\n\n- Add CommonJS build\n\n## v3.2.0 (2024-09-12)\n\n- Export `OpenFileOptions` from `lazy-file/fs`\n\n## v3.1.0 (2024-09-04)\n\n- Add writeFile method to `lazy-file/fs` and rename `getFile` => `openFile`\n- Accept an open file descriptor or file handle in `writeFile(fd)`\n\n## v3.0.0 (2024-08-25)\n\n- BREAKING: Do not accept regular string argument to `LazyFile`. This more closely matches `File` behavior\n- BREAKING: Move 4th `LazyFile()` argument `range` into `options.range`\n- BREAKING: Renamed `LazyFileContent` interface to `LazyContent` and `content.read()` => `content.stream()`\n- Added `LazyBlob` (`Blob` subclass) as a complement to `LazyFile`\n- Added `LazyBlobOptions` and `LazyFileOptions` interfaces (`endings` is not supported)\n- Return a `name`-less `Blob` from `file.slice()` to more closely match native `File` behavior\n\n## v2.2.0 (2024-08-24)\n\n- Added support for `getFile(, { lastModified })` to override `file.lastModified`\n- Export `GetFileOptions` interface from `lazy-file/fs`\n\n## v2.1.0 (2024-08-24)\n\n- Added `getFile` helper to `lazy-file/fs` export for reading files from the local filesystem\n\n## v2.0.0 (2024-08-23)\n\n- BREAKING: Do not automatically propagate `name` and `lastModified` in `file.slice()`. This matches the behavior of `File` more closely\n- BREAKING: Remove `LazyFile[Symbol.asyncIterator]` to match the behavior of `File` more closely\n- In `slice(start, end)` make `end` default to `size` instead of `Infinity`. This more closely matches the `File` spec\n- Small perf improvement when streaming content arrays with Blobs in them and ending early\n\n## v1.1.0 (2024-08-22)\n\n- Add ability to initialize a LazyFile with `BlobPart[]`, just like a normal `File`\n- Add async iterator support to LazyFile\n\n## v1.0.0 (2024-08-21)\n\n- Initial release\n"
  },
  {
    "path": "packages/lazy-file/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/lazy-file/README.md",
    "content": "# lazy-file\n\nA lazy, streaming `Blob`/`File` implementation for JavaScript.\n\nIt 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.\n\n## Features\n\n- **Deferred Loading** - Blob/file contents loaded on demand to minimize memory usage\n- **Familiar Interface** - `LazyBlob` and `LazyFile` implement the same interface as native `Blob` and `File`\n- **Easy Conversion** - Convert to native `ReadableStream` with `.stream()`, or to native `Blob`/`File` with `.toBlob()` and `.toFile()`\n- **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\n- **Slice Support** - Supports [`Blob.slice()`](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice), even on streaming content\n\n## Why You Need This\n\nJavaScript'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:\n\n```ts\nlet file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })\n```\n\nA `LazyFile` improves this model by accepting an additional content type in its constructor: `LazyContent`.\n\n```ts\nlet lazyContent: LazyContent = {\n  /* See below for usage */\n}\nlet lazyFile = new LazyFile(lazyContent, 'hello.txt', { type: 'text/plain' })\n```\n\nAll other `File` functionality works as you'd expect.\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nThe low-level API can be used to create a `LazyFile` that streams content from anywhere:\n\n```ts\nimport { type LazyContent, LazyFile } from 'remix/lazy-file'\n\nlet content: LazyContent = {\n  // The total length of this file in bytes.\n  byteLength: 100000,\n  // A function that provides a stream of data for the file contents,\n  // beginning at the `start` index and ending at `end`.\n  stream(start, end) {\n    // ... read the file contents from somewhere and return a ReadableStream\n    return new ReadableStream({\n      start(controller) {\n        controller.enqueue('X'.repeat(100000).slice(start, end))\n        controller.close()\n      },\n    })\n  },\n}\n\nlet lazyFile = new LazyFile(content, 'example.txt', { type: 'text/plain' })\nawait lazyFile.arrayBuffer() // ArrayBuffer of the file's content\nlazyFile.name // \"example.txt\"\nlazyFile.type // \"text/plain\"\n```\n\nAll file contents are read on-demand and nothing is ever buffered unless you explicitly call `.toFile()` or `.toBlob()`.\n\n### Streaming Content\n\nUse `.stream()` to get a `ReadableStream` for `Response` and other streaming APIs:\n\n```ts\nimport { openLazyFile } from 'remix/fs'\n\nlet lazyFile = openLazyFile('./large-video.mp4')\n\nlet response = new Response(lazyFile.stream(), {\n  headers: {\n    'Content-Type': lazyFile.type,\n    'Content-Length': String(lazyFile.size),\n  },\n})\n```\n\n### Converting to Native File/Blob\n\nFor non-streaming APIs that require a complete `File` or `Blob` (e.g. `FormData`), use `.toFile()` or `.toBlob()`.\n\n```ts\nlet lazyFile = openLazyFile('./document.pdf')\nlet realFile = await lazyFile.toFile()\n\nlet formData = new FormData()\nformData.append('document', realFile)\n```\n\n> **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.\n\n## Related Packages\n\n- [`fs`](https://github.com/remix-run/remix/tree/main/packages/fs) - Filesystem utilities for reading and writing files using the Web `File` API\n- [`file-storage`](https://github.com/remix-run/remix/tree/main/packages/file-storage) - Storage abstraction for files on disk or in memory\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/lazy-file/package.json",
    "content": "{\n  \"name\": \"@remix-run/lazy-file\",\n  \"version\": \"5.0.2\",\n  \"description\": \"Lazy, streaming files for JavaScript\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/lazy-file\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/lazy-file#readme\",\n  \"license\": \"MIT\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"dependencies\": {\n    \"@remix-run/mime\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"file\",\n    \"buffer\",\n    \"blob\"\n  ]\n}\n"
  },
  {
    "path": "packages/lazy-file/src/globals.ts",
    "content": "// This file provides global type augmentation for ReadableStream async iteration.\n// See https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/62651\n\ndeclare global {\n  interface ReadableStream<R = any> {\n    values(options?: { preventCancel?: boolean }): AsyncIterableIterator<R>\n    [Symbol.asyncIterator](): AsyncIterableIterator<R>\n  }\n}\n\nexport {}\n"
  },
  {
    "path": "packages/lazy-file/src/index.ts",
    "content": "import './globals.ts'\n\nexport { type ByteRange, getByteLength, getIndexes } from './lib/byte-range.ts'\nexport {\n  type LazyContent,\n  type LazyBlobOptions,\n  LazyBlob,\n  type LazyFileOptions,\n  LazyFile,\n} from './lib/lazy-file.ts'\n"
  },
  {
    "path": "packages/lazy-file/src/lib/byte-range.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { type ByteRange, getByteLength, getIndexes } from './byte-range.ts'\n\ndescribe('getByteLength', () => {\n  it('returns the correct length', () => {\n    let size = 100\n\n    let range: ByteRange = { start: 10, end: 20 }\n    assert.equal(getByteLength(range, size), 10)\n\n    range = { start: 10, end: -10 }\n    assert.equal(getByteLength(range, size), 80)\n\n    range = { start: -10, end: -10 }\n    assert.equal(getByteLength(range, size), 0)\n\n    range = { start: -10, end: 20 }\n    assert.equal(getByteLength(range, size), 0)\n\n    range = { start: 0, end: Infinity }\n    assert.equal(getByteLength(range, size), 100)\n\n    range = { start: Infinity, end: 0 }\n    assert.equal(getByteLength(range, size), 0)\n\n    range = { start: Infinity, end: Infinity }\n    assert.equal(getByteLength(range, size), 0)\n  })\n})\n\ndescribe('getIndexes', () => {\n  it('returns the correct indexes', () => {\n    let size = 100\n\n    let range: ByteRange = { start: 10, end: 20 }\n    assert.deepEqual(getIndexes(range, size), [10, 20])\n\n    range = { start: 10, end: -10 }\n    assert.deepEqual(getIndexes(range, size), [10, 90])\n\n    range = { start: -10, end: -10 }\n    assert.deepEqual(getIndexes(range, size), [90, 90])\n\n    range = { start: -10, end: 20 }\n    assert.deepEqual(getIndexes(range, size), [90, 90])\n\n    range = { start: 0, end: Infinity }\n    assert.deepEqual(getIndexes(range, size), [0, 100])\n\n    range = { start: Infinity, end: 0 }\n    assert.deepEqual(getIndexes(range, size), [100, 100])\n\n    range = { start: Infinity, end: Infinity }\n    assert.deepEqual(getIndexes(range, size), [100, 100])\n  })\n})\n"
  },
  {
    "path": "packages/lazy-file/src/lib/byte-range.ts",
    "content": "/**\n * A range of bytes in a buffer.\n */\nexport interface ByteRange {\n  /**\n   * The start index of the range (inclusive). If this number is negative, it represents an offset\n   * from the end of the content.\n   */\n  start: number\n  /**\n   * The end index of the range (exclusive). If this number is negative, it represents an offset\n   * from the end of the content. `Infinity` represents the end of the content.\n   */\n  end: number\n}\n\n/**\n * Returns the length of the byte range in a buffer of the given `size`.\n *\n * @param range The byte range\n * @param size The total size of the buffer\n * @returns The length of the byte range\n */\nexport function getByteLength(range: ByteRange, size: number): number {\n  let [start, end] = getIndexes(range, size)\n  return end - start\n}\n\n/**\n * Resolves a byte range to absolute indexes in a buffer of the given `size`.\n *\n * @param range The byte range\n * @param size The total size of the buffer\n * @returns A tuple of `[start, end]` indexes\n */\nexport function getIndexes(range: ByteRange, size: number): [number, number] {\n  let start = Math.min(Math.max(0, range.start < 0 ? size + range.start : range.start), size)\n  let end = Math.min(Math.max(start, range.end < 0 ? size + range.end : range.end), size)\n  return [start, end]\n}\n"
  },
  {
    "path": "packages/lazy-file/src/lib/lazy-file.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { type LazyContent, LazyBlob, LazyFile } from './lazy-file.ts'\n\n// Type assertions: ensure LazyBlob and LazyFile implement all native Blob/File APIs.\nnull as unknown as LazyBlob satisfies Record<keyof Blob, unknown>\nnull as unknown as LazyFile satisfies Record<keyof File, unknown>\n\nfunction createLazyContent(value = ''): LazyContent {\n  let buffer = new TextEncoder().encode(value)\n  return {\n    byteLength: buffer.byteLength,\n    stream() {\n      return new ReadableStream({\n        start(controller) {\n          controller.enqueue(buffer)\n          controller.close()\n        },\n      })\n    },\n  }\n}\n\ndescribe('LazyBlob', () => {\n  it('has the correct size and type', () => {\n    let blob = new LazyBlob(createLazyContent('X'.repeat(100)), {\n      type: 'text/plain',\n    })\n\n    assert.equal(blob.size, 100)\n    assert.equal(blob.type, 'text/plain')\n  })\n\n  it('is not an instance of Blob', () => {\n    let blob = new LazyBlob(createLazyContent('hello'), { type: 'text/plain' })\n    assert.equal(blob instanceof Blob, false)\n  })\n\n  it('has the correct Symbol.toStringTag', () => {\n    let blob = new LazyBlob(createLazyContent('hello'), { type: 'text/plain' })\n    assert.equal(Object.prototype.toString.call(blob), '[object LazyBlob]')\n  })\n\n  it(\"returns the blob's contents as a stream\", async () => {\n    let content = createLazyContent('hello world')\n    let blob = new LazyBlob(content, { type: 'text/plain' })\n\n    let decoder = new TextDecoder()\n    let result = ''\n    for await (let chunk of blob.stream()) {\n      result += decoder.decode(chunk, { stream: true })\n    }\n    result += decoder.decode()\n\n    assert.equal(result, 'hello world')\n  })\n\n  it(\"returns the blob's contents as a string\", async () => {\n    let content = createLazyContent('hello world')\n    let blob = new LazyBlob(content, { type: 'text/plain' })\n\n    assert.equal(await blob.text(), 'hello world')\n  })\n\n  it(\"returns the blob's contents as bytes\", async () => {\n    let content = createLazyContent('hello')\n    let blob = new LazyBlob(content, { type: 'text/plain' })\n    let bytes = await blob.bytes()\n\n    assert.equal(bytes.length, 5)\n    assert.deepEqual(bytes, new TextEncoder().encode('hello'))\n  })\n\n  it(\"returns the blob's contents as an ArrayBuffer\", async () => {\n    let content = createLazyContent('hello')\n    let blob = new LazyBlob(content, { type: 'text/plain' })\n    let buffer = await blob.arrayBuffer()\n\n    assert.equal(buffer.byteLength, 5)\n  })\n\n  describe('toBlob()', () => {\n    it('converts to a native Blob', async () => {\n      let lazyBlob = new LazyBlob(createLazyContent('hello world'), { type: 'text/plain' })\n      let blob = await lazyBlob.toBlob()\n\n      assert.equal(blob instanceof Blob, true)\n      assert.equal(blob.size, 11)\n      assert.equal(blob.type, 'text/plain')\n      assert.equal(await blob.text(), 'hello world')\n    })\n  })\n\n  describe('slice()', () => {\n    it('returns a LazyBlob with the correct size', () => {\n      let blob = new LazyBlob(createLazyContent('hello world'), { type: 'text/plain' })\n      let slice = blob.slice(0, 5)\n      assert.equal(slice instanceof LazyBlob, true)\n      assert.equal(slice.size, 5)\n    })\n  })\n\n  describe('toString()', () => {\n    it('throws a TypeError to prevent misuse with Response', () => {\n      let blob = new LazyBlob(createLazyContent('hello'), { type: 'text/plain' })\n      assert.throws(() => blob.toString(), {\n        name: 'TypeError',\n        message:\n          'Cannot convert LazyBlob to string. Use .stream() to get a ReadableStream for Response and other streaming APIs, or .toBlob() for non-streaming APIs that require a complete Blob (e.g. FormData). Always prefer .stream() when possible.',\n      })\n    })\n  })\n})\n\ndescribe('LazyFile', () => {\n  it('has the correct name, size, type, and lastModified timestamp', () => {\n    let now = Date.now()\n    let lazyFile = new LazyFile(createLazyContent('X'.repeat(100)), 'example.txt', {\n      type: 'text/plain',\n      lastModified: now,\n    })\n\n    assert.equal(lazyFile.name, 'example.txt')\n    assert.equal(lazyFile.size, 100)\n    assert.equal(lazyFile.type, 'text/plain')\n    assert.equal(lazyFile.lastModified, now)\n  })\n\n  it('is not an instance of File', () => {\n    let lazyFile = new LazyFile(createLazyContent('hello'), 'hello.txt', { type: 'text/plain' })\n    assert.equal(lazyFile instanceof File, false)\n  })\n\n  it('has the correct Symbol.toStringTag', () => {\n    let lazyFile = new LazyFile(createLazyContent('hello'), 'hello.txt', { type: 'text/plain' })\n    assert.equal(Object.prototype.toString.call(lazyFile), '[object LazyFile]')\n  })\n\n  it('can be initialized with a [Blob] as the content', async () => {\n    let content = [new Blob(['hello world'], { type: 'text/plain' })]\n    let lazyFile = new LazyFile(content, 'hello.txt', { type: 'text/plain' })\n    assert.equal(lazyFile.size, 11)\n    assert.equal('hello world', await lazyFile.text())\n  })\n\n  it('can be initialized with another LazyFile as the content', async () => {\n    let content = [new LazyFile(['hello world'], 'hello.txt', { type: 'text/plain' })]\n    let lazyFile = new LazyFile(content, 'hello.txt', { type: 'text/plain' })\n    assert.equal(lazyFile.size, 11)\n    assert.equal('hello world', await lazyFile.text())\n  })\n\n  it('can be initialized with multiple Blobs and strings as the content and can slice them correctly', async () => {\n    let parts = [\n      new Blob(['  hello '], { type: 'text/plain' }),\n      'world',\n      new Blob(['!', '  '], { type: 'text/plain' }),\n      'extra stuff',\n    ]\n    let lazyFile = new LazyFile(parts, 'hello.txt', { type: 'text/plain' })\n    assert.equal(lazyFile.size, 27)\n    assert.equal(await lazyFile.slice(2, -13).text(), 'hello world!')\n  })\n\n  it(\"returns the file's contents as a stream\", async () => {\n    let content = createLazyContent('hello world')\n    let lazyFile = new LazyFile(content, 'hello.txt', { type: 'text/plain' })\n\n    let decoder = new TextDecoder()\n    let result = ''\n    for await (let chunk of lazyFile.stream()) {\n      result += decoder.decode(chunk, { stream: true })\n    }\n    result += decoder.decode()\n\n    assert.equal(result, 'hello world')\n  })\n\n  it(\"returns the file's contents as a string\", async () => {\n    let content = createLazyContent('hello world')\n    let lazyFile = new LazyFile(content, 'hello.txt', {\n      type: 'text/plain',\n    })\n\n    assert.equal(await lazyFile.text(), 'hello world')\n  })\n\n  describe('toFile()', () => {\n    it('converts to a native File', async () => {\n      let now = Date.now()\n      let lazyFile = new LazyFile(createLazyContent('hello world'), 'hello.txt', {\n        type: 'text/plain',\n        lastModified: now,\n      })\n      let file = await lazyFile.toFile()\n\n      assert.equal(file instanceof File, true)\n      assert.equal(file.name, 'hello.txt')\n      assert.equal(file.size, 11)\n      assert.equal(file.type, 'text/plain')\n      assert.equal(file.lastModified, now)\n      assert.equal(await file.text(), 'hello world')\n    })\n  })\n\n  describe('toBlob()', () => {\n    it('converts to a native Blob', async () => {\n      let lazyFile = new LazyFile(createLazyContent('hello world'), 'hello.txt', {\n        type: 'text/plain',\n      })\n      let blob = await lazyFile.toBlob()\n\n      assert.equal(blob instanceof Blob, true)\n      assert.equal(blob.size, 11)\n      assert.equal(blob.type, 'text/plain')\n      assert.equal(await blob.text(), 'hello world')\n    })\n  })\n\n  describe('slice()', () => {\n    it('returns a LazyBlob with the same size as the original when slicing from 0 to the end', () => {\n      let lazyFile = new LazyFile(createLazyContent('hello world'), 'hello.txt', {\n        type: 'text/plain',\n      })\n      let slice = lazyFile.slice(0)\n      assert.equal(slice instanceof LazyBlob, true)\n      assert.equal(slice.size, lazyFile.size)\n    })\n\n    it('returns a LazyBlob with size 0 when the \"start\" index is greater than the content length', () => {\n      let lazyFile = new LazyFile(['hello world'], 'hello.txt', {\n        type: 'text/plain',\n      })\n      let slice = lazyFile.slice(100)\n      assert.equal(slice.size, 0)\n    })\n\n    it('returns a LazyBlob with size 0 when the \"start\" index is greater than the \"end\" index', () => {\n      let lazyFile = new LazyFile(['hello world'], 'hello.txt', {\n        type: 'text/plain',\n      })\n      let slice = lazyFile.slice(5, 0)\n      assert.equal(slice.size, 0)\n    })\n\n    it('calls content.stream() with the correct range', (t) => {\n      let content = createLazyContent('X'.repeat(100))\n      let read = t.mock.method(content, 'stream')\n      let lazyFile = new LazyFile(content, 'example.txt', { type: 'text/plain' })\n      lazyFile.slice(10, 20).stream()\n      assert.equal(read.mock.calls.length, 1)\n      assert.deepEqual(read.mock.calls[0].arguments, [10, 20])\n    })\n\n    it('calls content.stream() with the correct range when slicing a file with a negative \"start\" index', (t) => {\n      let content = createLazyContent('X'.repeat(100))\n      let read = t.mock.method(content, 'stream')\n      let lazyFile = new LazyFile(content, 'example.txt', { type: 'text/plain' })\n      lazyFile.slice(-10).stream()\n      assert.equal(read.mock.calls.length, 1)\n      assert.deepEqual(read.mock.calls[0].arguments, [90, 100])\n    })\n\n    it('calls content.stream() with the correct range when slicing a file with a negative \"end\" index', (t) => {\n      let content = createLazyContent('X'.repeat(100))\n      let read = t.mock.method(content, 'stream')\n      let lazyFile = new LazyFile(content, 'example.txt', { type: 'text/plain' })\n      lazyFile.slice(0, -10).stream()\n      assert.equal(read.mock.calls.length, 1)\n      assert.deepEqual(read.mock.calls[0].arguments, [0, 90])\n    })\n\n    it('calls content.stream() with the correct range when slicing a file with negative \"start\" and \"end\" indexes', (t) => {\n      let content = createLazyContent('X'.repeat(100))\n      let read = t.mock.method(content, 'stream')\n      let lazyFile = new LazyFile(content, 'example.txt', { type: 'text/plain' })\n      lazyFile.slice(-20, -10).stream()\n      assert.equal(read.mock.calls.length, 1)\n      assert.deepEqual(read.mock.calls[0].arguments, [80, 90])\n    })\n\n    it('calls content.stream() with the correct range when slicing a file with a \"start\" index greater than the \"end\" index', (t) => {\n      let content = createLazyContent('X'.repeat(100))\n      let read = t.mock.method(content, 'stream')\n      let lazyFile = new LazyFile(content, 'example.txt', { type: 'text/plain' })\n      lazyFile.slice(20, 10).stream()\n      assert.equal(read.mock.calls.length, 1)\n      assert.deepEqual(read.mock.calls[0].arguments, [20, 20])\n    })\n  })\n\n  describe('toString()', () => {\n    it('throws a TypeError to prevent misuse with Response', () => {\n      let lazyFile = new LazyFile(createLazyContent('hello'), 'hello.txt', { type: 'text/plain' })\n      assert.throws(() => lazyFile.toString(), {\n        name: 'TypeError',\n        message:\n          'Cannot convert LazyFile to string. Use .stream() to get a ReadableStream for Response and other streaming APIs, or .toFile()/.toBlob() for non-streaming APIs that require a complete File/Blob (e.g. FormData). Always prefer .stream() when possible.',\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/lazy-file/src/lib/lazy-file.ts",
    "content": "import { type ByteRange, getByteLength, getIndexes } from './byte-range.ts'\n\n/**\n * A streaming interface for blob/file content.\n */\nexport interface LazyContent {\n  /**\n   * The total length of the content.\n   */\n  byteLength: number\n  /**\n   * Returns a stream that can be used to read the content. When given, the `start` index is\n   * inclusive indicating the index of the first byte to read. The `end` index is exclusive\n   * indicating the index of the first byte not to read.\n   *\n   * @param start The start index (inclusive)\n   * @param end The end index (exclusive)\n   * @returns A readable stream of the content\n   */\n  stream(start?: number, end?: number): ReadableStream<Uint8Array<ArrayBuffer>>\n}\n\n/**\n * Options for creating a {@link LazyBlob}.\n */\nexport interface LazyBlobOptions {\n  /**\n   * The range of bytes to include from the content. If not specified, all content is included.\n   */\n  range?: ByteRange\n  /**\n   * The MIME type of the content.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob#type)\n   *\n   * @default ''\n   */\n  type?: string\n}\n\n/**\n * A lazy, streaming alternative to [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob).\n *\n * **Important:** Since `LazyBlob` is not a `Blob` subclass, you cannot pass it directly to APIs\n * that expect a real `Blob` (like `new Response(blob)` or `formData.append('file', blob)`).\n * Instead, use one of:\n *\n * - `.stream()` - Returns a `ReadableStream` for `Response` and other streaming APIs\n * - `.toBlob()` - Returns a `Promise<Blob>` for non-streaming APIs that require a complete `Blob` (e.g. `FormData`)\n *\n * [MDN `Blob` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob)\n */\nexport class LazyBlob {\n  readonly #content: BlobContent\n\n  /**\n   * @param parts The blob parts or lazy content\n   * @param options Options for the blob\n   */\n  constructor(parts: BlobPartLike[] | LazyContent, options?: LazyBlobOptions) {\n    this.#content = new BlobContent(parts, options)\n  }\n\n  /**\n   * The brand string exposed by `Object.prototype.toString.call()`.\n   */\n  get [Symbol.toStringTag](): string {\n    return 'LazyBlob'\n  }\n\n  /**\n   * Returns the blob's contents as an [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer).\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer)\n   *\n   * @returns A promise that resolves to an `ArrayBuffer`\n   */\n  arrayBuffer(): Promise<ArrayBuffer> {\n    return this.#content.arrayBuffer()\n  }\n\n  /**\n   * Returns the blob's contents as a byte array.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/bytes)\n   *\n   * @returns A promise that resolves to a `Uint8Array`\n   */\n  bytes(): Promise<Uint8Array<ArrayBuffer>> {\n    return this.#content.bytes()\n  }\n\n  /**\n   * The size of the blob in bytes.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/size)\n   */\n  get size(): number {\n    return this.#content.size\n  }\n\n  /**\n   * The MIME type of the blob.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/type)\n   */\n  get type(): string {\n    return this.#content.type\n  }\n\n  /**\n   * Returns a new `LazyBlob` that contains the data in the specified range.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice)\n   *\n   * @param start The start index (inclusive)\n   * @param end The end index (exclusive)\n   * @param contentType The content type of the new blob\n   * @returns A new `LazyBlob` containing the sliced data\n   */\n  slice(start?: number, end?: number, contentType?: string): LazyBlob {\n    return this.#content.slice(start, end, contentType)\n  }\n\n  /**\n   * Returns a stream that can be used to read the blob's contents.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/stream)\n   *\n   * @returns A readable stream of the blob's contents\n   */\n  stream(): ReadableStream<Uint8Array<ArrayBuffer>> {\n    return this.#content.stream()\n  }\n\n  /**\n   * Returns the blob's contents as a string.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/text)\n   *\n   * @returns A promise that resolves to the blob's contents as a string\n   */\n  text(): Promise<string> {\n    return this.#content.text()\n  }\n\n  /**\n   * Converts this `LazyBlob` to a native `Blob`.\n   *\n   * **Warning:** This reads the entire content into memory, which defeats the purpose of using\n   * a lazy blob for large files. Only use this for non-streaming APIs that require a complete `Blob`.\n   * Use `.stream()` to get a `ReadableStream` for `Response` and other streaming APIs.\n   *\n   * @returns A promise that resolves to a native `Blob`\n   */\n  async toBlob(): Promise<Blob> {\n    return new Blob([await this.bytes()], { type: this.type })\n  }\n\n  /**\n   * @throws Always throws a TypeError. LazyBlob cannot be implicitly converted to a string.\n   * Use `.stream()` to get a `ReadableStream` for `Response` and other streaming APIs, or `.toBlob()` for non-streaming APIs that require a complete `Blob` (e.g. `FormData`). Always prefer `.stream()` when possible.\n   */\n  toString(): never {\n    throw new TypeError(\n      'Cannot convert LazyBlob to string. Use .stream() to get a ReadableStream for Response and other streaming APIs, or .toBlob() for non-streaming APIs that require a complete Blob (e.g. FormData). Always prefer .stream() when possible.',\n    )\n  }\n}\n\n/**\n * Options for creating a {@link LazyFile}.\n */\nexport interface LazyFileOptions extends LazyBlobOptions {\n  /**\n   * The last modified timestamp of the file in milliseconds.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/File/File#lastmodified)\n   *\n   * @default `Date.now()`\n   */\n  lastModified?: number\n}\n\n/**\n * A lazy, streaming alternative to [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File).\n *\n * **Important:** Since `LazyFile` is not a `File` subclass, you cannot pass it directly to APIs\n * that expect a real `File` (like `new Response(file)` or `formData.append('file', file)`).\n * Instead, use one of:\n *\n * - `.stream()` - Returns a `ReadableStream` for `Response` and other streaming APIs\n * - `.toFile()` - Returns a `Promise<File>` for non-streaming APIs that require a complete `File` (e.g. `FormData`)\n * - `.toBlob()` - Returns a `Promise<Blob>` for non-streaming APIs that require a complete `Blob` (e.g. `FormData`)\n *\n * [MDN `File` Reference](https://developer.mozilla.org/en-US/docs/Web/API/File)\n */\nexport class LazyFile {\n  readonly #content: BlobContent\n\n  /**\n   * The name of the file.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/File/name)\n   */\n  readonly name: string\n\n  /**\n   * The last modified timestamp of the file in milliseconds.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/File/lastModified)\n   */\n  readonly lastModified: number\n\n  /**\n   * Always empty string. This property exists only for structural compatibility with the native\n   * `File` interface. It's a browser-specific property for files selected via `<input type=\"file\">`\n   * with the `webkitdirectory` attribute, which doesn't apply to programmatically created files.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/File/webkitRelativePath)\n   */\n  readonly webkitRelativePath = ''\n\n  /**\n   * @param parts The file parts or lazy content\n   * @param name The name of the file\n   * @param options Options for the file\n   */\n  constructor(parts: BlobPartLike[] | LazyContent, name: string, options?: LazyFileOptions) {\n    this.#content = new BlobContent(parts, options)\n    this.name = name\n    this.lastModified = options?.lastModified ?? Date.now()\n  }\n\n  /**\n   * The brand string exposed by `Object.prototype.toString.call()`.\n   */\n  get [Symbol.toStringTag](): string {\n    return 'LazyFile'\n  }\n\n  /**\n   * Returns the file's content as an [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer).\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer)\n   *\n   * @returns A promise that resolves to an `ArrayBuffer`\n   */\n  arrayBuffer(): Promise<ArrayBuffer> {\n    return this.#content.arrayBuffer()\n  }\n\n  /**\n   * Returns the file's contents as a byte array.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/bytes)\n   *\n   * @returns A promise that resolves to a `Uint8Array`\n   */\n  bytes(): Promise<Uint8Array<ArrayBuffer>> {\n    return this.#content.bytes()\n  }\n\n  /**\n   * The size of the file in bytes.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/size)\n   */\n  get size(): number {\n    return this.#content.size\n  }\n\n  /**\n   * The MIME type of the file.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/type)\n   */\n  get type(): string {\n    return this.#content.type\n  }\n\n  /**\n   * Returns a new `LazyBlob` that contains the data in the specified range.\n   *\n   * Note: Like the native `File.slice()`, this returns a `Blob` (not a `File`).\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice)\n   *\n   * @param start The start index (inclusive)\n   * @param end The end index (exclusive)\n   * @param contentType The content type of the new blob\n   * @returns A new `LazyBlob` containing the sliced data\n   */\n  slice(start?: number, end?: number, contentType?: string): LazyBlob {\n    return this.#content.slice(start, end, contentType)\n  }\n\n  /**\n   * Returns a stream that can be used to read the file's contents.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/stream)\n   *\n   * @returns A readable stream of the file's contents\n   */\n  stream(): ReadableStream<Uint8Array<ArrayBuffer>> {\n    return this.#content.stream()\n  }\n\n  /**\n   * Returns the file's contents as a string.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Blob/text)\n   *\n   * @returns A promise that resolves to the file's contents as a string\n   */\n  text(): Promise<string> {\n    return this.#content.text()\n  }\n\n  /**\n   * Converts this `LazyFile` to a native `Blob`.\n   *\n   * **Warning:** This reads the entire content into memory, which defeats the purpose of using\n   * a lazy file for large files. Only use this for non-streaming APIs that require a complete `Blob`.\n   * Use `.stream()` to get a `ReadableStream` for `Response` and other streaming APIs.\n   *\n   * @returns A promise that resolves to a native `Blob`\n   */\n  async toBlob(): Promise<Blob> {\n    return new Blob([await this.bytes()], { type: this.type })\n  }\n\n  /**\n   * Converts this `LazyFile` to a native `File`.\n   *\n   * **Warning:** This reads the entire content into memory, which defeats the purpose of using\n   * a lazy file for large files. Only use this for non-streaming APIs that require a complete `File`\n   * (e.g. `FormData`). For streaming, use `.stream()` instead.\n   *\n   * @returns A promise that resolves to a native `File`\n   */\n  async toFile(): Promise<File> {\n    return new File([await this.bytes()], this.name, {\n      type: this.type,\n      lastModified: this.lastModified,\n    })\n  }\n\n  /**\n   * @throws Always throws a TypeError. LazyFile cannot be implicitly converted to a string.\n   * Use `.stream()` to get a `ReadableStream` for `Response` and other streaming APIs, or `.toFile()`/`.toBlob()` for non-streaming APIs that require a complete `File`/`Blob` (e.g. `FormData`). Always prefer `.stream()` when possible.\n   */\n  toString(): never {\n    throw new TypeError(\n      'Cannot convert LazyFile to string. Use .stream() to get a ReadableStream for Response and other streaming APIs, or .toFile()/.toBlob() for non-streaming APIs that require a complete File/Blob (e.g. FormData). Always prefer .stream() when possible.',\n    )\n  }\n}\n\n/**\n * Union of Blob and lazy blob types.\n */\ntype BlobLike = Blob | LazyBlob | LazyFile\n\n/**\n * Union of BlobPart and lazy blob types. Used for constructor signatures.\n */\ntype BlobPartLike = BlobPart | LazyBlob | LazyFile\n\nfunction isBlobLike(value: unknown): value is BlobLike {\n  return value instanceof Blob || value instanceof LazyBlob || value instanceof LazyFile\n}\n\nclass BlobContent {\n  readonly source: (BlobLike | Uint8Array<ArrayBuffer>)[] | LazyContent\n  readonly totalSize: number\n  readonly range?: ByteRange\n  readonly type: string\n\n  constructor(parts: BlobPartLike[] | LazyContent, options?: LazyBlobOptions) {\n    if (Array.isArray(parts)) {\n      this.source = []\n      this.totalSize = 0\n\n      for (let part of parts) {\n        if (isBlobLike(part)) {\n          this.source.push(part)\n          this.totalSize += part.size\n        } else {\n          let array: Uint8Array\n          if (typeof part === 'string') {\n            array = new TextEncoder().encode(part)\n          } else if (ArrayBuffer.isView(part)) {\n            array = new Uint8Array(part.buffer, part.byteOffset, part.byteLength)\n          } else {\n            array = new Uint8Array(part)\n          }\n\n          this.source.push(array as Uint8Array<ArrayBuffer>)\n          this.totalSize += array.byteLength\n        }\n      }\n    } else {\n      this.source = parts\n      this.totalSize = parts.byteLength\n    }\n\n    this.range = options?.range\n    this.type = options?.type ?? ''\n  }\n\n  async arrayBuffer(): Promise<ArrayBuffer> {\n    return (await this.bytes()).buffer as ArrayBuffer\n  }\n\n  async bytes(): Promise<Uint8Array<ArrayBuffer>> {\n    let result = new Uint8Array(this.size)\n\n    let offset = 0\n    for await (let chunk of this.stream()) {\n      result.set(chunk, offset)\n      offset += chunk.length\n    }\n\n    return result\n  }\n\n  get size(): number {\n    return this.range != null ? getByteLength(this.range, this.totalSize) : this.totalSize\n  }\n\n  slice(start = 0, end?: number, contentType = ''): LazyBlob {\n    let range: ByteRange =\n      this.range != null\n        ? // file.slice().slice() is additive\n          { start: this.range.start + start, end: this.range.end + (end ?? 0) }\n        : { start, end: end ?? this.size }\n\n    return new LazyBlob(this.source, { range, type: contentType })\n  }\n\n  stream(): ReadableStream<Uint8Array<ArrayBuffer>> {\n    if (this.range != null) {\n      let [start, end] = getIndexes(this.range, this.totalSize)\n      return Array.isArray(this.source)\n        ? streamContentArray(this.source, start, end)\n        : this.source.stream(start, end)\n    }\n\n    return Array.isArray(this.source) ? streamContentArray(this.source) : this.source.stream()\n  }\n\n  async text(): Promise<string> {\n    return new TextDecoder('utf-8').decode(await this.bytes())\n  }\n}\n\nfunction streamContentArray(\n  content: (Blob | Uint8Array<ArrayBuffer>)[],\n  start = 0,\n  end = Infinity,\n): ReadableStream<Uint8Array<ArrayBuffer>> {\n  let index = 0\n  let bytesRead = 0\n\n  return new ReadableStream({\n    async pull(controller) {\n      if (index >= content.length) {\n        controller.close()\n        return\n      }\n\n      let hasPushed = false\n\n      function pushChunk(chunk: Uint8Array<ArrayBuffer>) {\n        let chunkLength = chunk.byteLength\n\n        if (!(bytesRead + chunkLength < start || bytesRead >= end)) {\n          let startIndex = Math.max(start - bytesRead, 0)\n          let endIndex = Math.min(end - bytesRead, chunkLength)\n          controller.enqueue(chunk.subarray(startIndex, endIndex))\n          hasPushed = true\n        }\n\n        bytesRead += chunkLength\n      }\n\n      async function pushPart(part: Blob | Uint8Array<ArrayBuffer>) {\n        if (isBlobLike(part)) {\n          if (bytesRead + part.size <= start) {\n            // We can skip this part entirely.\n            bytesRead += part.size\n            return\n          }\n\n          for await (let chunk of part.stream()) {\n            pushChunk(chunk)\n\n            if (bytesRead >= end) {\n              // We can stop reading now.\n              break\n            }\n          }\n        } else {\n          pushChunk(part)\n        }\n      }\n\n      while (!hasPushed && index < content.length) {\n        await pushPart(content[index++])\n\n        if (bytesRead >= end) {\n          controller.close()\n          break\n        }\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/lazy-file/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"global.d.ts\", \"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/lazy-file/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/logger-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/logger-middleware/CHANGELOG.md",
    "content": "# `logger-middleware` CHANGELOG\n\nThis is the changelog for [`logger-middleware`](https://github.com/remix-run/remix/tree/main/packages/logger-middleware). It follows [semantic versioning](https://semver.org/).\n\n## v0.1.3\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0)\n\n## v0.1.2\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0)\n\n## v0.1.1\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.1.0 (2025-11-19)\n\nInitial release extracted from `@remix-run/fetch-router` v0.9.0.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/logger-middleware/README.md) for more details.\n"
  },
  {
    "path": "packages/logger-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/logger-middleware/README.md",
    "content": "# logger-middleware\n\nHTTP request/response logging middleware for Remix. It logs request metadata and response details with configurable output formats.\n\n## Features\n\n- **Request/Response Logging** - Logs method, path, status, and response metadata\n- **Token-Based Formatting** - Customize log output with built-in placeholders\n- **Structured Timing Data** - Includes request duration and timestamps\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { logger } from 'remix/logger-middleware'\n\nlet router = createRouter({\n  middleware: [logger()],\n})\n\n// Logs: [19/Nov/2025:14:32:10 -0800] GET /users/123 200 1234\n```\n\n### Custom Format\n\nYou can use the `format` option to customize the log format. The following tokens are available:\n\n- `%date` - Date and time in Apache/nginx format (dd/Mon/yyyy:HH:mm:ss ±zzzz)\n- `%dateISO` - Date and time in ISO format\n- `%duration` - Request duration in milliseconds\n- `%contentLength` - Response Content-Length header\n- `%contentType` - Response Content-Type header\n- `%host` - Request URL host\n- `%hostname` - Request URL hostname\n- `%method` - Request method\n- `%path` - Request pathname + search\n- `%pathname` - Request pathname\n- `%port` - Request port\n- `%query` - Request query string (search)\n- `%referer` - Request Referer header\n- `%search` - Request search string\n- `%status` - Response status code\n- `%statusText` - Response status text\n- `%url` - Full request URL\n- `%userAgent` - Request User-Agent header\n\n```ts\nlet router = createRouter({\n  middleware: [\n    logger({\n      format: '%method %path - %status (%duration ms)',\n    }),\n  ],\n})\n// Logs: GET /users/123 - 200 (42 ms)\n```\n\nFor Apache-style combined log format, you can use the following format:\n\n```ts\nlet router = createRouter({\n  middleware: [\n    logger({\n      format: '%host - - [%date] \"%method %path\" %status %contentLength \"%referer\" \"%userAgent\"',\n    }),\n  ],\n})\n```\n\n### Custom Logger\n\nYou can use a custom logger to write logs to a file or other stream.\n\n```ts\nimport { createWriteStream } from 'node:fs'\n\nlet logStream = createWriteStream('access.log', { flags: 'a' })\n\nlet router = createRouter({\n  middleware: [\n    logger({\n      log(message) {\n        logStream.write(message + '\\n')\n      },\n    }),\n  ],\n})\n```\n\n## Related Packages\n\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/logger-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/logger-middleware\",\n  \"version\": \"0.1.3\",\n  \"description\": \"Middleware for logging HTTP requests and responses\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/logger-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/logger-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"logger\",\n    \"logging\",\n    \"http-logger\"\n  ]\n}\n"
  },
  {
    "path": "packages/logger-middleware/src/index.ts",
    "content": "export { type LoggerOptions, logger } from './lib/logger.ts'\n"
  },
  {
    "path": "packages/logger-middleware/src/lib/logger.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createRouter } from '@remix-run/fetch-router'\nimport { route } from '@remix-run/fetch-router/routes'\n\nimport { logger } from './logger.ts'\n\ndescribe('logger', () => {\n  it('logs the request', async () => {\n    let routes = route({\n      home: '/',\n    })\n\n    let messages: string[] = []\n\n    let router = createRouter({\n      middleware: [logger({ log: (message) => messages.push(message) })],\n    })\n\n    router.map(\n      routes.home,\n      () =>\n        new Response('Home', {\n          headers: {\n            'Content-Length': '4',\n            'Content-Type': 'text/plain',\n          },\n        }),\n    )\n\n    let response = await router.fetch('https://remix.run')\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Home')\n\n    assert.equal(messages.length, 1)\n    let message = messages[0]\n    assert.match(message, /\\[\\d{2}\\/\\w{3}\\/\\d{4}:\\d{2}:\\d{2}:\\d{2} [+-]\\d{4}\\] GET \\/ \\d+ \\d+/)\n  })\n})\n"
  },
  {
    "path": "packages/logger-middleware/src/lib/logger.ts",
    "content": "import type { Middleware } from '@remix-run/fetch-router'\n\n/**\n * Options for the {@link logger} middleware.\n */\nexport interface LoggerOptions {\n  /**\n   * The format to use for log messages.\n   *\n   * The following tokens are available:\n   *\n   * - `%date` - The date and time of the request in Apache/nginx log format (dd/Mon/yyyy:HH:mm:ss ±zzzz)\n   * - `%dateISO` - The date and time of the request in ISO format\n   * - `%duration` - The duration of the request in milliseconds\n   * - `%contentLength` - The `Content-Length` header of the response\n   * - `%contentType` - The `Content-Type` header of the response\n   * - `%host` - The host of the request URL\n   * - `%hostname` - The hostname of the request URL\n   * - `%method` - The method of the request\n   * - `%path` - The pathname + search of the request URL\n   * - `%pathname` - The pathname of the request URL\n   * - `%port` - The port of the request\n   * - `%query` - The query (search) string of the request URL\n   * - `%referer` - The `Referer` header of the request\n   * - `%search` - The search string of the request URL\n   * - `%status` - The status code of the response\n   * - `%statusText` - The status text of the response\n   * - `%url` - The full URL of the request\n   * - `%userAgent` - The `User-Agent` header of the request\n   *\n   * @default '[%date] %method %path %status %contentLength'\n   */\n  format?: string\n  /**\n   * The function to use to log messages.\n   *\n   * @default console.log\n   */\n  log?: (message: string) => void\n}\n\n/**\n * Creates a middleware handler that logs various request/response info.\n *\n * @param options Options for the logger\n * @returns The logger middleware\n */\nexport function logger(options: LoggerOptions = {}): Middleware {\n  let { format = '[%date] %method %path %status %contentLength', log = console.log } = options\n\n  return async ({ request, url }, next) => {\n    let start = new Date()\n    let response = await next()\n    let end = new Date()\n\n    let tokens: Record<string, () => string> = {\n      date: () => formatApacheDate(start),\n      dateISO: () => start.toISOString(),\n      duration: () => String(end.getTime() - start.getTime()),\n      contentLength: () => response.headers.get('Content-Length') ?? '-',\n      contentType: () => response.headers.get('Content-Type') ?? '-',\n      host: () => url.host,\n      hostname: () => url.hostname,\n      method: () => request.method,\n      path: () => url.pathname + url.search,\n      pathname: () => url.pathname,\n      port: () => url.port,\n      protocol: () => url.protocol,\n      query: () => url.search,\n      referer: () => request.headers.get('Referer') ?? '-',\n      search: () => url.search,\n      status: () => String(response.status),\n      statusText: () => response.statusText,\n      url: () => url.href,\n      userAgent: () => request.headers.get('User-Agent') ?? '-',\n    }\n\n    let message = format.replace(/%(\\w+)/g, (_, key) => tokens[key]?.() ?? '-')\n\n    log(message)\n\n    return response\n  }\n}\n\nconst months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']\n\n/**\n * Formats a date in Apache/nginx log format: \"dd/Mon/yyyy:HH:mm:ss ±zzzz\"\n * Example: \"23/Sep/2025:11:34:12 -0700\"\n *\n * @param date The date to format\n * @returns The formatted date string\n */\nfunction formatApacheDate(date: Date): string {\n  let day = String(date.getDate()).padStart(2, '0')\n  let month = months[date.getMonth()]\n  let year = date.getFullYear()\n  let hours = String(date.getHours()).padStart(2, '0')\n  let minutes = String(date.getMinutes()).padStart(2, '0')\n  let seconds = String(date.getSeconds()).padStart(2, '0')\n\n  // Get timezone offset in minutes and convert to ±HHMM format\n  let timezoneOffset = date.getTimezoneOffset()\n  let sign = timezoneOffset <= 0 ? '+' : '-'\n  let offsetHours = String(Math.floor(Math.abs(timezoneOffset) / 60)).padStart(2, '0')\n  let offsetMinutes = String(Math.abs(timezoneOffset) % 60).padStart(2, '0')\n  let timezone = `${sign}${offsetHours}${offsetMinutes}`\n\n  return `${day}/${month}/${year}:${hours}:${minutes}:${seconds} ${timezone}`\n}\n"
  },
  {
    "path": "packages/logger-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/logger-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/method-override-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/method-override-middleware/CHANGELOG.md",
    "content": "# `method-override-middleware` CHANGELOG\n\nThis 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/).\n\n## v0.1.4\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0)\n\n## v0.1.3\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0)\n\n## v0.1.2\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.1.1 (2025-11-25)\n\n- Re-use request methods from `fetch-router`\n\n## v0.1.0 (2025-11-19)\n\nInitial release extracted from `@remix-run/fetch-router` v0.9.0.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/method-override-middleware/README.md) for more details.\n"
  },
  {
    "path": "packages/method-override-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/method-override-middleware/README.md",
    "content": "# method-override-middleware\n\nMethod override middleware for Remix. It allows HTML forms to simulate `PUT`, `PATCH`, and `DELETE` requests using a hidden form field.\n\n## Features\n\n- **Form Method Overrides** - Translate posted form fields into request methods\n- **HTML Form Friendly** - Supports REST-style routes from standard browser forms\n- **Configurable Field Name** - Choose a custom override field key\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nThis 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.\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { formData } from 'remix/form-data-middleware'\nimport { methodOverride } from 'remix/method-override-middleware'\n\nlet router = createRouter({\n  // methodOverride must come AFTER formData middleware\n  middleware: [formData(), methodOverride()],\n})\n\nrouter.delete('/users/:id', async (context) => {\n  let userId = context.params.id\n  // Delete user logic...\n  return new Response('User deleted')\n})\n```\n\nIn your HTML form:\n\n```html\n<form method=\"POST\" action=\"/users/123\">\n  <input type=\"hidden\" name=\"_method\" value=\"DELETE\" />\n  <button type=\"submit\">Delete User</button>\n</form>\n```\n\n### Custom Field Name\n\nYou can customize the name of the method override field by passing a `fieldName` option to the `methodOverride()` middleware.\n\n```ts\nlet router = createRouter({\n  middleware: [formData(), methodOverride({ fieldName: '__method__' })],\n})\n```\n\n```html\n<form method=\"POST\" action=\"/users/123\">\n  <input type=\"hidden\" name=\"__method__\" value=\"PUT\" />\n  <button type=\"submit\">Update User</button>\n</form>\n```\n\n## Related Packages\n\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n- [`form-data-middleware`](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) - Required for parsing form data\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/method-override-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/method-override-middleware\",\n  \"version\": \"0.1.4\",\n  \"description\": \"Middleware for overriding HTTP request methods from form data\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/method-override-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/method-override-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/form-data-middleware\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"method-override\",\n    \"http-method\"\n  ]\n}\n"
  },
  {
    "path": "packages/method-override-middleware/src/index.ts",
    "content": "export { type MethodOverrideOptions, methodOverride } from './lib/method-override.ts'\n"
  },
  {
    "path": "packages/method-override-middleware/src/lib/method-override.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createRouter } from '@remix-run/fetch-router'\nimport { formData } from '@remix-run/form-data-middleware'\n\nimport { methodOverride } from './method-override.ts'\n\ndescribe('methodOverride middleware', () => {\n  it('overrides the request method with the value of the method override field', async () => {\n    let router = createRouter({\n      middleware: [formData(), methodOverride()],\n    })\n\n    router.post('/', () => new Response('Created'))\n    router.delete('/', () => new Response('Deleted'))\n\n    let response = await router.fetch('https://remix.run', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: '_method=DELETE',\n    })\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Deleted')\n  })\n})\n"
  },
  {
    "path": "packages/method-override-middleware/src/lib/method-override.ts",
    "content": "import { RequestMethods } from '@remix-run/fetch-router'\nimport type { Middleware, RequestContext, RequestMethod } from '@remix-run/fetch-router'\n\n/**\n * Options for the {@link methodOverride} middleware.\n */\nexport interface MethodOverrideOptions {\n  /**\n   * The name of the form field to check for request method override.\n   *\n   * @default '_method'\n   */\n  fieldName?: string\n}\n\n/**\n * Middleware that overrides `context.method` with the value of the method override field.\n *\n * Note: This middleware must be placed after the\n * {@link import('@remix-run/form-data-middleware').formData} middleware in the middleware\n * chain, or some other middleware that provides `context.get(FormData)`.\n *\n * @param options Options for the method override middleware\n * @returns A middleware that overrides `context.method` with the value of the method override field\n */\nexport function methodOverride(options?: MethodOverrideOptions): Middleware {\n  let fieldName = options?.fieldName ?? '_method'\n\n  return (context: RequestContext) => {\n    let method = context.has(FormData) ? context.get(FormData).get(fieldName) : null\n    if (typeof method !== 'string') {\n      return\n    }\n\n    let requestMethod = method.toUpperCase() as RequestMethod\n\n    if (RequestMethods.includes(requestMethod)) {\n      context.method = requestMethod\n    }\n  }\n}\n"
  },
  {
    "path": "packages/method-override-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/method-override-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/mime/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/mime/CHANGELOG.md",
    "content": "# `mime` CHANGELOG\n\nThis is the changelog for [`mime`](https://github.com/remix-run/remix/tree/main/packages/mime). It follows [semantic versioning](https://semver.org/).\n\n## v0.4.0\n\n### Minor Changes\n\n- Include all MIME types from mime-db, including experimental (`x-`) and vendor-specific (`vnd.`) types.\n\n## v0.3.0\n\n### Minor Changes\n\n- 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.\n\n  ```ts\n  import { defineMimeType, detectMimeType } from '@remix-run/mime'\n\n  defineMimeType({\n    extensions: 'myformat',\n    mimeType: 'application/x-myformat',\n  })\n\n  detectMimeType('file.myformat') // 'application/x-myformat'\n  ```\n\n## v0.2.0 (2025-12-18)\n\n- Add `detectContentType(extension)` function that returns a Content-Type header value with `charset` for text-based types.\n\n- Add `mimeTypeToContentType(mimeType)` function that converts a MIME type to a Content-Type header value, adding `charset` for text-based types.\n\n## v0.1.0 (2025-11-25)\n\nInitial release of this package.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/mime/README.md) for more details.\n"
  },
  {
    "path": "packages/mime/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/mime/README.md",
    "content": "# mime\n\nMIME type detection and content-type helpers for Remix. This package maps extensions to MIME types and provides utilities for charset and compressibility checks.\n\n## Features\n\n- **MIME Detection** - Detect MIME types from extensions and filenames\n- **Content-Type Helpers** - Build `Content-Type` values with charset handling\n- **Compression Signals** - Check whether a media type is likely compressible\n- **Generated Data** - Built from [mime-db](https://github.com/jshttp/mime-db)\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n### `detectMimeType(extension)`\n\nDetects the MIME type for a given file extension or filename.\n\n```ts\nimport { detectMimeType } from 'remix/mime'\n\ndetectMimeType('txt') // 'text/plain'\ndetectMimeType('.txt') // 'text/plain'\ndetectMimeType('file.txt') // 'text/plain'\ndetectMimeType('path/to/file.txt') // 'text/plain'\ndetectMimeType('unknown') // undefined\n```\n\n### `detectContentType(extension)`\n\nDetects the Content-Type header value for a given file extension or filename, including `charset` for text-based types. See [`mimeTypeToContentType`](#mimetypetocontenttypemimetype) for charset logic.\n\n```ts\nimport { detectContentType } from 'remix/mime'\n\ndetectContentType('css') // 'text/css; charset=utf-8'\ndetectContentType('.json') // 'application/json; charset=utf-8'\ndetectContentType('image.png') // 'image/png'\ndetectContentType('path/to/file.unknown') // undefined\n```\n\n### `isCompressibleMimeType(mimeType)`\n\nChecks if a MIME type is known to be compressible.\n\n```ts\nimport { isCompressibleMimeType } from 'remix/mime'\n\nisCompressibleMimeType('text/html') // true\nisCompressibleMimeType('application/json') // true\nisCompressibleMimeType('image/png') // false\nisCompressibleMimeType('video/mp4') // false\n```\n\nFor convenience, the function also accepts a full Content-Type header value:\n\n```ts\nimport { isCompressibleMimeType } from 'remix/mime'\n\nisCompressibleMimeType('text/html; charset=utf-8') // true\nisCompressibleMimeType('application/json; charset=utf-8') // true\nisCompressibleMimeType('image/png; charset=utf-8') // false\nisCompressibleMimeType('video/mp4; charset=utf-8') // false\n```\n\n### `mimeTypeToContentType(mimeType)`\n\nConverts 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.\n\n```ts\nimport { mimeTypeToContentType } from 'remix/mime'\n\nmimeTypeToContentType('text/css') // 'text/css; charset=utf-8'\nmimeTypeToContentType('application/json') // 'application/json; charset=utf-8'\nmimeTypeToContentType('application/ld+json') // 'application/ld+json; charset=utf-8'\nmimeTypeToContentType('image/png') // 'image/png'\n```\n\n### `defineMimeType(definition)`\n\nRegisters or overrides a MIME type for one or more file extensions.\n\n```ts\nimport { defineMimeType } from 'remix/mime'\n\ndefineMimeType({\n  extensions: ['myformat'],\n  mimeType: 'application/x-myformat',\n})\n```\n\nYou can also optionally configure the charset and whether the MIME type is compressible:\n\n```ts\ndefineMimeType({\n  extensions: ['myformat'],\n  mimeType: 'application/x-myformat',\n  compressible: true,\n  charset: 'utf-8',\n})\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/mime/package.json",
    "content": "{\n  \"name\": \"@remix-run/mime\",\n  \"version\": \"0.4.0\",\n  \"description\": \"Utilities for working with MIME types\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/mime\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/mime#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"mime-db\": \"^1.53.0\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"codegen\": \"node ./scripts/codegen.ts\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"mime\",\n    \"mime-type\",\n    \"media-type\",\n    \"content-type\",\n    \"compressible\"\n  ]\n}\n"
  },
  {
    "path": "packages/mime/scripts/codegen.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { readFileSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, join } from 'node:path'\nimport { describe, it } from 'node:test'\n\nimport { generateCompressibleMimeTypesContent, generateMimeTypesContent } from './codegen.ts'\n\ndescribe('generated files', () => {\n  // Normalize line endings for cross-platform compatibility\n  let normalizeLineEndings = (str: string) => str.replace(/\\r\\n/g, '\\n')\n\n  it('compressible-mime-types.ts is up to date with mime-db and has not been modified manually', () => {\n    let __dirname = dirname(fileURLToPath(import.meta.url))\n    let generatedPath = join(__dirname, '../src/generated/compressible-mime-types.ts')\n\n    let expectedContent = generateCompressibleMimeTypesContent()\n    let actualContent = readFileSync(generatedPath, 'utf-8')\n\n    assert.equal(\n      normalizeLineEndings(actualContent),\n      normalizeLineEndings(expectedContent),\n      'compressible-mime-types.ts does not match expected output. Run `pnpm codegen` to update it.',\n    )\n  })\n\n  it('mime-types.ts is up to date with mime-db and has not been modified manually', () => {\n    let __dirname = dirname(fileURLToPath(import.meta.url))\n    let generatedPath = join(__dirname, '../src/generated/mime-types.ts')\n\n    let expectedContent = generateMimeTypesContent()\n    let actualContent = readFileSync(generatedPath, 'utf-8')\n\n    assert.equal(\n      normalizeLineEndings(actualContent),\n      normalizeLineEndings(expectedContent),\n      'mime-types.ts does not match expected output. Run `pnpm codegen` to update it.',\n    )\n  })\n})\n"
  },
  {
    "path": "packages/mime/scripts/codegen.ts",
    "content": "import { readFileSync, writeFileSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, join } from 'node:path'\n\nimport { genericCompressibleMimeTypeRegex } from '../src/lib/is-compressible-mime-type.ts'\n\ninterface MimeDbEntry {\n  source?: string\n  compressible?: boolean\n  extensions?: string[]\n}\n\ntype MimeDb = Record<string, MimeDbEntry>\n\n// Generates the content for compressible-mime-types.ts from mime-db\nexport function generateCompressibleMimeTypesContent(): string {\n  let mimeDbPath = fileURLToPath(import.meta.resolve('mime-db/db.json'))\n  let mimeDb: MimeDb = JSON.parse(readFileSync(mimeDbPath, 'utf-8'))\n\n  let compressibleTypes: string[] = []\n\n  for (let [mimeType, entry] of Object.entries(mimeDb)) {\n    if (\n      entry.compressible &&\n      // Skip types already handled by generic compressibility logic\n      !genericCompressibleMimeTypeRegex.test(mimeType)\n    ) {\n      compressibleTypes.push(mimeType)\n    }\n  }\n\n  compressibleTypes.sort()\n\n  return `// DO NOT EDIT. THIS IS GENERATED CODE.\n// Run \\`pnpm codegen\\` to update.\n\nexport let compressibleMimeTypes = new Set([\n${compressibleTypes.map((type) => `  '${type}',`).join('\\n')}\n])\n`\n}\n\n// Generates the content for mime-types.ts from mime-db\nexport function generateMimeTypesContent(): string {\n  let mimeDbPath = fileURLToPath(import.meta.resolve('mime-db/db.json'))\n  let mimeDb: MimeDb = JSON.parse(readFileSync(mimeDbPath, 'utf-8'))\n\n  let isExperimentalOrVendor = (mimeType: string) => /[/](x-|vnd\\.)/.test(mimeType)\n\n  let extensionMap: Record<string, string> = {}\n\n  for (let [mimeType, entry] of Object.entries(mimeDb)) {\n    if (!entry.extensions) continue\n\n    for (let ext of entry.extensions) {\n      // Prefer standard types, then compressible types, then source=iana, then first seen\n      if (!extensionMap[ext]) {\n        extensionMap[ext] = mimeType\n      } else {\n        let existingMimeType = extensionMap[ext]\n        let existingEntry = mimeDb[existingMimeType]\n        let existingIsExperimental = isExperimentalOrVendor(existingMimeType)\n        let newIsExperimental = isExperimentalOrVendor(mimeType)\n\n        // Prefer standard types over experimental/vendor types\n        if (existingIsExperimental && !newIsExperimental) {\n          extensionMap[ext] = mimeType\n        }\n        // If both are standard or both are experimental, apply secondary rules\n        else if (existingIsExperimental === newIsExperimental) {\n          // Prefer compressible types\n          if (entry.compressible && !existingEntry.compressible) {\n            extensionMap[ext] = mimeType\n          }\n          // If both compressible or both not, prefer source=iana\n          else if (entry.compressible === existingEntry.compressible && entry.source === 'iana') {\n            extensionMap[ext] = mimeType\n          }\n        }\n      }\n    }\n  }\n\n  // Sort by extension for consistent output\n  let sortedExtensions = Object.keys(extensionMap).sort()\n  let entries = sortedExtensions.map((ext) => `  ${formatKey(ext)}: '${extensionMap[ext]}',`)\n\n  return `// DO NOT EDIT. THIS IS GENERATED CODE.\n// Run \\`pnpm codegen\\` to update.\n\nexport let mimeTypes: Record<string, string> = {\n${entries.join('\\n')}\n}\n`\n}\n\n// Formats a key for use as a property name in a JavaScript object\nfunction formatKey(key: string): string {\n  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `'${key}'`\n}\n\n// Only run when executed directly (not imported)\nif (import.meta.url === `file://${process.argv[1]}`) {\n  let __dirname = dirname(fileURLToPath(import.meta.url))\n\n  // Generate compressible-mime-types.ts\n  let compressibleOutputPath = join(__dirname, '../src/generated/compressible-mime-types.ts')\n  let compressibleContent = generateCompressibleMimeTypesContent()\n  let compressibleCount = (compressibleContent.match(/  '/g) || []).length\n  writeFileSync(compressibleOutputPath, compressibleContent)\n  console.log(`Generated ${compressibleCount} compressible MIME types to ${compressibleOutputPath}`)\n\n  // Generate mime-types.ts\n  let mimeTypesOutputPath = join(__dirname, '../src/generated/mime-types.ts')\n  let mimeTypesContent = generateMimeTypesContent()\n  let extensionCount = (mimeTypesContent.match(/  '/g) || []).length\n  writeFileSync(mimeTypesOutputPath, mimeTypesContent)\n  console.log(`Generated ${extensionCount} extension mappings to ${mimeTypesOutputPath}`)\n}\n"
  },
  {
    "path": "packages/mime/src/generated/compressible-mime-types.ts",
    "content": "// DO NOT EDIT. THIS IS GENERATED CODE.\n// Run `pnpm codegen` to update.\n\nexport let compressibleMimeTypes = new Set([\n  'application/dart',\n  'application/ecmascript',\n  'application/javascript',\n  'application/json',\n  'application/octet-stream',\n  'application/postscript',\n  'application/raml+yaml',\n  'application/rtf',\n  'application/tar',\n  'application/toml',\n  'application/vnd.dart',\n  'application/vnd.ms-fontobject',\n  'application/vnd.ms-opentype',\n  'application/wasm',\n  'application/x-httpd-php',\n  'application/x-javascript',\n  'application/x-ns-proxy-autoconfig',\n  'application/x-sh',\n  'application/x-tar',\n  'application/x-virtualbox-hdd',\n  'application/x-virtualbox-ova',\n  'application/x-virtualbox-ovf',\n  'application/x-virtualbox-vbox',\n  'application/x-virtualbox-vdi',\n  'application/x-virtualbox-vhd',\n  'application/x-virtualbox-vmdk',\n  'application/x-www-form-urlencoded',\n  'application/xml',\n  'application/xml-dtd',\n  'font/otf',\n  'font/ttf',\n  'image/bmp',\n  'image/vnd.adobe.photoshop',\n  'image/vnd.microsoft.icon',\n  'image/vnd.ms-dds',\n  'image/x-icon',\n  'image/x-ms-bmp',\n  'message/rfc822',\n  'model/gltf-binary',\n  'x-shader/x-fragment',\n  'x-shader/x-vertex',\n])\n"
  },
  {
    "path": "packages/mime/src/generated/mime-types.ts",
    "content": "// DO NOT EDIT. THIS IS GENERATED CODE.\n// Run `pnpm codegen` to update.\n\nexport let mimeTypes: Record<string, string> = {\n  '123': 'application/vnd.lotus-1-2-3',\n  '1km': 'application/vnd.1000minds.decision-model+xml',\n  '210': 'model/step',\n  '3dml': 'text/vnd.in3d.3dml',\n  '3ds': 'image/x-3ds',\n  '3g2': 'video/3gpp2',\n  '3gp': 'video/3gpp',\n  '3gpp': 'audio/3gpp',\n  '3mf': 'model/3mf',\n  '7z': 'application/x-7z-compressed',\n  aab: 'application/x-authorware-bin',\n  aac: 'audio/aac',\n  aam: 'application/x-authorware-map',\n  aas: 'application/x-authorware-seg',\n  abw: 'application/x-abiword',\n  ac: 'application/pkix-attr-cert',\n  acc: 'application/vnd.americandynamics.acc',\n  ace: 'application/x-ace-compressed',\n  acu: 'application/vnd.acucobol',\n  acutc: 'application/vnd.acucorp',\n  adp: 'audio/adpcm',\n  adts: 'audio/aac',\n  aep: 'application/vnd.audiograph',\n  afm: 'application/x-font-type1',\n  afp: 'application/vnd.ibm.modcap',\n  age: 'application/vnd.age',\n  ahead: 'application/vnd.ahead.space',\n  ai: 'application/postscript',\n  aif: 'audio/x-aiff',\n  aifc: 'audio/x-aiff',\n  aiff: 'audio/x-aiff',\n  air: 'application/vnd.adobe.air-application-installer-package+zip',\n  ait: 'application/vnd.dvb.ait',\n  ami: 'application/vnd.amiga.ami',\n  aml: 'application/automationml-aml+xml',\n  amlx: 'application/automationml-amlx+zip',\n  amr: 'audio/amr',\n  apk: 'application/vnd.android.package-archive',\n  apng: 'image/apng',\n  appcache: 'text/cache-manifest',\n  appinstaller: 'application/appinstaller',\n  application: 'application/x-ms-application',\n  appx: 'application/appx',\n  appxbundle: 'application/appxbundle',\n  apr: 'application/vnd.lotus-approach',\n  arc: 'application/x-freearc',\n  arj: 'application/x-arj',\n  asc: 'application/pgp-signature',\n  asf: 'video/x-ms-asf',\n  asm: 'text/x-asm',\n  aso: 'application/vnd.accpac.simply.aso',\n  asx: 'video/x-ms-asf',\n  atc: 'application/vnd.acucorp',\n  atom: 'application/atom+xml',\n  atomcat: 'application/atomcat+xml',\n  atomdeleted: 'application/atomdeleted+xml',\n  atomsvc: 'application/atomsvc+xml',\n  atx: 'application/vnd.antix.game-component',\n  au: 'audio/basic',\n  avci: 'image/avci',\n  avcs: 'image/avcs',\n  avi: 'video/x-msvideo',\n  avif: 'image/avif',\n  aw: 'application/applixware',\n  azf: 'application/vnd.airzip.filesecure.azf',\n  azs: 'application/vnd.airzip.filesecure.azs',\n  azv: 'image/vnd.airzip.accelerator.azv',\n  azw: 'application/vnd.amazon.ebook',\n  b16: 'image/vnd.pco.b16',\n  bary: 'model/vnd.bary',\n  bat: 'application/x-msdownload',\n  bcpio: 'application/x-bcpio',\n  bdf: 'application/x-font-bdf',\n  bdm: 'application/vnd.syncml.dm+wbxml',\n  bdo: 'application/vnd.nato.bindingdataobject+xml',\n  bdoc: 'application/bdoc',\n  bed: 'application/vnd.realvnc.bed',\n  bh2: 'application/vnd.fujitsu.oasysprs',\n  bin: 'application/octet-stream',\n  blb: 'application/x-blorb',\n  blend: 'application/x-blender',\n  blorb: 'application/x-blorb',\n  bmi: 'application/vnd.bmi',\n  bmml: 'application/vnd.balsamiq.bmml+xml',\n  bmp: 'image/bmp',\n  book: 'application/vnd.framemaker',\n  box: 'application/vnd.previewsystems.box',\n  boz: 'application/x-bzip2',\n  bpk: 'application/octet-stream',\n  brush: 'application/vnd.procreate.brush',\n  brushset: 'application/vnd.procrate.brushset',\n  bsp: 'model/vnd.valve.source.compiled-map',\n  btf: 'image/prs.btif',\n  btif: 'image/prs.btif',\n  buffer: 'application/octet-stream',\n  bz: 'application/x-bzip',\n  bz2: 'application/x-bzip2',\n  c: 'text/x-c',\n  c11amc: 'application/vnd.cluetrust.cartomobile-config',\n  c11amz: 'application/vnd.cluetrust.cartomobile-config-pkg',\n  c4d: 'application/vnd.clonk.c4group',\n  c4f: 'application/vnd.clonk.c4group',\n  c4g: 'application/vnd.clonk.c4group',\n  c4p: 'application/vnd.clonk.c4group',\n  c4u: 'application/vnd.clonk.c4group',\n  cab: 'application/vnd.ms-cab-compressed',\n  caf: 'audio/x-caf',\n  cap: 'application/vnd.tcpdump.pcap',\n  car: 'application/vnd.curl.car',\n  cat: 'application/vnd.ms-pki.seccat',\n  cb7: 'application/x-cbr',\n  cba: 'application/x-cbr',\n  cbr: 'application/x-cbr',\n  cbt: 'application/x-cbr',\n  cbz: 'application/x-cbr',\n  cc: 'text/x-c',\n  cco: 'application/x-cocoa',\n  cct: 'application/x-director',\n  ccxml: 'application/ccxml+xml',\n  cdbcmsg: 'application/vnd.contact.cmsg',\n  cdf: 'application/x-netcdf',\n  cdfx: 'application/cdfx+xml',\n  cdkey: 'application/vnd.mediastation.cdkey',\n  cdmia: 'application/cdmi-capability',\n  cdmic: 'application/cdmi-container',\n  cdmid: 'application/cdmi-domain',\n  cdmio: 'application/cdmi-object',\n  cdmiq: 'application/cdmi-queue',\n  cdx: 'chemical/x-cdx',\n  cdxml: 'application/vnd.chemdraw+xml',\n  cdy: 'application/vnd.cinderella',\n  cer: 'application/pkix-cert',\n  cfs: 'application/x-cfs-compressed',\n  cgm: 'image/cgm',\n  chat: 'application/x-chat',\n  chm: 'application/vnd.ms-htmlhelp',\n  chrt: 'application/vnd.kde.kchart',\n  cif: 'chemical/x-cif',\n  cii: 'application/vnd.anser-web-certificate-issue-initiation',\n  cil: 'application/vnd.ms-artgalry',\n  cjs: 'application/node',\n  cla: 'application/vnd.claymore',\n  class: 'application/java-vm',\n  cld: 'model/vnd.cld',\n  clkk: 'application/vnd.crick.clicker.keyboard',\n  clkp: 'application/vnd.crick.clicker.palette',\n  clkt: 'application/vnd.crick.clicker.template',\n  clkw: 'application/vnd.crick.clicker.wordbank',\n  clkx: 'application/vnd.crick.clicker',\n  clp: 'application/x-msclip',\n  cmc: 'application/vnd.cosmocaller',\n  cmdf: 'chemical/x-cmdf',\n  cml: 'chemical/x-cml',\n  cmp: 'application/vnd.yellowriver-custom-menu',\n  cmx: 'image/x-cmx',\n  cod: 'application/vnd.rim.cod',\n  coffee: 'text/coffeescript',\n  com: 'application/x-msdownload',\n  conf: 'text/plain',\n  cpio: 'application/x-cpio',\n  cpl: 'application/cpl+xml',\n  cpp: 'text/x-c',\n  cpt: 'application/mac-compactpro',\n  crd: 'application/x-mscardfile',\n  crl: 'application/pkix-crl',\n  crt: 'application/x-x509-ca-cert',\n  crx: 'application/x-chrome-extension',\n  cryptonote: 'application/vnd.rig.cryptonote',\n  csh: 'application/x-csh',\n  csl: 'application/vnd.citationstyles.style+xml',\n  csml: 'chemical/x-csml',\n  csp: 'application/vnd.commonspace',\n  css: 'text/css',\n  cst: 'application/x-director',\n  csv: 'text/csv',\n  cu: 'application/cu-seeme',\n  curl: 'text/vnd.curl',\n  cwl: 'application/cwl',\n  cww: 'application/prs.cww',\n  cxt: 'application/x-director',\n  cxx: 'text/x-c',\n  dae: 'model/vnd.collada+xml',\n  daf: 'application/vnd.mobius.daf',\n  dart: 'application/vnd.dart',\n  dataless: 'application/vnd.fdsn.seed',\n  davmount: 'application/davmount+xml',\n  dbf: 'application/vnd.dbf',\n  dbk: 'application/docbook+xml',\n  dcm: 'application/dicom',\n  dcmp: 'application/vnd.dcmp+xml',\n  dcr: 'application/x-director',\n  dcurl: 'text/vnd.curl.dcurl',\n  dd2: 'application/vnd.oma.dd2+xml',\n  ddd: 'application/vnd.fujixerox.ddd',\n  ddf: 'application/vnd.syncml.dmddf+xml',\n  dds: 'image/vnd.ms-dds',\n  deb: 'application/octet-stream',\n  def: 'text/plain',\n  deploy: 'application/octet-stream',\n  der: 'application/x-x509-ca-cert',\n  dfac: 'application/vnd.dreamfactory',\n  dgc: 'application/x-dgc-compressed',\n  dib: 'image/bmp',\n  dic: 'text/x-c',\n  dir: 'application/x-director',\n  dis: 'application/vnd.mobius.dis',\n  'disposition-notification': 'message/disposition-notification',\n  dist: 'application/octet-stream',\n  distz: 'application/octet-stream',\n  djv: 'image/vnd.djvu',\n  djvu: 'image/vnd.djvu',\n  dll: 'application/octet-stream',\n  dmg: 'application/octet-stream',\n  dmp: 'application/vnd.tcpdump.pcap',\n  dms: 'application/octet-stream',\n  dna: 'application/vnd.dna',\n  dng: 'image/x-adobe-dng',\n  doc: 'application/msword',\n  docm: 'application/vnd.ms-word.document.macroenabled.12',\n  docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n  dot: 'application/msword',\n  dotm: 'application/vnd.ms-word.template.macroenabled.12',\n  dotx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',\n  dp: 'application/vnd.osgi.dp',\n  dpg: 'application/vnd.dpgraph',\n  dpx: 'image/dpx',\n  dra: 'audio/vnd.dra',\n  drle: 'image/dicom-rle',\n  drm: 'application/vnd.procreate.dream',\n  dsc: 'text/prs.lines.tag',\n  dssc: 'application/dssc+der',\n  dtb: 'application/x-dtbook+xml',\n  dtd: 'application/xml-dtd',\n  dts: 'audio/vnd.dts',\n  dtshd: 'audio/vnd.dts.hd',\n  dump: 'application/octet-stream',\n  dvb: 'video/vnd.dvb.file',\n  dvi: 'application/x-dvi',\n  dwd: 'application/atsc-dwd+xml',\n  dwf: 'model/vnd.dwf',\n  dwg: 'image/vnd.dwg',\n  dxf: 'image/vnd.dxf',\n  dxp: 'application/vnd.spotfire.dxp',\n  dxr: 'application/x-director',\n  ear: 'application/java-archive',\n  ecelp4800: 'audio/vnd.nuera.ecelp4800',\n  ecelp7470: 'audio/vnd.nuera.ecelp7470',\n  ecelp9600: 'audio/vnd.nuera.ecelp9600',\n  ecma: 'application/ecmascript',\n  edm: 'application/vnd.novadigm.edm',\n  edx: 'application/vnd.novadigm.edx',\n  efif: 'application/vnd.picsel',\n  ei6: 'application/vnd.pg.osasli',\n  elc: 'application/octet-stream',\n  emf: 'image/emf',\n  eml: 'message/rfc822',\n  emma: 'application/emma+xml',\n  emotionml: 'application/emotionml+xml',\n  emz: 'application/x-msmetafile',\n  eol: 'audio/vnd.digital-winds',\n  eot: 'application/vnd.ms-fontobject',\n  eps: 'application/postscript',\n  epub: 'application/epub+zip',\n  es3: 'application/vnd.eszigno3+xml',\n  esa: 'application/vnd.osgi.subsystem',\n  esf: 'application/vnd.epson.esf',\n  et3: 'application/vnd.eszigno3+xml',\n  etx: 'text/x-setext',\n  eva: 'application/x-eva',\n  evy: 'application/x-envoy',\n  exe: 'application/octet-stream',\n  exi: 'application/exi',\n  exp: 'application/express',\n  exr: 'image/aces',\n  ext: 'application/vnd.novadigm.ext',\n  ez: 'application/andrew-inset',\n  ez2: 'application/vnd.ezpix-album',\n  ez3: 'application/vnd.ezpix-package',\n  f: 'text/x-fortran',\n  f4v: 'video/x-f4v',\n  f77: 'text/x-fortran',\n  f90: 'text/x-fortran',\n  fbs: 'image/vnd.fastbidsheet',\n  fbx: 'application/vnd.autodesk.fbx',\n  fcdt: 'application/vnd.adobe.formscentral.fcdt',\n  fcs: 'application/vnd.isac.fcs',\n  fdf: 'application/fdf',\n  fdt: 'application/fdt+xml',\n  fe_launch: 'application/vnd.denovo.fcselayout-link',\n  fg5: 'application/vnd.fujitsu.oasysgp',\n  fgd: 'application/x-director',\n  fh: 'image/x-freehand',\n  fh4: 'image/x-freehand',\n  fh5: 'image/x-freehand',\n  fh7: 'image/x-freehand',\n  fhc: 'image/x-freehand',\n  fig: 'application/x-xfig',\n  fits: 'image/fits',\n  flac: 'audio/x-flac',\n  fli: 'video/x-fli',\n  flo: 'application/vnd.micrografx.flo',\n  flv: 'video/x-flv',\n  flw: 'application/vnd.kde.kivio',\n  flx: 'text/vnd.fmi.flexstor',\n  fly: 'text/vnd.fly',\n  fm: 'application/vnd.framemaker',\n  fnc: 'application/vnd.frogans.fnc',\n  fo: 'application/vnd.software602.filler.form+xml',\n  for: 'text/x-fortran',\n  fpx: 'image/vnd.fpx',\n  frame: 'application/vnd.framemaker',\n  fsc: 'application/vnd.fsc.weblaunch',\n  fst: 'image/vnd.fst',\n  ftc: 'application/vnd.fluxtime.clip',\n  fti: 'application/vnd.anser-web-funds-transfer-initiation',\n  fvt: 'video/vnd.fvt',\n  fxp: 'application/vnd.adobe.fxp',\n  fxpl: 'application/vnd.adobe.fxp',\n  fzs: 'application/vnd.fuzzysheet',\n  g2w: 'application/vnd.geoplan',\n  g3: 'image/g3fax',\n  g3w: 'application/vnd.geospace',\n  gac: 'application/vnd.groove-account',\n  gam: 'application/x-tads',\n  gbr: 'application/rpki-ghostbusters',\n  gca: 'application/x-gca-compressed',\n  gdl: 'model/vnd.gdl',\n  gdoc: 'application/vnd.google-apps.document',\n  gdraw: 'application/vnd.google-apps.drawing',\n  ged: 'text/vnd.familysearch.gedcom',\n  geo: 'application/vnd.dynageo',\n  geojson: 'application/geo+json',\n  gex: 'application/vnd.geometry-explorer',\n  gform: 'application/vnd.google-apps.form',\n  ggb: 'application/vnd.geogebra.file',\n  ggs: 'application/vnd.geogebra.slides',\n  ggt: 'application/vnd.geogebra.tool',\n  ghf: 'application/vnd.groove-help',\n  gif: 'image/gif',\n  gim: 'application/vnd.groove-identity-message',\n  gjam: 'application/vnd.google-apps.jam',\n  glb: 'model/gltf-binary',\n  gltf: 'model/gltf+json',\n  gmap: 'application/vnd.google-apps.map',\n  gml: 'application/gml+xml',\n  gmx: 'application/vnd.gmx',\n  gnumeric: 'application/x-gnumeric',\n  gph: 'application/vnd.flographit',\n  gpx: 'application/gpx+xml',\n  gqf: 'application/vnd.grafeq',\n  gqs: 'application/vnd.grafeq',\n  gram: 'application/srgs',\n  gramps: 'application/x-gramps-xml',\n  gre: 'application/vnd.geometry-explorer',\n  grv: 'application/vnd.groove-injector',\n  grxml: 'application/srgs+xml',\n  gscript: 'application/vnd.google-apps.script',\n  gsf: 'application/x-font-ghostscript',\n  gsheet: 'application/vnd.google-apps.spreadsheet',\n  gsite: 'application/vnd.google-apps.site',\n  gslides: 'application/vnd.google-apps.presentation',\n  gtar: 'application/x-gtar',\n  gtm: 'application/vnd.groove-tool-message',\n  gtw: 'model/vnd.gtw',\n  gv: 'text/vnd.graphviz',\n  gxf: 'application/gxf',\n  gxt: 'application/vnd.geonext',\n  gz: 'application/gzip',\n  h: 'text/x-c',\n  h261: 'video/h261',\n  h263: 'video/h263',\n  h264: 'video/h264',\n  hal: 'application/vnd.hal+xml',\n  hbci: 'application/vnd.hbci',\n  hbs: 'text/x-handlebars-template',\n  hdd: 'application/x-virtualbox-hdd',\n  hdf: 'application/x-hdf',\n  heic: 'image/heic',\n  heics: 'image/heic-sequence',\n  heif: 'image/heif',\n  heifs: 'image/heif-sequence',\n  hej2: 'image/hej2k',\n  held: 'application/atsc-held+xml',\n  hh: 'text/x-c',\n  hjson: 'application/hjson',\n  hlp: 'application/winhlp',\n  hpgl: 'application/vnd.hp-hpgl',\n  hpid: 'application/vnd.hp-hpid',\n  hps: 'application/vnd.hp-hps',\n  hqx: 'application/mac-binhex40',\n  htc: 'text/x-component',\n  htke: 'application/vnd.kenameaapp',\n  htm: 'text/html',\n  html: 'text/html',\n  hvd: 'application/vnd.yamaha.hv-dic',\n  hvp: 'application/vnd.yamaha.hv-voice',\n  hvs: 'application/vnd.yamaha.hv-script',\n  i2g: 'application/vnd.intergeo',\n  icc: 'application/vnd.iccprofile',\n  ice: 'x-conference/x-cooltalk',\n  icm: 'application/vnd.iccprofile',\n  ico: 'image/vnd.microsoft.icon',\n  ics: 'text/calendar',\n  ief: 'image/ief',\n  ifb: 'text/calendar',\n  ifm: 'application/vnd.shana.informed.formdata',\n  iges: 'model/iges',\n  igl: 'application/vnd.igloader',\n  igm: 'application/vnd.insors.igm',\n  igs: 'model/iges',\n  igx: 'application/vnd.micrografx.igx',\n  iif: 'application/vnd.shana.informed.interchange',\n  img: 'application/octet-stream',\n  imp: 'application/vnd.accpac.simply.imp',\n  ims: 'application/vnd.ms-ims',\n  in: 'text/plain',\n  ini: 'text/plain',\n  ink: 'application/inkml+xml',\n  inkml: 'application/inkml+xml',\n  install: 'application/x-install-instructions',\n  iota: 'application/vnd.astraea-software.iota',\n  ipfix: 'application/ipfix',\n  ipk: 'application/vnd.shana.informed.package',\n  ipynb: 'application/x-ipynb+json',\n  irm: 'application/vnd.ibm.rights-management',\n  irp: 'application/vnd.irepository.package+xml',\n  iso: 'application/octet-stream',\n  itp: 'application/vnd.shana.informed.formtemplate',\n  its: 'application/its+xml',\n  ivp: 'application/vnd.immervision-ivp',\n  ivu: 'application/vnd.immervision-ivu',\n  jad: 'text/vnd.sun.j2me.app-descriptor',\n  jade: 'text/jade',\n  jaii: 'image/jaii',\n  jais: 'image/jais',\n  jam: 'application/vnd.jam',\n  jar: 'application/java-archive',\n  jardiff: 'application/x-java-archive-diff',\n  java: 'text/x-java-source',\n  jfif: 'image/pjpeg',\n  jhc: 'image/jphc',\n  jisp: 'application/vnd.jisp',\n  jls: 'image/jls',\n  jlt: 'application/vnd.hp-jlyt',\n  jng: 'image/x-jng',\n  jnlp: 'application/x-java-jnlp-file',\n  joda: 'application/vnd.joost.joda-archive',\n  jp2: 'image/jp2',\n  jpe: 'image/jpeg',\n  jpeg: 'image/jpeg',\n  jpf: 'image/jpx',\n  jpg: 'image/jpeg',\n  jpg2: 'image/jp2',\n  jpgm: 'image/jpm',\n  jpgv: 'video/jpeg',\n  jph: 'image/jph',\n  jpm: 'image/jpm',\n  jpx: 'image/jpx',\n  js: 'text/javascript',\n  json: 'application/json',\n  json5: 'application/json5',\n  jsonld: 'application/ld+json',\n  jsonml: 'application/jsonml+json',\n  jsx: 'text/jsx',\n  jt: 'model/jt',\n  jxl: 'image/jxl',\n  jxr: 'image/jxr',\n  jxra: 'image/jxra',\n  jxrs: 'image/jxrs',\n  jxs: 'image/jxs',\n  jxsc: 'image/jxsc',\n  jxsi: 'image/jxsi',\n  jxss: 'image/jxss',\n  kar: 'audio/midi',\n  karbon: 'application/vnd.kde.karbon',\n  kdbx: 'application/x-keepass2',\n  key: 'application/vnd.apple.keynote',\n  kfo: 'application/vnd.kde.kformula',\n  kia: 'application/vnd.kidspiration',\n  kml: 'application/vnd.google-earth.kml+xml',\n  kmz: 'application/vnd.google-earth.kmz',\n  kne: 'application/vnd.kinar',\n  knp: 'application/vnd.kinar',\n  kon: 'application/vnd.kde.kontour',\n  kpr: 'application/vnd.kde.kpresenter',\n  kpt: 'application/vnd.kde.kpresenter',\n  kpxx: 'application/vnd.ds-keypoint',\n  ksp: 'application/vnd.kde.kspread',\n  ktr: 'application/vnd.kahootz',\n  ktx: 'image/ktx',\n  ktx2: 'image/ktx2',\n  ktz: 'application/vnd.kahootz',\n  kwd: 'application/vnd.kde.kword',\n  kwt: 'application/vnd.kde.kword',\n  lasxml: 'application/vnd.las.las+xml',\n  latex: 'application/x-latex',\n  lbd: 'application/vnd.llamagraphics.life-balance.desktop',\n  lbe: 'application/vnd.llamagraphics.life-balance.exchange+xml',\n  les: 'application/vnd.hhe.lesson-player',\n  less: 'text/less',\n  lgr: 'application/lgr+xml',\n  lha: 'application/x-lzh-compressed',\n  link66: 'application/vnd.route66.link66+xml',\n  list: 'text/plain',\n  list3820: 'application/vnd.ibm.modcap',\n  listafp: 'application/vnd.ibm.modcap',\n  litcoffee: 'text/coffeescript',\n  lnk: 'application/x-ms-shortcut',\n  log: 'text/plain',\n  lostxml: 'application/lost+xml',\n  lottie: 'application/zip+dotlottie',\n  lrf: 'application/octet-stream',\n  lrm: 'application/vnd.ms-lrm',\n  ltf: 'application/vnd.frogans.ltf',\n  lua: 'text/x-lua',\n  luac: 'application/x-lua-bytecode',\n  lvp: 'audio/vnd.lucent.voice',\n  lwp: 'application/vnd.lotus-wordpro',\n  lzh: 'application/x-lzh-compressed',\n  m13: 'application/x-msmediaview',\n  m14: 'application/x-msmediaview',\n  m1v: 'video/mpeg',\n  m21: 'application/mp21',\n  m2a: 'audio/mpeg',\n  m2t: 'video/mp2t',\n  m2ts: 'video/mp2t',\n  m2v: 'video/mpeg',\n  m3a: 'audio/mpeg',\n  m3u: 'audio/x-mpegurl',\n  m3u8: 'application/vnd.apple.mpegurl',\n  m4a: 'audio/mp4',\n  m4b: 'audio/mp4',\n  m4p: 'application/mp4',\n  m4s: 'video/iso.segment',\n  m4u: 'video/vnd.mpegurl',\n  m4v: 'video/x-m4v',\n  ma: 'application/mathematica',\n  mads: 'application/mads+xml',\n  maei: 'application/mmt-aei+xml',\n  mag: 'application/vnd.ecowin.chart',\n  maker: 'application/vnd.framemaker',\n  man: 'text/troff',\n  manifest: 'text/cache-manifest',\n  map: 'application/json',\n  mar: 'application/octet-stream',\n  markdown: 'text/markdown',\n  mathml: 'application/mathml+xml',\n  mb: 'application/mathematica',\n  mbk: 'application/vnd.mobius.mbk',\n  mbox: 'application/mbox',\n  mc1: 'application/vnd.medcalcdata',\n  mcd: 'application/vnd.mcd',\n  mcurl: 'text/vnd.curl.mcurl',\n  md: 'text/markdown',\n  mdb: 'application/x-msaccess',\n  mdi: 'image/vnd.ms-modi',\n  mdx: 'text/mdx',\n  me: 'text/troff',\n  mesh: 'model/mesh',\n  meta4: 'application/metalink4+xml',\n  metalink: 'application/metalink+xml',\n  mets: 'application/mets+xml',\n  mfm: 'application/vnd.mfmp',\n  mft: 'application/rpki-manifest',\n  mgp: 'application/vnd.osgeo.mapguide.package',\n  mgz: 'application/vnd.proteus.magazine',\n  mht: 'message/rfc822',\n  mhtml: 'message/rfc822',\n  mid: 'audio/midi',\n  midi: 'audio/midi',\n  mie: 'application/x-mie',\n  mif: 'application/vnd.mif',\n  mime: 'message/rfc822',\n  mj2: 'video/mj2',\n  mjp2: 'video/mj2',\n  mjs: 'text/javascript',\n  mk3d: 'video/x-matroska',\n  mka: 'audio/x-matroska',\n  mkd: 'text/x-markdown',\n  mks: 'video/x-matroska',\n  mkv: 'video/x-matroska',\n  mlp: 'application/vnd.dolby.mlp',\n  mmd: 'application/vnd.chipnuts.karaoke-mmd',\n  mmf: 'application/vnd.smaf',\n  mml: 'text/mathml',\n  mmr: 'image/vnd.fujixerox.edmics-mmr',\n  mng: 'video/x-mng',\n  mny: 'application/x-msmoney',\n  mobi: 'application/x-mobipocket-ebook',\n  mods: 'application/mods+xml',\n  mov: 'video/quicktime',\n  movie: 'video/x-sgi-movie',\n  mp2: 'audio/mpeg',\n  mp21: 'application/mp21',\n  mp2a: 'audio/mpeg',\n  mp3: 'audio/mpeg',\n  mp4: 'application/mp4',\n  mp4a: 'audio/mp4',\n  mp4s: 'application/mp4',\n  mp4v: 'video/mp4',\n  mpc: 'application/vnd.mophun.certificate',\n  mpd: 'application/dash+xml',\n  mpe: 'video/mpeg',\n  mpeg: 'video/mpeg',\n  mpf: 'application/media-policy-dataset+xml',\n  mpg: 'video/mpeg',\n  mpg4: 'application/mp4',\n  mpga: 'audio/mpeg',\n  mpkg: 'application/vnd.apple.installer+xml',\n  mpm: 'application/vnd.blueice.multipass',\n  mpn: 'application/vnd.mophun.application',\n  mpp: 'application/dash-patch+xml',\n  mpt: 'application/vnd.ms-project',\n  mpy: 'application/vnd.ibm.minipay',\n  mqy: 'application/vnd.mobius.mqy',\n  mrc: 'application/marc',\n  mrcx: 'application/marcxml+xml',\n  ms: 'text/troff',\n  mscml: 'application/mediaservercontrol+xml',\n  mseed: 'application/vnd.fdsn.mseed',\n  mseq: 'application/vnd.mseq',\n  msf: 'application/vnd.epson.msf',\n  msg: 'application/vnd.ms-outlook',\n  msh: 'model/mesh',\n  msi: 'application/octet-stream',\n  msix: 'application/msix',\n  msixbundle: 'application/msixbundle',\n  msl: 'application/vnd.mobius.msl',\n  msm: 'application/octet-stream',\n  msp: 'application/octet-stream',\n  msty: 'application/vnd.muvee.style',\n  mtl: 'model/mtl',\n  mts: 'video/mp2t',\n  mus: 'application/vnd.musician',\n  musd: 'application/mmt-usd+xml',\n  musicxml: 'application/vnd.recordare.musicxml+xml',\n  mvb: 'application/x-msmediaview',\n  mvt: 'application/vnd.mapbox-vector-tile',\n  mwf: 'application/vnd.mfer',\n  mxf: 'application/mxf',\n  mxl: 'application/vnd.recordare.musicxml',\n  mxmf: 'audio/mobile-xmf',\n  mxml: 'application/xv+xml',\n  mxs: 'application/vnd.triscape.mxs',\n  mxu: 'video/vnd.mpegurl',\n  'n-gage': 'application/vnd.nokia.n-gage.symbian.install',\n  n3: 'text/n3',\n  nb: 'application/mathematica',\n  nbp: 'application/vnd.wolfram.player',\n  nc: 'application/x-netcdf',\n  ncx: 'application/x-dtbncx+xml',\n  nfo: 'text/x-nfo',\n  ngdat: 'application/vnd.nokia.n-gage.data',\n  nitf: 'application/vnd.nitf',\n  nlu: 'application/vnd.neurolanguage.nlu',\n  nml: 'application/vnd.enliven',\n  nnd: 'application/vnd.noblenet-directory',\n  nns: 'application/vnd.noblenet-sealer',\n  nnw: 'application/vnd.noblenet-web',\n  npx: 'image/vnd.net-fpx',\n  nq: 'application/n-quads',\n  nsc: 'application/x-conference',\n  nsf: 'application/vnd.lotus-notes',\n  nt: 'application/n-triples',\n  ntf: 'application/vnd.nitf',\n  numbers: 'application/vnd.apple.numbers',\n  nzb: 'application/x-nzb',\n  oa2: 'application/vnd.fujitsu.oasys2',\n  oa3: 'application/vnd.fujitsu.oasys3',\n  oas: 'application/vnd.fujitsu.oasys',\n  obd: 'application/x-msbinder',\n  obgx: 'application/vnd.openblox.game+xml',\n  obj: 'model/obj',\n  oda: 'application/oda',\n  odb: 'application/vnd.oasis.opendocument.database',\n  odc: 'application/vnd.oasis.opendocument.chart',\n  odf: 'application/vnd.oasis.opendocument.formula',\n  odft: 'application/vnd.oasis.opendocument.formula-template',\n  odg: 'application/vnd.oasis.opendocument.graphics',\n  odi: 'application/vnd.oasis.opendocument.image',\n  odm: 'application/vnd.oasis.opendocument.text-master',\n  odp: 'application/vnd.oasis.opendocument.presentation',\n  ods: 'application/vnd.oasis.opendocument.spreadsheet',\n  odt: 'application/vnd.oasis.opendocument.text',\n  oga: 'audio/ogg',\n  ogex: 'model/vnd.opengex',\n  ogg: 'audio/ogg',\n  ogv: 'video/ogg',\n  ogx: 'application/ogg',\n  omdoc: 'application/omdoc+xml',\n  one: 'application/onenote',\n  onea: 'application/onenote',\n  onepkg: 'application/onenote',\n  onetmp: 'application/onenote',\n  onetoc: 'application/onenote',\n  onetoc2: 'application/onenote',\n  opf: 'application/oebps-package+xml',\n  opml: 'text/x-opml',\n  oprc: 'application/vnd.palm',\n  opus: 'audio/ogg',\n  org: 'text/x-org',\n  osf: 'application/vnd.yamaha.openscoreformat',\n  osfpvg: 'application/vnd.yamaha.openscoreformat.osfpvg+xml',\n  osm: 'application/vnd.openstreetmap.data+xml',\n  otc: 'application/vnd.oasis.opendocument.chart-template',\n  otf: 'font/otf',\n  otg: 'application/vnd.oasis.opendocument.graphics-template',\n  oth: 'application/vnd.oasis.opendocument.text-web',\n  oti: 'application/vnd.oasis.opendocument.image-template',\n  otp: 'application/vnd.oasis.opendocument.presentation-template',\n  ots: 'application/vnd.oasis.opendocument.spreadsheet-template',\n  ott: 'application/vnd.oasis.opendocument.text-template',\n  ova: 'application/x-virtualbox-ova',\n  ovf: 'application/x-virtualbox-ovf',\n  owl: 'application/rdf+xml',\n  oxps: 'application/oxps',\n  oxt: 'application/vnd.openofficeorg.extension',\n  p: 'text/x-pascal',\n  p10: 'application/pkcs10',\n  p12: 'application/x-pkcs12',\n  p21: 'model/step',\n  p7b: 'application/x-pkcs7-certificates',\n  p7c: 'application/pkcs7-mime',\n  p7m: 'application/pkcs7-mime',\n  p7r: 'application/x-pkcs7-certreqresp',\n  p7s: 'application/pkcs7-signature',\n  p8: 'application/pkcs8',\n  pac: 'application/x-ns-proxy-autoconfig',\n  pages: 'application/vnd.apple.pages',\n  pas: 'text/x-pascal',\n  paw: 'application/vnd.pawaafile',\n  pbd: 'application/vnd.powerbuilder6',\n  pbm: 'image/x-portable-bitmap',\n  pcap: 'application/vnd.tcpdump.pcap',\n  pcf: 'application/x-font-pcf',\n  pcl: 'application/vnd.hp-pcl',\n  pclxl: 'application/vnd.hp-pclxl',\n  pct: 'image/x-pict',\n  pcurl: 'application/vnd.curl.pcurl',\n  pcx: 'image/vnd.zbrush.pcx',\n  pdb: 'application/vnd.palm',\n  pde: 'text/x-processing',\n  pdf: 'application/pdf',\n  pem: 'application/x-x509-ca-cert',\n  pfa: 'application/x-font-type1',\n  pfb: 'application/x-font-type1',\n  pfm: 'application/x-font-type1',\n  pfr: 'application/font-tdpfr',\n  pfx: 'application/x-pkcs12',\n  pgm: 'image/x-portable-graymap',\n  pgn: 'application/x-chess-pgn',\n  pgp: 'application/pgp-encrypted',\n  php: 'application/x-httpd-php',\n  pic: 'image/x-pict',\n  pkg: 'application/octet-stream',\n  pki: 'application/pkixcmp',\n  pkipath: 'application/pkix-pkipath',\n  pkpass: 'application/vnd.apple.pkpass',\n  pl: 'application/x-perl',\n  plb: 'application/vnd.3gpp.pic-bw-large',\n  plc: 'application/vnd.mobius.plc',\n  plf: 'application/vnd.pocketlearn',\n  pls: 'application/pls+xml',\n  pm: 'application/x-perl',\n  pml: 'application/vnd.ctc-posml',\n  png: 'image/png',\n  pnm: 'image/x-portable-anymap',\n  portpkg: 'application/vnd.macports.portpkg',\n  pot: 'application/vnd.ms-powerpoint',\n  potm: 'application/vnd.ms-powerpoint.template.macroenabled.12',\n  potx: 'application/vnd.openxmlformats-officedocument.presentationml.template',\n  ppam: 'application/vnd.ms-powerpoint.addin.macroenabled.12',\n  ppd: 'application/vnd.cups-ppd',\n  ppm: 'image/x-portable-pixmap',\n  pps: 'application/vnd.ms-powerpoint',\n  ppsm: 'application/vnd.ms-powerpoint.slideshow.macroenabled.12',\n  ppsx: 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',\n  ppt: 'application/vnd.ms-powerpoint',\n  pptm: 'application/vnd.ms-powerpoint.presentation.macroenabled.12',\n  pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n  pqa: 'application/vnd.palm',\n  prc: 'model/prc',\n  pre: 'application/vnd.lotus-freelance',\n  prf: 'application/pics-rules',\n  provx: 'application/provenance+xml',\n  ps: 'application/postscript',\n  psb: 'application/vnd.3gpp.pic-bw-small',\n  psd: 'image/vnd.adobe.photoshop',\n  psf: 'application/x-font-linux-psf',\n  pskcxml: 'application/pskc+xml',\n  pti: 'image/prs.pti',\n  ptid: 'application/vnd.pvi.ptid1',\n  pub: 'application/x-mspublisher',\n  pvb: 'application/vnd.3gpp.pic-bw-var',\n  pwn: 'application/vnd.3m.post-it-notes',\n  pya: 'audio/vnd.ms-playready.media.pya',\n  pyo: 'model/vnd.pytha.pyox',\n  pyox: 'model/vnd.pytha.pyox',\n  pyv: 'video/vnd.ms-playready.media.pyv',\n  qam: 'application/vnd.epson.quickanime',\n  qbo: 'application/vnd.intu.qbo',\n  qfx: 'application/vnd.intu.qfx',\n  qps: 'application/vnd.publishare-delta-tree',\n  qt: 'video/quicktime',\n  qwd: 'application/vnd.quark.quarkxpress',\n  qwt: 'application/vnd.quark.quarkxpress',\n  qxb: 'application/vnd.quark.quarkxpress',\n  qxd: 'application/vnd.quark.quarkxpress',\n  qxl: 'application/vnd.quark.quarkxpress',\n  qxt: 'application/vnd.quark.quarkxpress',\n  ra: 'audio/x-pn-realaudio',\n  ram: 'audio/x-pn-realaudio',\n  raml: 'application/raml+yaml',\n  rapd: 'application/route-apd+xml',\n  rar: 'application/vnd.rar',\n  ras: 'image/x-cmu-raster',\n  rcprofile: 'application/vnd.ipunplugged.rcprofile',\n  rdf: 'application/rdf+xml',\n  rdz: 'application/vnd.data-vision.rdz',\n  relo: 'application/p2p-overlay+xml',\n  rep: 'application/vnd.businessobjects',\n  res: 'application/x-dtbresource+xml',\n  rgb: 'image/x-rgb',\n  rif: 'application/reginfo+xml',\n  rip: 'audio/vnd.rip',\n  ris: 'application/x-research-info-systems',\n  rl: 'application/resource-lists+xml',\n  rlc: 'image/vnd.fujixerox.edmics-rlc',\n  rld: 'application/resource-lists-diff+xml',\n  rm: 'application/vnd.rn-realmedia',\n  rmi: 'audio/midi',\n  rmp: 'audio/x-pn-realaudio-plugin',\n  rms: 'application/vnd.jcp.javame.midlet-rms',\n  rmvb: 'application/vnd.rn-realmedia-vbr',\n  rnc: 'application/relax-ng-compact-syntax',\n  rng: 'application/xml',\n  roa: 'application/rpki-roa',\n  roff: 'text/troff',\n  rp9: 'application/vnd.cloanto.rp9',\n  rpm: 'application/x-redhat-package-manager',\n  rpss: 'application/vnd.nokia.radio-presets',\n  rpst: 'application/vnd.nokia.radio-preset',\n  rq: 'application/sparql-query',\n  rs: 'application/rls-services+xml',\n  rsat: 'application/atsc-rsat+xml',\n  rsd: 'application/rsd+xml',\n  rsheet: 'application/urc-ressheet+xml',\n  rss: 'application/rss+xml',\n  rtf: 'text/rtf',\n  rtx: 'text/richtext',\n  run: 'application/x-makeself',\n  rusd: 'application/route-usd+xml',\n  s: 'text/x-asm',\n  s3m: 'audio/s3m',\n  saf: 'application/vnd.yamaha.smaf-audio',\n  sass: 'text/x-sass',\n  sbml: 'application/sbml+xml',\n  sc: 'application/vnd.ibm.secure-container',\n  scd: 'application/x-msschedule',\n  scm: 'application/vnd.lotus-screencam',\n  scq: 'application/scvp-cv-request',\n  scs: 'application/scvp-cv-response',\n  scss: 'text/x-scss',\n  scurl: 'text/vnd.curl.scurl',\n  sda: 'application/vnd.stardivision.draw',\n  sdc: 'application/vnd.stardivision.calc',\n  sdd: 'application/vnd.stardivision.impress',\n  sdkd: 'application/vnd.solent.sdkm+xml',\n  sdkm: 'application/vnd.solent.sdkm+xml',\n  sdp: 'application/sdp',\n  sdw: 'application/vnd.stardivision.writer',\n  sea: 'application/x-sea',\n  see: 'application/vnd.seemail',\n  seed: 'application/vnd.fdsn.seed',\n  sema: 'application/vnd.sema',\n  semd: 'application/vnd.semd',\n  semf: 'application/vnd.semf',\n  senmlx: 'application/senml+xml',\n  sensmlx: 'application/sensml+xml',\n  ser: 'application/java-serialized-object',\n  setpay: 'application/set-payment-initiation',\n  setreg: 'application/set-registration-initiation',\n  'sfd-hdstx': 'application/vnd.hydrostatix.sof-data',\n  sfs: 'application/vnd.spotfire.sfs',\n  sfv: 'text/x-sfv',\n  sgi: 'image/sgi',\n  sgl: 'application/vnd.stardivision.writer-global',\n  sgm: 'text/sgml',\n  sgml: 'text/sgml',\n  sh: 'application/x-sh',\n  shar: 'application/x-shar',\n  shex: 'text/shex',\n  shf: 'application/shf+xml',\n  shtml: 'text/html',\n  sid: 'image/x-mrsid-image',\n  sieve: 'application/sieve',\n  sig: 'application/pgp-signature',\n  sil: 'audio/silk',\n  silo: 'model/mesh',\n  sis: 'application/vnd.symbian.install',\n  sisx: 'application/vnd.symbian.install',\n  sit: 'application/x-stuffit',\n  sitx: 'application/x-stuffitx',\n  siv: 'application/sieve',\n  skd: 'application/vnd.koan',\n  skm: 'application/vnd.koan',\n  skp: 'application/vnd.koan',\n  skt: 'application/vnd.koan',\n  sldm: 'application/vnd.ms-powerpoint.slide.macroenabled.12',\n  sldx: 'application/vnd.openxmlformats-officedocument.presentationml.slide',\n  slim: 'text/slim',\n  slm: 'text/slim',\n  sls: 'application/route-s-tsid+xml',\n  slt: 'application/vnd.epson.salt',\n  sm: 'application/vnd.stepmania.stepchart',\n  smf: 'application/vnd.stardivision.math',\n  smi: 'application/smil+xml',\n  smil: 'application/smil+xml',\n  smv: 'video/x-smv',\n  smzip: 'application/vnd.stepmania.package',\n  snd: 'audio/basic',\n  snf: 'application/x-font-snf',\n  so: 'application/octet-stream',\n  spc: 'application/x-pkcs7-certificates',\n  spdx: 'text/spdx',\n  spf: 'application/vnd.yamaha.smaf-phrase',\n  spl: 'application/x-futuresplash',\n  spot: 'text/vnd.in3d.spot',\n  spp: 'application/scvp-vp-response',\n  spq: 'application/scvp-vp-request',\n  spx: 'audio/ogg',\n  sql: 'application/sql',\n  src: 'application/x-wais-source',\n  srt: 'application/x-subrip',\n  sru: 'application/sru+xml',\n  srx: 'application/sparql-results+xml',\n  ssdl: 'application/ssdl+xml',\n  sse: 'application/vnd.kodak-descriptor',\n  ssf: 'application/vnd.epson.ssf',\n  ssml: 'application/ssml+xml',\n  st: 'application/vnd.sailingtracker.track',\n  stc: 'application/vnd.sun.xml.calc.template',\n  std: 'application/vnd.sun.xml.draw.template',\n  step: 'model/step',\n  stf: 'application/vnd.wt.stf',\n  sti: 'application/vnd.sun.xml.impress.template',\n  stk: 'application/hyperstudio',\n  stl: 'model/stl',\n  stp: 'model/step',\n  stpnc: 'model/step',\n  stpx: 'model/step+xml',\n  stpxz: 'model/step-xml+zip',\n  stpz: 'model/step+zip',\n  str: 'application/vnd.pg.format',\n  stw: 'application/vnd.sun.xml.writer.template',\n  styl: 'text/stylus',\n  stylus: 'text/stylus',\n  sub: 'text/vnd.dvb.subtitle',\n  sus: 'application/vnd.sus-calendar',\n  susp: 'application/vnd.sus-calendar',\n  sv4cpio: 'application/x-sv4cpio',\n  sv4crc: 'application/x-sv4crc',\n  svc: 'application/vnd.dvb.service',\n  svd: 'application/vnd.svd',\n  svg: 'image/svg+xml',\n  svgz: 'image/svg+xml',\n  swa: 'application/x-director',\n  swf: 'application/x-shockwave-flash',\n  swi: 'application/vnd.aristanetworks.swi',\n  swidtag: 'application/swid+xml',\n  sxc: 'application/vnd.sun.xml.calc',\n  sxd: 'application/vnd.sun.xml.draw',\n  sxg: 'application/vnd.sun.xml.writer.global',\n  sxi: 'application/vnd.sun.xml.impress',\n  sxm: 'application/vnd.sun.xml.math',\n  sxw: 'application/vnd.sun.xml.writer',\n  t: 'text/troff',\n  t3: 'application/x-t3vm-image',\n  t38: 'image/t38',\n  taglet: 'application/vnd.mynfc',\n  tao: 'application/vnd.tao.intent-module-archive',\n  tap: 'image/vnd.tencent.tap',\n  tar: 'application/x-tar',\n  tcap: 'application/vnd.3gpp2.tcap',\n  tcl: 'application/x-tcl',\n  td: 'application/urc-targetdesc+xml',\n  teacher: 'application/vnd.smart.teacher',\n  tei: 'application/tei+xml',\n  teicorpus: 'application/tei+xml',\n  tex: 'application/x-tex',\n  texi: 'application/x-texinfo',\n  texinfo: 'application/x-texinfo',\n  text: 'text/plain',\n  tfi: 'application/thraud+xml',\n  tfm: 'application/x-tex-tfm',\n  tfx: 'image/tiff-fx',\n  tga: 'image/x-tga',\n  thmx: 'application/vnd.ms-officetheme',\n  tif: 'image/tiff',\n  tiff: 'image/tiff',\n  tk: 'application/x-tcl',\n  tmo: 'application/vnd.tmobile-livetv',\n  toml: 'application/toml',\n  torrent: 'application/x-bittorrent',\n  tpl: 'application/vnd.groove-tool-template',\n  tpt: 'application/vnd.trid.tpt',\n  tr: 'text/troff',\n  tra: 'application/vnd.trueapp',\n  trig: 'application/trig',\n  trm: 'application/x-msterminal',\n  ts: 'video/mp2t',\n  tsd: 'application/timestamped-data',\n  tsv: 'text/tab-separated-values',\n  ttc: 'font/collection',\n  ttf: 'font/ttf',\n  ttl: 'text/turtle',\n  ttml: 'application/ttml+xml',\n  twd: 'application/vnd.simtech-mindmapper',\n  twds: 'application/vnd.simtech-mindmapper',\n  txd: 'application/vnd.genomatix.tuxedo',\n  txf: 'application/vnd.mobius.txf',\n  txt: 'text/plain',\n  u32: 'application/x-authorware-bin',\n  u3d: 'model/u3d',\n  u8dsn: 'message/global-delivery-status',\n  u8hdr: 'message/global-headers',\n  u8mdn: 'message/global-disposition-notification',\n  u8msg: 'message/global',\n  ubj: 'application/ubjson',\n  udeb: 'application/x-debian-package',\n  ufd: 'application/vnd.ufdl',\n  ufdl: 'application/vnd.ufdl',\n  ulx: 'application/x-glulx',\n  umj: 'application/vnd.umajin',\n  unityweb: 'application/vnd.unity',\n  uo: 'application/vnd.uoml+xml',\n  uoml: 'application/vnd.uoml+xml',\n  uri: 'text/uri-list',\n  uris: 'text/uri-list',\n  urls: 'text/uri-list',\n  usda: 'model/vnd.usda',\n  usdz: 'model/vnd.usdz+zip',\n  ustar: 'application/x-ustar',\n  utz: 'application/vnd.uiq.theme',\n  uu: 'text/x-uuencode',\n  uva: 'audio/vnd.dece.audio',\n  uvd: 'application/vnd.dece.data',\n  uvf: 'application/vnd.dece.data',\n  uvg: 'image/vnd.dece.graphic',\n  uvh: 'video/vnd.dece.hd',\n  uvi: 'image/vnd.dece.graphic',\n  uvm: 'video/vnd.dece.mobile',\n  uvp: 'video/vnd.dece.pd',\n  uvs: 'video/vnd.dece.sd',\n  uvt: 'application/vnd.dece.ttml+xml',\n  uvu: 'video/vnd.uvvu.mp4',\n  uvv: 'video/vnd.dece.video',\n  uvva: 'audio/vnd.dece.audio',\n  uvvd: 'application/vnd.dece.data',\n  uvvf: 'application/vnd.dece.data',\n  uvvg: 'image/vnd.dece.graphic',\n  uvvh: 'video/vnd.dece.hd',\n  uvvi: 'image/vnd.dece.graphic',\n  uvvm: 'video/vnd.dece.mobile',\n  uvvp: 'video/vnd.dece.pd',\n  uvvs: 'video/vnd.dece.sd',\n  uvvt: 'application/vnd.dece.ttml+xml',\n  uvvu: 'video/vnd.uvvu.mp4',\n  uvvv: 'video/vnd.dece.video',\n  uvvx: 'application/vnd.dece.unspecified',\n  uvvz: 'application/vnd.dece.zip',\n  uvx: 'application/vnd.dece.unspecified',\n  uvz: 'application/vnd.dece.zip',\n  vbox: 'application/x-virtualbox-vbox',\n  'vbox-extpack': 'application/x-virtualbox-vbox-extpack',\n  vcard: 'text/vcard',\n  vcd: 'application/x-cdlink',\n  vcf: 'text/x-vcard',\n  vcg: 'application/vnd.groove-vcard',\n  vcs: 'text/x-vcalendar',\n  vcx: 'application/vnd.vcx',\n  vdi: 'application/x-virtualbox-vdi',\n  vds: 'model/vnd.sap.vds',\n  vdx: 'application/vnd.ms-visio.viewer',\n  vhd: 'application/x-virtualbox-vhd',\n  vis: 'application/vnd.visionary',\n  viv: 'video/vnd.vivo',\n  vmdk: 'application/x-virtualbox-vmdk',\n  vob: 'video/x-ms-vob',\n  vor: 'application/vnd.stardivision.writer',\n  vox: 'application/x-authorware-bin',\n  vrml: 'model/vrml',\n  vsd: 'application/vnd.visio',\n  vsdx: 'application/vnd.visio',\n  vsf: 'application/vnd.vsf',\n  vss: 'application/vnd.visio',\n  vst: 'application/vnd.visio',\n  vsw: 'application/vnd.visio',\n  vtf: 'image/vnd.valve.source.texture',\n  vtt: 'text/vtt',\n  vtu: 'model/vnd.vtu',\n  vtx: 'application/vnd.visio',\n  vxml: 'application/voicexml+xml',\n  w3d: 'application/x-director',\n  wad: 'application/x-doom',\n  wadl: 'application/vnd.sun.wadl+xml',\n  war: 'application/java-archive',\n  wasm: 'application/wasm',\n  wav: 'audio/wav',\n  wax: 'audio/x-ms-wax',\n  wbmp: 'image/vnd.wap.wbmp',\n  wbs: 'application/vnd.criticaltools.wbs+xml',\n  wbxml: 'application/vnd.wap.wbxml',\n  wcm: 'application/vnd.ms-works',\n  wdb: 'application/vnd.ms-works',\n  wdp: 'image/vnd.ms-photo',\n  weba: 'audio/webm',\n  webapp: 'application/x-web-app-manifest+json',\n  webm: 'video/webm',\n  webmanifest: 'application/manifest+json',\n  webp: 'image/webp',\n  wg: 'application/vnd.pmi.widget',\n  wgsl: 'text/wgsl',\n  wgt: 'application/widget',\n  wif: 'application/watcherinfo+xml',\n  wks: 'application/vnd.ms-works',\n  wm: 'video/x-ms-wm',\n  wma: 'audio/x-ms-wma',\n  wmd: 'application/x-ms-wmd',\n  wmf: 'image/wmf',\n  wml: 'text/vnd.wap.wml',\n  wmlc: 'application/vnd.wap.wmlc',\n  wmls: 'text/vnd.wap.wmlscript',\n  wmlsc: 'application/vnd.wap.wmlscriptc',\n  wmv: 'video/x-ms-wmv',\n  wmx: 'video/x-ms-wmx',\n  wmz: 'application/x-ms-wmz',\n  woff: 'font/woff',\n  woff2: 'font/woff2',\n  wpd: 'application/vnd.wordperfect',\n  wpl: 'application/vnd.ms-wpl',\n  wps: 'application/vnd.ms-works',\n  wqd: 'application/vnd.wqd',\n  wri: 'application/x-mswrite',\n  wrl: 'model/vrml',\n  wsc: 'message/vnd.wfa.wsc',\n  wsdl: 'application/wsdl+xml',\n  wspolicy: 'application/wspolicy+xml',\n  wtb: 'application/vnd.webturbo',\n  wvx: 'video/x-ms-wvx',\n  x32: 'application/x-authorware-bin',\n  x3d: 'model/x3d+xml',\n  x3db: 'model/x3d+binary',\n  x3dbz: 'model/x3d+binary',\n  x3dv: 'model/x3d+vrml',\n  x3dvz: 'model/x3d+vrml',\n  x3dz: 'model/x3d+xml',\n  x_b: 'model/vnd.parasolid.transmit.binary',\n  x_t: 'model/vnd.parasolid.transmit.text',\n  xaml: 'application/xaml+xml',\n  xap: 'application/x-silverlight-app',\n  xar: 'application/vnd.xara',\n  xav: 'application/xcap-att+xml',\n  xbap: 'application/x-ms-xbap',\n  xbd: 'application/vnd.fujixerox.docuworks.binder',\n  xbm: 'image/x-xbitmap',\n  xca: 'application/xcap-caps+xml',\n  xcs: 'application/calendar+xml',\n  xdcf: 'application/vnd.gov.sk.xmldatacontainer+xml',\n  xdf: 'application/xcap-diff+xml',\n  xdm: 'application/vnd.syncml.dm+xml',\n  xdp: 'application/vnd.adobe.xdp+xml',\n  xdssc: 'application/dssc+xml',\n  xdw: 'application/vnd.fujixerox.docuworks',\n  xel: 'application/xcap-el+xml',\n  xenc: 'application/xenc+xml',\n  xer: 'application/patch-ops-error+xml',\n  xfdf: 'application/xfdf',\n  xfdl: 'application/vnd.xfdl',\n  xht: 'application/xhtml+xml',\n  xhtm: 'application/vnd.pwg-xhtml-print+xml',\n  xhtml: 'application/xhtml+xml',\n  xhvml: 'application/xv+xml',\n  xif: 'image/vnd.xiff',\n  xla: 'application/vnd.ms-excel',\n  xlam: 'application/vnd.ms-excel.addin.macroenabled.12',\n  xlc: 'application/vnd.ms-excel',\n  xlf: 'application/xliff+xml',\n  xlm: 'application/vnd.ms-excel',\n  xls: 'application/vnd.ms-excel',\n  xlsb: 'application/vnd.ms-excel.sheet.binary.macroenabled.12',\n  xlsm: 'application/vnd.ms-excel.sheet.macroenabled.12',\n  xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n  xlt: 'application/vnd.ms-excel',\n  xltm: 'application/vnd.ms-excel.template.macroenabled.12',\n  xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',\n  xlw: 'application/vnd.ms-excel',\n  xm: 'audio/xm',\n  xml: 'text/xml',\n  xns: 'application/xcap-ns+xml',\n  xo: 'application/vnd.olpc-sugar',\n  xop: 'application/xop+xml',\n  xpi: 'application/x-xpinstall',\n  xpl: 'application/xproc+xml',\n  xpm: 'image/x-xpixmap',\n  xpr: 'application/vnd.is-xpr',\n  xps: 'application/vnd.ms-xpsdocument',\n  xpw: 'application/vnd.intercon.formnet',\n  xpx: 'application/vnd.intercon.formnet',\n  xsd: 'application/xml',\n  xsf: 'application/prs.xsf+xml',\n  xsl: 'application/xslt+xml',\n  xslt: 'application/xslt+xml',\n  xsm: 'application/vnd.syncml+xml',\n  xspf: 'application/xspf+xml',\n  xul: 'application/vnd.mozilla.xul+xml',\n  xvm: 'application/xv+xml',\n  xvml: 'application/xv+xml',\n  xwd: 'image/x-xwindowdump',\n  xyz: 'chemical/x-xyz',\n  xz: 'application/x-xz',\n  yaml: 'text/yaml',\n  yang: 'application/yang',\n  yin: 'application/yin+xml',\n  yml: 'text/yaml',\n  ymp: 'text/x-suse-ymp',\n  z1: 'application/x-zmachine',\n  z2: 'application/x-zmachine',\n  z3: 'application/x-zmachine',\n  z4: 'application/x-zmachine',\n  z5: 'application/x-zmachine',\n  z6: 'application/x-zmachine',\n  z7: 'application/x-zmachine',\n  z8: 'application/x-zmachine',\n  zaz: 'application/vnd.zzazz.deck+xml',\n  zip: 'application/zip',\n  zir: 'application/vnd.zul',\n  zirz: 'application/vnd.zul',\n  zmm: 'application/vnd.handheld-entertainment+xml',\n}\n"
  },
  {
    "path": "packages/mime/src/index.ts",
    "content": "export { detectContentType } from './lib/detect-content-type.ts'\nexport { detectMimeType } from './lib/detect-mime-type.ts'\nexport { isCompressibleMimeType } from './lib/is-compressible-mime-type.ts'\nexport { mimeTypeToContentType } from './lib/mime-type-to-content-type.ts'\nexport { defineMimeType } from './lib/define-mime-type.ts'\nexport type { MimeTypeDefinition } from './lib/define-mime-type.ts'\n"
  },
  {
    "path": "packages/mime/src/lib/define-mime-type.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { beforeEach, describe, it } from 'node:test'\n\nimport { detectContentType } from './detect-content-type.ts'\nimport { detectMimeType } from './detect-mime-type.ts'\nimport { isCompressibleMimeType } from './is-compressible-mime-type.ts'\nimport { mimeTypeToContentType } from './mime-type-to-content-type.ts'\nimport { defineMimeType, resetMimeTypes } from './define-mime-type.ts'\n\ndescribe('defineMimeType()', () => {\n  beforeEach(() => {\n    resetMimeTypes()\n  })\n\n  describe('custom mime types', () => {\n    it('registers a new extension', () => {\n      defineMimeType({\n        extensions: 'customext1',\n        mimeType: 'text/custom1',\n      })\n\n      assert.equal(detectMimeType('customext1'), 'text/custom1')\n    })\n\n    it('overrides a builtin extension', () => {\n      assert.equal(detectMimeType('ts'), 'video/mp2t')\n\n      defineMimeType({\n        extensions: 'ts',\n        mimeType: 'text/typescript',\n      })\n\n      assert.equal(detectMimeType('ts'), 'text/typescript')\n    })\n\n    it('normalizes extension to lowercase', () => {\n      defineMimeType({\n        extensions: 'MDX',\n        mimeType: 'text/mdx',\n      })\n\n      assert.equal(detectMimeType('mdx'), 'text/mdx')\n      assert.equal(detectMimeType('MDX'), 'text/mdx')\n    })\n\n    it('handles extension with leading dot', () => {\n      defineMimeType({\n        extensions: '.mdx',\n        mimeType: 'text/mdx',\n      })\n\n      assert.equal(detectMimeType('mdx'), 'text/mdx')\n      assert.equal(detectMimeType('.mdx'), 'text/mdx')\n    })\n\n    it('trims extension whitespace', () => {\n      defineMimeType({\n        extensions: '  mdx  ',\n        mimeType: 'text/mdx',\n      })\n\n      assert.equal(detectMimeType('mdx'), 'text/mdx')\n    })\n\n    it('works with detectContentType', () => {\n      defineMimeType({\n        extensions: 'mdx',\n        mimeType: 'text/mdx',\n      })\n\n      // text/* types get charset by default\n      assert.equal(detectContentType('mdx'), 'text/mdx; charset=utf-8')\n      assert.equal(detectContentType('file.mdx'), 'text/mdx; charset=utf-8')\n    })\n\n    it('registers multiple extensions for the same MIME type', () => {\n      defineMimeType({\n        extensions: ['jpg', 'jpeg', 'jpe'],\n        mimeType: 'image/jpeg',\n      })\n\n      assert.equal(detectMimeType('jpg'), 'image/jpeg')\n      assert.equal(detectMimeType('jpeg'), 'image/jpeg')\n      assert.equal(detectMimeType('jpe'), 'image/jpeg')\n    })\n  })\n\n  describe('custom compressibility', () => {\n    it('registers a compressible type', () => {\n      defineMimeType({\n        extensions: 'myext',\n        mimeType: 'application/x-myformat',\n        compressible: true,\n      })\n\n      assert.equal(isCompressibleMimeType('application/x-myformat'), true)\n    })\n\n    it('registers a non-compressible type', () => {\n      defineMimeType({\n        extensions: 'myext',\n        mimeType: 'application/x-myformat',\n        compressible: false,\n      })\n\n      assert.equal(isCompressibleMimeType('application/x-myformat'), false)\n    })\n\n    it('can mark a custom format as compressible', () => {\n      // Custom application/* formats don't match default compressibility heuristics\n      defineMimeType({\n        extensions: 'mydata',\n        mimeType: 'application/x-mydata',\n        compressible: true,\n      })\n\n      assert.equal(isCompressibleMimeType('application/x-mydata'), true)\n    })\n\n    it('can override builtin compressibility', () => {\n      // text/html is compressible by default\n      assert.equal(isCompressibleMimeType('text/html'), true)\n\n      defineMimeType({\n        extensions: 'html',\n        mimeType: 'text/html',\n        compressible: false,\n      })\n\n      assert.equal(isCompressibleMimeType('text/html'), false)\n    })\n\n    it('falls back to default heuristics when compressible is omitted', () => {\n      // text/* types are compressible by default heuristic\n      defineMimeType({\n        extensions: 'mdx',\n        mimeType: 'text/mdx',\n      })\n\n      assert.equal(isCompressibleMimeType('text/mdx'), true)\n\n      // application/* types without +json are not compressible by default\n      defineMimeType({\n        extensions: 'mybin',\n        mimeType: 'application/x-mybin',\n      })\n\n      assert.equal(isCompressibleMimeType('application/x-mybin'), false)\n    })\n\n    it('handles Content-Type with parameters', () => {\n      defineMimeType({\n        extensions: 'myext',\n        mimeType: 'application/x-myformat',\n        compressible: true,\n      })\n\n      assert.equal(isCompressibleMimeType('application/x-myformat; charset=utf-8'), true)\n    })\n  })\n\n  describe('custom charset', () => {\n    it('adds charset when specified', () => {\n      defineMimeType({\n        extensions: 'myext',\n        mimeType: 'application/x-myformat',\n        charset: 'utf-8',\n      })\n\n      assert.equal(\n        mimeTypeToContentType('application/x-myformat'),\n        'application/x-myformat; charset=utf-8',\n      )\n    })\n\n    it('adds custom charset', () => {\n      defineMimeType({\n        extensions: 'myext',\n        mimeType: 'application/x-myformat',\n        charset: 'iso-8859-1',\n      })\n\n      assert.equal(\n        mimeTypeToContentType('application/x-myformat'),\n        'application/x-myformat; charset=iso-8859-1',\n      )\n    })\n\n    it('falls back to default heuristics when charset is omitted', () => {\n      // text/* types get charset by default\n      defineMimeType({\n        extensions: 'mdx',\n        mimeType: 'text/mdx',\n      })\n\n      assert.equal(mimeTypeToContentType('text/mdx'), 'text/mdx; charset=utf-8')\n\n      // application/* types without +json don't get charset\n      defineMimeType({\n        extensions: 'mybin',\n        mimeType: 'application/x-mybin',\n      })\n\n      assert.equal(mimeTypeToContentType('application/x-mybin'), 'application/x-mybin')\n    })\n\n    it('does not add charset if already present in input', () => {\n      defineMimeType({\n        extensions: 'myext',\n        mimeType: 'application/x-myformat',\n        charset: 'utf-8',\n      })\n\n      assert.equal(\n        mimeTypeToContentType('application/x-myformat; charset=iso-8859-1'),\n        'application/x-myformat; charset=iso-8859-1',\n      )\n    })\n\n    it('does not override text/xml exception (XML has built-in encoding)', () => {\n      defineMimeType({\n        extensions: 'xml',\n        mimeType: 'text/xml',\n        charset: 'utf-8',\n      })\n\n      // text/xml is a hardcoded exception because XML documents have\n      // built-in encoding detection via BOM and <?xml?> declarations\n      assert.equal(mimeTypeToContentType('text/xml'), 'text/xml')\n    })\n  })\n\n  describe('combined options', () => {\n    it('supports all options together', () => {\n      defineMimeType({\n        extensions: 'myext',\n        mimeType: 'application/x-myformat',\n        compressible: true,\n        charset: 'utf-8',\n      })\n\n      assert.equal(detectMimeType('myext'), 'application/x-myformat')\n      assert.equal(detectMimeType('file.myext'), 'application/x-myformat')\n      assert.equal(isCompressibleMimeType('application/x-myformat'), true)\n      assert.equal(\n        mimeTypeToContentType('application/x-myformat'),\n        'application/x-myformat; charset=utf-8',\n      )\n      assert.equal(detectContentType('myext'), 'application/x-myformat; charset=utf-8')\n    })\n\n    it('allows multiple registrations', () => {\n      defineMimeType({\n        extensions: 'mdx',\n        mimeType: 'text/mdx',\n        compressible: true,\n      })\n\n      defineMimeType({\n        extensions: 'prisma',\n        mimeType: 'text/x-prisma',\n        compressible: true,\n      })\n\n      assert.equal(detectMimeType('mdx'), 'text/mdx')\n      assert.equal(detectMimeType('prisma'), 'text/x-prisma')\n    })\n\n    it('last registration wins for same extension', () => {\n      defineMimeType({\n        extensions: 'myext',\n        mimeType: 'application/x-first',\n      })\n\n      defineMimeType({\n        extensions: 'myext',\n        mimeType: 'application/x-second',\n      })\n\n      assert.equal(detectMimeType('myext'), 'application/x-second')\n    })\n  })\n\n  describe('resetMimeTypes()', () => {\n    it('clears all custom registrations', () => {\n      defineMimeType({\n        extensions: 'reset-test',\n        mimeType: 'application/x-reset-test',\n        compressible: true,\n        charset: 'utf-8',\n      })\n\n      assert.equal(detectMimeType('reset-test'), 'application/x-reset-test')\n      assert.equal(isCompressibleMimeType('application/x-reset-test'), true)\n      assert.equal(\n        mimeTypeToContentType('application/x-reset-test'),\n        'application/x-reset-test; charset=utf-8',\n      )\n\n      resetMimeTypes()\n\n      // Falls back to defaults (reset-test is unknown)\n      assert.equal(detectMimeType('reset-test'), undefined)\n      // Falls back to heuristic (application/* without +json is not compressible)\n      assert.equal(isCompressibleMimeType('application/x-reset-test'), false)\n      // Falls back to heuristic (application/* without +json gets no charset)\n      assert.equal(mimeTypeToContentType('application/x-reset-test'), 'application/x-reset-test')\n    })\n\n    it('restores overridden builtins', () => {\n      defineMimeType({\n        extensions: 'txt',\n        mimeType: 'text/custom',\n      })\n\n      assert.equal(detectMimeType('txt'), 'text/custom')\n\n      resetMimeTypes()\n\n      assert.equal(detectMimeType('txt'), 'text/plain')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/mime/src/lib/define-mime-type.ts",
    "content": "/**\n * Definition used to register a custom MIME type.\n */\nexport interface MimeTypeDefinition {\n  /** The file extension(s) to register (e.g., ['x-myformat']) */\n  extensions: string | string[]\n  /** The MIME type for these extensions (e.g., 'application/x-myformat') */\n  mimeType: string\n  /**\n   * Whether this MIME type is compressible.\n   * If omitted, falls back to default heuristics (text/*, +json, +text, +xml).\n   */\n  compressible?: boolean\n  /**\n   * Charset to include in Content-Type header.\n   * - `'utf-8'` or other string → '; charset={value}'\n   * - `undefined` → falls back to default heuristics (`'utf-8'` for `text/*`, `application/json`, `+json`)\n   */\n  charset?: string\n}\n\n// Custom registries - only created when defineMimeType is called.\n// Exported for direct access to avoid function call overhead in hot paths.\nexport let customMimeTypeByExtension: Map<string, string> | undefined\nexport let customCompressibleByMimeType: Map<string, boolean> | undefined\nexport let customCharsetByMimeType: Map<string, string> | undefined\n\n/**\n * Registers a custom MIME type for one or more file extensions.\n *\n * Use this to add support for file extensions not included in the defaults,\n * or to override the behavior of existing extensions.\n *\n * @param definition The MIME type definition to register\n *\n * @example\n * defineMimeType({\n *   extensions: ['x-myformat'],\n *   mimeType: 'application/x-myformat',\n * })\n *\n * @example\n * // Configure compressibility and charset\n * defineMimeType({\n *   extensions: ['x-myformat'],\n *   mimeType: 'application/x-myformat',\n *   compressible: true, // Optional\n *   charset: 'utf-8', // Optional\n * })\n */\nexport function defineMimeType(definition: MimeTypeDefinition): void {\n  let extensions = Array.isArray(definition.extensions)\n    ? definition.extensions\n    : [definition.extensions]\n\n  customMimeTypeByExtension ??= new Map()\n  for (let ext of extensions) {\n    ext = ext.trim().toLowerCase()\n    // Remove leading dot if present\n    if (ext.startsWith('.')) {\n      ext = ext.slice(1)\n    }\n    customMimeTypeByExtension.set(ext, definition.mimeType)\n  }\n\n  if (definition.compressible !== undefined) {\n    customCompressibleByMimeType ??= new Map()\n    customCompressibleByMimeType.set(definition.mimeType, definition.compressible)\n  }\n\n  if (definition.charset !== undefined) {\n    customCharsetByMimeType ??= new Map()\n    customCharsetByMimeType.set(definition.mimeType, definition.charset)\n  }\n}\n\n// @internal - Resets all custom registrations. Used in tests for isolation.\nexport function resetMimeTypes(): void {\n  customMimeTypeByExtension = undefined\n  customCompressibleByMimeType = undefined\n  customCharsetByMimeType = undefined\n}\n"
  },
  {
    "path": "packages/mime/src/lib/detect-content-type.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { detectContentType } from './detect-content-type.ts'\n\ndescribe('detectContentType()', () => {\n  it('returns Content-Type with charset for text types', () => {\n    assert.equal(detectContentType('css'), 'text/css; charset=utf-8')\n    assert.equal(detectContentType('.css'), 'text/css; charset=utf-8')\n    assert.equal(detectContentType('style.css'), 'text/css; charset=utf-8')\n  })\n\n  it('returns Content-Type with charset for JavaScript', () => {\n    assert.equal(detectContentType('js'), 'text/javascript; charset=utf-8')\n    assert.equal(detectContentType('mjs'), 'text/javascript; charset=utf-8')\n  })\n\n  it('returns Content-Type with charset for JSON', () => {\n    assert.equal(detectContentType('json'), 'application/json; charset=utf-8')\n    assert.equal(detectContentType('data.json'), 'application/json; charset=utf-8')\n  })\n\n  it('returns Content-Type without charset for binary types', () => {\n    assert.equal(detectContentType('png'), 'image/png')\n    assert.equal(detectContentType('jpg'), 'image/jpeg')\n    assert.equal(detectContentType('gif'), 'image/gif')\n    assert.equal(detectContentType('pdf'), 'application/pdf')\n    assert.equal(detectContentType('zip'), 'application/zip')\n  })\n\n  it('returns Content-Type with charset for all text/* types', () => {\n    assert.equal(detectContentType('txt'), 'text/plain; charset=utf-8')\n    assert.equal(detectContentType('html'), 'text/html; charset=utf-8')\n    assert.equal(detectContentType('md'), 'text/markdown; charset=utf-8')\n    assert.equal(detectContentType('csv'), 'text/csv; charset=utf-8')\n  })\n\n  it('returns Content-Type without charset for XML types', () => {\n    // XML has built-in encoding declarations, so charset is not added\n    assert.equal(detectContentType('xml'), 'text/xml')\n    assert.equal(detectContentType('svg'), 'image/svg+xml')\n  })\n\n  it('returns undefined for unknown extensions', () => {\n    assert.equal(detectContentType('unknown'), undefined)\n    assert.equal(detectContentType('.xxyyzz'), undefined)\n    assert.equal(detectContentType('file.xxyyzz'), undefined)\n  })\n\n  it('handles paths', () => {\n    assert.equal(detectContentType('path/to/style.css'), 'text/css; charset=utf-8')\n    assert.equal(detectContentType('/absolute/path/image.png'), 'image/png')\n  })\n\n  it('is case-insensitive', () => {\n    assert.equal(detectContentType('CSS'), 'text/css; charset=utf-8')\n    assert.equal(detectContentType('JSON'), 'application/json; charset=utf-8')\n    assert.equal(detectContentType('PNG'), 'image/png')\n  })\n})\n"
  },
  {
    "path": "packages/mime/src/lib/detect-content-type.ts",
    "content": "import { detectMimeType } from './detect-mime-type.ts'\nimport { mimeTypeToContentType } from './mime-type-to-content-type.ts'\n\n/**\n * Detects the Content-Type header value for a given file extension or filename.\n *\n * Returns a full Content-Type value including charset when appropriate, based on\n * the charset defined in mime-db for the detected MIME type.\n *\n * @param extension The file extension (e.g. \"css\", \".css\") or filename (e.g. \"style.css\")\n * @returns The Content-Type value, or undefined if not found\n *\n * @example\n * detectContentType('css')           // 'text/css;charset=utf-8'\n * detectContentType('.css')          // 'text/css;charset=utf-8'\n * detectContentType('style.css')     // 'text/css;charset=utf-8'\n * detectContentType('image.png')     // 'image/png'\n * detectContentType('unknown')       // undefined\n */\nexport function detectContentType(extension: string): string | undefined {\n  let mimeType = detectMimeType(extension)\n  return mimeType ? mimeTypeToContentType(mimeType) : undefined\n}\n"
  },
  {
    "path": "packages/mime/src/lib/detect-mime-type.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { detectMimeType } from './detect-mime-type.ts'\n\ndescribe('detectMimeType()', () => {\n  it('returns MIME type for plain extension', () => {\n    assert.equal(detectMimeType('txt'), 'text/plain')\n    assert.equal(detectMimeType('html'), 'text/html')\n    assert.equal(detectMimeType('json'), 'application/json')\n    assert.equal(detectMimeType('js'), 'text/javascript')\n    assert.equal(detectMimeType('css'), 'text/css')\n  })\n\n  it('returns MIME type for extension with leading dot', () => {\n    assert.equal(detectMimeType('.txt'), 'text/plain')\n    assert.equal(detectMimeType('.html'), 'text/html')\n    assert.equal(detectMimeType('.json'), 'application/json')\n  })\n\n  it('returns MIME type for filename', () => {\n    assert.equal(detectMimeType('file.txt'), 'text/plain')\n    assert.equal(detectMimeType('index.html'), 'text/html')\n    assert.equal(detectMimeType('data.json'), 'application/json')\n    assert.equal(detectMimeType('script.js'), 'text/javascript')\n    assert.equal(detectMimeType('style.css'), 'text/css')\n  })\n\n  it('returns MIME type for filename with multiple dots', () => {\n    assert.equal(detectMimeType('file.backup.txt'), 'text/plain')\n    assert.equal(detectMimeType('app.min.js'), 'text/javascript')\n    assert.equal(detectMimeType('data.v1.json'), 'application/json')\n  })\n\n  it('returns MIME type for filename with path', () => {\n    assert.equal(detectMimeType('path/to/file.txt'), 'text/plain')\n    assert.equal(detectMimeType('/absolute/path/file.html'), 'text/html')\n    assert.equal(detectMimeType('../relative/file.json'), 'application/json')\n  })\n\n  it('handles uppercase extensions', () => {\n    assert.equal(detectMimeType('TXT'), 'text/plain')\n    assert.equal(detectMimeType('.HTML'), 'text/html')\n    assert.equal(detectMimeType('FILE.JSON'), 'application/json')\n  })\n\n  it('handles mixed case extensions', () => {\n    assert.equal(detectMimeType('TxT'), 'text/plain')\n    assert.equal(detectMimeType('HtMl'), 'text/html')\n    assert.equal(detectMimeType('file.JsOn'), 'application/json')\n  })\n\n  it('trims whitespace', () => {\n    assert.equal(detectMimeType('  txt  '), 'text/plain')\n    assert.equal(detectMimeType('  .html  '), 'text/html')\n    assert.equal(detectMimeType('  file.json  '), 'application/json')\n  })\n\n  it('returns undefined for unknown extensions', () => {\n    assert.equal(detectMimeType('notarealextension'), undefined)\n    assert.equal(detectMimeType('.unknown'), undefined)\n    assert.equal(detectMimeType('file.notarealextension'), undefined)\n  })\n\n  it('returns undefined for empty string', () => {\n    assert.equal(detectMimeType(''), undefined)\n    assert.equal(detectMimeType('   '), undefined)\n  })\n\n  it('returns MIME type for common image formats', () => {\n    assert.equal(detectMimeType('png'), 'image/png')\n    assert.equal(detectMimeType('jpg'), 'image/jpeg')\n    assert.equal(detectMimeType('jpeg'), 'image/jpeg')\n    assert.equal(detectMimeType('gif'), 'image/gif')\n    assert.equal(detectMimeType('svg'), 'image/svg+xml')\n    assert.equal(detectMimeType('webp'), 'image/webp')\n  })\n\n  it('returns MIME type for common video formats', () => {\n    assert.equal(detectMimeType('mp4'), 'application/mp4')\n    assert.equal(detectMimeType('webm'), 'video/webm')\n    assert.equal(detectMimeType('mov'), 'video/quicktime')\n  })\n\n  it('returns MIME type for common audio formats', () => {\n    assert.equal(detectMimeType('mp3'), 'audio/mpeg')\n    assert.equal(detectMimeType('wav'), 'audio/wav')\n    assert.equal(detectMimeType('ogg'), 'audio/ogg')\n  })\n\n  it('returns MIME type for common archive formats', () => {\n    assert.equal(detectMimeType('zip'), 'application/zip')\n    assert.equal(detectMimeType('gz'), 'application/gzip')\n  })\n\n  it('returns MIME type for common document formats', () => {\n    assert.equal(detectMimeType('pdf'), 'application/pdf')\n    assert.equal(detectMimeType('doc'), 'application/msword')\n    assert.equal(detectMimeType('rtf'), 'text/rtf')\n  })\n})\n"
  },
  {
    "path": "packages/mime/src/lib/detect-mime-type.ts",
    "content": "import { mimeTypes } from '../generated/mime-types.ts'\nimport { customMimeTypeByExtension } from './define-mime-type.ts'\n\n/**\n * Detects the MIME type for a given file extension or filename.\n *\n * Custom MIME types registered via {@link import('./define-mime-type.ts').defineMimeType}\n * take precedence over built-in types.\n *\n * @param extension The file extension (e.g. \"txt\", \".txt\") or filename (e.g. \"file.txt\")\n * @returns The MIME type string, or undefined if not found\n *\n * @example\n * detectMimeType('txt')           // 'text/plain'\n * detectMimeType('.txt')          // 'text/plain'\n * detectMimeType('file.txt')      // 'text/plain'\n * detectMimeType('unknown')       // undefined\n */\nexport function detectMimeType(extension: string): string | undefined {\n  let ext = extension.trim().toLowerCase()\n  let idx = ext.lastIndexOf('.')\n  // If no dot found (~idx === -1, so !~idx === true), use ext as-is.\n  // Otherwise, skip past the dot (++idx) and extract the extension.\n  // Credit to mrmime for this technique.\n  ext = !~idx ? ext : ext.substring(++idx)\n  return customMimeTypeByExtension?.get(ext) ?? mimeTypes[ext]\n}\n"
  },
  {
    "path": "packages/mime/src/lib/is-compressible-mime-type.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { isCompressibleMimeType } from './is-compressible-mime-type.ts'\n\ndescribe('isCompressibleMimeType()', () => {\n  it('returns true for common compressible MIME types', () => {\n    assert.equal(isCompressibleMimeType('text/html'), true)\n    assert.equal(isCompressibleMimeType('text/plain'), true)\n    assert.equal(isCompressibleMimeType('application/json'), true)\n    assert.equal(isCompressibleMimeType('application/javascript'), true)\n    assert.equal(isCompressibleMimeType('text/css'), true)\n  })\n\n  it('returns true for text/* types', () => {\n    assert.equal(isCompressibleMimeType('text/custom'), true)\n    assert.equal(isCompressibleMimeType('text/markdown'), true)\n  })\n\n  it('returns true for types with +json, +text, or +xml suffix', () => {\n    assert.equal(isCompressibleMimeType('application/vnd.api+json'), true)\n    assert.equal(isCompressibleMimeType('application/custom+xml'), true)\n    assert.equal(isCompressibleMimeType('application/something+text'), true)\n  })\n\n  it('returns false for non-compressible MIME types', () => {\n    assert.equal(isCompressibleMimeType('image/png'), false)\n    assert.equal(isCompressibleMimeType('image/jpeg'), false)\n    assert.equal(isCompressibleMimeType('video/mp4'), false)\n    assert.equal(isCompressibleMimeType('audio/mpeg'), false)\n  })\n\n  it('returns false for empty string', () => {\n    assert.equal(isCompressibleMimeType(''), false)\n  })\n\n  it('handles Content-Type header values', () => {\n    assert.equal(isCompressibleMimeType('text/html; charset=utf-8'), true)\n    assert.equal(isCompressibleMimeType('application/json; charset=utf-8'), true)\n    assert.equal(isCompressibleMimeType('text/plain; charset=iso-8859-1'), true)\n    assert.equal(\n      isCompressibleMimeType('multipart/form-data; boundary=----WebKitFormBoundary'),\n      false,\n    )\n    assert.equal(isCompressibleMimeType('image/png; name=\"photo.png\"'), false)\n  })\n\n  it('handles Content-Type with whitespace around semicolon', () => {\n    assert.equal(isCompressibleMimeType('text/html ; charset=utf-8'), true)\n    assert.equal(isCompressibleMimeType(' text/html;charset=utf-8'), true)\n    assert.equal(isCompressibleMimeType('  application/json  ;  charset=utf-8'), true)\n  })\n})\n"
  },
  {
    "path": "packages/mime/src/lib/is-compressible-mime-type.ts",
    "content": "import { compressibleMimeTypes } from '../generated/compressible-mime-types.ts'\nimport { customCompressibleByMimeType } from './define-mime-type.ts'\n\n/**\n * Checks if a MIME type is known to be compressible.\n *\n * Returns true for:\n * - Compressible MIME types from mime-db\n * - Any text/* type\n * - Types with +json, +text, or +xml suffix\n * - MIME types explicitly registered as compressible via\n *   {@link import('./define-mime-type.ts').defineMimeType}\n *\n * Accepts either a bare MIME type or a full Content-Type header value with parameters.\n *\n * @param mimeType The MIME type to check (e.g. \"application/json\" or \"text/html; charset=utf-8\")\n * @returns true if the MIME type is known to be compressible\n */\nexport function isCompressibleMimeType(mimeType: string): boolean {\n  if (!mimeType) return false\n\n  // Extract MIME type from Content-Type header if it includes parameters\n  let idx = mimeType.indexOf(';')\n  let type = ~idx ? mimeType.substring(0, idx).trim() : mimeType\n\n  let customCompressible = customCompressibleByMimeType?.get(type)\n  if (customCompressible !== undefined) {\n    return customCompressible\n  }\n\n  if (compressibleMimeTypes.has(type)) {\n    return true\n  }\n\n  return genericCompressibleMimeTypeRegex.test(type)\n}\n\n// Check for text/*, or anything with +json, +text, or +xml suffix\n// Exported for use in codegen to filter redundant entries from compressible-mime-types.ts.\nexport const genericCompressibleMimeTypeRegex = /^text\\/|\\+(?:json|text|xml)$/i\n"
  },
  {
    "path": "packages/mime/src/lib/mime-type-to-content-type.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { mimeTypeToContentType } from './mime-type-to-content-type.ts'\n\ndescribe('mimeTypeToContentType()', () => {\n  it('adds charset for all text/* types (except text/xml)', () => {\n    assert.equal(mimeTypeToContentType('text/plain'), 'text/plain; charset=utf-8')\n    assert.equal(mimeTypeToContentType('text/html'), 'text/html; charset=utf-8')\n    assert.equal(mimeTypeToContentType('text/css'), 'text/css; charset=utf-8')\n    assert.equal(mimeTypeToContentType('text/javascript'), 'text/javascript; charset=utf-8')\n    assert.equal(mimeTypeToContentType('text/markdown'), 'text/markdown; charset=utf-8')\n    assert.equal(mimeTypeToContentType('text/csv'), 'text/csv; charset=utf-8')\n  })\n\n  it('adds charset for +json suffixed types', () => {\n    assert.equal(mimeTypeToContentType('application/json'), 'application/json; charset=utf-8')\n    assert.equal(\n      mimeTypeToContentType('application/manifest+json'),\n      'application/manifest+json; charset=utf-8',\n    )\n    assert.equal(mimeTypeToContentType('application/ld+json'), 'application/ld+json; charset=utf-8')\n    assert.equal(\n      mimeTypeToContentType('application/geo+json'),\n      'application/geo+json; charset=utf-8',\n    )\n  })\n\n  it('adds charset for application/javascript', () => {\n    assert.equal(\n      mimeTypeToContentType('application/javascript'),\n      'application/javascript; charset=utf-8',\n    )\n  })\n\n  it('does not add charset for text/xml (has built-in encoding declarations)', () => {\n    assert.equal(mimeTypeToContentType('text/xml'), 'text/xml')\n  })\n\n  it('does not add charset for binary types', () => {\n    assert.equal(mimeTypeToContentType('image/png'), 'image/png')\n    assert.equal(mimeTypeToContentType('image/jpeg'), 'image/jpeg')\n    assert.equal(mimeTypeToContentType('video/mp4'), 'video/mp4')\n    assert.equal(mimeTypeToContentType('audio/mpeg'), 'audio/mpeg')\n    assert.equal(mimeTypeToContentType('application/pdf'), 'application/pdf')\n    assert.equal(mimeTypeToContentType('application/zip'), 'application/zip')\n    assert.equal(mimeTypeToContentType('application/octet-stream'), 'application/octet-stream')\n    assert.equal(mimeTypeToContentType('font/woff2'), 'font/woff2')\n  })\n\n  it('does not duplicate charset if already present', () => {\n    assert.equal(mimeTypeToContentType('text/plain; charset=utf-8'), 'text/plain; charset=utf-8')\n    assert.equal(\n      mimeTypeToContentType('text/html;charset=iso-8859-1'),\n      'text/html;charset=iso-8859-1',\n    )\n  })\n\n  it('handles unknown MIME types', () => {\n    assert.equal(mimeTypeToContentType('application/x-custom'), 'application/x-custom')\n  })\n})\n"
  },
  {
    "path": "packages/mime/src/lib/mime-type-to-content-type.ts",
    "content": "import { customCharsetByMimeType } from './define-mime-type.ts'\n\n/**\n * Converts a MIME type to a Content-Type header value, adding charset when appropriate.\n *\n * By default, adds `; charset=utf-8` to text-based MIME types:\n * - All `text/*` types (except `text/xml`)\n * - All `+json` suffixed types (RFC 8259 defines JSON as UTF-8)\n * - `application/json`, `application/javascript`\n *\n * Custom charset registered via {@link import('./define-mime-type.ts').defineMimeType}\n * takes precedence over built-in rules.\n *\n * Note: `text/xml` is excluded because XML has built-in encoding detection.\n * Per the XML spec, documents without an encoding declaration must be UTF-8 or\n * UTF-16, detectable from byte patterns. Adding an external charset parameter\n * is redundant and can conflict with the document's internal declaration.\n *\n * @see https://www.w3.org/TR/xml/#charencoding\n *\n * @param mimeType The MIME type (e.g. \"text/css\", \"image/png\")\n * @returns The Content-Type value with charset if appropriate\n *\n * @example\n * mimeTypeToContentType('text/html')           // 'text/html; charset=utf-8'\n * mimeTypeToContentType('application/json')    // 'application/json; charset=utf-8'\n * mimeTypeToContentType('application/ld+json') // 'application/ld+json; charset=utf-8'\n * mimeTypeToContentType('image/png')           // 'image/png'\n * mimeTypeToContentType('text/xml')            // 'text/xml'\n */\nexport function mimeTypeToContentType(mimeType: string): string {\n  // Already has charset - return as-is\n  if (mimeType.includes('charset')) {\n    return mimeType\n  }\n\n  // Exclude text/xml - XML has built-in encoding detection (see JSDoc above)\n  if (mimeType === 'text/xml') {\n    return mimeType\n  }\n\n  // Check custom charset registry\n  let customCharset = customCharsetByMimeType?.get(mimeType)\n  if (customCharset !== undefined) {\n    return `${mimeType}; charset=${customCharset}`\n  }\n\n  // Text-based types that should have charset=utf-8\n  if (\n    mimeType.startsWith('text/') ||\n    mimeType.endsWith('+json') ||\n    mimeType === 'application/json' ||\n    mimeType === 'application/javascript'\n  ) {\n    return `${mimeType}; charset=utf-8`\n  }\n\n  return mimeType\n}\n"
  },
  {
    "path": "packages/mime/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"global.d.ts\", \"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/mime/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"demos\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/multipart-parser/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/multipart-parser/.changes/minor.aggregate-multipart-limits.md",
    "content": "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.\n\nApps that intentionally accept large multipart requests may need to raise `maxParts` or `maxTotalSize` explicitly.\n"
  },
  {
    "path": "packages/multipart-parser/CHANGELOG.md",
    "content": "# `multipart-parser` CHANGELOG\n\nThis is the changelog for [`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser). It follows [semantic versioning](https://semver.org/).\n\n## v0.14.2\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.14.1\n\n### Patch Changes\n\n- Update `@remix-run/headers` peer dependency to use the new header parsing methods.\n\n## v0.14.0 (2025-11-26)\n\n- Move `@remix-run/headers` to `peerDependencies`\n\n## v0.13.0 (2025-11-04)\n\n- Build using `tsc` instead of `esbuild`. This means modules in the `dist` directory now mirror the layout of modules in the `src` directory.\n\n## v0.12.0 (2025-10-22)\n\n- 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()`.\n\n## v0.11.0 (2025-07-24)\n\n- Renamed package from `@mjackson/multipart-parser` to `@remix-run/multipart-parser`\n\n## v0.10.1 (2025-06-13)\n\n- Add doc comments on custom error classes\n\n## v0.10.0 (2025-06-13)\n\nThis 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.\n\nThis 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.\n\nWith 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.\n\n- BREAKING CHANGE: `parseMultipart` and `parseMultipartRequest` are now generators that `yield` `MultipartPart` objects as they are parsed\n- BREAKING CHANGE: `parseMultipart` no longer parses streams, use `parseMultipartStream` instead\n- BREAKING CHANGE: `parser.parse()` is removed\n- BREAKING CHANGE: `part.body`, `part.bodyUsed` are removed\n- BREAKING CHANGE: `part.arrayBuffer`, `part.bytes`, `part.text` are now sync getters instead of `async` methods\n- 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)\n\nNew APIs:\n\n- `parseMultipartStream(stream, options)` is a generator that parses a stream of data\n- `parser.write(chunk)` and `parser.finish()` are low-level APIs for running the parser directly\n- `part.content` is a `Uint8Array[]` of all content in that part\n- `part.isText` is `true` if the part originates from a text field\n- `part.size` is the total size of the content in bytes\n\nIf you're upgrading, check the README for current usage recommendations. Here's a high-level taste of the before/after of this release.\n\n```ts\nimport { parseMultipartRequest } from '@remix-run/multipart-parser'\n\n// before\nawait parseMultipartRequest(request, async (part) => {\n  let buffer = await part.arrayBuffer()\n  // ...\n})\n\n// after\nfor await (let part of parseMultipartRequest(request)) {\n  let buffer = part.arrayBuffer\n  // ...\n}\n```\n\n## v0.9.0 (2025-06-10)\n\n- Add `/src` to npm package, so \"go to definition\" goes to the actual source\n- Use one set of types for all built files, instead of separate types for ESM and CJS\n- Build using esbuild directly instead of tsup\n\n## v0.8.2 (2025-02-04)\n\n- Add `Promise<void>` to `MultipartPartHandler` return type\n\n## v0.8.1 (2025-01-27)\n\n- Fix bad publish that left a `workspace:^` version identifier in package.json\n\n## v0.8.0 (2025-01-24)\n\nThis release improves error handling and simplifies some of the internals of the parser.\n\n- BREAKING CHANGE: Change `parseMultipartRequest` and `parseMultipart` interfaces from `for await...of` to `await` + callback API.\n\n```ts\nimport { parseMultipartRequest } from '@remix-run/multipart-parser'\n\n// before\nfor await (let part of parseMultipartRequest(request)) {\n  // ...\n}\n\n// after\nawait parseMultipartRequest(request, (part) => {\n  // ...\n})\n```\n\nThis 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).\n\n- Add `MaxHeaderSizeExceededError` and `MaxFileSizeExceededError` to make it easier to have finer-grained error handling.\n\n```ts\nimport * as http from 'node:http'\nimport {\n  MultipartParseError,\n  MaxFileSizeExceededError,\n  parseMultipartRequest,\n} from '@remix-run/multipart-parser/node'\n\nconst tenMb = 10 * Math.pow(2, 20)\n\nconst server = http.createServer(async (req, res) => {\n  try {\n    await parseMultipartRequest(req, { maxFileSize: tenMb }, (part) => {\n      // ...\n    })\n  } catch (error) {\n    if (error instanceof MaxFileSizeExceededError) {\n      res.writeHead(413)\n      res.end(error.message)\n    } else if (error instanceof MultipartParseError) {\n      res.writeHead(400)\n      res.end('Invalid multipart request')\n    } else {\n      console.error(error)\n      res.writeHead(500)\n      res.end('Internal Server Error')\n    }\n  }\n})\n```\n\n## v0.7.3 (2025-01-24)\n\n- Add support for environments that do not support `ReadableStream.prototype[Symbol.asyncIterator]` (i.e. Safari), see #46\n\n## v0.7.2 (2024-12-12)\n\n- Fix dependency on `headers` in package.json\n\n## v0.7.1 (2024-12-07)\n\n- 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`.\n\n- ## v0.7.0 (2024-11-14)\n\n- Added CommonJS build\n\n## v0.6.3 (2024-09-05)\n\n- Moved to a new monorepo\n\n## v0.6.2 (2024-08-19)\n\n- Provide correct type for `part.arrayBuffer()`\n- `part.isFile` now correctly detects `part.mediaType === 'application/octet-stream'`\n\n## v0.6.1 (2024-08-18)\n\n- More small performance improvements\n\n## v0.6.0 (2024-08-17)\n\n- BREAKING: Removed some low-level API (`parser.push()` and `parser.reset()`) that was duplicating higher-level API. Use `parser.parse()` instead.\n- Added `parser.maxHeaderSize` and `parser.maxFileSize` properties\n- Small performance improvements when parsing large files\n\n## v0.5.0 (2024-08-15)\n\n- Change default `maxFileSize` from 10 MB to `Infinity`\n- Simplify internal buffer management and search, which leads to more consistent chunk flow when handling large file uploads\n\n## v0.4.2 (2024-08-13)\n\n- Fix bug where max file size exceeded error would crash Node.js servers (https://github.com/mjackson/multipart-parser/issues/8)\n\n## v0.4.1 (2024-08-12)\n\n- Add `type` keyword to `MultipartParserOptions` export for Deno (https://github.com/mjackson/multipart-parser/pull/11)\n\n## v0.4.0 (2024-08-12)\n\n- Switch dependency from `fetch-super-headers` to `@remix-run/headers`\n- 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.\n"
  },
  {
    "path": "packages/multipart-parser/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/multipart-parser/README.md",
    "content": "# multipart-parser\n\nFast 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.\n\n## Features\n\n- **File Upload Parsing** - Parse file uploads (`multipart/form-data`) with automatic field and file detection\n- **Full Multipart Support** - Support for all `multipart/*` content types (mixed, alternative, related, etc.)\n- **Convenient API** - `MultipartPart` API with `arrayBuffer`, `bytes`, `text`, `size`, and metadata access\n- **Built-in Limits** - Header, per-part, part-count, and aggregate-size limits to prevent abuse\n- **Node.js Support** - First-class Node.js support with native `http.IncomingMessage` compatibility\n- **Runtime Demos** - [Demos for every major runtime](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nThe 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.\n\n```ts\nimport { MultipartParseError, parseMultipartRequest } from 'remix/multipart-parser'\n\nasync function handleRequest(request: Request): void {\n  try {\n    for await (let part of parseMultipartRequest(request)) {\n      if (part.isFile) {\n        // Access file data in multiple formats\n        let buffer = part.arrayBuffer // ArrayBuffer\n        console.log(`File received: ${part.filename} (${buffer.byteLength} bytes)`)\n        console.log(`Content type: ${part.mediaType}`)\n        console.log(`Field name: ${part.name}`)\n\n        // Save to disk, upload to cloud storage, etc.\n        await saveFile(part.filename, part.bytes)\n      } else {\n        let text = part.text // string\n        console.log(`Field received: ${part.name} = ${JSON.stringify(text)}`)\n      }\n    }\n  } catch (error) {\n    if (error instanceof MultipartParseError) {\n      console.error('Failed to parse multipart request:', error.message)\n    } else {\n      console.error('An unexpected error occurred:', error)\n    }\n  }\n}\n```\n\n## Size Limits\n\nA 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.\n\n```ts\nimport {\n  MultipartParseError,\n  MaxFileSizeExceededError,\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n  parseMultipartRequest,\n} from 'remix/multipart-parser/node'\n\nconst oneMb = Math.pow(2, 20)\nconst limits = {\n  maxFileSize: 10 * oneMb,\n  maxParts: 100,\n  maxTotalSize: 25 * oneMb,\n}\n\nasync function handleRequest(request: Request): Promise<Response> {\n  try {\n    for await (let part of parseMultipartRequest(request, limits)) {\n      // ...\n    }\n  } catch (error) {\n    if (error instanceof MaxFileSizeExceededError) {\n      return new Response('File size limit exceeded', { status: 413 })\n    } else if (error instanceof MaxPartsExceededError) {\n      return new Response('Too many multipart parts', { status: 413 })\n    } else if (error instanceof MaxTotalSizeExceededError) {\n      return new Response('Multipart request is too large', { status: 413 })\n    } else if (error instanceof MultipartParseError) {\n      return new Response('Failed to parse multipart request', { status: 400 })\n    } else {\n      console.error(error)\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  }\n}\n```\n\n## Node.js Bindings\n\nThe 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).\n\nIf 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.\n\n```ts\nimport * as http from 'node:http'\nimport { MultipartParseError, parseMultipartRequest } from 'remix/multipart-parser/node'\n\nlet server = http.createServer(async (req, res) => {\n  try {\n    for await (let part of parseMultipartRequest(req)) {\n      // ...\n    }\n  } catch (error) {\n    if (error instanceof MultipartParseError) {\n      console.error('Failed to parse multipart request:', error.message)\n    } else {\n      console.error('An unexpected error occurred:', error)\n    }\n  }\n})\n\nserver.listen(8080)\n```\n\n## Low-level API\n\nIf 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:\n\n```ts\nimport { parseMultipart } from 'remix/multipart-parser'\n\nlet message = new Uint8Array(/* ... */)\nlet boundary = '----WebKitFormBoundary56eac3x'\n\nfor (let part of parseMultipart(message, { boundary })) {\n  // ...\n}\n```\n\nIn addition, the `parseMultipartStream` function provides an `async` generator interface for multipart data in a `ReadableStream`:\n\n```ts\nimport { parseMultipartStream } from 'remix/multipart-parser'\n\nlet message = new ReadableStream(/* ... */)\nlet boundary = '----WebKitFormBoundary56eac3x'\n\nfor await (let part of parseMultipartStream(message, { boundary })) {\n  // ...\n}\n```\n\n## Demos\n\nThe [`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:\n\n- [`demos/bun`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/bun) - using multipart-parser in Bun\n- [`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\n- [`demos/deno`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/deno) - using multipart-parser in Deno\n- [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/node) - using multipart-parser in Node.js\n\n## Benchmark\n\n`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`.\n\nThe results of running the benchmarks on my laptop:\n\n```\n> @remix-run/multipart-parser@0.10.1 bench:node /Users/michael/Projects/remix-the-web/packages/multipart-parser\n> node ./bench/runner.ts\n\nPlatform: Darwin (24.5.0)\nCPU: Apple M1 Pro\nDate: 6/13/2025, 12:27:09 PM\nNode.js v24.0.2\n┌──────────────────┬──────────────────┬──────────────────┬──────────────────┬───────────────────┐\n│ (index)          │ 1 small file     │ 1 large file     │ 100 small files  │ 5 large files     │\n├──────────────────┼──────────────────┼──────────────────┼──────────────────┼───────────────────┤\n│ multipart-parser │ '0.01 ms ± 0.03' │ '1.08 ms ± 0.08' │ '0.04 ms ± 0.01' │ '10.50 ms ± 0.38' │\n│ multipasta       │ '0.02 ms ± 0.06' │ '1.07 ms ± 0.02' │ '0.15 ms ± 0.02' │ '10.46 ms ± 0.11' │\n│ busboy           │ '0.06 ms ± 0.17' │ '3.07 ms ± 0.24' │ '0.24 ms ± 0.05' │ '29.85 ms ± 0.18' │\n│ @fastify/busboy  │ '0.05 ms ± 0.13' │ '1.23 ms ± 0.09' │ '0.45 ms ± 0.22' │ '11.81 ms ± 0.11' │\n└──────────────────┴──────────────────┴──────────────────┴──────────────────┴───────────────────┘\n\n> @remix-run/multipart-parser@0.10.1 bench:bun /Users/michael/Projects/remix-the-web/packages/multipart-parser\n> bun run ./bench/runner.ts\n\nPlatform: Darwin (24.5.0)\nCPU: Apple M1 Pro\nDate: 6/13/2025, 12:27:31 PM\nBun 1.2.13\n┌──────────────────┬────────────────┬────────────────┬─────────────────┬─────────────────┐\n│                  │ 1 small file   │ 1 large file   │ 100 small files │ 5 large files   │\n├──────────────────┼────────────────┼────────────────┼─────────────────┼─────────────────┤\n│ multipart-parser │ 0.01 ms ± 0.04 │ 0.86 ms ± 0.09 │ 0.04 ms ± 0.01  │ 8.32 ms ± 0.26  │\n│       multipasta │ 0.02 ms ± 0.07 │ 0.87 ms ± 0.03 │ 0.25 ms ± 0.21  │ 8.27 ms ± 0.09  │\n│           busboy │ 0.05 ms ± 0.17 │ 3.54 ms ± 0.10 │ 0.30 ms ± 0.03  │ 34.79 ms ± 0.38 │\n│  @fastify/busboy │ 0.06 ms ± 0.18 │ 4.04 ms ± 0.08 │ 0.48 ms ± 0.06  │ 39.91 ms ± 0.37 │\n└──────────────────┴────────────────┴────────────────┴─────────────────┴─────────────────┘\n\n> @remix-run/multipart-parser@0.10.1 bench:deno /Users/michael/Projects/remix-the-web/packages/multipart-parser\n> deno run --allow-sys ./bench/runner.ts\n\nPlatform: Darwin (24.5.0)\nCPU: Apple M1 Pro\nDate: 6/13/2025, 12:28:12 PM\nDeno 2.3.6\n┌──────────────────┬──────────────────┬────────────────────┬──────────────────┬─────────────────────┐\n│ (idx)            │ 1 small file     │ 1 large file       │ 100 small files  │ 5 large files       │\n├──────────────────┼──────────────────┼────────────────────┼──────────────────┼─────────────────────┤\n│ multipart-parser │ \"0.01 ms ± 0.03\" │ \"1.03 ms ± 0.04\"   │ \"0.05 ms ± 0.01\" │ \"10.05 ms ± 0.20\"   │\n│ multipasta       │ \"0.02 ms ± 0.07\" │ \"1.04 ms ± 0.03\"   │ \"0.16 ms ± 0.02\" │ \"10.10 ms ± 0.08\"   │\n│ busboy           │ \"0.05 ms ± 0.19\" │ \"3.06 ms ± 0.15\"   │ \"0.32 ms ± 0.05\" │ \"29.92 ms ± 0.24\"   │\n│ @fastify/busboy  │ \"0.06 ms ± 0.14\" │ \"14.72 ms ± 11.42\" │ \"0.81 ms ± 0.20\" │ \"127.63 ms ± 35.77\" │\n└──────────────────┴──────────────────┴────────────────────┴──────────────────┴─────────────────────┘\n```\n\n## Related Packages\n\n- [`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\n- [`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`\n\n## Credits\n\nThanks to Jacob Ebey who gave me several code reviews on this project prior to publishing.\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/multipart-parser/bench/messages.ts",
    "content": "import { concat, getRandomBytes } from './utils.ts'\n\nconst NodeDefaultHighWaterMark = 65536\n\nexport class MultipartMessage {\n  boundary: string\n  content: Uint8Array\n  #chunkCache: Map<number, Uint8Array[]> = new Map()\n\n  constructor(boundary: string, partSizesOrContents: number[] | Uint8Array[]) {\n    this.boundary = boundary\n\n    let chunks: Uint8Array[] = []\n\n    function pushString(string: string): void {\n      chunks.push(new TextEncoder().encode(string))\n    }\n\n    function pushLine(line = ''): void {\n      pushString(line + '\\r\\n')\n    }\n\n    let partContents =\n      typeof partSizesOrContents[0] === 'number'\n        ? (partSizesOrContents as number[]).map((size) => getRandomBytes(size))\n        : (partSizesOrContents as Uint8Array[])\n\n    for (let i = 0; i < partContents.length; i++) {\n      pushLine(`--${boundary}`)\n      pushLine(`Content-Disposition: form-data; name=\"file${i}\"; filename=\"file${i}.dat\"`)\n      pushLine('Content-Type: application/octet-stream')\n      pushLine()\n      chunks.push(partContents[i])\n      pushLine()\n    }\n\n    pushString(`--${boundary}--`)\n\n    this.content = concat(chunks)\n  }\n\n  getChunks(chunkSize = NodeDefaultHighWaterMark): Uint8Array[] {\n    let cached = this.#chunkCache.get(chunkSize)\n    if (cached !== undefined) {\n      return cached\n    }\n\n    let chunks: Uint8Array[] = []\n    for (let i = 0; i < this.content.length; i += chunkSize) {\n      chunks.push(this.content.subarray(i, i + chunkSize))\n    }\n\n    this.#chunkCache.set(chunkSize, chunks)\n    return chunks\n  }\n\n  *generateChunks(chunkSize = NodeDefaultHighWaterMark): Generator<Uint8Array> {\n    for (let chunk of this.getChunks(chunkSize)) {\n      yield chunk\n    }\n  }\n}\n\nconst oneKb = 1024\nconst oneMb = 1024 * oneKb\nconst boundary = '----WebKitFormBoundaryzv0Og5zWtGjvzP2A'\n\nfunction createAdversarialBytes(size: number, boundary: string): Uint8Array {\n  let repeatingPattern = new TextEncoder().encode(`\\r\\n--${boundary.slice(0, -1)}X`)\n  let bytes = new Uint8Array(size)\n\n  for (let i = 0; i < size; i += repeatingPattern.length) {\n    bytes.set(repeatingPattern.subarray(0, Math.min(repeatingPattern.length, size - i)), i)\n  }\n\n  return bytes\n}\n\nexport const oneSmallFile = new MultipartMessage(boundary, [oneKb])\n\nexport const oneLargeFile = new MultipartMessage(boundary, [10 * oneMb])\n\nexport const oneHundredSmallFiles = new MultipartMessage(boundary, Array(100).fill(oneKb))\n\nexport const fiveLargeFiles = new MultipartMessage(boundary, [\n  10 * oneMb,\n  10 * oneMb,\n  10 * oneMb,\n  20 * oneMb,\n  50 * oneMb,\n])\n\nexport const oneLargeFileAdversarial = new MultipartMessage(boundary, [\n  createAdversarialBytes(10 * oneMb, boundary),\n])\n\nexport const fiveLargeFilesAdversarial = new MultipartMessage(boundary, [\n  createAdversarialBytes(10 * oneMb, boundary),\n  createAdversarialBytes(10 * oneMb, boundary),\n  createAdversarialBytes(10 * oneMb, boundary),\n  createAdversarialBytes(20 * oneMb, boundary),\n  createAdversarialBytes(50 * oneMb, boundary),\n])\n"
  },
  {
    "path": "packages/multipart-parser/bench/package.json",
    "content": "{\n  \"name\": \"multipart-parser-bench\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@fastify/busboy\": \"^3.0.0\",\n    \"@remix-run/multipart-parser\": \"workspace:^\",\n    \"busboy\": \"^1.6.0\",\n    \"multipasta\": \"^0.2.4\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"^1.1.6\",\n    \"@types/busboy\": \"^1.5.4\"\n  }\n}\n"
  },
  {
    "path": "packages/multipart-parser/bench/parsers/busboy.ts",
    "content": "import { Readable } from 'node:stream'\nimport busboy from 'busboy'\n\nimport { MultipartMessage } from '../messages.ts'\n\nexport function parse(message: MultipartMessage): Promise<number> {\n  let stream = new Readable({\n    read() {\n      for (let chunk of message.generateChunks()) {\n        this.push(chunk)\n      }\n      this.push(null)\n    },\n  })\n\n  return new Promise((resolve, reject) => {\n    let start = performance.now()\n\n    let bb = busboy({\n      headers: { 'content-type': `multipart/form-data; boundary=${message.boundary}` },\n      limits: { fileSize: Infinity },\n    })\n\n    bb.on('field', () => {})\n\n    bb.on('file', (_name, stream) => {\n      stream.resume()\n    })\n\n    bb.on('error', reject)\n\n    bb.on('close', () => {\n      resolve(performance.now() - start)\n    })\n\n    stream.pipe(bb)\n  })\n}\n"
  },
  {
    "path": "packages/multipart-parser/bench/parsers/fastify-busboy.ts",
    "content": "import { Readable } from 'node:stream'\nimport * as busboy from '@fastify/busboy'\n\nimport { MultipartMessage } from '../messages.ts'\n\nexport function parse(message: MultipartMessage): Promise<number> {\n  let stream = new Readable({\n    read() {\n      for (let chunk of message.generateChunks()) {\n        this.push(chunk)\n      }\n      this.push(null)\n    },\n  })\n\n  return new Promise((resolve, reject) => {\n    let start = performance.now()\n\n    let bb = new busboy.Busboy({\n      headers: { 'content-type': `multipart/form-data; boundary=${message.boundary}` },\n      limits: { fileSize: Infinity },\n    })\n\n    bb.on('field', () => {})\n\n    bb.on('file', (_name, stream) => {\n      stream.resume()\n    })\n\n    bb.on('error', reject)\n\n    bb.on('finish', () => {\n      resolve(performance.now() - start)\n    })\n\n    stream.pipe(bb)\n  })\n}\n"
  },
  {
    "path": "packages/multipart-parser/bench/parsers/multipart-parser.ts",
    "content": "import { parseMultipart } from '@remix-run/multipart-parser'\n\nimport { MultipartMessage } from '../messages.ts'\n\nconst BenchmarkMaxFileSize = 100 * 1024 * 1024 // 100 MiB\n\nexport function parse(message: MultipartMessage): number {\n  let start = performance.now()\n\n  for (let _ of parseMultipart(message.generateChunks(), {\n    boundary: message.boundary,\n    maxFileSize: BenchmarkMaxFileSize,\n  })) {\n    // Do nothing with the part, just iterate through it to measure parsing time\n  }\n\n  return performance.now() - start\n}\n"
  },
  {
    "path": "packages/multipart-parser/bench/parsers/multipasta.ts",
    "content": "import * as Multipasta from 'multipasta'\n\nimport { MultipartMessage } from '../messages.ts'\n\nexport function parse(message: MultipartMessage): Promise<number> {\n  return new Promise((resolve, reject) => {\n    const start = performance.now()\n    const parser = Multipasta.make({\n      headers: { 'content-type': `multipart/form-data; boundary=${message.boundary}` },\n      onDone() {\n        resolve(performance.now() - start)\n      },\n      onError: reject,\n      onFile(_info) {\n        return (_chunk) => {}\n      },\n      onField() {},\n    })\n    for (const chunk of message.generateChunks()) {\n      parser.write(chunk)\n    }\n    parser.end()\n  })\n}\n"
  },
  {
    "path": "packages/multipart-parser/bench/runner.ts",
    "content": "import * as os from 'node:os'\nimport * as process from 'node:process'\n\nimport * as messages from './messages.ts'\nimport * as busboy from './parsers/busboy.ts'\nimport * as fastifyBusboy from './parsers/fastify-busboy.ts'\nimport * as multipartParser from './parsers/multipart-parser.ts'\nimport * as multipasta from './parsers/multipasta.ts'\n\nconst benchmarks = [\n  { id: '1-small-file', name: '1 small file', message: messages.oneSmallFile },\n  { id: '1-large-file', name: '1 large file', message: messages.oneLargeFile },\n  { id: '100-small-files', name: '100 small files', message: messages.oneHundredSmallFiles },\n  { id: '5-large-files', name: '5 large files', message: messages.fiveLargeFiles },\n  {\n    id: '1-large-file-adversarial',\n    name: '1 large file (adversarial)',\n    message: messages.oneLargeFileAdversarial,\n  },\n  {\n    id: '5-large-files-adversarial',\n    name: '5 large files (adversarial)',\n    message: messages.fiveLargeFilesAdversarial,\n  },\n]\n\ninterface Parser {\n  parse(message: messages.MultipartMessage): Promise<number>\n}\n\ninterface ParsedOptions {\n  parserName?: string\n  benchmarkId?: string\n  times?: number\n  warmupTimes: number\n  metrics: boolean\n  steady: boolean\n}\n\ninterface BenchmarkStats {\n  meanMs: number\n  stdDevMs: number\n  throughputMibPerSec: number\n  heapDeltaBytesPerOp: number\n  retainedHeapBytesPerOp: number\n}\n\ninterface BenchmarkResult {\n  summary: string\n  stats?: BenchmarkStats\n}\n\nasync function runParserBenchmarks(\n  parser: Parser,\n  options?: ParsedOptions,\n): Promise<BenchmarkResults[string]> {\n  let results: BenchmarkResults[string] = {}\n  let times = options?.times ?? 200\n  let warmupTimes = options?.steady ? options.warmupTimes : 0\n\n  for (let benchmark of benchmarks) {\n    if (options?.benchmarkId !== undefined && benchmark.id !== options.benchmarkId) {\n      continue\n    }\n\n    if (options?.steady) {\n      benchmark.message.getChunks()\n    }\n\n    for (let i = 0; i < warmupTimes; ++i) {\n      await parser.parse(benchmark.message)\n    }\n\n    let beforeHeapUsed = getHeapUsed()\n    if (options?.metrics) {\n      runGc()\n    }\n    let beforeRetainedHeap = getHeapUsed()\n\n    let measurements: number[] = []\n    for (let i = 0; i < times; ++i) {\n      measurements.push(await parser.parse(benchmark.message))\n    }\n\n    let afterHeapUsed = getHeapUsed()\n    if (options?.metrics) {\n      runGc()\n    }\n    let afterRetainedHeap = getHeapUsed()\n\n    results[benchmark.name] = getBenchmarkResult(\n      measurements,\n      benchmark.message.content.length,\n      (afterHeapUsed - beforeHeapUsed) / times,\n      (afterRetainedHeap - beforeRetainedHeap) / times,\n      options?.metrics === true,\n    )\n  }\n\n  return results\n}\n\nfunction getBenchmarkStats(\n  measurements: number[],\n  messageSizeBytes: number,\n  heapDeltaBytesPerOp: number,\n  retainedHeapBytesPerOp: number,\n): BenchmarkStats {\n  let mean = measurements.reduce((a, b) => a + b, 0) / measurements.length\n  let variance = measurements.reduce((a, b) => a + (b - mean) ** 2, 0) / measurements.length\n  let stdDev = Math.sqrt(variance)\n  let throughputMibPerSec = messageSizeBytes / (1024 * 1024) / (mean / 1000)\n\n  return {\n    meanMs: mean,\n    stdDevMs: stdDev,\n    throughputMibPerSec,\n    heapDeltaBytesPerOp,\n    retainedHeapBytesPerOp,\n  }\n}\n\ninterface BenchmarkResults {\n  [parserName: string]: {\n    [benchmarkName: string]: BenchmarkResult\n  }\n}\n\nfunction getBenchmarkResult(\n  measurements: number[],\n  messageSizeBytes: number,\n  heapDeltaBytesPerOp: number,\n  retainedHeapBytesPerOp: number,\n  withStats: boolean,\n): BenchmarkResult {\n  let stats = getBenchmarkStats(\n    measurements,\n    messageSizeBytes,\n    heapDeltaBytesPerOp,\n    retainedHeapBytesPerOp,\n  )\n\n  return {\n    summary: `${stats.meanMs.toFixed(2)} ms ± ${stats.stdDevMs.toFixed(2)}`,\n    stats: withStats ? stats : undefined,\n  }\n}\n\nfunction getHeapUsed(): number {\n  if (typeof process.memoryUsage !== 'function') {\n    return 0\n  }\n  return process.memoryUsage().heapUsed\n}\n\nfunction runGc(): void {\n  let gc = (globalThis as unknown as { gc?: () => void }).gc\n  gc?.()\n}\n\nfunction parseArgOptions(): ParsedOptions {\n  let args = process.argv.slice(2)\n  if (args[0] === '--') {\n    args = args.slice(1)\n  }\n\n  let positionalArgs: string[] = []\n  let optionArgs: string[] = []\n  for (let arg of args) {\n    if (arg.startsWith('--')) {\n      optionArgs.push(arg)\n    } else {\n      positionalArgs.push(arg)\n    }\n  }\n\n  let parserName = positionalArgs[0]\n  let benchmarkId = positionalArgs[1]\n  let timesArg = positionalArgs[2]\n\n  if (timesArg === undefined && benchmarkId !== undefined && /^\\d+$/.test(benchmarkId)) {\n    timesArg = benchmarkId\n    benchmarkId = undefined\n  }\n  let times = timesArg === undefined ? undefined : Number.parseInt(timesArg, 10)\n\n  if (benchmarkId !== undefined && !benchmarks.some((benchmark) => benchmark.id === benchmarkId)) {\n    let availableIds = benchmarks.map((benchmark) => benchmark.id).join(', ')\n    throw new Error(\n      `Unknown benchmark id \"${benchmarkId}\". Use one of: ${availableIds} (or omit for all cases)`,\n    )\n  }\n\n  if (times !== undefined && (!Number.isFinite(times) || times <= 0)) {\n    throw new Error(`Invalid iterations \"${timesArg}\". Expected a positive integer`)\n  }\n\n  let steady = false\n  let metrics = false\n  let warmupTimes = 50\n\n  for (let arg of optionArgs) {\n    if (arg === '--') {\n      continue\n    }\n\n    if (arg === '--steady') {\n      steady = true\n      continue\n    }\n\n    if (arg === '--metrics') {\n      metrics = true\n      continue\n    }\n\n    if (arg.startsWith('--warmup=')) {\n      let parsedWarmupTimes = Number.parseInt(arg.slice('--warmup='.length), 10)\n      if (!Number.isFinite(parsedWarmupTimes) || parsedWarmupTimes < 0) {\n        throw new Error(`Invalid warmup iteration count \"${arg}\"`)\n      }\n      warmupTimes = parsedWarmupTimes\n      continue\n    }\n\n    throw new Error(\n      `Unknown option \"${arg}\". Supported options: --steady, --metrics, --warmup=<count>`,\n    )\n  }\n\n  return { parserName, benchmarkId, times, warmupTimes, metrics, steady }\n}\n\nasync function runBenchmarks(\n  parserName?: string,\n  options?: ParsedOptions,\n): Promise<BenchmarkResults> {\n  let results: BenchmarkResults = {}\n\n  if (parserName === 'multipart-parser' || parserName === undefined) {\n    results['multipart-parser'] = await runParserBenchmarks(multipartParser, options)\n  }\n  if (parserName === 'multipasta' || parserName === undefined) {\n    results['multipasta'] = await runParserBenchmarks(multipasta, options)\n  }\n  if (parserName === 'busboy' || parserName === undefined) {\n    results.busboy = await runParserBenchmarks(busboy, options)\n  }\n  if (parserName === 'fastify-busboy' || parserName === undefined) {\n    results['@fastify/busboy'] = await runParserBenchmarks(fastifyBusboy, options)\n  }\n\n  return results\n}\n\nfunction printResults(results: BenchmarkResults) {\n  console.log(`Platform: ${os.type()} (${os.release()})`)\n  console.log(`CPU: ${os.cpus()[0].model}`)\n  console.log(`Date: ${new Date().toLocaleString()}`)\n\n  if (typeof Bun !== 'undefined') {\n    console.log(`Bun ${Bun.version}`)\n  } else if (typeof Deno !== 'undefined') {\n    console.log(`Deno ${Deno.version.deno}`)\n  } else {\n    console.log(`Node.js ${process.version}`)\n  }\n\n  let summaryResults: Record<string, Record<string, string>> = {}\n  for (let parserName of Object.keys(results)) {\n    summaryResults[parserName] = {}\n    for (let benchmarkName of Object.keys(results[parserName])) {\n      summaryResults[parserName][benchmarkName] = results[parserName][benchmarkName].summary\n    }\n  }\n  console.table(summaryResults)\n\n  let hasStats = Object.values(results).some((resultByBenchmark) =>\n    Object.values(resultByBenchmark).some((result) => result.stats !== undefined),\n  )\n\n  if (hasStats) {\n    let metricResults: Record<string, Record<string, string>> = {}\n    for (let parserName of Object.keys(results)) {\n      metricResults[parserName] = {}\n      for (let benchmarkName of Object.keys(results[parserName])) {\n        let stats = results[parserName][benchmarkName].stats\n        if (!stats) continue\n        metricResults[parserName][benchmarkName] =\n          `${stats.throughputMibPerSec.toFixed(2)} MiB/s | heapΔ ${stats.heapDeltaBytesPerOp.toFixed(0)} B/op | retainedΔ ${stats.retainedHeapBytesPerOp.toFixed(0)} B/op`\n      }\n    }\n    console.log('\\nThroughput and allocation metrics')\n    console.table(metricResults)\n  }\n}\n\nlet options = parseArgOptions()\n\nrunBenchmarks(options.parserName, {\n  benchmarkId: options.benchmarkId,\n  times: options.times,\n  warmupTimes: options.warmupTimes,\n  metrics: options.metrics,\n  steady: options.steady,\n}).then(printResults, (error) => {\n  console.error(error)\n  process.exit(1)\n})\n"
  },
  {
    "path": "packages/multipart-parser/bench/utils.ts",
    "content": "export function concat(chunks: Uint8Array[]): Uint8Array {\n  if (chunks.length === 1) return chunks[0]\n\n  let length = 0\n  for (let chunk of chunks) {\n    length += chunk.length\n  }\n\n  let result = new Uint8Array(length)\n  let offset = 0\n\n  for (let chunk of chunks) {\n    result.set(chunk, offset)\n    offset += chunk.length\n  }\n\n  return result\n}\n\nexport function getRandomBytes(size: number): Uint8Array {\n  let chunks: Uint8Array[] = []\n\n  for (let i = 0; i < size; i += 65536) {\n    chunks.push(crypto.getRandomValues(new Uint8Array(Math.min(size - i, 65536))))\n  }\n\n  return concat(chunks)\n}\n"
  },
  {
    "path": "packages/multipart-parser/demos/bun/.gitignore",
    "content": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Caches\n\n.cache\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\n\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# Runtime data\n\npids\n_.pid\n_.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\n\nlib-cov\n\n# Coverage directory used by tools like istanbul\n\ncoverage\n*.lcov\n\n# nyc test coverage\n\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n\n.grunt\n\n# Bower dependency directory (https://bower.io/)\n\nbower_components\n\n# node-waf configuration\n\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\n\nbuild/Release\n\n# Dependency directories\n\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\n\nweb_modules/\n\n# TypeScript cache\n\n*.tsbuildinfo\n\n# Optional npm cache directory\n\n.npm\n\n# Optional eslint cache\n\n.eslintcache\n\n# Optional stylelint cache\n\n.stylelintcache\n\n# Microbundle cache\n\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n\n.node_repl_history\n\n# Output of 'npm pack'\n\n*.tgz\n\n# Yarn Integrity file\n\n.yarn-integrity\n\n# dotenv environment variable files\n\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n\n.parcel-cache\n\n# Next.js build output\n\n.next\nout\n\n# Nuxt.js build / generate output\n\n.nuxt\ndist\n\n# Gatsby files\n\n# Comment in the public line in if your project uses Gatsby and not Next.js\n\n# https://nextjs.org/blog/next-9-1#public-directory-support\n\n# public\n\n# vuepress build output\n\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n\n.temp\n\n# Docusaurus cache and generated files\n\n.docusaurus\n\n# Serverless directories\n\n.serverless/\n\n# FuseBox cache\n\n.fusebox/\n\n# DynamoDB Local files\n\n.dynamodb/\n\n# TernJS port file\n\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# IntelliJ based IDEs\n.idea\n\n# Finder (MacOS) folder config\n.DS_Store\n"
  },
  {
    "path": "packages/multipart-parser/demos/bun/README.md",
    "content": "# multipart-parser Bun Example\n\nThis is an example of handling a `<form enctype=\"multipart/form-data\">` POST to [a Bun server](https://bun.sh) and streaming any files it contains to a tmp directory on the filesystem.\n"
  },
  {
    "path": "packages/multipart-parser/demos/bun/package.json",
    "content": "{\n  \"name\": \"multipart-parser-bun-example\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@remix-run/multipart-parser\": \"workspace:^\",\n    \"tmp\": \"^0.2.3\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"^1.1.6\",\n    \"@types/tmp\": \"^0.2.6\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"start\": \"bun run server.ts\",\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "packages/multipart-parser/demos/bun/server.ts",
    "content": "import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser'\nimport tmp from 'tmp'\n\nconst server = Bun.serve({\n  port: 44100,\n  async fetch(request) {\n    if (request.method === 'GET') {\n      return new Response(\n        `\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>multipart-parser Bun Example</title>\n  </head>\n  <body>\n    <h1>multipart-parser Bun Example</h1>\n    <form method=\"post\" enctype=\"multipart/form-data\">\n      <p><input name=\"text1\" type=\"text\" /></p>\n      <p><input name=\"file1\" type=\"file\" /></p>\n      <p><button type=\"submit\">Submit</button></p>\n    </form>\n  </body>\n</html>\n`,\n        {\n          headers: { 'Content-Type': 'text/html' },\n        },\n      )\n    }\n\n    if (request.method === 'POST') {\n      try {\n        let parts: any[] = []\n\n        for await (let part of parseMultipartRequest(request)) {\n          if (part.isFile) {\n            let tmpfile = tmp.fileSync()\n            Bun.write(tmpfile.name, part.bytes)\n\n            parts.push({\n              name: part.name,\n              filename: part.filename,\n              mediaType: part.mediaType,\n              size: part.size,\n              file: tmpfile.name,\n            })\n          } else {\n            parts.push({ name: part.name, value: part.text })\n          }\n        }\n\n        return new Response(JSON.stringify({ parts }, null, 2), {\n          headers: { 'Content-Type': 'application/json' },\n        })\n      } catch (error) {\n        if (error instanceof MultipartParseError) {\n          return new Response(`Error: ${error.message}`, { status: 400 })\n        }\n\n        console.error(error)\n\n        return new Response('Internal Server Error', { status: 500 })\n      }\n    }\n\n    return new Response('Method Not Allowed', { status: 405 })\n  },\n})\n\nconsole.log(`Server listening on http://localhost:${server.port} ...`)\n"
  },
  {
    "path": "packages/multipart-parser/demos/bun/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    // Enable latest features\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": true,\n\n    // Bundler mode\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n\n    // Best practices\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    // Some stricter flags (disabled by default)\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noPropertyAccessFromIndexSignature\": false\n  }\n}\n"
  },
  {
    "path": "packages/multipart-parser/demos/cf-workers/.gitignore",
    "content": ".wrangler"
  },
  {
    "path": "packages/multipart-parser/demos/cf-workers/README.md",
    "content": "# multipart-parser CF Workers Example\n\nThis is a demo of how you can upload a file directly to [a Cloudflare worker](https://developers.cloudflare.com/workers/) and store it in R2.\n\nNotice: `multipart-parser` doesn't use any node-specific APIs, so this demo does not rely on Cloudflare Workers' [`nodejs_compat` flag](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) in order to run.\n"
  },
  {
    "path": "packages/multipart-parser/demos/cf-workers/package.json",
    "content": "{\n  \"name\": \"multipart-parser-cf-workers-example\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@remix-run/multipart-parser\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.20251014.0\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"wrangler\": \"^4.20.0\"\n  },\n  \"scripts\": {\n    \"deploy\": \"wrangler deploy\",\n    \"dev\": \"wrangler dev --port 44100\",\n    \"start\": \"wrangler dev --port 44100\",\n    \"cf-typegen\": \"wrangler types\",\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "packages/multipart-parser/demos/cf-workers/src/index.ts",
    "content": "import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser'\n\nexport default {\n  async fetch(request, env): Promise<Response> {\n    if (request.method === 'GET') {\n      return new Response(\n        `\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>multipart-parser CF Workers Example</title>\n  </head>\n  <body>\n    <h1>multipart-parser CF Workers Example</h1>\n    <form method=\"post\" enctype=\"multipart/form-data\">\n      <p><input name=\"text1\" type=\"text\" /></p>\n      <p><input name=\"file1\" type=\"file\" /></p>\n      <p><button type=\"submit\">Submit</button></p>\n    </form>\n  </body>\n</html>\n`,\n        {\n          headers: { 'Content-Type': 'text/html' },\n        },\n      )\n    }\n\n    if (request.method === 'POST') {\n      try {\n        let bucket = env.MULTIPART_UPLOADS\n        let parts: any[] = []\n\n        for await (let part of parseMultipartRequest(request)) {\n          if (part.isFile) {\n            let uniqueKey = `upload-${new Date().getTime()}-${Math.random()\n              .toString(36)\n              .slice(2, 8)}`\n\n            await bucket.put(uniqueKey, part.bytes, {\n              httpMetadata: {\n                contentType: part.headers.get('Content-Type')!,\n              },\n            })\n\n            parts.push({\n              name: part.name,\n              filename: part.filename,\n              mediaType: part.mediaType,\n              size: part.size,\n            })\n          } else {\n            parts.push({ name: part.name, value: part.text })\n          }\n        }\n\n        return new Response(JSON.stringify({ parts }, null, 2), {\n          headers: { 'Content-Type': 'application/json' },\n        })\n      } catch (error) {\n        if (error instanceof MultipartParseError) {\n          return new Response(`Error: ${error.message}`, { status: 400 })\n        }\n\n        console.error(error)\n\n        return new Response('Internal Server Error', { status: 500 })\n      }\n    }\n\n    return new Response('Method Not Allowed', { status: 405 })\n  },\n} satisfies ExportedHandler<Env>\n"
  },
  {
    "path": "packages/multipart-parser/demos/cf-workers/tsconfig.json",
    "content": "{\n  \"include\": [\"worker-configuration.d.ts\", \"src/**/*.ts\"],\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"target\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"allowJs\": true,\n    \"checkJs\": false,\n    \"noEmit\": true,\n    \"isolatedModules\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"@cloudflare/workers-types/2023-07-01\", \"node\"]\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/multipart-parser/demos/cf-workers/worker-configuration.d.ts",
    "content": "// Generated by Wrangler on Wed Jul 31 2024 09:58:02 GMT-0700 (Pacific Daylight Time)\n// by running `wrangler types`\n\ninterface Env {\n  MULTIPART_UPLOADS: R2Bucket\n}\n"
  },
  {
    "path": "packages/multipart-parser/demos/cf-workers/wrangler.toml",
    "content": "#:schema node_modules/wrangler/config-schema.json\nname = \"multipart-parser-cf-workers\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2024-07-25\"\n# compatibility_flags = [\"nodejs_compat\"]\n\n# Automatically place your workloads in an optimal location to minimize latency.\n# If you are running back-end logic in a Worker, running it closer to your back-end infrastructure\n# rather than the end user may result in better performance.\n# Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement\n# [placement]\n# mode = \"smart\"\n\n# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)\n# Docs:\n# - https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables\n# Note: Use secrets to store sensitive data.\n# - https://developers.cloudflare.com/workers/configuration/secrets/\n# [vars]\n# MY_VARIABLE = \"production_value\"\n\n# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai\n# [ai]\n# binding = \"AI\"\n\n# Bind an Analytics Engine dataset. Use Analytics Engine to write analytics within your Pages Function.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#analytics-engine-datasets\n# [[analytics_engine_datasets]]\n# binding = \"MY_DATASET\"\n\n# Bind a headless browser instance running on Cloudflare's global network.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering\n# [browser]\n# binding = \"MY_BROWSER\"\n\n# Bind a D1 database. D1 is Cloudflare’s native serverless SQL database.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases\n# [[d1_databases]]\n# binding = \"MY_DB\"\n# database_name = \"my-database\"\n# database_id = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n\n# Bind a dispatch namespace. Use Workers for Platforms to deploy serverless functions programmatically on behalf of your customers.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#dispatch-namespace-bindings-workers-for-platforms\n# [[dispatch_namespaces]]\n# binding = \"MY_DISPATCHER\"\n# namespace = \"my-namespace\"\n\n# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.\n# Durable Objects can live for as long as needed. Use these when you need a long-running \"server\", such as in realtime apps.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects\n# [[durable_objects.bindings]]\n# name = \"MY_DURABLE_OBJECT\"\n# class_name = \"MyDurableObject\"\n\n# Durable Object migrations.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations\n# [[migrations]]\n# tag = \"v1\"\n# new_classes = [\"MyDurableObject\"]\n\n# Bind a Hyperdrive configuration. Use to accelerate access to your existing databases from Cloudflare Workers.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive\n# [[hyperdrive]]\n# binding = \"MY_HYPERDRIVE\"\n# id = \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\n# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces\n# [[kv_namespaces]]\n# binding = \"MY_KV_NAMESPACE\"\n# id = \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\n# Bind an mTLS certificate. Use to present a client certificate when communicating with another service.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates\n# [[mtls_certificates]]\n# binding = \"MY_CERTIFICATE\"\n# certificate_id = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n\n# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues\n# [[queues.producers]]\n# binding = \"MY_QUEUE\"\n# queue = \"my-queue\"\n\n# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues\n# [[queues.consumers]]\n# queue = \"my-queue\"\n\n# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets\n[[r2_buckets]]\nbinding = \"MULTIPART_UPLOADS\"\nbucket_name = \"multipart-uploads\"\n\n# Bind another Worker service. Use this binding to call another Worker without network overhead.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings\n# [[services]]\n# binding = \"MY_SERVICE\"\n# service = \"my-service\"\n\n# Bind a Vectorize index. Use to store and query vector embeddings for semantic search, classification and other vector search use-cases.\n# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes\n# [[vectorize]]\n# binding = \"MY_INDEX\"\n# index_name = \"my-index\"\n"
  },
  {
    "path": "packages/multipart-parser/demos/deno/README.md",
    "content": "# multipart-parser Deno Example\n\nThis example demonstrates handling a `<form enctype=\"multipart/form-data\">` POST to [a Deno server](https://deno.com/) and streaming any file uploads it contains to a tmp directory on the filesystem.\n"
  },
  {
    "path": "packages/multipart-parser/demos/deno/main.ts",
    "content": "// deno-lint-ignore-file prefer-const\nimport { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser'\n// @deno-types=\"npm:@types/tmp\"\nimport tmp from 'npm:tmp'\n\nconst PORT = 44100\n\nasync function requestHandler(request: Request): Promise<Response> {\n  if (request.method === 'GET') {\n    return new Response(\n      `\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>multipart-parser Deno Example</title>\n  </head>\n  <body>\n    <h1>multipart-parser Deno Example</h1>\n    <form method=\"post\" enctype=\"multipart/form-data\">\n      <p><input name=\"text1\" type=\"text\" /></p>\n      <p><input name=\"file1\" type=\"file\" /></p>\n      <p><button type=\"submit\">Submit</button></p>\n    </form>\n  </body>\n</html>\n`,\n      {\n        headers: { 'Content-Type': 'text/html' },\n      },\n    )\n  }\n\n  if (request.method === 'POST') {\n    try {\n      // deno-lint-ignore no-explicit-any\n      let parts: any[] = []\n\n      for await (let part of parseMultipartRequest(request)) {\n        if (part.isFile) {\n          let tmpfile = tmp.fileSync()\n          await Deno.writeFile(tmpfile.name, part.bytes)\n\n          parts.push({\n            name: part.name,\n            filename: part.filename,\n            mediaType: part.mediaType,\n            size: part.size,\n            file: tmpfile.name,\n          })\n        } else {\n          parts.push({ name: part.name, value: part.text })\n        }\n      }\n\n      return new Response(JSON.stringify({ parts }, null, 2), {\n        headers: { 'Content-Type': 'application/json' },\n      })\n    } catch (error) {\n      if (error instanceof MultipartParseError) {\n        return new Response(`Error: ${error.message}`, { status: 400 })\n      }\n\n      console.error(error)\n\n      return new Response('Internal Server Error', { status: 500 })\n    }\n  }\n\n  return new Response('Method Not Allowed', { status: 405 })\n}\n\nDeno.serve(\n  {\n    port: PORT,\n    onListen() {\n      console.log(`Server listening on http://localhost:${PORT} ...`)\n    },\n  },\n  requestHandler,\n)\n"
  },
  {
    "path": "packages/multipart-parser/demos/deno/package.json",
    "content": "{\n  \"name\": \"multipart-parser-deno-example\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@remix-run/multipart-parser\": \"workspace:^\",\n    \"tmp\": \"^0.2.3\"\n  },\n  \"devDependencies\": {\n    \"@types/tmp\": \"^0.2.6\"\n  },\n  \"scripts\": {\n    \"start\": \"deno run --allow-all main.ts\"\n  }\n}\n"
  },
  {
    "path": "packages/multipart-parser/demos/node/README.md",
    "content": "# multipart-parser Node Example\n\nThis example is a [Node.js server](https://nodejs.org/) that handles file uploads and streams them to a tmp file on disk.\n"
  },
  {
    "path": "packages/multipart-parser/demos/node/package.json",
    "content": "{\n  \"name\": \"multipart-parser-node-example\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@remix-run/multipart-parser\": \"workspace:^\",\n    \"tmp\": \"^0.2.3\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@types/tmp\": \"^0.2.6\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"start\": \"node server.js\",\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "packages/multipart-parser/demos/node/server.js",
    "content": "import * as fs from 'node:fs'\nimport * as http from 'node:http'\nimport tmp from 'tmp'\n\nimport { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser/node'\n\nconst PORT = 44100\n\nconst server = http.createServer(async (req, res) => {\n  if (req.method === 'GET') {\n    res.writeHead(200, { 'Content-Type': 'text/html' })\n    res.end(`\n<!DOCTYPE html>\n<html>\n  <head>\n    <title>multipart-parser Node Example</title>\n  </head>\n  <body>\n    <h1>multipart-parser Node Example</h1>\n    <form method=\"post\" enctype=\"multipart/form-data\">\n      <p><input name=\"text1\" type=\"text\" /></p>\n      <p><input name=\"file1\" type=\"file\" /></p>\n      <p><button type=\"submit\">Submit</button></p>\n    </form>\n  </body>\n</html>\n`)\n    return\n  }\n\n  if (req.method === 'POST') {\n    try {\n      /** @type any[] */\n      let parts = []\n\n      for await (let part of parseMultipartRequest(req)) {\n        if (part.isFile) {\n          let tmpfile = tmp.fileSync()\n          fs.writeFileSync(tmpfile.name, part.bytes, 'binary')\n\n          parts.push({\n            name: part.name,\n            filename: part.filename,\n            mediaType: part.mediaType,\n            size: part.size,\n            file: tmpfile.name,\n          })\n        } else {\n          parts.push({ name: part.name, value: part.text })\n        }\n      }\n\n      res.writeHead(200, { 'Content-Type': 'application/json' })\n      res.end(JSON.stringify({ parts }, null, 2))\n      return\n    } catch (error) {\n      if (error instanceof MultipartParseError) {\n        res.writeHead(400, { 'Content-Type': 'text/plain', Connection: 'close' })\n        res.end(`Error: ${error.message}`)\n        return\n      }\n\n      console.error(error)\n\n      res.writeHead(500, { 'Content-Type': 'text/plain' })\n      res.end('Internal Server Error')\n      return\n    }\n  }\n\n  res.writeHead(405)\n  res.end('Method Not Allowed')\n})\n\nserver.listen(PORT, () => {\n  console.log(`Server listening on http://localhost:${PORT} ...`)\n})\n"
  },
  {
    "path": "packages/multipart-parser/demos/node/tsconfig.json",
    "content": "{\n  \"include\": [\"server.js\"],\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\"],\n    \"noEmit\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/multipart-parser/package.json",
    "content": "{\n  \"name\": \"@remix-run/multipart-parser\",\n  \"version\": \"0.14.2\",\n  \"description\": \"A fast, efficient parser for multipart streams in any JavaScript environment\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/multipart-parser\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/multipart-parser#readme\",\n  \"license\": \"MIT\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./node\": \"./src/node.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./node\": {\n        \"types\": \"./dist/node.d.ts\",\n        \"default\": \"./dist/node.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"dependencies\": {\n    \"@remix-run/headers\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"bench\": \"pnpm run bench:node && pnpm run bench:bun && pnpm run bench:deno\",\n    \"bench:bun\": \"bun run ./bench/runner.ts\",\n    \"bench:deno\": \"deno run --allow-sys ./bench/runner.ts\",\n    \"bench:node\": \"node ./bench/runner.ts\",\n    \"bench:node:single\": \"node ./bench/runner.ts\",\n    \"bench:node:profile\": \"node --cpu-prof --heap-prof ./bench/runner.ts\",\n    \"bench:node:steady\": \"node --expose-gc ./bench/runner.ts --steady --metrics\",\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"multipart\",\n    \"parser\",\n    \"stream\",\n    \"http\"\n  ]\n}\n"
  },
  {
    "path": "packages/multipart-parser/src/index.ts",
    "content": "export type { ParseMultipartOptions, MultipartParserOptions } from './lib/multipart.ts'\nexport {\n  MultipartParseError,\n  MaxHeaderSizeExceededError,\n  MaxFileSizeExceededError,\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n  parseMultipart,\n  parseMultipartStream,\n  MultipartParser,\n  MultipartPart,\n} from './lib/multipart.ts'\n\nexport {\n  getMultipartBoundary,\n  isMultipartRequest,\n  parseMultipartRequest,\n} from './lib/multipart-request.ts'\n"
  },
  {
    "path": "packages/multipart-parser/src/lib/buffer-search.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createSearch, createPartialTailSearch } from './buffer-search.ts'\n\nfunction buf(string: string): Uint8Array {\n  return new TextEncoder().encode(string)\n}\n\ndescribe('createSearch', () => {\n  it('finds the first occurrence of a pattern in a buffer', () => {\n    let search = createSearch('world')\n    assert.equal(search(buf('hello world')), 6)\n  })\n\n  it('returns -1 if the pattern is not found', () => {\n    let search = createSearch('world')\n    assert.equal(search(buf('hello worl')), -1)\n  })\n})\n\ndescribe('createPartialTailSearch', () => {\n  it('finds the last partial occurrence of a pattern in a buffer', () => {\n    let search = createPartialTailSearch('world')\n    assert.equal(search(buf('hello worl')), 6)\n  })\n\n  it('returns -1 if the pattern is not found at the end', () => {\n    let search = createPartialTailSearch('world')\n    assert.equal(search(buf('hello worlds')), -1)\n  })\n})\n"
  },
  {
    "path": "packages/multipart-parser/src/lib/buffer-search.ts",
    "content": "export interface SearchFunction {\n  (haystack: Uint8Array, start?: number): number\n}\n\nexport function createSearch(pattern: string): SearchFunction {\n  let needle = new TextEncoder().encode(pattern)\n\n  let search: SearchFunction\n  // Use the built-in Buffer.indexOf method on Node.js for better perf.\n  let BufferClass = (globalThis as any).Buffer as\n    | { prototype: { indexOf(this: Uint8Array, needle: Uint8Array, start: number): number } }\n    | undefined\n  if (BufferClass && !('Bun' in globalThis || 'Deno' in globalThis)) {\n    search = (haystack, start = 0) => BufferClass.prototype.indexOf.call(haystack, needle, start)\n  } else {\n    let needleEnd = needle.length - 1\n    let skipTable = new Uint8Array(256).fill(needle.length)\n    for (let i = 0; i < needleEnd; ++i) {\n      skipTable[needle[i]] = needleEnd - i\n    }\n\n    search = (haystack, start = 0) => {\n      let haystackLength = haystack.length\n      let i = start + needleEnd\n\n      while (i < haystackLength) {\n        for (let j = needleEnd, k = i; j >= 0 && haystack[k] === needle[j]; --j, --k) {\n          if (j === 0) return k\n        }\n\n        i += skipTable[haystack[i]]\n      }\n\n      return -1\n    }\n  }\n\n  return search\n}\n\nexport interface PartialTailSearchFunction {\n  (haystack: Uint8Array): number\n}\n\nexport function createPartialTailSearch(pattern: string): PartialTailSearchFunction {\n  let needle = new TextEncoder().encode(pattern)\n\n  let byteIndexes: Record<number, number[]> = {}\n  for (let i = 0; i < needle.length; ++i) {\n    let byte = needle[i]\n    if (byteIndexes[byte] === undefined) byteIndexes[byte] = []\n    byteIndexes[byte].push(i)\n  }\n\n  return function (haystack: Uint8Array): number {\n    let haystackEnd = haystack.length - 1\n\n    if (haystack[haystackEnd] in byteIndexes) {\n      let indexes = byteIndexes[haystack[haystackEnd]]\n\n      for (let i = indexes.length - 1; i >= 0; --i) {\n        for (let j = indexes[i], k = haystackEnd; j >= 0 && haystack[k] === needle[j]; --j, --k) {\n          if (j === 0) return k\n        }\n      }\n    }\n\n    return -1\n  }\n}\n"
  },
  {
    "path": "packages/multipart-parser/src/lib/multipart-request.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createMultipartMessage, getRandomBytes } from '../../test/utils.ts'\n\nimport type { MultipartParserOptions, MultipartPart } from './multipart.ts'\nimport {\n  MultipartParseError,\n  MaxHeaderSizeExceededError,\n  MaxFileSizeExceededError,\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n} from './multipart.ts'\nimport {\n  getMultipartBoundary,\n  isMultipartRequest,\n  parseMultipartRequest,\n} from './multipart-request.ts'\n\nconst CRLF = '\\r\\n'\n\ndescribe('getMultipartBoundary', async () => {\n  it('returns the boundary from the Content-Type header', async () => {\n    assert.equal(getMultipartBoundary('multipart/form-data; boundary=boundary123'), 'boundary123')\n  })\n\n  it('returns null when boundary is missing', async () => {\n    assert.equal(getMultipartBoundary('multipart/form-data'), null)\n  })\n\n  it('returns null when Content-Type header is not multipart', async () => {\n    assert.equal(getMultipartBoundary('text/plain'), null)\n  })\n})\n\ndescribe('isMultipartRequest', async () => {\n  it('returns true for multipart/form-data requests', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data',\n      },\n    })\n\n    assert.ok(isMultipartRequest(request))\n  })\n\n  it('returns true for multipart/mixed requests', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/mixed',\n      },\n    })\n\n    assert.ok(isMultipartRequest(request))\n  })\n\n  it('returns false for other content types', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'text/plain',\n      },\n    })\n\n    assert.ok(!isMultipartRequest(request))\n  })\n})\n\ndescribe('parseMultipartRequest', async () => {\n  let boundary = '----WebKitFormBoundaryz8Zv2UxQ7f4a0Z3H'\n  let boundaryBytes = new TextEncoder().encode(`\\r\\n--${boundary}`)\n\n  function indexOfBytes(haystack: Uint8Array, needle: Uint8Array, start = 0): number {\n    outer: for (let i = start; i <= haystack.length - needle.length; ++i) {\n      for (let j = 0; j < needle.length; ++j) {\n        if (haystack[i + j] !== needle[j]) {\n          continue outer\n        }\n      }\n      return i\n    }\n\n    return -1\n  }\n\n  function createChunkedRequest(body: Uint8Array, chunkSize: number): Request {\n    let stream = new ReadableStream<Uint8Array>({\n      start(controller) {\n        for (let i = 0; i < body.length; i += chunkSize) {\n          controller.enqueue(body.subarray(i, i + chunkSize))\n        }\n        controller.close()\n      },\n    })\n\n    return new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: stream,\n      duplex: 'half',\n    } as RequestInit & { duplex: 'half' })\n  }\n\n  it('parses an empty multipart message', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: `--${boundary}--`,\n    })\n\n    let parts = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 0)\n  })\n\n  it('parses a simple multipart form', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: createMultipartMessage(boundary, {\n        field1: 'value1',\n      }),\n    })\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 1)\n    assert.equal(parts[0].name, 'field1')\n    assert.equal(parts[0].text, 'value1')\n  })\n\n  it('parses multiple parts correctly', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: createMultipartMessage(boundary, {\n        field1: 'value1',\n        field2: 'value2',\n      }),\n    })\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 2)\n    assert.equal(parts[0].name, 'field1')\n    assert.equal(parts[0].text, 'value1')\n    assert.equal(parts[1].name, 'field2')\n    assert.equal(parts[1].text, 'value2')\n  })\n\n  it('parses empty parts correctly', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: createMultipartMessage(boundary, {\n        empty: '',\n      }),\n    })\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 1)\n    assert.equal(parts[0].name, 'empty')\n    assert.equal(parts[0].bytes.byteLength, 0)\n  })\n\n  it('parses file uploads correctly', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: createMultipartMessage(boundary, {\n        file1: {\n          filename: 'test.txt',\n          mediaType: 'text/plain',\n          content: 'File content',\n        },\n      }),\n    })\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 1)\n    assert.equal(parts[0].name, 'file1')\n    assert.equal(parts[0].filename, 'test.txt')\n    assert.equal(parts[0].mediaType, 'text/plain')\n    assert.equal(parts[0].text, 'File content')\n  })\n\n  it('parses multiple fields and a file upload', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: createMultipartMessage(boundary, {\n        field1: 'value1',\n        field2: 'value2',\n        file1: {\n          filename: 'test.txt',\n          mediaType: 'text/plain',\n          content: 'File content',\n        },\n      }),\n    })\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 3)\n    assert.equal(parts[0].name, 'field1')\n    assert.equal(parts[0].text, 'value1')\n    assert.equal(parts[1].name, 'field2')\n    assert.equal(parts[1].text, 'value2')\n    assert.equal(parts[2].name, 'file1')\n    assert.equal(parts[2].filename, 'test.txt')\n    assert.equal(parts[2].mediaType, 'text/plain')\n    assert.equal(parts[2].text, 'File content')\n  })\n\n  it('parses large file uploads correctly', async () => {\n    let maxFileSize = 10 * 1024 * 1024 // 10 MiB\n    let content = getRandomBytes(maxFileSize) // 10 MiB file\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: createMultipartMessage(boundary, {\n        file1: {\n          filename: 'random.dat',\n          mediaType: 'application/octet-stream',\n          content,\n        },\n      }),\n    })\n\n    let parts: { name?: string; filename?: string; mediaType?: string; content: Uint8Array }[] = []\n    for await (let part of parseMultipartRequest(request, { maxFileSize })) {\n      parts.push({\n        name: part.name,\n        filename: part.filename,\n        mediaType: part.mediaType,\n        content: part.bytes,\n      })\n    }\n\n    assert.equal(parts.length, 1)\n    assert.equal(parts[0].name, 'file1')\n    assert.equal(parts[0].filename, 'random.dat')\n    assert.equal(parts[0].mediaType, 'application/octet-stream')\n    assert.deepEqual(parts[0].content, content)\n  })\n\n  it('parses when boundary is split across chunks', async () => {\n    let body = createMultipartMessage(boundary, {\n      field1: 'value1',\n      field2: 'value2',\n    })\n    let boundaryIndex = indexOfBytes(body, boundaryBytes, 1)\n    assert.ok(boundaryIndex > 0)\n\n    let request = createChunkedRequest(body, boundaryIndex + 3)\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 2)\n    assert.equal(parts[0].name, 'field1')\n    assert.equal(parts[0].text, 'value1')\n    assert.equal(parts[1].name, 'field2')\n    assert.equal(parts[1].text, 'value2')\n  })\n\n  it('parses when a partial boundary tail is at chunk edge', async () => {\n    let body = createMultipartMessage(boundary, {\n      field1: 'value1',\n      field2: 'value2',\n    })\n    let boundaryIndex = indexOfBytes(body, boundaryBytes, 1)\n    assert.ok(boundaryIndex > 0)\n\n    // End first chunk with only '\\r' from the '\\r\\n--boundary' marker.\n    let request = createChunkedRequest(body, boundaryIndex + 1)\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 2)\n    assert.equal(parts[0].name, 'field1')\n    assert.equal(parts[0].text, 'value1')\n    assert.equal(parts[1].name, 'field2')\n    assert.equal(parts[1].text, 'value2')\n  })\n\n  it('parses when boundary starts exactly at next chunk edge', async () => {\n    let body = createMultipartMessage(boundary, {\n      field1: 'value1',\n      field2: 'value2',\n    })\n    let boundaryIndex = indexOfBytes(body, boundaryBytes, 1)\n    assert.ok(boundaryIndex > 0)\n\n    // End first chunk right before '\\r\\n--boundary'.\n    let request = createChunkedRequest(body, boundaryIndex)\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 2)\n    assert.equal(parts[0].name, 'field1')\n    assert.equal(parts[0].text, 'value1')\n    assert.equal(parts[1].name, 'field2')\n    assert.equal(parts[1].text, 'value2')\n  })\n\n  it('throws when Content-Type is not multipart/form-data', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'text/plain',\n      },\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request)) {\n        // ...\n      }\n    }, MultipartParseError)\n  })\n\n  it('throws when initial boundary is missing', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'multipart/form-data',\n      },\n      body: 'Content-Disposition: form-data; name=\"field1\"\\r\\n\\r\\nvalue1',\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request)) {\n        // ...\n      }\n    }, MultipartParseError)\n  })\n\n  it('throws when header exceeds maximum size', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: [\n        `--${boundary}`,\n        'Content-Disposition: form-data; name=\"field1\"',\n        'X-Large-Header: ' + 'X'.repeat(6 * 1024), // 6 KB header\n        '',\n        'value1',\n        `--${boundary}--`,\n      ].join(CRLF),\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request, { maxHeaderSize: 4 * 1024 })) {\n        // ...\n      }\n    }, MaxHeaderSizeExceededError)\n  })\n\n  it('throws when a file exceeds maximum size', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: createMultipartMessage(boundary, {\n        file1: {\n          filename: 'random.dat',\n          mediaType: 'application/octet-stream',\n          content: getRandomBytes(11 * 1024 * 1024), // 11 MB file\n        },\n      }),\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request, { maxFileSize: 10 * 1024 * 1024 })) {\n        // ...\n      }\n    }, MaxFileSizeExceededError)\n  })\n\n  it('throws when the number of parts exceeds maxParts', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: createMultipartMessage(boundary, {\n        field1: 'value1',\n        field2: 'value2',\n        field3: 'value3',\n      }),\n    })\n\n    let options: MultipartParserOptions = { maxParts: 2 }\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request, options)) {\n        // ...\n      }\n    }, MaxPartsExceededError)\n  })\n\n  it('throws when the aggregate content size exceeds maxTotalSize', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: createMultipartMessage(boundary, {\n        field1: 'hello',\n        field2: 'world',\n      }),\n    })\n\n    let options: MultipartParserOptions = { maxTotalSize: 9 }\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request, options)) {\n        // ...\n      }\n    }, MaxTotalSizeExceededError)\n  })\n\n  it('parses malformed parts', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: [`--${boundary}`, 'Invalid-Header', '', 'Some content', `--${boundary}--`].join(CRLF),\n    })\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 1)\n    assert.equal(parts[0].headers.get('Invalid-Header'), null)\n    assert.equal(parts[0].text, 'Some content')\n  })\n\n  it('throws error when final boundary is missing', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: [\n        `--${boundary}`,\n        'Content-Disposition: form-data; name=\"field1\"',\n        '',\n        'value1',\n        `--${boundary}`,\n      ].join(CRLF),\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request)) {\n        // ...\n      }\n    }, MultipartParseError)\n  })\n\n  it('throws error when request body is empty', async () => {\n    let request = new Request('https://example.com', {\n      method: 'POST',\n      headers: {\n        'Content-Type': `multipart/form-data; boundary=${boundary}`,\n      },\n      body: null,\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request)) {\n        // ...\n      }\n    }, MultipartParseError)\n  })\n})\n"
  },
  {
    "path": "packages/multipart-parser/src/lib/multipart-request.ts",
    "content": "import type { MultipartParserOptions, MultipartPart } from './multipart.ts'\nimport { MultipartParseError, parseMultipartStream } from './multipart.ts'\n\n/**\n * Extracts the boundary string from a `multipart/*` content type.\n *\n * @param contentType The `Content-Type` header value from the request\n * @returns The boundary string if found, or null if not present\n */\nexport function getMultipartBoundary(contentType: string): string | null {\n  let match = /boundary=(?:\"([^\"]+)\"|([^;]+))/i.exec(contentType)\n  return match ? (match[1] ?? match[2]) : null\n}\n\n/**\n * Returns true if the given request contains multipart data.\n *\n * @param request The `Request` object to check\n * @returns `true` if the request is a multipart request, `false` otherwise\n */\nexport function isMultipartRequest(request: Request): boolean {\n  let contentType = request.headers.get('Content-Type')\n  return contentType != null && contentType.startsWith('multipart/')\n}\n\n/**\n * Parse a multipart [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)\n * and yield each part as a {@link MultipartPart} object. Useful in HTTP server contexts\n * for handling incoming `multipart/*` requests.\n *\n * @param request The `Request` object containing multipart data\n * @param options Optional parser options, such as `maxHeaderSize`, `maxFileSize`, `maxParts`,\n * and `maxTotalSize`\n * @returns An async generator yielding {@link MultipartPart} objects\n */\nexport async function* parseMultipartRequest(\n  request: Request,\n  options?: MultipartParserOptions,\n): AsyncGenerator<MultipartPart, void, unknown> {\n  if (!isMultipartRequest(request)) {\n    throw new MultipartParseError('Request is not a multipart request')\n  }\n  if (!request.body) {\n    throw new MultipartParseError('Request body is empty')\n  }\n\n  let boundary = getMultipartBoundary(request.headers.get('Content-Type')!)\n  if (!boundary) {\n    throw new MultipartParseError('Invalid Content-Type header: missing boundary')\n  }\n\n  yield* parseMultipartStream(request.body, {\n    boundary,\n    maxHeaderSize: options?.maxHeaderSize,\n    maxFileSize: options?.maxFileSize,\n    maxParts: options?.maxParts,\n    maxTotalSize: options?.maxTotalSize,\n  })\n}\n"
  },
  {
    "path": "packages/multipart-parser/src/lib/multipart.node.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { getRandomBytes } from '../../test/utils.ts'\nimport { createMultipartRequest } from '../../test/utils.node.ts'\n\nimport type { MultipartPart } from './multipart.ts'\nimport { MaxPartsExceededError, MaxTotalSizeExceededError } from './multipart.ts'\nimport { parseMultipartRequest } from './multipart.node.ts'\n\ndescribe('parseMultipartRequest (node)', () => {\n  let boundary = '----WebKitFormBoundaryzv5f5B2cY6tjQ0Rn'\n\n  it('parses an empty multipart message', async () => {\n    let request = createMultipartRequest(boundary)\n\n    let parts = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 0)\n  })\n\n  it('parses a simple multipart form', async () => {\n    let request = createMultipartRequest(boundary, {\n      field1: 'value1',\n    })\n\n    let parts: MultipartPart[] = []\n    for await (let part of parseMultipartRequest(request)) {\n      parts.push(part)\n    }\n\n    assert.equal(parts.length, 1)\n    assert.equal(parts[0].name, 'field1')\n    assert.equal(parts[0].text, 'value1')\n  })\n\n  it('parses large file uploads correctly', async () => {\n    let maxFileSize = 1024 * 1024 * 10 // 10 MiB\n    let content = getRandomBytes(maxFileSize)\n    let request = createMultipartRequest(boundary, {\n      file1: {\n        filename: 'tesla.jpg',\n        mediaType: 'image/jpeg',\n        content,\n      },\n    })\n\n    let parts: { name?: string; filename?: string; mediaType?: string; content: Uint8Array }[] = []\n    for await (let part of parseMultipartRequest(request, { maxFileSize })) {\n      parts.push({\n        name: part.name,\n        filename: part.filename,\n        mediaType: part.mediaType,\n        content: part.bytes,\n      })\n    }\n\n    assert.equal(parts.length, 1)\n    assert.equal(parts[0].name, 'file1')\n    assert.equal(parts[0].filename, 'tesla.jpg')\n    assert.equal(parts[0].mediaType, 'image/jpeg')\n    assert.deepEqual(parts[0].content, content)\n  })\n\n  it('throws when the number of parts exceeds maxParts', async () => {\n    let request = createMultipartRequest(boundary, {\n      field1: 'value1',\n      field2: 'value2',\n      field3: 'value3',\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request, { maxParts: 2 })) {\n        // ...\n      }\n    }, MaxPartsExceededError)\n  })\n\n  it('throws when the aggregate content size exceeds maxTotalSize', async () => {\n    let request = createMultipartRequest(boundary, {\n      field1: 'hello',\n      field2: 'world',\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartRequest(request, { maxTotalSize: 9 })) {\n        // ...\n      }\n    }, MaxTotalSizeExceededError)\n  })\n})\n"
  },
  {
    "path": "packages/multipart-parser/src/lib/multipart.node.ts",
    "content": "import type * as http from 'node:http'\nimport { Readable } from 'node:stream'\n\nimport type { ParseMultipartOptions, MultipartParserOptions, MultipartPart } from './multipart.ts'\nimport {\n  MultipartParseError,\n  parseMultipart as parseMultipartWeb,\n  parseMultipartStream as parseMultipartStreamWeb,\n} from './multipart.ts'\nimport { getMultipartBoundary } from './multipart-request.ts'\n\n/**\n * Parse a `multipart/*` Node.js `Buffer` and yield each part as a {@link MultipartPart} object.\n *\n * Note: This is a low-level API that requires manual handling of the content and boundary.\n * If you're building a web server, consider using {@link parseMultipartRequest} instead.\n *\n * @param message The multipart message as a `Buffer` or an iterable of `Buffer` chunks\n * @param options Options for the parser\n * @returns A generator yielding {@link MultipartPart} objects\n */\nexport function* parseMultipart(\n  message: Buffer | Iterable<Buffer>,\n  options: ParseMultipartOptions,\n): Generator<MultipartPart, void, unknown> {\n  yield* parseMultipartWeb(message as Uint8Array | Iterable<Uint8Array>, options)\n}\n\n/**\n * Parse a `multipart/*` Node.js `Readable` stream and yield each part as a\n * {@link MultipartPart} object.\n *\n * Note: This is a low-level API that requires manual handling of the stream and boundary.\n * If you're building a web server, consider using {@link parseMultipartRequest} instead.\n *\n * @param stream A Node.js `Readable` stream containing multipart data\n * @param options Options for the parser\n * @returns An async generator yielding {@link MultipartPart} objects\n */\nexport async function* parseMultipartStream(\n  stream: Readable,\n  options: ParseMultipartOptions,\n): AsyncGenerator<MultipartPart, void, unknown> {\n  yield* parseMultipartStreamWeb(Readable.toWeb(stream) as ReadableStream, options)\n}\n\n/**\n * Returns true if the given request is a multipart request.\n *\n * @param req The Node.js `http.IncomingMessage` object to check\n * @returns `true` if the request is a multipart request, `false` otherwise\n */\nexport function isMultipartRequest(req: http.IncomingMessage): boolean {\n  let contentType = req.headers['content-type']\n  return contentType != null && /^multipart\\//i.test(contentType)\n}\n\n/**\n * Parse a multipart Node.js request and yield each part as a {@link MultipartPart} object.\n *\n * @param req The Node.js `http.IncomingMessage` object containing multipart data\n * @param options Options for the parser, such as `maxHeaderSize`, `maxFileSize`, `maxParts`,\n * and `maxTotalSize`\n * @returns An async generator yielding {@link MultipartPart} objects\n */\nexport async function* parseMultipartRequest(\n  req: http.IncomingMessage,\n  options?: MultipartParserOptions,\n): AsyncGenerator<MultipartPart, void, unknown> {\n  if (!isMultipartRequest(req)) {\n    throw new MultipartParseError('Request is not a multipart request')\n  }\n\n  let boundary = getMultipartBoundary(req.headers['content-type']!)\n  if (!boundary) {\n    throw new MultipartParseError('Invalid Content-Type header: missing boundary')\n  }\n\n  yield* parseMultipartStream(req, {\n    boundary,\n    maxHeaderSize: options?.maxHeaderSize,\n    maxFileSize: options?.maxFileSize,\n    maxParts: options?.maxParts,\n    maxTotalSize: options?.maxTotalSize,\n  })\n}\n"
  },
  {
    "path": "packages/multipart-parser/src/lib/multipart.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createMultipartMessage } from '../../test/utils.ts'\n\nimport {\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n  parseMultipart,\n  parseMultipartStream,\n} from './multipart.ts'\n\nlet boundary = '----WebKitFormBoundaryPMcT9NSv6M3P8D4Q'\n\nfunction createChunkedIterable(body: Uint8Array, chunkSize: number): Uint8Array[] {\n  let chunks: Uint8Array[] = []\n\n  for (let i = 0; i < body.length; i += chunkSize) {\n    chunks.push(body.subarray(i, i + chunkSize))\n  }\n\n  return chunks\n}\n\nfunction createChunkedStream(body: Uint8Array, chunkSize: number): ReadableStream<Uint8Array> {\n  let chunks = createChunkedIterable(body, chunkSize)\n\n  return new ReadableStream<Uint8Array>({\n    start(controller) {\n      for (let chunk of chunks) {\n        controller.enqueue(chunk)\n      }\n\n      controller.close()\n    },\n  })\n}\n\ndescribe('parseMultipart', async () => {\n  it('throws when the number of parts exceeds maxParts', () => {\n    let message = createMultipartMessage(boundary, {\n      field1: 'value1',\n      field2: 'value2',\n      field3: 'value3',\n    })\n\n    assert.throws(() => {\n      Array.from(parseMultipart(message, { boundary, maxParts: 2 }))\n    }, MaxPartsExceededError)\n  })\n\n  it('throws when aggregate content size exceeds maxTotalSize for iterable input', () => {\n    let message = createMultipartMessage(boundary, {\n      field1: 'hello',\n      field2: 'world',\n    })\n\n    assert.throws(() => {\n      Array.from(parseMultipart(createChunkedIterable(message, 7), { boundary, maxTotalSize: 9 }))\n    }, MaxTotalSizeExceededError)\n  })\n})\n\ndescribe('parseMultipartStream', async () => {\n  it('throws when the number of parts exceeds maxParts', async () => {\n    let message = createMultipartMessage(boundary, {\n      field1: 'value1',\n      field2: 'value2',\n      field3: 'value3',\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartStream(createChunkedStream(message, 11), {\n        boundary,\n        maxParts: 2,\n      })) {\n        // ...\n      }\n    }, MaxPartsExceededError)\n  })\n\n  it('throws when aggregate content size exceeds maxTotalSize', async () => {\n    let message = createMultipartMessage(boundary, {\n      field1: 'hello',\n      field2: 'world',\n    })\n\n    await assert.rejects(async () => {\n      for await (let _ of parseMultipartStream(createChunkedStream(message, 7), {\n        boundary,\n        maxTotalSize: 9,\n      })) {\n        // ...\n      }\n    }, MaxTotalSizeExceededError)\n  })\n})\n"
  },
  {
    "path": "packages/multipart-parser/src/lib/multipart.ts",
    "content": "import { ContentDisposition, ContentType, parse as parseRawHeaders } from '@remix-run/headers'\n\nimport {\n  createSearch,\n  createPartialTailSearch,\n  type SearchFunction,\n  type PartialTailSearchFunction,\n} from './buffer-search.ts'\nimport { readStream } from './read-stream.ts'\n\n/**\n * The base class for errors thrown by the multipart parser.\n */\nexport class MultipartParseError extends Error {\n  /**\n   * @param message The error message\n   */\n  constructor(message: string) {\n    super(message)\n    this.name = 'MultipartParseError'\n  }\n}\n\n/**\n * An error thrown when the maximum allowed size of a header is exceeded.\n */\nexport class MaxHeaderSizeExceededError extends MultipartParseError {\n  /**\n   * @param maxHeaderSize The maximum header size that was exceeded\n   */\n  constructor(maxHeaderSize: number) {\n    super(`Multipart header size exceeds maximum allowed size of ${maxHeaderSize} bytes`)\n    this.name = 'MaxHeaderSizeExceededError'\n  }\n}\n\n/**\n * An error thrown when the maximum allowed size of a file is exceeded.\n */\nexport class MaxFileSizeExceededError extends MultipartParseError {\n  /**\n   * @param maxFileSize The maximum file size that was exceeded\n   */\n  constructor(maxFileSize: number) {\n    super(`File size exceeds maximum allowed size of ${maxFileSize} bytes`)\n    this.name = 'MaxFileSizeExceededError'\n  }\n}\n\n/**\n * An error thrown when the maximum allowed number of multipart parts is exceeded.\n */\nexport class MaxPartsExceededError extends MultipartParseError {\n  /**\n   * @param maxParts The maximum number of parts that was exceeded\n   */\n  constructor(maxParts: number) {\n    super(`Multipart part count exceeds maximum allowed count of ${maxParts}`)\n    this.name = 'MaxPartsExceededError'\n  }\n}\n\n/**\n * An error thrown when the maximum allowed aggregate multipart content size is exceeded.\n */\nexport class MaxTotalSizeExceededError extends MultipartParseError {\n  /**\n   * @param maxTotalSize The maximum total size that was exceeded\n   */\n  constructor(maxTotalSize: number) {\n    super(`Multipart content size exceeds maximum allowed size of ${maxTotalSize} bytes`)\n    this.name = 'MaxTotalSizeExceededError'\n  }\n}\n\n/**\n * Options for parsing a multipart message.\n */\nexport interface ParseMultipartOptions {\n  /**\n   * The boundary string used to separate parts in the multipart message,\n   * e.g. the `boundary` parameter in the `Content-Type` header.\n   */\n  boundary: string\n  /**\n   * The maximum allowed size of a header in bytes. If an individual part's header\n   * exceeds this size, a `MaxHeaderSizeExceededError` will be thrown.\n   *\n   * @default 8192 (8 KiB)\n   */\n  maxHeaderSize?: number\n  /**\n   * The maximum allowed size of a file in bytes. If an individual part's content\n   * exceeds this size, a `MaxFileSizeExceededError` will be thrown.\n   *\n   * @default 2097152 (2 MiB)\n   */\n  maxFileSize?: number\n  /**\n   * The maximum allowed number of parts in the multipart message. If this limit\n   * is exceeded, a `MaxPartsExceededError` will be thrown.\n   *\n   * @default 1000\n   */\n  maxParts?: number\n  /**\n   * The maximum allowed aggregate size of all part content in bytes. If this\n   * limit is exceeded, a `MaxTotalSizeExceededError` will be thrown.\n   *\n   * @default `maxFileSize * 20 + 1048576` (1 MiB)\n   */\n  maxTotalSize?: number\n}\n\n/**\n * Parse a `multipart/*` message from a buffer/iterable and yield each part as a\n * {@link MultipartPart} object.\n *\n * Note: This is a low-level API that requires manual handling of the content and boundary.\n * If you're building a web server, consider using\n * {@link import('./multipart-request.ts').parseMultipartRequest} instead.\n *\n * @param message The multipart message as a `Uint8Array` or an iterable of `Uint8Array` chunks\n * @param options Options for the parser\n * @returns A generator that yields {@link MultipartPart} objects\n */\nexport function* parseMultipart(\n  message: Uint8Array | Iterable<Uint8Array>,\n  options: ParseMultipartOptions,\n): Generator<MultipartPart, void, unknown> {\n  let parser = new MultipartParser(options.boundary, {\n    maxHeaderSize: options.maxHeaderSize,\n    maxFileSize: options.maxFileSize,\n    maxParts: options.maxParts,\n    maxTotalSize: options.maxTotalSize,\n  })\n\n  if (message instanceof Uint8Array) {\n    if (message.length === 0) {\n      return // No data to parse\n    }\n\n    yield* parser.write(message)\n  } else {\n    for (let chunk of message) {\n      yield* parser.write(chunk)\n    }\n  }\n\n  parser.finish()\n}\n\n/**\n * Parse a `multipart/*` message stream and yield each part as a {@link MultipartPart} object.\n *\n * Note: This is a low-level API that requires manual handling of the content and boundary.\n * If you're building a web server, consider using\n * {@link import('./multipart-request.ts').parseMultipartRequest} instead.\n *\n * @param stream A stream containing multipart data as a `ReadableStream<Uint8Array>`\n * @param options Options for the parser\n * @returns An async generator that yields {@link MultipartPart} objects\n */\nexport async function* parseMultipartStream(\n  stream: ReadableStream<Uint8Array>,\n  options: ParseMultipartOptions,\n): AsyncGenerator<MultipartPart, void, unknown> {\n  let parser = new MultipartParser(options.boundary, {\n    maxHeaderSize: options.maxHeaderSize,\n    maxFileSize: options.maxFileSize,\n    maxParts: options.maxParts,\n    maxTotalSize: options.maxTotalSize,\n  })\n\n  for await (let chunk of readStream(stream)) {\n    if (chunk.length === 0) {\n      continue // No data to parse\n    }\n\n    yield* parser.write(chunk)\n  }\n\n  parser.finish()\n}\n\n/**\n * Options for configuring a {@link MultipartParser}.\n */\nexport type MultipartParserOptions = Omit<ParseMultipartOptions, 'boundary'>\n\nconst MultipartParserStateStart = 0\nconst MultipartParserStateAfterBoundary = 1\nconst MultipartParserStateHeader = 2\nconst MultipartParserStateBody = 3\nconst MultipartParserStateDone = 4\n\nconst findDoubleNewline = createSearch('\\r\\n\\r\\n')\n\nconst oneKb = 1024\nconst oneMb = 1024 * oneKb\nconst defaultMaxParts = 1000\nconst defaultMaxTotalSizePartAllowance = 20\n\n/**\n * A streaming parser for `multipart/*` HTTP messages.\n */\nexport class MultipartParser {\n  /**\n   * Boundary string used to detect part separators.\n   */\n  readonly boundary: string\n\n  /**\n   * Maximum header size allowed for each multipart part.\n   */\n  readonly maxHeaderSize: number\n\n  /**\n   * Maximum file size allowed for each multipart part.\n   */\n  readonly maxFileSize: number\n\n  /**\n   * Maximum number of parts allowed in a multipart message.\n   */\n  readonly maxParts: number\n\n  /**\n   * Maximum aggregate content size allowed across all parts.\n   */\n  readonly maxTotalSize: number\n\n  #findOpeningBoundary: SearchFunction\n  #openingBoundaryLength: number\n  #findBoundary: SearchFunction\n  #findPartialTailBoundary: PartialTailSearchFunction\n  #boundaryLength: number\n  #boundaryBytes: Uint8Array\n\n  #state = MultipartParserStateStart\n  #buffer: Uint8Array | null = null\n  #currentHeader: Uint8Array | null = null\n  #currentContent: Uint8Array[] | null = null\n  #contentLength = 0\n  #partCount = 0\n  #totalContentLength = 0\n\n  /**\n   * @param boundary The boundary string used to separate parts\n   * @param options Options for the parser\n   */\n  constructor(boundary: string, options?: MultipartParserOptions) {\n    this.boundary = boundary\n    this.maxHeaderSize = options?.maxHeaderSize ?? 8 * oneKb\n    this.maxFileSize = options?.maxFileSize ?? 2 * oneMb\n    this.maxParts = options?.maxParts ?? defaultMaxParts\n    this.maxTotalSize =\n      options?.maxTotalSize ?? this.maxFileSize * defaultMaxTotalSizePartAllowance + oneMb\n\n    this.#findOpeningBoundary = createSearch(`--${boundary}`)\n    this.#openingBoundaryLength = 2 + boundary.length // length of '--' + boundary\n    let boundaryPattern = `\\r\\n--${boundary}`\n    this.#findBoundary = createSearch(boundaryPattern)\n    this.#findPartialTailBoundary = createPartialTailSearch(boundaryPattern)\n    this.#boundaryLength = 4 + boundary.length // length of '\\r\\n--' + boundary\n    this.#boundaryBytes = new TextEncoder().encode(boundaryPattern)\n  }\n\n  /**\n   * Write a chunk of data to the parser.\n   *\n   * @param chunk A chunk of data to write to the parser\n   * @returns A generator yielding `MultipartPart` objects as they are parsed\n   */\n  *write(chunk: Uint8Array): Generator<MultipartPart, void, unknown> {\n    if (this.#state === MultipartParserStateDone) {\n      throw new MultipartParseError('Unexpected data after end of stream')\n    }\n\n    let index = 0\n    let chunkLength = chunk.length\n\n    if (this.#buffer !== null) {\n      if (this.#state === MultipartParserStateBody) {\n        let carry = this.#buffer\n        let carryResult = this.#analyzeCarryBoundary(carry, chunk)\n\n        if (carryResult.kind === 'none') {\n          this.#append(carry)\n        } else if (carryResult.kind === 'partial') {\n          if (carryResult.start > 0) {\n            this.#append(carry.subarray(0, carryResult.start))\n          }\n\n          let tailLength = carry.length + chunk.length - carryResult.start\n          let tail = new Uint8Array(tailLength)\n          let carryTail = carry.subarray(carryResult.start)\n          tail.set(carryTail, 0)\n          tail.set(chunk, carryTail.length)\n          this.#buffer = tail\n          return\n        } else {\n          if (carryResult.start > 0) {\n            this.#append(carry.subarray(0, carryResult.start))\n          }\n\n          yield this.#createPart()\n\n          this.#state = MultipartParserStateAfterBoundary\n\n          let carryAfterStart = carry.length - carryResult.start\n          index = this.#boundaryLength - carryAfterStart\n        }\n      } else {\n        let newChunk = new Uint8Array(this.#buffer.length + chunkLength)\n        newChunk.set(this.#buffer, 0)\n        newChunk.set(chunk, this.#buffer.length)\n        chunk = newChunk\n        chunkLength = chunk.length\n      }\n\n      this.#buffer = null\n    }\n\n    while (true) {\n      if (this.#state === MultipartParserStateBody) {\n        if (chunkLength - index < this.#boundaryLength) {\n          this.#buffer = chunk.subarray(index)\n          break\n        }\n\n        let boundaryIndex = this.#findBoundary(chunk, index)\n        if (boundaryIndex === -1) {\n          // No boundary found, but there may be a partial match at the end of the chunk.\n          let partialTailIndex = this.#findPartialTailBoundary(chunk)\n\n          if (partialTailIndex === -1) {\n            this.#append(index === 0 ? chunk : chunk.subarray(index))\n          } else {\n            if (partialTailIndex > index) {\n              this.#append(chunk.subarray(index, partialTailIndex))\n            }\n            this.#buffer = chunk.subarray(partialTailIndex)\n          }\n\n          break\n        }\n\n        if (boundaryIndex > index) {\n          this.#append(chunk.subarray(index, boundaryIndex))\n        }\n\n        yield this.#createPart()\n\n        index = boundaryIndex + this.#boundaryLength\n\n        this.#state = MultipartParserStateAfterBoundary\n      }\n\n      if (this.#state === MultipartParserStateAfterBoundary) {\n        if (chunkLength - index < 2) {\n          this.#buffer = chunk.subarray(index)\n          break\n        }\n\n        if (chunk[index] === 45 && chunk[index + 1] === 45) {\n          this.#state = MultipartParserStateDone\n          break\n        }\n\n        index += 2 // Skip \\r\\n after boundary\n\n        this.#state = MultipartParserStateHeader\n      }\n\n      if (this.#state === MultipartParserStateHeader) {\n        if (chunkLength - index < 4) {\n          this.#buffer = chunk.subarray(index)\n          break\n        }\n\n        let headerEndIndex = findDoubleNewline(chunk, index)\n\n        if (headerEndIndex === -1) {\n          if (chunkLength - index > this.maxHeaderSize) {\n            throw new MaxHeaderSizeExceededError(this.maxHeaderSize)\n          }\n\n          this.#buffer = chunk.subarray(index)\n          break\n        }\n\n        if (headerEndIndex - index > this.maxHeaderSize) {\n          throw new MaxHeaderSizeExceededError(this.maxHeaderSize)\n        }\n\n        this.#currentHeader = chunk.subarray(index, headerEndIndex)\n        this.#currentContent = []\n        this.#contentLength = 0\n\n        index = headerEndIndex + 4 // Skip header + \\r\\n\\r\\n\n\n        this.#state = MultipartParserStateBody\n\n        continue\n      }\n\n      if (this.#state === MultipartParserStateStart) {\n        if (chunkLength < this.#openingBoundaryLength) {\n          this.#buffer = chunk\n          break\n        }\n\n        if (this.#findOpeningBoundary(chunk) !== 0) {\n          throw new MultipartParseError('Invalid multipart stream: missing initial boundary')\n        }\n\n        index = this.#openingBoundaryLength\n\n        this.#state = MultipartParserStateAfterBoundary\n      }\n    }\n  }\n\n  #append(chunk: Uint8Array): void {\n    if (chunk.length === 0) {\n      return\n    }\n\n    if (this.#contentLength + chunk.length > this.maxFileSize) {\n      throw new MaxFileSizeExceededError(this.maxFileSize)\n    }\n\n    if (this.#totalContentLength + chunk.length > this.maxTotalSize) {\n      throw new MaxTotalSizeExceededError(this.maxTotalSize)\n    }\n\n    this.#currentContent!.push(chunk)\n    this.#contentLength += chunk.length\n    this.#totalContentLength += chunk.length\n  }\n\n  #createPart(): MultipartPart {\n    if (++this.#partCount > this.maxParts) {\n      throw new MaxPartsExceededError(this.maxParts)\n    }\n\n    return new MultipartPart(this.#currentHeader!, this.#currentContent!)\n  }\n\n  #analyzeCarryBoundary(\n    carry: Uint8Array,\n    chunk: Uint8Array,\n  ): { kind: 'none' } | { kind: 'partial'; start: number } | { kind: 'full'; start: number } {\n    let totalLength = carry.length + chunk.length\n\n    for (let start = 0; start < carry.length; ++start) {\n      let availableLength = totalLength - start\n      let compareLength = Math.min(this.#boundaryLength, availableLength)\n\n      let matched = true\n      for (let i = 0; i < compareLength; ++i) {\n        let sourceIndex = start + i\n        let sourceByte =\n          sourceIndex < carry.length ? carry[sourceIndex] : chunk[sourceIndex - carry.length]\n        if (sourceByte !== this.#boundaryBytes[i]) {\n          matched = false\n          break\n        }\n      }\n\n      if (!matched) {\n        continue\n      }\n\n      if (availableLength >= this.#boundaryLength) {\n        return { kind: 'full', start }\n      }\n\n      return { kind: 'partial', start }\n    }\n\n    return { kind: 'none' }\n  }\n\n  /**\n   * Should be called after all data has been written to the parser.\n   *\n   * Note: This will throw if the multipart message is incomplete or\n   * wasn't properly terminated.\n   */\n  finish(): void {\n    if (this.#state !== MultipartParserStateDone) {\n      throw new MultipartParseError('Multipart stream not finished')\n    }\n  }\n}\n\nconst decoder = new TextDecoder('utf-8', { fatal: true })\n\n/**\n * A part of a `multipart/*` HTTP message.\n */\nexport class MultipartPart {\n  /**\n   * The raw content of this part as an array of `Uint8Array` chunks.\n   */\n  readonly content: Uint8Array[]\n\n  #header: Uint8Array\n  #headers?: Headers\n\n  /**\n   * @param header The raw header bytes\n   * @param content The content chunks\n   */\n  constructor(header: Uint8Array, content: Uint8Array[]) {\n    this.#header = header\n    this.content = content\n  }\n\n  /**\n   * The content of this part as an `ArrayBuffer`.\n   */\n  get arrayBuffer(): ArrayBuffer {\n    return this.bytes.buffer as ArrayBuffer\n  }\n\n  /**\n   * The content of this part as a single `Uint8Array`. In `multipart/form-data` messages, this is useful\n   * for reading the value of files that were uploaded using `<input type=\"file\">` fields.\n   */\n  get bytes(): Uint8Array {\n    let buffer = new Uint8Array(this.size)\n\n    let offset = 0\n    for (let chunk of this.content) {\n      buffer.set(chunk, offset)\n      offset += chunk.length\n    }\n\n    return buffer\n  }\n\n  /**\n   * The headers associated with this part.\n   */\n  get headers(): Headers {\n    if (!this.#headers) {\n      this.#headers = parseRawHeaders(decoder.decode(this.#header))\n    }\n\n    return this.#headers\n  }\n\n  /**\n   * True if this part originated from a file upload.\n   */\n  get isFile(): boolean {\n    return this.filename !== undefined || this.mediaType === 'application/octet-stream'\n  }\n\n  /**\n   * True if this part originated from a text input field in a form submission.\n   */\n  get isText(): boolean {\n    return !this.isFile\n  }\n\n  /**\n   * The filename of the part, if it is a file upload.\n   */\n  get filename(): string | undefined {\n    return ContentDisposition.from(this.headers.get('content-disposition')).preferredFilename\n  }\n\n  /**\n   * The media type of the part.\n   */\n  get mediaType(): string | undefined {\n    return ContentType.from(this.headers.get('content-type')).mediaType\n  }\n\n  /**\n   * The name of the part, usually the `name` of the field in the `<form>` that submitted the request.\n   */\n  get name(): string | undefined {\n    return ContentDisposition.from(this.headers.get('content-disposition')).name\n  }\n\n  /**\n   * The size of the content in bytes.\n   */\n  get size(): number {\n    let size = 0\n\n    for (let chunk of this.content) {\n      size += chunk.length\n    }\n\n    return size\n  }\n\n  /**\n   * The content of this part as a string. In `multipart/form-data` messages, this is useful for\n   * reading the value of parts that originated from `<input type=\"text\">` fields.\n   *\n   * Note: Do not use this for binary data, use `part.bytes` or `part.arrayBuffer` instead.\n   */\n  get text(): string {\n    return decoder.decode(this.bytes)\n  }\n}\n"
  },
  {
    "path": "packages/multipart-parser/src/lib/read-stream.ts",
    "content": "// We need this little helper for environments that do not support\n// ReadableStream.prototype[Symbol.asyncIterator] yet. See #46\nexport async function* readStream(stream: ReadableStream<Uint8Array>): AsyncIterable<Uint8Array> {\n  let reader = stream.getReader()\n\n  try {\n    while (true) {\n      let result = await reader.read()\n      if (result.done) break\n      yield result.value\n    }\n  } finally {\n    reader.releaseLock()\n  }\n}\n"
  },
  {
    "path": "packages/multipart-parser/src/node.ts",
    "content": "// Re-export all core functionality\nexport type { ParseMultipartOptions, MultipartParserOptions } from './lib/multipart.ts'\nexport {\n  MultipartParseError,\n  MaxHeaderSizeExceededError,\n  MaxFileSizeExceededError,\n  MaxPartsExceededError,\n  MaxTotalSizeExceededError,\n  MultipartParser,\n  MultipartPart,\n} from './lib/multipart.ts'\n\nexport { getMultipartBoundary } from './lib/multipart-request.ts'\n\n// Export Node.js-specific functionality\nexport {\n  isMultipartRequest,\n  parseMultipartRequest,\n  parseMultipart,\n  parseMultipartStream,\n} from './lib/multipart.node.ts'\n"
  },
  {
    "path": "packages/multipart-parser/test/utils.node.ts",
    "content": "import type * as http from 'node:http'\nimport { Readable } from 'node:stream'\n\nimport { type PartValue, createMultipartMessage } from './utils.ts'\n\nexport function createMultipartRequest(\n  boundary: string,\n  parts?: { [name: string]: PartValue },\n): http.IncomingMessage {\n  let body = createMultipartMessage(boundary, parts)\n\n  let request = createReadable(body) as http.IncomingMessage\n  request.headers = {\n    'content-type': `multipart/form-data; boundary=${boundary}`,\n  }\n\n  return request\n}\n\nexport function createReadable(content: Uint8Array, chunkSize = 16 * 1024): Readable {\n  let i = 0\n\n  return new Readable({\n    read() {\n      if (i < content.length) {\n        this.push(content.subarray(i, i + chunkSize))\n        i += chunkSize\n      } else {\n        this.push(null)\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/multipart-parser/test/utils.ts",
    "content": "import {\n  ContentDisposition,\n  ContentType,\n  stringify as stringifyRawHeaders,\n} from '@remix-run/headers'\n\nexport type PartValue =\n  | string\n  | {\n      filename?: string\n      filenameSplat?: string\n      mediaType?: string\n      content: string | Uint8Array\n    }\n\nexport function createMultipartMessage(\n  boundary: string,\n  parts?: { [name: string]: PartValue },\n): Uint8Array<ArrayBuffer> {\n  let chunks: Uint8Array<ArrayBuffer>[] = []\n\n  function pushString(string: string) {\n    chunks.push(new TextEncoder().encode(string))\n  }\n\n  function pushLine(line = '') {\n    pushString(line + '\\r\\n')\n  }\n\n  if (parts) {\n    for (let [name, value] of Object.entries(parts)) {\n      pushLine(`--${boundary}`)\n\n      if (typeof value === 'string') {\n        let headers = new Headers({\n          'Content-Disposition': ContentDisposition.from({\n            type: 'form-data',\n            name,\n          }).toString(),\n        })\n\n        pushLine(stringifyRawHeaders(headers))\n        pushLine()\n        pushLine(value)\n      } else {\n        let headers = new Headers({\n          'Content-Disposition': ContentDisposition.from({\n            type: 'form-data',\n            name,\n            filename: value.filename,\n            filenameSplat: value.filenameSplat,\n          }).toString(),\n        })\n\n        if (value.mediaType) {\n          headers.set('Content-Type', ContentType.from({ mediaType: value.mediaType }).toString())\n        }\n\n        pushLine(stringifyRawHeaders(headers))\n        pushLine()\n        if (typeof value.content === 'string') {\n          pushLine(value.content)\n        } else {\n          chunks.push(value.content as Uint8Array<ArrayBuffer>)\n          pushLine()\n        }\n      }\n    }\n  }\n\n  pushString(`--${boundary}--`)\n\n  return concat(chunks)\n}\n\nexport function getRandomBytes(size: number): Uint8Array {\n  let chunks: Uint8Array<ArrayBuffer>[] = []\n\n  for (let i = 0; i < size; i += 65536) {\n    chunks.push(crypto.getRandomValues(new Uint8Array(Math.min(size - i, 65536))))\n  }\n\n  return concat(chunks)\n}\n\nexport function concat(chunks: Uint8Array<ArrayBuffer>[]): Uint8Array<ArrayBuffer> {\n  if (chunks.length === 1) return chunks[0]\n\n  let length = 0\n  for (let chunk of chunks) {\n    length += chunk.length\n  }\n\n  let result = new Uint8Array(length)\n  let offset = 0\n\n  for (let chunk of chunks) {\n    result.set(chunk, offset)\n    offset += chunk.length\n  }\n\n  return result\n}\n"
  },
  {
    "path": "packages/multipart-parser/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/multipart-parser/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"bench\", \"demos\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/node-fetch-server/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/node-fetch-server/CHANGELOG.md",
    "content": "# `node-fetch-server` CHANGELOG\n\nThis 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/).\n\n## v0.13.0 (2025-12-18)\n\n- Use the `:authority` header to set the URL of http/2 requests.\n\n## v0.12.0 (2025-11-04)\n\n- 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.\n\n## v0.11.0 (2025-10-22)\n\n- 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()`.\n\n## v0.10.0 (2025-10-04)\n\n- Fire `close` and `finish` listeners only once (#10757)\n\n## v0.9.0 (2025-09-16)\n\n- Support `statusText` in HTTP/1 responses (#10745)\n\n## v0.8.1 (2025-09-11)\n\n- Only abort `request.signal` when the connection closes before the response completes (see #10726)\n\n## v0.8.0 (2025-07-24)\n\n- Renamed package from `@mjackson/node-fetch-server` to `@remix-run/node-fetch-server`\n- Handle backpressure correctly in response streaming\n\n## v0.7.0 (2025-06-06)\n\n- Add `/src` to npm package, so \"go to definition\" goes to the actual source\n- Use one set of types for all built files, instead of separate types for ESM and CJS\n- Build using esbuild directly instead of tsup\n\n## v0.6.1 (2025-02-07)\n\n- Update typings and docs for http2 support\n\n## v0.6.0 (2025-02-06)\n\n- Add http/2 support\n\n```ts\nimport * as http2 from 'node:http2'\nimport { createRequestListener } from '@remix-run/node-fetch-server'\n\nlet server = http2.createSecureServer(options)\n\nserver.on(\n  'request',\n  createRequestListener((request) => {\n    let url = new URL(request.url)\n\n    if (url.pathname === '/') {\n      return new Response('Hello HTTP/2!', {\n        headers: {\n          'Content-Type': 'text/plain',\n        },\n      })\n    }\n\n    return new Response('Not Found', { status: 404 })\n  }),\n)\n```\n\n## v0.5.1 (2025-01-25)\n\n- 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.\n\n## v0.5.0 (2024-12-09)\n\n- Expose `createHeaders(req: http.IncomingMessage): Headers` API for creating headers from the headers of incoming request objects.\n- Update `sendResponse` to use an object to add support for libraries such as express while maintaining `node:http` and `node:https` compatibility.\n\n## v0.4.1 (2024-12-04)\n\n- Fix low-level API example in the README\n\n## v0.4.0 (2024-11-26)\n\n- BREAKING: Change `createRequest` signature to `createRequest(req, res, options)` so the abort signal fires on the `res`'s \"end\" event instead of `req`\n\n## v0.3.0 (2024-11-20)\n\n- Added `createRequest(req: http.IncomingMessage, options): Request` and `sendResponse(res: http.ServerResponse, response: Response): Promise<void>` exports to assist with building custom fetch servers\n\n## v0.2.0 (2024-11-14)\n\n- Small perf improvement from avoiding accessing `req.headers` and reading `req.rawHeaders` instead\n- Added CommonJS build\n\n## v0.1.0 (2024-09-05)\n\n- Initial release\n"
  },
  {
    "path": "packages/node-fetch-server/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/node-fetch-server/README.md",
    "content": "# node-fetch-server\n\nBuild 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.\n\n## Features\n\n- **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\n- **Drop-in Integration** - Works with `node:http` and `node:https` modules\n- **Streaming Support** - Response support with `ReadableStream`\n- **Custom Hostname** - Configuration for deployment flexibility\n- **Client Info** - Access to client connection info (IP address, port)\n- **TypeScript** - Full TypeScript support with type definitions\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Quick Start\n\n### Basic Server\n\nHere's a complete working example with a simple in-memory data store:\n\n```ts\nimport * as http from 'node:http'\nimport { createRequestListener } from 'remix/node-fetch-server'\n\n// Example: Simple in-memory user storage\nlet users = new Map([\n  ['1', { id: '1', name: 'Alice', email: 'alice@example.com' }],\n  ['2', { id: '2', name: 'Bob', email: 'bob@example.com' }],\n])\n\nasync function handler(request: Request) {\n  let url = new URL(request.url)\n\n  // GET / - Home page\n  if (url.pathname === '/' && request.method === 'GET') {\n    return new Response('Welcome to the User API! Try GET /api/users')\n  }\n\n  // GET /api/users - List all users\n  if (url.pathname === '/api/users' && request.method === 'GET') {\n    return Response.json(Array.from(users.values()))\n  }\n\n  // GET /api/users/:id - Get specific user\n  let userMatch = url.pathname.match(/^\\/api\\/users\\/(\\w+)$/)\n  if (userMatch && request.method === 'GET') {\n    let user = users.get(userMatch[1])\n    if (user) {\n      return Response.json(user)\n    }\n    return new Response('User not found', { status: 404 })\n  }\n\n  return new Response('Not Found', { status: 404 })\n}\n\n// Create a standard Node.js server\nlet server = http.createServer(createRequestListener(handler))\n\nserver.listen(3000, () => {\n  console.log('Server running at http://localhost:3000')\n})\n```\n\n### Working with Request Data\n\nHandle different types of request data using standard web APIs:\n\n```ts\nasync function handler(request: Request) {\n  let url = new URL(request.url)\n\n  // Handle JSON data\n  if (request.method === 'POST' && url.pathname === '/api/users') {\n    try {\n      let userData = await request.json()\n\n      // Validate required fields\n      if (!userData.name || !userData.email) {\n        return Response.json({ error: 'Name and email are required' }, { status: 400 })\n      }\n\n      // Create user (your implementation)\n      let newUser = {\n        id: Date.now().toString(),\n        ...userData,\n      }\n\n      return Response.json(newUser, { status: 201 })\n    } catch (error) {\n      return Response.json({ error: 'Invalid JSON' }, { status: 400 })\n    }\n  }\n\n  // Handle URL search params\n  if (url.pathname === '/api/search') {\n    let query = url.searchParams.get('q')\n    let limit = parseInt(url.searchParams.get('limit') || '10')\n\n    return Response.json({\n      query,\n      limit,\n      results: [], // Your search results here\n    })\n  }\n\n  return new Response('Not Found', { status: 404 })\n}\n```\n\n### Streaming Responses\n\nTake advantage of web-standard streaming with `ReadableStream`:\n\n```ts\nasync function handler(request: Request) {\n  if (request.url.endsWith('/stream')) {\n    // Create a streaming response\n    let stream = new ReadableStream({\n      async start(controller) {\n        for (let i = 0; i < 5; i++) {\n          controller.enqueue(new TextEncoder().encode(`Chunk ${i}\\n`))\n          await new Promise((resolve) => setTimeout(resolve, 1000))\n        }\n        controller.close()\n      },\n    })\n\n    return new Response(stream, {\n      headers: { 'Content-Type': 'text/plain' },\n    })\n  }\n\n  return new Response('Not Found', { status: 404 })\n}\n```\n\n### Custom Hostname Configuration\n\nConfigure custom hostnames for deployment on VPS or custom environments:\n\n```ts\nimport * as http from 'node:http'\nimport { createRequestListener } from 'remix/node-fetch-server'\n\n// Use a custom hostname (e.g., from environment variable)\nlet hostname = process.env.HOST || 'api.example.com'\n\nasync function handler(request: Request) {\n  // request.url will now use your custom hostname\n  console.log(request.url) // https://api.example.com/path\n\n  return Response.json({\n    message: 'Hello from custom domain!',\n    url: request.url,\n  })\n}\n\nlet server = http.createServer(createRequestListener(handler, { host: hostname }))\n\nserver.listen(3000)\n```\n\n### Accessing Client Information\n\nGet client connection details (IP address, port) for logging or security:\n\n```ts\nimport { type FetchHandler } from 'remix/node-fetch-server'\n\nlet handler: FetchHandler = async (request, client) => {\n  // Log client information\n  console.log(`Request from ${client.address}:${client.port}`)\n\n  // Use for rate limiting, geolocation, etc.\n  if (isRateLimited(client.address)) {\n    return new Response('Too Many Requests', { status: 429 })\n  }\n\n  return Response.json({\n    message: 'Hello!',\n    yourIp: client.address,\n  })\n}\n```\n\n### HTTPS Support\n\nUse with Node.js HTTPS module for secure connections:\n\n```ts\nimport * as https from 'node:https'\nimport * as fs from 'node:fs'\nimport { createRequestListener } from 'remix/node-fetch-server'\n\nlet options = {\n  key: fs.readFileSync('private-key.pem'),\n  cert: fs.readFileSync('certificate.pem'),\n}\n\nlet server = https.createServer(options, createRequestListener(handler))\n\nserver.listen(443, () => {\n  console.log('HTTPS Server running on port 443')\n})\n```\n\n## Advanced Usage\n\n### Low-level API\n\nFor more control over request/response handling, use the low-level API:\n\n```ts\nimport * as http from 'node:http'\nimport { createRequest, sendResponse } from 'remix/node-fetch-server'\n\nlet server = http.createServer(async (req, res) => {\n  // Convert Node.js request to Fetch API Request\n  let request = createRequest(req, res, { host: process.env.HOST })\n\n  try {\n    // Add custom headers or middleware logic\n    let startTime = Date.now()\n\n    // Process the request with your handler\n    let response = await handler(request)\n    // Make sure the response is mutable\n    response = new Response(response.body, response)\n\n    // Add response timing header\n    let duration = Date.now() - startTime\n    response.headers.set('X-Response-Time', `${duration}ms`)\n\n    // Send the response\n    await sendResponse(res, response)\n  } catch (error) {\n    console.error('Server error:', error)\n    res.writeHead(500, { 'Content-Type': 'text/plain' })\n    res.end('Internal Server Error')\n  }\n})\n\nserver.listen(3000)\n```\n\nThe low-level API provides:\n\n- `createRequest(req, res, options)` - Converts Node.js IncomingMessage to web Request\n- `sendResponse(res, response)` - Sends web Response using Node.js ServerResponse\n\nThis is useful for:\n\n- Building custom middleware systems\n- Integrating with existing Node.js code\n- Implementing custom error handling\n- Performance-critical applications\n\n## Migration from Express\n\nTransitioning from Express? Here's a comparison of common patterns:\n\n### Basic Routing\n\n```ts\n// Express\nlet app = express()\n\napp.get('/users/:id', async (req, res) => {\n  let user = await db.getUser(req.params.id)\n  if (!user) {\n    return res.status(404).json({ error: 'User not found' })\n  }\n  res.json(user)\n})\n\napp.listen(3000)\n\n// node-fetch-server\nimport { createRequestListener } from 'remix/node-fetch-server'\n\nasync function handler(request: Request) {\n  let url = new URL(request.url)\n  let match = url.pathname.match(/^\\/users\\/(\\w+)$/)\n\n  if (match && request.method === 'GET') {\n    let user = await db.getUser(match[1])\n    if (!user) {\n      return Response.json({ error: 'User not found' }, { status: 404 })\n    }\n    return Response.json(user)\n  }\n\n  return new Response('Not Found', { status: 404 })\n}\n\nhttp.createServer(createRequestListener(handler)).listen(3000)\n```\n\n## Demos\n\nThe [`demos` directory](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos) contains working demos:\n\n- [`demos/http2`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos/http2) - HTTP/2 server with TLS certificates\n\n## Benchmark\n\nTo run benchmarks comparing `node-fetch-server` performance with comparable libraries:\n\n```sh\npnpm run bench\n```\n\n## Related Packages\n\n- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) - Build HTTP proxy servers using the web fetch API\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/node-fetch-server/bench/package.json",
    "content": "{\n  \"name\": \"node-fetch-server-bench\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@remix-run/node-fetch-server\": \"workspace:^\",\n    \"express\": \"^4.19.2\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^4.17.21\"\n  }\n}\n"
  },
  {
    "path": "packages/node-fetch-server/bench/runner.sh",
    "content": "#!/bin/bash\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\npushd \"$SCRIPT_DIR\" > /dev/null\n\nrun_benchmark() {\n    local server_name=$1\n    local start_command=$2\n\n    export PORT=3000\n\n    echo -e \"\\nRunning benchmark for $server_name ...\\n\"\n\n    # Start the server in the background\n    $start_command &\n    local server_pid=$!\n\n    # Wait for the server to start\n    sleep 2\n\n    wrk -t12 -c400 -d30s http://127.0.0.1:3000/\n\n    kill -SIGINT $server_pid\n\n    wait $server_pid\n}\n\necho $(node -e 'console.log(`Platform: ${os.type()} (${os.release()})`)')\necho $(node -e 'console.log(`CPU: ${os.cpus()[0].model}`)')\necho $(node -e 'console.log(`Date: ${new Date().toLocaleString()}`)')\n\nNODE_VERSION=$(node -e 'console.log(process.version.slice(1))')\nrun_benchmark \"node:http@$NODE_VERSION\" \\\n  \"node ./servers/node-http.ts\"\n\nNODE_FETCH_SERVER_VERSION=$(node -e 'console.log(require(\"../package.json\").version)')\nrun_benchmark \"node-fetch-server@$NODE_FETCH_SERVER_VERSION\" \\\n  \"node ./servers/node-fetch-server.ts\"\n\nEXPRESS_VERSION=$(node -e 'console.log(require(\"express/package.json\").version)')\nrun_benchmark \"express@$EXPRESS_VERSION\" \\\n  \"node ./servers/express.ts\"\n\npopd > /dev/null\n"
  },
  {
    "path": "packages/node-fetch-server/bench/servers/express.ts",
    "content": "import * as stream from 'node:stream'\nimport express from 'express'\n\nconst PORT = process.env.PORT || 3000\n\nlet app = express()\n\napp.get('/', (_req, res) => {\n  res.type('text/html')\n\n  let body = new stream.Readable({\n    read() {\n      this.push('<html><body><h1>Hello, world!</h1></body></html>')\n      this.push(null)\n    },\n  })\n\n  body.pipe(res)\n})\n\napp.listen(PORT)\n"
  },
  {
    "path": "packages/node-fetch-server/bench/servers/node-fetch-server.ts",
    "content": "import * as http from 'node:http'\nimport { createRequestListener } from '@remix-run/node-fetch-server'\n\nconst PORT = process.env.PORT || 3000\n\nlet server = http.createServer(\n  createRequestListener(() => {\n    let stream = new ReadableStream({\n      start(controller) {\n        controller.enqueue('<html><body><h1>Hello, world!</h1></body></html>')\n        controller.close()\n      },\n    })\n\n    return new Response(stream, {\n      headers: { 'Content-Type': 'text/html' },\n    })\n  }),\n)\n\nserver.listen(PORT)\n"
  },
  {
    "path": "packages/node-fetch-server/bench/servers/node-http.ts",
    "content": "import * as http from 'node:http'\nimport * as stream from 'node:stream'\n\nconst PORT = process.env.PORT || 3000\n\nlet server = http.createServer((_req, res) => {\n  res.writeHead(200, { 'Content-Type': 'text/html' })\n\n  let body = new stream.Readable({\n    read() {\n      this.push('<html><body><h1>Hello, world!</h1></body></html>')\n      this.push(null)\n    },\n  })\n\n  body.pipe(res)\n})\n\nserver.listen(PORT)\n"
  },
  {
    "path": "packages/node-fetch-server/demos/http2/README.md",
    "content": "# node-fetch-server HTTP/2 Example\n\nThis is an example of building a http/2 server for Node.js using [the built-in `http2` module](https://nodejs.org/api/http2.html).\n"
  },
  {
    "path": "packages/node-fetch-server/demos/http2/package.json",
    "content": "{\n  \"name\": \"node-fetch-server-http2-example\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@remix-run/node-fetch-server\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "packages/node-fetch-server/demos/http2/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICpDCCAYwCCQDont9/q43GLTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls\nb2NhbGhvc3QwHhcNMjUwMjA2MTkyMjU5WhcNMjYwMjA2MTkyMjU5WjAUMRIwEAYD\nVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDX\nz69g6TBkbdv0TMeTL3jRLgdiSoTyqjcjLEakgEfPDtS0vLadkrqiTipx5do6CpyB\n0vMqrYVgFnnUKykJuTPh5pFIdTFbAKkv3YcyVMRfzt5eMLRFQ5iFbmbzme7BksaZ\nD6b0ahwnNPvRGX/Kz362wQ10nK4YP7JCXbc0Ybxvdfq5o8L0uoOjW5K+8JULVLmc\nimeXA8NLe8ui/flvBgc/P4qt5SVh0S+/FjTEa12DCzxkvaOmWKqpHjzUMg0aKkrV\nQ22hjaEdkgcKomqC/4I3de6bGr828yJltXxTp80/8TaM4U7xPi8hvUfOW+wLPUK9\nODJ+XtQu1LhtQwwe1a0RAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADsHhsAveJ7N\nLOS/J8+y5/KGphlW0/bZzsJGCkm9YMFfPW6U3Ac85X67oahzd7kPI5n2z867dVXO\nIx60enYF3xf2IzcL/FVSL+dDW6JKIp6lB+qdlA7EeN3Segx+NV/fErHUZmL5WmBN\nhNlOdEx06uo6Pp21+RyktDMFKBbpDbPrWHziNWJz0mT6SA1RMT8ElWxKXalO6XFT\nyJCM/iwpm4SrTZ11j8Nszcv730DrMr/hF+uO4d2N4FzV1NAKv66JpXnzPC1jcCFs\nJs7880NtdEdBVadp8gB5Cmu7I6ZcNKe24x1nRNtyddQkgUllKOoIGobrHk02l0PV\n0gnz6agIzfQ=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "packages/node-fetch-server/demos/http2/server.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEA18+vYOkwZG3b9EzHky940S4HYkqE8qo3IyxGpIBH\nzw7UtLy2nZK6ok4qceXaOgqcgdLzKq2FYBZ51CspCbkz4eaRSHUxWwCpL92HMlTE\nX87eXjC0RUOYhW5m85nuwZLGmQ+m9GocJzT70Rl/ys9+tsENdJyuGD+yQl23NGG8\nb3X6uaPC9LqDo1uSvvCVC1S5nIpnlwPDS3vLov35bwYHPz+KreUlYdEvvxY0xGtd\ngws8ZL2jpliqqR481DINGipK1UNtoY2hHZIHCqJqgv+CN3Xumxq/NvMiZbV8U6fN\nP/E2jOFO8T4vIb1HzlvsCz1CvTgyfl7ULtS4bUMMHtWtEQIDAQABoAAwDQYJKoZI\nhvcNAQELBQADggEBAMyfFv3WSqg/X6RmrIZjuFdZ6fnV8Gd5twFuzGWcxcTaxQZx\nOwY7hKXXFsTRjx15T84wV7KcRK26RY9CJB+28zWp5r67/zLO1DkeZ+DuDjyazLPD\niPMmpOhzK9dsu3ymhRjlQcKtUHfK5x/hlSvKaL8LYhEMY8G16ST8GQxoNAR+ax6t\nzq+wS7WccS7kKXr4uJf0H0KL3CqkmzLDlyiV/4f2UGQPk2riYihPuaikb5tLftUv\ncDsLwa4YOU6shdKa6Z0yq2oMjRYzAS79/gF22oxs5pK0Fzf5geQGjbzqd/CPrvWM\nM0IMtej9xpq+CfQv/pxAxmLEMW4WCnnqFwCN/xI=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "packages/node-fetch-server/demos/http2/server.js",
    "content": "import * as http2 from 'node:http2'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { createRequestListener } from '@remix-run/node-fetch-server'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\nconst PORT = 44100\n\nlet options = {\n  key: fs.readFileSync(path.join(__dirname, 'server.key')),\n  cert: fs.readFileSync(path.join(__dirname, 'server.crt')),\n}\n\nlet server = http2.createSecureServer(options)\n\nserver.on(\n  'request',\n  createRequestListener((request) => {\n    let url = new URL(request.url)\n\n    if (url.pathname === '/') {\n      return new Response('Hello HTTP/2!', {\n        headers: {\n          'Content-Type': 'text/plain',\n        },\n      })\n    }\n\n    return new Response('Not Found', { status: 404 })\n  }),\n)\n\nserver.on('error', (err) => {\n  console.error('Server error:', err)\n})\n\nserver.listen(PORT, () => {\n  console.log(`Server running at https://localhost:${PORT}`)\n})\n"
  },
  {
    "path": "packages/node-fetch-server/demos/http2/server.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA18+vYOkwZG3b9EzHky940S4HYkqE8qo3IyxGpIBHzw7UtLy2\nnZK6ok4qceXaOgqcgdLzKq2FYBZ51CspCbkz4eaRSHUxWwCpL92HMlTEX87eXjC0\nRUOYhW5m85nuwZLGmQ+m9GocJzT70Rl/ys9+tsENdJyuGD+yQl23NGG8b3X6uaPC\n9LqDo1uSvvCVC1S5nIpnlwPDS3vLov35bwYHPz+KreUlYdEvvxY0xGtdgws8ZL2j\npliqqR481DINGipK1UNtoY2hHZIHCqJqgv+CN3Xumxq/NvMiZbV8U6fNP/E2jOFO\n8T4vIb1HzlvsCz1CvTgyfl7ULtS4bUMMHtWtEQIDAQABAoIBAGlbyDgcv/ZXt+lF\nzq0poOcmfI5c6Rj7Rp3SUM6gne4VRHzUIKc+6gSw+oHOgEKTyaKL1RFB03p8no+Z\nXpiTpSOlB8qDBEx0PyTSFt3YimJnwSHkzy19eamyo2pL/UbdnD0/aferEgGGGWYU\n99GQiUE5cJM8prXJ6wIBdJ6LFI6o+fNWjWAQeg6wEuA3MbfvQ4uZo8WvknVaJLqa\nAYMPK/V1bT9/hmaLFfDiLMrD92ZC+mJqgEvsLrU64mQmASa7Zfh9v6sqqS6h4vjZ\nfmJfQjG1+g1rCkE2kHEYKmziz3A0A5FUjWwny3RSt9lbIJh2E0vcgYtbF3abUCIe\naQOQkP0CgYEA7MLq430P+Arn2xFPX8Y7XeQftv++qBsLZeq4lSV6ru1/mG68xFrm\nfqXI/xi3lyfbezpG0BFQGqVFB1f70IMGKcM7Ph1l5mI4OWcpqzcKi8UqGRzeFJ9Z\nNB7HWrU8h2dl/d5yRF4EmYj2RrBxoZdjN0H+3yL/TotcTuPPPbDKeRMCgYEA6Vj3\nKjnn3avIMK/UWkREXoGCvRQH2wD4h6vQs1ro2PNMyWB+JS1XxucJbPfDmZhqxkyt\nrWehadRrmlMfTX27llRjeoVH3h54QE58lgLme+Ak3qjNiiUlFVFiCpEaYrba3iDk\nA3jqQ1naqXyP6sbiItRZ96v05hiWIYe8WkaiCcsCgYAUziAT84Z2toafUosWEHZh\nDs3Wp+yaGx5KS3EC8jMwsgAXZgvCeXZtxKW//O0NJFx+HKXiXNMcNE+3kHy5Wvos\nq1JGaBDvSMxGBxG7UO/lTmMfp9DAISyWjunXx7tU7rogr+58oYJn94gkuBaUK5h1\nX6BE/W9P+KEY8Z3hfuqb7wKBgHGDhtEy1BmgvET68/lpZjz3Eat7OAsQoNYW/fKS\nEd2gFcWMvDDHqwCmWY55xNxOKfsHSCGn3PzHigTL1Nl0hbGuoanzdi+WcPcPd0ne\nZVLzidwLD9nZEf4Z1fC//67vtu8B3wnVY1iaOGXko3oZf95joNR8ASmB4l6zUiw0\nWhUbAoGBAJIAzNQMMIF3fSUL4CnhTaWuo6UNUvQjXiSfx9opzWLfMWgGqoJ1RqEF\nHTL3jLQQfypqy6E0LtVRMhXJL/J1dY46ZJmkYxosDyfi1Om1wmKV4mbUGoIKiMFd\n1uOjZjUX3/XGA5rt1pdQxdwbauDgrPorQkKJFFWbCYZ9YCAxGxP9\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "packages/node-fetch-server/demos/http2/tsconfig.json",
    "content": "{\n  \"include\": [\"server.js\"],\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\"],\n    \"noEmit\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/node-fetch-server/package.json",
    "content": "{\n  \"name\": \"@remix-run/node-fetch-server\",\n  \"version\": \"0.13.0\",\n  \"description\": \"Build servers for Node.js using the web fetch API\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/node-fetch-server\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/node-fetch-server#readme\",\n  \"license\": \"MIT\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"bench\": \"bash ./bench/runner.sh\",\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"http\",\n    \"server\",\n    \"request\",\n    \"response\",\n    \"fetch\",\n    \"web\"\n  ]\n}\n"
  },
  {
    "path": "packages/node-fetch-server/src/index.ts",
    "content": "export { type ClientAddress, type ErrorHandler, type FetchHandler } from './lib/fetch-handler.ts'\nexport {\n  type RequestListenerOptions,\n  createRequestListener,\n  type RequestOptions,\n  createRequest,\n  createHeaders,\n  sendResponse,\n} from './lib/request-listener.ts'\n"
  },
  {
    "path": "packages/node-fetch-server/src/lib/fetch-handler.ts",
    "content": "/**\n * Information about the client that sent a request.\n */\nexport interface ClientAddress {\n  /**\n   * The IP address of the client that sent the request.\n   *\n   * [Node.js Reference](https://nodejs.org/api/net.html#socketremoteaddress)\n   */\n  address: string\n  /**\n   * The family of the client IP address.\n   *\n   * [Node.js Reference](https://nodejs.org/api/net.html#socketremotefamily)\n   */\n  family: 'IPv4' | 'IPv6'\n  /**\n   * The remote port of the client that sent the request.\n   *\n   * [Node.js Reference](https://nodejs.org/api/net.html#socketremoteport)\n   */\n  port: number\n}\n\n/**\n * A function that handles an error that occurred during request handling. May return a response to\n * send to the client, or `undefined` to allow the server to send a default error response.\n *\n * [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n *\n * @param error The error that was thrown\n * @returns A response to send to the client, or `undefined` for the default error response\n */\nexport interface ErrorHandler {\n  /**\n   * Handles a thrown request-processing error and may return a custom response.\n   */\n  (error: unknown): void | Response | Promise<void | Response>\n}\n\n/**\n * A function that handles an incoming request and returns a response.\n *\n * [MDN `Request` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Request)\n *\n * [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n *\n * @param request The incoming request\n * @param client Information about the client that sent the request\n * @returns A response to send to the client\n */\nexport interface FetchHandler {\n  /**\n   * Handles an incoming request and returns the response sent to the client.\n   */\n  (request: Request, client: ClientAddress): Response | Promise<Response>\n}\n"
  },
  {
    "path": "packages/node-fetch-server/src/lib/read-stream.ts",
    "content": "export async function* readStream(stream: ReadableStream<Uint8Array>): AsyncIterable<Uint8Array> {\n  let reader = stream.getReader()\n\n  try {\n    while (true) {\n      let result = await reader.read()\n      if (result.done) break\n      yield result.value\n    }\n  } finally {\n    reader.releaseLock()\n  }\n}\n"
  },
  {
    "path": "packages/node-fetch-server/src/lib/request-listener.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it, mock } from 'node:test'\n\nimport type * as http from 'node:http'\nimport * as stream from 'node:stream'\n\nimport { type FetchHandler } from './fetch-handler.ts'\nimport { createRequest, createRequestListener } from './request-listener.ts'\n\ndescribe('createRequestListener', () => {\n  it('returns a request listener', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async () => new Response('Hello, world!')\n\n      let listener = createRequestListener(handler)\n      assert.ok(listener)\n\n      let req = createMockRequest()\n      let res = createMockResponse({ req })\n\n      let chunks: Uint8Array[] = []\n      mock.method(res, 'write', (chunk: Uint8Array) => {\n        chunks.push(chunk)\n      })\n\n      mock.method(res, 'end', () => {\n        let body = Buffer.concat(chunks).toString()\n        assert.equal(body, 'Hello, world!')\n        resolve()\n      })\n\n      listener(req, res)\n    })\n  })\n\n  it('returns custom status, statusText, and header values (HTTP/1)', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async () =>\n        new Response('Hello, world!', {\n          status: 201,\n          statusText: 'Created!',\n          headers: {\n            'x-a': 'A',\n            'x-b': 'B',\n          },\n        })\n\n      let listener = createRequestListener(handler)\n      assert.ok(listener)\n\n      let req = createMockRequest()\n      req.httpVersionMajor = 1\n      let res = createMockResponse({ req })\n\n      mock.method(\n        res,\n        'writeHead',\n        (status: number, statusText: string, headers: Record<string, string | string[]>) => {\n          assert.equal(status, 201)\n          assert.equal(statusText, 'Created!')\n          assert.equal(headers['x-a'], 'A')\n          assert.equal(headers['x-b'], 'B')\n        },\n      )\n\n      mock.method(res, 'end', () => resolve())\n\n      listener(req, res)\n    })\n  })\n\n  it('returns custom status, statusText, and header values (HTTP/2)', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async () =>\n        new Response('Hello, world!', {\n          status: 201,\n          statusText: 'Created!',\n          headers: {\n            'x-a': 'A',\n            'x-b': 'B',\n          },\n        })\n\n      let listener = createRequestListener(handler)\n      assert.ok(listener)\n\n      let req = createMockRequest()\n      req.httpVersionMajor = 2\n      let res = createMockResponse({ req })\n\n      mock.method(\n        res,\n        'writeHead',\n        (status: number, headers: Record<string, string | string[]>) => {\n          assert.equal(status, 201)\n          assert.equal(headers['x-a'], 'A')\n          assert.equal(headers['x-b'], 'B')\n        },\n      )\n\n      mock.method(res, 'end', () => resolve())\n\n      listener(req, res)\n    })\n  })\n\n  it('calls onError when an error is thrown in the request handler', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async () => {\n        throw new Error('boom!')\n      }\n      let errorHandler = mock.fn()\n\n      let listener = createRequestListener(handler, { onError: errorHandler })\n      assert.ok(listener)\n\n      let req = createMockRequest()\n      let res = createMockResponse({ req })\n\n      mock.method(res, 'end', () => {\n        assert.equal(errorHandler.mock.calls.length, 1)\n        resolve()\n      })\n\n      listener(req, res)\n    })\n  })\n\n  it('returns a 500 \"Internal Server Error\" response when an error is thrown in the request handler', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async () => {\n        throw new Error('boom!')\n      }\n      let errorHandler = async () => {\n        // ignore\n      }\n\n      let listener = createRequestListener(handler, { onError: errorHandler })\n      assert.ok(listener)\n\n      let req = createMockRequest()\n      let res = createMockResponse({ req })\n\n      let status: number | undefined\n      mock.method(res, 'writeHead', (statusCode: number) => {\n        status = statusCode\n      })\n\n      let chunks: Uint8Array[] = []\n      mock.method(res, 'write', (chunk: Uint8Array) => {\n        chunks.push(chunk)\n      })\n\n      mock.method(res, 'end', () => {\n        assert.equal(status, 500)\n        let body = Buffer.concat(chunks).toString()\n        assert.equal(body, 'Internal Server Error')\n        resolve()\n      })\n\n      listener(req, res)\n    })\n  })\n\n  it('uses the `Host` header to construct the URL by default', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async (request) => {\n        assert.equal(request.url, 'http://example.com/')\n        return new Response('Hello, world!')\n      }\n\n      let listener = createRequestListener(handler)\n      assert.ok(listener)\n\n      let req = createMockRequest({ headers: { host: 'example.com' } })\n      let res = createMockResponse({ req })\n\n      listener(req, res)\n      resolve()\n    })\n  })\n\n  it('uses the `:authority` header to construct the URL for http/2 requests', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async (request) => {\n        assert.equal(request.url, 'http://example.com/')\n        return new Response('Hello, world!')\n      }\n\n      let listener = createRequestListener(handler)\n      assert.ok(listener)\n\n      let req = createMockRequest({ headers: { ':authority': 'example.com' } })\n      let res = createMockResponse({ req })\n\n      listener(req, res)\n      resolve()\n    })\n  })\n\n  it('uses the `host` option to override the `Host` header', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async (request) => {\n        assert.equal(request.url, 'http://remix.run/')\n        return new Response('Hello, world!')\n      }\n\n      let listener = createRequestListener(handler, { host: 'remix.run' })\n      assert.ok(listener)\n\n      let req = createMockRequest({ headers: { host: 'example.com' } })\n      let res = createMockResponse({ req })\n\n      listener(req, res)\n      resolve()\n    })\n  })\n\n  it('uses the `protocol` option to construct the URL', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async (request) => {\n        assert.equal(request.url, 'https://example.com/')\n        return new Response('Hello, world!')\n      }\n\n      let listener = createRequestListener(handler, { protocol: 'https:' })\n      assert.ok(listener)\n\n      let req = createMockRequest({ headers: { host: 'example.com' } })\n      let res = createMockResponse({ req })\n\n      listener(req, res)\n      resolve()\n    })\n  })\n\n  it('sets multiple Set-Cookie headers', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async () => {\n        let headers = new Headers()\n        headers.set('Content-Type', 'text/plain')\n        headers.append('Set-Cookie', 'a=1')\n        headers.append('Set-Cookie', 'b=2')\n        return new Response('Hello, world!', { headers })\n      }\n\n      let listener = createRequestListener(handler)\n      assert.ok(listener)\n\n      let req = createMockRequest()\n      req.httpVersionMajor = 1\n      let res = createMockResponse({ req })\n\n      let headers: Record<string, string | string[]>\n      mock.method(\n        res,\n        'writeHead',\n        (_status: number, _statusText: string, headersObj: Record<string, string | string[]>) => {\n          headers = headersObj\n        },\n      )\n\n      mock.method(res, 'end', () => {\n        assert.deepEqual(headers, {\n          'content-type': 'text/plain',\n          'set-cookie': ['a=1', 'b=2'],\n        })\n        resolve()\n      })\n\n      listener(req, res)\n    })\n  })\n\n  it('truncates the response body when the request method is HEAD', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async () => new Response('Hello, world!')\n\n      let listener = createRequestListener(handler)\n      assert.ok(listener)\n\n      let req = createMockRequest({ method: 'HEAD' })\n      let res = createMockResponse({ req })\n\n      let chunks: Uint8Array[] = []\n      mock.method(res, 'write', (chunk: Uint8Array) => {\n        chunks.push(chunk)\n      })\n\n      mock.method(res, 'end', () => {\n        assert.equal(chunks.length, 0)\n        resolve()\n      })\n\n      listener(req, res)\n    })\n  })\n\n  it('handles backpressure when writing response chunks', async () => {\n    await new Promise<void>((resolve) => {\n      let handler: FetchHandler = async () => {\n        let chunks = ['chunk1', 'chunk2', 'chunk3', 'chunk4', 'chunk5']\n        let body = new ReadableStream({\n          async start(controller) {\n            for (let chunk of chunks) {\n              controller.enqueue(new TextEncoder().encode(chunk))\n            }\n            controller.close()\n          },\n        })\n\n        return new Response(body)\n      }\n\n      let listener = createRequestListener(handler)\n      assert.ok(listener)\n\n      let req = createMockRequest()\n\n      let writtenChunks: Uint8Array[] = []\n      let writeCallCount = 0\n      let drainListenerCalled = false\n\n      let writable = new stream.Writable()\n      let res = Object.assign(writable, {\n        req,\n        writeHead() {},\n        write(chunk: Uint8Array) {\n          writtenChunks.push(chunk)\n          writeCallCount++\n\n          // Simulate backpressure on chunks 2 and 4\n          if (writeCallCount === 2 || writeCallCount === 4) {\n            setTimeout(() => {\n              writable.emit('drain')\n            }, 0)\n            return false // Buffer is full\n          }\n          return true // Buffer has space\n        },\n        end() {\n          assert.equal(writtenChunks.length, 5)\n          assert.equal(writeCallCount, 5)\n\n          assert.ok(drainListenerCalled, 'drain listener should have been registered')\n\n          let receivedText = writtenChunks.map((chunk) => new TextDecoder().decode(chunk)).join('')\n          assert.equal(receivedText, 'chunk1chunk2chunk3chunk4chunk5')\n\n          resolve()\n        },\n        once(event: string, callback: () => void) {\n          if (event === 'drain') {\n            drainListenerCalled = true\n          }\n          stream.Writable.prototype.once.call(writable, event, callback)\n        },\n      }) as unknown as http.ServerResponse\n\n      listener(req, res)\n    })\n  })\n})\n\ndescribe('createRequest abort behavior', () => {\n  it('aborts the request.signal when response closes before finishing', () => {\n    let req = createMockRequest()\n    let res = createMockResponse({ req })\n    let request = createRequest(req, res)\n\n    assert.equal(request.signal.aborted, false)\n    res.emit('close')\n    assert.equal(request.signal.aborted, true)\n  })\n\n  it('does not abort after finish even if close occurs later', () => {\n    let req = createMockRequest()\n    let res = createMockResponse({ req })\n    let request = createRequest(req, res)\n\n    res.emit('finish')\n    res.emit('close')\n    assert.equal(request.signal.aborted, false)\n  })\n})\n\nfunction createMockRequest({\n  url = '/',\n  method = 'GET',\n  headers = {},\n  socket = {},\n  body,\n}: {\n  method?: string\n  url?: string\n  headers?: Record<string, string>\n  socket?: {\n    encrypted?: boolean\n    remoteAddress?: string\n  }\n  body?: string | Buffer\n} = {}): http.IncomingMessage {\n  let rawHeaders = Object.entries(headers).flatMap(([key, value]) => [key, value])\n\n  return Object.assign(\n    new stream.Readable({\n      read() {\n        if (body != null) this.push(Buffer.from(body))\n        this.push(null)\n      },\n    }),\n    {\n      url,\n      method,\n      rawHeaders,\n      socket,\n      headers,\n    },\n  ) as http.IncomingMessage\n}\n\nfunction createMockResponse({\n  req = createMockRequest(),\n}: {\n  req: http.IncomingMessage\n}): http.ServerResponse {\n  return Object.assign(new stream.Writable(), {\n    req,\n    writeHead() {},\n    write() {},\n    end() {},\n  }) as unknown as http.ServerResponse\n}\n"
  },
  {
    "path": "packages/node-fetch-server/src/lib/request-listener.ts",
    "content": "import type * as http from 'node:http'\nimport type * as http2 from 'node:http2'\n\nimport type { ClientAddress, ErrorHandler, FetchHandler } from './fetch-handler.ts'\nimport { readStream } from './read-stream.ts'\n\n/**\n * Options for creating a Node.js request listener.\n */\nexport interface RequestListenerOptions {\n  /**\n   * Overrides the host portion of the incoming request URL. By default the request URL host is\n   * derived from the HTTP `Host` header.\n   *\n   * For example, if you have a `$HOST` environment variable that contains the hostname of your\n   * server, you can use it to set the host of all incoming request URLs like so:\n   *\n   * ```ts\n   * createRequestListener(handler, { host: process.env.HOST })\n   * ```\n   */\n  host?: string\n  /**\n   * An error handler that determines the response when the request handler throws an error. By\n   * default a 500 Internal Server Error response will be sent.\n   */\n  onError?: ErrorHandler\n  /**\n   * Overrides the protocol of the incoming request URL. By default the request URL protocol is\n   * derived from the connection protocol. So e.g. when serving over HTTPS (using\n   * `https.createServer()`), the request URL will begin with `https:`.\n   */\n  protocol?: string\n}\n\n/**\n * Wraps a fetch handler in a Node.js request listener that can be used with:\n *\n * - [`http.createServer()`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener)\n * - [`https.createServer()`](https://nodejs.org/api/https.html#httpscreateserveroptions-requestlistener)\n * - [`http2.createServer()`](https://nodejs.org/api/http2.html#http2createserveroptions-onrequesthandler)\n * - [`http2.createSecureServer()`](https://nodejs.org/api/http2.html#http2createsecureserveroptions-onrequesthandler)\n *\n * Example:\n *\n * ```ts\n * import * as http from 'node:http';\n * import { createRequestListener } from 'remix/node-fetch-server';\n *\n * async function handler(request) {\n *   return new Response('Hello, world!');\n * }\n *\n * let server = http.createServer(\n *   createRequestListener(handler)\n * );\n *\n * server.listen(3000);\n * ```\n *\n * @param handler The fetch handler to use for processing incoming requests\n * @param options Request listener options\n * @returns A Node.js request listener function\n */\nexport function createRequestListener(\n  handler: FetchHandler,\n  options?: RequestListenerOptions,\n): http.RequestListener {\n  let onError = options?.onError ?? defaultErrorHandler\n\n  return async (req, res) => {\n    let request = createRequest(req, res, options)\n    let client = {\n      address: req.socket.remoteAddress!,\n      family: req.socket.remoteFamily! as ClientAddress['family'],\n      port: req.socket.remotePort!,\n    }\n\n    let response: Response\n    try {\n      response = await handler(request, client)\n    } catch (error) {\n      try {\n        response = (await onError(error)) ?? internalServerError()\n      } catch (error) {\n        console.error(`There was an error in the error handler: ${error}`)\n        response = internalServerError()\n      }\n    }\n\n    await sendResponse(res, response)\n  }\n}\n\nfunction defaultErrorHandler(error: unknown): Response {\n  console.error(error)\n  return internalServerError()\n}\n\nfunction internalServerError(): Response {\n  return new Response(\n    // \"Internal Server Error\"\n    new Uint8Array([\n      73, 110, 116, 101, 114, 110, 97, 108, 32, 83, 101, 114, 118, 101, 114, 32, 69, 114, 114, 111,\n      114,\n    ]),\n    {\n      status: 500,\n      headers: {\n        'Content-Type': 'text/plain',\n      },\n    },\n  )\n}\n\n/**\n * Options for creating a `Request` from a Node.js incoming message.\n */\nexport type RequestOptions = Omit<RequestListenerOptions, 'onError'>\n\n/**\n * Creates a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object from\n *\n * - a [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse) pair\n * - a [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse) pair\n *\n * @param req The incoming request object\n * @param res The server response object\n * @param options Options for creating the request\n * @returns A `Request` object\n */\nexport function createRequest(\n  req: http.IncomingMessage | http2.Http2ServerRequest,\n  res: http.ServerResponse | http2.Http2ServerResponse,\n  options?: RequestOptions,\n): Request {\n  let controller: AbortController | null = new AbortController()\n\n  // Abort once we can no longer write a response if we have\n  // not yet sent a response (i.e., `close` without `finish`)\n  // `finish` -> done rendering the response\n  // `close` -> response can no longer be written to\n  res.once('close', () => controller?.abort())\n  res.once('finish', () => (controller = null))\n\n  let method = req.method ?? 'GET'\n  let headers = createHeaders(req)\n\n  let protocol =\n    options?.protocol ?? ('encrypted' in req.socket && req.socket.encrypted ? 'https:' : 'http:')\n  let host = options?.host ?? headers.get('Host') ?? req.headers[':authority'] ?? 'localhost'\n  let url = new URL(req.url!, `${protocol}//${host}`)\n\n  let init: RequestInit = { method, headers, signal: controller.signal }\n\n  if (method !== 'GET' && method !== 'HEAD') {\n    init.body = new ReadableStream({\n      start(controller) {\n        req.on('data', (chunk) => {\n          controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength))\n        })\n        req.on('end', () => {\n          controller.close()\n        })\n      },\n    })\n\n    // init.duplex = 'half' must be set when body is a ReadableStream, and Node follows the spec.\n    // However, this property is not defined in the TypeScript types for RequestInit, so we have\n    // to cast it here in order to set it without a type error.\n    // See https://fetch.spec.whatwg.org/#dom-requestinit-duplex\n    ;(init as { duplex: 'half' }).duplex = 'half'\n  }\n\n  return new Request(url, init)\n}\n\n/**\n * Creates a [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object from the headers in a Node.js\n * [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest).\n *\n * @param req The incoming request object\n * @returns A `Headers` object\n */\nexport function createHeaders(req: http.IncomingMessage | http2.Http2ServerRequest): Headers {\n  let headers = new Headers()\n\n  let rawHeaders = req.rawHeaders\n  for (let i = 0; i < rawHeaders.length; i += 2) {\n    if (rawHeaders[i].startsWith(':')) continue\n    headers.append(rawHeaders[i], rawHeaders[i + 1])\n  }\n\n  return headers\n}\n\n/**\n * Sends a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) to the client using a Node.js\n * [`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse)\n * object.\n *\n * @param res The server response object\n * @param response The response to send\n */\nexport async function sendResponse(\n  res: http.ServerResponse | http2.Http2ServerResponse,\n  response: Response,\n): Promise<void> {\n  // Iterate over response.headers so we are sure to send multiple Set-Cookie headers correctly.\n  // These would incorrectly be merged into a single header if we tried to use\n  // `Object.fromEntries(response.headers.entries())`.\n  let headers: Record<string, string | string[]> = {}\n  for (let [key, value] of response.headers) {\n    if (key in headers) {\n      if (Array.isArray(headers[key])) {\n        headers[key].push(value)\n      } else {\n        headers[key] = [headers[key] as string, value]\n      }\n    } else {\n      headers[key] = value\n    }\n  }\n\n  if (res.req.httpVersionMajor === 1) {\n    ;(res as http.ServerResponse).writeHead(response.status, response.statusText, headers)\n  } else {\n    // HTTP/2 doesn't support status messages\n    // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.4\n    //\n    // HTTP2 `res.writeHead()` will safely ignore the statusText parameter, but\n    // it will emit a warning which we want to avoid.\n    // https://nodejs.org/docs/latest-v22.x/api/http2.html#responsewriteheadstatuscode-statusmessage-headers\n    ;(res as http2.Http2ServerResponse).writeHead(response.status, headers)\n  }\n\n  if (response.body != null && res.req.method !== 'HEAD') {\n    for await (let chunk of readStream(response.body)) {\n      // @ts-expect-error - Node typings for http2 require a 2nd parameter to write but it's optional\n      if (res.write(chunk) === false) {\n        await new Promise<void>((resolve) => {\n          res.once('drain', resolve)\n        })\n      }\n    }\n  }\n\n  res.end()\n}\n"
  },
  {
    "path": "packages/node-fetch-server/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/node-fetch-server/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"bench\", \"demos\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/remix/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/remix/.changes/config.json",
    "content": "{\n  \"prereleaseChannel\": \"alpha\"\n}\n"
  },
  {
    "path": "packages/remix/.changes/minor.remix.add-cors-middleware-export.md",
    "content": "Add `remix/cors-middleware` to re-export the CORS middleware APIs from `@remix-run/cors-middleware`.\n"
  },
  {
    "path": "packages/remix/.changes/minor.remix.component-exports.md",
    "content": "Update `remix/component` and `remix/component/server` to re-export the latest `@remix-run/component` frame-navigation APIs.\n\n`remix/component` 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/component/server` re-exports the SSR frame source APIs including `frameSrc`, `topFrameSrc`, and `ResolveFrameContext`.\n"
  },
  {
    "path": "packages/remix/.changes/minor.remix.update-exports.md",
    "content": "BREAKING CHANGE: Remove the `remix/data-table/sql` export. Import `SqlStatement`, `sql`, and `rawSql` from `remix/data-table` instead.\n\n`remix/data-table/sql-helpers` remains available for adapter-facing SQL utilities.\n\n`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.\n\nBREAKING 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.\n\n`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.\n\nThe incidental `QueryMethod` type export has also been removed; use `Database['query']` or `QueryForTable<table>` when you need that type shape.\n"
  },
  {
    "path": "packages/remix/.changes/minor.request-protection-middlewares.md",
    "content": "Add browser-origin and CSRF protection middleware APIs to `remix`.\n\n- `remix/cop-middleware` exposes `cop(options)` for browser-focused cross-origin protection\n  using `Sec-Fetch-Site` with `Origin` fallback, trusted origins, and configurable bypasses.\n- `remix/csrf-middleware` exposes `csrf(options)` and `getCsrfToken(context)` for\n  session-backed CSRF tokens plus origin validation.\n- Apps can use either middleware independently or layer `cop()`, `session()`, and `csrf()`\n  together when they want both browser-origin filtering and token-backed protection.\n"
  },
  {
    "path": "packages/remix/CHANGELOG.md",
    "content": "# `remix` CHANGELOG\n\nThis is the changelog for [`remix`](https://github.com/remix-run/remix/tree/main/packages/remix). It follows [semantic versioning](https://semver.org/).\n\n## v3.0.0-alpha.3\n\n### Pre-release Changes\n\n- Added `package.json` `exports`:\n\n  - `remix/data-schema` to re-export APIs from `@remix-run/data-schema`\n  - `remix/data-schema/checks` to re-export APIs from `@remix-run/data-schema/checks`\n  - `remix/data-schema/coerce` to re-export APIs from `@remix-run/data-schema/coerce`\n  - `remix/data-schema/lazy` to re-export APIs from `@remix-run/data-schema/lazy`\n  - `remix/data-table` to re-export APIs from `@remix-run/data-table`\n  - `remix/data-table-mysql` to re-export APIs from `@remix-run/data-table-mysql`\n  - `remix/data-table-postgres` to re-export APIs from `@remix-run/data-table-postgres`\n  - `remix/data-table-sqlite` to re-export APIs from `@remix-run/data-table-sqlite`\n  - `remix/fetch-router/routes` to re-export APIs from `@remix-run/fetch-router/routes`\n  - `remix/file-storage-s3` to re-export APIs from `@remix-run/file-storage-s3`\n  - `remix/session-storage-memcache` to re-export APIs from `@remix-run/session-storage-memcache`\n  - `remix/session-storage-redis` to re-export APIs from `@remix-run/session-storage-redis`\n\n- 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/component`\n\n- Bumped `@remix-run/*` dependencies:\n  - [`async-context-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/async-context-middleware@0.1.3)\n  - [`component@0.5.0`](https://github.com/remix-run/remix/releases/tag/component@0.5.0)\n  - [`compression-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/compression-middleware@0.1.3)\n  - [`data-schema@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-schema@0.1.0)\n  - [`data-table@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table@0.1.0)\n  - [`data-table-mysql@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table-mysql@0.1.0)\n  - [`data-table-postgres@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table-postgres@0.1.0)\n  - [`data-table-sqlite@0.1.0`](https://github.com/remix-run/remix/releases/tag/data-table-sqlite@0.1.0)\n  - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0)\n  - [`file-storage@0.13.3`](https://github.com/remix-run/remix/releases/tag/file-storage@0.13.3)\n  - [`file-storage-s3@0.1.0`](https://github.com/remix-run/remix/releases/tag/file-storage-s3@0.1.0)\n  - [`form-data-middleware@0.1.4`](https://github.com/remix-run/remix/releases/tag/form-data-middleware@0.1.4)\n  - [`fs@0.4.2`](https://github.com/remix-run/remix/releases/tag/fs@0.4.2)\n  - [`lazy-file@5.0.2`](https://github.com/remix-run/remix/releases/tag/lazy-file@5.0.2)\n  - [`logger-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/logger-middleware@0.1.3)\n  - [`method-override-middleware@0.1.4`](https://github.com/remix-run/remix/releases/tag/method-override-middleware@0.1.4)\n  - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0)\n  - [`response@0.3.2`](https://github.com/remix-run/remix/releases/tag/response@0.3.2)\n  - [`route-pattern@0.19.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.19.0)\n  - [`session-middleware@0.1.4`](https://github.com/remix-run/remix/releases/tag/session-middleware@0.1.4)\n  - [`session-storage-memcache@0.1.0`](https://github.com/remix-run/remix/releases/tag/session-storage-memcache@0.1.0)\n  - [`session-storage-redis@0.1.0`](https://github.com/remix-run/remix/releases/tag/session-storage-redis@0.1.0)\n  - [`static-middleware@0.4.4`](https://github.com/remix-run/remix/releases/tag/static-middleware@0.4.4)\n\n## v3.0.0-alpha.2\n\n### Pre-release Changes\n\n- Added `package.json` `exports`:\n\n  - `remix/route-pattern/specificity` to re-export APIs from `@remix-run/route-pattern/specificity`\n\n- Bumped `@remix-run/*` dependencies:\n  - [`async-context-middleware@0.1.2`](https://github.com/remix-run/remix/releases/tag/async-context-middleware@0.1.2)\n  - [`component@0.4.0`](https://github.com/remix-run/remix/releases/tag/component@0.4.0)\n  - [`compression-middleware@0.1.2`](https://github.com/remix-run/remix/releases/tag/compression-middleware@0.1.2)\n  - [`fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0)\n  - [`form-data-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/form-data-middleware@0.1.3)\n  - [`form-data-parser@0.15.0`](https://github.com/remix-run/remix/releases/tag/form-data-parser@0.15.0)\n  - [`interaction@0.5.0`](https://github.com/remix-run/remix/releases/tag/interaction@0.5.0)\n  - [`logger-middleware@0.1.2`](https://github.com/remix-run/remix/releases/tag/logger-middleware@0.1.2)\n  - [`method-override-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/method-override-middleware@0.1.3)\n  - [`route-pattern@0.18.0`](https://github.com/remix-run/remix/releases/tag/route-pattern@0.18.0)\n  - [`session-middleware@0.1.3`](https://github.com/remix-run/remix/releases/tag/session-middleware@0.1.3)\n  - [`static-middleware@0.4.3`](https://github.com/remix-run/remix/releases/tag/static-middleware@0.4.3)\n\n## v3.0.0-alpha.1\n\n### Pre-release Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v3.0.0-alpha.0\n\n### Major Changes\n\n- Initial alpha release of `remix` package for Remix 3\n"
  },
  {
    "path": "packages/remix/README.md",
    "content": "# remix\n\nA modern web framework for JavaScript.\n\nSee [remix.run](https://remix.run) for more information.\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/remix/package.json",
    "content": "{\n  \"name\": \"remix\",\n  \"version\": \"3.0.0-alpha.3\",\n  \"description\": \"Remix Web Framework\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/remix\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/remix#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \"./async-context-middleware\": \"./src/async-context-middleware.ts\",\n    \"./component\": \"./src/component.ts\",\n    \"./component/jsx-dev-runtime\": \"./src/component/jsx-dev-runtime.ts\",\n    \"./component/jsx-runtime\": \"./src/component/jsx-runtime.ts\",\n    \"./component/server\": \"./src/component/server.ts\",\n    \"./compression-middleware\": \"./src/compression-middleware.ts\",\n    \"./cookie\": \"./src/cookie.ts\",\n    \"./cop-middleware\": \"./src/cop-middleware.ts\",\n    \"./cors-middleware\": \"./src/cors-middleware.ts\",\n    \"./csrf-middleware\": \"./src/csrf-middleware.ts\",\n    \"./data-schema\": \"./src/data-schema.ts\",\n    \"./data-schema/checks\": \"./src/data-schema/checks.ts\",\n    \"./data-schema/coerce\": \"./src/data-schema/coerce.ts\",\n    \"./data-schema/form-data\": \"./src/data-schema/form-data.ts\",\n    \"./data-schema/lazy\": \"./src/data-schema/lazy.ts\",\n    \"./data-table\": \"./src/data-table.ts\",\n    \"./data-table-mysql\": \"./src/data-table-mysql.ts\",\n    \"./data-table-postgres\": \"./src/data-table-postgres.ts\",\n    \"./data-table-sqlite\": \"./src/data-table-sqlite.ts\",\n    \"./data-table/migrations\": \"./src/data-table/migrations.ts\",\n    \"./data-table/migrations/node\": \"./src/data-table/migrations/node.ts\",\n    \"./data-table/operators\": \"./src/data-table/operators.ts\",\n    \"./data-table/sql-helpers\": \"./src/data-table/sql-helpers.ts\",\n    \"./fetch-proxy\": \"./src/fetch-proxy.ts\",\n    \"./fetch-router\": \"./src/fetch-router.ts\",\n    \"./fetch-router/routes\": \"./src/fetch-router/routes.ts\",\n    \"./file-storage\": \"./src/file-storage.ts\",\n    \"./file-storage-s3\": \"./src/file-storage-s3.ts\",\n    \"./file-storage/fs\": \"./src/file-storage/fs.ts\",\n    \"./file-storage/memory\": \"./src/file-storage/memory.ts\",\n    \"./form-data-middleware\": \"./src/form-data-middleware.ts\",\n    \"./form-data-parser\": \"./src/form-data-parser.ts\",\n    \"./fs\": \"./src/fs.ts\",\n    \"./headers\": \"./src/headers.ts\",\n    \"./html-template\": \"./src/html-template.ts\",\n    \"./lazy-file\": \"./src/lazy-file.ts\",\n    \"./logger-middleware\": \"./src/logger-middleware.ts\",\n    \"./method-override-middleware\": \"./src/method-override-middleware.ts\",\n    \"./mime\": \"./src/mime.ts\",\n    \"./multipart-parser\": \"./src/multipart-parser.ts\",\n    \"./multipart-parser/node\": \"./src/multipart-parser/node.ts\",\n    \"./node-fetch-server\": \"./src/node-fetch-server.ts\",\n    \"./response/compress\": \"./src/response/compress.ts\",\n    \"./response/file\": \"./src/response/file.ts\",\n    \"./response/html\": \"./src/response/html.ts\",\n    \"./response/redirect\": \"./src/response/redirect.ts\",\n    \"./route-pattern\": \"./src/route-pattern.ts\",\n    \"./route-pattern/specificity\": \"./src/route-pattern/specificity.ts\",\n    \"./session\": \"./src/session.ts\",\n    \"./session-middleware\": \"./src/session-middleware.ts\",\n    \"./session-storage-memcache\": \"./src/session-storage-memcache.ts\",\n    \"./session-storage-redis\": \"./src/session-storage-redis.ts\",\n    \"./session/cookie-storage\": \"./src/session/cookie-storage.ts\",\n    \"./session/fs-storage\": \"./src/session/fs-storage.ts\",\n    \"./session/memory-storage\": \"./src/session/memory-storage.ts\",\n    \"./static-middleware\": \"./src/static-middleware.ts\",\n    \"./tar-parser\": \"./src/tar-parser.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \"./async-context-middleware\": {\n        \"types\": \"./dist/async-context-middleware.d.ts\",\n        \"default\": \"./dist/async-context-middleware.js\"\n      },\n      \"./component\": {\n        \"types\": \"./dist/component.d.ts\",\n        \"default\": \"./dist/component.js\"\n      },\n      \"./component/jsx-dev-runtime\": {\n        \"types\": \"./dist/component/jsx-dev-runtime.d.ts\",\n        \"default\": \"./dist/component/jsx-dev-runtime.js\"\n      },\n      \"./component/jsx-runtime\": {\n        \"types\": \"./dist/component/jsx-runtime.d.ts\",\n        \"default\": \"./dist/component/jsx-runtime.js\"\n      },\n      \"./component/server\": {\n        \"types\": \"./dist/component/server.d.ts\",\n        \"default\": \"./dist/component/server.js\"\n      },\n      \"./compression-middleware\": {\n        \"types\": \"./dist/compression-middleware.d.ts\",\n        \"default\": \"./dist/compression-middleware.js\"\n      },\n      \"./cookie\": {\n        \"types\": \"./dist/cookie.d.ts\",\n        \"default\": \"./dist/cookie.js\"\n      },\n      \"./cop-middleware\": {\n        \"types\": \"./dist/cop-middleware.d.ts\",\n        \"default\": \"./dist/cop-middleware.js\"\n      },\n      \"./cors-middleware\": {\n        \"types\": \"./dist/cors-middleware.d.ts\",\n        \"default\": \"./dist/cors-middleware.js\"\n      },\n      \"./csrf-middleware\": {\n        \"types\": \"./dist/csrf-middleware.d.ts\",\n        \"default\": \"./dist/csrf-middleware.js\"\n      },\n      \"./data-schema\": {\n        \"types\": \"./dist/data-schema.d.ts\",\n        \"default\": \"./dist/data-schema.js\"\n      },\n      \"./data-schema/checks\": {\n        \"types\": \"./dist/data-schema/checks.d.ts\",\n        \"default\": \"./dist/data-schema/checks.js\"\n      },\n      \"./data-schema/coerce\": {\n        \"types\": \"./dist/data-schema/coerce.d.ts\",\n        \"default\": \"./dist/data-schema/coerce.js\"\n      },\n      \"./data-schema/form-data\": {\n        \"types\": \"./dist/data-schema/form-data.d.ts\",\n        \"default\": \"./dist/data-schema/form-data.js\"\n      },\n      \"./data-schema/lazy\": {\n        \"types\": \"./dist/data-schema/lazy.d.ts\",\n        \"default\": \"./dist/data-schema/lazy.js\"\n      },\n      \"./data-table\": {\n        \"types\": \"./dist/data-table.d.ts\",\n        \"default\": \"./dist/data-table.js\"\n      },\n      \"./data-table-mysql\": {\n        \"types\": \"./dist/data-table-mysql.d.ts\",\n        \"default\": \"./dist/data-table-mysql.js\"\n      },\n      \"./data-table-postgres\": {\n        \"types\": \"./dist/data-table-postgres.d.ts\",\n        \"default\": \"./dist/data-table-postgres.js\"\n      },\n      \"./data-table-sqlite\": {\n        \"types\": \"./dist/data-table-sqlite.d.ts\",\n        \"default\": \"./dist/data-table-sqlite.js\"\n      },\n      \"./data-table/migrations\": {\n        \"types\": \"./dist/data-table/migrations.d.ts\",\n        \"default\": \"./dist/data-table/migrations.js\"\n      },\n      \"./data-table/migrations/node\": {\n        \"types\": \"./dist/data-table/migrations/node.d.ts\",\n        \"default\": \"./dist/data-table/migrations/node.js\"\n      },\n      \"./data-table/operators\": {\n        \"types\": \"./dist/data-table/operators.d.ts\",\n        \"default\": \"./dist/data-table/operators.js\"\n      },\n      \"./data-table/sql-helpers\": {\n        \"types\": \"./dist/data-table/sql-helpers.d.ts\",\n        \"default\": \"./dist/data-table/sql-helpers.js\"\n      },\n      \"./fetch-proxy\": {\n        \"types\": \"./dist/fetch-proxy.d.ts\",\n        \"default\": \"./dist/fetch-proxy.js\"\n      },\n      \"./fetch-router\": {\n        \"types\": \"./dist/fetch-router.d.ts\",\n        \"default\": \"./dist/fetch-router.js\"\n      },\n      \"./fetch-router/routes\": {\n        \"types\": \"./dist/fetch-router/routes.d.ts\",\n        \"default\": \"./dist/fetch-router/routes.js\"\n      },\n      \"./file-storage\": {\n        \"types\": \"./dist/file-storage.d.ts\",\n        \"default\": \"./dist/file-storage.js\"\n      },\n      \"./file-storage-s3\": {\n        \"types\": \"./dist/file-storage-s3.d.ts\",\n        \"default\": \"./dist/file-storage-s3.js\"\n      },\n      \"./file-storage/fs\": {\n        \"types\": \"./dist/file-storage/fs.d.ts\",\n        \"default\": \"./dist/file-storage/fs.js\"\n      },\n      \"./file-storage/memory\": {\n        \"types\": \"./dist/file-storage/memory.d.ts\",\n        \"default\": \"./dist/file-storage/memory.js\"\n      },\n      \"./form-data-middleware\": {\n        \"types\": \"./dist/form-data-middleware.d.ts\",\n        \"default\": \"./dist/form-data-middleware.js\"\n      },\n      \"./form-data-parser\": {\n        \"types\": \"./dist/form-data-parser.d.ts\",\n        \"default\": \"./dist/form-data-parser.js\"\n      },\n      \"./fs\": {\n        \"types\": \"./dist/fs.d.ts\",\n        \"default\": \"./dist/fs.js\"\n      },\n      \"./headers\": {\n        \"types\": \"./dist/headers.d.ts\",\n        \"default\": \"./dist/headers.js\"\n      },\n      \"./html-template\": {\n        \"types\": \"./dist/html-template.d.ts\",\n        \"default\": \"./dist/html-template.js\"\n      },\n      \"./lazy-file\": {\n        \"types\": \"./dist/lazy-file.d.ts\",\n        \"default\": \"./dist/lazy-file.js\"\n      },\n      \"./logger-middleware\": {\n        \"types\": \"./dist/logger-middleware.d.ts\",\n        \"default\": \"./dist/logger-middleware.js\"\n      },\n      \"./method-override-middleware\": {\n        \"types\": \"./dist/method-override-middleware.d.ts\",\n        \"default\": \"./dist/method-override-middleware.js\"\n      },\n      \"./mime\": {\n        \"types\": \"./dist/mime.d.ts\",\n        \"default\": \"./dist/mime.js\"\n      },\n      \"./multipart-parser\": {\n        \"types\": \"./dist/multipart-parser.d.ts\",\n        \"default\": \"./dist/multipart-parser.js\"\n      },\n      \"./multipart-parser/node\": {\n        \"types\": \"./dist/multipart-parser/node.d.ts\",\n        \"default\": \"./dist/multipart-parser/node.js\"\n      },\n      \"./node-fetch-server\": {\n        \"types\": \"./dist/node-fetch-server.d.ts\",\n        \"default\": \"./dist/node-fetch-server.js\"\n      },\n      \"./response/compress\": {\n        \"types\": \"./dist/response/compress.d.ts\",\n        \"default\": \"./dist/response/compress.js\"\n      },\n      \"./response/file\": {\n        \"types\": \"./dist/response/file.d.ts\",\n        \"default\": \"./dist/response/file.js\"\n      },\n      \"./response/html\": {\n        \"types\": \"./dist/response/html.d.ts\",\n        \"default\": \"./dist/response/html.js\"\n      },\n      \"./response/redirect\": {\n        \"types\": \"./dist/response/redirect.d.ts\",\n        \"default\": \"./dist/response/redirect.js\"\n      },\n      \"./route-pattern\": {\n        \"types\": \"./dist/route-pattern.d.ts\",\n        \"default\": \"./dist/route-pattern.js\"\n      },\n      \"./route-pattern/specificity\": {\n        \"types\": \"./dist/route-pattern/specificity.d.ts\",\n        \"default\": \"./dist/route-pattern/specificity.js\"\n      },\n      \"./session\": {\n        \"types\": \"./dist/session.d.ts\",\n        \"default\": \"./dist/session.js\"\n      },\n      \"./session-middleware\": {\n        \"types\": \"./dist/session-middleware.d.ts\",\n        \"default\": \"./dist/session-middleware.js\"\n      },\n      \"./session-storage-memcache\": {\n        \"types\": \"./dist/session-storage-memcache.d.ts\",\n        \"default\": \"./dist/session-storage-memcache.js\"\n      },\n      \"./session-storage-redis\": {\n        \"types\": \"./dist/session-storage-redis.d.ts\",\n        \"default\": \"./dist/session-storage-redis.js\"\n      },\n      \"./session/cookie-storage\": {\n        \"types\": \"./dist/session/cookie-storage.d.ts\",\n        \"default\": \"./dist/session/cookie-storage.js\"\n      },\n      \"./session/fs-storage\": {\n        \"types\": \"./dist/session/fs-storage.d.ts\",\n        \"default\": \"./dist/session/fs-storage.js\"\n      },\n      \"./session/memory-storage\": {\n        \"types\": \"./dist/session/memory-storage.d.ts\",\n        \"default\": \"./dist/session/memory-storage.js\"\n      },\n      \"./static-middleware\": {\n        \"types\": \"./dist/static-middleware.d.ts\",\n        \"default\": \"./dist/static-middleware.js\"\n      },\n      \"./tar-parser\": {\n        \"types\": \"./dist/tar-parser.d.ts\",\n        \"default\": \"./dist/tar-parser.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/dom-navigation\": \"^1.0.7\",\n    \"@types/node\": \"catalog:\",\n    \"typescript\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/async-context-middleware\": \"workspace:^\",\n    \"@remix-run/component\": \"workspace:^\",\n    \"@remix-run/compression-middleware\": \"workspace:^\",\n    \"@remix-run/cors-middleware\": \"workspace:^\",\n    \"@remix-run/csrf-middleware\": \"workspace:^\",\n    \"@remix-run/cookie\": \"workspace:^\",\n    \"@remix-run/data-schema\": \"workspace:^\",\n    \"@remix-run/data-table\": \"workspace:^\",\n    \"@remix-run/data-table-mysql\": \"workspace:^\",\n    \"@remix-run/data-table-postgres\": \"workspace:^\",\n    \"@remix-run/data-table-sqlite\": \"workspace:^\",\n    \"@remix-run/fetch-proxy\": \"workspace:^\",\n    \"@remix-run/fetch-router\": \"workspace:^\",\n    \"@remix-run/file-storage\": \"workspace:^\",\n    \"@remix-run/file-storage-s3\": \"workspace:^\",\n    \"@remix-run/form-data-middleware\": \"workspace:^\",\n    \"@remix-run/form-data-parser\": \"workspace:^\",\n    \"@remix-run/fs\": \"workspace:^\",\n    \"@remix-run/headers\": \"workspace:^\",\n    \"@remix-run/html-template\": \"workspace:^\",\n    \"@remix-run/lazy-file\": \"workspace:^\",\n    \"@remix-run/logger-middleware\": \"workspace:^\",\n    \"@remix-run/method-override-middleware\": \"workspace:^\",\n    \"@remix-run/mime\": \"workspace:^\",\n    \"@remix-run/multipart-parser\": \"workspace:^\",\n    \"@remix-run/node-fetch-server\": \"workspace:^\",\n    \"@remix-run/response\": \"workspace:^\",\n    \"@remix-run/route-pattern\": \"workspace:^\",\n    \"@remix-run/session\": \"workspace:^\",\n    \"@remix-run/session-middleware\": \"workspace:^\",\n    \"@remix-run/session-storage-memcache\": \"workspace:^\",\n    \"@remix-run/session-storage-redis\": \"workspace:^\",\n    \"@remix-run/cop-middleware\": \"workspace:^\",\n    \"@remix-run/static-middleware\": \"workspace:^\",\n    \"@remix-run/tar-parser\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test './src/**/*.test.ts'\",\n    \"typecheck\": \"tsc --noEmit\"\n  }\n}\n"
  },
  {
    "path": "packages/remix/src/async-context-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/async-context-middleware'\n"
  },
  {
    "path": "packages/remix/src/component/jsx-dev-runtime.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/component/jsx-dev-runtime'\n"
  },
  {
    "path": "packages/remix/src/component/jsx-runtime.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/component/jsx-runtime'\n"
  },
  {
    "path": "packages/remix/src/component/server.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/component/server'\n"
  },
  {
    "path": "packages/remix/src/component.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/component'\n"
  },
  {
    "path": "packages/remix/src/compression-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/compression-middleware'\n"
  },
  {
    "path": "packages/remix/src/cookie.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/cookie'\n"
  },
  {
    "path": "packages/remix/src/cop-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/cop-middleware'\n"
  },
  {
    "path": "packages/remix/src/cors-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/cors-middleware'\n"
  },
  {
    "path": "packages/remix/src/csrf-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/csrf-middleware'\n"
  },
  {
    "path": "packages/remix/src/data-schema/checks.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-schema/checks'\n"
  },
  {
    "path": "packages/remix/src/data-schema/coerce.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-schema/coerce'\n"
  },
  {
    "path": "packages/remix/src/data-schema/form-data.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-schema/form-data'\n"
  },
  {
    "path": "packages/remix/src/data-schema/lazy.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-schema/lazy'\n"
  },
  {
    "path": "packages/remix/src/data-schema.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-schema'\n"
  },
  {
    "path": "packages/remix/src/data-table/migrations/node.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-table/migrations/node'\n"
  },
  {
    "path": "packages/remix/src/data-table/migrations.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-table/migrations'\n"
  },
  {
    "path": "packages/remix/src/data-table/operators.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-table/operators'\n"
  },
  {
    "path": "packages/remix/src/data-table/sql-helpers.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-table/sql-helpers'\n"
  },
  {
    "path": "packages/remix/src/data-table-mysql.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-table-mysql'\n"
  },
  {
    "path": "packages/remix/src/data-table-postgres.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-table-postgres'\n"
  },
  {
    "path": "packages/remix/src/data-table-sqlite.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-table-sqlite'\n"
  },
  {
    "path": "packages/remix/src/data-table.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/data-table'\n"
  },
  {
    "path": "packages/remix/src/fetch-proxy.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/fetch-proxy'\n"
  },
  {
    "path": "packages/remix/src/fetch-router/routes.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/fetch-router/routes'\n"
  },
  {
    "path": "packages/remix/src/fetch-router.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/fetch-router'\n"
  },
  {
    "path": "packages/remix/src/file-storage/fs.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/file-storage/fs'\n"
  },
  {
    "path": "packages/remix/src/file-storage/memory.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/file-storage/memory'\n"
  },
  {
    "path": "packages/remix/src/file-storage-s3.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/file-storage-s3'\n"
  },
  {
    "path": "packages/remix/src/file-storage.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport type * from '@remix-run/file-storage'\n"
  },
  {
    "path": "packages/remix/src/form-data-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/form-data-middleware'\n"
  },
  {
    "path": "packages/remix/src/form-data-parser.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/form-data-parser'\n"
  },
  {
    "path": "packages/remix/src/fs.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/fs'\n"
  },
  {
    "path": "packages/remix/src/headers.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/headers'\n"
  },
  {
    "path": "packages/remix/src/html-template.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/html-template'\n"
  },
  {
    "path": "packages/remix/src/lazy-file.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/lazy-file'\n"
  },
  {
    "path": "packages/remix/src/logger-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/logger-middleware'\n"
  },
  {
    "path": "packages/remix/src/method-override-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/method-override-middleware'\n"
  },
  {
    "path": "packages/remix/src/mime.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/mime'\n"
  },
  {
    "path": "packages/remix/src/multipart-parser/node.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/multipart-parser/node'\n"
  },
  {
    "path": "packages/remix/src/multipart-parser.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/multipart-parser'\n"
  },
  {
    "path": "packages/remix/src/node-fetch-server.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/node-fetch-server'\n"
  },
  {
    "path": "packages/remix/src/response/compress.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/response/compress'\n"
  },
  {
    "path": "packages/remix/src/response/file.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/response/file'\n"
  },
  {
    "path": "packages/remix/src/response/html.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/response/html'\n"
  },
  {
    "path": "packages/remix/src/response/redirect.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/response/redirect'\n"
  },
  {
    "path": "packages/remix/src/route-pattern/specificity.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/route-pattern/specificity'\n"
  },
  {
    "path": "packages/remix/src/route-pattern.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/route-pattern'\n"
  },
  {
    "path": "packages/remix/src/session/cookie-storage.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/session/cookie-storage'\n"
  },
  {
    "path": "packages/remix/src/session/fs-storage.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/session/fs-storage'\n"
  },
  {
    "path": "packages/remix/src/session/memory-storage.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/session/memory-storage'\n"
  },
  {
    "path": "packages/remix/src/session-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/session-middleware'\n"
  },
  {
    "path": "packages/remix/src/session-storage-memcache.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/session-storage-memcache'\n"
  },
  {
    "path": "packages/remix/src/session-storage-redis.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/session-storage-redis'\n"
  },
  {
    "path": "packages/remix/src/session.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/session'\n"
  },
  {
    "path": "packages/remix/src/static-middleware.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/static-middleware'\n"
  },
  {
    "path": "packages/remix/src/tar-parser.ts",
    "content": "// IMPORTANT: This file is auto-generated, please do not edit manually.\nexport * from '@remix-run/tar-parser'\n"
  },
  {
    "path": "packages/remix/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/remix/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\", \"DOM.AsyncIterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@remix-run/component\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/response/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/response/CHANGELOG.md",
    "content": "# `response` CHANGELOG\n\nThis is the changelog for [`response`](https://github.com/remix-run/remix/tree/main/packages/response). It follows [semantic versioning](https://semver.org/).\n\n## v0.3.2\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0)\n\n## v0.3.1\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.3.0\n\n### Minor Changes\n\n- `createFileResponse()` is now generic and accepts any file-like object\n\n  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`.\n\n  The generic type flows through to the `digest` callback in options, so you get the exact type you passed in:\n\n  ```ts\n  // With native File - digest receives File\n  createFileResponse(nativeFile, request, {\n    digest: async (file) => {\n      /* file is typed as File */\n    },\n  })\n\n  // With LazyFile - digest receives LazyFile\n  createFileResponse(lazyFile, request, {\n    digest: async (file) => {\n      /* file is typed as LazyFile */\n    },\n  })\n  ```\n\n- Add `redirect` export which is a shorthand alias for `createRedirectResponse`\n\n### Patch Changes\n\n- Update `@remix-run/headers` peer dependency to use the new header parsing methods.\n\n## v0.2.1 (2025-12-18)\n\n- `createFileResponse` now includes `charset` in Content-Type for text-based files.\n\n## v0.2.0 (2025-11-25)\n\n- 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.\n\n- Add `compressResponse` helper\n\n- 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.\n\n  To restore the previous behavior where all files support range requests:\n\n  ```ts\n  return createFileResponse(file, request, {\n    acceptRanges: true,\n  })\n  ```\n\n  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.\n\n## v0.1.0 (2025-11-25)\n\nInitial release with response helpers extracted from `@remix-run/fetch-router`.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/response/README.md) for more details.\n"
  },
  {
    "path": "packages/response/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/response/README.md",
    "content": "# response\n\nResponse helper utilities for the web Fetch API. `response` provides focused helpers for common HTTP responses with correct headers and caching semantics.\n\n## Features\n\n- **Web Standards Compliant:** Built on the standard `Response` API, works in any JavaScript runtime (Node.js, Bun, Deno, Cloudflare Workers)\n- [**File Responses:**](#file-responses) Full HTTP semantics including ETags, Last-Modified, conditional requests, and Range support\n- [**HTML Responses:**](#html-responses) Automatic DOCTYPE prepending and proper Content-Type headers\n- [**Redirect Responses:**](#redirect-responses) Simple redirect creation with customizable status codes\n- [**Compress Responses:**](#compress-responses) Streaming compression based on Accept-Encoding header\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nThis package provides no default export. Instead, import the specific helper you need:\n\n```ts\nimport { createFileResponse } from 'remix/response/file'\nimport { createHtmlResponse } from 'remix/response/html'\nimport { createRedirectResponse } from 'remix/response/redirect'\nimport { compressResponse } from 'remix/response/compress'\n```\n\n### File Responses\n\nThe `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`:\n\n```ts\nimport { createFileResponse } from 'remix/response/file'\nimport { openLazyFile } from 'remix/fs'\n\nlet lazyFile = openLazyFile('./public/image.jpg')\nlet response = await createFileResponse(lazyFile, request, {\n  cacheControl: 'public, max-age=3600',\n})\n```\n\n#### Features\n\n- **Content-Type** and **Content-Length** headers\n- **ETag** generation (weak or strong)\n- **Last-Modified** headers\n- **Cache-Control** headers\n- **Conditional requests** (`If-None-Match`, `If-Modified-Since`, `If-Match`, `If-Unmodified-Since`)\n- **Range requests** for partial content (`206 Partial Content`)\n- **HEAD** request support\n\n#### Options\n\n```ts\nawait createFileResponse(file, request, {\n  // Cache-Control header value.\n  // Defaults to `undefined` (no Cache-Control header).\n  cacheControl: 'public, max-age=3600',\n\n  // ETag generation strategy:\n  // - 'weak': Generates weak ETags based on file size and mtime (default)\n  // - 'strong': Generates strong ETags by hashing file content\n  // - false: Disables ETag generation\n  etag: 'weak',\n\n  // Hash algorithm for strong ETags (Web Crypto API algorithm names).\n  // Only used when etag: 'strong'.\n  // Defaults to 'SHA-256'.\n  digest: 'SHA-256',\n\n  // Whether to generate Last-Modified headers.\n  // Defaults to `true`.\n  lastModified: true,\n\n  // Whether to support HTTP Range requests for partial content.\n  // Defaults to `true`.\n  acceptRanges: true,\n})\n```\n\n#### Strong ETags and Content Hashing\n\nFor 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:\n\n```ts\nreturn createFileResponse(file, request, {\n  etag: 'strong',\n})\n```\n\nBy default, strong ETags are generated using the Web Crypto API with the `'SHA-256'` algorithm. You can customize this:\n\n```ts\nreturn createFileResponse(file, request, {\n  etag: 'strong',\n  // Specify a different hash algorithm\n  digest: 'SHA-512',\n})\n```\n\nFor large files or custom hashing requirements, provide a custom digest function:\n\n```ts\nawait createFileResponse(file, request, {\n  etag: 'strong',\n  async digest(file) {\n    // Custom streaming hash for large files\n    let { createHash } = await import('node:crypto')\n    let hash = createHash('sha256')\n    for await (let chunk of file.stream()) {\n      hash.update(chunk)\n    }\n    return hash.digest('hex')\n  },\n})\n```\n\n### HTML Responses\n\nThe `createHtmlResponse` helper creates HTML responses with proper `Content-Type` and DOCTYPE handling:\n\n```ts\nimport { createHtmlResponse } from 'remix/response/html'\n\nlet response = createHtmlResponse('<h1>Hello, World!</h1>')\n// Content-Type: text/html; charset=UTF-8\n// Body: <!DOCTYPE html><h1>Hello, World!</h1>\n```\n\nThe 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.\n\n```ts\nimport { html } from 'remix/html-template'\nimport { createHtmlResponse } from 'remix/response/html'\n\nlet name = '<script>alert(1)</script>'\nlet response = createHtmlResponse(html`<h1>Hello, ${name}!</h1>`)\n// Safely escaped HTML\n```\n\n### Redirect Responses\n\nThe `createRedirectResponse` helper creates redirect responses. The main improvements over the native `Response.redirect` API are:\n\n- 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.\n- Accepts a `ResponseInit` object as the second argument, allowing you to set additional headers and status code.\n\n```ts\nimport { createRedirectResponse } from 'remix/response/redirect'\n\n// Default 302 redirect\nlet response = createRedirectResponse('/login')\n\n// Custom status code\nlet response = createRedirectResponse('/new-page', 301)\n\n// With additional headers\nlet response = createRedirectResponse('/dashboard', {\n  status: 303,\n  headers: { 'X-Redirect-Reason': 'authentication' },\n})\n```\n\n### Compress Responses\n\nThe `compressResponse` helper compresses a `Response` based on the client's `Accept-Encoding` header:\n\n```ts\nimport { compressResponse } from 'remix/response/compress'\n\nlet response = new Response(JSON.stringify(data), {\n  headers: { 'Content-Type': 'application/json' },\n})\nlet compressed = await compressResponse(response, request)\n```\n\nCompression is automatically skipped for:\n\n- Responses with no `Accept-Encoding` header\n- Responses that are already compressed (existing `Content-Encoding`)\n- Responses with `Cache-Control: no-transform`\n- Responses with `Content-Length` below threshold (default: 1024 bytes)\n- Responses with range support (`Accept-Ranges: bytes`)\n- 206 Partial Content responses\n- HEAD requests (only headers are modified)\n\n#### Options\n\nThe `compressResponse` helper accepts options to customize compression behavior:\n\n```ts\nawait compressResponse(response, request, {\n  // Minimum size in bytes to compress (only enforced if Content-Length is present).\n  // Default: 1024\n  threshold: 1024,\n\n  // Which encodings the server supports for negotiation.\n  // Defaults to ['br', 'gzip', 'deflate']\n  encodings: ['br', 'gzip', 'deflate'],\n\n  // node:zlib options for gzip/deflate compression.\n  // For SSE responses (text/event-stream), flush: Z_SYNC_FLUSH\n  // is automatically applied unless you explicitly set a flush value.\n  // See: https://nodejs.org/api/zlib.html#class-options\n  zlib: {\n    level: 6,\n  },\n\n  // node:zlib options for Brotli compression.\n  // For SSE responses (text/event-stream), flush: BROTLI_OPERATION_FLUSH\n  // is automatically applied unless you explicitly set a flush value.\n  // See: https://nodejs.org/api/zlib.html#class-brotlioptions\n  brotli: {\n    params: {\n      [zlib.constants.BROTLI_PARAM_QUALITY]: 4,\n    },\n  },\n})\n```\n\n#### Range Requests and Compression\n\nRange 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.\n\n## Related Packages\n\n- [`@remix-run/headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - Type-safe HTTP header manipulation\n- [`@remix-run/html-template`](https://github.com/remix-run/remix/tree/main/packages/html-template) - Safe HTML templating with automatic escaping\n- [`@remix-run/fs`](https://github.com/remix-run/remix/tree/main/packages/fs) - File system utilities including `openFile`\n- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Build HTTP routers using the web fetch API\n- [`@remix-run/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) - MIME type utilities\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/response/package.json",
    "content": "{\n  \"name\": \"@remix-run/response\",\n  \"version\": \"0.3.2\",\n  \"description\": \"Response helpers for the web Fetch API\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/response\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/response#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \"./compress\": \"./src/compress.ts\",\n    \"./file\": \"./src/file.ts\",\n    \"./html\": \"./src/html.ts\",\n    \"./redirect\": \"./src/redirect.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \"./compress\": {\n        \"types\": \"./dist/compress.d.ts\",\n        \"default\": \"./dist/compress.js\"\n      },\n      \"./file\": {\n        \"types\": \"./dist/file.d.ts\",\n        \"default\": \"./dist/file.js\"\n      },\n      \"./html\": {\n        \"types\": \"./dist/html.d.ts\",\n        \"default\": \"./dist/html.js\"\n      },\n      \"./redirect\": {\n        \"types\": \"./dist/redirect.d.ts\",\n        \"default\": \"./dist/redirect.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/lazy-file\": \"workspace:*\",\n    \"@remix-run/mime\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/headers\": \"workspace:^\",\n    \"@remix-run/html-template\": \"workspace:^\",\n    \"@remix-run/mime\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"response\",\n    \"http\",\n    \"html\",\n    \"file\",\n    \"redirect\"\n  ]\n}\n"
  },
  {
    "path": "packages/response/src/compress.ts",
    "content": "export { compressResponse, type CompressResponseOptions, type Encoding } from './lib/compress.ts'\n"
  },
  {
    "path": "packages/response/src/file.ts",
    "content": "export {\n  createFileResponse,\n  type FileDigestFunction,\n  type FileResponseOptions,\n} from './lib/file.ts'\n"
  },
  {
    "path": "packages/response/src/html.ts",
    "content": "export { createHtmlResponse } from './lib/html.ts'\n"
  },
  {
    "path": "packages/response/src/lib/compress.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport {\n  gunzip,\n  brotliDecompress,\n  inflate,\n  constants,\n  createGunzip,\n  createBrotliDecompress,\n  createInflate,\n} from 'node:zlib'\nimport { promisify } from 'node:util'\nimport { Readable } from 'node:stream'\nimport { EventEmitter } from 'node:events'\nimport { describe, it } from 'node:test'\n\nimport { Vary } from '@remix-run/headers'\nimport { compressResponse, compressStream, type Encoding } from './compress.ts'\n\nconst isWindows = process.platform === 'win32'\n\nconst gunzipAsync = promisify(gunzip)\nconst brotliDecompressAsync = promisify(brotliDecompress)\nconst inflateAsync = promisify(inflate)\n\n// Type for mock compressors used in tests\ninterface MockCompressor extends EventEmitter {\n  write(chunk: Buffer, callback?: (error?: Error) => void): boolean\n  end(): void\n  destroy(error?: Error): void\n}\n\n// Helper to create mock compressors with required methods\nfunction createMockCompressor(impl: {\n  write: (chunk: Buffer, callback?: (error?: Error) => void) => boolean\n  end: () => void\n  destroy: (error?: Error) => void\n}): MockCompressor {\n  let emitter = new EventEmitter()\n  return Object.assign(emitter, impl) as MockCompressor\n}\n\ndescribe('compressResponse()', () => {\n  it('compresses response with gzip when client accepts it', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n    assert.equal(compressed.headers.get('Accept-Ranges'), 'none')\n    let vary = Vary.from(compressed.headers.get('vary'))\n    assert.ok(vary.has('Accept-Encoding'))\n\n    let buffer = Buffer.from(await compressed.arrayBuffer())\n    let decompressed = await gunzipAsync(buffer)\n    assert.equal(decompressed.toString(), 'Hello, World!')\n  })\n\n  it('compresses response with brotli when client prefers it', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'br, gzip' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'br')\n\n    let buffer = Buffer.from(await compressed.arrayBuffer())\n    let decompressed = await brotliDecompressAsync(buffer)\n    assert.equal(decompressed.toString(), 'Hello, World!')\n  })\n\n  it('compresses response with deflate', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'deflate' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request, {\n      encodings: ['deflate'],\n    })\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'deflate')\n    assert.equal(compressed.headers.get('Accept-Ranges'), 'none')\n    let vary = Vary.from(compressed.headers.get('vary'))\n    assert.ok(vary.has('Accept-Encoding'))\n\n    let buffer = Buffer.from(await compressed.arrayBuffer())\n    let decompressed = await inflateAsync(buffer)\n    assert.equal(decompressed.toString(), 'Hello, World!')\n  })\n\n  it('preserves existing Vary header values when adding Accept-Encoding', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Hello, World!', {\n      headers: {\n        Vary: 'Accept-Language, User-Agent',\n      },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    let varyHeader = compressed.headers.get('Vary') || ''\n    let varyValues = varyHeader\n      .toLowerCase()\n      .split(',')\n      .map((v) => v.trim())\n    assert.ok(varyValues.includes('accept-language'))\n    assert.ok(varyValues.includes('user-agent'))\n    assert.ok(varyValues.includes('accept-encoding'))\n  })\n\n  it('does not duplicate Accept-Encoding in Vary header', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Hello, World!', {\n      headers: {\n        Vary: 'Accept-Encoding, Accept-Language',\n      },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    let varyHeader = compressed.headers.get('Vary') || ''\n    let encodingMatches = varyHeader.match(/accept-encoding/gi) || []\n    assert.equal(encodingMatches.length, 1)\n  })\n\n  it('does not compress when client does not send Accept-Encoding', async () => {\n    let request = new Request('https://remix.run')\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request)\n\n    // Per RFC 7231, when no Accept-Encoding header is present,\n    // server should use identity (uncompressed) for compatibility\n    assert.equal(compressed.headers.get('Content-Encoding'), null)\n    assert.equal(await compressed.text(), 'Hello, World!')\n  })\n\n  it('compresses responses when Content-Length is not set', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    // Response without Content-Length header\n    let response = new Response('Small')\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n  })\n\n  it('skips compression when Content-Length is below threshold', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Small', {\n      headers: { 'Content-Length': '5' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Content-Encoding'), null)\n    assert.equal(await compressed.text(), 'Small')\n  })\n\n  it('respects custom threshold option', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Small', {\n      headers: { 'Content-Length': '5' },\n    })\n\n    let compressed = await compressResponse(response, request, { threshold: 3 })\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n  })\n\n  it('skips compression for already compressed responses', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Already compressed', {\n      headers: { 'Content-Encoding': 'gzip' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed, response)\n  })\n\n  it('skips compression when Cache-Control: no-transform is present', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Do not transform', {\n      headers: { 'Cache-Control': 'public, no-transform, max-age=3600' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed, response)\n  })\n\n  it('skips compression when response has no body', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response(null, { status: 204 })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed, response)\n  })\n\n  it('compresses with custom compression level', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    // Use a large, repetitive string where compression level makes a difference\n    let content = 'Hello, World!'.repeat(1000)\n\n    // Compress with level 1 (fast, less compression)\n    let level1 = await compressResponse(new Response(content), request, {\n      zlib: { level: 1 },\n    })\n    let level1Buffer = Buffer.from(await level1.arrayBuffer())\n\n    // Compress with level 9 (slow, max compression)\n    let level9 = await compressResponse(new Response(content), request, {\n      zlib: { level: 9 },\n    })\n    let level9Buffer = Buffer.from(await level9.arrayBuffer())\n\n    // Verify both are valid gzip\n    assert.equal(level1.headers.get('Content-Encoding'), 'gzip')\n    assert.equal(level9.headers.get('Content-Encoding'), 'gzip')\n\n    // Verify both decompress correctly\n    let decompressed1 = await gunzipAsync(level1Buffer)\n    let decompressed9 = await gunzipAsync(level9Buffer)\n    assert.equal(decompressed1.toString(), content)\n    assert.equal(decompressed9.toString(), content)\n\n    // Level 9 should produce smaller output than level 1\n    assert.ok(level9Buffer.length < level1Buffer.length)\n  })\n\n  it('compresses with custom brotli options', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'br' },\n    })\n    // Use repetitive content where window size affects compression ratio\n    let content = 'Hello, World! '.repeat(10000)\n\n    // Compress with window size 10 (small window)\n    let windowSmall = await compressResponse(new Response(content), request, {\n      encodings: ['br'],\n      brotli: {\n        params: {\n          [constants.BROTLI_PARAM_LGWIN]: 10,\n        },\n      },\n    })\n    let windowSmallBuffer = Buffer.from(await windowSmall.arrayBuffer())\n\n    // Compress with window size 22 (large window, better for repetitive data)\n    let windowLarge = await compressResponse(new Response(content), request, {\n      encodings: ['br'],\n      brotli: {\n        params: {\n          [constants.BROTLI_PARAM_LGWIN]: 22,\n        },\n      },\n    })\n    let windowLargeBuffer = Buffer.from(await windowLarge.arrayBuffer())\n\n    // Verify both are valid brotli\n    assert.equal(windowSmall.headers.get('Content-Encoding'), 'br')\n    assert.equal(windowLarge.headers.get('Content-Encoding'), 'br')\n\n    // Verify both decompress correctly\n    let decompressedSmall = await brotliDecompressAsync(windowSmallBuffer)\n    let decompressedLarge = await brotliDecompressAsync(windowLargeBuffer)\n    assert.equal(decompressedSmall.toString(), content)\n    assert.equal(decompressedLarge.toString(), content)\n\n    // Different window sizes should produce different compressed output\n    // (if options aren't passed through, both would use the same default window)\n    assert.notEqual(windowSmallBuffer.length, windowLargeBuffer.length)\n  })\n\n  it('limits to specified encodings', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'br, gzip, deflate' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request, {\n      encodings: ['gzip', 'deflate'],\n    })\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n  })\n\n  it('returns uncompressed when encodings is empty array', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip, br' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request, { encodings: [] })\n\n    assert.equal(compressed, response)\n    assert.equal(compressed.headers.get('Content-Encoding'), null)\n  })\n\n  it('handles quality factors in Accept-Encoding', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip;q=0.8, deflate;q=1.0' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'deflate')\n  })\n\n  it('client quality factors override server preference order', async () => {\n    let request = new Request('https://remix.run', {\n      // Client strongly prefers gzip over br\n      headers: { 'Accept-Encoding': 'br;q=0.5, gzip;q=1.0, deflate;q=0.8' },\n    })\n    let response = new Response('Hello, World!')\n\n    // Server prefers br > gzip > deflate, but client q-values should win\n    let compressed = await compressResponse(response, request, {\n      encodings: ['br', 'gzip', 'deflate'],\n    })\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n  })\n\n  it('server preference order breaks ties when client has equal quality factors', async () => {\n    let request = new Request('https://remix.run', {\n      // Client has no preference (all default to q=1.0)\n      headers: { 'Accept-Encoding': 'gzip, deflate, br' },\n    })\n    let response = new Response('Hello, World!')\n\n    // Server prefers deflate first, so it should win the tie\n    let compressed = await compressResponse(response, request, {\n      encodings: ['deflate', 'br', 'gzip'],\n    })\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'deflate')\n  })\n\n  it('respects explicit rejection with q=0', async () => {\n    let request = new Request('https://remix.run', {\n      // Client explicitly rejects gzip and deflate with q=0\n      headers: { 'Accept-Encoding': 'gzip;q=0, deflate;q=0, br;q=1.0' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request, {\n      encodings: ['gzip', 'deflate', 'br'],\n    })\n\n    // Should use br since others are explicitly rejected\n    assert.equal(compressed.headers.get('Content-Encoding'), 'br')\n  })\n\n  it('returns uncompressed when all encodings are rejected but identity is acceptable', async () => {\n    let request = new Request('https://remix.run', {\n      // Client explicitly rejects all compression but accepts identity (default q=1.0)\n      headers: { 'Accept-Encoding': 'gzip;q=0, deflate;q=0, br;q=0, identity' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request, {\n      encodings: ['gzip', 'deflate', 'br'],\n    })\n\n    // Should return identity (no compression)\n    assert.equal(compressed.headers.get('Content-Encoding'), null)\n    assert.equal(compressed, response) // Should be the same response object\n  })\n\n  it('requires compression when identity is explicitly rejected', async () => {\n    let request = new Request('https://remix.run', {\n      // Client rejects uncompressed but accepts gzip\n      headers: { 'Accept-Encoding': 'identity;q=0, gzip' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request)\n\n    // Must compress since identity is rejected\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n  })\n\n  it('returns 406 when all encodings including identity are rejected', async () => {\n    let request = new Request('https://remix.run', {\n      // Client rejects everything including identity\n      headers: { 'Accept-Encoding': 'gzip;q=0, deflate;q=0, br;q=0, identity;q=0' },\n    })\n    let response = new Response('Hello, World!')\n\n    let result = await compressResponse(response, request, {\n      encodings: ['gzip', 'deflate', 'br'],\n    })\n\n    // Should return 406 Not Acceptable per RFC 7231\n    assert.equal(result.status, 406)\n    assert.equal(result.statusText, 'Not Acceptable')\n  })\n\n  it('handles wildcard with quality factor', async () => {\n    let request = new Request('https://remix.run', {\n      // Client accepts gzip preferentially, but any other encoding at q=0.5\n      headers: { 'Accept-Encoding': 'gzip, *;q=0.5' },\n    })\n    let response = new Response('Hello, World!')\n\n    // Server offers br, which should match the wildcard\n    let compressed = await compressResponse(response, request, {\n      encodings: ['br', 'deflate'], // No gzip offered\n    })\n\n    // Should use br (matches wildcard with q=0.5)\n    assert.equal(compressed.headers.get('Content-Encoding'), 'br')\n  })\n\n  it('prefers explicit encoding over wildcard', async () => {\n    let request = new Request('https://remix.run', {\n      // Explicit gzip (q=1.0) vs wildcard (q=0.8)\n      headers: { 'Accept-Encoding': 'gzip, *;q=0.8' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request, {\n      encodings: ['br', 'gzip', 'deflate'],\n    })\n\n    // Should prefer explicit gzip (q=1.0) over br matched by wildcard (q=0.8)\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n  })\n\n  it('respects wildcard rejection with q=0', async () => {\n    let request = new Request('https://remix.run', {\n      // Only accepts gzip explicitly, rejects all others including identity with *;q=0\n      headers: { 'Accept-Encoding': 'gzip, *;q=0' },\n    })\n    let response = new Response('Hello, World!')\n\n    // Server tries to offer br which matches the rejected wildcard\n    let result = await compressResponse(response, request, {\n      encodings: ['br', 'deflate'], // No gzip offered\n    })\n\n    // Wildcard *;q=0 also rejects identity, so should return 406 per RFC 7231\n    assert.equal(result.status, 406)\n    assert.equal(result.statusText, 'Not Acceptable')\n  })\n\n  it('allows identity when wildcard rejects compression but identity is explicit', async () => {\n    let request = new Request('https://remix.run', {\n      // Reject compression but explicitly allow identity\n      headers: { 'Accept-Encoding': 'identity, *;q=0' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request, {\n      encodings: ['gzip', 'deflate', 'br'],\n    })\n\n    // Should return uncompressed since identity is explicitly acceptable\n    assert.equal(compressed.headers.get('Content-Encoding'), null)\n    assert.equal(compressed, response)\n  })\n\n  it('handles wildcard with identity rejection', async () => {\n    let request = new Request('https://remix.run', {\n      // Accepts any compression but rejects identity\n      headers: { 'Accept-Encoding': '*;q=1.0, identity;q=0' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request, {\n      encodings: ['gzip', 'deflate', 'br'],\n    })\n\n    // Must compress since identity is rejected (should use first supported: gzip)\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n  })\n\n  it('preserves response status and statusText', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Created', {\n      status: 201,\n      statusText: 'Created',\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.status, 201)\n    assert.equal(compressed.statusText, 'Created')\n  })\n\n  it('removes Content-Length header', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let largeContent = 'Hello, World!'.repeat(100) // Make it larger than threshold\n    let response = new Response(largeContent, {\n      headers: { 'Content-Length': String(largeContent.length) },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Content-Length'), null)\n  })\n\n  it('sets Accept-Ranges to none', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Accept-Ranges'), 'none')\n  })\n\n  it('converts strong ETags to weak', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Hello, World!', {\n      headers: { ETag: '\"abc123\"' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('ETag'), 'W/\"abc123\"')\n  })\n\n  it('preserves weak ETags', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Hello, World!', {\n      headers: { ETag: 'W/\"abc123\"' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('ETag'), 'W/\"abc123\"')\n  })\n\n  it('does not add ETag when not present', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Hello, World!')\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('ETag'), null)\n  })\n\n  it('converts strong ETags to weak for HEAD requests', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'HEAD',\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let content = 'x'.repeat(2000)\n    let response = new Response(content, {\n      headers: {\n        'Content-Type': 'text/plain',\n        ETag: '\"xyz789\"',\n      },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('ETag'), 'W/\"xyz789\"')\n  })\n\n  it('skips responses with Accept-Ranges: bytes', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Hello, World!', {\n      headers: { 'Accept-Ranges': 'bytes' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed, response)\n    assert.equal(compressed.headers.get('Content-Encoding'), null)\n  })\n\n  it('skips 206 partial content responses', async () => {\n    let request = new Request('https://remix.run', {\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    let response = new Response('Partial content', {\n      status: 206,\n      headers: { 'Content-Range': 'bytes 0-9/100' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed, response)\n    assert.equal(compressed.headers.get('Content-Encoding'), null)\n  })\n\n  it('sets compression headers for HEAD requests without compressing', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'HEAD',\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    // Content larger than default threshold (1024 bytes)\n    let content = 'x'.repeat(2000)\n    let response = new Response(content, {\n      headers: { 'Content-Type': 'text/plain' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n    assert.equal(compressed.headers.get('Accept-Ranges'), 'none')\n    assert.equal(compressed.headers.get('Content-Length'), null)\n    let vary = Vary.from(compressed.headers.get('vary'))\n    assert.ok(vary.has('Accept-Encoding'))\n    assert.equal(compressed.body, null)\n  })\n\n  it('negotiates encoding for HEAD requests', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'HEAD',\n      headers: { 'Accept-Encoding': 'br' },\n    })\n    let content = 'x'.repeat(2000)\n    let response = new Response(content, {\n      headers: { 'Content-Type': 'text/plain' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'br')\n  })\n\n  it('returns identity for HEAD when client does not accept compression', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'HEAD',\n    })\n    let content = 'x'.repeat(2000)\n    let response = new Response(content, {\n      headers: { 'Content-Type': 'text/plain' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed, response)\n    assert.equal(compressed.headers.get('Content-Encoding'), null)\n  })\n\n  it('sets compression headers for HEAD requests even when body is already null', async () => {\n    let request = new Request('https://remix.run', {\n      method: 'HEAD',\n      headers: { 'Accept-Encoding': 'gzip' },\n    })\n    // Response with body already removed (common pattern at route level)\n    let response = new Response(null, {\n      headers: { 'Content-Type': 'text/plain', 'Content-Length': '2000' },\n    })\n\n    let compressed = await compressResponse(response, request)\n\n    assert.equal(compressed.headers.get('Content-Encoding'), 'gzip')\n    assert.equal(compressed.headers.get('Accept-Ranges'), 'none')\n    assert.equal(compressed.headers.get('Content-Length'), null)\n    let vary = Vary.from(compressed.headers.get('vary'))\n    assert.ok(vary.has('Accept-Encoding'))\n    assert.equal(compressed.body, null)\n  })\n\n  describe('Server-Sent Events', () => {\n    async function testSSEFlush(\n      encodingName: Encoding,\n      createDecompressor: () => ReturnType<\n        typeof createBrotliDecompress | typeof createGunzip | typeof createInflate\n      >,\n    ) {\n      let sendEvent: ((data: string) => void) | undefined\n      let controller: ReadableStreamDefaultController<Uint8Array> | undefined\n\n      let stream = new ReadableStream({\n        start(c) {\n          controller = c\n          sendEvent = (data: string) => {\n            controller!.enqueue(new TextEncoder().encode(data))\n          }\n        },\n      })\n\n      let response = new Response(stream, {\n        headers: { 'Content-Type': 'text/event-stream' },\n      })\n\n      let request = new Request('https://remix.run', {\n        headers: { 'Accept-Encoding': encodingName },\n      })\n\n      let compressed = await compressResponse(response, request, {\n        encodings: [encodingName],\n        // Provide custom options WITHOUT flush\n        // compressResponse() should automatically apply flush for SSE\n        zlib: {\n          level: 9,\n        },\n        brotli: {\n          params: {\n            [constants.BROTLI_PARAM_QUALITY]: 11,\n          },\n        },\n      })\n\n      assert.equal(compressed.headers.get('Content-Encoding'), encodingName)\n      assert.ok(compressed.body)\n\n      let decompressor = createDecompressor()\n      let nodeReadable = Readable.fromWeb(compressed.body as any)\n      let decompressed = nodeReadable.pipe(decompressor)\n\n      // Test that data arrives before stream closes AND is valid SSE format\n      let receivedData = await new Promise<string>((resolve, reject) => {\n        let timeout = setTimeout(\n          () => {\n            reject(new Error(`Timeout: data not flushed - flush may not be working`))\n          },\n          isWindows ? 2_000 : 500,\n        )\n\n        decompressed.once('data', (chunk) => {\n          clearTimeout(timeout)\n          resolve(chunk.toString())\n        })\n\n        decompressed.resume()\n\n        // Send SSE event - with flush, it should arrive immediately\n        // Without flush, stream stays open and data buffers, causing timeout\n        setImmediate(() => {\n          sendEvent!('event: message\\ndata: test-payload\\n\\n')\n        })\n      })\n\n      // Verify the decompressed data is valid SSE format\n      assert.ok(receivedData.includes('event: message'), 'Missing event type')\n      assert.ok(receivedData.includes('data: test-payload'), 'Missing data payload')\n      assert.ok(receivedData.includes('\\n\\n'), 'Missing SSE message terminator')\n\n      controller!.close()\n      decompressed.destroy()\n    }\n\n    it('automatically applies flush for SSE with br', async () => {\n      await testSSEFlush('br', createBrotliDecompress)\n    })\n\n    it('automatically applies flush for SSE with gzip', async () => {\n      await testSSEFlush('gzip', createGunzip)\n    })\n\n    it('automatically applies flush for SSE with deflate', async () => {\n      await testSSEFlush('deflate', createInflate)\n    })\n  })\n\n  describe('Streaming compression', () => {\n    /**\n     * Helper: Create a ReadableStream from a string\n     */\n    function createStreamFromString(content: string): ReadableStream<Uint8Array> {\n      return new ReadableStream({\n        start(controller) {\n          controller.enqueue(new TextEncoder().encode(content))\n          controller.close()\n        },\n      })\n    }\n\n    /**\n     * Helper: Create a ReadableStream that emits chunks\n     */\n    function createChunkedStream(chunks: string[]): ReadableStream<Uint8Array> {\n      return new ReadableStream({\n        start(controller) {\n          for (let chunk of chunks) {\n            controller.enqueue(new TextEncoder().encode(chunk))\n          }\n          controller.close()\n        },\n      })\n    }\n\n    /**\n     * Helper: Create a ReadableStream that errors\n     */\n    function createErrorStream(errorAfterChunks: number): ReadableStream<Uint8Array> {\n      return new ReadableStream({\n        start(controller) {\n          for (let i = 0; i < errorAfterChunks; i++) {\n            controller.enqueue(new TextEncoder().encode('chunk'))\n          }\n          controller.error(new Error('Stream error'))\n        },\n      })\n    }\n\n    /**\n     * Helper: Read entire stream to string\n     */\n    async function streamToString(stream: ReadableStream<Uint8Array>): Promise<string> {\n      let reader = stream.getReader()\n      let chunks: Uint8Array[] = []\n\n      while (true) {\n        let { done, value } = await reader.read()\n        if (done) break\n        if (value) chunks.push(value)\n      }\n\n      let concatenated = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0))\n      let offset = 0\n      for (let chunk of chunks) {\n        concatenated.set(chunk, offset)\n        offset += chunk.length\n      }\n\n      return new TextDecoder().decode(concatenated)\n    }\n\n    /**\n     * Helper: Decompress a web stream using node:zlib\n     */\n    function getDecompressor(encoding: Encoding) {\n      switch (encoding) {\n        case 'gzip':\n          return createGunzip()\n        case 'deflate':\n          return createInflate()\n        case 'br':\n          return createBrotliDecompress()\n        default:\n          throw new Error(`Unsupported encoding: ${encoding}`)\n      }\n    }\n\n    function decompressStream(\n      compressed: ReadableStream<Uint8Array>,\n      encoding: Encoding,\n    ): ReadableStream<Uint8Array> {\n      let decompressor = getDecompressor(encoding)\n\n      return new ReadableStream({\n        async start(controller) {\n          decompressor.on('data', (chunk: Buffer) => {\n            controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength))\n          })\n\n          decompressor.on('end', () => {\n            controller.close()\n          })\n\n          decompressor.on('error', (error: Error) => {\n            controller.error(error)\n          })\n\n          let reader = compressed.getReader()\n\n          try {\n            while (true) {\n              let { done, value } = await reader.read()\n\n              if (done) {\n                decompressor.end()\n                break\n              }\n\n              if (!value) {\n                continue\n              }\n\n              decompressor.write(Buffer.from(value))\n            }\n          } catch (error) {\n            decompressor.destroy(error as Error)\n          }\n        },\n      })\n    }\n\n    /**\n     * Helper: Compress and decompress round-trip\n     */\n    async function roundTrip(input: string, encoding: Encoding): Promise<string> {\n      let request = new Request('https://remix.run', {\n        headers: {\n          'Accept-Encoding': encoding,\n        },\n      })\n\n      let response = new Response(createStreamFromString(input), {\n        headers: {\n          'Content-Type': 'text/plain',\n        },\n      })\n\n      let compressed = await compressResponse(response, request, { encodings: [encoding] })\n\n      assert.equal(compressed.headers.get('Content-Encoding'), encoding)\n\n      let decompressed = decompressStream(compressed.body!, encoding)\n      return await streamToString(decompressed)\n    }\n\n    describe('correctness (round-trip compression)', () => {\n      it('handles binary data byte-perfectly', async () => {\n        let request = new Request('https://remix.run', {\n          headers: { 'Accept-Encoding': 'br' },\n        })\n\n        // Create binary data with all byte values (0-255)\n        let binaryData = new Uint8Array(256)\n        for (let i = 0; i < 256; i++) {\n          binaryData[i] = i\n        }\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(binaryData)\n            controller.close()\n          },\n        })\n\n        let response = new Response(stream, {\n          headers: { 'Content-Type': 'application/octet-stream' },\n        })\n\n        let compressed = await compressResponse(response, request, { encodings: ['br'] })\n\n        // Decompress and verify byte-perfect match\n        let decompressed = decompressStream(compressed.body!, 'br')\n        let chunks: Uint8Array[] = []\n        let reader = decompressed.getReader()\n\n        while (true) {\n          let { done, value } = await reader.read()\n          if (done) break\n          if (value) chunks.push(value)\n        }\n\n        let result = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0))\n        let offset = 0\n        for (let chunk of chunks) {\n          result.set(chunk, offset)\n          offset += chunk.length\n        }\n\n        assert.equal(result.length, binaryData.length)\n        for (let i = 0; i < result.length; i++) {\n          assert.equal(result[i], binaryData[i], `Byte mismatch at index ${i}`)\n        }\n      })\n\n      describe('gzip', () => {\n        it('compresses and decompresses simple text', async () => {\n          let result = await roundTrip('hello world', 'gzip')\n          assert.equal(result, 'hello world')\n        })\n\n        it('compresses and decompresses empty string', async () => {\n          let result = await roundTrip('', 'gzip')\n          assert.equal(result, '')\n        })\n\n        it('compresses and decompresses unicode', async () => {\n          let unicode = 'Hello 世界 🌍 émoji'\n          let result = await roundTrip(unicode, 'gzip')\n          assert.equal(result, unicode)\n        })\n      })\n\n      describe('deflate', () => {\n        it('compresses and decompresses simple text', async () => {\n          let result = await roundTrip('hello world', 'deflate')\n          assert.equal(result, 'hello world')\n        })\n\n        it('compresses and decompresses empty string', async () => {\n          let result = await roundTrip('', 'deflate')\n          assert.equal(result, '')\n        })\n\n        it('compresses and decompresses unicode', async () => {\n          let unicode = 'Hello 世界 🌍 émoji'\n          let result = await roundTrip(unicode, 'deflate')\n          assert.equal(result, unicode)\n        })\n      })\n\n      describe('br', () => {\n        it('compresses and decompresses simple text', async () => {\n          let result = await roundTrip('hello world', 'br')\n          assert.equal(result, 'hello world')\n        })\n\n        it('compresses and decompresses empty string', async () => {\n          let result = await roundTrip('', 'br')\n          assert.equal(result, '')\n        })\n\n        it('compresses and decompresses unicode', async () => {\n          let unicode = 'Hello 世界 🌍 émoji'\n          let result = await roundTrip(unicode, 'br')\n          assert.equal(result, unicode)\n        })\n      })\n    })\n\n    describe('chunk handling', () => {\n      it('handles multiple chunks correctly', async () => {\n        let request = new Request('https://remix.run', {\n          headers: { 'Accept-Encoding': 'br' },\n        })\n\n        let response = new Response(createChunkedStream(['hello', ' ', 'world']), {\n          headers: { 'Content-Type': 'text/plain' },\n        })\n\n        let compressed = await compressResponse(response, request, { encodings: ['br'] })\n        let decompressed = decompressStream(compressed.body!, 'br')\n        let result = await streamToString(decompressed)\n\n        assert.equal(result, 'hello world')\n      })\n\n      it('handles single-byte chunks', async () => {\n        let bytes = ['h', 'e', 'l', 'l', 'o']\n\n        let request = new Request('https://remix.run', {\n          headers: { 'Accept-Encoding': 'br' },\n        })\n\n        let response = new Response(createChunkedStream(bytes), {\n          headers: { 'Content-Type': 'text/plain' },\n        })\n\n        let compressed = await compressResponse(response, request, { encodings: ['br'] })\n        let decompressed = decompressStream(compressed.body!, 'br')\n        let result = await streamToString(decompressed)\n\n        assert.equal(result, 'hello')\n      })\n\n      it('handles empty chunks in stream', async () => {\n        let request = new Request('https://remix.run', {\n          headers: { 'Accept-Encoding': 'br' },\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new Uint8Array(0)) // Empty chunk\n            controller.enqueue(new TextEncoder().encode('hello '))\n            controller.enqueue(new Uint8Array(0)) // Another empty chunk\n            controller.enqueue(new TextEncoder().encode('world'))\n            controller.close()\n          },\n        })\n\n        let response = new Response(stream, {\n          headers: { 'Content-Type': 'text/plain' },\n        })\n\n        let compressed = await compressResponse(response, request, { encodings: ['br'] })\n        let decompressed = decompressStream(compressed.body!, 'br')\n        let result = await streamToString(decompressed)\n\n        assert.equal(result, 'hello world')\n      })\n\n      it('handles stream that closes immediately without data', async () => {\n        let request = new Request('https://remix.run', {\n          headers: { 'Accept-Encoding': 'br' },\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.close() // Close immediately without writing\n          },\n        })\n\n        let response = new Response(stream, {\n          headers: { 'Content-Type': 'text/plain' },\n        })\n\n        let compressed = await compressResponse(response, request, { encodings: ['br'] })\n        let decompressed = decompressStream(compressed.body!, 'br')\n        let result = await streamToString(decompressed)\n\n        assert.equal(result, '')\n      })\n    })\n\n    it('propagates errors from input stream', async () => {\n      let request = new Request('https://remix.run', {\n        headers: { 'Accept-Encoding': 'br' },\n      })\n\n      let response = new Response(createErrorStream(2), {\n        headers: { 'Content-Type': 'text/plain' },\n      })\n\n      let compressed = await compressResponse(response, request, { encodings: ['br'] })\n\n      let reader = compressed.body!.getReader()\n\n      await assert.rejects(\n        async () => {\n          while (true) {\n            let { done } = await reader.read()\n            if (done) break\n          }\n        },\n        {\n          message: 'Stream error',\n        },\n      )\n    })\n\n    it('handles output stream cancellation and stops processing source stream', async () => {\n      let request = new Request('https://remix.run', {\n        headers: { 'Accept-Encoding': 'br' },\n      })\n\n      let streamCancelled = false\n      let cancelReason: string | undefined\n\n      let stream = new ReadableStream({\n        async pull(controller) {\n          controller.enqueue(new TextEncoder().encode('chunk\\n'.repeat(100)))\n          await new Promise((resolve) => setTimeout(resolve, 10))\n        },\n        cancel(reason) {\n          streamCancelled = true\n          cancelReason = reason\n        },\n      })\n\n      let response = new Response(stream, {\n        headers: { 'Content-Type': 'text/plain' },\n      })\n\n      let compressed = await compressResponse(response, request, { encodings: ['br'] })\n      let reader = compressed.body!.getReader()\n\n      // Start reading to activate the stream\n      reader.read()\n\n      // Wait for streaming to start\n      await new Promise((resolve) => setTimeout(resolve, 30))\n\n      // Cancel the output stream\n      await reader.cancel('User cancelled')\n\n      // Wait for cancellation to propagate\n      await new Promise((resolve) => setTimeout(resolve, 100))\n\n      // Verify source stream was cancelled\n      assert.equal(streamCancelled, true, 'Source stream should be cancelled')\n      assert.equal(cancelReason, 'User cancelled', 'Cancel reason should be passed through')\n    })\n\n    describe('Compressor interactions', () => {\n      it('ignores data events emitted after error', async () => {\n        let errorEmitted = false\n\n        let mockCompressor = createMockCompressor({\n          write: (chunk) => {\n            // Emit error, then try to emit data (should be ignored)\n            setImmediate(() => {\n              mockCompressor.emit('error', new Error('Compressor failed'))\n              errorEmitted = true\n              // Try to emit data after error (should be ignored)\n              mockCompressor.emit('data', chunk)\n            })\n            // Don't call callback - error will reject the Promise\n            return false\n          },\n          end: () => {},\n          destroy: () => {},\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('test'))\n            controller.close()\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n        let chunks: Uint8Array[] = []\n\n        // Should get error, not the data chunk\n        await assert.rejects(\n          async () => {\n            while (true) {\n              let result = await reader.read()\n              if (result.done) break\n              chunks.push(result.value)\n            }\n          },\n          {\n            message: 'Compressor failed',\n          },\n        )\n\n        // Wait for any pending microtasks\n        await Promise.resolve()\n\n        // Should not have received the data chunk emitted after error\n        assert.equal(chunks.length, 0, 'Should not receive data chunks after error')\n        assert.equal(errorEmitted, true, 'Error should have been emitted')\n      })\n\n      it('ignores data events emitted after cancellation', async () => {\n        let dataAfterCancel = false\n        let lastChunk: Buffer\n\n        let mockCompressor = createMockCompressor({\n          write: (chunk) => {\n            // Store the chunk for later emission\n            lastChunk = chunk\n            // Signal backpressure but never call callback or emit drain\n            // This will cause the write to hang\n            return false\n          },\n          end: () => {},\n          destroy: () => {\n            mockCompressor.emit('data', lastChunk)\n            dataAfterCancel = true\n          },\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('test'))\n            controller.close()\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n        let chunks: Uint8Array[] = []\n\n        // Start reading (will block on backpressure)\n        let readPromise = reader.read().then((result) => {\n          if (!result.done) chunks.push(result.value)\n        })\n\n        // Cancel while blocked\n        await Promise.resolve()\n        await reader.cancel()\n\n        await readPromise.catch(() => {\n          // May error or not, doesn't matter\n        })\n\n        // Wait for any pending microtasks\n        await Promise.resolve()\n\n        assert.equal(chunks.length, 0, 'Should not receive data chunks after cancel')\n        assert.equal(dataAfterCancel, true, 'Data should have been emitted by mock')\n      })\n\n      it('handles multiple data chunks emitted during single write', async () => {\n        let mockCompressor = createMockCompressor({\n          write: (chunk, callback) => {\n            // Real zlib can emit multiple data chunks for one write\n            // Emit the chunk split into parts\n            let length = chunk.length\n            let part1 = chunk.subarray(0, Math.floor(length / 3))\n            let part2 = chunk.subarray(Math.floor(length / 3), Math.floor((length * 2) / 3))\n            let part3 = chunk.subarray(Math.floor((length * 2) / 3))\n            mockCompressor.emit('data', part1)\n            mockCompressor.emit('data', part2)\n            mockCompressor.emit('data', part3)\n            if (callback) setImmediate(callback)\n            return true\n          },\n          end: () => {\n            mockCompressor.emit('end')\n          },\n          destroy: () => {},\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('input'))\n            controller.close()\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n        let chunks: Uint8Array[] = []\n\n        while (true) {\n          let result = await reader.read()\n          if (result.done) break\n          chunks.push(result.value)\n        }\n\n        assert.equal(chunks.length, 3, 'Should receive all data chunks from single write')\n        // Verify the chunks when reassembled equal the original input\n        let reassembled = new TextDecoder().decode(Buffer.concat(chunks))\n        assert.equal(reassembled, 'input', 'Chunks should reassemble to original input')\n      })\n\n      it('handles backpressure (write returns false, then drain)', async () => {\n        // Create mock compressor that signals backpressure on second write\n        let writeCount = 0\n        let mockCompressor = createMockCompressor({\n          write: (chunk, callback) => {\n            writeCount++\n            // Emit data immediately\n            setImmediate(() => mockCompressor.emit('data', chunk))\n            // Second write returns false (backpressure)\n            if (writeCount === 2) {\n              // Emit drain and call callback after returning\n              setImmediate(() => {\n                if (callback) callback()\n              })\n              return false\n            }\n            // No backpressure\n            if (callback) setImmediate(callback)\n            return true\n          },\n          end: () => {\n            setImmediate(() => mockCompressor.emit('end'))\n          },\n          destroy: () => {},\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('chunk1'))\n            controller.enqueue(new TextEncoder().encode('chunk2'))\n            controller.enqueue(new TextEncoder().encode('chunk3'))\n            controller.close()\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n        let chunks: Uint8Array[] = []\n\n        while (true) {\n          let { done, value } = await reader.read()\n          if (done) break\n          if (value) chunks.push(value)\n        }\n\n        let result = new TextDecoder().decode(Buffer.concat(chunks))\n\n        // Verify all chunks came through despite backpressure\n        assert.equal(result, 'chunk1chunk2chunk3')\n        assert.equal(writeCount, 3, 'Should have written all chunks')\n      })\n\n      it('handles cancel while waiting for drain', async () => {\n        let destroyed = false\n        let mockCompressor = createMockCompressor({\n          write: () =>\n            // Never call callback or emit drain, forcing the wait\n            false,\n          end: () => {},\n          destroy: () => {\n            destroyed = true\n          },\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('test'))\n            controller.close()\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        // Start reading (will get stuck waiting for drain)\n        let readPromise = reader.read()\n\n        // Cancel after a short delay\n        await Promise.resolve()\n        await reader.cancel('User cancelled')\n\n        // Wait for the read to complete\n        await readPromise.catch(() => {\n          // Expected to fail\n        })\n\n        assert.equal(destroyed, true, 'Compressor should be destroyed on cancel')\n      })\n\n      it('handles error emitted while waiting for drain and stops loop', async () => {\n        let writeCount = 0\n        let mockCompressor = createMockCompressor({\n          write: () => {\n            // Real zlib compressors continue to accept writes after error\n            writeCount++\n            if (writeCount === 1) {\n              // Emit error on next microtask (callback won't be called)\n              setImmediate(() => {\n                mockCompressor.emit('error', new Error('Compressor failed'))\n              })\n            }\n            return false // Always signal backpressure\n          },\n          end: () => {},\n          destroy: () => {},\n        })\n\n        let stream = new ReadableStream({\n          pull(controller) {\n            controller.enqueue(new TextEncoder().encode('chunk'))\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        // Expect error\n        await assert.rejects(\n          async () => {\n            await reader.read()\n          },\n          {\n            message: 'Compressor failed',\n          },\n        )\n\n        // Wait for any pending microtasks\n        await Promise.resolve()\n\n        let finalWriteCount = writeCount\n\n        assert.equal(\n          writeCount,\n          finalWriteCount,\n          `Should only write once before error stops loop (got ${writeCount})`,\n        )\n      })\n\n      it('handles compressor error after successful chunks', async () => {\n        let writeCount = 0\n        let mockCompressor = createMockCompressor({\n          write: (_chunk, callback) => {\n            // Real zlib compressors continue to accept writes after error\n            writeCount++\n            if (writeCount === 2) {\n              // Emit error on next microtask (callback won't be called)\n              setImmediate(() => {\n                mockCompressor.emit('error', new Error('Compressor failed mid-stream'))\n              })\n              return false // Signal backpressure to create an await point\n            }\n            if (callback) setImmediate(callback)\n            return true\n          },\n          end: () => {},\n          destroy: () => {},\n        })\n\n        let stream = new ReadableStream({\n          pull(controller) {\n            controller.enqueue(new TextEncoder().encode('chunk'))\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        // Expect error\n        await assert.rejects(\n          async () => {\n            while (true) {\n              let result = await reader.read()\n              if (result.done) break\n            }\n          },\n          {\n            message: 'Compressor failed mid-stream',\n          },\n        )\n\n        // Wait for any pending microtasks\n        await Promise.resolve()\n\n        let finalWriteCount = writeCount\n\n        assert.equal(\n          writeCount,\n          finalWriteCount,\n          `Should only write twice before error stops loop (got ${writeCount})`,\n        )\n      })\n\n      it('stops calling reader.read() after error', async () => {\n        let writeCount = 0\n        let mockCompressor = createMockCompressor({\n          write: () => {\n            writeCount++\n            if (writeCount === 1) {\n              setImmediate(() => {\n                mockCompressor.emit('error', new Error('Compressor failed'))\n              })\n            }\n            return false // Backpressure (callback won't be called)\n          },\n          end: () => {},\n          destroy: () => {},\n        })\n\n        let stream = new ReadableStream({\n          pull(controller) {\n            controller.enqueue(new TextEncoder().encode('chunk'))\n          },\n        })\n\n        // Monkey patch stream.getReader to count reader.read() calls\n        let readCount = 0\n        let originalGetReader = stream.getReader.bind(stream)\n        stream.getReader = function () {\n          let reader = originalGetReader()\n          let originalRead = reader.read.bind(reader)\n          reader.read = function () {\n            readCount++\n            return originalRead()\n          }\n          return reader\n        }\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        await assert.rejects(\n          async () => {\n            await reader.read()\n          },\n          {\n            message: 'Compressor failed',\n          },\n        )\n\n        // Wait for any pending microtasks\n        await Promise.resolve()\n\n        assert.equal(\n          readCount,\n          1,\n          `Should only call reader.read() once (called ${readCount} times)`,\n        )\n        assert.equal(writeCount, 1, 'Should only write once')\n      })\n\n      it('propagates reader.cancel() errors', async () => {\n        let mockCompressor = createMockCompressor({\n          write: (_chunk, callback) => {\n            if (callback) setImmediate(callback)\n            return true\n          },\n          end: () => {},\n          destroy: () => {},\n        })\n\n        // Create a stream where cancel() rejects\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('test'))\n          },\n          cancel() {\n            return Promise.reject(new Error('Cancel failed'))\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        // Start reading (will be in progress)\n        let readPromise = reader.read()\n\n        // Wait a bit for processing to start\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        // Cancel should propagate the error\n        await assert.rejects(\n          async () => {\n            await reader.cancel('User cancelled')\n          },\n          {\n            message: 'Cancel failed',\n          },\n        )\n\n        // Clean up the read promise\n        await readPromise.catch(() => {})\n      })\n\n      it('handles error passed to write() callback', async () => {\n        let mockCompressor = createMockCompressor({\n          write: (_chunk, callback) => {\n            // Call callback with error immediately\n            if (callback) {\n              callback(new Error('Write callback error'))\n            }\n            return true\n          },\n          end: () => {},\n          destroy: () => {\n            // Real compressors typically don't emit error when destroyed with an error\n            // because the error is already being propagated. However, for completeness\n            // and to ensure resilience, we also test this scenario below.\n          },\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('test'))\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        // The first read will trigger the write with callback error\n        await assert.rejects(\n          async () => {\n            await reader.read()\n          },\n          {\n            message: 'Write callback error',\n          },\n        )\n      })\n\n      it('prevents duplicate error reporting if destroy() also emits error', async () => {\n        let errorEmitCount = 0\n        let mockCompressor = createMockCompressor({\n          write: (_chunk, callback) => {\n            // Call callback with error immediately\n            if (callback) {\n              callback(new Error('Write callback error'))\n            }\n            return true\n          },\n          end: () => {},\n          destroy: (error) => {\n            if (error) {\n              setImmediate(() => {\n                errorEmitCount++\n                mockCompressor.emit('error', error)\n              })\n            }\n          },\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('test'))\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        // The first read will trigger the write with callback error\n        await assert.rejects(\n          async () => {\n            await reader.read()\n          },\n          {\n            message: 'Write callback error',\n          },\n        )\n\n        // Wait for any pending error emissions\n        await new Promise((resolve) => setImmediate(resolve))\n\n        // Error event should have been emitted but ignored (duplicate)\n        assert.equal(errorEmitCount, 1, 'Error event should have been emitted by destroy()')\n      })\n\n      it('calls compressor.end() when input stream is done', async () => {\n        let endCalled = false\n        let mockCompressor = createMockCompressor({\n          write: (_chunk, callback) => {\n            if (callback) setImmediate(callback)\n            return true\n          },\n          end: () => {\n            endCalled = true\n            mockCompressor.emit('end')\n          },\n          destroy: () => {},\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('test'))\n            controller.close()\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        // Read all chunks\n        while (true) {\n          let result = await reader.read()\n          if (result.done) break\n        }\n\n        assert.equal(endCalled, true, 'compressor.end() should be called when input stream is done')\n      })\n\n      it('closes output stream when compressor emits end event', async () => {\n        let mockCompressor = createMockCompressor({\n          write: (chunk, callback) => {\n            // Emit data and end immediately\n            mockCompressor.emit('data', chunk)\n            if (callback) setImmediate(callback)\n            return true\n          },\n          end: () => {\n            setImmediate(() => {\n              mockCompressor.emit('end')\n            })\n          },\n          destroy: () => {},\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('test'))\n            controller.close()\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        let chunks: Uint8Array[] = []\n        while (true) {\n          let result = await reader.read()\n          if (result.done) {\n            break\n          }\n          chunks.push(result.value)\n        }\n\n        assert.equal(chunks.length, 1, 'Should receive data chunk')\n        assert.equal(new TextDecoder().decode(chunks[0]), 'test')\n      })\n\n      it('handles end event with final data chunk', async () => {\n        let mockCompressor = createMockCompressor({\n          write: (chunk, callback) => {\n            mockCompressor.emit('data', chunk)\n            if (callback) setImmediate(callback)\n            return true\n          },\n          end: () => {\n            setImmediate(() => {\n              // Emit final data chunk before end\n              mockCompressor.emit('data', Buffer.from(' final'))\n              mockCompressor.emit('end')\n            })\n          },\n          destroy: () => {},\n        })\n\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('test'))\n            controller.close()\n          },\n        })\n\n        let compressed = compressStream(stream, mockCompressor as any)\n        let reader = compressed.getReader()\n\n        let chunks: Uint8Array[] = []\n        while (true) {\n          let result = await reader.read()\n          if (result.done) break\n          chunks.push(result.value)\n        }\n\n        // Should receive both chunks\n        assert.equal(chunks.length, 2, 'Should receive all data chunks including final')\n        let fullText = chunks.map((c) => new TextDecoder().decode(c)).join('')\n        assert.equal(fullText, 'test final')\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/response/src/lib/compress.ts",
    "content": "import {\n  constants,\n  createBrotliCompress,\n  createDeflate,\n  createGzip,\n  type BrotliCompress,\n  type Gzip,\n  type Deflate,\n} from 'node:zlib'\nimport type { BrotliOptions, ZlibOptions } from 'node:zlib'\n\nimport { AcceptEncoding, CacheControl, Vary } from '@remix-run/headers'\n\n/**\n * Encodings supported by {@link compressResponse}.\n */\nexport type Encoding = 'br' | 'gzip' | 'deflate'\nconst defaultEncodings: Encoding[] = ['br', 'gzip', 'deflate']\n\n/**\n * Configuration for negotiated response compression in {@link compressResponse}.\n */\nexport interface CompressResponseOptions {\n  /**\n   * Minimum size in bytes to compress (only enforced if Content-Length is present).\n   * If Content-Length is absent, compression is applied regardless of this threshold.\n   *\n   * Default: 1024\n   */\n  threshold?: number\n\n  /**\n   * Which encodings the server supports for negotiation in order of preference.\n   * Supported encodings: 'br', 'gzip', 'deflate'.\n   * Default: ['br', 'gzip', 'deflate']\n   */\n  encodings?: Encoding[]\n\n  /**\n   * node:zlib options for gzip/deflate compression.\n   *\n   * For SSE responses (text/event-stream), `flush: Z_SYNC_FLUSH` is automatically\n   * applied unless you explicitly set a flush value.\n   *\n   * See: https://nodejs.org/api/zlib.html#class-options\n   */\n  zlib?: ZlibOptions\n\n  /**\n   * node:zlib options for Brotli compression.\n   *\n   * For SSE responses (text/event-stream), `flush: BROTLI_OPERATION_FLUSH` is\n   * automatically applied unless you explicitly set a flush value.\n   *\n   * See: https://nodejs.org/api/zlib.html#class-brotlioptions\n   */\n  brotli?: BrotliOptions\n}\n\n/**\n * Compresses a Response based on the client's Accept-Encoding header.\n *\n * Compression is skipped for:\n * - Responses with no Accept-Encoding header (RFC 7231)\n * - Empty responses\n * - Already compressed responses\n * - Responses with Content-Length below threshold (default: 1024 bytes)\n * - Responses with Cache-Control: no-transform\n * - Responses advertising range support (Accept-Ranges: bytes)\n * - Partial content responses (206 status)\n *\n * When compressing, this function:\n * - Sets Content-Encoding header\n * - Removes Content-Length header\n * - Sets Accept-Ranges to 'none'\n * - Adds 'Accept-Encoding' to Vary header\n * - Converts strong ETags to weak ETags (per RFC 7232)\n *\n * @param response The response to compress\n * @param request The request (needed to check Accept-Encoding header)\n * @param options Optional compression settings\n * @returns A compressed Response or the original if no compression is suitable\n */\nexport async function compressResponse(\n  response: Response,\n  request: Request,\n  options?: CompressResponseOptions,\n): Promise<Response> {\n  let compressOptions = options ?? {}\n  let supportedEncodings = compressOptions.encodings ?? defaultEncodings\n  let threshold = compressOptions.threshold ?? 1024\n  let acceptEncodingHeader = request.headers.get('Accept-Encoding')\n  let responseHeaders = new Headers(response.headers)\n\n  let contentEncodingHeader = responseHeaders.get('content-encoding')\n  let contentLengthHeader = responseHeaders.get('content-length')\n  let contentLength = contentLengthHeader != null ? parseInt(contentLengthHeader, 10) : null\n  let acceptRangesHeader = responseHeaders.get('accept-ranges')\n  let cacheControl = CacheControl.from(responseHeaders.get('cache-control'))\n\n  if (\n    !acceptEncodingHeader ||\n    supportedEncodings.length === 0 ||\n    // Empty response\n    (request.method !== 'HEAD' && !response.body) ||\n    // Already compressed\n    contentEncodingHeader != null ||\n    // Content-Length below threshold\n    (contentLength != null && contentLength < threshold) ||\n    // Cache-Control: no-transform\n    cacheControl.noTransform ||\n    // Response advertising range support\n    acceptRangesHeader === 'bytes' ||\n    // Partial content responses\n    response.status === 206\n  ) {\n    return response\n  }\n\n  let acceptEncoding = AcceptEncoding.from(acceptEncodingHeader)\n  let selectedEncoding = negotiateEncoding(acceptEncoding, supportedEncodings)\n  if (selectedEncoding === null) {\n    // Client has explicitly rejected all supported encodings, including 'identity'\n    return new Response(\n      `Only ${[supportedEncodings, 'identity'].map((encoding) => `'${encoding}'`).join(', ')} encodings are supported`,\n      {\n        status: 406,\n        statusText: 'Not Acceptable',\n      },\n    )\n  }\n\n  if (selectedEncoding === 'identity') {\n    return response\n  }\n\n  // For HEAD requests, set compression headers without actually compressing\n  if (request.method === 'HEAD') {\n    setCompressionHeaders(responseHeaders, selectedEncoding)\n\n    return new Response(null, {\n      status: response.status,\n      statusText: response.statusText,\n      headers: responseHeaders,\n    })\n  }\n\n  return applyCompression(response, responseHeaders, selectedEncoding, compressOptions)\n}\n\nfunction negotiateEncoding(\n  acceptEncoding: AcceptEncoding,\n  supportedEncodings: readonly Encoding[],\n): Encoding | 'identity' | null {\n  if (acceptEncoding.encodings.length === 0) {\n    return 'identity'\n  }\n\n  let preferred = acceptEncoding.getPreferred(supportedEncodings)\n\n  if (!preferred) {\n    // Clients can explicitly reject 'identity' by setting its weight to 0,\n    // otherwise it is considered an acceptable fallback.\n    return acceptEncoding.getWeight('identity') === 0 ? null : 'identity'\n  }\n\n  return preferred\n}\n\nfunction setCompressionHeaders(headers: Headers, encoding: string): void {\n  headers.set('content-encoding', encoding)\n  headers.set('accept-ranges', 'none')\n  headers.delete('content-length')\n\n  // Update Vary header to include Accept-Encoding\n  let vary = Vary.from(headers.get('vary'))\n  vary.add('Accept-Encoding')\n  headers.set('vary', vary.toString())\n\n  // Convert strong ETags to weak since compressed representation is byte-different\n  let etagHeader = headers.get('etag')\n  if (etagHeader && !etagHeader.startsWith('W/')) {\n    headers.set('etag', `W/${etagHeader}`)\n  }\n}\n\nconst zlibFlushOptions = {\n  flush: constants.Z_SYNC_FLUSH,\n}\n\nconst brotliFlushOptions = {\n  flush: constants.BROTLI_OPERATION_FLUSH,\n}\n\nfunction applyCompression(\n  response: Response,\n  responseHeaders: Headers,\n  encoding: Encoding,\n  options: CompressResponseOptions,\n): Response {\n  if (!response.body) {\n    return response\n  }\n\n  // Detect SSE for automatic flush configuration\n  let contentTypeHeader = response.headers.get('Content-Type')\n  let mediaType = contentTypeHeader?.split(';')[0].trim()\n  let isSSE = mediaType === 'text/event-stream'\n\n  let compressor = createCompressor(encoding, {\n    ...options,\n    // Apply SSE flush defaults if not explicitly set\n    brotli: {\n      ...options.brotli,\n      ...(isSSE && options.brotli?.flush === undefined ? brotliFlushOptions : null),\n    },\n    zlib: {\n      ...options.zlib,\n      ...(isSSE && options.zlib?.flush === undefined ? zlibFlushOptions : null),\n    },\n  })\n\n  setCompressionHeaders(responseHeaders, encoding)\n\n  return new Response(compressStream(response.body, compressor), {\n    status: response.status,\n    statusText: response.statusText,\n    headers: responseHeaders,\n  })\n}\n\n/**\n * Compresses a response stream that bridges node:zlib to Web Streams.\n * Reads from the input stream, compresses chunks through the compressor,\n * and returns a new ReadableStream with the compressed data.\n *\n * @param input The input stream to compress\n * @param compressor The zlib compressor instance to use\n * @returns A new ReadableStream with the compressed data\n */\nexport function compressStream(\n  input: ReadableStream<Uint8Array>,\n  compressor: Gzip | Deflate | BrotliCompress,\n): ReadableStream<Uint8Array> {\n  let reader: ReadableStreamDefaultReader<Uint8Array> | null = null\n  let cancelled = false\n  let errored = false\n\n  return new ReadableStream<Uint8Array>({\n    async start(controller) {\n      reader = input.getReader()\n\n      compressor.on('data', (chunk: Buffer) => {\n        if (!cancelled && !errored) {\n          controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength))\n        }\n      })\n\n      compressor.on('end', () => {\n        if (!cancelled && !errored) {\n          controller.close()\n        }\n      })\n\n      compressor.on('error', (error) => {\n        // Ignore duplicate error events\n        if (errored) {\n          return\n        }\n        errored = true\n        if (!cancelled) {\n          controller.error(error)\n        }\n      })\n\n      try {\n        while (true) {\n          if (cancelled || errored) {\n            break\n          }\n\n          let { done, value } = await reader.read()\n\n          if (cancelled || errored) {\n            break\n          }\n\n          if (done) {\n            compressor.end()\n            break\n          }\n\n          if (!value) {\n            continue\n          }\n\n          await new Promise<void>((resolve, reject) => {\n            let resolvedImmediately = false\n\n            let canContinue = compressor.write(Buffer.from(value), (error) => {\n              if (resolvedImmediately) {\n                return\n              }\n              if (error) {\n                reject(error)\n              } else {\n                resolve()\n              }\n            })\n\n            if (canContinue) {\n              resolvedImmediately = true\n              resolve()\n            }\n          })\n        }\n      } catch (error) {\n        errored = true\n        compressor.destroy(error as Error)\n        if (!cancelled) {\n          controller.error(error)\n        }\n      } finally {\n        reader.releaseLock()\n      }\n    },\n\n    async cancel(reason) {\n      cancelled = true\n      // Destroy compressor first to unblock any pending write operations\n      compressor.destroy()\n      await reader?.cancel(reason)\n    },\n  })\n}\n\nfunction createCompressor(\n  encoding: Encoding,\n  options: CompressResponseOptions,\n): Gzip | Deflate | BrotliCompress {\n  switch (encoding) {\n    case 'br':\n      return createBrotliCompress(options.brotli)\n    case 'gzip':\n      return createGzip(options.zlib)\n    case 'deflate':\n      return createDeflate(options.zlib)\n    default:\n      throw new Error(`Unsupported encoding: ${encoding}`)\n  }\n}\n"
  },
  {
    "path": "packages/response/src/lib/file.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { LazyFile } from '@remix-run/lazy-file'\n\nimport { createFileResponse, type FileLike } from './file.ts'\n\n// Type assertions: ensure FileLike is compatible with native File and LazyFile.\n// If FileLike drifts from their APIs, TypeScript will error here.\nnull as unknown as File satisfies FileLike\nnull as unknown as LazyFile satisfies FileLike\n\ndescribe('createFileResponse()', () => {\n  it('serves a file', async () => {\n    let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n    let request = new Request('http://localhost/test.txt')\n\n    let response = await createFileResponse(mockFile, request)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, World!')\n    assert.equal(response.headers.get('Content-Type'), 'text/plain; charset=utf-8')\n    assert.equal(response.headers.get('Content-Length'), '13')\n  })\n\n  it('serves a LazyFile', async () => {\n    let lazyFile = new LazyFile(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n    let request = new Request('http://localhost/test.txt')\n\n    let response = await createFileResponse(lazyFile, request)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, World!')\n    assert.equal(response.headers.get('Content-Type'), 'text/plain; charset=utf-8')\n    assert.equal(response.headers.get('Content-Length'), '13')\n  })\n\n  it('serves a file with HEAD request', async () => {\n    let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n    let request = new Request('http://localhost/test.txt', { method: 'HEAD' })\n\n    let response = await createFileResponse(mockFile, request)\n\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), '')\n    assert.equal(response.headers.get('Content-Type'), 'text/plain; charset=utf-8')\n    assert.equal(response.headers.get('Content-Length'), '13')\n  })\n\n  describe('ETag support', () => {\n    it('includes weak ETag header by default', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: 1000000,\n      })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request)\n\n      let etag = response.headers.get('ETag')\n      assert.equal(response.status, 200)\n      assert.ok(etag)\n      assert.match(etag, /^W\\/\"[\\d]+-[\\d]+\\.?[\\d]*\"$/)\n    })\n\n    it('does not include ETag when etag=false', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, { etag: false })\n\n      assert.equal(response.status, 200)\n      assert.equal(response.headers.get('ETag'), null)\n    })\n\n    it('generates strong ETag when etag=strong', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, { etag: 'strong' })\n\n      let etag = response.headers.get('ETag')\n      assert.equal(response.status, 200)\n      assert.ok(etag)\n      assert.ok(!etag.startsWith('W/'), 'Should not be a weak ETag')\n      assert.match(etag, /^\"[a-f0-9]+\"$/, 'Should be a hex digest wrapped in quotes')\n    })\n\n    it('uses SHA-256 by default for strong ETags', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, { etag: 'strong' })\n\n      let etag = response.headers.get('ETag')\n      assert.ok(etag)\n      // SHA-256 produces 64 hex characters (32 bytes * 2)\n      assert.match(etag, /^\"[a-f0-9]{64}\"$/)\n    })\n\n    it('supports custom digest algorithm', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, {\n        etag: 'strong',\n        digest: 'SHA-512',\n      })\n\n      let etag = response.headers.get('ETag')\n      assert.ok(etag)\n      // SHA-512 produces 128 hex characters (64 bytes * 2)\n      assert.match(etag, /^\"[a-f0-9]{128}\"$/)\n    })\n\n    it('supports custom digest function', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, {\n        etag: 'strong',\n        digest: async () => 'custom-hash-12345',\n      })\n\n      let etag = response.headers.get('ETag')\n      assert.equal(etag, '\"custom-hash-12345\"')\n    })\n\n    it('throws error for unsupported algorithm', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      await assert.rejects(\n        async () => {\n          await createFileResponse(mockFile, request, {\n            etag: 'strong',\n            digest: 'MD5',\n          })\n        },\n        {\n          name: 'NotSupportedError',\n        },\n      )\n    })\n\n    it('supports SHA-1 algorithm', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, {\n        etag: 'strong',\n        digest: 'SHA-1',\n      })\n\n      let etag = response.headers.get('ETag')\n      assert.ok(etag)\n      // SHA-1 produces 40 hex characters (20 bytes * 2)\n      assert.match(etag, /^\"[a-f0-9]{40}\"$/)\n    })\n\n    it('supports SHA-384 algorithm', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, {\n        etag: 'strong',\n        digest: 'SHA-384',\n      })\n\n      let etag = response.headers.get('ETag')\n      assert.ok(etag)\n      // SHA-384 produces 96 hex characters (48 bytes * 2)\n      assert.match(etag, /^\"[a-f0-9]{96}\"$/)\n    })\n  })\n\n  describe('If-None-Match support', () => {\n    it('returns 304 (Not Modified) when If-None-Match matches ETag', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: 1000000,\n      })\n      let request1 = new Request('http://localhost/test.txt')\n\n      let response1 = await createFileResponse(mockFile, request1)\n      let etag = response1.headers.get('ETag')\n      assert.ok(etag)\n\n      let request2 = new Request('http://localhost/test.txt', {\n        headers: { 'If-None-Match': etag },\n      })\n      let response2 = await createFileResponse(mockFile, request2)\n\n      assert.equal(response2.status, 304)\n      assert.equal(await response2.text(), '')\n    })\n\n    it('returns 304 (Not Modified) when If-None-Match is *', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt', {\n        headers: { 'If-None-Match': '*' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 304)\n    })\n\n    it('returns 200 (OK) when If-None-Match does not match', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt', {\n        headers: { 'If-None-Match': 'W/\"wrong-etag\"' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), 'Hello, World!')\n    })\n\n    it('handles multiple ETags in If-None-Match', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: 1000000,\n      })\n      let request1 = new Request('http://localhost/test.txt')\n\n      let response1 = await createFileResponse(mockFile, request1)\n      let etag = response1.headers.get('ETag')\n      assert.ok(etag)\n\n      let request2 = new Request('http://localhost/test.txt', {\n        headers: { 'If-None-Match': `W/\"wrong-1\", ${etag}, W/\"wrong-2\"` },\n      })\n      let response2 = await createFileResponse(mockFile, request2)\n\n      assert.equal(response2.status, 304)\n    })\n\n    it('ignores If-None-Match when etag is disabled', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n\n      // First, get the ETag that would be generated\n      let request1 = new Request('http://localhost/test.txt')\n      let response1 = await createFileResponse(mockFile, request1)\n      let etag = response1.headers.get('ETag')\n      assert.ok(etag)\n\n      // Now test with etag disabled but send the matching ETag\n      let request2 = new Request('http://localhost/test.txt', {\n        headers: { 'If-None-Match': etag },\n      })\n      let response2 = await createFileResponse(mockFile, request2, { etag: false })\n\n      // Should return 200, not 304, because etag is disabled\n      assert.equal(response2.status, 200)\n      assert.equal(await response2.text(), 'Hello, World!')\n    })\n\n    it('ignores If-Modified-Since when If-None-Match is present but does not match', async () => {\n      let fileDate = new Date('2025-01-01')\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: fileDate.getTime(),\n      })\n      let request = new Request('http://localhost/test.txt', {\n        headers: {\n          'If-None-Match': '\"wrong-etag\"',\n          'If-Modified-Since': fileDate.toUTCString(), // Would normally return 304\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      // Should return 200, not 304, because If-None-Match takes precedence\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), 'Hello, World!')\n    })\n  })\n\n  describe('If-Match support', () => {\n    describe('precondition validation', () => {\n      it('returns 412 (Precondition Failed) when resource has weak ETag', async () => {\n        let mockFile = new File(['Hello, World!'], 'test.txt', {\n          type: 'text/plain',\n          lastModified: 1000000,\n        })\n        let request1 = new Request('http://localhost/test.txt')\n\n        let response1 = await createFileResponse(mockFile, request1)\n        let etag = response1.headers.get('ETag')\n        assert.ok(etag)\n        assert.ok(etag.startsWith('W/')) // Verify it's a weak ETag\n\n        // If-Match uses strong comparison, so weak ETags never match\n        let request2 = new Request('http://localhost/test.txt', {\n          headers: { 'If-Match': etag },\n        })\n        let response2 = await createFileResponse(mockFile, request2)\n\n        assert.equal(response2.status, 412)\n      })\n\n      it('returns 200 (OK) when resource has strong ETag and If-Match matches', async () => {\n        let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n        let request1 = new Request('http://localhost/test.txt')\n\n        // Get the strong ETag\n        let response1 = await createFileResponse(mockFile, request1, { etag: 'strong' })\n        let etag = response1.headers.get('ETag')\n        assert.ok(etag)\n        assert.ok(!etag.startsWith('W/')) // Verify it's a strong ETag\n\n        // If-Match should work with strong ETags\n        let request2 = new Request('http://localhost/test.txt', {\n          headers: { 'If-Match': etag },\n        })\n        let response2 = await createFileResponse(mockFile, request2, { etag: 'strong' })\n\n        assert.equal(response2.status, 200)\n        assert.equal(await response2.text(), 'Hello, World!')\n      })\n\n      it('returns 412 (Precondition Failed) when If-Match does not match (weak ETag)', async () => {\n        let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n        let request = new Request('http://localhost/test.txt', {\n          headers: { 'If-Match': '\"wrong-etag\"' },\n        })\n\n        let response = await createFileResponse(mockFile, request)\n\n        assert.equal(response.status, 412)\n      })\n\n      it('returns 412 (Precondition Failed) when If-Match does not match (strong ETag)', async () => {\n        let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n        let request = new Request('http://localhost/test.txt', {\n          headers: { 'If-Match': '\"wrong-etag\"' },\n        })\n\n        let response = await createFileResponse(mockFile, request, { etag: 'strong' })\n\n        assert.equal(response.status, 412)\n      })\n\n      it('returns 200 (OK) when If-Match is *', async () => {\n        let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n        let request = new Request('http://localhost/test.txt', {\n          headers: { 'If-Match': '*' },\n        })\n\n        let response = await createFileResponse(mockFile, request)\n\n        assert.equal(response.status, 200)\n        assert.equal(await response.text(), 'Hello, World!')\n      })\n\n      it('returns 412 (Precondition Failed) when If-Match contains multiple ETags and none match', async () => {\n        let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n        let request = new Request('http://localhost/test.txt', {\n          headers: { 'If-Match': '\"wrong-1\", \"wrong-2\"' },\n        })\n\n        let response = await createFileResponse(mockFile, request)\n\n        assert.equal(response.status, 412)\n      })\n    })\n\n    describe('prioritization', () => {\n      it('returns 412 (Precondition Failed) when If-Match fails, even if If-None-Match would match', async () => {\n        let mockFile = new File(['Hello, World!'], 'test.txt', {\n          type: 'text/plain',\n          lastModified: 1000000,\n        })\n        let request1 = new Request('http://localhost/test.txt')\n\n        let response1 = await createFileResponse(mockFile, request1)\n        let etag = response1.headers.get('ETag')\n        assert.ok(etag)\n\n        let request2 = new Request('http://localhost/test.txt', {\n          headers: {\n            'If-Match': 'W/\"wrong-etag\"',\n            'If-None-Match': etag,\n          },\n        })\n        let response2 = await createFileResponse(mockFile, request2)\n\n        assert.equal(response2.status, 412)\n      })\n    })\n\n    it('ignores If-Match when etag is disabled', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n\n      // First, get the ETag that would be generated\n      let request1 = new Request('http://localhost/test.txt')\n      let response1 = await createFileResponse(mockFile, request1)\n      let etag = response1.headers.get('ETag')\n      assert.ok(etag)\n\n      // Now test with etag disabled but send a non-matching ETag\n      // (If we weren't ignoring it, this would return 412)\n      let request2 = new Request('http://localhost/test.txt', {\n        headers: { 'If-Match': 'W/\"wrong-etag\"' },\n      })\n      let response2 = await createFileResponse(mockFile, request2, { etag: false })\n\n      // Should return 200, not 412, because etag is disabled\n      assert.equal(response2.status, 200)\n      assert.equal(await response2.text(), 'Hello, World!')\n    })\n  })\n\n  describe('If-Unmodified-Since support', () => {\n    describe('precondition validation', () => {\n      it('returns 200 (OK) when If-Unmodified-Since is after Last-Modified', async () => {\n        let fileDate = new Date('2025-01-01')\n        let futureDate = new Date('2026-01-01')\n        let mockFile = new File(['Hello, World!'], 'test.txt', {\n          type: 'text/plain',\n          lastModified: fileDate.getTime(),\n        })\n        let request = new Request('http://localhost/test.txt', {\n          headers: { 'If-Unmodified-Since': futureDate.toUTCString() },\n        })\n\n        let response = await createFileResponse(mockFile, request)\n\n        assert.equal(response.status, 200)\n        assert.equal(await response.text(), 'Hello, World!')\n      })\n\n      it('returns 200 (OK) when If-Unmodified-Since matches Last-Modified', async () => {\n        let fileDate = new Date('2025-01-01')\n        let mockFile = new File(['Hello, World!'], 'test.txt', {\n          type: 'text/plain',\n          lastModified: fileDate.getTime(),\n        })\n        let request = new Request('http://localhost/test.txt', {\n          headers: { 'If-Unmodified-Since': fileDate.toUTCString() },\n        })\n\n        let response = await createFileResponse(mockFile, request)\n\n        assert.equal(response.status, 200)\n        assert.equal(await response.text(), 'Hello, World!')\n      })\n\n      it('returns 412 (Precondition Failed) when If-Unmodified-Since is before Last-Modified', async () => {\n        let fileDate = new Date('2025-01-01')\n        let pastDate = new Date('2024-01-01')\n        let mockFile = new File(['Hello, World!'], 'test.txt', {\n          type: 'text/plain',\n          lastModified: fileDate.getTime(),\n        })\n        let request = new Request('http://localhost/test.txt', {\n          headers: { 'If-Unmodified-Since': pastDate.toUTCString() },\n        })\n\n        let response = await createFileResponse(mockFile, request)\n\n        assert.equal(response.status, 412)\n      })\n\n      it('ignores malformed If-Unmodified-Since', async () => {\n        let fileDate = new Date('2025-01-01')\n        let mockFile = new File(['Hello, World!'], 'test.txt', {\n          type: 'text/plain',\n          lastModified: fileDate.getTime(),\n        })\n        let request = new Request('http://localhost/test.txt', {\n          headers: { 'If-Unmodified-Since': 'invalid-date' },\n        })\n\n        let response = await createFileResponse(mockFile, request)\n\n        assert.equal(response.status, 200)\n        assert.equal(await response.text(), 'Hello, World!')\n      })\n\n      it('treats dates with same second but different milliseconds as equal', async () => {\n        // File last modified at 1000100ms (1.000100 seconds)\n        let mockFile = new File(['Hello, World!'], 'test.txt', {\n          type: 'text/plain',\n          lastModified: 1000100,\n        })\n        // Client's If-Unmodified-Since at 1000900ms (1.000900 seconds) - same second\n        let ifUnmodifiedSinceDate = new Date(1000900)\n        let request = new Request('http://localhost/test.txt', {\n          headers: { 'If-Unmodified-Since': ifUnmodifiedSinceDate.toUTCString() },\n        })\n\n        let response = await createFileResponse(mockFile, request)\n\n        // Should return 200 because both round down to the same second\n        assert.equal(response.status, 200)\n        assert.equal(await response.text(), 'Hello, World!')\n      })\n    })\n\n    describe('prioritization', () => {\n      it('returns 412 (Precondition Failed) when If-Match fails, even if If-Unmodified-Since would pass', async () => {\n        let fileDate = new Date('2025-01-01')\n        let futureDate = new Date('2026-01-01')\n        let mockFile = new File(['Hello, World!'], 'test.txt', {\n          type: 'text/plain',\n          lastModified: fileDate.getTime(),\n        })\n        let request = new Request('http://localhost/test.txt', {\n          headers: {\n            'If-Match': 'W/\"wrong-etag\"',\n            'If-Unmodified-Since': futureDate.toUTCString(),\n          },\n        })\n\n        let response = await createFileResponse(mockFile, request)\n\n        assert.equal(response.status, 412)\n      })\n\n      it('ignores If-Unmodified-Since when If-Match is present (strong ETag)', async () => {\n        let pastDate = new Date('2024-01-01')\n        let mockFile = new File(['Hello, World!'], 'test.txt', {\n          type: 'text/plain',\n          lastModified: pastDate.getTime(),\n        })\n        let request1 = new Request('http://localhost/test.txt')\n\n        // Get the strong ETag\n        let response1 = await createFileResponse(mockFile, request1, { etag: 'strong' })\n        let etag = response1.headers.get('ETag')\n        assert.ok(etag)\n        assert.ok(!etag.startsWith('W/')) // Verify it's a strong ETag\n\n        // If-Match passes, so If-Unmodified-Since should be ignored\n        // (even though it would fail if evaluated - pastDate is before file's lastModified)\n        let request2 = new Request('http://localhost/test.txt', {\n          headers: {\n            'If-Match': etag,\n            'If-Unmodified-Since': pastDate.toUTCString(),\n          },\n        })\n        let response2 = await createFileResponse(mockFile, request2, { etag: 'strong' })\n\n        assert.equal(response2.status, 200)\n        assert.equal(await response2.text(), 'Hello, World!')\n      })\n    })\n\n    it('ignores If-Unmodified-Since when lastModified is disabled', async () => {\n      let pastDate = new Date('2024-01-01')\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt', {\n        headers: { 'If-Unmodified-Since': pastDate.toUTCString() },\n      })\n\n      let response = await createFileResponse(mockFile, request, { lastModified: false })\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), 'Hello, World!')\n    })\n  })\n\n  describe('Last-Modified support', () => {\n    it('includes Last-Modified header', async () => {\n      let fileDate = new Date('2025-01-01')\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: fileDate.getTime(),\n      })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(response.headers.get('Last-Modified'), fileDate.toUTCString())\n    })\n\n    it('does not include Last-Modified when lastModified=false', async () => {\n      let mockFile = new File(['Hello, World!'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, { lastModified: false })\n\n      assert.equal(response.status, 200)\n      assert.equal(response.headers.get('Last-Modified'), null)\n    })\n\n    it('returns 304 (Not Modified) when If-Modified-Since matches Last-Modified', async () => {\n      let fileDate = new Date('2025-01-01')\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: fileDate.getTime(),\n      })\n      let request = new Request('http://localhost/test.txt', {\n        headers: { 'If-Modified-Since': fileDate.toUTCString() },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 304)\n      assert.equal(await response.text(), '')\n    })\n\n    it('returns 304 (Not Modified) when If-Modified-Since is after Last-Modified', async () => {\n      let fileDate = new Date('2025-01-01')\n      let futureDate = new Date('2026-01-01')\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: fileDate.getTime(),\n      })\n      let request = new Request('http://localhost/test.txt', {\n        headers: { 'If-Modified-Since': futureDate.toUTCString() },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 304)\n    })\n\n    it('returns 200 (OK) when If-Modified-Since is before Last-Modified', async () => {\n      let fileDate = new Date('2025-01-01')\n      let pastDate = new Date('2024-01-01')\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: fileDate.getTime(),\n      })\n      let request = new Request('http://localhost/test.txt', {\n        headers: { 'If-Modified-Since': pastDate.toUTCString() },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), 'Hello, World!')\n    })\n\n    it('treats dates with same second but different milliseconds as equal', async () => {\n      // File last modified at 1000999ms (1.000999 seconds)\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: 1000999,\n      })\n      // Client's If-Modified-Since at 1000500ms (1.000500 seconds) - same second\n      let ifModifiedSinceDate = new Date(1000500)\n      let request = new Request('http://localhost/test.txt', {\n        headers: { 'If-Modified-Since': ifModifiedSinceDate.toUTCString() },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      // Should return 304 because both round down to the same second\n      assert.equal(response.status, 304)\n    })\n\n    it('prioritizes ETag over If-Modified-Since when both are present', async () => {\n      let fileDate = new Date('2025-01-01')\n      let mockFile = new File(['Hello, World!'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: fileDate.getTime(),\n      })\n      let request1 = new Request('http://localhost/test.txt')\n\n      let response1 = await createFileResponse(mockFile, request1)\n      let etag = response1.headers.get('ETag')\n\n      let request2 = new Request('http://localhost/test.txt', {\n        headers: {\n          'If-None-Match': 'W/\"wrong-etag\"',\n          'If-Modified-Since': fileDate.toUTCString(),\n        },\n      })\n      let response2 = await createFileResponse(mockFile, request2)\n\n      assert.equal(response2.status, 200)\n    })\n  })\n\n  describe('Range requests', () => {\n    it('includes Accept-Ranges header for non-compressible media types by default', async () => {\n      let mockFile = new File(['fake video data'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4')\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.headers.get('Accept-Ranges'), 'bytes')\n    })\n\n    it('does not include Accept-Ranges header for compressible media types by default', async () => {\n      let mockFile = new File(['Hello'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.headers.get('Accept-Ranges'), null)\n    })\n\n    it('includes Accept-Ranges header when explicitly enabled', async () => {\n      let mockFile = new File(['Hello'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, { acceptRanges: true })\n\n      assert.equal(response.headers.get('Accept-Ranges'), 'bytes')\n    })\n\n    it('omits Accept-Ranges header when acceptRanges=false', async () => {\n      let mockFile = new File(['Hello'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, { acceptRanges: false })\n\n      assert.equal(response.headers.get('Accept-Ranges'), null)\n    })\n\n    it('handles simple range request', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'bytes=0-4' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 206)\n      assert.equal(await response.text(), '01234')\n      assert.equal(response.headers.get('Content-Range'), 'bytes 0-4/10')\n      assert.equal(response.headers.get('Content-Length'), '5')\n    })\n\n    it('handles range with only start', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'bytes=5-' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 206)\n      assert.equal(await response.text(), '56789')\n      assert.equal(response.headers.get('Content-Range'), 'bytes 5-9/10')\n    })\n\n    it('handles suffix range (last N bytes)', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'bytes=-3' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 206)\n      assert.equal(await response.text(), '789')\n      assert.equal(response.headers.get('Content-Range'), 'bytes 7-9/10')\n    })\n\n    it('clamps end byte to file size when it exceeds', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'bytes=0-999' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 206)\n      assert.equal(await response.text(), '0123456789')\n      assert.equal(response.headers.get('Content-Range'), 'bytes 0-9/10')\n      assert.equal(response.headers.get('Content-Length'), '10')\n    })\n\n    it('ignores Range header for non-GET/HEAD requests', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        method: 'POST',\n        headers: { Range: 'bytes=0-4' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '0123456789')\n      assert.equal(response.headers.get('Content-Range'), null)\n    })\n\n    it('ignores Range header for HEAD requests', async () => {\n      let mockFile = new File(['0123456789'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt', {\n        method: 'HEAD',\n        headers: { Range: 'bytes=0-4' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(response.headers.get('Content-Range'), null)\n      assert.equal(response.headers.get('Content-Length'), '10')\n      assert.equal(await response.text(), '')\n    })\n\n    it('returns 416 (Range Not Satisfiable) for unsatisfiable range', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'bytes=20-30' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 416)\n      assert.equal(response.headers.get('Content-Range'), 'bytes */10')\n    })\n\n    it('returns 416 (Range Not Satisfiable) for multipart ranges (not supported)', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'bytes=0-2,5-7' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 416)\n      assert.equal(response.headers.get('Content-Range'), 'bytes */10')\n    })\n\n    it('returns 400 (Bad Request) for malformed multipart range syntax', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'bytes=0-2,garbage' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 400)\n      assert.equal(await response.text(), 'Bad Request')\n    })\n\n    it('returns 400 (Bad Request) for start > end', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'bytes=5-2' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 400)\n      assert.equal(await response.text(), 'Bad Request')\n    })\n\n    it('returns 400 (Bad Request) for malformed range', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'invalid' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 400)\n      assert.equal(await response.text(), 'Bad Request')\n    })\n\n    it('returns 400 (Bad Request) for \"bytes=\" with no range', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: { Range: 'bytes=' },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 400)\n      assert.equal(await response.text(), 'Bad Request')\n    })\n\n    it('returns full file when acceptRanges=false', async () => {\n      let mockFile = new File(['0123456789'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt', {\n        headers: { Range: 'bytes=0-4' },\n      })\n\n      let response = await createFileResponse(mockFile, request, { acceptRanges: false })\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '0123456789')\n      assert.equal(response.headers.get('Content-Range'), null)\n    })\n\n    it('returns 412 (Precondition Failed) when If-Match fails before processing Range', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-Match': 'W/\"wrong-etag\"',\n          Range: 'bytes=0-4',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 412)\n      assert.equal(response.headers.get('Content-Range'), null)\n    })\n\n    it('returns 206 (Partial Content) when If-Match succeeds with Range request (strong ETag)', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request1 = new Request('http://localhost/video.mp4')\n\n      // Get the strong ETag\n      let response1 = await createFileResponse(mockFile, request1, {\n        etag: 'strong',\n      })\n      let etag = response1.headers.get('ETag')\n      assert.ok(etag)\n      assert.ok(!etag.startsWith('W/')) // Verify it's a strong ETag\n\n      // If-Match passes, Range should be processed\n      let request2 = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-Match': etag,\n          Range: 'bytes=0-4',\n        },\n      })\n      let response2 = await createFileResponse(mockFile, request2, {\n        etag: 'strong',\n      })\n\n      assert.equal(response2.status, 206)\n      assert.equal(await response2.text(), '01234')\n      assert.equal(response2.headers.get('Content-Range'), 'bytes 0-4/10')\n    })\n\n    it('returns 206 (Partial Content) when If-Unmodified-Since passes with Range request', async () => {\n      let fileDate = new Date('2025-01-01')\n      let futureDate = new Date('2026-01-01')\n      let mockFile = new File(['0123456789'], 'video.mp4', {\n        type: 'video/mp4',\n        lastModified: fileDate.getTime(),\n      })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-Unmodified-Since': futureDate.toUTCString(),\n          Range: 'bytes=0-4',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 206)\n      assert.equal(await response.text(), '01234')\n      assert.equal(response.headers.get('Content-Range'), 'bytes 0-4/10')\n    })\n\n    it('returns 412 (Precondition Failed) when If-Unmodified-Since fails before processing Range', async () => {\n      let fileDate = new Date('2025-01-01')\n      let pastDate = new Date('2024-01-01')\n      let mockFile = new File(['0123456789'], 'test.txt', {\n        type: 'text/plain',\n        lastModified: fileDate.getTime(),\n      })\n      let request = new Request('http://localhost/test.txt', {\n        headers: {\n          'If-Unmodified-Since': pastDate.toUTCString(),\n          Range: 'bytes=0-4',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 412)\n      assert.equal(response.headers.get('Content-Range'), null)\n    })\n\n    it('returns 304 (Not Modified) when If-None-Match matches etag', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', {\n        type: 'video/mp4',\n        lastModified: 1000000,\n      })\n      let request1 = new Request('http://localhost/video.mp4')\n\n      let response1 = await createFileResponse(mockFile, request1)\n      let etag = response1.headers.get('ETag')\n      assert.ok(etag)\n\n      let request2 = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-None-Match': etag,\n          Range: 'bytes=0-4',\n        },\n      })\n      let response2 = await createFileResponse(mockFile, request2)\n\n      assert.equal(response2.status, 304)\n      assert.equal(response2.headers.get('Content-Range'), null)\n    })\n\n    it('returns 206 (Partial Content) when If-None-Match does not match', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-None-Match': '\"wrong-etag\"',\n          Range: 'bytes=0-4',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 206)\n      assert.equal(await response.text(), '01234')\n      assert.equal(response.headers.get('Content-Range'), 'bytes 0-4/10')\n    })\n\n    it('returns 304 (Not Modified) when If-Modified-Since matches', async () => {\n      let fileDate = new Date('2025-01-01')\n      let mockFile = new File(['0123456789'], 'video.mp4', {\n        type: 'video/mp4',\n        lastModified: fileDate.getTime(),\n      })\n      let request1 = new Request('http://localhost/video.mp4')\n\n      let response1 = await createFileResponse(mockFile, request1)\n      let lastModified = response1.headers.get('Last-Modified')\n      assert.ok(lastModified)\n\n      let request2 = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-Modified-Since': lastModified,\n          Range: 'bytes=0-4',\n        },\n      })\n      let response2 = await createFileResponse(mockFile, request2)\n\n      assert.equal(response2.status, 304)\n      assert.equal(response2.headers.get('Content-Range'), null)\n    })\n\n    it('returns 206 (Partial Content) when If-Modified-Since does not match', async () => {\n      let fileDate = new Date('2025-01-01')\n      let pastDate = new Date('2024-01-01')\n      let mockFile = new File(['0123456789'], 'video.mp4', {\n        type: 'video/mp4',\n        lastModified: fileDate.getTime(),\n      })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-Modified-Since': pastDate.toUTCString(),\n          Range: 'bytes=0-4',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 206)\n      assert.equal(await response.text(), '01234')\n      assert.equal(response.headers.get('Content-Range'), 'bytes 0-4/10')\n    })\n\n    it('returns 206 (Partial Content) when If-Range matches Last-Modified date', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request1 = new Request('http://localhost/video.mp4')\n\n      let response1 = await createFileResponse(mockFile, request1)\n      let lastModified = response1.headers.get('Last-Modified')\n      assert.ok(lastModified)\n\n      let request2 = new Request('http://localhost/video.mp4', {\n        headers: {\n          Range: 'bytes=0-4',\n          'If-Range': lastModified,\n        },\n      })\n      let response2 = await createFileResponse(mockFile, request2)\n\n      assert.equal(response2.status, 206)\n      assert.equal(await response2.text(), '01234')\n      assert.equal(response2.headers.get('Content-Range'), 'bytes 0-4/10')\n    })\n\n    it('returns 200 (OK, full file) when If-Range does not match Last-Modified date', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: {\n          Range: 'bytes=0-4',\n          'If-Range': 'Wed, 21 Oct 2015 07:28:00 GMT',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '0123456789')\n      assert.equal(response.headers.get('Content-Range'), null)\n    })\n\n    it('ignores If-Range with weak ETag value (only Last-Modified date supported)', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', {\n        type: 'video/mp4',\n        lastModified: 1000000,\n      })\n      let request1 = new Request('http://localhost/video.mp4')\n\n      let response1 = await createFileResponse(mockFile, request1)\n      let etag = response1.headers.get('ETag')\n      assert.ok(etag)\n\n      let request2 = new Request('http://localhost/video.mp4', {\n        headers: {\n          Range: 'bytes=0-4',\n          'If-Range': etag,\n        },\n      })\n      let response2 = await createFileResponse(mockFile, request2)\n\n      assert.equal(response2.status, 200)\n      assert.equal(await response2.text(), '0123456789')\n    })\n\n    it('returns full file when If-Range has invalid date format', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: {\n          Range: 'bytes=0-4',\n          'If-Range': '2025-01-01',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '0123456789')\n      assert.equal(response.headers.get('Content-Range'), null)\n    })\n\n    it('returns full file when If-Range is malformed', async () => {\n      let mockFile = new File(['0123456789'], 'video.mp4', { type: 'video/mp4' })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: {\n          Range: 'bytes=0-4',\n          'If-Range': 'not-a-valid-value',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '0123456789')\n      assert.equal(response.headers.get('Content-Range'), null)\n    })\n\n    it('ignores If-Range when acceptRanges is disabled', async () => {\n      let mockFile = new File(['0123456789'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt', {\n        headers: {\n          Range: 'bytes=0-4',\n          'If-Range': 'Wed, 21 Oct 2015 07:28:00 GMT',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request, { acceptRanges: false })\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '0123456789')\n      assert.equal(response.headers.get('Content-Range'), null)\n    })\n\n    it('ignores If-Range when lastModified is disabled', async () => {\n      let mockFile = new File(['0123456789'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt', {\n        headers: {\n          Range: 'bytes=0-4',\n          'If-Range': 'Wed, 21 Oct 2015 07:28:00 GMT',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request, { lastModified: false })\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '0123456789')\n    })\n\n    it('returns 304 (Not Modified) with If-None-Match + If-Range when If-None-Match matches', async () => {\n      let fileDate = new Date('2025-01-01')\n      let mockFile = new File(['0123456789'], 'video.mp4', {\n        type: 'video/mp4',\n        lastModified: fileDate.getTime(),\n      })\n      let request1 = new Request('http://localhost/video.mp4')\n\n      let response1 = await createFileResponse(mockFile, request1)\n      let etag = response1.headers.get('ETag')\n      assert.ok(etag)\n      let lastModified = response1.headers.get('Last-Modified')\n      assert.ok(lastModified)\n\n      let request2 = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-None-Match': etag,\n          'If-Range': lastModified,\n          Range: 'bytes=0-4',\n        },\n      })\n      let response2 = await createFileResponse(mockFile, request2)\n\n      assert.equal(response2.status, 304)\n      assert.equal(response2.headers.get('Content-Range'), null)\n    })\n\n    it('returns 206 (Partial Content) with If-None-Match + If-Range when If-Range matches and If-None-Match does not match', async () => {\n      let fileDate = new Date('2025-01-01')\n      let mockFile = new File(['0123456789'], 'video.mp4', {\n        type: 'video/mp4',\n        lastModified: fileDate.getTime(),\n      })\n      let request1 = new Request('http://localhost/video.mp4')\n\n      let response1 = await createFileResponse(mockFile, request1)\n      let lastModified = response1.headers.get('Last-Modified')\n      assert.ok(lastModified)\n\n      let request2 = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-None-Match': '\"wrong-etag\"',\n          'If-Range': lastModified,\n          Range: 'bytes=0-4',\n        },\n      })\n      let response2 = await createFileResponse(mockFile, request2)\n\n      assert.equal(response2.status, 206)\n      assert.equal(await response2.text(), '01234')\n      assert.equal(response2.headers.get('Content-Range'), 'bytes 0-4/10')\n    })\n\n    it('returns 200 (OK) with If-None-Match + If-Range when both If-None-Match and If-Range do not match', async () => {\n      let fileDate = new Date('2025-01-01')\n      let pastDate = new Date('2024-01-01')\n      let mockFile = new File(['0123456789'], 'video.mp4', {\n        type: 'video/mp4',\n        lastModified: fileDate.getTime(),\n      })\n      let request = new Request('http://localhost/video.mp4', {\n        headers: {\n          'If-None-Match': '\"wrong-etag\"',\n          'If-Range': pastDate.toUTCString(),\n          Range: 'bytes=0-4',\n        },\n      })\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '0123456789')\n      assert.equal(response.headers.get('Content-Range'), null)\n    })\n  })\n\n  describe('Cache-Control', () => {\n    it('does not include Cache-Control header by default', async () => {\n      let mockFile = new File(['Hello'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.headers.get('Cache-Control'), null)\n    })\n\n    it('uses custom Cache-Control header', async () => {\n      let mockFile = new File(['Hello'], 'test.txt', { type: 'text/plain' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request, {\n        cacheControl: 'no-cache',\n      })\n\n      assert.equal(response.headers.get('Cache-Control'), 'no-cache')\n    })\n  })\n\n  describe('Content-Type', () => {\n    it('sets Content-Type from file with charset for text-based types', async () => {\n      let testCases = [\n        { type: 'text/html', name: 'test.html', expected: 'text/html; charset=utf-8' },\n        { type: 'text/css', name: 'test.css', expected: 'text/css; charset=utf-8' },\n        { type: 'text/plain', name: 'test.txt', expected: 'text/plain; charset=utf-8' },\n        { type: 'text/javascript', name: 'test.js', expected: 'text/javascript; charset=utf-8' },\n        {\n          type: 'application/json',\n          name: 'test.json',\n          expected: 'application/json; charset=utf-8',\n        },\n      ]\n\n      for (let { type, name, expected } of testCases) {\n        let mockFile = new File(['test content'], name, { type })\n        let request = new Request(`http://localhost/${name}`)\n\n        let response = await createFileResponse(mockFile, request)\n        assert.equal(response.status, 200)\n        assert.equal(response.headers.get('Content-Type'), expected)\n      }\n    })\n\n    it('does not add charset to XML types', async () => {\n      let testCases = [\n        { type: 'image/svg+xml', name: 'test.svg' },\n        { type: 'application/xml', name: 'test.xml' },\n      ]\n\n      for (let { type, name } of testCases) {\n        let mockFile = new File(['test content'], name, { type })\n        let request = new Request(`http://localhost/${name}`)\n\n        let response = await createFileResponse(mockFile, request)\n        assert.equal(response.status, 200)\n        assert.equal(response.headers.get('Content-Type'), type)\n      }\n    })\n\n    it('sets Content-Type with charset for application/javascript', async () => {\n      let mockFile = new File(['test content'], 'app.js', { type: 'application/javascript' })\n      let request = new Request('http://localhost/app.js')\n\n      let response = await createFileResponse(mockFile, request)\n      assert.equal(response.status, 200)\n      assert.equal(response.headers.get('Content-Type'), 'application/javascript; charset=utf-8')\n    })\n\n    it('does not add charset to binary types', async () => {\n      let testCases = [\n        { type: 'image/png', name: 'test.png' },\n        { type: 'image/jpeg', name: 'test.jpg' },\n        { type: 'application/pdf', name: 'test.pdf' },\n        { type: 'application/zip', name: 'test.zip' },\n      ]\n\n      for (let { type, name } of testCases) {\n        let mockFile = new File(['test content'], name, { type })\n        let request = new Request(`http://localhost/${name}`)\n\n        let response = await createFileResponse(mockFile, request)\n        assert.equal(response.status, 200)\n        assert.equal(response.headers.get('Content-Type'), type)\n      }\n    })\n\n    it('handles file with empty type', async () => {\n      let mockFile = new File(['test content'], 'test.txt', { type: '' })\n      let request = new Request('http://localhost/test.txt')\n\n      let response = await createFileResponse(mockFile, request)\n\n      assert.equal(response.status, 200)\n      assert.equal(response.headers.get('Content-Type'), null)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/response/src/lib/file.ts",
    "content": "import {\n  type ContentRangeInit,\n  ContentRange,\n  IfMatch,\n  IfNoneMatch,\n  IfRange,\n  Range,\n} from '@remix-run/headers'\nimport { isCompressibleMimeType, mimeTypeToContentType } from '@remix-run/mime'\n\n/**\n * Minimal interface for file-like objects used by {@link createFileResponse}.\n */\nexport interface FileLike {\n  /** File compatibility - included for interface completeness */\n  readonly name: string\n  /** Used for Content-Length header and range calculations */\n  readonly size: number\n  /** Used for Content-Type header */\n  readonly type: string\n  /** Used for Last-Modified header and weak ETag generation */\n  readonly lastModified: number\n  /** Used for streaming the response body */\n  stream(): ReadableStream<Uint8Array>\n  /** Used for strong ETag digest calculation */\n  arrayBuffer(): Promise<ArrayBuffer>\n  /** Used for range requests (206 Partial Content) */\n  slice(\n    start?: number,\n    end?: number,\n    contentType?: string,\n  ): { stream(): ReadableStream<Uint8Array> }\n}\n\n/**\n * Custom function for computing file digests.\n *\n * @param file The file to hash\n * @returns The computed digest as a string\n *\n * @example\n * async (file) => {\n *   let buffer = await file.arrayBuffer()\n *   return customHash(buffer)\n * }\n */\nexport type FileDigestFunction<file extends FileLike = File> = (file: file) => Promise<string>\n\n/**\n * Options for creating a file response with {@link createFileResponse}.\n */\nexport interface FileResponseOptions<file extends FileLike = File> {\n  /**\n   * Cache-Control header value. If not provided, no Cache-Control header will be set.\n   *\n   * @example 'public, max-age=31536000, immutable' // for hashed assets\n   * @example 'public, max-age=3600' // 1 hour\n   * @example 'no-cache' // always revalidate\n   */\n  cacheControl?: string\n  /**\n   * ETag generation strategy.\n   *\n   * - `'weak'`: Generates weak ETags based on file size and last modified time (`W/\"<size>-<mtime>\"`)\n   * - `'strong'`: Generates strong ETags by hashing file content (requires digest computation)\n   * - `false`: Disables ETag generation\n   *\n   * @default 'weak'\n   */\n  etag?: false | 'weak' | 'strong'\n  /**\n   * Hash algorithm or custom digest function for strong ETags.\n   *\n   * - String: Web Crypto API algorithm name ('SHA-256', 'SHA-384', 'SHA-512', 'SHA-1').\n   *   Note: Using strong ETags will buffer the entire file into memory before hashing.\n   *   Consider using weak ETags (default) or a custom digest function for large files.\n   * - Function: Custom digest computation that receives a file and returns the digest string\n   *\n   * Only used when `etag: 'strong'`. Ignored for weak ETags.\n   *\n   * @default 'SHA-256'\n   * @example async (file) => await customHash(file)\n   */\n  digest?: AlgorithmIdentifier | FileDigestFunction<file>\n  /**\n   * Whether to include `Last-Modified` headers.\n   *\n   * @default true\n   */\n  lastModified?: boolean\n  /**\n   * Whether to support HTTP `Range` requests for partial content.\n   *\n   * When enabled, includes `Accept-Ranges` header and handles `Range` requests\n   * with 206 Partial Content responses.\n   *\n   * Defaults to enabling ranges only for non-compressible MIME types,\n   * as defined by `isCompressibleMimeType()` from `@remix-run/mime`.\n   *\n   * Note: Range requests and compression are mutually exclusive. When\n   * `Accept-Ranges: bytes` is present in the response headers, the compression\n   * middleware will not compress the response. This is why the default behavior\n   * enables ranges only for non-compressible types.\n   */\n  acceptRanges?: boolean\n}\n\n/**\n * Creates a file [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * with full HTTP semantics including ETags, Last-Modified, conditional requests, and Range support.\n *\n * Accepts both native `File` objects and\n * {@link import('@remix-run/lazy-file').LazyFile} values.\n *\n * @param file The file to send (native `File` or `LazyFile`)\n * @param request The request object\n * @param options Configuration options\n * @returns A `Response` object containing the file\n *\n * @example\n * import { createFileResponse } from 'remix/response/file'\n * import { openLazyFile } from 'remix/fs'\n *\n * let lazyFile = openLazyFile('./public/image.jpg')\n * return createFileResponse(lazyFile, request, {\n *   cacheControl: 'public, max-age=3600'\n * })\n */\nexport async function createFileResponse<file extends FileLike>(\n  file: file,\n  request: Request,\n  options: FileResponseOptions<file> = {},\n): Promise<Response> {\n  let {\n    cacheControl,\n    etag: etagStrategy = 'weak',\n    digest: digestOption = 'SHA-256',\n    lastModified: lastModifiedEnabled = true,\n    acceptRanges: acceptRangesOption,\n  } = options\n\n  let headers = request.headers\n\n  let contentType = mimeTypeToContentType(file.type)\n  let contentLength = file.size\n\n  let etag: string | undefined\n  if (etagStrategy === 'weak') {\n    etag = generateWeakETag(file)\n  } else if (etagStrategy === 'strong') {\n    let digest = await computeDigest(file, digestOption)\n    etag = `\"${digest}\"`\n  }\n\n  let lastModified: number | undefined\n  if (lastModifiedEnabled) {\n    lastModified = file.lastModified\n  }\n\n  // Determine if we should accept ranges\n  // Default: enable ranges only for non-compressible MIME types\n  let acceptRangesEnabled =\n    acceptRangesOption !== undefined ? acceptRangesOption : !isCompressibleMimeType(contentType)\n\n  let acceptRanges: 'bytes' | undefined\n  if (acceptRangesEnabled) {\n    acceptRanges = 'bytes'\n  }\n\n  let hasIfMatch = headers.has('If-Match')\n\n  // If-Match support: https://httpwg.org/specs/rfc9110.html#field.if-match\n  if (etag && hasIfMatch) {\n    let ifMatch = IfMatch.from(headers.get('if-match'))\n    if (!ifMatch.matches(etag)) {\n      return new Response('Precondition Failed', {\n        status: 412,\n        headers: buildResponseHeaders({\n          etag,\n          lastModified,\n          acceptRanges,\n        }),\n      })\n    }\n  }\n\n  // If-Unmodified-Since support: https://httpwg.org/specs/rfc9110.html#field.if-unmodified-since\n  if (lastModified && !hasIfMatch) {\n    let ifUnmodifiedSinceHeader = headers.get('if-unmodified-since')\n    if (ifUnmodifiedSinceHeader != null) {\n      let ifUnmodifiedSince = new Date(ifUnmodifiedSinceHeader)\n      if (removeMilliseconds(lastModified) > removeMilliseconds(ifUnmodifiedSince)) {\n        return new Response('Precondition Failed', {\n          status: 412,\n          headers: buildResponseHeaders({\n            etag,\n            lastModified,\n            acceptRanges,\n          }),\n        })\n      }\n    }\n  }\n\n  // If-None-Match support: https://httpwg.org/specs/rfc9110.html#field.if-none-match\n  // If-Modified-Since support: https://httpwg.org/specs/rfc9110.html#field.if-modified-since\n  if (etag || lastModified) {\n    let shouldReturnNotModified = false\n    let ifNoneMatch = IfNoneMatch.from(headers.get('if-none-match'))\n\n    if (etag && ifNoneMatch.matches(etag)) {\n      shouldReturnNotModified = true\n    } else if (lastModified && ifNoneMatch.tags.length === 0) {\n      let ifModifiedSinceHeader = headers.get('if-modified-since')\n      if (ifModifiedSinceHeader != null) {\n        let ifModifiedSince = new Date(ifModifiedSinceHeader)\n        if (removeMilliseconds(lastModified) <= removeMilliseconds(ifModifiedSince)) {\n          shouldReturnNotModified = true\n        }\n      }\n    }\n\n    if (shouldReturnNotModified) {\n      return new Response(null, {\n        status: 304,\n        headers: buildResponseHeaders({\n          etag,\n          lastModified,\n          acceptRanges,\n        }),\n      })\n    }\n  }\n\n  // Range support: https://httpwg.org/specs/rfc9110.html#field.range\n  // If-Range support: https://httpwg.org/specs/rfc9110.html#field.if-range\n  if (acceptRanges && request.method === 'GET' && headers.has('Range')) {\n    let range = Range.from(headers.get('range'))\n\n    // Check if the Range header was sent but parsing resulted in no valid ranges (malformed)\n    if (range.ranges.length === 0) {\n      return new Response('Bad Request', {\n        status: 400,\n      })\n    }\n\n    // If-Range support: https://httpwg.org/specs/rfc9110.html#field.if-range\n    let ifRange = IfRange.from(headers.get('if-range'))\n    if (\n      ifRange.matches({\n        etag,\n        lastModified,\n      })\n    ) {\n      if (!range.canSatisfy(file.size)) {\n        return new Response('Range Not Satisfiable', {\n          status: 416,\n          headers: buildResponseHeaders({\n            contentRange: ContentRange.from({ unit: 'bytes', size: file.size }),\n          }),\n        })\n      }\n\n      let normalizedRanges = range.normalize(file.size)\n\n      // We only support single ranges (not multipart)\n      if (normalizedRanges.length > 1) {\n        return new Response('Range Not Satisfiable', {\n          status: 416,\n          headers: buildResponseHeaders({\n            contentRange: ContentRange.from({ unit: 'bytes', size: file.size }),\n          }),\n        })\n      }\n\n      let { start, end } = normalizedRanges[0]\n      let { size } = file\n\n      return new Response(file.slice(start, end + 1).stream(), {\n        status: 206,\n        headers: buildResponseHeaders({\n          contentType,\n          contentLength: end - start + 1,\n          contentRange: { unit: 'bytes', start, end, size },\n          etag,\n          lastModified,\n          cacheControl,\n          acceptRanges,\n        }),\n      })\n    }\n  }\n\n  return new Response(request.method === 'HEAD' ? null : file.stream(), {\n    status: 200,\n    headers: buildResponseHeaders({\n      contentType,\n      contentLength,\n      etag,\n      lastModified,\n      cacheControl,\n      acceptRanges,\n    }),\n  })\n}\n\nfunction generateWeakETag(file: FileLike): string {\n  return `W/\"${file.size}-${file.lastModified}\"`\n}\n\ninterface ResponseHeaderValues {\n  contentType?: string\n  contentLength?: number\n  contentRange?: ContentRangeInit\n  etag?: string\n  lastModified?: number\n  cacheControl?: string\n  acceptRanges?: 'bytes'\n}\n\nfunction buildResponseHeaders(values: ResponseHeaderValues): Headers {\n  let headers = new Headers()\n\n  if (values.contentType) {\n    headers.set('Content-Type', values.contentType)\n  }\n  if (values.contentLength != null) {\n    headers.set('Content-Length', String(values.contentLength))\n  }\n  if (values.contentRange) {\n    let str = ContentRange.from(values.contentRange).toString()\n    if (str) headers.set('Content-Range', str)\n  }\n  if (values.etag) {\n    headers.set('ETag', values.etag)\n  }\n  if (values.lastModified != null) {\n    headers.set('Last-Modified', new Date(values.lastModified).toUTCString())\n  }\n  if (values.cacheControl) {\n    headers.set('Cache-Control', values.cacheControl)\n  }\n  if (values.acceptRanges) {\n    headers.set('Accept-Ranges', values.acceptRanges)\n  }\n\n  return headers\n}\n\n/**\n * Computes a digest (hash) for a file.\n *\n * @param file The file to hash\n * @param digestOption Web Crypto algorithm name or custom digest function\n * @returns The computed digest as a hex string\n */\nasync function computeDigest<file extends FileLike>(\n  file: file,\n  digestOption: AlgorithmIdentifier | FileDigestFunction<file>,\n): Promise<string> {\n  return typeof digestOption === 'function'\n    ? await digestOption(file)\n    : await hashFile(file, digestOption)\n}\n\n/**\n * Hashes a file using Web Crypto API.\n *\n * Note: This loads the entire file into memory before hashing. For large files,\n * consider using weak ETags (default) or providing a custom digest function.\n *\n * @param file The file to hash\n * @param algorithm Web Crypto API algorithm name (default: 'SHA-256')\n * @returns The hash as a hex string\n */\nasync function hashFile<F extends FileLike>(\n  file: F,\n  algorithm: AlgorithmIdentifier = 'SHA-256',\n): Promise<string> {\n  let buffer = await file.arrayBuffer()\n  let hashBuffer = await crypto.subtle.digest(algorithm, buffer)\n  return Array.from(new Uint8Array(hashBuffer))\n    .map((b) => b.toString(16).padStart(2, '0'))\n    .join('')\n}\n\n/**\n * Removes milliseconds from a timestamp, returning seconds.\n * HTTP dates only have second precision, so this is useful for date comparisons.\n *\n * @param time The timestamp or Date to truncate\n * @returns The timestamp in seconds (milliseconds removed)\n */\nfunction removeMilliseconds(time: number | Date): number {\n  let timestamp = time instanceof Date ? time.getTime() : time\n  return Math.floor(timestamp / 1000)\n}\n"
  },
  {
    "path": "packages/response/src/lib/html.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\nimport { html as safeHtml } from '@remix-run/html-template'\n\nimport { createHtmlResponse } from './html.ts'\n\ndescribe('createHtmlResponse()', () => {\n  it('creates a Response with HTML content-type header', async () => {\n    let response = createHtmlResponse('<h1>Hello</h1>')\n    assert.equal(response.headers.get('Content-Type'), 'text/html; charset=UTF-8')\n    assert.equal(await response.text(), '<!DOCTYPE html><h1>Hello</h1>')\n  })\n\n  it('preserves custom headers and status from init', async () => {\n    let response = createHtmlResponse('<h1>Hello</h1>', {\n      headers: { 'X-Custom': 'a' },\n      status: 201,\n    })\n    assert.equal(response.headers.get('Content-Type'), 'text/html; charset=UTF-8')\n    assert.equal(response.headers.get('X-Custom'), 'a')\n    assert.equal(response.status, 201)\n    assert.equal(await response.text(), '<!DOCTYPE html><h1>Hello</h1>')\n  })\n\n  it('allows overriding Content-Type header', async () => {\n    let response = createHtmlResponse('<h1>Hello</h1>', {\n      headers: { 'Content-Type': 'text/plain' },\n    })\n    assert.equal(response.headers.get('Content-Type'), 'text/plain')\n  })\n\n  it('accepts SafeHtml from escape tag without re-escaping', async () => {\n    let snippet = safeHtml`<strong>${'Hi'}</strong>`\n    let response = createHtmlResponse(snippet)\n    assert.equal(await response.text(), '<!DOCTYPE html><strong>Hi</strong>')\n  })\n\n  describe('DOCTYPE prepending', () => {\n    describe('string body', () => {\n      it('prepends DOCTYPE to string body', async () => {\n        let response = createHtmlResponse('<html><body>Hello</body></html>')\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('does not prepend DOCTYPE if already present', async () => {\n        let response = createHtmlResponse('<!DOCTYPE html><html><body>Hello</body></html>')\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('handles DOCTYPE with leading whitespace', async () => {\n        let response = createHtmlResponse('  <!DOCTYPE html><html><body>Hello</body></html>')\n        assert.equal(await response.text(), '  <!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('handles DOCTYPE case-insensitively', async () => {\n        let response = createHtmlResponse('<!doctype html><html><body>Hello</body></html>')\n        assert.equal(await response.text(), '<!doctype html><html><body>Hello</body></html>')\n      })\n    })\n\n    describe('SafeHtml body', () => {\n      it('prepends DOCTYPE to SafeHtml body', async () => {\n        let snippet = safeHtml`<html><body>Hello</body></html>`\n        let response = createHtmlResponse(snippet)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('does not prepend DOCTYPE if already present in SafeHtml', async () => {\n        let snippet = safeHtml`<!DOCTYPE html><html><body>Hello</body></html>`\n        let response = createHtmlResponse(snippet)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n    })\n\n    describe('Blob body', () => {\n      it('prepends DOCTYPE to Blob body', async () => {\n        let blob = new Blob(['<html><body>Hello</body></html>'], { type: 'text/html' })\n        let response = createHtmlResponse(blob)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('does not prepend DOCTYPE if already present in Blob', async () => {\n        let blob = new Blob(['<!DOCTYPE html><html><body>Hello</body></html>'], {\n          type: 'text/html',\n        })\n        let response = createHtmlResponse(blob)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n    })\n\n    describe('ArrayBuffer body', () => {\n      it('prepends DOCTYPE to ArrayBuffer body', async () => {\n        let buffer = new TextEncoder().encode('<html><body>Hello</body></html>')\n        let response = createHtmlResponse(buffer.buffer)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('does not prepend DOCTYPE if already present in ArrayBuffer', async () => {\n        let buffer = new TextEncoder().encode('<!DOCTYPE html><html><body>Hello</body></html>')\n        let response = createHtmlResponse(buffer.buffer)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n    })\n\n    describe('Uint8Array body', () => {\n      it('prepends DOCTYPE to Uint8Array body', async () => {\n        let buffer = new TextEncoder().encode('<html><body>Hello</body></html>')\n        let response = createHtmlResponse(buffer)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('does not prepend DOCTYPE if already present in Uint8Array', async () => {\n        let buffer = new TextEncoder().encode('<!DOCTYPE html><html><body>Hello</body></html>')\n        let response = createHtmlResponse(buffer)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n    })\n\n    describe('DataView body', () => {\n      it('prepends DOCTYPE to DataView body', async () => {\n        let buffer = new TextEncoder().encode('<html><body>Hello</body></html>')\n        let dataView = new DataView(buffer.buffer)\n        let response = createHtmlResponse(dataView)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('does not prepend DOCTYPE if already present in DataView', async () => {\n        let buffer = new TextEncoder().encode('<!DOCTYPE html><html><body>Hello</body></html>')\n        let dataView = new DataView(buffer.buffer)\n        let response = createHtmlResponse(dataView)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n    })\n\n    describe('ReadableStream body', () => {\n      it('prepends DOCTYPE to ReadableStream body', async () => {\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('<html><body>Hello</body></html>'))\n            controller.close()\n          },\n        })\n        let response = createHtmlResponse(stream)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('does not prepend DOCTYPE if already present in ReadableStream', async () => {\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(\n              new TextEncoder().encode('<!DOCTYPE html><html><body>Hello</body></html>'),\n            )\n            controller.close()\n          },\n        })\n        let response = createHtmlResponse(stream)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n\n      it('handles empty ReadableStream', async () => {\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.close()\n          },\n        })\n        let response = createHtmlResponse(stream)\n        assert.equal(await response.text(), '<!DOCTYPE html>')\n      })\n\n      it('handles multi-chunk ReadableStream', async () => {\n        let stream = new ReadableStream({\n          start(controller) {\n            controller.enqueue(new TextEncoder().encode('<html>'))\n            controller.enqueue(new TextEncoder().encode('<body>'))\n            controller.enqueue(new TextEncoder().encode('Hello</body></html>'))\n            controller.close()\n          },\n        })\n        let response = createHtmlResponse(stream)\n        assert.equal(await response.text(), '<!DOCTYPE html><html><body>Hello</body></html>')\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/response/src/lib/html.ts",
    "content": "import { isSafeHtml, type SafeHtml } from '@remix-run/html-template'\n\nconst DOCTYPE = '<!DOCTYPE html>'\n\ntype HtmlBody = string | SafeHtml | Blob | BufferSource | ReadableStream<Uint8Array>\n\n/**\n * Creates an HTML [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * that ensures the response has a valid DOCTYPE and appropriate `Content-Type` header.\n *\n * @param body The body of the response\n * @param init The `ResponseInit` object for the response\n * @returns A `Response` object with a HTML body and the appropriate `Content-Type` header\n */\nexport function createHtmlResponse(body: HtmlBody, init?: ResponseInit): Response {\n  let payload: BodyInit = ensureDoctype(body)\n\n  let headers = new Headers(init?.headers)\n  if (!headers.has('Content-Type')) {\n    headers.set('Content-Type', 'text/html; charset=UTF-8')\n  }\n\n  return new Response(payload, { ...init, headers })\n}\n\nfunction ensureDoctype(body: HtmlBody): BodyInit {\n  if (isSafeHtml(body)) {\n    let str = String(body)\n    return startsWithDoctype(str) ? str : DOCTYPE + str\n  }\n\n  if (typeof body === 'string') {\n    return startsWithDoctype(body) ? body : DOCTYPE + body\n  }\n\n  if (body instanceof Blob) {\n    return prependDoctypeToStream(body.stream())\n  }\n\n  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {\n    let text = new TextDecoder().decode(body)\n    return startsWithDoctype(text) ? text : DOCTYPE + text\n  }\n\n  if (body instanceof ReadableStream) {\n    return prependDoctypeToStream(body)\n  }\n\n  return body\n}\n\nfunction startsWithDoctype(str: string): boolean {\n  return /^\\s*<!doctype html/i.test(str)\n}\n\nfunction prependDoctypeToStream(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {\n  let doctypeBytes = new TextEncoder().encode(DOCTYPE)\n  let reader = stream.getReader()\n\n  return new ReadableStream({\n    async start(controller) {\n      try {\n        // Read first chunk to check for DOCTYPE\n        let firstChunk = await reader.read()\n\n        if (firstChunk.done) {\n          // Empty stream, just add DOCTYPE\n          controller.enqueue(doctypeBytes)\n          controller.close()\n          return\n        }\n\n        // Check if the first chunk starts with DOCTYPE\n        let text = new TextDecoder().decode(firstChunk.value, { stream: true })\n        if (startsWithDoctype(text)) {\n          // Already has DOCTYPE, pass through\n          controller.enqueue(firstChunk.value)\n        } else {\n          // Prepend DOCTYPE\n          controller.enqueue(doctypeBytes)\n          controller.enqueue(firstChunk.value)\n        }\n\n        // Pass through remaining chunks\n        while (true) {\n          let { done, value } = await reader.read()\n          if (done) break\n          controller.enqueue(value)\n        }\n\n        controller.close()\n      } catch (error) {\n        controller.error(error)\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/response/src/lib/redirect.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createRedirectResponse } from './redirect.ts'\n\ndescribe('createRedirectResponse()', () => {\n  it('creates a redirect with default 302 status', () => {\n    let response = createRedirectResponse('/home')\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/home')\n  })\n\n  it('creates a redirect with custom status code', () => {\n    let response = createRedirectResponse('/login', 301)\n\n    assert.equal(response.status, 301)\n    assert.equal(response.headers.get('Location'), '/login')\n  })\n\n  it('accepts ResponseInit object as second parameter', () => {\n    let response = createRedirectResponse('/dashboard', {\n      status: 307,\n      headers: { 'X-Redirect-Reason': 'authentication' },\n    })\n\n    assert.equal(response.status, 307)\n    assert.equal(response.headers.get('Location'), '/dashboard')\n    assert.equal(response.headers.get('X-Redirect-Reason'), 'authentication')\n  })\n\n  it('handles relative URLs', () => {\n    let response = createRedirectResponse('../parent', 303)\n\n    assert.equal(response.status, 303)\n    assert.equal(response.headers.get('Location'), '../parent')\n  })\n\n  it('allows overriding Location header', () => {\n    let response = createRedirectResponse('/default', {\n      headers: { Location: '/override' },\n    })\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/override')\n  })\n\n  it('allows overriding Location header with Headers object', () => {\n    let headers = new Headers()\n    headers.set('Location', '/custom-location')\n    headers.set('X-Custom', 'value')\n\n    let response = createRedirectResponse('/default', { headers })\n\n    assert.equal(response.status, 302)\n    assert.equal(response.headers.get('Location'), '/custom-location')\n    assert.equal(response.headers.get('X-Custom'), 'value')\n  })\n\n  it('accepts a URL object', () => {\n    let response = createRedirectResponse(new URL('https://example.com/login'), 301)\n    assert.equal(response.status, 301)\n    assert.equal(response.headers.get('Location'), 'https://example.com/login')\n  })\n})\n"
  },
  {
    "path": "packages/response/src/lib/redirect.ts",
    "content": "/**\n * Creates a redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).\n *\n * @alias redirect\n * @param location The location to redirect to\n * @param init The `ResponseInit` object for the response, or a status code\n * @returns A `Response` object with a redirect header\n */\nexport function createRedirectResponse(\n  location: string | URL,\n  init?: ResponseInit | number,\n): Response {\n  let status = 302\n  if (typeof init === 'number') {\n    status = init\n    init = undefined\n  }\n\n  let headers = new Headers(init?.headers)\n  if (!headers.has('Location')) {\n    headers.set('Location', typeof location === 'string' ? location : location.toString())\n  }\n\n  return new Response(null, { status, ...init, headers })\n}\n"
  },
  {
    "path": "packages/response/src/redirect.ts",
    "content": "export {\n  createRedirectResponse,\n  createRedirectResponse as redirect, // shorthand\n} from './lib/redirect.ts'\n"
  },
  {
    "path": "packages/response/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/response/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/route-pattern/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/route-pattern/.changes/minor.readonly-ast.md",
    "content": "BREAKING CHANGE: `RoutePattern.ast` is now typed as deeply readonly.\n\nThis was always the intended design; the type system now reflects it:\n\n```ts\n// Before\npattern.ast = { ...pattern.ast, protocol: 'https' }\npattern.ast.protocol = 'https'\npattern.ast.port = '443'\npattern.ast.hostname = null\npattern.ast.pathname = otherPattern.ast.pathname\npattern.ast.search.set('q', new Set(['x']))\npattern.ast.pathname.tokens.push({ type: 'text', text: 'x' })\npattern.ast.pathname.optionals.set(0, 1)\n\n// After\npattern.ast = { ...pattern.ast, protocol: 'https' }\n//      ~~~\n// Cannot assign to 'ast' because it is a read-only property. (2703)\n\npattern.ast.protocol = 'https'\n//          ~~~~~~~~~\n// Cannot assign to 'protocol' because it is a read-only property. (2540)\n\npattern.ast.port = '443'\n//          ~~~~\n// Cannot assign to 'port' because it is a read-only property. (2540)\n\npattern.ast.hostname = null\n//          ~~~~~~~~\n// Cannot assign to 'hostname' because it is a read-only property. (2540)\n\npattern.ast.pathname = otherPattern.ast.pathname\n//          ~~~~~~~~\n// Cannot assign to 'pathname' because it is a read-only property. (2540)\n\npattern.ast.search.set('q', new Set(['x']))\n//                 ~~~\n// Property 'set' does not exist on type 'ReadonlyMap<string, ReadonlySet<string> | null>'. (2339)\n\npattern.ast.pathname.tokens.push({ type: 'text', text: 'x' })\n//                          ~~~~\n// Property 'push' does not exist on type 'ReadonlyArray<PartPatternToken>'. (2339)\n\npattern.ast.pathname.optionals.set(0, 1)\n//                             ~~~\n// Property 'set' does not exist on type 'ReadonlyMap<number, number>'. (2339)\n```\n"
  },
  {
    "path": "packages/route-pattern/.changes/patch.type-inference-perf.md",
    "content": "Faster type inference for `RoutePattern.href`, `RoutePattern.match`, and `Params`\n\nReduced type instantiations for parsing param types, resulting in\n~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.\nMay fix `\"Type instantiation is excessively deep and possibly infinite\" (ts2589)` for some apps.\n"
  },
  {
    "path": "packages/route-pattern/CHANGELOG.md",
    "content": "# `route-pattern` CHANGELOG\n\nThis is the changelog for [`route-pattern`](https://github.com/remix-run/remix/tree/main/packages/route-pattern). It follows [semantic versioning](https://semver.org/).\n\n## v0.19.0\n\n### Minor Changes\n\n- BREAKING CHANGE: Remove `RoutePatternOptions` type and rework `ignoreCase`\n\n  `RoutePattern.ignoreCase` field has been removed and `ignoreCase` now only applies to `pathname` (no longer applies to `search`)\n\n  Case sensitivity is now determined only when matching.\n\n  - `RoutePattern.match` now accept `ignoreCase` option\n  - `Matcher` constructors now accept `ignoreCase` option\n\n  ```ts\n  // BEFORE\n  let pattern = new RoutePattern('/Posts/:id', { ignoreCase: true })\n  pattern.match(url)\n  pattern.join(other, { ignoreCase: true })\n\n  let matcher = new ArrayMatcher()\n\n  // AFTER\n  let pattern = new RoutePattern('/Posts/:id')\n  pattern.match(url) // default: ignoreCase = false\n  pattern.match(url, { ignoreCase: true })\n  pattern.join(other)\n\n  let arrayMatcher = new ArrayMatcher() // default: ignoreCase = false\n  // OR\n  let arrayMatcher = new ArrayMatcher({ ignoreCase: true })\n\n  let trieMatcher = new TrieMatcher() // default: ignoreCase = false\n  // OR\n  let trieMatcher = new TrieMatcher({ ignoreCase: true })\n  ```\n\n- BREAKING CHANGE: Change how params are represented within `RoutePattern.ast`\n\n  Previously, `RoutePattern.ast.{hostname,pathname}.tokens` had param tokens like:\n\n  ```ts\n  type ParamToken = { type: ':'; '*'; nameIndex: number }\n  ```\n\n  where the `nameIndex` was used to access the param name from `paramNames`:\n\n  ```ts\n  let { pathname } = pattern.ast\n\n  for (let token of pathname.tokens) {\n    if (token.type === ':' || token.type === '*') {\n      let paramName = pathname.paramNames[token.nameIndex]\n      console.log('name: ', paramName)\n    }\n  }\n  ```\n\n  This has now been simplified so that param tokens contain their own name:\n\n  ```ts\n  type ParamToken = { type: ':' | '*'; name: string }\n\n  let { pathname } = pattern.ast\n\n  for (let token of pathname.tokens) {\n    if (token.type === ':' || token.type === '*') {\n      console.log('name: ', token.name)\n    }\n  }\n  ```\n\n  If you want to iterate over _just_ the params, there's a new `.params` getter:\n\n  ```ts\n  let { pathname } = pattern.ast\n\n  for (let param of pathname.params) {\n    console.log('type: ', param.type)\n    console.log('name: ', param.name)\n  }\n  ```\n\n- BREAKING CHANGE: Rename match `meta` to `paramsMeta`\n\n  For `RoutePattern.match` and `type RoutePatternMatch`:\n\n  ```ts\n  import { RoutePattern, type RoutePatternMatch } from 'remix/route-pattern'\n\n  let pattern = new RoutePattern('...')\n  let match = pattern.match(url)\n\n  // BEFORE\n  type Meta = RoutePatternMatch['meta']\n  match.meta\n\n  // AFTER\n  type ParamsMeta = RoutePatternMatch['paramsMeta']\n  match.paramsMeta\n  ```\n\n  For `Matcher.match` and `type Match`:\n\n  ```ts\n  import { Matcher, type Match } from 'remix/route-pattern'\n\n  let matcher: Matcher = new ArrayMatcher() // Or TrieMatcher\n\n  let match = matcher.match(url)\n\n  // BEFORE\n  type Meta = Match['meta']\n  match.meta\n\n  // AFTER\n  type ParamsMeta = Match['paramsMeta']\n  match.paramsMeta\n  ```\n\n### Patch Changes\n\n- Previously, `href` was throwing an `HrefError` with `missing-params` type when a nameless wildcard was encountered outside of an optional.\n  But that was misleading since nameless optionals aren't something the user should be passing in values for.\n  Instead, `href` now throws an `HrefError` with the correct `nameless-wildcard` type for this case.\n\n  Error messages have also been improved for many of the `HrefError` types.\n  Notably, the variants shown in `missing-params` were confusing since they leaked internal formatting for params.\n  That has been removed and the resulting error message is now shorter and simpler.\n\n- Previously, including extra params in `RoutePattern.href` resulted in a type error:\n\n  ```ts\n  let pattern = new RoutePattern('/posts/:id')\n  pattern.href({ id: 1, extra: 'stuff' })\n  //                     ^^^^^\n  // 'extra' does not exist in type 'HrefParams<\"/posts/:id\">'\n  ```\n\n  Now, extra params are allowed and autocomplete for inferred params still works:\n\n  ```ts\n  let pattern = new RoutePattern('/posts/:id')\n  pattern.href({ id: 1, extra: 'stuff' }) // no type error\n\n  pattern.href({})\n  //             ^ autocomplete suggests `id`\n  ```\n\n- `ArrayMatcher.match` (optimized for small apps) got ~1.06x faster for our small app benchmark.\n  `TrieMatcher.match` (optimized for large apps) got ~1.17x faster across the board.\n\n- Patterns with omitted port only match URLs with empty port `''`\n\n  Previously, there was a bug that caused omitted ports in patterns to match any ports.\n\n- `paramsMeta` shows a nameless wildcard match for omitted hostname\n\n  An omitted hostname is already coerced to `*` (nameless wildcard) to represent \"match any hostname\" during matching.\n  Previously, `paramsMeta` did not distinguish between a fully static hostname and an omitted hostname as both had `hostname` set to `[]`.\n  Now, `paramsMeta` returns a nameless wildcard match for the entire hostname when the hostname is omitted.\n\n  Example:\n\n  ```ts\n  const pattern = new RoutePattern('/users/:id')\n  const match = pattern.match('http://example.com/users/123')\n  // match.paramsMeta.hostname is now [{ type: '*', name: '*', begin: 0, end: 11, value: 'example.com' }]\n  ```\n\n  As a result, `Specificity.descending` (the default ordering for matchers) now correctly orders patterns with static hostname before patterns with omitted hostnames.\n\n- `TrieMatcher` allows overlapping routes\n\n  For example:\n\n  ```ts\n  let pattern1 = new RoutePattern('://api.example.com/users/:id')\n  let pattern2 = new RoutePattern('://api.example.com/users(/:id)')\n\n  matcher.add(pattern1)\n  matcher.add(pattern2)\n  ```\n\n  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.\n\n  Now, `TrieMatcher` allows overlapping routes by storing an array of route patterns in the trie nodes.\n\n## v0.18.0\n\n### Minor Changes\n\n- BREAKING CHANGE: Remove `createHrefBuilder`, `type HrefBuilder`, `type HrefBuilderArg`\n\n  `createHrefBuilder` was the original design and implementation of href generation,\n  but with the new `RoutePattern.href` method it is now obsolete.\n\n  Use `HrefArgs` instead of `HrefBuilderArgs`:\n\n  ```ts\n  // before\n  type Args = HrefBuilderArgs<Source>\n\n  // after\n  type Args = HrefArgs<Source>\n  ```\n\n- BREAKING CHANGE: simplify protocol to only accept `http`, `https`, and `http(s)`\n\n  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).\n\n  That means params or arbitrary optionals are no longer allowed within the protocol and will result in a `ParseError`.\n\n### Patch Changes\n\n- Add `ast` property to `RoutePattern`\n\n  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.\n\n  ```ts\n  let ast: AST = pattern.ast\n\n  type AST = {\n    protocol: PartPattern\n    hostname: PartPattern\n    port: string | null\n    pathname: PartPattern\n    search: SearchConstraints\n  }\n  ```\n\n  ```ts\n  type PartPattern = {\n    tokens: Array<Token>\n    paramNames: Array<string>\n    /** Map of `(` token index to its corresponding `)` token index for optional segments */\n    optionals: Map<number, number>\n    separator: '.' | '/' | ''\n  }\n\n  type Token =\n    | { type: 'text'; text: string }\n    | { type: 'separator' }\n    | { type: '(' | ')' }\n    | { type: ':' | '*'; nameIndex: number } // nameIndex references paramNames array\n\n  // `posts/:id(/edit)`\n  let part: PartPattern = {\n    tokens: [\n      { type: 'text', text: 'posts' },\n      { type: 'separator' },\n      { type: ':', nameIndex: 0 },\n      { type: '(' },\n      { type: 'separator' },\n      { type: 'text', text: 'edit' },\n      { type: ')' },\n    ],\n    paramNames: ['id'],\n    optionals: new Map([[3, 6]]), // token at index 3 '(' maps to token at index 6 ')'\n    separator: '/',\n  }\n  ```\n\n  ```ts\n  type SearchConstraints = Map<string, Set<string> | null>\n\n  // - `null`: key must be present (matches ?q, ?q=, ?q=1)\n  // - Empty Set: key must be present with a value (matches ?q=1)\n  // - Non-empty Set: key must be present with all these values (matches ?q=x&q=y)\n  ```\n\n- Add getters to `RoutePattern`\n\n  The `protocol`, `hostname`, `port`, `pathname`, and `search` getters display the normalized pattern parts as strings.\n\n  ```ts\n  let pattern = new RoutePattern('https://:tenant.example.com:3000/:lang/docs/*?version=:version')\n\n  pattern.protocol // 'https'\n  pattern.hostname // ':tenant.example.com'\n  pattern.port // '3000'\n  pattern.pathname // ':lang/docs/*'\n  pattern.search // 'version=:version'\n  ```\n\n  Omitted parts return empty strings.\n\n- Add `meta` to match returned by `RoutePattern.match()`\n\n  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.\n\n  ```ts\n  import * as assert from 'node:assert/strict'\n\n  let pattern = new RoutePattern('https://:tenant.example.com/:lang/docs/*')\n  let match = pattern.match('https://acme.example.com/en/docs/api/routes')\n\n  assert.deepEqual(match.params, { tenant: 'acme', lang: 'en' })\n  assert.deepEqual(match.meta.hostname, [\n    { type: ':', name: 'tenant', value: 'acme', begin: 0, end: 4 },\n  ])\n  assert.deepEqual(match.meta.pathname, [\n    { type: ':', name: 'lang', value: 'en', begin: 0, end: 2 },\n    { type: '*', name: '*', value: 'api/routes', begin: 8, end: 18 },\n  ])\n  ```\n\n- Add functions for comparing match specificity\n\n  Specificity is our intuitive metric for finding the \"best\" match.\n\n  ```ts\n  import * as Specificity from '@remix-run/route-pattern/specificity'\n\n  Specificity.lessThan(a, b) // `true` when `a` is more specific than `b`. `false` otherwise\n  Specificity.greaterThan(a, b)\n  Specificity.equal(a, b)\n\n  matches.sort(Specificity.ascending)\n  matches.sort(Specificity.descending)\n  ```\n\n  Specificity compares patterns char-by-char where static matches beat variable matches, which beat wildcard matches.\n\n  ```typescript\n  import { RoutePattern } from '@remix-run/route-pattern'\n  import * as Specificity from '@remix-run/route-pattern/specificity'\n  import * as assert from 'node:assert/strict'\n\n  let url = 'https://example.com/posts/new'\n\n  let pattern1 = new RoutePattern('/posts/:id')\n  let pattern2 = new RoutePattern('/posts/new')\n\n  let match1 = pattern1.match(url)\n  let match2 = pattern2.match(url)\n\n  assert.ok(Specificity.lessThan(match1, match2))\n  ```\n\n  **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:\n\n  ```typescript\n  import * as assert from 'node:assert/strict'\n\n  let url = 'https://app-api.example.com'\n\n  let pattern1 = new RoutePattern('https://app-*.example.com')\n  let match1 = pattern1.match(url)\n\n  let pattern2 = new RoutePattern('https://*-api.example.com')\n  let match2 = pattern2.match(url)\n\n  assert.ok(Specificity.lessThan(match1, match2))\n  ```\n\n## v0.17.0\n\n### Minor Changes\n\n- BREAKING CHANGE: Remove exports for `TrieMatcher` and `TrieMatcherOptions`\n\n  `TrieMatcher` prototype produces inconsistent matches based on ad hoc scoring.\n  That means that swapping `ArrayMatcher` for `TrieMatcher` could alter which route was picked as the best match for a given URL.\n\n  We'll restore the `TrieMatcher` export after it produces correct, consistent matches.\n\n## v0.16.0 (2025-12-18)\n\n- BREAKING CHANGE: Rename `RegExpMatcher` to `ArrayMatcher`\n\n## v0.15.3 (2025-11-19)\n\n- Exclude benchmark files from published npm package\n\n## v0.15.2 (2025-11-19)\n\n- Exclude test files from published npm package\n\n## v0.15.1 (2025-11-19)\n\n- `href()` now filters out `undefined` and `null` values from search parameters, preventing them from appearing in the generated URL's query string\n- `href()` no longer adds a trailing `?` when search parameters are empty\n\n## v0.15.0 (2025-11-05)\n\n- Build using `tsc` instead of `esbuild`. This means modules in the `dist` directory now mirror the layout of modules in the `src` directory.\n\n## v0.14.0 (2025-10-04)\n\n- Add `Matcher` and `MatchResult` interfaces. These are new public APIs for matching sets of patterns.\n- Add `RegExpMatcher` and `TrieMatcher` concrete implementations of the `Matcher` interface\n\n  - `RegExpMatcher` is a simple array-based matcher that compiles route patterns to regular expressions.\n  - `TrieMatcher` is a trie-based matcher optimized for large route sets and long-running server applications.\n\n  ```tsx\n  import { TrieMatcher } from '@remix-run/route-pattern'\n\n  let matcher = new TrieMatcher<{ name: string }>()\n  matcher.add('users/:id', { name: 'user' })\n  matcher.add('posts/:id', { name: 'post' })\n\n  let match = matcher.match('https://example.com/users/123')\n  // { data: { name: 'user' }, params: { id: '123' }, url: ... }\n  ```\n\n## v0.13.0 (2025-09-29)\n\n- BREAKING CHANGE: removed `createRoutes` and corresponding types (`RouteMap`, `RouteDefs`, and `RouteDef`). This functionality will be re-introduced in a future \"router\" package.\n- BREAKING CHANGE: removed `RouteMap` from `createHrefBuilder` generic type.\n- Expose `Join` type as public API\n- Expose `HrefBuilderArgs` type as public API\n- Optimization: compile patterns as needed instead of on instantiation\n\n## v0.12.0 (2025-09-25)\n\n- BREAKING CHANGE: removed `options` arg from `createHrefBuilder`\n- BREAKING CHANGE: removed support for enum patterns\n- Add `pattern.href(...args)` method for generating URLs from patterns\n\n  ```tsx\n  import { RoutePattern } from '@remix-run/route-pattern'\n\n  let pattern = new RoutePattern('users/:id')\n  pattern.href({ id: '123' }) // \"/users/123\"\n  ```\n\n- 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.\n\n  ```tsx\n  import { createRoutes } from '@remix-run/route-pattern'\n\n  let routes = createRoutes({\n    home: '/',\n    blog: {\n      index: '/blog',\n      post: '/blog/:slug',\n    },\n  })\n\n  routes.home.match('https://remix.run/')\n  // { params: {} }\n  routes.blog.post.match('https://remix.run/blog/my-post')\n  // { params: { slug: 'my-post' } }\n\n  routes.blog.post.href({ slug: 'my-post' }) // \"/blog/my-post\"\n  ```\n\n  A `RouteMap` also works as a generic to `createHrefBuilder()` to restrict the set of patterns that may be used as the first argument.\n\n  ```tsx\n  import { createHrefBuilder } from '@remix-run/route-pattern'\n\n  let href = createHrefBuilder<typeof routes>()\n  href('/blog/:slug', { slug: 'my-post' }) // \"/blog/my-post\"\n  ```\n\n- Add `pattern.join(input, options)`, which allows a pattern to be built relative\n  to another pattern\n\n  ```tsx\n  import { RoutePattern } from '@remix-run/route-pattern'\n\n  let base = new RoutePattern('https://remix.run/api')\n  let pattern = base.join('users/:id')\n  pattern.source // \"https://remix.run/api/users/:id\"\n  ```\n\n- Export `RouteMatch` type as public API\n- Allow `null` and `undefined` as values for optional params\n\n## v0.11.0 (2025-09-11)\n\n- `createHrefBuilder<T>` now accepts a `RoutePattern` directly instead of just `string`s\n- `Variant<T>` preserves leading slashes in pathname-only patterns\n\n## v0.10.0 (2025-09-04)\n\n- BREAKING CHANGE: removed `match.protocol`, `match.hostname`, `match.port`, `match.pathname`, `match.search`, and `match.searchParams`. Use `match.url` instead\n- Fix search matching and add more fine-grained examples\n\n## v0.9.1 (2025-09-04)\n\n- Fix handling of patterns with leading slash\n- Make variables not greedy\n\n```tsx\nlet pattern = new RoutePattern('/:id(.json)')\n// Before :id was greedy and would consume \".json\"\npattern.match('https://remix.run/123.json')\n// { params: { id: '123.json' } }\n// After\npattern.match('https://remix.run/123.json')\n// { params: { id: '123' } }\n```\n\n- Allow search params values to have type `string | number | bigint | boolean` and automatically stringify\n\n## v0.9.0 (2025-09-03)\n\n- 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)`\n- Fix `protocol` and `hostname` to always ignore case given in the pattern\n- Add `ignoreCase` option to `RoutePattern` constructor to match URL pathnames in a case-insensitive way\n\n```tsx\nlet pattern = new RoutePattern('https://remix.run/users/:id', { ignoreCase: true })\npattern.match('https://remix.run/Users/123') // { ..., params: { id: '123' } }\n```\n\n## v0.8.0 (2025-09-03)\n\n- Any valid pattern is also valid in `href(pattern)`\n- Href generation with missing optional variables omits the optional section entirely\n\n```tsx\nlet href = createHrefBuilder()\nhref('products(/:id)', { id: 'remix' }) // /products/remix\n\n// These all used to fail, but are now OK!\nhref('products(/:id)') // /products\nhref('products(/:id)', {}) // /products\nhref('products(/:id)', { id: null }) // /products (type error)\nhref('products(/:id)', { id: undefined }) // /products (type error)\n```\n\n- Param values may be `string | number | bigint | boolean` and are automatically stringified\n\n```tsx\nlet href = createHrefBuilder()\n\n// These used to be a type errors, but are now OK!\nhref('products(/:id)', { id: 1 }) // /products/1\nhref('products(/:id)', { id: false }) // /products/false\n```\n\n## v0.7.0 (2025-09-01)\n\n- Add support for nested optionals in route patterns\n\n```tsx\n// Now you can do stuff like\nlet pattern = new RoutePattern('api(/v:major(.:minor))')\npattern.match('https://remix.run/api') // { params: {} }\npattern.match('https://remix.run/api/v1') // { params: { major: '1' } }\npattern.match('https://remix.run/api/v1.2') // { params: { major: '1', minor : '2' } }\n```\n\n- Make `pattern.match().params` type-safe\n- Export top-level `Params<pattern>` helper for extracting params from a pattern\n- Tighten up some types in `href()`. Now you get variants for\n  - all the different values of an enum\n  - unnamed wildcards\n- Fix bug when using unnamed wildcards in `href()`\n\n## v0.6.0 (2025-08-29)\n\n- Use a single RegExp to match protocol, hostname, port, and pathname\n- Allow duplicate variable names in patterns, right-most shows up in `match.params`\n- Allow route patterns to match on port\n- All variables require names, wildcards may have a name or be \"unnamed\"\n\n## v0.4.0 (2025-07-24)\n\n- Renamed package from `@mjackson/route-pattern` to `@remix-run/route-pattern`\n"
  },
  {
    "path": "packages/route-pattern/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/route-pattern/README.md",
    "content": "# route-pattern\n\nType-safe URL matching and href generation for JavaScript. `route-pattern` supports path params, wildcards, optionals, and full-URL patterns with predictable ranking.\n\n## Features\n\n- **Type-Safe Params** - Infer params from patterns for compile-time route correctness\n- **Flexible Pattern Syntax** - Variables, wildcards, optionals, and query constraints\n- **Full URL Support** - Match protocol, host, pathname, and search params\n- **Deterministic Ranking** - Static segments beat params, and params beat wildcards\n- **Runtime Agnostic** - Works across Node.js, Bun, Deno, Cloudflare Workers, and browsers\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Quick Example\n\n```ts\nimport { RoutePattern } from 'remix/route-pattern'\n\nlet blog = new RoutePattern('blog/:slug')\nblog.match('https://remix.run/blog/v3') // { params: { slug: 'v3' } }\nblog.href({ slug: 'v3' }) // '/blog/v3'\n\nlet api = new RoutePattern('api(/v:version)/*path')\napi.match('https://api.com/api/v2/users/profile') // { params: { version: '2', path: 'users/profile' } }\napi.href({ version: '2', path: 'users/profile' }) // '/api/v2/users/profile'\napi.href({ path: 'users/profile' }) // '/api/users/profile'\n\nlet cdn = new RoutePattern('http(s)://:region.cdn.com/assets/*file.:ext')\ncdn.match('https://us-west.cdn.com/assets/images/logo.png') // { params: { region: 'us-west', file: 'images/logo', ext: 'png' } }\ncdn.href({ region: 'us-west', file: 'images/logo', ext: 'png' }) // 'https://us-west.cdn.com/assets/images/logo.png'\n```\n\n## Intuitive syntax\n\n**Variables** capture dynamic segments using `:name`:\n\n```ts\nnew RoutePattern('users/:id') // matches /users/123\nnew RoutePattern('blog/:year-:month-:day/:slug') // matches /blog/2024-01-15/hello\n```\n\n**Wildcards** match multi-segment paths using `*name`:\n\n```ts\nnew RoutePattern('files/*path') // matches /files/images/logo.png\nnew RoutePattern('node_modules/*package/dist/index.js') // matches /node_modules/@remix-run/router/dist/index.js\nnew RoutePattern('files/*') // matches any path under /files, but doesn't capture the value for the wildcard\n```\n\n**Optionals** make parts optional using `()`:\n\n```ts\nnew RoutePattern('api(/v:version)/users') // matches /api/users AND /api/v2/users\nnew RoutePattern('blog/:slug(.html)') // matches /blog/hello AND /blog/hello.html\nnew RoutePattern('docs(/guides/:category)') // multiple segments optional: /docs OR /docs/guides/routing\nnew RoutePattern('api(/v:major(.:minor))') // nested optionals: /api, /api/v2, /api/v2.1\n```\n\n**Search params** narrow matches using `?key` or `?key=value`:\n\n```ts\nnew RoutePattern('search?q') // requires ?q in URL\nnew RoutePattern('search?q=') // requires ?q with any value\nnew RoutePattern('search?q=routing') // requires ?q=routing exactly\n```\n\n**Flexible matching** for partial URL patterns:\n\n```ts\nnew RoutePattern('blog/:slug') // omits protocol/hostname, matches any origin\nnew RoutePattern('://example.com/api') // omits protocol, matches http and https\nnew RoutePattern('search?q') // allows additional search params beyond ?q\n```\n\n## Matchers\n\nMatch URLs against multiple patterns. Each pattern can have associated data (handlers, route IDs, metadata, etc.):\n\n```ts\nimport { ArrayMatcher as Matcher } from 'remix/route-pattern'\n\n// Any data type you want!  👇\nlet matcher = new Matcher<string>()\n\nmatcher.add('/', 'home')\nmatcher.add('blog/:slug', 'blog-post')\nmatcher.add('api(/v:version)/*path', 'api')\n\nmatcher.match('https://example.com/blog/v3')\n// { pattern: 'blog/:slug', params: { slug: 'v3' }, data: 'blog-post' }\n\nmatcher.match('https://example.com/api/v2/users/profile')\n// { pattern: 'api(/v:version)/*path', params: { version: '2', path: 'users/profile' }, data: 'api' }\n```\n\n**ArrayMatcher vs TrieMatcher**\n\n- **ArrayMatcher**: Best for small apps (~80 routes or fewer)\n- **TrieMatcher**: Best for large apps (hundreds of routes)\n\nNote: Performance depends on your specific patterns—benchmark both to verify which is faster for your app.\n\nBoth implement the `Matcher` API so you can swap them out easily:\n\n```ts\n// import { ArrayMatcher as Matcher } from 'remix/route-pattern'\nimport { TrieMatcher as Matcher } from 'remix/route-pattern'\n```\n\n## Specificity\n\nWhen multiple patterns match a URL, the most specific pattern wins.\n\n**Pathname specificity** (left-to-right):\n\n```ts\nimport { ArrayMatcher } from 'remix/route-pattern'\n\nlet matcher = new ArrayMatcher<string>()\nmatcher.add('blog/hello', 'static')\nmatcher.add('blog/:slug', 'variable')\nmatcher.add('blog/*path', 'wildcard')\nmatcher.add('*path', 'catch-all')\n\nmatcher.match('https://example.com/blog/hello')\n// { pattern: 'blog/hello', params: {}, data: 'static' }\n// 'blog/hello' wins: static segments beat variables/wildcards at each position\n```\n\n**Search parameter specificity**:\n\n```ts\nlet router = new ArrayMatcher<string>()\nrouter.add('search', 'no-params')\nrouter.add('search?q', 'has-q')\nrouter.add('search?q=', 'has-q-with-value')\nrouter.add('search?q=hello', 'exact-match')\n\nrouter.match('https://example.com/search?q=hello')\n// { pattern: 'search?q=hello', params: {}, data: 'exact-match' }\n// More constrained search params = more specific\n```\n\n## Benchmark\n\nTo run benchmarks comparing `route-pattern` performance with comparable libraries:\n\n```sh\npnpm bench bench/comparison.bench.ts\n```\n\n## Related Work\n\n- [`path-to-regexp`](https://www.npmjs.com/package/path-to-regexp)\n- [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/route-pattern/bench/.gitignore",
    "content": "*.json"
  },
  {
    "path": "packages/route-pattern/bench/README.md",
    "content": "# Benchmarks\n\n## Runtime benchmarks\n\nRuntime benchmarks are in [src/](./src/) and use [Vitest benchmarking](https://vitest.dev/guide/features.html#benchmarking).\n\n```sh\n# All benchmarks\npnpm bench\n\n# Specific benchmark\npnpm bench comparison.bench.json # full name\npnpm bench comparison            # pattern match\n```\n\n### Compare performance across branches\n\n```bash\ngit checkout main\npnpm bench comparison.bench.ts --outputJson=main.json\n\ngit checkout feature-branch\npnpm bench comparison.bench.ts --compare=main.json\n```\n\n## Type benchmarks\n\nType benchmarks are in [types/](./types/) and use [ArkType Attest](https://github.com/arktypeio/arktype/blob/main/ark/attest/README.md).\n\n```sh\n# Run type benchmarks directly with Node\nnode types/href.ts\n```\n"
  },
  {
    "path": "packages/route-pattern/bench/patterns/mediarss.ts",
    "content": "/** From https://github.com/kentcdodds/mediarss/blob/main/app/config/routes.ts */\nexport const patterns = [\n  '/feed/:token',\n  '/media/:token/*path',\n  '/art/:token/*path',\n  // OAuth routes (public)\n  '/oauth/token',\n  '/oauth/jwks',\n  '/oauth/register',\n  '/.well-known/oauth-authorization-server',\n  // MCP routes\n  '/mcp',\n  '/.well-known/oauth-protected-resource/mcp',\n  '/mcp/widget/:token/*path',\n  // Admin routes\n  '/admin/health',\n  '/admin/api/version',\n  '/admin/authorize',\n  '/admin',\n  '/admin/*path',\n  '/admin/api/feeds',\n  '/admin/api/directories',\n  '/admin/api/browse',\n  '/admin/api/feeds/directory',\n  '/admin/api/feeds/curated',\n  '/admin/api/feeds/:id',\n  '/admin/api/feeds/:id/tokens',\n  '/admin/api/feeds/:id/items',\n  '/admin/api/feeds/:id/artwork',\n  '/admin/api/tokens/:token',\n  '/admin/api/media',\n  '/admin/api/media/assignments',\n  '/admin/api/media/*path',\n  '/admin/api/media/*path/metadata',\n  '/admin/api/media-stream/*path',\n  '/admin/api/media/upload',\n  '/admin/api/artwork/*path',\n] as const\n"
  },
  {
    "path": "packages/route-pattern/bench/patterns/shopify.ts",
    "content": "export const patterns = [\n  '*',\n  '(:locale)/wrec-for-agencies',\n  '(:locale)/walmart',\n  '(:locale)/webinars',\n  '(:locale)/welcomekickstarter',\n  '(:locale)/upgrade-and-grow-bundle',\n  '(:locale)/tools/shipping-label-template',\n  '(:locale)/tools/qr-code-generator/show/:imageId',\n  '(:locale)/tools/qr-code-generator',\n  '(:locale)/tools/slogan-maker',\n  '(:locale)/tools/purchase-order-template/:vertical',\n  '(:locale)/tools/purchase-order-template',\n  '(:locale)/tools/profit-margin-calculator/:vertical',\n  '(:locale)/tools/profit-margin-calculator',\n  '(:locale)/tools/pay-stub-generator',\n  '(:locale)/tools/policy-generator/:vertical',\n  '(:locale)/tools/policy-generator',\n  '(:locale)/tools/invoice-generator',\n  '(:locale)/tools/logo-maker/result',\n  '(:locale)/tools/logo-maker/finalize',\n  '(:locale)/tools/logo-maker/data-gathering',\n  '(:locale)/tools/logo-maker/editor/icons',\n  '(:locale)/tools/logo-maker/editor/data',\n  '(:locale)/tools/logo-maker/editor',\n  '(:locale)/tools/logo-maker',\n  '(:locale)/tools/impressum-generator',\n  '(:locale)/tools/image-resizer',\n  '(:locale)/tools/domain-name-generator/search',\n  '(:locale)/tools/domain-name-generator',\n  '(:locale)/tools/business-name-generator/terms',\n  '(:locale)/tools/business-name-generator/:vertical',\n  '(:locale)/tools/business-name-generator',\n  '(:locale)/tools/business-loan-calculator',\n  '(:locale)/tools/business-card-maker/:vertical',\n  '(:locale)/tools/business-card-maker',\n  '(:locale)/tools/barcode-generator',\n  '(:locale)/tools/ai-store-builder/:generationId/loading',\n  '(:locale)/tools/ai-store-builder/:generationId/result',\n  '(:locale)/tools/ai-store-builder',\n  '(:locale)/tools/sitemap.xml',\n  '(:locale)/tools/bill-of-lading',\n  '(:locale)/tools/generated/:urlToPdf',\n  '(:locale)/tools',\n  '(:locale)/ucp',\n  '(:locale)/tax-platform',\n  '(:locale)/tax',\n  '(:locale)/test-auth',\n  '(:locale)/store-signup',\n  '(:locale)/subscriptions',\n  '(:locale)/starter',\n  '(:locale)/start',\n  '(:locale)/sonsie',\n  '(:locale)/store-login',\n  '(:locale)/sitemap/case-studies.xml',\n  '(:locale)/sitemap/case-studies',\n  '(:locale)/sitemap/:blog',\n  '(:locale)/sitemap',\n  '(:locale)/sidekick',\n  '(:locale)/sixty-day-hustle',\n  '(:locale)/shopify-by-invitation',\n  '(:locale)/shop-sign-in',\n  '(:locale)/shop-pay-installments/book-a-call',\n  '(:locale)/shop-pay-installments',\n  '(:locale)/shop-pay',\n  '(:locale)/shop-campaigns/book-a-call',\n  '(:locale)/shop-campaigns',\n  '(:locale)/shop-promise',\n  '(:locale)/shipping/usps',\n  '(:locale)/shipping/sendle',\n  '(:locale)/shipping/ups',\n  '(:locale)/shipping/royal-mail',\n  '(:locale)/shipping/dhl-canada',\n  '(:locale)/shipping/dhl',\n  '(:locale)/shipping/fedex',\n  '(:locale)/shipping/carriers',\n  '(:locale)/shipping/canada-post',\n  '(:locale)/shipping/book-a-call',\n  '(:locale)/shipping/australia-post',\n  '(:locale)/shipping',\n  '(:locale)/shop',\n  '(:locale)/sell-on-tiktok',\n  '(:locale)/sell-on-spotify',\n  '(:locale)/sell-on-wordpress',\n  '(:locale)/segmentation',\n  '(:locale)/sell/groceries',\n  '(:locale)/sell/:vertical',\n  '(:locale)/sell',\n  '(:locale)/security/transparency-report/:report',\n  '(:locale)/security/transparency-report',\n  '(:locale)/security/pci-compliant',\n  '(:locale)/security',\n  '(:locale)/retail-migration-bundle',\n  '(:locale)/search-and-discovery',\n  '(:locale)/retail/topics/:topic',\n  '(:locale)/retail/topics',\n  '(:locale)/retail/search',\n  '(:locale)/retail/latest',\n  '(:locale)/retail/:article',\n  '(:locale)/retail/authors/:author',\n  '(:locale)/retail',\n  '(:locale)/recreferralofferdetails2026',\n  '(:locale)/q4offerdetails',\n  '(:locale)/protect',\n  '(:locale)/product-network',\n  '(:locale)/privacy-dev',\n  '(:locale)/pricing',\n  '(:locale)/print-on-demand/:vertical',\n  '(:locale)/print-on-demand',\n  '(:locale)/products',\n  '(:locale)/pre-orders',\n  '(:locale)/pos/wine-store',\n  '(:locale)/pos/turn-visits-into-customers',\n  '(:locale)/pos/tap-to-pay-iphone',\n  '(:locale)/pos/tap-to-pay-android',\n  '(:locale)/pos/tap-to-pay',\n  '(:locale)/pos/switchnow',\n  '(:locale)/pos/staff-management',\n  '(:locale)/pos/sporting-goods-store',\n  '(:locale)/pos/simplify-in-store',\n  '(:locale)/pos/shopify-vs-square',\n  '(:locale)/pos/shopify-vs-clover',\n  '(:locale)/pos/shopify-vs-lightspeed',\n  '(:locale)/pos/retail-pos',\n  '(:locale)/pos/retail-customer-experience',\n  '(:locale)/pos/ski-store',\n  '(:locale)/pos/request-info',\n  '(:locale)/pos/pos-system-small-business',\n  '(:locale)/pos/pricing',\n  '(:locale)/pos/qb-discover',\n  '(:locale)/pos/pos-pro-yearly',\n  '(:locale)/pos/pos-software',\n  '(:locale)/pos/pos-app',\n  '(:locale)/pos/pop-up-sales',\n  '(:locale)/pos/pos-inventory-system',\n  '(:locale)/pos/payments',\n  '(:locale)/pos/omnichannel',\n  '(:locale)/pos/jewelry-store-pos',\n  '(:locale)/pos/meet-retail-collection',\n  '(:locale)/pos/ipad-pos',\n  '(:locale)/pos/hardware-paint-supply-store',\n  '(:locale)/pos/hardware',\n  '(:locale)/pos/golf-store',\n  '(:locale)/pos/gift-hobby-toy-store',\n  '(:locale)/pos/furniture-home-decor-store',\n  '(:locale)/pos/features',\n  '(:locale)/pos/demo/thank-you/free-trial',\n  '(:locale)/pos/demo/thank-you',\n  '(:locale)/pos/demo/contact',\n  '(:locale)/pos/demo',\n  '(:locale)/pos/customization',\n  '(:locale)/pos/contact',\n  '(:locale)/pos/clothing-shoes-store',\n  '(:locale)/pos/book-a-call',\n  '(:locale)/pos/bike-store',\n  '(:locale)/pos/android-pos',\n  '(:locale)/pos/multi-store-pos',\n  '(:locale)/pos/webinar/getting-started/thank-you',\n  '(:locale)/pos/webinar/getting-started',\n  '(:locale)/pos/unified-by-design/:persona',\n  '(:locale)/pos/free-trial/sell-retail',\n  '(:locale)/pos/free-trial/card-reader',\n  '(:locale)/pos/free-trial/faire-shopify',\n  '(:locale)/pos/unified-experience',\n  '(:locale)/pos/unified-data',\n  '(:locale)/pos/tco',\n  '(:locale)/pos/accelerate-innovation',\n  '(:locale)/pos/resources/top-tips-in-store-experience',\n  '(:locale)/pos/resources/square-switcher-report',\n  '(:locale)/pos/resources/retail-upmarket-guide/download',\n  '(:locale)/pos/resources/retail-upmarket-guide',\n  '(:locale)/pos/resources/retail-pos-buyers-guide',\n  '(:locale)/pos/resources/pos-migration-guide',\n  '(:locale)/pos/resources/pos-total-cost-ownership-report',\n  '(:locale)/pos/resources/lightspeed-switcher-report',\n  '(:locale)/pos/resources/home-and-garden-guide/download',\n  '(:locale)/pos/resources/home-and-garden-guide',\n  '(:locale)/pos/resources/apparel-pos-buyers-guide/download',\n  '(:locale)/pos/resources/apparel-pos-buyers-guide',\n  '(:locale)/pos',\n  '(:locale)/plus/upgrade',\n  '(:locale)/plus/unified-international-selling',\n  '(:locale)/plus/unified-retail',\n  '(:locale)/plus/switch',\n  '(:locale)/plus/sitemap',\n  '(:locale)/plus/sell',\n  '(:locale)/plus/referral',\n  '(:locale)/plus/reconnect',\n  '(:locale)/plus/pricing',\n  '(:locale)/plus/platform-demo',\n  '(:locale)/plus/platform',\n  '(:locale)/plus/migration',\n  '(:locale)/plus/manage',\n  '(:locale)/plus/internal-referral',\n  '(:locale)/plus/integrate',\n  '(:locale)/plus/g2-abm-switch',\n  '(:locale)/plus/enterprise-ecommerce',\n  '(:locale)/plus/contact-sales-reengagement-nurture',\n  '(:locale)/plus/contact-sales-content-nurture',\n  '(:locale)/plus/contact',\n  '(:locale)/plus/build-for-flow',\n  '(:locale)/plus/abm-unified-retail',\n  '(:locale)/plus/abm-unified-international-selling',\n  '(:locale)/plus/abm-unified-b2b',\n  '(:locale)/plus/sitemap.xml',\n  '(:locale)/plus/unified-b2b',\n  '(:locale)/plus/solutions/shipping',\n  '(:locale)/plus/solutions/retail-and-point-of-sale',\n  '(:locale)/plus/solutions/payments',\n  '(:locale)/plus/solutions/omnichannel-commerce',\n  '(:locale)/plus/solutions/headless-commerce',\n  '(:locale)/plus/solutions/ecommerce-campaigns-flash-sale-automation',\n  '(:locale)/plus/solutions/ecommerce-automation',\n  '(:locale)/plus/solutions/international-ecommerce',\n  '(:locale)/plus/solutions/b2b-ecommerce',\n  '(:locale)/plus/solutions/online-store',\n  '(:locale)/plus/site-speed/:prospect',\n  '(:locale)/plus/industries/home-furnishing',\n  '(:locale)/plus/industries/food-beverage',\n  '(:locale)/plus/industries/fashion-apparel',\n  '(:locale)/plus/industries/consumer-electronics',\n  '(:locale)/plus/industries/beauty-cosmetics',\n  '(:locale)/plus/industry-reports/fashion-and-apparel',\n  '(:locale)/plus',\n  '(:locale)/paypal',\n  '(:locale)/payment-gateways',\n  '(:locale)/payments/book-a-call',\n  '(:locale)/payments',\n  '(:locale)/paymentsplatformapplication',\n  '(:locale)/partners-devreferralofferdetails012025',\n  '(:locale)/partners/terms',\n  '(:locale)/partners/technology-partners',\n  '(:locale)/partners/shopify-cheat-sheet',\n  '(:locale)/partners/service-partners',\n  '(:locale)/partners/investment-partners',\n  '(:locale)/partners/resources',\n  '(:locale)/partners/find-a-partner',\n  '(:locale)/partners/fast-track',\n  '(:locale)/partners/education',\n  '(:locale)/partners/directory/technologies/:category/:service',\n  '(:locale)/partners/directory/technologies/:category',\n  '(:locale)/partners/directory/technologies',\n  '(:locale)/partners/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  '(:locale)/partners/directory/services/:category/:service',\n  '(:locale)/partners/directory/services/:category',\n  '(:locale)/partners/directory/services',\n  '(:locale)/partners/directory/locations/:country',\n  '(:locale)/partners/directory/locations',\n  '(:locale)/partners/directory/partner/:partner',\n  '(:locale)/partners/directory/merchant-review/:token',\n  '(:locale)/partners/directory/api/search',\n  '(:locale)/partners/directory',\n  '(:locale)/partners/blog/topics/:topic',\n  '(:locale)/partners/blog/topics',\n  '(:locale)/partners/blog/search',\n  '(:locale)/partners/blog/latest',\n  '(:locale)/partners/blog/:article',\n  '(:locale)/partners/blog/authors/:author',\n  '(:locale)/partners/blog',\n  '(:locale)/partners/featured-partner/google-cloud',\n  '(:locale)/partners',\n  '(:locale)/orders',\n  '(:locale)/online',\n  '(:locale)/onboarding-webinar',\n  '(:locale)/news/search',\n  '(:locale)/news/finance',\n  '(:locale)/news/feed',\n  '(:locale)/news/capital',\n  '(:locale)/news/about-us',\n  '(:locale)/news/:page',\n  '(:locale)/news/category/:category',\n  '(:locale)/news',\n  '(:locale)/mrbeast',\n  '(:locale)/migrate',\n  '(:locale)/markets',\n  '(:locale)/marketplace-connect',\n  '(:locale)/marketing-automation-tools',\n  '(:locale)/marketing-automation',\n  '(:locale)/marketing',\n  '(:locale)/mobile',\n  '(:locale)/manage',\n  '(:locale)/magic',\n  '(:locale)/logout',\n  '(:locale)/logistics',\n  '(:locale)/local-delivery',\n  '(:locale)/legal/tools/subprocessor-notification',\n  '(:locale)/legal/tools/report-an-issue/threat-of-violence',\n  '(:locale)/legal/tools/report-an-issue/trademark-infringement',\n  '(:locale)/legal/tools/report-an-issue/terrorist-organization',\n  '(:locale)/legal/tools/report-an-issue/theme-license-appeal/:token',\n  '(:locale)/legal/tools/report-an-issue/theme-license-appeal',\n  '(:locale)/legal/tools/report-an-issue/spam',\n  '(:locale)/legal/tools/report-an-issue/report-a-partner-violation',\n  '(:locale)/legal/tools/report-an-issue/report-a-merchant',\n  '(:locale)/legal/tools/report-an-issue/order-issue',\n  '(:locale)/legal/tools/report-an-issue/inquiry',\n  '(:locale)/legal/tools/report-an-issue/illegal-activities',\n  '(:locale)/legal/tools/report-an-issue/personal-information',\n  '(:locale)/legal/tools/report-an-issue/fraud',\n  '(:locale)/legal/tools/report-an-issue/dmca',\n  '(:locale)/legal/tools/report-an-issue/child-exploitation',\n  '(:locale)/legal/tools/report-an-issue/financial-complaint',\n  '(:locale)/legal/tools/report-an-issue/access-request',\n  '(:locale)/legal/tools/report-an-issue/dispute/:token/download',\n  '(:locale)/legal/tools/report-an-issue/dispute/:token',\n  '(:locale)/legal/tools/report-an-issue',\n  '(:locale)/legal/tools',\n  '(:locale)/legal/terms-faq',\n  '(:locale)/legal/terms-payments/ca/complaint-handling',\n  '(:locale)/legal/terms-payments/ca',\n  '(:locale)/legal/terms-payments/*',\n  '(:locale)/legal/p2b/legal-notices',\n  '(:locale)/legal/p2b',\n  '(:locale)/legal/apple-pay',\n  '(:locale)/legal/:article',\n  '(:locale)/legal/fulfillment',\n  '(:locale)/legal/privacy/:article',\n  '(:locale)/legal/compliance/reports',\n  '(:locale)/legal',\n  '(:locale)/investors/stock-information',\n  '(:locale)/investors/resources',\n  '(:locale)/investors/media-center',\n  '(:locale)/investors/ir-direct',\n  '(:locale)/investors/financial-reports',\n  '(:locale)/investors/events',\n  '(:locale)/investors/committee-composition',\n  '(:locale)/investors/board-of-directors',\n  '(:locale)/investors/analyst-coverage',\n  '(:locale)/investors/:fallback-redirect',\n  '(:locale)/investors/sitemap.xml',\n  '(:locale)/investors/governance-documents',\n  '(:locale)/investors/press-releases/:article',\n  '(:locale)/investors/calendar/:event',\n  '(:locale)/investors',\n  '(:locale)/international/pricing',\n  '(:locale)/international/features',\n  '(:locale)/international/managed/ey-report/download',\n  '(:locale)/international/managed/ey-report',\n  '(:locale)/international/managed',\n  '(:locale)/international/managed/resources/ey-report/download',\n  '(:locale)/international/managed/resources/ey-report',\n  '(:locale)/international/managed/resources/dhl-express-report/download',\n  '(:locale)/international/managed/resources/dhl-express-report',\n  '(:locale)/international',\n  '(:locale)/install/pos',\n  '(:locale)/install',\n  '(:locale)/introducing-credit',\n  '(:locale)/inventory-management',\n  '(:locale)/inbox',\n  '(:locale)/ig25h1g1offerdetails',\n  '(:locale)/ig25h1g04offerdetails',\n  '(:locale)/ig25h1g5offerdetails',\n  '(:locale)/ig25h1g02offerdetails',\n  '(:locale)/ig25h1g3offerdetails',\n  '(:locale)/google-pay',\n  '(:locale)/google',\n  '(:locale)/fulfillment/pricing',\n  '(:locale)/fulfillment/features',\n  '(:locale)/fulfillment/book-a-call',\n  '(:locale)/fulfillment',\n  '(:locale)/go-international-bundle',\n  '(:locale)/free-trial-new-years',\n  '(:locale)/free-trial/editions/winter2025',\n  '(:locale)/free-trial',\n  '(:locale)/fraud-solutions',\n  '(:locale)/flow',\n  '(:locale)/forms',\n  '(:locale)/free-trial-offer',\n  '(:locale)/first-sale/:subpage',\n  '(:locale)/first-sale',\n  '(:locale)/find-products',\n  '(:locale)/faq',\n  '(:locale)/facebook-instagram',\n  '(:locale)/finance/theme',\n  '(:locale)/finance',\n  '(:locale)/examples',\n  '(:locale)/events',\n  '(:locale)/email-verified',\n  '(:locale)/emea-rec-for-agencies',\n  '(:locale)/email-marketing',\n  '(:locale)/enterprise/state-of-commerce-report-2024',\n  '(:locale)/enterprise/retail-report-2024',\n  '(:locale)/enterprise/premium-fashion-trends-report',\n  '(:locale)/enterprise/resources-center',\n  '(:locale)/enterprise/peak-season-report-2024',\n  '(:locale)/enterprise/blog/topics/:topic',\n  '(:locale)/enterprise/blog/topics',\n  '(:locale)/enterprise/blog/search',\n  '(:locale)/enterprise/blog/latest',\n  '(:locale)/enterprise/blog/sitemap.xml',\n  '(:locale)/enterprise/blog/:article',\n  '(:locale)/enterprise/blog/authors/:author',\n  '(:locale)/enterprise/blog',\n  '(:locale)/enterprise/*',\n  '(:locale)/enterprise/sitemap.xml',\n  '(:locale)/enterprise',\n  '(:locale)/dropshipping',\n  '(:locale)/dotdev',\n  '(:locale)/domains/shop',\n  '(:locale)/domains/:vertical',\n  '(:locale)/domains',\n  '(:locale)/discounts',\n  '(:locale)/customer-accounts',\n  '(:locale)/download-information',\n  '(:locale)/credit/book-a-call',\n  '(:locale)/credit/activate',\n  '(:locale)/credit',\n  '(:locale)/contact',\n  '(:locale)/complete-store',\n  '(:locale)/commerce-for-agents',\n  '(:locale)/compare/tco/results',\n  '(:locale)/compare/tco/webinar',\n  '(:locale)/compare/tco',\n  '(:locale)/compare/time-to-value',\n  '(:locale)/compare/sitespeed/results',\n  '(:locale)/compare/sitespeed/download',\n  '(:locale)/compare/sitespeed',\n  '(:locale)/compare/shopify-vs-woocommerce',\n  '(:locale)/compare/shopify-vs-square',\n  '(:locale)/compare/shopify-vs-squarespace',\n  '(:locale)/compare/shopify-vs-shopware',\n  '(:locale)/compare/shopify-vs-wix',\n  '(:locale)/compare/shopify-vs-salesforce-commercecloud',\n  '(:locale)/compare/shopify-vs-others',\n  '(:locale)/compare/shopify-vs-godaddy',\n  '(:locale)/compare/shopify-vs-etsy',\n  '(:locale)/compare/shopify-vs-ebay',\n  '(:locale)/compare/shopify-vs-lightspeed',\n  '(:locale)/compare/shopify-vs-commercetools',\n  '(:locale)/compare/shopify-vs-custom-platform',\n  '(:locale)/compare/shopify-vs-clover',\n  '(:locale)/compare/shopify-vs-amazon',\n  '(:locale)/compare/shopify-vs-adobe',\n  '(:locale)/compare/shopify-enterprise-vs-salesforce-commercecloud',\n  '(:locale)/compare/shopify-vs-bigcommerce',\n  '(:locale)/compare/shopify-enterprise-vs-adobe',\n  '(:locale)/compare/shopify-enterprise-vs-custom-platform',\n  '(:locale)/compare/:competitor',\n  '(:locale)/compare',\n  '(:locale)/collective',\n  '(:locale)/climate-report',\n  '(:locale)/climate/sustainability-fund',\n  '(:locale)/climate',\n  '(:locale)/checkout-kit',\n  '(:locale)/checkout',\n  '(:locale)/chatgpt',\n  '(:locale)/charge',\n  '(:locale)/channels',\n  '(:locale)/case-studies/:caseStudy',\n  '(:locale)/case-studies',\n  '(:locale)/careers/vip-access',\n  '(:locale)/careers/product-principles',\n  '(:locale)/careers/principal-engineering',\n  '(:locale)/careers/nda',\n  '(:locale)/careers/faq',\n  '(:locale)/careers/extraordinary',\n  '(:locale)/careers/candidate-privacy-notice',\n  '(:locale)/careers/revenue',\n  '(:locale)/careers/candidate-guide/world-view',\n  '(:locale)/careers/candidate-guide/why-it-might-be-hard',\n  '(:locale)/careers/candidate-guide/leadership',\n  '(:locale)/careers/candidate-guide/digital-by-design',\n  '(:locale)/careers/candidate-guide/compensation',\n  '(:locale)/careers/candidate-guide/are-you-ready',\n  '(:locale)/careers/candidate-guide/ai-usage',\n  '(:locale)/careers/candidate-guide',\n  '(:locale)/careers/sitemap.xml',\n  '(:locale)/careers/feed.xml',\n  '(:locale)/careers/:posting/apply/thank-you',\n  '(:locale)/careers/:posting',\n  '(:locale)/careers/uwaterloo/alumni',\n  '(:locale)/careers/teams/:team',\n  '(:locale)/careers/disciplines/design',\n  '(:locale)/careers/disciplines/:discipline',\n  '(:locale)/careers',\n  '(:locale)/carbon-commerce',\n  '(:locale)/capital/book-a-call',\n  '(:locale)/capital/bfcm',\n  '(:locale)/capital/upmarket/cash-advances-loans/book-a-call',\n  '(:locale)/capital',\n  '(:locale)/buy-button',\n  '(:locale)/bugbounty/rewards',\n  '(:locale)/bugbounty/resources/:resource',\n  '(:locale)/bugbounty/resources',\n  '(:locale)/bugbounty/faq',\n  '(:locale)/bugbounty/calculator_legacy',\n  '(:locale)/bugbounty/calculator',\n  '(:locale)/bugbounty/criteria',\n  '(:locale)/bugbounty/about',\n  '(:locale)/bugbounty',\n  '(:locale)/brand-assets',\n  '(:locale)/blog/topics/:topic',\n  '(:locale)/blog/topics',\n  '(:locale)/blog/search',\n  '(:locale)/blog/markets',\n  '(:locale)/blog/latest',\n  '(:locale)/blog/email-marketing',\n  '(:locale)/blog/:article',\n  '(:locale)/blog/authors/:author',\n  '(:locale)/blog/api/subscribe',\n  '(:locale)/blog',\n  '(:locale)/bill-pay',\n  '(:locale)/balance',\n  '(:locale)/b2b-demo-store/redirect',\n  '(:locale)/b2b-demo-store',\n  '(:locale)/auto-entrepreneur',\n  '(:locale)/audiences',\n  '(:locale)/apple-pay',\n  '(:locale)/analytics',\n  '(:locale)/amazon-pay',\n  '(:locale)/agentic-storefronts',\n  '(:locale)/agentic-plan',\n  '(:locale)/affiliates/:affiliate',\n  '(:locale)/affiliates',\n  '(:locale)/affiliate-active-confirmation',\n  '(:locale)/accessibility/:article',\n  '(:locale)/accessibility',\n  '(:locale)/about',\n  '(:locale)',\n  '(:locale)/shopify-security.pub',\n  '(:locale)/robots.txt',\n  '(:locale)/llms.txt',\n  '(:locale)/1mbb',\n  '(:locale)/welcome-to-shopify/:vertical',\n  '(:locale)/website/hosting',\n  '(:locale)/website/design',\n  '(:locale)/website/builder',\n  '(:locale)/waived-pos-fees/terms-and-conditions',\n  '(:locale)/toolkit/cto',\n  '(:locale)/toolkit/cmo',\n  '(:locale)/toolkit/ceo',\n  '(:locale)/tech-flexes/nestle',\n  '(:locale)/tech-flexes/crystal-ball',\n  '(:locale)/tech-flexes/nfc/:id',\n  '(:locale)/technology-partners/enterprise',\n  '(:locale)/support/enterprise',\n  '(:locale)/sp-fees-discount-existing-merchants/terms-and-conditions',\n  '(:locale)/solutions/b2b/enterprise',\n  '(:locale)/solutions/b2b',\n  '(:locale)/solutions/shop-component/enterprise',\n  '(:locale)/solutions/retail/enterprise',\n  '(:locale)/solutions/payments/enterprise',\n  '(:locale)/solutions/growth/enterprise',\n  '(:locale)/solutions/b2c/enterprise',\n  '(:locale)/solutions/automotive/enterprise',\n  '(:locale)/social/get-started',\n  '(:locale)/set-up-promotion/terms-and-conditions',\n  '(:locale)/rewards-promotion/terms-and-conditions',\n  '(:locale)/sales-bonus/terms-and-conditions',\n  '(:locale)/reactivation-promotion/terms-and-conditions',\n  '(:locale)/resources/seeing-around-corners',\n  '(:locale)/resources/idc-enterprise-marketscape',\n  '(:locale)/resources/gartner-magic-quadrant',\n  '(:locale)/resources/beauty-guide',\n  '(:locale)/resources/acing-ecommerce-southeast-asia-marketplaces-dtc',\n  '(:locale)/resources/:resource',\n  '(:locale)/resources/b2b-ecommerce',\n  '(:locale)/resources/templates/swot-analysis-template',\n  '(:locale)/resources/templates/sop-template',\n  '(:locale)/resources/templates/one-pager-template',\n  '(:locale)/resources/templates/profit-and-loss-statement-template',\n  '(:locale)/resources/templates/gantt-chart-template',\n  '(:locale)/resources/templates/creative-brief-templates',\n  '(:locale)/resources/templates/business-model-canvas-template',\n  '(:locale)/resources/templates/business-proposal-template',\n  '(:locale)/resources/retention/yotpo',\n  '(:locale)/resources/retention/forter',\n  '(:locale)/resources/retention/ordergroove',\n  '(:locale)/resources/retention/bloomreach',\n  '(:locale)/resources/enterprise/product-demos',\n  '(:locale)/referrals/terms-and-conditions',\n  '(:locale)/professional-services/enterprise',\n  '(:locale)/ppc/dropshipping/:incentive',\n  '(:locale)/ppc/dropshipping',\n  '(:locale)/ppc/compare',\n  '(:locale)/ppc/:area/:subarea/:vertical',\n  '(:locale)/ppc/:area/:subarea',\n  '(:locale)/ppc/:area',\n  '(:locale)/ppc/affiliates/foundr',\n  '(:locale)/plus-shopify-payments-rewards/terms-and-conditions',\n  '(:locale)/plus-shopify-payments-rewards-2025/terms-and-conditions',\n  '(:locale)/platform/enterprise',\n  '(:locale)/payments-promotion/terms-and-conditions',\n  '(:locale)/partner-solutions/enterprise/:solution',\n  '(:locale)/partner-solutions/enterprise',\n  '(:locale)/pages/:business',\n  '(:locale)/partner-migration-promotion/terms-and-conditions',\n  '(:locale)/new-year-gmv-promotion/terms-and-conditions',\n  '(:locale)/lending/term-loans',\n  '(:locale)/landing/plus-ppc',\n  '(:locale)/lightspeed-migration-promotion/terms-and-conditions',\n  '(:locale)/learn/*',\n  '(:locale)/hardware-promotion/terms-and-conditions',\n  '(:locale)/gmv-promotion/terms-and-conditions',\n  '(:locale)/education-partner/:affiliate',\n  '(:locale)/editions/winter2023',\n  '(:locale)/editions/summer2022/dev',\n  '(:locale)/editions/summer2022',\n  '(:locale)/editions/webinar/summer2024',\n  '(:locale)/domain-promotion/terms-and-conditions',\n  '(:locale)/damelio-footwear/terms-and-conditions',\n  '(:locale)/credit-fs/book-a-call',\n  '(:locale)/commerce-coach/:affiliate',\n  '(:locale)/collabs/flow',\n  '(:locale)/collabs/find-influencers',\n  '(:locale)/collabs/creators',\n  '(:locale)/colin-and-samir/youtube/auth',\n  '(:locale)/colin-and-samir/youtube',\n  '(:locale)/colin-and-samir/terms-and-conditions',\n  '(:locale)/climate-report-test/:article',\n  '(:locale)/cash-promotion/terms-and-conditions',\n  '(:locale)/card-reader-promotion/terms-and-conditions',\n  '(:locale)/card-reader-discount/terms-and-conditions',\n  '(:locale)/capital-fs/book-a-call',\n  '(:locale)/billpay-fs/book-a-call',\n  '(:locale)/billpay/book-a-call',\n  '(:locale)/webinar/winter-26-edition',\n  '(:locale)/webinar/winter-25-edition',\n  '(:locale)/webinar/whats-new-pos',\n  '(:locale)/webinar/upgradetoplus-firesidechat',\n  '(:locale)/webinar/upgradetoplus',\n  '(:locale)/webinar/unlocking-customer-value-oct-2023',\n  '(:locale)/webinar/unified-commerce',\n  '(:locale)/webinar/unified-commerce-buzzword-or-business-strategy',\n  '(:locale)/webinar/uk-winter-25-edition',\n  '(:locale)/webinar/uk-compare-sitespeed',\n  '(:locale)/webinar/talking-summer-25-edition',\n  '(:locale)/webinar/supercharging-sales-apparel-webinar',\n  '(:locale)/webinar/time-to-value',\n  '(:locale)/webinar/supercharge-your-growth-first-party-data',\n  '(:locale)/webinar/summer-25-edition',\n  '(:locale)/webinar/summer-25-edition-au',\n  '(:locale)/webinar/spcc',\n  '(:locale)/webinar/test-private-webinar',\n  '(:locale)/webinar/smart-money-moves',\n  '(:locale)/webinar/shopify-tax-filing',\n  '(:locale)/webinar/shopify-epam-commerce-transformation',\n  '(:locale)/webinar/shopify-ey-webinar',\n  '(:locale)/webinar/shopify-email-onboarding',\n  '(:locale)/webinar/shopify-deloitte-oracle-cdp',\n  '(:locale)/webinar/shop-campaigns-for-beginners',\n  '(:locale)/webinar/shipping-demo',\n  '(:locale)/webinar/retail-winter-edition-2025',\n  '(:locale)/webinar/shopify-vs-clover',\n  '(:locale)/webinar/retail-winter-edition-2025-de',\n  '(:locale)/webinar/retail-summer-edition-2025',\n  '(:locale)/webinar/retail-store-customer-acquisition',\n  '(:locale)/webinar/retail-keen-loop',\n  '(:locale)/webinar/retail-home-and-garden',\n  '(:locale)/webinar/retail-france-commerce-unified',\n  '(:locale)/webinar/retail-personalization-strategies-2026',\n  '(:locale)/webinar/retail-edition-inverno-2025',\n  '(:locale)/webinar/retail-pos-second-look',\n  '(:locale)/webinar/retail-cotopaxi-netsuite',\n  '(:locale)/webinar/resources-shopify-ey-webinar',\n  '(:locale)/webinar/record-attendance',\n  '(:locale)/webinar/rebuy-apac',\n  '(:locale)/webinar/pos-workmate-bike-shops',\n  '(:locale)/webinar/rebuy',\n  '(:locale)/webinar/pos-ui-extensions',\n  '(:locale)/webinar/pos-total-cost-ownership-webinar',\n  '(:locale)/webinar/pos-partner-apps',\n  '(:locale)/webinar/rebuy-emea',\n  '(:locale)/webinar/pos-partner-accelerator-3',\n  '(:locale)/webinar/pos-enhance-instore-experiences',\n  '(:locale)/webinar/pos-outdoor-retailers',\n  '(:locale)/webinar/pos-10-group-demo-jun2025',\n  '(:locale)/webinar/pos-10-group-demo-dec2025',\n  '(:locale)/webinar/pos-10-group-demo-aug2025',\n  '(:locale)/webinar/pos-customization-stories',\n  '(:locale)/webinar/plus-marketing-tools',\n  '(:locale)/webinar/plusupgrade',\n  '(:locale)/webinar/plus-home-furniture-international-expansion',\n  '(:locale)/webinar/partners-website-essentials-001',\n  '(:locale)/webinar/partner-marketing-2025',\n  '(:locale)/webinar/partners-into-to-competitive-sales',\n  '(:locale)/webinar/partners-outbound-workshop',\n  '(:locale)/webinar/oct2025fasttrackapac',\n  '(:locale)/webinar/oct2025fasttrackemea',\n  '(:locale)/webinar/navigating-market-insights',\n  '(:locale)/webinar/oct2025fasttrackamer',\n  '(:locale)/webinar/migration-round-table-italia',\n  '(:locale)/webinar/migration-round-table-espana',\n  '(:locale)/webinar/migration-prestashop',\n  '(:locale)/webinar/migration-round-table-france',\n  '(:locale)/webinar/merz-b-schwanen',\n  '(:locale)/webinar/marketing-automation',\n  '(:locale)/webinar/last-min-peak-webinar',\n  '(:locale)/webinar/klaviyo-shopify',\n  '(:locale)/webinar/kickstart',\n  '(:locale)/webinar/july2025fasttrackemea',\n  '(:locale)/webinar/jul2025fasttrackamer',\n  '(:locale)/webinar/july2025fasttrackapac',\n  '(:locale)/webinar/laura-canada-ey-retail-transformation',\n  '(:locale)/webinar/it-winter-25-edition',\n  '(:locale)/webinar/it-compare-sitespeed',\n  '(:locale)/webinar/introduction-to-unified-commerce-uk',\n  '(:locale)/webinar/inside-the-migration',\n  '(:locale)/webinar/growth-shopify-vml-ambitious-brands',\n  '(:locale)/webinar/holidays-sales-on-shop',\n  '(:locale)/webinar/introduction-to-unified-commerce-au',\n  '(:locale)/webinar/gorgias-emea',\n  '(:locale)/webinar/good-american',\n  '(:locale)/webinar/getstarted-session1',\n  '(:locale)/webinar/fr-compare-sitespeed',\n  '(:locale)/webinar/ey-pos-market-study',\n  '(:locale)/webinar/europe-unified-commerce-eng-25',\n  '(:locale)/webinar/es-winter-25-edition',\n  '(:locale)/webinar/fr-winter-25-edition',\n  '(:locale)/webinar/enterprise-demo-webinar',\n  '(:locale)/webinar/emeashopifypaymentsforagencies',\n  '(:locale)/webinar/email-segmentation',\n  '(:locale)/webinar/email-bfcm',\n  '(:locale)/webinar/emea-migration-roundtable',\n  '(:locale)/webinar/efficient-growth-levers-au',\n  '(:locale)/webinar/efficient-growth-levers',\n  '(:locale)/webinar/de-winter-25-edition',\n  '(:locale)/webinar/de-compare-sitespeed',\n  '(:locale)/webinar/customer-retention-2024',\n  '(:locale)/webinar/easyteam-20225',\n  '(:locale)/webinar/customer-migration-neu',\n  '(:locale)/webinar/custom-switcher',\n  '(:locale)/webinar/es-compare-sitespeed',\n  '(:locale)/webinar/cost-of-bad-retail-data',\n  '(:locale)/webinar/compare-woocommerce',\n  '(:locale)/webinar/compare-bigcommerce',\n  '(:locale)/webinar/clienteling-playbook',\n  '(:locale)/webinar/compare-sitespeed',\n  '(:locale)/webinar/capital-growth-au',\n  '(:locale)/webinar/choosing-the-right-commerce-platform-nudient',\n  '(:locale)/webinar/capital-growth',\n  '(:locale)/webinar/cto-fireside',\n  '(:locale)/webinar/bklyn-larder-switch-from-square-to-shopify',\n  '(:locale)/webinar/bfs-embed-your-app',\n  '(:locale)/webinar/bfs-admin-performance',\n  '(:locale)/webinar/bfcm-retail-2024',\n  '(:locale)/webinar/bfcm-retail-2025',\n  '(:locale)/webinar/bfcm-growth-edge-25',\n  '(:locale)/webinar/b2b-on-shopify-live-demo',\n  '(:locale)/webinar/b2b-insights-deloitte-digital-shopify-report-2024',\n  '(:locale)/webinar/aviator-nation-unified-commerce',\n  '(:locale)/webinar/au-winter-25-edition',\n  '(:locale)/webinar/au-compare-sitespeed',\n  '(:locale)/webinar/b2b-commerce',\n  '(:locale)/webinar/au-b2b-commerce',\n  '(:locale)/webinar/april2025fasttrackamer',\n  '(:locale)/webinar/april2025fasttrackemea',\n  '(:locale)/webinar/apac-b2b-workshop-2',\n  '(:locale)/webinar/apac-b2b-workshop-3',\n  '(:locale)/webinar/anzroafasttrack',\n  '(:locale)/webinar/apac-b2b-workshop-1',\n  '(:locale)/webinar/anz-editions-sneakpeak',\n  '(:locale)/webinar/ai-seo-agentic-commerce',\n  '(:locale)/webinar/aicommercereadiness',\n  '(:locale)/webinar/ai-discovery',\n  '(:locale)/webinar/aiforsmbemea',\n  '(:locale)/webinar/adobe-migration-roundtable',\n  '(:locale)/webinar/aftersell',\n  '(:locale)/webinar/adobe-commerce-compare',\n  '(:locale)/webinar/Getstarted-session-08',\n  '(:locale)/webinar/Getstarted-session-07',\n  '(:locale)/webinar/POS-partner-accelerator-1',\n  '(:locale)/webinar/POS-partner-accelerator-2',\n  '(:locale)/webinar/Getstarted-session-06',\n  '(:locale)/webinar/Getstarted-session-04',\n  '(:locale)/webinar/Getstarted-session-05',\n  '(:locale)/webinar/Getstarted-session-02',\n  '(:locale)/webinar/Getstarted-session-01',\n  '(:locale)/webinar/B2B-workshop-3',\n  '(:locale)/webinar/B2B-workshop-1',\n  '(:locale)/webinar/Getstarted-session-03',\n  '(:locale)/webinar/2023-audiences',\n  '(:locale)/webinar/B2B-workshop-2',\n  '(:locale)/webinar/pos/shopify-vs-lightspeed',\n  '(:locale)/webinar',\n  '(:locale)/pos/webinar/omnichannel-management-for-apparel-brands-webinar',\n  '(:locale)/sitemaps_list.xml',\n  '(:locale)/sitemap_news.xml',\n  '(:locale)/sitemap_blog.xml',\n  '(:locale)/sitemap_blog_partners.xml',\n  '(:locale)/sitemap_blog_retail.xml',\n  '(:locale)/sitemap.xml',\n  '(:locale)/content-services/subscribers-triggered',\n  '(:locale)/content-services/subscribers.json',\n  '(:locale)/content-services/subscribers-triggered.json',\n  '(:locale)/content-services/subscribers',\n  '(:locale)/404-static',\n  '(:locale)/404',\n  '(:locale)/unified-commerce',\n  '(:locale)/contact/submit',\n  '(:locale)/contact/china-wechat-contact-us',\n  '(:locale)/contact/apac-upgrade-now',\n  '(:locale)/contact/pos-contact-us',\n  '(:locale)/resource/woocommerce-technical-migration-guide',\n  '(:locale)/resource/tco-report',\n  '(:locale)/resource/time-to-value-guide',\n  '(:locale)/resource/shoppy',\n  '(:locale)/resource/shopify-vs-bigcommerce-unified-commerce',\n  '(:locale)/resource/shop-pay-component-guide',\n  '(:locale)/resource/shopify-vs-adobe',\n  '(:locale)/resource/time-to-value-guide-jp',\n  '(:locale)/resource/sfcc-technical-migration-guide',\n  '(:locale)/resource/samsung',\n  '(:locale)/resource/resource-unified-commerce-growth-guide',\n  '(:locale)/resource/migrate-from-adobe-commerce-magento',\n  '(:locale)/resource/platform-readiness-assessment',\n  '(:locale)/resource/retail-digital-transformation-report',\n  '(:locale)/resource/internationalisation-it',\n  '(:locale)/resource/internationalisation-de',\n  '(:locale)/resource/internationalisation-fr',\n  '(:locale)/resource/mindretail',\n  '(:locale)/resource/innovationmindset',\n  '(:locale)/resource/internationalisation-es',\n  '(:locale)/resource/innovation-in-action/video',\n  '(:locale)/resource/innovation-in-action',\n  '(:locale)/resource/internationalisation',\n  '(:locale)/resource/how-to-connect-to-apac-consumers',\n  '(:locale)/resource/innovation-als-mindset',\n  '(:locale)/resource/idc-marketscape-pos-report',\n  '(:locale)/resource/growth-strategies',\n  '(:locale)/resource/growth-ordergroove',\n  '(:locale)/resource/growth-yotpo',\n  '(:locale)/resource/growth-constructor',\n  '(:locale)/resource/growth-braze',\n  '(:locale)/resource/grocery-playbook',\n  '(:locale)/resource/growth-klaviyo',\n  '(:locale)/resource/gartner-voice-of-customer',\n  '(:locale)/resource/growth-bloomreach',\n  '(:locale)/resource/gartner-report-what-is-unified-retail-commerce',\n  '(:locale)/resource/forrester-wave-b2c',\n  '(:locale)/resource/forrester-carrier-b2b',\n  '(:locale)/resource/gartner-report-hype-cycle-for-retail-technologies-2024',\n  '(:locale)/resource/flexibility-as-philosophy',\n  '(:locale)/resource/forrester-wave-b2b',\n  '(:locale)/resource/fashion-whitepaper',\n  '(:locale)/resource/enterprise-platform-assessment',\n  '(:locale)/resource/ecplatform-comparison-guide-japan',\n  '(:locale)/resource/ecommerce-growth-guide',\n  '(:locale)/resource/emea-beauty-growth-playbook',\n  '(:locale)/resource/custom-technical-migration-guide',\n  '(:locale)/resource/custom-platform-whitepaper',\n  '(:locale)/resource/cto-mindset',\n  '(:locale)/resource/cmo-maximizing-roi-1',\n  '(:locale)/resource/ey-known-customer-report',\n  '(:locale)/resource/cto-unblocking-growth',\n  '(:locale)/resource/china-plus-upgrade-whitepaper',\n  '(:locale)/resource/china-launch-global-whitepaper',\n  '(:locale)/resource/b2b-unified-commerce',\n  '(:locale)/resource/bigcommerce-technical-migration-guide',\n  '(:locale)/resource/ceo-guide-unified-commerce',\n  '(:locale)/resource/china-global-expansion-whitepaper',\n  '(:locale)/resource/b2b-playbook',\n  '(:locale)/resource/automotive-guide',\n  '(:locale)/resource/China-Site-Speed-Whitepaper',\n  '(:locale)/resource/adobe-technical-migration-guide',\n  '(:locale)/resource/accelerating-B2B-sales-report',\n  '(:locale)/resource/China-B2B-Whitepaper',\n  '(:locale)/resource/6monthsonus',\n  '(:locale)/resource/ultimate-guide-to-site-speed',\n  '(:locale)/resource/peak-sales-season-2023',\n  '(:locale)/resource/global-commerce',\n  '(:locale)/resource/media-and-entertainment-playbook',\n  '(:locale)/resource/headless-commerce-guide',\n  '(:locale)/resource/omnichannel-guide',\n  '(:locale)/resource/furniture-report',\n  '(:locale)/resource/football-sports-commerce',\n  '(:locale)/resource/ecommerce-migration',\n  '(:locale)/resource/flash-sale-checklist',\n  '(:locale)/resource/direct-to-consumer-guide',\n  '(:locale)/resource/ecommerce-automation-101',\n  '(:locale)/resource/b2b-ecommerce-checklist',\n  '(:locale)/resource',\n  '(:locale)/services/preview/webinar',\n  '(:locale)/services/preview/lpg',\n  '(:locale)/services/preview/resource',\n  '(:locale)/services/preview/event',\n  '(:locale)/services/preview/contact',\n  '(:locale)/marketing-preferences/resubscribe/:token',\n  '(:locale)/marketing-preferences/manage/:token',\n  '(:locale)/marketing-preferences/unsubscribe/:token',\n  '(:locale)/developers/tools/integrations',\n  '(:locale)/event/zurich-ai',\n  '(:locale)/event/women-leaders-christmas',\n  '(:locale)/event/virtual-dec18',\n  '(:locale)/event/viphacklabs',\n  '(:locale)/event/vip-commerce-sessions',\n  '(:locale)/event/tre-2025',\n  '(:locale)/event/toronto-oct23',\n  '(:locale)/event/test-protected-event',\n  '(:locale)/event/test-landing-page-url-emails',\n  '(:locale)/event/test-enterprise-event',\n  '(:locale)/event/sydconnect2026',\n  '(:locale)/event/smart-commerce-dublin23oct',\n  '(:locale)/event/shoptalk2026',\n  '(:locale)/event/sanfran-oct23',\n  '(:locale)/event/test-url-formation-registration-emails',\n  '(:locale)/event/salud-belleza-barce',\n  '(:locale)/event/retailtechjapan2026',\n  '(:locale)/event/sandiego-nov5',\n  '(:locale)/event/puresport-runclub',\n  '(:locale)/event/philly-dec11',\n  '(:locale)/event/palmsprings-feb23',\n  '(:locale)/event/retail-executive-roundtable-q4-2025',\n  '(:locale)/event/offline-event-end-to-end-testing-oct-16',\n  '(:locale)/event/offline-event-end-to-end-testing-oct-25',\n  '(:locale)/event/nzwinedine2025',\n  '(:locale)/event/nyc-port-mm-microevent2025',\n  '(:locale)/event/nyc-feb19',\n  '(:locale)/event/nyc-nov19',\n  '(:locale)/event/nrf26-partner-breakfast',\n  '(:locale)/event/nrf26-supperclub',\n  '(:locale)/event/nrf26-leadership-dinner',\n  '(:locale)/event/nrf26-afterhours',\n  '(:locale)/event/nrf2026-partners',\n  '(:locale)/event/nyc-nov3',\n  '(:locale)/event/nrf2026',\n  '(:locale)/event/nrf2026-concierge',\n  '(:locale)/event/napa-nov13',\n  '(:locale)/event/my-event',\n  '(:locale)/event/mr-marvis',\n  '(:locale)/event/luxury-leaders-milan',\n  '(:locale)/event/miami-nov5',\n  '(:locale)/event/nrf2026-tecovas',\n  '(:locale)/event/la-oct29',\n  '(:locale)/event/la-clippers-nov3',\n  '(:locale)/event/innovative-customers',\n  '(:locale)/event/health-beauty-connect',\n  '(:locale)/event/fashion-leaders-cologne',\n  '(:locale)/event/eshow2025',\n  '(:locale)/event/dallas-oct22',\n  '(:locale)/event/home-of-ett-hem-dec12',\n  '(:locale)/event/connect-uk',\n  '(:locale)/event/connect-spain',\n  '(:locale)/event/connect-italy',\n  '(:locale)/event/connect-germany',\n  '(:locale)/event/commerceday2025',\n  '(:locale)/event/commerceconnectinkansai',\n  '(:locale)/event/connect-france',\n  '(:locale)/event/commerce-castles-poland',\n  '(:locale)/event/chicago-oct30',\n  '(:locale)/event/berlin-netzwerk-16oct',\n  '(:locale)/event/b2b-future-commerce',\n  '(:locale)/event/aoshopify2026',\n  '(:locale)/event/book-meeting-end-to-end-testing-oct-25',\n  '(:locale)/pos/resources/unified-commerce-architecture',\n  '(:locale)/pos/resources/pos-market-report',\n  '__partials/HeaderAndFooter',\n  '__partials/Dux',\n  'br',\n  'br/ferramentas/modelos-etiquetas-personalizadas-de-frete-para-impressao',\n  'br/ferramentas/gerador-de-qr-code',\n  'br/ferramentas/gerador-slogan-para-empresas',\n  'br/ferramentas/modelo-pedido-de-compra',\n  'br/ferramentas/calculadora-margem-de-lucro',\n  'br/ferramentas/gerador-de-contracheque',\n  'br/ferramentas/gerador-politica-de-privacidade/:vertical',\n  'br/ferramentas/gerador-politica-de-privacidade',\n  'br/ferramentas/gerador-de-fatura',\n  'br/ferramentas/criador-de-logo/result',\n  'br/ferramentas/criador-de-logo/finalize',\n  'br/ferramentas/criador-de-logo/data-gathering',\n  'br/ferramentas/criador-de-logo/editor',\n  'br/ferramentas/criador-de-logo',\n  'br/ferramentas/redimensionador-e-otimizador-de-imagens',\n  'br/ferramentas/gerador-nome-de-dominio/pesquisar',\n  'br/ferramentas/gerador-nome-de-dominio',\n  'br/ferramentas/gerador-nomes-para-empresas/termos',\n  'br/ferramentas/gerador-nomes-para-empresas/:vertical',\n  'br/ferramentas/gerador-nomes-para-empresas',\n  'br/ferramentas/calculadora-online-emprestimo-para-pequenas-empresas',\n  'br/ferramentas/gerador-modelo-cartao-de-visita',\n  'br/ferramentas/gerador-de-codigo-de-barras',\n  'br/ferramentas/sitemap.xml',\n  'br/ferramentas/formulario-conhecimento-de-transporte',\n  'br/ferramentas/generated/:urlToPdf',\n  'br/ferramentas',\n  'br/comecar',\n  'br/vender/:vertical',\n  'br/vender',\n  'br/precos',\n  'br/impressao-sob-demanda/:vertical',\n  'br/impressao-sob-demanda',\n  'br/produtos',\n  'br/pdv/gerenciamento-membros-equipe',\n  'br/pdv/varejo-pdv',\n  'br/pdv/precos',\n  'br/pdv/omnicanal',\n  'br/pdv/recursos',\n  'br/pdv/ensaio-gratis/venda-a-retalho',\n  'br/pdv',\n  'br/gateways-de-pagamento',\n  'br/parcerias/directory/technologies/:category/:service',\n  'br/parcerias/directory/technologies/:category',\n  'br/parcerias/directory/technologies',\n  'br/parcerias/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  'br/parcerias/directory/services/:category/:service',\n  'br/parcerias/directory/services/:category',\n  'br/parcerias/directory/services',\n  'br/parcerias/directory/locations/:country',\n  'br/parcerias/directory/locations',\n  'br/parcerias/directory/merchant-review/:token',\n  'br/parcerias/directory',\n  'br/parcerias',\n  'br/gerenciar',\n  'br/legal/:article',\n  'br/avaliacao-gratuita',\n  'br/oferta-avaliacao-gratuita',\n  'br/exemplos',\n  'br/dominios/shop',\n  'br/dominios/:vertical',\n  'br/dominios',\n  'br/contato',\n  'br/comparar',\n  'br/comparar/shopify-vs-squarespace',\n  'br/comparar/shopify-vs-wix',\n  'br/comparar/shopify-vs-godaddy',\n  'br/canais',\n  'br/botao-de-compra',\n  'br/blog/:article',\n  'br/afiliados',\n  'br/quem-somos',\n  'br/promocao-de-configuracao/termos-e-condicoes',\n  'br/promocao-do-payments/termos-e-condicoes',\n  'br/ano-novo-promocao-gmv/termos-e-condicoes',\n  'br/promocao-gmv/termos-e-condicoes',\n  'br/promocao-dominio/termos-e-condicoes',\n  'br/promocao-do-cash/termos-e-condicoes',\n  'pt',\n  'pt/ferramentas/modelos-etiquetas-personalizadas-de-frete-para-impressao',\n  'pt/ferramentas/gerador-politica-de-privacidade/:vertical',\n  'pt/ferramentas/gerador-politica-de-privacidade',\n  'pt/ferramentas/criador-de-logo/result',\n  'pt/ferramentas/criador-de-logo/finalize',\n  'pt/ferramentas/criador-de-logo/data-gathering',\n  'pt/ferramentas/criador-de-logo/editor',\n  'pt/ferramentas/criador-de-logo',\n  'pt/ferramentas/gerador-nome-de-dominio/pesquisar',\n  'pt/ferramentas/gerador-nome-de-dominio',\n  'pt/ferramentas/gerador-nomes-para-empresas/:vertical',\n  'pt/ferramentas/gerador-nomes-para-empresas',\n  'pt/ferramentas/gerador-de-codigo-de-barras',\n  'pt/ferramentas/sitemap.xml',\n  'pt/ferramentas/formulario-conhecimento-de-transporte',\n  'pt/ferramentas/generated/:urlToPdf',\n  'pt/ferramentas',\n  'pt/comecar',\n  'pt/vender/:vertical',\n  'pt/vender',\n  'pt/precos',\n  'pt/impressao-sob-demanda/:vertical',\n  'pt/impressao-sob-demanda',\n  'pt/produtos',\n  'pt/pdv/gerenciamento-membros-equipe',\n  'pt/pdv/varejo-pdv',\n  'pt/pdv/precos',\n  'pt/pdv/omnicanal',\n  'pt/pdv/recursos',\n  'pt/pdv/ensaio-gratis/venda-a-retalho',\n  'pt/pdv',\n  'pt/gateways-de-pagamento',\n  'pt/parcerias/directory/technologies/:category/:service',\n  'pt/parcerias/directory/technologies/:category',\n  'pt/parcerias/directory/technologies',\n  'pt/parcerias/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  'pt/parcerias/directory/services/:category/:service',\n  'pt/parcerias/directory/services/:category',\n  'pt/parcerias/directory/services',\n  'pt/parcerias/directory/locations/:country',\n  'pt/parcerias/directory/locations',\n  'pt/parcerias/directory/merchant-review/:token',\n  'pt/parcerias/directory',\n  'pt/parcerias',\n  'pt/gerenciar',\n  'pt/legal/:article',\n  'pt/avaliacao-gratuita',\n  'pt/oferta-avaliacao-gratuita',\n  'pt/exemplos',\n  'pt/dominios/shop',\n  'pt/dominios/:vertical',\n  'pt/comparar',\n  'pt/comparar/shopify-vs-squarespace',\n  'pt/comparar/shopify-vs-wix',\n  'pt/comparar/shopify-vs-godaddy',\n  'pt/canais',\n  'pt/botao-de-compra',\n  'pt/blog/topics/:topic',\n  'pt/blog/:article',\n  'pt/afiliados',\n  'pt/quem-somos',\n  'pt/promocao-de-configuracao/termos-e-condicoes',\n  'pt/promocao-do-payments/termos-e-condicoes',\n  'pt/ano-novo-promocao-gmv/termos-e-condicoes',\n  'pt/promocao-gmv/termos-e-condicoes',\n  'pt/promocao-dominio/termos-e-condicoes',\n  'pt/promocao-do-cash/termos-e-condicoes',\n  'es',\n  'es/herramientas/plantilla-etiqueta-de-envio',\n  'es/herramientas/generador-de-codigo-qr',\n  'es/herramientas/generador-eslogan-para-empresas',\n  'es/herramientas/plantilla-orden-de-compra',\n  'es/herramientas/calculadora-margen-utilidad-comercial',\n  'es/herramientas/generador-recibo-sueldo',\n  'es/herramientas/generador-politica-de-privacidad/:vertical',\n  'es/herramientas/generador-politica-de-privacidad',\n  'es/herramientas/generador-factura',\n  'es/herramientas/generador-de-logos/result',\n  'es/herramientas/generador-de-logos/finalize',\n  'es/herramientas/generador-de-logos/data-gathering',\n  'es/herramientas/generador-de-logos/editor',\n  'es/herramientas/generador-de-logos',\n  'es/herramientas/redimensionador-optimizador-imagenes',\n  'es/herramientas/generador-nombre-dominios/buscar',\n  'es/herramientas/generador-nombre-dominios',\n  'es/herramientas/generador-nombre-para-empresas/terminos',\n  'es/herramientas/generador-nombre-para-empresas/:vertical',\n  'es/herramientas/generador-nombre-para-empresas',\n  'es/herramientas/calculadora-online-prestamo-pymes',\n  'es/herramientas/generador-tarjeta-negocios',\n  'es/herramientas/generador-codigo-barras',\n  'es/herramientas/sitemap.xml',\n  'es/herramientas/formulario-conocimiento-de-embarque',\n  'es/herramientas/generated/:urlToPdf',\n  'es/herramientas',\n  'es/suscripciones',\n  'es/comienza',\n  'es/shipping/transportistas',\n  'es/vender/:vertical',\n  'es/vender',\n  'es/precios',\n  'es/productos',\n  'es/pos',\n  'es/pos/administracion-de-personal',\n  'es/pos/pos-para-minorista',\n  'es/pos/precios',\n  'es/pos/pagos',\n  'es/pos/omnicanal',\n  'es/pos/caracteristicas',\n  'es/plus/vender',\n  'es/plus/precios',\n  'es/plus/plataforma',\n  'es/plus/migracion',\n  'es/plus/gestion',\n  'es/plus/integrar',\n  'es/plus/ecommerce-para-empresas',\n  'es/plus/contacto',\n  'es/plus/soluciones/ventas-omnicanal',\n  'es/plus/soluciones/ecommerce-internacional',\n  'es/aceptar-pagos-en-linea',\n  'es/gestiona',\n  'es/legal/:article',\n  'es/prueba-gratis',\n  'es/oferta-prueba-gratis',\n  'es/ejemplos',\n  'es/dominios/shop',\n  'es/dominios/:vertical',\n  'es/dominios',\n  'es/contacto',\n  'es/comparación',\n  'es/comparación/shopify-vs-squarespace',\n  'es/comparación/shopify-vs-wix',\n  'es/comparación/shopify-vs-godaddy',\n  'es/canales',\n  'es/boton-de-compras',\n  'es/blog/:article',\n  'es/afiliados',\n  'es/acerca-de-nosotros',\n  'es/configuracion-de-tienda/terminos-y-condiciones',\n  'es/recursos/gartner-magic-quadrant',\n  'es/promocion-de-payments/terminos-y-condiciones',\n  'es/promocion-gmv-fin-de-anio/terminos-y-condiciones',\n  'es/promocion-gmv/terminos-y-condiciones',\n  'es/promocion-de-dominio/terminos-y-condiciones',\n  'es/promocion-de-cash/terminos-y-condiciones',\n  'es/resource/checklist-para-ventas-flash',\n  'es/resource/guia-directa-al-consumidor',\n  'es/resource/automatizacion-del-ecommerce',\n  'fr',\n  'fr/outils/etiquette-d-expedition-personnalisee',\n  'fr/outils/generateur-de-qr-code',\n  'fr/outils/generateur-de-slogan-d-entreprise',\n  'fr/outils/modele-de-bon-de-commande-gratuit',\n  'fr/outils/calcul-du-taux-de-marque',\n  'fr/outils/modele-de-fiche-de-paie-en-ligne',\n  'fr/outils/generateur-de-politique/:vertical',\n  'fr/outils/generateur-de-politique',\n  'fr/outils/modele-de-facture-en-ligne',\n  'fr/outils/createur-de-logo/result',\n  'fr/outils/createur-de-logo/finalize',\n  'fr/outils/createur-de-logo/data-gathering',\n  'fr/outils/createur-de-logo/editor',\n  'fr/outils/createur-de-logo',\n  'fr/outils/redimensionner-une-image-en-ligne',\n  'fr/outils/generateur-de-nom-de-domaine/rechercher',\n  'fr/outils/generateur-de-nom-de-domaine',\n  'fr/outils/generateur-de-nom-d-entreprise/conditions',\n  'fr/outils/generateur-de-nom-d-entreprise/:vertical',\n  'fr/outils/generateur-de-nom-d-entreprise',\n  'fr/outils/simulateur-de-pret-d-entreprise',\n  'fr/outils/modele-de-carte-de-visite-professionnelle',\n  'fr/outils/generateur-de-code-barre',\n  'fr/outils/sitemap.xml',\n  'fr/outils/modele-de-connaissement',\n  'fr/outils/generated/:urlToPdf',\n  'fr/outils',\n  'fr/abonnements',\n  'fr/demarrer',\n  'fr/shipping/transporteurs',\n  'fr/vendre/:vertical',\n  'fr/vendre',\n  'fr/tarifs',\n  'fr/impression-a-la-demande/:vertical',\n  'fr/impression-a-la-demande',\n  'fr/produits',\n  'fr/pdv/gestion-du-personnel',\n  'fr/pdv/point-de-vente-retail',\n  'fr/pdv/tarification',\n  'fr/pdv/pdv-pro-annuel',\n  'fr/pdv/paiements',\n  'fr/pdv/omnicanal',\n  'fr/pdv/materiel',\n  'fr/pdv/fonctionnalites',\n  'fr/pdv/multi-store-pos',\n  'fr/pdv/essai-gratuit/vente-au-detail',\n  'fr/pdv',\n  'fr/plus/vendre',\n  'fr/plus/tarifs',\n  'fr/plus/platforme',\n  'fr/plus/gerer',\n  'fr/plus/integration',\n  'fr/plus/entreprise-ecommerce',\n  'fr/plus/solutions/retail-et-point-de-vente',\n  'fr/plus/solutions/commerce-omnicanal',\n  'fr/plus/solutions/ecommerce-international',\n  'fr/systemes-de-paiement',\n  'fr/paiements',\n  'fr/partenaires/directory/technologies/:category/:service',\n  'fr/partenaires/directory/technologies/:category',\n  'fr/partenaires/directory/technologies',\n  'fr/partenaires/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  'fr/partenaires/directory/services/:category/:service',\n  'fr/partenaires/directory/services/:category',\n  'fr/partenaires/directory/services',\n  'fr/partenaires/directory/locations/:country',\n  'fr/partenaires/directory/locations',\n  'fr/partenaires/directory/merchant-review/:token',\n  'fr/partenaires/directory',\n  'fr/partenaires',\n  'fr/site-de-vente-en-ligne',\n  'fr/test-de-cookie-marketing-marchand',\n  'fr/gerer',\n  'fr/legal/:article',\n  'fr/essai-gratuit',\n  'fr/offre-essai-gratuit',\n  'fr/exemples',\n  'fr/domaines/shop',\n  'fr/domaines/:vertical',\n  'fr/domaines',\n  'fr/comparaison/shopify-vs-woocommerce',\n  'fr/comparaison/shopify-vs-squarespace',\n  'fr/comparaison/shopify-vs-wix',\n  'fr/comparaison/shopify-vs-salesforce-commercecloud',\n  'fr/comparaison/shopify-vs-godaddy',\n  'fr/comparaison/shopify-vs-etsy',\n  'fr/comparaison/shopify-vs-clover',\n  'fr/comparaison/shopify-vs-bigcommerce',\n  'fr/comparaison/:competitor',\n  'fr/comparaison',\n  'fr/canaux',\n  'fr/carrieres/nda',\n  'fr/bouton-d-achat',\n  'fr/blog/:article',\n  'fr/affilies',\n  'fr/a-propos-de-nous',\n  'fr/solutions/paiements/entreprise',\n  'fr/promotion-configuration/conditions',\n  'fr/ressources/gartner-magic-quadrant',\n  'fr/promotion-shopify-payments/conditions',\n  'fr/promotion-gmv-nouvel-an/conditions-generales-utilisation',\n  'fr/promotion-gmv/conditions-generales-utilisation',\n  'fr/promotion-sur-le-domaine/conditions',\n  'fr/rapport-climatique-test/:article',\n  'fr/promotion-en-especes/conditions',\n  'fr/resource/guide-amelioration-vitesse-site',\n  'fr/resource/migration-ecommerce',\n  'fr/resource/check-list-vente-flash',\n  'fr/resource/guide-dtoc',\n  'fr/resource/automatisation-ecommerce',\n  'nl',\n  'nl/hulpmiddelen/sjabloon-verzendlabel',\n  'nl/hulpmiddelen/qr-code-generator',\n  'nl/hulpmiddelen/slogan-maker',\n  'nl/hulpmiddelen/loonstrook-generator',\n  'nl/hulpmiddelen/beleid-generator/:vertical',\n  'nl/hulpmiddelen/beleid-generator',\n  'nl/hulpmiddelen/image-resizer',\n  'nl/hulpmiddelen/domeinnaam-generator/search',\n  'nl/hulpmiddelen/domeinnaam-generator',\n  'nl/hulpmiddelen/bedrijfsnaam-generator/terms',\n  'nl/hulpmiddelen/bedrijfsnaam-generator/:vertical',\n  'nl/hulpmiddelen/bedrijfsnaam-generator',\n  'nl/hulpmiddelen/business-card-maker',\n  'nl/hulpmiddelen/streepjescode-generator',\n  'nl/hulpmiddelen/sitemap.xml',\n  'nl/hulpmiddelen/generated/:urlToPdf',\n  'nl/hulpmiddelen',\n  'nl/abonnementen',\n  'nl/verkoop/:vertical',\n  'nl/verkoop',\n  'nl/prijzen',\n  'nl/producten',\n  'nl/pos',\n  'nl/pos/personeelsmanagement',\n  'nl/pos/prijzen',\n  'nl/pos/betalingen',\n  'nl/pos/kenmerken',\n  'nl/betaalmethodes',\n  'nl/betalingen',\n  'nl/mobiel',\n  'nl/beheer',\n  'nl/juridisch/:article',\n  'nl/gratis-proef',\n  'nl/gratis-uitproberen',\n  'nl/voorbeelden',\n  'nl/domeinen/shop',\n  'nl/domeinen/:vertical',\n  'nl/domeinen',\n  'nl/webshop-vergelijken/shopify-vs-squarespace',\n  'nl/webshop-vergelijken/shopify-vs-wix',\n  'nl/webshop-vergelijken/shopify-vs-salesforce-commercecloud',\n  'nl/webshop-vergelijken/shopify-vs-godaddy',\n  'nl/webshop-vergelijken/shopify-vs-lightspeed',\n  'nl/webshop-vergelijken/shopify-vs-clover',\n  'nl/webshop-vergelijken/:competitor',\n  'nl/webshop-vergelijken',\n  'nl/verkoopkanalen',\n  'nl/blog/:article',\n  'nl/over',\n  'nl/oplossingen/betalingen/onderneming',\n  'nl/promotie-voor-instellen/voorwaarden',\n  'nl/payments-promotie/voorwaarden',\n  'nl/domein-promotie/voorwaarden',\n  'nl/cash-promotie/voorwaarden',\n  'de',\n  'de/tools',\n  'de/tools/versandaufkleber-vorlage',\n  'de/tools/werbespruche-generator',\n  'de/tools/gewinnmargenrechner',\n  'de/tools/lohnabrechnungen',\n  'de/tools/richtlinien-generator/:vertical',\n  'de/tools/richtlinien-generator',\n  'de/tools/rechnungsgenerator',\n  'de/tools/domain-check/search',\n  'de/tools/domain-check',\n  'de/tools/firmennamen-generator/terms',\n  'de/tools/firmennamen-generator/:vertical',\n  'de/tools/firmennamen-generator',\n  'de/tools/visitenkarten-erstellen',\n  'de/tools/strichcode-generator',\n  'de/abonnements',\n  'de/starten',\n  'de/shipping/versandunternehmen',\n  'de/verkaufen/:vertical',\n  'de/verkaufen',\n  'de/preise',\n  'de/produkte',\n  'de/pos',\n  'de/pos/mitarbeiterverwaltung',\n  'de/pos/einzelhandels-pos',\n  'de/pos/preisgestaltung',\n  'de/pos/zahlungen',\n  'de/pos/funktionen',\n  'de/plus/verkaufen',\n  'de/plus/preis',\n  'de/plus/plattform',\n  'de/plus/migrieren',\n  'de/plus/verwalten',\n  'de/plus/integrieren',\n  'de/plus/unternehmen-e-commerce',\n  'de/plus/kontakt',\n  'de/plus/losungen/omnichannel-commerce',\n  'de/plus/losungen/internationaler-ecommerce',\n  'de/zahlungsportal',\n  'de/onlineshop-erstellen',\n  'de/verwalten',\n  'de/legal/:article',\n  'de/kostenloser-test',\n  'de/kostenloses-testangebot',\n  'de/beispiele',\n  'de/kontakt',\n  'de/vergleich/shopify-vs-woocommerce',\n  'de/vergleich/shopify-vs-squarespace',\n  'de/vergleich/shopify-vs-shopware',\n  'de/vergleich/shopify-vs-wix',\n  'de/vergleich/shopify-vs-salesforce-commercecloud',\n  'de/vergleich/shopify-vs-godaddy',\n  'de/vergleich/shopify-vs-etsy',\n  'de/vergleich/shopify-vs-clover',\n  'de/vergleich/shopify-vs-bigcommerce',\n  'de/vergleich/:competitor',\n  'de/vergleich',\n  'de/vertriebskanale',\n  'de/blog/:article',\n  'de/unsere-geschichte',\n  'de/werbeaktion-einrichtung/geschaftsbedingungen',\n  'de/ressourcen/gartner-magic-quadrant',\n  'de/payments-aktion/geschaeftsbedingungen',\n  'de/gmv-Werbeaktion-zum-neuen-jahr/geschaeftsbedingungen',\n  'de/gmv-werbeaktion/geschaeftsbedingungen',\n  'de/domain-aktion/geschaeftsbedingungen',\n  'de/cash-aktion/allgemeine-geschaeftsbedingungen',\n  'de/resource/ultimativer-leitfaden-fuer-eine-schnelle-website',\n  'de/resource/e-commerce-migration',\n  'de/resource/flash-sale-checkliste',\n  'de/resource/direct-to-consumer-leitfaden',\n  'de/resource/automatisierung-im-e-commerce',\n  'it',\n  'it/strumenti/template-etichette-di-spedizione',\n  'it/strumenti/generatore-di-codice-qr',\n  'it/strumenti/slogan-pubblicitari',\n  'it/strumenti/template-ordini-d-acquisto',\n  'it/strumenti/calcolatrice-per-margini-profitto',\n  'it/strumenti/generatore-di-buste-paga',\n  'it/strumenti/generatore-di-policy/:vertical',\n  'it/strumenti/generatore-di-policy',\n  'it/strumenti/generatore-di-fatture',\n  'it/strumenti/creatore-di-logo/result',\n  'it/strumenti/creatore-di-logo/finalize',\n  'it/strumenti/creatore-di-logo/data-gathering',\n  'it/strumenti/creatore-di-logo/editor',\n  'it/strumenti/creatore-di-logo',\n  'it/strumenti/ridimensionare-foto',\n  'it/strumenti/generatore-nome-dominio/search',\n  'it/strumenti/generatore-nome-dominio',\n  'it/strumenti/generatore-nomi-aziendali/terms',\n  'it/strumenti/generatore-nomi-aziendali/:vertical',\n  'it/strumenti/generatore-nomi-aziendali',\n  'it/strumenti/calcolatrice-per-prestiti-commerciali',\n  'it/strumenti/biglietti-da-visita',\n  'it/strumenti/generatore-codici-a-barre',\n  'it/strumenti/sitemap.xml',\n  'it/strumenti/generated/:urlToPdf',\n  'it/strumenti',\n  'it/abbonamenti',\n  'it/aprire',\n  'it/shipping/corrieri',\n  'it/vendere/:vertical',\n  'it/vendere',\n  'it/prezzi',\n  'it/prodotti',\n  'it/pos',\n  'it/pos/gestione-staff',\n  'it/pos/pos-dettagli',\n  'it/pos/prezzi',\n  'it/pos/pagamenti',\n  'it/pos/Omnicanale',\n  'it/pos/funzionalita',\n  'it/plus/vendere',\n  'it/plus/prezzi',\n  'it/plus/piattaforma',\n  'it/plus/migrazione',\n  'it/plus/gestisci',\n  'it/plus/integrazioni',\n  'it/plus/ecommerce-per-imprese',\n  'it/plus/contatti',\n  'it/plus/solutions/commercio-multicanale',\n  'it/plus/solutions/ecommerce-internazionale',\n  'it/canali-di-pagamento',\n  'it/partner/directory/technologies/:category/:service',\n  'it/partner/directory/technologies/:category',\n  'it/partner/directory/technologies',\n  'it/partner/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  'it/partner/directory/services/:category/:service',\n  'it/partner/directory/services/:category',\n  'it/partner/directory/services',\n  'it/partner/directory/locations/:country',\n  'it/partner/directory/locations',\n  'it/partner/directory/merchant-review/:token',\n  'it/partner/directory',\n  'it/partner',\n  'it/gestire',\n  'it/legal/:article',\n  'it/prova-gratuita',\n  'it/offerta-prova-gratuita',\n  'it/esempi',\n  'it/dominio/shop',\n  'it/dominio/:vertical',\n  'it/dominio',\n  'it/contatti',\n  'it/confronto/shopify-vs-woocommerce',\n  'it/confronto/shopify-vs-squarespace',\n  'it/confronto/shopify-vs-wix',\n  'it/confronto/shopify-vs-salesforce-commercecloud',\n  'it/confronto/shopify-vs-godaddy',\n  'it/confronto/shopify-vs-etsy',\n  'it/confronto/shopify-vs-clover',\n  'it/confronto/shopify-vs-bigcommerce',\n  'it/confronto/:competitor',\n  'it/confronto',\n  'it/canali',\n  'it/blog/:article',\n  'it/affiliati',\n  'it/chi-siamo',\n  'it/promozione-configurazione/termini-e-condizioni',\n  'it/risorse/magic-quadrant-gartner',\n  'it/promozione-pagamenti/termini-e-condizioni',\n  'it/promozione-gmv-anno-nuovo/termini-e-condizioni',\n  'it/promozione-gmv/termini-e-condizioni',\n  'it/promozione-dominio/termini-e-condizioni',\n  'it/promozione-cash/termini-e-condizioni',\n  'it/resource/migrazione-ecommerce',\n  'it/resource/offerte-lampo',\n  'it/resource/guida-diretto-al-consumatore',\n  'it/resource/automazione-per-ecommerce',\n  'dk',\n  'dk/tools',\n  'dk/tools/slogandesigner',\n  'dk/begynd',\n  'dk/saelg',\n  'dk/priser',\n  'dk/produkter',\n  'dk/pos',\n  'dk/pos/medarbejderstyring',\n  'dk/pos/detailpos',\n  'dk/pos/priser',\n  'dk/pos/betalinger',\n  'dk/pos/omnikanal',\n  'dk/pos/egenskaber',\n  'dk/betalingsgateways',\n  'dk/partnere/directory/technologies/:category/:service',\n  'dk/partnere/directory/technologies/:category',\n  'dk/partnere/directory/technologies',\n  'dk/partnere/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  'dk/partnere/directory/services/:category/:service',\n  'dk/partnere/directory/services/:category',\n  'dk/partnere/directory/services',\n  'dk/partnere/directory/locations/:country',\n  'dk/partnere/directory/locations',\n  'dk/partnere/directory/merchant-review/:token',\n  'dk/partnere/directory',\n  'dk/partnere',\n  'dk/administrer',\n  'dk/gratis-trial',\n  'dk/gratis-proeve',\n  'dk/eksempler',\n  'dk/kontakt',\n  'dk/sammenlign/shopify-vs-squarespace',\n  'dk/sammenlign/shopify-vs-wix',\n  'dk/sammenlign/:competitor',\n  'dk/sammenlign',\n  'dk/kanaler',\n  'dk/koeb-knap',\n  'dk/blog/:article',\n  'dk/om',\n  'dk/opsaet-kampagne/vilkar-og-betingelser',\n  'dk/payments-kampagne/vilkaar-og-betingelser',\n  'dk/domænekampagne/vilkaar-og-betingelser',\n  'dk/kontantkampagne/vilkaar-og-betingelser',\n  'se',\n  'se/tools',\n  'se/tools/slogangenerator',\n  'se/borja',\n  'se/salja',\n  'se/priser',\n  'se/produkter',\n  'se/betalnings-gatewayer',\n  'se/administrera',\n  'se/gratis-trial',\n  'se/gratis-testversion',\n  'se/exempel',\n  'se/kontakta',\n  'se/jamfor/shopify-vs-squarespace',\n  'se/jamfor/shopify-vs-wix',\n  'se/jamfor/shopify-vs-salesforce-commercecloud',\n  'se/jamfor/:competitor',\n  'se/jamfor',\n  'se/kanaler',\n  'se/kop-knapp',\n  'se/blog/topics/:topic',\n  'se/blog/:article',\n  'se/konfigurering-kampanj/villkor',\n  'se/payments-kampanj/regler-och-villkor',\n  'se/domänkampanj/regler-och-villkor',\n  'se/cash-kampanj/regler-och-villkor',\n  'es-es',\n  'es-es/herramientas/generador-nombre-dominios/buscar',\n  'es-es/herramientas/generador-nombre-dominios',\n  'es-es/herramientas',\n  'es-es/suscripciones',\n  'es-es/comienza',\n  'es-es/shipping/transportistas',\n  'es-es/vender/:vertical',\n  'es-es/vender',\n  'es-es/precios',\n  'es-es/productos',\n  'es-es/pos',\n  'es-es/pos/administracion-de-personal',\n  'es-es/pos/pos-para-minorista',\n  'es-es/pos/precios',\n  'es-es/pos/pagos',\n  'es-es/pos/omnicanal',\n  'es-es/pos/caracteristicas',\n  'es-es/plus/vender',\n  'es-es/plus/precios',\n  'es-es/plus/plataforma',\n  'es-es/plus/migracion',\n  'es-es/plus/gestion',\n  'es-es/plus/integrar',\n  'es-es/plus/ecommerce-para-empresas',\n  'es-es/plus/contacto',\n  'es-es/plus/soluciones/ventas-omnicanal',\n  'es-es/plus/soluciones/ecommerce-internacional',\n  'es-es/aceptar-pagos-en-linea',\n  'es-es/gestiona',\n  'es-es/legal/:article',\n  'es-es/prueba-gratis',\n  'es-es/oferta-prueba-gratis',\n  'es-es/ejemplos',\n  'es-es/dominios/shop',\n  'es-es/dominios/:vertical',\n  'es-es/dominios',\n  'es-es/contacto',\n  'es-es/comparación',\n  'es-es/comparación/shopify-vs-squarespace',\n  'es-es/comparación/shopify-vs-wix',\n  'es-es/comparación/shopify-vs-godaddy',\n  'es-es/canales',\n  'es-es/boton-de-compras',\n  'es-es/blog/:article',\n  'es-es/afiliados',\n  'es-es/acerca-de-nosotros',\n  'es-es/configuracion-de-tienda/terminos-y-condiciones',\n  'es-es/recursos/gartner-magic-quadrant',\n  'es-es/promocion-de-payments/terminos-y-condiciones',\n  'es-es/promocion-gmv-fin-de-anio/terminos-y-condiciones',\n  'es-es/promocion-gmv/terminos-y-condiciones',\n  'es-es/promocion-de-dominio/terminos-y-condiciones',\n  'es-es/promocion-de-cash/terminos-y-condiciones',\n  'es-es/resource/checklist-para-ventas-flash',\n  'es-es/resource/guia-directa-al-consumidor',\n  'es-es/resource/automatizacion-del-ecommerce',\n  'co',\n  'co/herramientas/generador-nombre-dominios/buscar',\n  'co/herramientas/generador-nombre-dominios',\n  'co/herramientas',\n  'co/suscripciones',\n  'co/comienza',\n  'co/vender/:vertical',\n  'co/vender',\n  'co/precios',\n  'co/productos',\n  'co/pos',\n  'co/pos/administracion-de-personal',\n  'co/pos/pos-para-minorista',\n  'co/pos/precios',\n  'co/pos/pagos',\n  'co/pos/omnicanal',\n  'co/pos/caracteristicas',\n  'co/plus/vender',\n  'co/plus/precios',\n  'co/plus/plataforma',\n  'co/plus/migracion',\n  'co/plus/gestion',\n  'co/plus/integrar',\n  'co/plus/ecommerce-para-empresas',\n  'co/plus/contacto',\n  'co/plus/soluciones/ventas-omnicanal',\n  'co/plus/soluciones/ecommerce-internacional',\n  'co/aceptar-pagos-en-linea',\n  'co/gestiona',\n  'co/legal/:article',\n  'co/prueba-gratis',\n  'co/oferta-prueba-gratis',\n  'co/ejemplos',\n  'co/dominios/shop',\n  'co/dominios/:vertical',\n  'co/dominios',\n  'co/contacto',\n  'co/comparación',\n  'co/comparación/shopify-vs-squarespace',\n  'co/comparación/shopify-vs-wix',\n  'co/comparación/shopify-vs-godaddy',\n  'co/canales',\n  'co/boton-de-compras',\n  'co/blog/:article',\n  'co/afiliados',\n  'co/acerca-de-nosotros',\n  'co/configuracion-de-tienda/terminos-y-condiciones',\n  'co/recursos/gartner-magic-quadrant',\n  'co/promocion-de-payments/terminos-y-condiciones',\n  'co/promocion-gmv-fin-de-anio/terminos-y-condiciones',\n  'co/promocion-gmv/terminos-y-condiciones',\n  'co/promocion-de-dominio/terminos-y-condiciones',\n  'co/promocion-de-cash/terminos-y-condiciones',\n  'co/resource/checklist-para-ventas-flash',\n  'co/resource/guia-directa-al-consumidor',\n  'co/resource/automatizacion-del-ecommerce',\n  'mx',\n  'mx/herramientas/generador-nombre-dominios/buscar',\n  'mx/herramientas/generador-nombre-dominios',\n  'mx/herramientas',\n  'mx/suscripciones',\n  'mx/comienza',\n  'mx/vender/:vertical',\n  'mx/vender',\n  'mx/precios',\n  'mx/productos',\n  'mx/pos',\n  'mx/pos/administracion-de-personal',\n  'mx/pos/pos-para-minorista',\n  'mx/pos/precios',\n  'mx/pos/pagos',\n  'mx/pos/omnicanal',\n  'mx/pos/caracteristicas',\n  'mx/plus/vender',\n  'mx/plus/precios',\n  'mx/plus/plataforma',\n  'mx/plus/migracion',\n  'mx/plus/gestion',\n  'mx/plus/integrar',\n  'mx/plus/ecommerce-para-empresas',\n  'mx/plus/contacto',\n  'mx/plus/soluciones/ventas-omnicanal',\n  'mx/plus/soluciones/ecommerce-internacional',\n  'mx/aceptar-pagos-en-linea',\n  'mx/gestiona',\n  'mx/legal/:article',\n  'mx/prueba-gratis',\n  'mx/oferta-prueba-gratis',\n  'mx/ejemplos',\n  'mx/dominios/shop',\n  'mx/dominios/:vertical',\n  'mx/dominios',\n  'mx/contacto',\n  'mx/comparación',\n  'mx/comparación/shopify-vs-squarespace',\n  'mx/comparación/shopify-vs-wix',\n  'mx/comparación/shopify-vs-godaddy',\n  'mx/canales',\n  'mx/boton-de-compras',\n  'mx/blog/:article',\n  'mx/afiliados',\n  'mx/acerca-de-nosotros',\n  'mx/configuracion-de-tienda/terminos-y-condiciones',\n  'mx/recursos/gartner-magic-quadrant',\n  'mx/promocion-de-payments/terminos-y-condiciones',\n  'mx/promocion-gmv-fin-de-anio/terminos-y-condiciones',\n  'mx/promocion-gmv/terminos-y-condiciones',\n  'mx/promocion-de-dominio/terminos-y-condiciones',\n  'mx/promocion-de-cash/terminos-y-condiciones',\n  'mx/resource/checklist-para-ventas-flash',\n  'mx/resource/guia-directa-al-consumidor',\n  'mx/resource/automatizacion-del-ecommerce',\n  'ca-fr',\n  'ca-fr/outils/generateur-de-nom-de-domaine/rechercher',\n  'ca-fr/outils/generateur-de-nom-de-domaine',\n  'ca-fr/outils',\n  'ca-fr/abonnements',\n  'ca-fr/demarrer',\n  'ca-fr/shipping/transporteurs',\n  'ca-fr/shipping/postes-canada',\n  'ca-fr/vendre/:vertical',\n  'ca-fr/vendre',\n  'ca-fr/tarifs',\n  'ca-fr/impression-a-la-demande/:vertical',\n  'ca-fr/impression-a-la-demande',\n  'ca-fr/produits',\n  'ca-fr/pdv/gestion-du-personnel',\n  'ca-fr/pdv/point-de-vente-retail',\n  'ca-fr/pdv/tarification',\n  'ca-fr/pdv/pdv-pro-annuel',\n  'ca-fr/pdv/paiements',\n  'ca-fr/pdv/omnicanal',\n  'ca-fr/pdv/materiel',\n  'ca-fr/pdv/fonctionnalites',\n  'ca-fr/pdv/multi-store-pos',\n  'ca-fr/pdv/essai-gratuit/vente-au-detail',\n  'ca-fr/pdv',\n  'ca-fr/plus/vendre',\n  'ca-fr/plus/tarifs',\n  'ca-fr/plus/platforme',\n  'ca-fr/plus/gerer',\n  'ca-fr/plus/integration',\n  'ca-fr/plus/entreprise-ecommerce',\n  'ca-fr/plus/solutions/retail-et-point-de-vente',\n  'ca-fr/plus/solutions/commerce-omnicanal',\n  'ca-fr/plus/solutions/ecommerce-international',\n  'ca-fr/systemes-de-paiement',\n  'ca-fr/paiements',\n  'ca-fr/partenaires/directory/technologies/:category/:service',\n  'ca-fr/partenaires/directory/technologies/:category',\n  'ca-fr/partenaires/directory/technologies',\n  'ca-fr/partenaires/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  'ca-fr/partenaires/directory/services/:category/:service',\n  'ca-fr/partenaires/directory/services/:category',\n  'ca-fr/partenaires/directory/services',\n  'ca-fr/partenaires/directory/locations/:country',\n  'ca-fr/partenaires/directory/locations',\n  'ca-fr/partenaires/directory/merchant-review/:token',\n  'ca-fr/partenaires/directory',\n  'ca-fr/partenaires',\n  'ca-fr/site-de-vente-en-ligne',\n  'ca-fr/gerer',\n  'ca-fr/legal/:article',\n  'ca-fr/essai-gratuit',\n  'ca-fr/offre-essai-gratuit',\n  'ca-fr/exemples',\n  'ca-fr/domaines/shop',\n  'ca-fr/domaines/:vertical',\n  'ca-fr/comparaison/shopify-vs-woocommerce',\n  'ca-fr/comparaison/shopify-vs-squarespace',\n  'ca-fr/comparaison/shopify-vs-wix',\n  'ca-fr/comparaison/shopify-vs-salesforce-commercecloud',\n  'ca-fr/comparaison/shopify-vs-godaddy',\n  'ca-fr/comparaison/shopify-vs-etsy',\n  'ca-fr/comparaison/shopify-vs-clover',\n  'ca-fr/comparaison/shopify-vs-bigcommerce',\n  'ca-fr/comparaison/:competitor',\n  'ca-fr/comparaison',\n  'ca-fr/canaux',\n  'ca-fr/carrieres/nda',\n  'ca-fr/bouton-d-achat',\n  'ca-fr/blog/:article',\n  'ca-fr/affilies',\n  'ca-fr/a-propos-de-nous',\n  'ca-fr/solutions/paiements/entreprise',\n  'ca-fr/promotion-configuration/conditions',\n  'ca-fr/ressources/gartner-magic-quadrant',\n  'ca-fr/promotion-shopify-payments/conditions',\n  'ca-fr/promotion-gmv-nouvel-an/conditions-generales-utilisation',\n  'ca-fr/promotion-gmv/conditions-generales-utilisation',\n  'ca-fr/promotion-sur-le-domaine/conditions',\n  'ca-fr/promotion-en-especes/conditions',\n  'ca-fr/resource/guide-amelioration-vitesse-site',\n  'ca-fr/resource/migration-ecommerce',\n  'ca-fr/resource/check-list-vente-flash',\n  'ca-fr/resource/guide-dtoc',\n  'ca-fr/resource/automatisation-ecommerce',\n  'be-fr',\n  'be-fr/outils/generateur-de-nom-de-domaine/rechercher',\n  'be-fr/outils/generateur-de-nom-de-domaine',\n  'be-fr/outils',\n  'be-fr/abonnements',\n  'be-fr/demarrer',\n  'be-fr/vendre/:vertical',\n  'be-fr/vendre',\n  'be-fr/tarifs',\n  'be-fr/impression-a-la-demande/:vertical',\n  'be-fr/impression-a-la-demande',\n  'be-fr/produits',\n  'be-fr/pdv/gestion-du-personnel',\n  'be-fr/pdv/point-de-vente-retail',\n  'be-fr/pdv/tarification',\n  'be-fr/pdv/pdv-pro-annuel',\n  'be-fr/pdv/paiements',\n  'be-fr/pdv/omnicanal',\n  'be-fr/pdv/materiel',\n  'be-fr/pdv/fonctionnalites',\n  'be-fr/pdv/multi-store-pos',\n  'be-fr/pdv/essai-gratuit/vente-au-detail',\n  'be-fr/pdv',\n  'be-fr/plus/vendre',\n  'be-fr/plus/tarifs',\n  'be-fr/plus/platforme',\n  'be-fr/plus/gerer',\n  'be-fr/plus/integration',\n  'be-fr/plus/entreprise-ecommerce',\n  'be-fr/plus/solutions/retail-et-point-de-vente',\n  'be-fr/plus/solutions/commerce-omnicanal',\n  'be-fr/plus/solutions/ecommerce-international',\n  'be-fr/systemes-de-paiement',\n  'be-fr/paiements',\n  'be-fr/partenaires/directory/technologies/:category/:service',\n  'be-fr/partenaires/directory/technologies/:category',\n  'be-fr/partenaires/directory/technologies',\n  'be-fr/partenaires/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  'be-fr/partenaires/directory/services/:category/:service',\n  'be-fr/partenaires/directory/services/:category',\n  'be-fr/partenaires/directory/services',\n  'be-fr/partenaires/directory/locations/:country',\n  'be-fr/partenaires/directory/locations',\n  'be-fr/partenaires/directory/merchant-review/:token',\n  'be-fr/partenaires/directory',\n  'be-fr/partenaires',\n  'be-fr/site-de-vente-en-ligne',\n  'be-fr/gerer',\n  'be-fr/legal/:article',\n  'be-fr/essai-gratuit',\n  'be-fr/offre-essai-gratuit',\n  'be-fr/exemples',\n  'be-fr/domaines/shop',\n  'be-fr/domaines/:vertical',\n  'be-fr/comparaison/shopify-vs-woocommerce',\n  'be-fr/comparaison/shopify-vs-squarespace',\n  'be-fr/comparaison/shopify-vs-wix',\n  'be-fr/comparaison/shopify-vs-salesforce-commercecloud',\n  'be-fr/comparaison/shopify-vs-godaddy',\n  'be-fr/comparaison/shopify-vs-etsy',\n  'be-fr/comparaison/shopify-vs-clover',\n  'be-fr/comparaison/shopify-vs-bigcommerce',\n  'be-fr/comparaison/:competitor',\n  'be-fr/comparaison',\n  'be-fr/canaux',\n  'be-fr/bouton-d-achat',\n  'be-fr/affilies',\n  'be-fr/a-propos-de-nous',\n  'be-fr/solutions/paiements/entreprise',\n  'be-fr/promotion-configuration/conditions',\n  'be-fr/ressources/gartner-magic-quadrant',\n  'be-fr/promotion-shopify-payments/conditions',\n  'be-fr/promotion-gmv-nouvel-an/conditions-generales-utilisation',\n  'be-fr/promotion-gmv/conditions-generales-utilisation',\n  'be-fr/promotion-sur-le-domaine/conditions',\n  'be-fr/promotion-en-especes/conditions',\n  'be-fr/resource/guide-amelioration-vitesse-site',\n  'be-fr/resource/migration-ecommerce',\n  'be-fr/resource/check-list-vente-flash',\n  'be-fr/resource/guide-dtoc',\n  'be-fr/resource/automatisation-ecommerce',\n  'be',\n  'be/hulpmiddelen/domeinnaam-generator/search',\n  'be/hulpmiddelen/domeinnaam-generator',\n  'be/hulpmiddelen',\n  'be/abonnementen',\n  'be/verkoop/:vertical',\n  'be/verkoop',\n  'be/prijzen',\n  'be/producten',\n  'be/pos',\n  'be/pos/personeelsmanagement',\n  'be/pos/prijzen',\n  'be/pos/betalingen',\n  'be/pos/kenmerken',\n  'be/betaalmethodes',\n  'be/betalingen',\n  'be/mobiel',\n  'be/beheer',\n  'be/juridisch/:article',\n  'be/gratis-proef',\n  'be/gratis-uitproberen',\n  'be/voorbeelden',\n  'be/domeinen/shop',\n  'be/domeinen/:vertical',\n  'be/domeinen',\n  'be/webshop-vergelijken/shopify-vs-squarespace',\n  'be/webshop-vergelijken/shopify-vs-wix',\n  'be/webshop-vergelijken/shopify-vs-salesforce-commercecloud',\n  'be/webshop-vergelijken/shopify-vs-godaddy',\n  'be/webshop-vergelijken/shopify-vs-lightspeed',\n  'be/webshop-vergelijken/shopify-vs-clover',\n  'be/webshop-vergelijken/:competitor',\n  'be/webshop-vergelijken',\n  'be/verkoopkanalen',\n  'be/over',\n  'be/oplossingen/betalingen/onderneming',\n  'be/promotie-voor-instellen/voorwaarden',\n  'be/payments-promotie/voorwaarden',\n  'be/domein-promotie/voorwaarden',\n  'be/cash-promotie/voorwaarden',\n  'be-de',\n  'be-de/tools',\n  'be-de/tools/domain-check/search',\n  'be-de/tools/domain-check',\n  'be-de/abonnements',\n  'be-de/starten',\n  'be-de/verkaufen/:vertical',\n  'be-de/verkaufen',\n  'be-de/preise',\n  'be-de/produkte',\n  'be-de/pos',\n  'be-de/pos/mitarbeiterverwaltung',\n  'be-de/pos/einzelhandels-pos',\n  'be-de/pos/preisgestaltung',\n  'be-de/pos/zahlungen',\n  'be-de/pos/funktionen',\n  'be-de/plus/verkaufen',\n  'be-de/plus/preis',\n  'be-de/plus/plattform',\n  'be-de/plus/migrieren',\n  'be-de/plus/verwalten',\n  'be-de/plus/integrieren',\n  'be-de/plus/unternehmen-e-commerce',\n  'be-de/plus/kontakt',\n  'be-de/plus/losungen/omnichannel-commerce',\n  'be-de/plus/losungen/internationaler-ecommerce',\n  'be-de/zahlungsportal',\n  'be-de/onlineshop-erstellen',\n  'be-de/verwalten',\n  'be-de/legal/:article',\n  'be-de/kostenloser-test',\n  'be-de/kostenloses-testangebot',\n  'be-de/beispiele',\n  'be-de/kontakt',\n  'be-de/vergleich/shopify-vs-woocommerce',\n  'be-de/vergleich/shopify-vs-squarespace',\n  'be-de/vergleich/shopify-vs-shopware',\n  'be-de/vergleich/shopify-vs-wix',\n  'be-de/vergleich/shopify-vs-salesforce-commercecloud',\n  'be-de/vergleich/shopify-vs-godaddy',\n  'be-de/vergleich/shopify-vs-etsy',\n  'be-de/vergleich/shopify-vs-clover',\n  'be-de/vergleich/shopify-vs-bigcommerce',\n  'be-de/vergleich/:competitor',\n  'be-de/vergleich',\n  'be-de/vertriebskanale',\n  'be-de/blog/:article',\n  'be-de/unsere-geschichte',\n  'be-de/werbeaktion-einrichtung/geschaftsbedingungen',\n  'be-de/ressourcen/gartner-magic-quadrant',\n  'be-de/payments-aktion/geschaeftsbedingungen',\n  'be-de/gmv-Werbeaktion-zum-neuen-jahr/geschaeftsbedingungen',\n  'be-de/gmv-werbeaktion/geschaeftsbedingungen',\n  'be-de/domain-aktion/geschaeftsbedingungen',\n  'be-de/cash-aktion/allgemeine-geschaeftsbedingungen',\n  'be-de/resource/ultimativer-leitfaden-fuer-eine-schnelle-website',\n  'be-de/resource/e-commerce-migration',\n  'be-de/resource/flash-sale-checkliste',\n  'be-de/resource/direct-to-consumer-leitfaden',\n  'be-de/resource/automatisierung-im-e-commerce',\n  'at',\n  'at/tools',\n  'at/tools/domain-check/search',\n  'at/tools/domain-check',\n  'at/abonnements',\n  'at/starten',\n  'at/verkaufen/:vertical',\n  'at/verkaufen',\n  'at/preise',\n  'at/produkte',\n  'at/pos',\n  'at/pos/mitarbeiterverwaltung',\n  'at/pos/einzelhandels-pos',\n  'at/pos/preisgestaltung',\n  'at/pos/zahlungen',\n  'at/pos/funktionen',\n  'at/plus/verkaufen',\n  'at/plus/preis',\n  'at/plus/plattform',\n  'at/plus/migrieren',\n  'at/plus/verwalten',\n  'at/plus/integrieren',\n  'at/plus/unternehmen-e-commerce',\n  'at/plus/kontakt',\n  'at/plus/losungen/omnichannel-commerce',\n  'at/plus/losungen/internationaler-ecommerce',\n  'at/zahlungsportal',\n  'at/onlineshop-erstellen',\n  'at/verwalten',\n  'at/legal/:article',\n  'at/kostenloser-test',\n  'at/kostenloses-testangebot',\n  'at/beispiele',\n  'at/kontakt',\n  'at/vergleich/shopify-vs-woocommerce',\n  'at/vergleich/shopify-vs-squarespace',\n  'at/vergleich/shopify-vs-shopware',\n  'at/vergleich/shopify-vs-wix',\n  'at/vergleich/shopify-vs-salesforce-commercecloud',\n  'at/vergleich/shopify-vs-godaddy',\n  'at/vergleich/shopify-vs-etsy',\n  'at/vergleich/shopify-vs-clover',\n  'at/vergleich/shopify-vs-bigcommerce',\n  'at/vergleich/:competitor',\n  'at/vergleich',\n  'at/vertriebskanale',\n  'at/blog/:article',\n  'at/unsere-geschichte',\n  'at/werbeaktion-einrichtung/geschaftsbedingungen',\n  'at/ressourcen/gartner-magic-quadrant',\n  'at/payments-aktion/geschaeftsbedingungen',\n  'at/gmv-Werbeaktion-zum-neuen-jahr/geschaeftsbedingungen',\n  'at/gmv-werbeaktion/geschaeftsbedingungen',\n  'at/domain-aktion/geschaeftsbedingungen',\n  'at/cash-aktion/allgemeine-geschaeftsbedingungen',\n  'at/resource/ultimativer-leitfaden-fuer-eine-schnelle-website',\n  'at/resource/e-commerce-migration',\n  'at/resource/flash-sale-checkliste',\n  'at/resource/direct-to-consumer-leitfaden',\n  'at/resource/automatisierung-im-e-commerce',\n  'ch',\n  'ch/tools',\n  'ch/tools/domain-check/search',\n  'ch/tools/domain-check',\n  'ch/abonnements',\n  'ch/starten',\n  'ch/verkaufen/:vertical',\n  'ch/verkaufen',\n  'ch/preise',\n  'ch/produkte',\n  'ch/pos',\n  'ch/pos/mitarbeiterverwaltung',\n  'ch/pos/einzelhandels-pos',\n  'ch/pos/preisgestaltung',\n  'ch/pos/zahlungen',\n  'ch/pos/funktionen',\n  'ch/plus/verkaufen',\n  'ch/plus/preis',\n  'ch/plus/plattform',\n  'ch/plus/migrieren',\n  'ch/plus/verwalten',\n  'ch/plus/integrieren',\n  'ch/plus/unternehmen-e-commerce',\n  'ch/plus/kontakt',\n  'ch/plus/losungen/omnichannel-commerce',\n  'ch/plus/losungen/internationaler-ecommerce',\n  'ch/zahlungsportal',\n  'ch/onlineshop-erstellen',\n  'ch/verwalten',\n  'ch/legal/:article',\n  'ch/kostenloser-test',\n  'ch/kostenloses-testangebot',\n  'ch/beispiele',\n  'ch/kontakt',\n  'ch/vergleich/shopify-vs-woocommerce',\n  'ch/vergleich/shopify-vs-squarespace',\n  'ch/vergleich/shopify-vs-shopware',\n  'ch/vergleich/shopify-vs-wix',\n  'ch/vergleich/shopify-vs-salesforce-commercecloud',\n  'ch/vergleich/shopify-vs-godaddy',\n  'ch/vergleich/shopify-vs-etsy',\n  'ch/vergleich/shopify-vs-clover',\n  'ch/vergleich/shopify-vs-bigcommerce',\n  'ch/vergleich/:competitor',\n  'ch/vergleich',\n  'ch/vertriebskanale',\n  'ch/blog/:article',\n  'ch/unsere-geschichte',\n  'ch/werbeaktion-einrichtung/geschaftsbedingungen',\n  'ch/ressourcen/gartner-magic-quadrant',\n  'ch/payments-aktion/geschaeftsbedingungen',\n  'ch/gmv-Werbeaktion-zum-neuen-jahr/geschaeftsbedingungen',\n  'ch/gmv-werbeaktion/geschaeftsbedingungen',\n  'ch/domain-aktion/geschaeftsbedingungen',\n  'ch/cash-aktion/allgemeine-geschaeftsbedingungen',\n  'ch/resource/ultimativer-leitfaden-fuer-eine-schnelle-website',\n  'ch/resource/e-commerce-migration',\n  'ch/resource/flash-sale-checkliste',\n  'ch/resource/direct-to-consumer-leitfaden',\n  'ch/resource/automatisierung-im-e-commerce',\n  'ch-fr',\n  'ch-fr/outils/generateur-de-nom-de-domaine/rechercher',\n  'ch-fr/outils/generateur-de-nom-de-domaine',\n  'ch-fr/outils',\n  'ch-fr/abonnements',\n  'ch-fr/demarrer',\n  'ch-fr/vendre/:vertical',\n  'ch-fr/vendre',\n  'ch-fr/tarifs',\n  'ch-fr/impression-a-la-demande/:vertical',\n  'ch-fr/impression-a-la-demande',\n  'ch-fr/produits',\n  'ch-fr/pdv/gestion-du-personnel',\n  'ch-fr/pdv/point-de-vente-retail',\n  'ch-fr/pdv/tarification',\n  'ch-fr/pdv/pdv-pro-annuel',\n  'ch-fr/pdv/paiements',\n  'ch-fr/pdv/omnicanal',\n  'ch-fr/pdv/materiel',\n  'ch-fr/pdv/fonctionnalites',\n  'ch-fr/pdv/multi-store-pos',\n  'ch-fr/pdv/essai-gratuit/vente-au-detail',\n  'ch-fr/pdv',\n  'ch-fr/plus/vendre',\n  'ch-fr/plus/tarifs',\n  'ch-fr/plus/platforme',\n  'ch-fr/plus/gerer',\n  'ch-fr/plus/integration',\n  'ch-fr/plus/entreprise-ecommerce',\n  'ch-fr/plus/solutions/retail-et-point-de-vente',\n  'ch-fr/plus/solutions/commerce-omnicanal',\n  'ch-fr/plus/solutions/ecommerce-international',\n  'ch-fr/systemes-de-paiement',\n  'ch-fr/paiements',\n  'ch-fr/partenaires/directory/technologies/:category/:service',\n  'ch-fr/partenaires/directory/technologies/:category',\n  'ch-fr/partenaires/directory/technologies',\n  'ch-fr/partenaires/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  'ch-fr/partenaires/directory/services/:category/:service',\n  'ch-fr/partenaires/directory/services/:category',\n  'ch-fr/partenaires/directory/services',\n  'ch-fr/partenaires/directory/locations/:country',\n  'ch-fr/partenaires/directory/locations',\n  'ch-fr/partenaires/directory/merchant-review/:token',\n  'ch-fr/partenaires/directory',\n  'ch-fr/partenaires',\n  'ch-fr/site-de-vente-en-ligne',\n  'ch-fr/gerer',\n  'ch-fr/legal/:article',\n  'ch-fr/essai-gratuit',\n  'ch-fr/offre-essai-gratuit',\n  'ch-fr/exemples',\n  'ch-fr/domaines/shop',\n  'ch-fr/domaines/:vertical',\n  'ch-fr/domaines',\n  'ch-fr/comparaison/shopify-vs-woocommerce',\n  'ch-fr/comparaison/shopify-vs-squarespace',\n  'ch-fr/comparaison/shopify-vs-wix',\n  'ch-fr/comparaison/shopify-vs-salesforce-commercecloud',\n  'ch-fr/comparaison/shopify-vs-godaddy',\n  'ch-fr/comparaison/shopify-vs-etsy',\n  'ch-fr/comparaison/shopify-vs-clover',\n  'ch-fr/comparaison/shopify-vs-bigcommerce',\n  'ch-fr/comparaison/:competitor',\n  'ch-fr/comparaison',\n  'ch-fr/canaux',\n  'ch-fr/bouton-d-achat',\n  'ch-fr/affilies',\n  'ch-fr/a-propos-de-nous',\n  'ch-fr/solutions/paiements/entreprise',\n  'ch-fr/promotion-configuration/conditions',\n  'ch-fr/ressources/gartner-magic-quadrant',\n  'ch-fr/promotion-shopify-payments/conditions',\n  'ch-fr/promotion-gmv-nouvel-an/conditions-generales-utilisation',\n  'ch-fr/promotion-gmv/conditions-generales-utilisation',\n  'ch-fr/promotion-sur-le-domaine/conditions',\n  'ch-fr/promotion-en-especes/conditions',\n  'ch-fr/resource/guide-amelioration-vitesse-site',\n  'ch-fr/resource/migration-ecommerce',\n  'ch-fr/resource/check-list-vente-flash',\n  'ch-fr/resource/guide-dtoc',\n  'ch-fr/resource/automatisation-ecommerce',\n  'ch-it',\n  'ch-it/strumenti/generatore-nome-dominio/search',\n  'ch-it/strumenti/generatore-nome-dominio',\n  'ch-it/strumenti',\n  'ch-it/abbonamenti',\n  'ch-it/aprire',\n  'ch-it/vendere/:vertical',\n  'ch-it/vendere',\n  'ch-it/prezzi',\n  'ch-it/prodotti',\n  'ch-it/pos',\n  'ch-it/pos/gestione-staff',\n  'ch-it/pos/pos-dettagli',\n  'ch-it/pos/prezzi',\n  'ch-it/pos/pagamenti',\n  'ch-it/pos/Omnicanale',\n  'ch-it/pos/funzionalita',\n  'ch-it/plus/vendere',\n  'ch-it/plus/prezzi',\n  'ch-it/plus/piattaforma',\n  'ch-it/plus/migrazione',\n  'ch-it/plus/gestisci',\n  'ch-it/plus/integrazioni',\n  'ch-it/plus/ecommerce-per-imprese',\n  'ch-it/plus/contatti',\n  'ch-it/plus/solutions/commercio-multicanale',\n  'ch-it/plus/solutions/ecommerce-internazionale',\n  'ch-it/canali-di-pagamento',\n  'ch-it/partner/directory/technologies/:category/:service',\n  'ch-it/partner/directory/technologies/:category',\n  'ch-it/partner/directory/technologies',\n  'ch-it/partner/directory/services/:category/:service/dG9wLXBhcnRuZXJz',\n  'ch-it/partner/directory/services/:category/:service',\n  'ch-it/partner/directory/services/:category',\n  'ch-it/partner/directory/services',\n  'ch-it/partner/directory/locations/:country',\n  'ch-it/partner/directory/locations',\n  'ch-it/partner/directory/merchant-review/:token',\n  'ch-it/partner/directory',\n  'ch-it/partner',\n  'ch-it/gestire',\n  'ch-it/legal/:article',\n  'ch-it/prova-gratuita',\n  'ch-it/offerta-prova-gratuita',\n  'ch-it/esempi',\n  'ch-it/dominio/shop',\n  'ch-it/dominio/:vertical',\n  'ch-it/dominio',\n  'ch-it/contatti',\n  'ch-it/confronto/shopify-vs-woocommerce',\n  'ch-it/confronto/shopify-vs-squarespace',\n  'ch-it/confronto/shopify-vs-wix',\n  'ch-it/confronto/shopify-vs-salesforce-commercecloud',\n  'ch-it/confronto/shopify-vs-godaddy',\n  'ch-it/confronto/shopify-vs-etsy',\n  'ch-it/confronto/shopify-vs-clover',\n  'ch-it/confronto/shopify-vs-bigcommerce',\n  'ch-it/confronto/:competitor',\n  'ch-it/confronto',\n  'ch-it/canali',\n  'ch-it/affiliati',\n  'ch-it/chi-siamo',\n  'ch-it/promozione-configurazione/termini-e-condizioni',\n  'ch-it/risorse/magic-quadrant-gartner',\n  'ch-it/promozione-pagamenti/termini-e-condizioni',\n  'ch-it/promozione-gmv-anno-nuovo/termini-e-condizioni',\n  'ch-it/promozione-gmv/termini-e-condizioni',\n  'ch-it/promozione-dominio/termini-e-condizioni',\n  'ch-it/promozione-cash/termini-e-condizioni',\n  'ch-it/resource/migrazione-ecommerce',\n  'ch-it/resource/offerte-lampo',\n  'ch-it/resource/guida-diretto-al-consumatore',\n  'ch-it/resource/automazione-per-ecommerce',\n  'ar',\n  'ar/herramientas/generador-nombre-dominios/buscar',\n  'ar/herramientas/generador-nombre-dominios',\n  'ar/herramientas',\n  'ar/suscripciones',\n  'ar/comienza',\n  'ar/vender/:vertical',\n  'ar/vender',\n  'ar/precios',\n  'ar/productos',\n  'ar/pos',\n  'ar/pos/administracion-de-personal',\n  'ar/pos/pos-para-minorista',\n  'ar/pos/precios',\n  'ar/pos/pagos',\n  'ar/pos/omnicanal',\n  'ar/pos/caracteristicas',\n  'ar/plus/vender',\n  'ar/plus/precios',\n  'ar/plus/plataforma',\n  'ar/plus/migracion',\n  'ar/plus/gestion',\n  'ar/plus/integrar',\n  'ar/plus/ecommerce-para-empresas',\n  'ar/plus/contacto',\n  'ar/plus/soluciones/ventas-omnicanal',\n  'ar/plus/soluciones/ecommerce-internacional',\n  'ar/aceptar-pagos-en-linea',\n  'ar/gestiona',\n  'ar/legal/:article',\n  'ar/prueba-gratis',\n  'ar/oferta-prueba-gratis',\n  'ar/ejemplos',\n  'ar/dominios/shop',\n  'ar/dominios/:vertical',\n  'ar/dominios',\n  'ar/contacto',\n  'ar/comparación',\n  'ar/comparación/shopify-vs-squarespace',\n  'ar/comparación/shopify-vs-wix',\n  'ar/comparación/shopify-vs-godaddy',\n  'ar/canales',\n  'ar/boton-de-compras',\n  'ar/afiliados',\n  'ar/acerca-de-nosotros',\n  'ar/configuracion-de-tienda/terminos-y-condiciones',\n  'ar/recursos/gartner-magic-quadrant',\n  'ar/promocion-de-payments/terminos-y-condiciones',\n  'ar/promocion-gmv-fin-de-anio/terminos-y-condiciones',\n  'ar/promocion-gmv/terminos-y-condiciones',\n  'ar/promocion-de-dominio/terminos-y-condiciones',\n  'ar/promocion-de-cash/terminos-y-condiciones',\n  'ar/resource/checklist-para-ventas-flash',\n  'ar/resource/guia-directa-al-consumidor',\n  'ar/resource/automatizacion-del-ecommerce',\n  'cl',\n  'cl/herramientas/generador-nombre-dominios/buscar',\n  'cl/herramientas/generador-nombre-dominios',\n  'cl/herramientas',\n  'cl/suscripciones',\n  'cl/comienza',\n  'cl/vender/:vertical',\n  'cl/vender',\n  'cl/precios',\n  'cl/productos',\n  'cl/pos',\n  'cl/pos/administracion-de-personal',\n  'cl/pos/pos-para-minorista',\n  'cl/pos/precios',\n  'cl/pos/pagos',\n  'cl/pos/omnicanal',\n  'cl/pos/caracteristicas',\n  'cl/plus/vender',\n  'cl/plus/precios',\n  'cl/plus/plataforma',\n  'cl/plus/migracion',\n  'cl/plus/gestion',\n  'cl/plus/integrar',\n  'cl/plus/ecommerce-para-empresas',\n  'cl/plus/contacto',\n  'cl/plus/soluciones/ventas-omnicanal',\n  'cl/plus/soluciones/ecommerce-internacional',\n  'cl/aceptar-pagos-en-linea',\n  'cl/gestiona',\n  'cl/legal/:article',\n  'cl/prueba-gratis',\n  'cl/oferta-prueba-gratis',\n  'cl/ejemplos',\n  'cl/dominios/shop',\n  'cl/dominios/:vertical',\n  'cl/dominios',\n  'cl/contacto',\n  'cl/comparación',\n  'cl/comparación/shopify-vs-squarespace',\n  'cl/comparación/shopify-vs-wix',\n  'cl/comparación/shopify-vs-godaddy',\n  'cl/canales',\n  'cl/boton-de-compras',\n  'cl/afiliados',\n  'cl/acerca-de-nosotros',\n  'cl/configuracion-de-tienda/terminos-y-condiciones',\n  'cl/recursos/gartner-magic-quadrant',\n  'cl/promocion-de-payments/terminos-y-condiciones',\n  'cl/promocion-gmv-fin-de-anio/terminos-y-condiciones',\n  'cl/promocion-gmv/terminos-y-condiciones',\n  'cl/promocion-de-dominio/terminos-y-condiciones',\n  'cl/promocion-de-cash/terminos-y-condiciones',\n  'cl/resource/checklist-para-ventas-flash',\n  'cl/resource/guia-directa-al-consumidor',\n  'cl/resource/automatizacion-del-ecommerce',\n  'pe',\n  'pe/herramientas/generador-nombre-dominios/buscar',\n  'pe/herramientas/generador-nombre-dominios',\n  'pe/herramientas',\n  'pe/suscripciones',\n  'pe/comienza',\n  'pe/vender/:vertical',\n  'pe/vender',\n  'pe/precios',\n  'pe/productos',\n  'pe/pos',\n  'pe/pos/administracion-de-personal',\n  'pe/pos/pos-para-minorista',\n  'pe/pos/precios',\n  'pe/pos/pagos',\n  'pe/pos/omnicanal',\n  'pe/pos/caracteristicas',\n  'pe/plus/vender',\n  'pe/plus/precios',\n  'pe/plus/plataforma',\n  'pe/plus/migracion',\n  'pe/plus/gestion',\n  'pe/plus/integrar',\n  'pe/plus/ecommerce-para-empresas',\n  'pe/plus/contacto',\n  'pe/plus/soluciones/ventas-omnicanal',\n  'pe/plus/soluciones/ecommerce-internacional',\n  'pe/aceptar-pagos-en-linea',\n  'pe/gestiona',\n  'pe/legal/:article',\n  'pe/prueba-gratis',\n  'pe/oferta-prueba-gratis',\n  'pe/ejemplos',\n  'pe/dominios/shop',\n  'pe/dominios/:vertical',\n  'pe/dominios',\n  'pe/contacto',\n  'pe/comparación',\n  'pe/comparación/shopify-vs-squarespace',\n  'pe/comparación/shopify-vs-wix',\n  'pe/comparación/shopify-vs-godaddy',\n  'pe/canales',\n  'pe/boton-de-compras',\n  'pe/afiliados',\n  'pe/acerca-de-nosotros',\n  'pe/configuracion-de-tienda/terminos-y-condiciones',\n  'pe/recursos/gartner-magic-quadrant',\n  'pe/promocion-de-payments/terminos-y-condiciones',\n  'pe/promocion-gmv-fin-de-anio/terminos-y-condiciones',\n  'pe/promocion-gmv/terminos-y-condiciones',\n  'pe/promocion-de-dominio/terminos-y-condiciones',\n  'pe/promocion-de-cash/terminos-y-condiciones',\n  'pe/resource/checklist-para-ventas-flash',\n  'pe/resource/guia-directa-al-consumidor',\n  'pe/resource/automatizacion-del-ecommerce',\n  'jp',\n  'jp/tools',\n  'jp/tools/business-name-generator/terms',\n  'jp/tools/business-name-generator/:vertical',\n  'jp/resources/gartner-magic-quadrant',\n  'zh',\n  'zh/tools',\n  'zh/tools/business-name-generator/terms',\n  'zh/tools/business-name-generator/:vertical',\n  'zh/tools/business-name-generator',\n  'zh/blog/topics/:topic',\n  'zh/resources/gartner-magic-quadrant',\n  'fi',\n  'fi/ilmainen-kokeilu',\n  'fi/blog/topics/:topic',\n  'fi/blog/:article',\n  'fi/verkkosivu/suunnittelu',\n  'kr',\n  'kr/blog/topics/:topic',\n  'tr',\n  'tr/blog/topics/:topic',\n  'tr/blog/:article',\n  'tr/websitesi/tasarim',\n  'cz',\n  'cz/blog/topics/:topic',\n  'cz/blog/:article',\n  'th',\n  'th/blog/topics/:topic',\n  'no',\n  'no/blog/topics/:topic',\n  'no/blog/:article',\n  'bg',\n  'bg/blog/topics/:topic',\n  'gr',\n  'gr/blog/topics/:topic',\n  'hu',\n  'hu/blog/topics/:topic',\n  'in-hi',\n  'in-hi/blog/topics/:topic',\n  'id-id',\n  'id-id/blog/topics/:topic',\n  'ro',\n  'ro/blog/topics/:topic',\n  'by',\n  'by/blog/topics/:topic',\n  'lt',\n  'lt/blog/topics/:topic',\n  'pl',\n  'pl/blog/:article',\n] as const\n"
  },
  {
    "path": "packages/route-pattern/bench/src/comparison.bench.ts",
    "content": "/**\n * Comparing industry standard routers vs route-pattern\n *\n * This benchmark uses only patterns that all libraries\n * can handle for a fair apples-to-apples comparison.\n * `route-pattern` also supports full URLs with protocols and\n * query string constraints which other libraries cannot handle.\n */\n\nimport { bench, describe } from 'vitest'\nimport FindMyWay from 'find-my-way'\nimport { match } from 'path-to-regexp'\nimport { ArrayMatcher, TrieMatcher } from '@remix-run/route-pattern'\n\ntype Syntax = 'route-pattern' | 'find-my-way' | 'path-to-regexp'\n\ntype Matcher = {\n  add: (pattern: string, data?: unknown) => void\n  match: (url: URL) => { params: unknown } | null\n}\n\nconst matchers: Array<{\n  name: string\n  syntax: Syntax\n  createMatcher: () => Matcher\n}> = [\n  {\n    name: 'route-pattern/array',\n    syntax: 'route-pattern',\n    createMatcher: () => new ArrayMatcher(),\n  },\n  {\n    name: 'route-pattern/trie',\n    syntax: 'route-pattern',\n    createMatcher: () => new TrieMatcher(),\n  },\n  {\n    /** https://github.com/delvedor/find-my-way */\n    name: 'find-my-way',\n    syntax: 'find-my-way',\n    createMatcher: () => {\n      let router = FindMyWay()\n      return {\n        add(pattern) {\n          router.on('GET', pattern, () => {})\n        },\n        match(url) {\n          let result = router.find('GET', url.pathname)\n          if (!result) return null\n          return { params: result.params }\n        },\n      }\n    },\n  },\n  {\n    /** https://github.com/pillarjs/path-to-regexp */\n    name: 'path-to-regexp',\n    syntax: 'path-to-regexp',\n    createMatcher: () => {\n      let matchers: Array<ReturnType<typeof match>> = []\n      return {\n        add(pattern) {\n          let matchFn = match(pattern, { decode: decodeURIComponent })\n          matchers.push(matchFn)\n        },\n        match(url) {\n          for (let match of matchers) {\n            let result = match(url.pathname)\n            if (result !== false) {\n              return { params: result.params }\n            }\n          }\n          return null\n        },\n      }\n    },\n  },\n]\n\ntype Pattern = Record<Syntax, string>\n\nfunction generateCommonPatterns(count: number): Array<Pattern> {\n  let patterns: Array<Pattern> = []\n  for (let i = 0; i < count; i++) {\n    if (i % 5 === 0) {\n      // Static paths\n      patterns.push({\n        'route-pattern': `api/v${Math.floor(i / 100)}/users/${i}`,\n        'path-to-regexp': `/api/v${Math.floor(i / 100)}/users/${i}`,\n        'find-my-way': `/api/v${Math.floor(i / 100)}/users/${i}`,\n      })\n    } else if (i % 5 === 1) {\n      // Dynamic segments\n      patterns.push({\n        'route-pattern': `posts/:id/comments/${i}`,\n        'path-to-regexp': `/posts/:id/comments/${i}`,\n        'find-my-way': `/posts/:id/comments/${i}`,\n      })\n    } else if (i % 5 === 2) {\n      // Multiple dynamic segments\n      patterns.push({\n        'route-pattern': `users/:userId/posts/:postId/${i}`,\n        'path-to-regexp': `/users/:userId/posts/:postId/${i}`,\n        'find-my-way': `/users/:userId/posts/:postId/${i}`,\n      })\n    } else if (i % 5 === 3) {\n      // Optional segments at end (find-my-way requires optionals at end)\n      patterns.push({\n        'route-pattern': `api/resource/${i}(/:version)`,\n        'path-to-regexp': `/api/resource/${i}{/:version}`,\n        'find-my-way': `/api/resource/${i}/:version?`,\n      })\n    } else {\n      // Wildcard at end of pathname\n      patterns.push({\n        'route-pattern': `files/${i}/*path`,\n        'path-to-regexp': `/files/${i}/*path`,\n        'find-my-way': `/files/${i}/*`,\n      })\n    }\n  }\n  return patterns\n}\n\nfunction generateUrls(): Array<URL> {\n  let urls: string[] = []\n\n  for (let i = 0; i < 20; i++) {\n    urls.push(`api/v${i % 3}/users/${i}`)\n    urls.push(`posts/post-${i}/comments/${i}`)\n    urls.push(`users/user${i}/posts/post${i}/${i}`)\n    urls.push(`api/resource/${i}`)\n    urls.push(`api/resource/${i}/v${i % 2}`)\n    urls.push(`files/${i}/deep/nested/path.txt`)\n    urls.push(`files/${i}/another/file.js`)\n    urls.push(`nonexistent/path/${i}`)\n  }\n\n  return urls.map((url) => new URL(`https://example.com/${url}`))\n}\n\nlet urls = generateUrls()\n\nfor (let count of [10, 100, 1000, 5000]) {\n  describe(`common patterns (${count})`, () => {\n    let patterns = generateCommonPatterns(count)\n    for (let { name, syntax, createMatcher } of matchers) {\n      let matcher = createMatcher()\n      patterns.forEach((pattern) => matcher.add(pattern[syntax]))\n      bench(name, () => {\n        urls.forEach((url) => matcher.match(url))\n      })\n    }\n  })\n}\n"
  },
  {
    "path": "packages/route-pattern/bench/src/href.bench.ts",
    "content": "/**\n * This file benchmarks the `href` method of the `RoutePattern` class.\n *\n * The purpose of this benchmark is to capture current performance with `pnpm bench href --outputJson=main.json`\n * on the `main` branch, and then compare that to the performance of a feature branch with `pnpm bench href --compare=main.json`.\n *\n * Therefore, all `bench` calls happen in their own `describe` block, and the name passed to `bench` is arbitrary.\n */\n\nimport { execSync } from 'node:child_process'\nimport { bench, describe } from 'vitest'\nimport { RoutePattern } from '@remix-run/route-pattern'\n\nlet benchName = getBenchName()\n\n/**\n * Returns the benchmark name as `<branch> (<short commit>)`.\n * Fallback to 'bench' if git commands fail.\n */\nfunction getBenchName(): string {\n  try {\n    let branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim()\n    let shortCommit = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim()\n    return `${branch} (${shortCommit})`\n  } catch {\n    return 'bench'\n  }\n}\n\ndescribe('static', () => {\n  let pattern = new RoutePattern('/posts/new')\n  bench(benchName, () => {\n    pattern.href()\n  })\n})\n\ndescribe('one variable', () => {\n  let pattern = new RoutePattern('/posts/:id')\n  bench(benchName, () => {\n    pattern.href({ id: '123' })\n  })\n})\n\ndescribe('one wildcard', () => {\n  let pattern = new RoutePattern('/files/*path')\n  bench(benchName, () => {\n    pattern.href({ path: 'docs/readme.md' })\n  })\n})\n\ndescribe('multiple variables', () => {\n  let pattern = new RoutePattern('/users/:userId/posts/:postId')\n  bench(benchName, () => {\n    pattern.href({ userId: '42', postId: '123' })\n  })\n})\n\ndescribe('optional, all params', () => {\n  let pattern = new RoutePattern('/posts(/:id)')\n  bench(benchName, () => {\n    pattern.href({ id: '123' })\n  })\n})\n\ndescribe('optional, omit', () => {\n  let pattern = new RoutePattern('/posts(/:id)')\n  bench(benchName, () => {\n    pattern.href()\n  })\n})\n\ndescribe('complex (8 variants), all params', () => {\n  let pattern = new RoutePattern(\n    '/dashboard/:tenant/files/*path/view(/:year(/:month(/:day)))(/format/:fmt)',\n  )\n  bench(benchName, () => {\n    pattern.href({\n      tenant: 'acme',\n      path: 'client/reports',\n      year: '2024',\n      month: '01',\n      day: '15',\n      fmt: 'pdf',\n    })\n  })\n})\n\ndescribe('complex (8 variants), no optionals', () => {\n  let pattern = new RoutePattern(\n    '/dashboard/:tenant/files/*path/view(/:year(/:month(/:day)))(/format/:fmt)',\n  )\n  bench(benchName, () => {\n    pattern.href({\n      tenant: 'acme',\n      path: 'client/reports',\n    })\n  })\n})\n\ndescribe('with search params', () => {\n  let pattern = new RoutePattern('/posts/:id?tag=featured&tag=popular')\n  bench(benchName, () => {\n    pattern.href({ id: '123' }, { tag: 'tutorial' })\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/bench/src/pathological.bench.ts",
    "content": "import { bench, describe } from 'vitest'\nimport { ArrayMatcher, TrieMatcher } from '@remix-run/route-pattern'\n\nfunction generateRoutes(): string[] {\n  let routes: string[] = []\n\n  for (let i = 0; i < 200; i++) {\n    routes.push(`/static/path/${i}/resource`)\n  }\n\n  for (let i = 0; i < 300; i++) {\n    let paramCount = (i % 4) + 1\n    let segments: Array<string> = []\n    for (let j = 0; j < paramCount; j++) {\n      segments.push(`:param${j}`)\n    }\n    routes.push(`/dynamic/${i}/${segments.join('/')}`)\n  }\n\n  for (let i = 0; i < 150; i++) {\n    let depth = (i % 3) + 1\n    let pattern = `/optional/${i}`\n    for (let j = 0; j < depth; j++) {\n      pattern += `(/:opt${j})`\n    }\n    routes.push(pattern)\n  }\n\n  for (let i = 0; i < 100; i++) {\n    routes.push(`/wildcard/${i}/*path`)\n    routes.push(`/*prefix/middle/${i}/suffix`)\n  }\n\n  for (let i = 0; i < 150; i++) {\n    routes.push(`https://:tenant${i}.myapp.com/admin`)\n    routes.push(`http(s)://:subdomain.example${i}.com/api`)\n    routes.push(`://:store.shop${i}.com/products/:id`)\n  }\n\n  for (let i = 0; i < 100; i++) {\n    routes.push(`/search/${i}?q=&filter=`)\n    routes.push(`/products/${i}?category=&price_min=&price_max=`)\n  }\n\n  routes.push('/:a/:b/:c/:d/:e/:f/:g/:h/:i/:j')\n  routes.push('/blog/:year-:month-:day/:slug')\n  routes.push('/archive/:year/:month/:day/:hour-:minute')\n  routes.push('/api(/v:major(.:minor(.:patch)))/resources/:id(.:format)')\n  routes.push('/docs(/:lang)(/:version)(/:section)(/:page)')\n  routes.push('/*tenant/api/*version/resources/:id')\n  routes.push('://localhost:3000/dev')\n  routes.push('://localhost:8080/api')\n  routes.push('/prefix:param/middle:param2/suffix')\n\n  return routes\n}\n\nlet routes = generateRoutes()\nlet urls = [\n  'https://example.com/static/path/42/resource',\n  'https://example.com/static/path/199/resource',\n  'https://example.com/static/path/1000/resource',\n\n  'https://example.com/dynamic/50/value1',\n  'https://example.com/dynamic/150/value1/value2',\n  'https://example.com/dynamic/250/value1/value2/value3',\n  'https://example.com/dynamic/299/value1/value2/value3/value4',\n\n  'https://example.com/optional/10',\n  'https://example.com/optional/10/a',\n  'https://example.com/optional/11/a/b',\n  'https://example.com/optional/12/a/b/c',\n\n  'https://example.com/wildcard/25/some/nested/path',\n  'https://example.com/wildcard/99/deeply/nested/file.txt',\n  'https://example.com/prefix/value/middle/50/suffix',\n  'https://example.com/a/b/c/middle/99/suffix',\n\n  'https://tenant42.myapp.com/admin',\n  'https://tenant99.myapp.com/admin',\n  'https://subdomain.example50.com/api',\n  'http://subdomain.example100.com/api',\n  'https://store.shop25.com/products/123',\n  'https://store.shop149.com/products/abc-xyz',\n\n  'https://example.com/search/42?q=test&filter=active',\n  'https://example.com/products/99?category=electronics&price_min=100&price_max=500',\n\n  'https://example.com/a/b/c/d/e/f/g/h/i/j',\n  'https://example.com/blog/2024-12-01/my-post',\n  'https://example.com/archive/2024/12/01/14-30',\n  'https://example.com/api/resources/123',\n  'https://example.com/api/v2/resources/456',\n  'https://example.com/api/v2.1/resources/789.json',\n  'https://example.com/api/v2.1.5/resources/999.xml',\n  'https://example.com/docs',\n  'https://example.com/docs/en',\n  'https://example.com/docs/en/v2',\n  'https://example.com/docs/en/v2/api',\n  'https://example.com/docs/en/v2/api/reference',\n  'https://example.com/tenant123/api/v1/resources/456',\n  'http://localhost:3000/dev',\n  'http://localhost:8080/api',\n  'https://example.com/prefixvalue/middlevalue2/suffix',\n\n  // misses\n  'https://example.com/nonexistent/path',\n  'https://example.com/static/wrong',\n  'https://example.com/dynamic',\n  'https://tenant999.wrongdomain.com/admin',\n  'https://example.com/optional/999/extra/segments/that/dont/match',\n  'http://localhost:9999/api',\n  'https://example.com/search?missing=params',\n]\n\ndescribe('setup', () => {\n  bench('array', () => {\n    let matcher = new ArrayMatcher<null>()\n    routes.forEach((route) => matcher.add(route, null))\n  })\n\n  bench('trie', () => {\n    let matcher = new TrieMatcher<null>()\n    routes.forEach((route) => matcher.add(route, null))\n  })\n})\n\ndescribe('match', () => {\n  let arrayMatcher = new ArrayMatcher<null>()\n  routes.forEach((route) => arrayMatcher.add(route, null))\n  bench('array', () => {\n    urls.forEach((url) => arrayMatcher.match(url))\n  })\n\n  let trieMatcher = new TrieMatcher<null>()\n  routes.forEach((route) => trieMatcher.add(route, null))\n  bench('trie', () => {\n    urls.forEach((url) => trieMatcher.match(url))\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/bench/src/shopify.bench.ts",
    "content": "import { bench, describe } from 'vitest'\nimport { match } from 'path-to-regexp'\nimport { ArrayMatcher, TrieMatcher } from '@remix-run/route-pattern'\n\nimport { patterns } from '../patterns/shopify.ts'\n\nlet urls: Array<URL> = [\n  '/',\n  '/start',\n  '/free-trial',\n  '/es/prueba-gratis',\n  '/free-trial-new-years',\n  '/uk/start',\n  '/ca/free-trial',\n  '/uk/free-trial',\n  '/fr/demarrer',\n  '/mx/prueba-gratis',\n  '/co/prueba-gratis',\n  '/au/start',\n  '/it/aprire',\n  '/ar/prueba-gratis',\n  '/au/free-trial',\n  '/es-es/prueba-gratis',\n  '/de/starten',\n  '/pe/prueba-gratis',\n  '/nl/gratis-proef',\n  '/tr/start',\n  '/login',\n  '/ca/free-trial-new-years',\n  '/cl/prueba-gratis',\n  '/__exp/manual-assignments',\n  '/ca/start',\n  '/ie/start',\n  '/br/comecar',\n  '/br/parcerias/directory/services/123/456',\n  '/pl/blog/123',\n].map((pathname) => new URL(`https://shopify.com${pathname}`))\n\ndescribe('match shopify', () => {\n  let pathToRegexpMatcher: Array<ReturnType<typeof match>> = []\n  patterns.forEach((pattern) => {\n    let matchFn = match(pattern.replace('(:locale)', '{:locale}').replace('*', '*path'), {\n      decode: decodeURIComponent,\n    })\n    pathToRegexpMatcher.push(matchFn)\n    pathToRegexpMatcher.reverse()\n  })\n  bench('path-to-regexp', () => {\n    urls.forEach((url) => {\n      for (let matchFn of pathToRegexpMatcher) {\n        let match = matchFn(url.pathname)\n        if (match !== false) {\n          return\n        }\n      }\n    })\n  })\n\n  let arrayMatcher = new ArrayMatcher<null>()\n  patterns.forEach((pattern) => arrayMatcher.add(pattern, null))\n  bench('array', () => {\n    urls.forEach((url) => arrayMatcher.match(url))\n  })\n\n  let trieMatcher = new TrieMatcher<null>()\n  patterns.forEach((pattern) => trieMatcher.add(pattern, null))\n  bench('trie', () => {\n    urls.forEach((url) => trieMatcher.match(url))\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/bench/src/simple.bench.ts",
    "content": "import { bench, describe } from 'vitest'\nimport { ArrayMatcher, TrieMatcher } from '@remix-run/route-pattern'\n\nlet routes = [\n  '/',\n  '/about',\n  '/contact',\n  '/pricing',\n  '/blog',\n  '/docs',\n  '/login',\n  '/signup',\n  '/blog/:slug',\n  '/users/:id',\n  '/users/:id/settings',\n  '/users/:id/posts',\n  '/products/:id',\n  '/products/:id/reviews',\n  '/docs/:category',\n  '/docs/:category/:page',\n  '/api/v1/products',\n  '/api/v1/products/:id',\n  '/api/v1/users/:userId',\n  '/posts/:id',\n  '/posts/:id/comments',\n  '/posts/:id/comments/:commentId',\n  '/categories/:category',\n  '/tags/:tag',\n  '/users/:userId/posts/:postId',\n  '/products/:id/reviews/:reviewId',\n  '/products/:category/:slug',\n  '/blog/:year/:month/:day/:slug',\n  '/api/v1/users/:userId/orders/:orderId',\n  '/docs/:lang/:category/:page',\n  '/api(/v:version)/orders',\n  '/api(/v:version)/orders/:orderId',\n  '/users/:id(.:format)',\n  '/posts/:slug(.html)',\n  '/docs(/:section)(/:page)',\n  '/products/:id(/reviews)',\n  '/assets/images/*path',\n  '/downloads/*',\n  '/files/*path',\n  '/static/*',\n]\n\nlet urls = [\n  'https://example.com/',\n  'https://example.com/about',\n  'https://example.com/contact',\n  'https://example.com/pricing',\n  'https://example.com/blog',\n  'https://example.com/blog/introducing-remix',\n  'https://example.com/blog/route-patterns',\n  'https://example.com/users/123',\n  'https://example.com/users/456/settings',\n  'https://example.com/users/789/posts',\n  'https://example.com/users/123/posts/456',\n  'https://example.com/products/wireless-headphones',\n  'https://example.com/products/laptop/reviews',\n  'https://example.com/products/laptop/reviews/5',\n  'https://example.com/products/electronics/laptop',\n  'https://example.com/docs/getting-started',\n  'https://example.com/docs/api/reference',\n  'https://example.com/api/v1/products',\n  'https://example.com/api/v1/products/123',\n  'https://example.com/api/v1/users/456',\n  'https://example.com/api/orders',\n  'https://example.com/api/v2/orders',\n  'https://example.com/api/v2/orders/789',\n  'https://example.com/posts/hello-world',\n  'https://example.com/posts/123/comments',\n  'https://example.com/posts/123/comments/456',\n  'https://example.com/categories/electronics',\n  'https://example.com/tags/javascript',\n  'https://example.com/blog/2024/12/01/year-in-review',\n  'https://example.com/users/123.json',\n  'https://example.com/posts/my-post.html',\n  'https://example.com/docs/getting-started',\n  'https://example.com/docs/en/api/reference',\n  'https://example.com/products/123/reviews',\n  'https://example.com/assets/images/logo.png',\n  'https://example.com/assets/images/icons/home.svg',\n  'https://example.com/downloads/report.pdf',\n  'https://example.com/files/documents/contract.pdf',\n  'https://example.com/static/css/main.css',\n  'https://example.com/login',\n  'https://example.com/signup',\n  'https://example.com/api/v1/users/999/orders/888',\n  'https://example.com/blog/2024/12',\n  'https://example.com/users',\n  'https://example.com/products',\n  'https://example.com/api/v3/products',\n  'https://example.com/docs/en',\n  'https://example.com/posts/123/likes',\n  'https://example.com/categories',\n  'https://example.com/users/123/followers',\n  'https://example.com/api/orders/123/items',\n  'https://example.com/blog/2024',\n  'https://example.com/settings',\n  'https://example.com/profile',\n  'https://example.com/admin',\n  'https://example.com/dashboard',\n  'https://example.com/api/v1/admin',\n  'https://example.com/nonexistent',\n  'https://example.com/foo/bar/baz',\n  'https://example.com/test',\n]\n\ndescribe('setup', () => {\n  bench('array', () => {\n    let matcher = new ArrayMatcher<null>()\n    for (let route of routes) {\n      matcher.add(route, null)\n    }\n  })\n\n  bench('trie', () => {\n    let matcher = new TrieMatcher<null>()\n    for (let route of routes) {\n      matcher.add(route, null)\n    }\n  })\n})\n\ndescribe('match', () => {\n  let arrayMatcher = new ArrayMatcher<null>()\n  for (let route of routes) {\n    arrayMatcher.add(route, null)\n  }\n  bench('array', () => {\n    urls.forEach((url) => arrayMatcher.match(url))\n  })\n\n  let trieMatcher = new TrieMatcher<null>()\n  for (let route of routes) {\n    trieMatcher.add(route, null)\n  }\n  bench('trie', () => {\n    urls.forEach((url) => trieMatcher.match(url))\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/bench/types/href.ts",
    "content": "import { bench } from '@ark/attest'\nimport { RoutePattern } from '@remix-run/route-pattern'\n\nbench.baseline(() => {\n  let pattern = new RoutePattern('/')\n  pattern.href()\n})\n\nbench('href > simple route', () => {\n  let pattern = new RoutePattern('/posts/:id')\n  pattern.href({ id: '123' })\n}).types([493, 'instantiations'])\n\nbench('href > complex route', () => {\n  let pattern = new RoutePattern('/api(/v:major(.:minor))/*path/help')\n  pattern.href({ major: '1', minor: '2', path: 'users', help: 'help' })\n}).types([1252, 'instantiations'])\n\nbench('href > mediarss', async () => {\n  let { patterns } = await import('../patterns/mediarss.ts')\n  eagerlyEvaluateTypesForHrefParams(patterns)\n}).types([13867, 'instantiations'])\n\n// NOTE: This benchmark brings type checking to a crawl.\n// Uncomment to run the benchmark, but keep it commented to avoid CI failures.\n//\n// bench('href > shopify', async () => {\n//   let { patterns } = await import('../patterns/shopify.ts')\n//   eagerlyEvaluateTypesForHrefParams(patterns)\n// }).types([1540592, 'instantiations'])\n\n/** Type-only utility to force eager evaluation of href param types */\nfunction eagerlyEvaluateTypesForHrefParams<patterns extends ReadonlyArray<string>>(\n  // prettier-ignore\n  _: patterns & (\n    { [pattern in patterns[number]]: Parameters<RoutePattern<pattern>['href']>[0] } extends\n    { [pattern in patterns[number]]: Record<string, unknown> | null | undefined }\n    ? patterns : never\n  ),\n): void {}\n"
  },
  {
    "path": "packages/route-pattern/bench/types/join.ts",
    "content": "import { bench } from '@ark/attest'\nimport { RoutePattern, type Join } from '@remix-run/route-pattern'\n\nbench.baseline(() => {\n  let pattern = new RoutePattern('/')\n  pattern.join('/other')\n})\n\nbench('join', () => {\n  let pattern = new RoutePattern('/posts/:id')\n  pattern.join('/comments/:commentId')\n}).types([2445, 'instantiations'])\n\nbench('join > mediarss', async () => {\n  let { patterns } = await import('../patterns/mediarss.ts')\n  eagerlyEvaluateTypesForJoin(patterns)\n}).types([74069, 'instantiations'])\n\n// NOTE: This benchmark brings type checking to a crawl.\n// Uncomment to run the benchmark, but keep it commented to avoid CI failures.\n//\n// bench('join > shopify', async () => {\n//   let { patterns } = await import('../patterns/shopify.ts')\n//   // @ts-expect-error Type instantiation is excessively deep and possibly infinite. ts(2589)\n//   eagerlyEvaluateTypesForJoin(patterns)\n// }).types([5169925, 'instantiations'])\n\nfunction eagerlyEvaluateTypesForJoin<patterns extends ReadonlyArray<string>>(\n  // prettier-ignore\n  _: patterns & (\n    { [pattern in patterns[number]]: Join<patterns[number], string> } extends\n    { [pattern in patterns[number]]: string }\n    ? patterns : never\n  ),\n): void {}\n"
  },
  {
    "path": "packages/route-pattern/bench/types/match.ts",
    "content": "import { bench } from '@ark/attest'\nimport { RoutePattern, type RoutePatternMatch } from '@remix-run/route-pattern'\n\nbench.baseline(() => {\n  let pattern = new RoutePattern('/:var/*wild')\n  pattern.match('')?.params\n})\n\nbench('match > simple route', () => {\n  let pattern = new RoutePattern('/posts/:id')\n  let match = pattern.match('https://example.com/posts/123')\n  match?.params.id\n}).types([268, 'instantiations'])\n\nbench('match > complex route', () => {\n  let pattern = new RoutePattern('/api(/v:major(.:minor))/*path/help')\n  pattern.match('https://example.com/api/v1/users/123')?.params\n}).types([971, 'instantiations'])\n\nbench('match > mediarss', async () => {\n  let { patterns } = await import('../patterns/mediarss.ts')\n  eagerlyEvaluateTypesForMatchParams(patterns)\n}).types([12253, 'instantiations'])\n\n// NOTE: This benchmark brings type checking to a crawl.\n// Uncomment to run the benchmark, but keep it commented to avoid CI failures.\n//\n// bench('match > shopify', async () => {\n//   let { patterns } = await import('../patterns/shopify.ts')\n//   eagerlyEvaluateTypesForMatchParams(patterns)\n// }).types([1444090, 'instantiations'])\n\n/** Type-only utility to force eager evaluation of match param types */\nfunction eagerlyEvaluateTypesForMatchParams<patterns extends ReadonlyArray<string>>(\n  // prettier-ignore\n  _: patterns & (\n    { [pattern in patterns[number]]: GetMatchParams<ReturnType<RoutePattern<pattern>['match']>> } extends\n    { [pattern in patterns[number]]: Record<string, unknown> | null }\n    ? patterns : never\n  ),\n): void {}\n\n// prettier-ignore\ntype GetMatchParams<match extends RoutePatternMatch<string> | null> =\n  match extends RoutePatternMatch<string> ? match['params'] :\n  null\n"
  },
  {
    "path": "packages/route-pattern/bench/types/new.ts",
    "content": "import { bench } from '@ark/attest'\nimport { RoutePattern } from '@remix-run/route-pattern'\n\nbench.baseline(() => {\n  new RoutePattern('')\n})\n\nbench('new > simple route', () => {\n  let pattern = new RoutePattern('/posts/:id')\n  pattern.source\n}).types([3, 'instantiations'])\n\nbench('new > complex route', () => {\n  let pattern = new RoutePattern('/api(/v:major(.:minor))/*path/help')\n  pattern.source\n}).types([3, 'instantiations'])\n\nbench('new > mediarss', async () => {\n  let { patterns } = await import('../patterns/mediarss.ts')\n  eagerlyEvaluateTypesForNew(patterns)\n}).types([2648, 'instantiations'])\n\nbench('new > shopify', async () => {\n  let { patterns } = await import('../patterns/shopify.ts')\n  eagerlyEvaluateTypesForNew(patterns)\n}).types([12609, 'instantiations'])\n\n/** Type-only utility to force eager evaluation of href param types */\nfunction eagerlyEvaluateTypesForNew<patterns extends ReadonlyArray<string>>(\n  // prettier-ignore\n  _: patterns & (\n    { [pattern in patterns[number]]: RoutePattern<pattern> } extends\n    { [pattern in patterns[number]]: RoutePattern<string> }\n    ? patterns : never\n  ),\n): void {}\n"
  },
  {
    "path": "packages/route-pattern/bench/types/params.ts",
    "content": "import type { Params } from '@remix-run/route-pattern'\nimport { bench } from '@ark/attest'\n\nbench.baseline(() => {\n  type _ = Params<''>\n})\n\nbench('simple > Params', () => {\n  type _ = Params<'posts/:id'>\n}).types([381, 'instantiations'])\n\nbench('complex > Params', () => {\n  type _ = Params<'api(/v:major(.:minor))/*path/help'>\n}).types([1083, 'instantiations'])\n\nbench('mediarss > Params', async () => {\n  let { patterns } = await import('../patterns/mediarss.ts')\n  eagerlyEvaluateTypesForParams(patterns)\n}).types([12140, 'instantiations'])\n\n// NOTE: This benchmark brings type checking to a crawl.\n// Uncomment to run the benchmark, but keep it commented to avoid CI failures.\n//\n// bench('shopify > Params', async () => {\n//   let { patterns } = await import('../patterns/shopify.ts')\n//   eagerlyEvaluateTypesForParams(patterns)\n// }).types([1424185, 'instantiations'])\n\nfunction eagerlyEvaluateTypesForParams<patterns extends ReadonlyArray<string>>(\n  // prettier-ignore\n  _: patterns & (\n    { [pattern in patterns[number]]: Params<pattern> } extends\n    { [pattern in patterns[number]]: Record<string, unknown> }\n    ? patterns : never\n  ),\n): void {}\n"
  },
  {
    "path": "packages/route-pattern/package.json",
    "content": "{\n  \"name\": \"@remix-run/route-pattern\",\n  \"version\": \"0.19.0\",\n  \"description\": \"Match and generate URLs with strong typing\",\n  \"contributors\": [\n    \"Michael Jackson <mjijackson@gmail.com>\",\n    \"Pedro Cattori <pcattori@gmail.com>\"\n  ],\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/route-pattern\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/route-pattern#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\",\n    \"!src/**/*.types.bench.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./specificity\": \"./src/specificity.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./specificity\": {\n        \"types\": \"./dist/specificity.d.ts\",\n        \"default\": \"./dist/specificity.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"dedent\": \"^1.7.1\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"route\",\n    \"pattern\",\n    \"url\",\n    \"match\",\n    \"matcher\"\n  ]\n}\n"
  },
  {
    "path": "packages/route-pattern/src/index.ts",
    "content": "export { RoutePattern, type RoutePatternMatch } from './lib/route-pattern.ts'\nexport type { Join } from './lib/types/index.ts'\nexport type { Params } from './lib/route-pattern/params.ts'\nexport { ParseError } from './lib/route-pattern/parse.ts'\nexport { type HrefArgs, HrefError } from './lib/route-pattern/href.ts'\n\nexport { type Matcher, type Match } from './lib/matcher.ts'\nexport { ArrayMatcher } from './lib/array-matcher.ts'\nexport { TrieMatcher } from './lib/trie-matcher.ts'\n"
  },
  {
    "path": "packages/route-pattern/src/lib/array-matcher.ts",
    "content": "import { RoutePattern } from './route-pattern.ts'\nimport type { Match, Matcher } from './matcher.ts'\nimport * as Specificity from './specificity.ts'\n\n/**\n * Matcher implementation that checks patterns in insertion order and sorts matches by specificity.\n */\nexport class ArrayMatcher<data> implements Matcher<data> {\n  /**\n   * Whether pathname matching is case-insensitive.\n   */\n  readonly ignoreCase: boolean\n  #patterns: Array<{ pattern: RoutePattern; data: data }> = []\n\n  /**\n   * @param options Constructor options\n   * @param options.ignoreCase When `true`, pathname matching is case-insensitive for all patterns. Defaults to `false`.\n   */\n  constructor(options?: { ignoreCase?: boolean }) {\n    this.ignoreCase = options?.ignoreCase ?? false\n  }\n\n  /**\n   * Adds a pattern and associated data to the matcher.\n   *\n   * @param pattern Pattern to register.\n   * @param data Data returned when the pattern matches.\n   */\n  add(pattern: string | RoutePattern, data: data): void {\n    pattern = typeof pattern === 'string' ? new RoutePattern(pattern) : pattern\n    this.#patterns.push({ pattern, data })\n  }\n\n  /**\n   * Returns the best matching pattern for a URL.\n   *\n   * @param url URL to match.\n   * @param compareFn Specificity comparer used to rank matches.\n   * @returns The best match, or `null` when nothing matches.\n   */\n  match(url: string | URL, compareFn = Specificity.descending): Match<string, data> | null {\n    let bestMatch: Match<string, data> | null = null\n    for (let entry of this.#patterns) {\n      let match = entry.pattern.match(url, { ignoreCase: this.ignoreCase })\n      if (match) {\n        if (bestMatch === null || compareFn(match, bestMatch) < 0) {\n          bestMatch = { ...match, data: entry.data }\n        }\n      }\n    }\n    return bestMatch\n  }\n\n  /**\n   * Returns every pattern that matches a URL.\n   *\n   * @param url URL to match.\n   * @param compareFn Specificity comparer used to sort matches.\n   * @returns All matching routes sorted by specificity.\n   */\n  matchAll(url: string | URL, compareFn = Specificity.descending): Array<Match<string, data>> {\n    let matches: Array<Match<string, data>> = []\n    for (let entry of this.#patterns) {\n      let match = entry.pattern.match(url, { ignoreCase: this.ignoreCase })\n      if (match) {\n        matches.push({ ...match, data: entry.data })\n      }\n    }\n    return matches.sort(compareFn)\n  }\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/matcher.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport type { Matcher } from './matcher.ts'\nimport * as Specificity from './specificity.ts'\nimport { ArrayMatcher } from './array-matcher.ts'\nimport { TrieMatcher } from './trie-matcher.ts'\n\ntype MatcherConstructor = new (options?: { ignoreCase?: boolean }) => Matcher<null>\n\ndescribe('ArrayMatcher', () => testSuite(ArrayMatcher))\ndescribe('TrieMatcher', () => testSuite(TrieMatcher))\n\nfunction testSuite(MatcherClass: MatcherConstructor): void {\n  describe('match', () => {\n    describe('protocol', () => {\n      it('matches any protocol when protocol is omitted', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users', null)\n\n        let httpMatch = matcher.match('http://example.com/users')\n        assert.ok(httpMatch)\n\n        let httpsMatch = matcher.match('https://example.com/users')\n        assert.ok(httpsMatch)\n      })\n\n      it('matches http only when protocol is explicit http', () => {\n        let matcher = new MatcherClass()\n        matcher.add('http://example.com/users', null)\n\n        let match = matcher.match('http://example.com/users')\n        assert.ok(match)\n      })\n\n      it('matches https only when protocol is explicit https', () => {\n        let matcher = new MatcherClass()\n        matcher.add('https://example.com/users', null)\n\n        let match = matcher.match('https://example.com/users')\n        assert.ok(match)\n      })\n\n      it('matches both http and https when protocol is http(s)', () => {\n        let matcher = new MatcherClass()\n        matcher.add('http(s)://example.com/users', null)\n\n        let httpMatch = matcher.match('http://example.com/users')\n        assert.ok(httpMatch)\n\n        let httpsMatch = matcher.match('https://example.com/users')\n        assert.ok(httpsMatch)\n      })\n\n      it('returns null when protocol does not match', () => {\n        let matcher = new MatcherClass()\n        matcher.add('http://example.com/users', null)\n\n        let match = matcher.match('https://example.com/users')\n        assert.equal(match, null)\n      })\n    })\n\n    describe('hostname', () => {\n      it('matches any hostname when hostname is omitted', () => {\n        let matcher = new MatcherClass()\n        matcher.add('users', null)\n\n        let match1 = matcher.match('https://example.com/users')\n        assert.ok(match1)\n\n        let match2 = matcher.match('https://other.com/users')\n        assert.ok(match2)\n\n        let match3 = matcher.match('http://localhost/users')\n        assert.ok(match3)\n      })\n\n      it('matches exact static hostname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users', null)\n\n        let match = matcher.match('https://example.com/users')\n        assert.ok(match)\n        assert.deepEqual(match.params, {})\n      })\n\n      it('returns null when static hostname does not match', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users', null)\n\n        let match = matcher.match('https://other.com/users')\n        assert.equal(match, null)\n      })\n\n      it('matches variable segment in hostname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://:subdomain.example.com/api', null)\n\n        let match = matcher.match('https://api.example.com/api')\n        assert.ok(match)\n        assert.deepEqual(match.params, { subdomain: 'api' })\n      })\n\n      it('matches multiple variables in hostname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://:subdomain.:env.example.com/api', null)\n\n        let match = matcher.match('https://api.prod.example.com/api')\n        assert.ok(match)\n        assert.deepEqual(match.params, { subdomain: 'api', env: 'prod' })\n      })\n\n      it('matches wildcard segment in hostname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://*host.example.com/api', null)\n\n        let match = matcher.match('https://api.v1.example.com/api')\n        assert.ok(match)\n        assert.deepEqual(match.params, { host: 'api.v1' })\n      })\n\n      it('excludes unnamed wildcard from params', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://*.example.com/api', null)\n\n        let match = matcher.match('https://api.v1.example.com/api')\n        assert.ok(match)\n        assert.deepEqual(match.params, {})\n      })\n\n      it('matches optional hostname segments when present', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://api(-:version).example.com/users', null)\n\n        let match = matcher.match('https://api-v2.example.com/users')\n        assert.ok(match)\n        assert.deepEqual(match.params, { version: 'v2' })\n      })\n\n      it('matches optional hostname segments when absent', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://api(-:version).example.com/users', null)\n\n        let match = matcher.match('https://api.example.com/users')\n        assert.ok(match)\n        assert.deepEqual(match.params, { version: undefined })\n      })\n\n      it('matches nested optionals in hostname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://api(.:region(-:zone)).example.com/users', null)\n\n        let matchAll = matcher.match('https://api.us-east1.example.com/users')\n        assert.ok(matchAll)\n        assert.deepEqual(matchAll.params, { region: 'us', zone: 'east1' })\n\n        let matchPartial = matcher.match('https://api.us.example.com/users')\n        assert.ok(matchPartial)\n        assert.deepEqual(matchPartial.params, { region: 'us', zone: undefined })\n\n        let matchNone = matcher.match('https://api.example.com/users')\n        assert.ok(matchNone)\n        assert.deepEqual(matchNone.params, { region: undefined, zone: undefined })\n      })\n\n      it('matches multiple optionals in hostname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://:sub(-:version).example(.:tld).com/api', null)\n\n        let match = matcher.match('https://api-v2.example.dev.com/api')\n        assert.ok(match)\n        assert.deepEqual(match.params, { sub: 'api', version: 'v2', tld: 'dev' })\n      })\n\n      it('matches mixed static/variable/wildcard segments', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://*prefix.:env.example.com/api', null)\n\n        let match = matcher.match('https://api.v1.prod.example.com/api')\n        assert.ok(match)\n        assert.deepEqual(match.params, { prefix: 'api.v1', env: 'prod' })\n      })\n\n      it('prefers static over variable hostname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://:subdomain.example.com/api', null)\n        matcher.add('://api.example.com/api', null)\n\n        let match = matcher.match('https://api.example.com/api')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://api.example.com/api')\n      })\n\n      it('prefers variable over wildcard hostname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://*host.example.com/api', null)\n        matcher.add('://:subdomain.example.com/api', null)\n\n        let match = matcher.match('https://api.example.com/api')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://:subdomain.example.com/api')\n      })\n\n      it('prefers longer static prefix in hostname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://:subdomain.com/api', null)\n        matcher.add('://:subdomain.example.com/api', null)\n\n        let match = matcher.match('https://api.example.com/api')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://:subdomain.example.com/api')\n      })\n    })\n\n    describe('port', () => {\n      it('matches omitted port in URL when port is omitted in pattern', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users', null)\n\n        assert.ok(matcher.match('http://example.com/users'))\n        assert.ok(matcher.match('https://example.com/users'))\n      })\n\n      it('matches explicit port', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com:8080/users', null)\n\n        let match = matcher.match('http://example.com:8080/users')\n        assert.ok(match)\n        assert.deepEqual(match.params, {})\n      })\n\n      it('returns null when explicit port does not match', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com:8080/users', null)\n\n        assert.equal(matcher.match('http://example.com:3000/users'), null)\n      })\n\n      it('returns null when pattern has explicit port but URL omits port', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com:8080/users', null)\n\n        assert.equal(matcher.match('http://example.com/users'), null)\n      })\n\n      it('returns null when pattern omits port but URL has explicit port', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users', null)\n\n        assert.equal(matcher.match('http://example.com:8080/users'), null)\n      })\n\n      it('matches port with hostname variables', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://:subdomain.example.com:8080/api', null)\n\n        let match = matcher.match('https://api.example.com:8080/api')\n        assert.ok(match)\n        assert.deepEqual(match.params, { subdomain: 'api' })\n      })\n    })\n\n    describe('pathname', () => {\n      it('matches root pathname when pathname is empty', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com', null)\n\n        assert.ok(matcher.match('http://example.com/'))\n      })\n\n      it('matches static segments', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users/list', null)\n\n        let match = matcher.match('http://example.com/users/list')\n        assert.ok(match)\n        assert.deepEqual(match.params, {})\n      })\n\n      it('returns null when static segment does not match', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users', null)\n\n        assert.equal(matcher.match('http://example.com/posts'), null)\n      })\n\n      it('returns null when URL is shorter than pattern', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users/list', null)\n\n        assert.equal(matcher.match('http://example.com/users'), null)\n      })\n\n      it('returns null when trailing slash does not match', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users', null)\n\n        assert.equal(matcher.match('http://example.com/users/'), null)\n      })\n\n      it('matches variable segments', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users/:id', null)\n\n        let match = matcher.match('http://example.com/users/123')\n        assert.ok(match)\n        assert.deepEqual(match.params, { id: '123' })\n      })\n\n      it('matches multiple variables', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users/:userId/posts/:postId', null)\n\n        let match = matcher.match('http://example.com/users/42/posts/99')\n        assert.ok(match)\n        assert.deepEqual(match.params, { userId: '42', postId: '99' })\n      })\n\n      it('matches special characters in variable values', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/files/:filename', null)\n\n        let match = matcher.match('http://example.com/files/my-file_v2.txt')\n        assert.ok(match)\n        assert.deepEqual(match.params, { filename: 'my-file_v2.txt' })\n      })\n\n      it('preserves URL encoding in variable values', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search/:query', null)\n\n        let match = matcher.match('http://example.com/search/hello%20world')\n        assert.ok(match)\n        assert.deepEqual(match.params, { query: 'hello%20world' })\n      })\n\n      it('matches wildcard segments', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/files/*path', null)\n\n        let match = matcher.match('http://example.com/files/docs/readme.md')\n        assert.ok(match)\n        assert.deepEqual(match.params, { path: 'docs/readme.md' })\n      })\n\n      it('matches wildcard with continuation', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/files/*path/status', null)\n\n        let match = matcher.match('http://example.com/files/docs/api/status')\n        assert.ok(match)\n        assert.deepEqual(match.params, { path: 'docs/api' })\n      })\n\n      it('excludes unnamed wildcard from params', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/files/*/download', null)\n\n        let match = matcher.match('http://example.com/files/docs/download')\n        assert.ok(match)\n        assert.deepEqual(match.params, {})\n      })\n\n      it('matches optional segments when present', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/posts(/:lang)', null)\n\n        let match = matcher.match('http://example.com/posts/en')\n        assert.ok(match)\n        assert.deepEqual(match.params, { lang: 'en' })\n      })\n\n      it('matches optional segments when absent', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/posts(/:lang)', null)\n\n        let match = matcher.match('http://example.com/posts')\n        assert.ok(match)\n        assert.deepEqual(match.params, { lang: undefined })\n      })\n\n      it('matches nested optionals', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/docs(/:version(/:page))', null)\n\n        let match1 = matcher.match('http://example.com/docs/v1/intro')\n        assert.ok(match1)\n        assert.deepEqual(match1.params, { version: 'v1', page: 'intro' })\n\n        let match2 = matcher.match('http://example.com/docs/v1')\n        assert.ok(match2)\n        assert.deepEqual(match2.params, { version: 'v1', page: undefined })\n\n        let match3 = matcher.match('http://example.com/docs')\n        assert.ok(match3)\n        assert.deepEqual(match3.params, { version: undefined, page: undefined })\n      })\n\n      it('matches multiple optionals', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/api(/:version)/users(/:id)', null)\n\n        let match = matcher.match('http://example.com/api/v2/users/123')\n        assert.ok(match)\n        assert.deepEqual(match.params, { version: 'v2', id: '123' })\n      })\n\n      it('matches complex optionals for file extensions', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/files/:id(.:format)', null)\n\n        let match1 = matcher.match('http://example.com/files/doc123.pdf')\n        assert.ok(match1)\n        assert.deepEqual(match1.params, { id: 'doc123', format: 'pdf' })\n\n        let match2 = matcher.match('http://example.com/files/doc123')\n        assert.ok(match2)\n        assert.deepEqual(match2.params, { id: 'doc123', format: undefined })\n      })\n\n      it('matches mixed static/variable/wildcard segments', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/api/:version/files/*path', null)\n\n        let match = matcher.match('http://example.com/api/v1/files/docs/guide.pdf')\n        assert.ok(match)\n        assert.deepEqual(match.params, { version: 'v1', path: 'docs/guide.pdf' })\n      })\n\n      it('matches deep nesting', () => {\n        let matcher = new MatcherClass()\n        matcher.add(\n          '://example.com/products/electronics/computers/laptops/gaming/accessories/keyboards',\n          null,\n        )\n\n        let match = matcher.match(\n          'http://example.com/products/electronics/computers/laptops/gaming/accessories/keyboards',\n        )\n        assert.ok(match)\n        assert.deepEqual(match.params, {})\n      })\n\n      it('prefers static over variable pathname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users/new', null)\n        matcher.add('://example.com/users/:id', null)\n\n        let match = matcher.match('http://example.com/users/new')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/users/new')\n      })\n\n      it('prefers variable over wildcard pathname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/files/*path', null)\n        matcher.add('://example.com/files/:id', null)\n\n        let match = matcher.match('http://example.com/files/123')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/files/:id')\n      })\n\n      it('prefers longer static prefix in pathname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/api/:id', null)\n        matcher.add('://example.com/api/v1/:id', null)\n\n        let match = matcher.match('http://example.com/api/v1/users')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/api/v1/:id')\n      })\n    })\n\n    describe('search', () => {\n      it('matches any query params when no search constraints', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search', null)\n\n        assert.ok(matcher.match('http://example.com/search'))\n        assert.ok(matcher.match('http://example.com/search?q=test'))\n        assert.ok(matcher.match('http://example.com/search?q=test&lang=en'))\n      })\n\n      it('matches bare parameter for presence check', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search?q', null)\n\n        let match = matcher.match('http://example.com/search?q')\n        assert.ok(match)\n      })\n\n      it('matches bare parameter with any value', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search?q', null)\n\n        assert.ok(matcher.match('http://example.com/search?q=test'))\n        assert.ok(matcher.match('http://example.com/search?q=hello'))\n      })\n\n      it('matches any value constraint with non-empty value', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search?q=', null)\n\n        let match = matcher.match('http://example.com/search?q=test')\n        assert.ok(match)\n      })\n\n      it('returns null when any value constraint has empty value', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search?q=', null)\n\n        assert.equal(matcher.match('http://example.com/search?q='), null)\n        assert.equal(matcher.match('http://example.com/search?q'), null)\n      })\n\n      it('matches specific value with exact match', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/api?format=json', null)\n\n        let match = matcher.match('http://example.com/api?format=json')\n        assert.ok(match)\n      })\n\n      it('returns null when specific value does not match', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/api?format=json', null)\n\n        assert.equal(matcher.match('http://example.com/api?format=xml'), null)\n      })\n\n      it('matches multiple constraints', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search?q=&lang=en', null)\n\n        let match = matcher.match('http://example.com/search?q=test&lang=en')\n        assert.ok(match)\n      })\n\n      it('matches constraints regardless of order', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/api?format=json&version=v1', null)\n\n        let match = matcher.match('http://example.com/api?version=v1&format=json')\n        assert.ok(match)\n      })\n\n      it('allows extra params beyond constraints', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search?q', null)\n\n        let match = matcher.match('http://example.com/search?q=test&lang=en&page=2')\n        assert.ok(match)\n      })\n\n      it('preserves URL encoding in search parameter values', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search?q=hello%20world', null)\n\n        let match = matcher.match('http://example.com/search?q=hello%20world')\n        assert.ok(match)\n      })\n\n      it('matches repeated parameter values', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/filter?tags', null)\n\n        let match = matcher.match('http://example.com/filter?tags=a&tags=b')\n        assert.ok(match)\n      })\n\n      it('returns null when constraint is not met', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/api?auth', null)\n\n        assert.equal(matcher.match('http://example.com/api'), null)\n        assert.equal(matcher.match('http://example.com/api?other=value'), null)\n      })\n\n      it('prefers more constraints over fewer', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search?q', null)\n        matcher.add('://example.com/search?q&lang', null)\n\n        let match = matcher.match('http://example.com/search?q=test&lang=en')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/search?q&lang')\n      })\n\n      it('prefers exact value over any value', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/api?format', null)\n        matcher.add('://example.com/api?format=json', null)\n\n        let match = matcher.match('http://example.com/api?format=json')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/api?format=json')\n      })\n\n      it('prefers any value over bare presence', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search?q', null)\n        matcher.add('://example.com/search?q=', null)\n\n        let match = matcher.match('http://example.com/search?q=test')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/search?q=')\n      })\n    })\n\n    describe('ignoreCase', () => {\n      it('uses case-sensitive pathname matching by default', () => {\n        let matcher = new MatcherClass()\n        matcher.add('/Posts/:id', null)\n\n        assert.equal(matcher.match('https://example.com/posts/123'), null)\n        assert.equal(matcher.match('https://example.com/POSTS/123'), null)\n        assert.ok(matcher.match('https://example.com/Posts/123'))\n      })\n\n      it('ignores pathname case when ignoreCase is true', () => {\n        let matcher = new MatcherClass({ ignoreCase: true })\n        matcher.add('/Posts/:id', null)\n\n        assert.ok(matcher.match('https://example.com/posts/123'))\n        assert.ok(matcher.match('https://example.com/POSTS/123'))\n        assert.ok(matcher.match('https://example.com/Posts/123'))\n      })\n\n      it('ignores hostname case regardless of ignoreCase', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://Example.COM/users', null)\n\n        assert.ok(matcher.match('https://example.com/users'))\n        assert.ok(matcher.match('https://EXAMPLE.COM/users'))\n      })\n\n      it('matches search params case-sensitively regardless of ignoreCase', () => {\n        let matcher = new MatcherClass({ ignoreCase: true })\n        matcher.add('/api?Sort', null)\n\n        assert.ok(matcher.match('https://example.com/api?Sort'))\n        assert.equal(matcher.match('https://example.com/api?sort'), null)\n      })\n\n      it('defaults to false', () => {\n        let matcher = new MatcherClass()\n        matcher.add('/Posts/:id', null)\n        assert.equal(matcher.match('https://example.com/posts/123'), null)\n      })\n    })\n\n    describe('paramsMeta', () => {\n      it('returns empty arrays when no params', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users', null)\n\n        let match = matcher.match('http://example.com/users')\n        assert.ok(match)\n        assert.deepEqual(match.paramsMeta.hostname, [])\n        assert.deepEqual(match.paramsMeta.pathname, [])\n      })\n\n      it('includes wildcard hostname metadata for pathname-only patterns', () => {\n        let matcher = new MatcherClass()\n        matcher.add('/users/:id', null)\n\n        let match = matcher.match('http://example.com/users/123')\n        assert.ok(match)\n        assert.deepEqual(match.paramsMeta.hostname, [\n          { type: '*', name: '*', begin: 0, end: 11, value: 'example.com' },\n        ])\n        assert.deepEqual(match.paramsMeta.pathname, [\n          { type: ':', name: 'id', begin: 6, end: 9, value: '123' },\n        ])\n      })\n\n      it('includes hostname params with metadata', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://:subdomain.example.com/api', null)\n\n        let match = matcher.match('https://api.example.com/api')\n        assert.ok(match)\n        assert.equal(match.paramsMeta.hostname.length, 1)\n        assert.equal(match.paramsMeta.hostname[0].name, 'subdomain')\n        assert.equal(match.paramsMeta.hostname[0].type, ':')\n        assert.equal(match.paramsMeta.hostname[0].value, 'api')\n        assert.deepEqual(match.paramsMeta.pathname, [])\n      })\n\n      it('includes pathname params with metadata', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users/:id', null)\n\n        let match = matcher.match('http://example.com/users/123')\n        assert.ok(match)\n        assert.deepEqual(match.paramsMeta.hostname, [])\n        assert.equal(match.paramsMeta.pathname.length, 1)\n        assert.equal(match.paramsMeta.pathname[0].name, 'id')\n        assert.equal(match.paramsMeta.pathname[0].type, ':')\n        assert.equal(match.paramsMeta.pathname[0].value, '123')\n      })\n\n      it('includes params from both hostname and pathname', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://:subdomain.example.com/users/:id', null)\n\n        let match = matcher.match('https://api.example.com/users/123')\n        assert.ok(match)\n        assert.equal(match.paramsMeta.hostname.length, 1)\n        assert.equal(match.paramsMeta.hostname[0].name, 'subdomain')\n        assert.equal(match.paramsMeta.hostname[0].value, 'api')\n        assert.equal(match.paramsMeta.pathname.length, 1)\n        assert.equal(match.paramsMeta.pathname[0].name, 'id')\n        assert.equal(match.paramsMeta.pathname[0].value, '123')\n      })\n\n      it('includes wildcard params in metadata', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/files/*path', null)\n\n        let match = matcher.match('http://example.com/files/docs/readme.md')\n        assert.ok(match)\n        assert.equal(match.paramsMeta.pathname.length, 1)\n        assert.equal(match.paramsMeta.pathname[0].name, 'path')\n        assert.equal(match.paramsMeta.pathname[0].type, '*')\n        assert.equal(match.paramsMeta.pathname[0].value, 'docs/readme.md')\n      })\n\n      it('includes unnamed wildcards in metadata with name \"*\"', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/files/*/download', null)\n\n        let match = matcher.match('http://example.com/files/docs/download')\n        assert.ok(match)\n        assert.equal(match.paramsMeta.pathname.length, 1)\n        assert.equal(match.paramsMeta.pathname[0].name, '*')\n        assert.equal(match.paramsMeta.pathname[0].type, '*')\n        assert.equal(match.paramsMeta.pathname[0].value, 'docs')\n      })\n\n      it('excludes undefined optional params from metadata', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/posts(/:lang)', null)\n\n        let match = matcher.match('http://example.com/posts')\n        assert.ok(match)\n        assert.deepEqual(match.paramsMeta.pathname, [])\n      })\n\n      it('includes only matched optional params in metadata', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/docs(/:version(/:page))', null)\n\n        let match = matcher.match('http://example.com/docs/v1')\n        assert.ok(match)\n        assert.equal(match.paramsMeta.pathname.length, 1)\n        assert.equal(match.paramsMeta.pathname[0].name, 'version')\n        assert.equal(match.paramsMeta.pathname[0].value, 'v1')\n      })\n    })\n\n    describe('specificity', () => {\n      it('prefers static over variable', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/:segment', null)\n        matcher.add('://example.com/users', null)\n\n        let match = matcher.match('http://example.com/users')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/users')\n      })\n\n      it('prefers variable over wildcard', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/*path', null)\n        matcher.add('://example.com/:id', null)\n\n        let match = matcher.match('http://example.com/123')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/:id')\n      })\n\n      it('prefers longer static prefix over shorter', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/:id', null)\n        matcher.add('://example.com/api/:id', null)\n\n        let match = matcher.match('http://example.com/api/users')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/api/:id')\n      })\n\n      it('prefers hostname specificity over pathname specificity', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/*path', null)\n        matcher.add('://:subdomain.example.com/users', null)\n\n        let match = matcher.match('http://api.example.com/users')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://:subdomain.example.com/users')\n      })\n\n      it('increases specificity with search constraints', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/search', null)\n        matcher.add('://example.com/search?q', null)\n\n        let match = matcher.match('http://example.com/search?q=test')\n        assert.ok(match)\n        assert.equal(match.pattern.source, '://example.com/search?q')\n      })\n\n      it('returns null when no patterns match', () => {\n        let matcher = new MatcherClass()\n        matcher.add('://example.com/users', null)\n        matcher.add('://example.com/posts', null)\n\n        assert.equal(matcher.match('http://example.com/comments'), null)\n      })\n    })\n  })\n\n  describe('matchAll', () => {\n    it('returns all matches sorted by specificity', () => {\n      let matcher = new MatcherClass()\n      matcher.add('://example.com/*path', null)\n      matcher.add('://example.com/users/:id', null)\n      matcher.add('://example.com/users/new', null)\n\n      let matches = matcher.matchAll('http://example.com/users/new')\n      assert.deepEqual(\n        matches.map((m) => m.pattern.source),\n        ['://example.com/users/new', '://example.com/users/:id', '://example.com/*path'],\n      )\n    })\n\n    it('returns empty array when no matches', () => {\n      let matcher = new MatcherClass()\n      matcher.add('://example.com/users', null)\n      matcher.add('://example.com/posts', null)\n\n      let matches = matcher.matchAll('http://example.com/comments')\n      assert.deepEqual(matches, [])\n    })\n\n    it('includes patterns with same specificity', () => {\n      let matcher = new MatcherClass()\n      matcher.add('://example.com/users/:id', null)\n      matcher.add('://example.com/posts/:id', null)\n\n      let matches = matcher.matchAll('http://example.com/users/123')\n      assert.deepEqual(\n        matches.map((m) => m.pattern.source),\n        ['://example.com/users/:id'],\n      )\n\n      let matches2 = matcher.matchAll('http://example.com/posts/456')\n      assert.deepEqual(\n        matches2.map((m) => m.pattern.source),\n        ['://example.com/posts/:id'],\n      )\n    })\n\n    it('orders complex specificity scenarios consistently', () => {\n      let matcher = new MatcherClass()\n      // Add patterns in random order to ensure ordering is by specificity, not insertion order\n      matcher.add('/*path', null)\n      matcher.add('://api.example.com/users/:id', null)\n      matcher.add('://:subdomain.example.com/*path', null)\n      matcher.add('://api.example.com/*path', null)\n      matcher.add('/users/:id', null)\n      matcher.add('://api.example.com/users/123', null)\n      matcher.add('://:subdomain.example.com/users/:id', null)\n      matcher.add('://api.example.com/:resource/:id', null)\n      matcher.add('://api.example.com/users(/:id)', null)\n      matcher.add('/users(/:id)', null)\n      matcher.add('://api(.:region).example.com/users/:id', null)\n\n      let matches = matcher.matchAll('http://api.example.com/users/123')\n\n      assert.deepEqual(\n        matches.map((m) => m.pattern.source),\n        [\n          '://api.example.com/users/123',\n          '://api.example.com/users/:id',\n          '://api.example.com/users(/:id)',\n          '://api(.:region).example.com/users/:id',\n          '://api.example.com/:resource/:id',\n          '://api.example.com/*path',\n          '://:subdomain.example.com/users/:id',\n          '://:subdomain.example.com/*path',\n          '/users/:id',\n          '/users(/:id)',\n          '/*path',\n        ],\n      )\n    })\n  })\n\n  describe('custom compareFn', () => {\n    it('uses custom compareFn for match() to select best', () => {\n      let matcher = new MatcherClass()\n      matcher.add('://example.com/*path', null)\n      matcher.add('://example.com/users/:id', null)\n      matcher.add('://example.com/users/123', null)\n\n      // Default behavior: static wins\n      let defaultMatch = matcher.match('http://example.com/users/123')\n      assert.ok(defaultMatch)\n      assert.equal(defaultMatch.pattern.source, '://example.com/users/123')\n\n      // Custom: prefer least specific (reverse of default)\n      let customMatch = matcher.match('http://example.com/users/123', Specificity.ascending)\n      assert.ok(customMatch)\n      assert.equal(customMatch.pattern.source, '://example.com/*path')\n    })\n\n    it('uses custom compareFn for matchAll() to sort results', () => {\n      let matcher = new MatcherClass()\n      matcher.add('://example.com/*path', null)\n      matcher.add('://example.com/users/:id', null)\n      matcher.add('://example.com/users/123', null)\n\n      // Custom: sort by pattern source alphabetically\n      let matches = matcher.matchAll('http://example.com/users/123', (a, b) =>\n        a.pattern.source.localeCompare(b.pattern.source),\n      )\n\n      assert.deepEqual(\n        matches.map((m) => m.pattern.source),\n        ['://example.com/*path', '://example.com/users/:id', '://example.com/users/123'],\n      )\n    })\n\n    it('supports ascending specificity order', () => {\n      let matcher = new MatcherClass()\n      matcher.add('://example.com/*path', null)\n      matcher.add('://example.com/users/:id', null)\n      matcher.add('://example.com/users/123', null)\n\n      let matches = matcher.matchAll('http://example.com/users/123', Specificity.ascending)\n\n      assert.deepEqual(\n        matches.map((m) => m.pattern.source),\n        ['://example.com/*path', '://example.com/users/:id', '://example.com/users/123'],\n      )\n    })\n  })\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/matcher.ts",
    "content": "import type { RoutePattern, RoutePatternMatch } from './route-pattern.ts'\n\n/**\n * Successful pattern match paired with matcher-specific data.\n */\nexport type Match<source extends string = string, data = unknown> = RoutePatternMatch<source> & {\n  data: data\n}\n\ntype CompareFn = (a: RoutePatternMatch, b: RoutePatternMatch) => number\n\n/**\n * A type for matching URLs against patterns.\n */\nexport type Matcher<data = unknown> = {\n  /**\n   * When `true`, pathname matching is case-insensitive for all patterns in this matcher. Hostname is always case-insensitive; search remains case-sensitive.\n   */\n  readonly ignoreCase: boolean\n\n  /**\n   * Add a pattern to the matcher.\n   *\n   * @param pattern The pattern to add\n   * @param data The data to associate with the pattern\n   */\n  add(pattern: string | RoutePattern, data: data): void\n\n  /**\n   * Find the best match for a URL.\n   *\n   * @param url The URL to match\n   * @returns The match result, or `null` if no match was found\n   */\n  match(url: string | URL, compareFn?: CompareFn): Match<string, data> | null\n\n  /**\n   * Find all matches for a URL.\n   *\n   * @param url The URL to match\n   * @returns All matches\n   */\n  matchAll(url: string | URL, compareFn?: CompareFn): Array<Match<string, data>>\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/regexp.ts",
    "content": "/**\n * Emulates the `RegExp.escape()` available in all latest browsers and runtimes, but not in Node 22.\n * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape#browser_compatibility\n *\n * @param text The text to escape.\n * @returns The escaped text.\n */\nexport function escape(text: string): string {\n  return text.replace(/[.*+?^${}()|[\\]\\\\-]/g, '\\\\$&')\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/AGENTS.md",
    "content": "# Route Pattern Helpers\n\nThis directory contains helpers for [`route-pattern.ts`](../route-pattern.ts).\n\n## Organization\n\n- **[`part-pattern.ts`](./part-pattern.ts)**: Logic that applies to any `PartPattern` (i.e. hostname _and_ pathname)\n- **Other files**: Organize by feature (not by pattern part)\n  - [`href.ts`](./href.ts): Href generation\n  - [`join.ts`](./join.ts): Pattern joining\n  - [`match.ts`](./match.ts): URL matching\n  - [`parse.ts`](./parse.ts): Parsing patterns\n  - [`serialize.ts`](./serialize.ts): Serializing to strings\n  - [`split.ts`](./split.ts): Splitting source strings\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/href.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport dedent from 'dedent'\n\nimport { HrefError } from './href.ts'\nimport { RoutePattern } from '../route-pattern.ts'\n\ndescribe('HrefError', () => {\n  describe('missing-hostname', () => {\n    it('shows pattern', () => {\n      let pattern = new RoutePattern('https://*:8080/api')\n      let error = new HrefError({\n        type: 'missing-hostname',\n        pattern,\n      })\n      assert.equal(\n        error.toString(),\n        dedent`\n          HrefError: pattern requires hostname\n\n          Pattern: https://:8080/api\n        `,\n      )\n    })\n  })\n\n  describe('missing-params', () => {\n    it('shows missing param, pattern, and params', () => {\n      let pattern = new RoutePattern('https://example.com/:collection/:id')\n      let error = new HrefError({\n        type: 'missing-params',\n        pattern,\n        partPattern: pattern.ast.pathname,\n        missingParams: ['collection', 'id'],\n        params: {},\n      })\n      assert.equal(\n        error.toString(),\n        dedent`\n          HrefError: missing param(s): 'collection', 'id'\n\n          Pattern: https://example.com/:collection/:id\n          Params: {}\n        `,\n      )\n    })\n  })\n\n  describe('missing-search-params', () => {\n    it('shows single missing search param', () => {\n      let pattern = new RoutePattern('https://example.com/search?q=')\n      let error = new HrefError({\n        type: 'missing-search-params',\n        pattern,\n        missingParams: ['q'],\n        searchParams: {},\n      })\n      assert.equal(\n        error.toString(),\n        dedent`\n          HrefError: missing required search param(s): 'q'\n\n          Pattern: https://example.com/search?q=\n          Search params: {}\n        `,\n      )\n    })\n\n    it('shows multiple missing search params', () => {\n      let pattern = new RoutePattern('https://example.com/search?q=&sort=')\n      let error = new HrefError({\n        type: 'missing-search-params',\n        pattern,\n        missingParams: ['q', 'sort'],\n        searchParams: { page: 1 },\n      })\n      assert.equal(\n        error.toString(),\n        dedent`\n          HrefError: missing required search param(s): 'q', 'sort'\n\n          Pattern: https://example.com/search?q=&sort=\n          Search params: {\"page\":1}\n        `,\n      )\n    })\n  })\n\n  describe('nameless-wildcard', () => {\n    it('shows error message with pattern', () => {\n      let pattern = new RoutePattern('https://example.com/api/*/users')\n      let error = new HrefError({\n        type: 'nameless-wildcard',\n        pattern,\n      })\n      assert.equal(\n        error.toString(),\n        dedent`\n          HrefError: pattern contains nameless wildcard\n\n          Pattern: https://example.com/api/*/users\n        `,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/href.ts",
    "content": "import { unreachable } from '../unreachable.ts'\nimport type { RoutePattern } from '../route-pattern.ts'\nimport type { PartPattern } from './part-pattern.ts'\nimport type { ParseParams } from './params.ts'\nimport type { Split, SplitPattern } from '../types/split.ts'\nimport type { Simplify } from '../types/utils.ts'\n\n// todo: `Split<source>` return { hostname: \"\" } instead of { hostname: undefined } which causes issues\n/**\n * Tuple of arguments accepted by `RoutePattern.href()` for a given pattern.\n */\nexport type HrefArgs<source extends string> = _HrefArgs<ParseHrefParams<source>>\n// prettier-ignore\ntype _HrefArgs<params> =\n  {} extends params ?\n    [params?: Simplify<params & Record<string, unknown>> | null | undefined, searchParams?: SearchParams]\n  :\n  [params: Simplify<params & Record<string, unknown>>, searchParams?: SearchParams]\n\ntype SearchParams = Record<\n  string,\n  string | number | null | undefined | Array<string | number | null | undefined>\n>\n\n// prettier-ignore\ntype ParseHrefParams<source extends string> =\n  Split<source> extends infer split extends SplitPattern ?\n    split extends ({ protocol: string, hostname: undefined } | { hostname: undefined, port: string }) ? never : // missing-hostname\n    ParseParams<split> extends infer params extends Record<string, string | undefined> ?\n      params extends { '*': string } ? never : // nameless-wildcard\n      Optionalize<Omit<params, '*'>>\n    :\n    never\n  :\n  never\n\n// prettier-ignore\ntype Optionalize<record extends Record<string, string | undefined>> =\n  // { name: string } -> { name: string | number }\n  & { [key in keyof record as undefined extends record[key] ? never : key]: string | number }\n  // { name: string | undefined } -> { name?: string | number | null | undefined }\n  & { [key in keyof record as undefined extends record[key] ? key : never]?: string | number | null | undefined }\n\n/**\n * Generate a search query string from a pattern and params.\n *\n * @param pattern the route pattern containing search constraints\n * @param searchParams the search params to include in the href\n * @returns the query string (without leading `?`), or undefined if empty\n */\nexport function hrefSearch(pattern: RoutePattern, searchParams: SearchParams): string | undefined {\n  let constraints = pattern.ast.search\n  if (constraints.size === 0 && Object.keys(searchParams).length === 0) {\n    return undefined\n  }\n\n  let urlSearchParams = new URLSearchParams()\n\n  for (let [key, value] of Object.entries(searchParams)) {\n    if (Array.isArray(value)) {\n      for (let v of value) {\n        if (v != null) {\n          urlSearchParams.append(key, String(v))\n        }\n      }\n    } else if (value != null) {\n      urlSearchParams.append(key, String(value))\n    }\n  }\n\n  let missingParams: Array<string> = []\n  for (let [key, constraint] of constraints) {\n    if (constraint === null) {\n      if (key in searchParams) continue\n      urlSearchParams.append(key, '')\n    } else if (constraint.size === 0) {\n      if (key in searchParams) continue\n      missingParams.push(key)\n    } else {\n      for (let value of constraint) {\n        if (urlSearchParams.getAll(key).includes(value)) continue\n        urlSearchParams.append(key, value)\n      }\n    }\n  }\n\n  if (missingParams.length > 0) {\n    throw new HrefError({\n      type: 'missing-search-params',\n      pattern,\n      missingParams,\n      searchParams: searchParams,\n    })\n  }\n\n  let result = urlSearchParams.toString()\n  return result || undefined\n}\n\ntype HrefErrorDetails =\n  | {\n      type: 'missing-hostname'\n      pattern: RoutePattern\n    }\n  | {\n      type: 'missing-params'\n      pattern: RoutePattern\n      partPattern: PartPattern\n      missingParams: Array<string>\n      params: Record<string, unknown>\n    }\n  | {\n      type: 'missing-search-params'\n      pattern: RoutePattern\n      missingParams: Array<string>\n      searchParams: SearchParams\n    }\n  | {\n      type: 'nameless-wildcard'\n      pattern: RoutePattern\n    }\n\n/**\n * Error thrown when a route pattern cannot generate an href from the supplied args.\n */\nexport class HrefError extends Error {\n  /**\n   * Structured details describing why href generation failed.\n   */\n  details: HrefErrorDetails\n\n  constructor(details: HrefErrorDetails) {\n    let message = HrefError.message(details)\n\n    super(message)\n    this.name = 'HrefError'\n    this.details = details\n  }\n\n  /**\n   * Formats an error message for the given href failure details.\n   *\n   * @param details Structured href failure details.\n   * @returns A human-readable error message.\n   */\n  static message(details: HrefErrorDetails): string {\n    let pattern = details.pattern.toString()\n\n    if (details.type === 'missing-hostname') {\n      return `pattern requires hostname\\n\\nPattern: ${pattern}`\n    }\n\n    if (details.type === 'nameless-wildcard') {\n      return `pattern contains nameless wildcard\\n\\nPattern: ${pattern}`\n    }\n\n    if (details.type === 'missing-search-params') {\n      let params = details.missingParams.map((p) => `'${p}'`).join(', ')\n      let searchParamsStr = JSON.stringify(details.searchParams)\n      return `missing required search param(s): ${params}\\n\\nPattern: ${pattern}\\nSearch params: ${searchParamsStr}`\n    }\n\n    if (details.type === 'missing-params') {\n      let params = details.missingParams.map((p) => `'${p}'`).join(', ')\n      return `missing param(s): ${params}\\n\\nPattern: ${pattern}\\nParams: ${JSON.stringify(details.params)}`\n    }\n\n    unreachable(details)\n  }\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/join.ts",
    "content": "import type { RoutePattern } from '../route-pattern.ts'\nimport { PartPattern, type PartPatternToken } from './part-pattern.ts'\n\ntype Pathname = RoutePattern['ast']['pathname']\n\n/**\n * Joins two pathnames, adding slash between them if needed.\n *\n * Trailing slash is omitted from `a`.\n * A slash is added between `a` and `b` if `b` does not have a leading slash.\n *\n * Definitions:\n * - A leading slash can only have parens `(` `)` before it.\n * - A trailing slash can only have parens `(` `)` after it.\n *\n * Conceptually:\n *\n * ```ts\n * join('a', 'b') -> 'a/b'\n * join('a/', 'b') -> 'a/b'\n * join('a', '/b') -> 'a/b'\n * join('a/', '/b') -> 'a/b'\n * join('(a)', '(b)') -> '(a)/(b)'\n * join('(a/)', '(b)') -> '(a)/(b)'\n * join('(a)', '(/b)') -> '(a)(/b)'\n * join('(a/)', '(/b)') -> '(a)(/b)'\n * ```\n *\n * @param a the first pathname pattern\n * @param b the second pathname pattern\n * @returns the joined pathname pattern\n */\nexport function joinPathname(a: Pathname, b: Pathname): Pathname {\n  if (a.tokens.length === 0) return b\n  if (b.tokens.length === 0) return a\n\n  let tokens: Array<PartPatternToken> = []\n\n  // if `a` has a trailing separator (only optionals after it)\n  // then omit the separator\n  let aLastNonOptionalIndex = a.tokens.findLastIndex(\n    (token) => token.type !== '(' && token.type !== ')',\n  )\n  let aLastNonOptional = a.tokens[aLastNonOptionalIndex]\n  let aHasTrailingSeparator = aLastNonOptional?.type === 'separator'\n\n  a.tokens.forEach((token, index) => {\n    if (index === aLastNonOptionalIndex && token.type === 'separator') {\n      return\n    }\n    tokens.push(token)\n  })\n\n  // if `b` does not have a leading separator (only optionals before it)\n  // then add a separator\n  let bFirstNonOptional = b.tokens.find((token) => token.type !== '(' && token.type !== ')')\n  let needsSeparator = bFirstNonOptional === undefined || bFirstNonOptional.type !== 'separator'\n  if (needsSeparator) {\n    tokens.push({ type: 'separator' })\n  }\n\n  let tokenOffset = tokens.length\n\n  b.tokens.forEach((token) => {\n    tokens.push(token)\n  })\n\n  let optionals = new Map()\n  for (let [begin, end] of a.optionals) {\n    if (aHasTrailingSeparator) {\n      // one less token before this optional since trailing slash token was omitted\n      if (begin > aLastNonOptionalIndex) begin -= 1\n      if (end > aLastNonOptionalIndex) end -= 1\n    }\n    optionals.set(begin, end)\n  }\n  for (let [begin, end] of b.optionals) {\n    optionals.set(tokenOffset + begin, tokenOffset + end)\n  }\n\n  return new PartPattern({ tokens, optionals }, { type: 'pathname' })\n}\n\ntype Search = RoutePattern['ast']['search']\n\n/**\n * Joins two search patterns, merging params and their constraints.\n *\n * Conceptually:\n *\n * ```ts\n * search('?a', '?b') -> '?a&b'\n * search('?a=1', '?a=2') -> '?a=1&a=2'\n * search('?a=1', '?b=2') -> '?a=1&b=2'\n * search('', '?a') -> '?a'\n * ```\n *\n * @param a the first search constraints\n * @param b the second search constraints\n * @returns the merged search constraints\n */\nexport function joinSearch(a: Search, b: Search): Search {\n  let result = new Map<string, Set<string> | null>()\n\n  for (let [name, constraint] of a) {\n    result.set(name, constraint === null ? null : new Set(constraint))\n  }\n\n  for (let [name, constraint] of b) {\n    let current = result.get(name)\n\n    if (current === null || current === undefined) {\n      result.set(name, constraint === null ? null : new Set(constraint))\n      continue\n    }\n\n    if (constraint !== null) {\n      for (let value of constraint) {\n        current.add(value)\n      }\n    }\n  }\n\n  return result\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/match.ts",
    "content": "import type { RoutePattern } from '../route-pattern.ts'\n\n/**\n * Test if URL search params satisfy the given constraints. Matching is case-sensitive.\n *\n * @param params The URL search params to test\n * @param constraints The search constraints to check against\n * @returns `true` if the params satisfy all constraints\n */\nexport function matchSearch(\n  params: URLSearchParams,\n  constraints: RoutePattern['ast']['search'],\n): boolean {\n  for (let [name, constraint] of constraints) {\n    let hasParam = params.has(name)\n    let values = params.getAll(name)\n\n    if (constraint === null) {\n      if (!hasParam) return false\n      continue\n    }\n\n    if (constraint.size === 0) {\n      if (values.every((value) => value === '')) return false\n      continue\n    }\n\n    for (let value of constraint) {\n      if (!values.includes(value)) return false\n    }\n  }\n  return true\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/params.test.ts",
    "content": "import type { Assert, IsEqual } from '../types/utils.ts'\nimport type { Params } from './params.ts'\n\n// prettier-ignore\nexport type Tests = [\n  // No params\n  Assert<IsEqual<\n    Params<'products'>,\n    {}\n  >>,\n\n  // Required variables in pathname\n  Assert<IsEqual<\n    Params<'products/:id'>,\n    { id: string }\n  >>,\n\n  Assert<IsEqual<\n    Params<'users/:userId/albums/:albumId'>,\n    { userId: string; albumId: string }\n  >>,\n\n  // Optional variables in pathname\n  Assert<IsEqual<\n    Params<'products(/:id)'>,\n    { id: string | undefined }\n  >>,\n\n  Assert<IsEqual<\n    Params<'products/:sku(/:variant)'>,\n    { sku: string; variant: string | undefined }\n  >>,\n\n  // Wildcards\n  Assert<IsEqual<\n    Params<'files/*'>,\n    {}\n  >>,\n\n  Assert<IsEqual<\n    Params<'files/*path'>,\n    { path: string  }\n  >>,\n\n  Assert<IsEqual<\n    Params<'files/*(.:ext)'>,\n    { ext: string | undefined }\n  >>,\n\n  // Unnamed wildcard within optional\n  Assert<IsEqual<\n    Params<'files(/*).:ext'>,\n    { ext: string }\n  >>,\n\n  Assert<IsEqual<\n    Params<'files/*path(.:ext)'>,\n    { path: string; ext: string | undefined }\n  >>,\n\n  // Enums (no params)\n  Assert<IsEqual<\n    Params<'avatar.{jpg,png,gif}'>,\n    {}\n  >>,\n\n  Assert<IsEqual<\n    Params<':path.{jpg,png,gif}'>,\n    { path: string }\n  >>,\n\n  // Hostname/protocol params\n  Assert<IsEqual<\n    Params<'https://:sub.example.com/posts'>,\n    { sub: string }\n  >>,\n\n  Assert<IsEqual<\n    Params<':proto://example.com/path'>,\n    {}\n  >>,\n\n  // Mixed host + path params\n  Assert<IsEqual<\n    Params<'https://:sub.example.com/:id(.:ext)'>,\n    { sub: string; id: string; ext: string | undefined }\n  >>,\n\n  // Nested optionals: variables\n  Assert<IsEqual<\n    Params<'api(/:major(/:minor))'>,\n    { major: string | undefined; minor: string | undefined }\n  >>,\n\n  // Nested optionals: named wildcard + variable\n  Assert<IsEqual<\n    Params<'files(/*path(.:ext))'>,\n    { path: string | undefined; ext: string | undefined }\n  >>,\n\n  // Nested optionals: unnamed wildcard + variable\n  Assert<IsEqual<\n    Params<'files(/*(.:ext))'>,\n    { ext: string | undefined }\n  >>\n]\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/params.ts",
    "content": "import type { Split, SplitPattern } from '../types/split'\nimport type { Simplify } from '../types/utils'\n\n/**\n * Extracted route params for a route-pattern source string.\n */\nexport type Params<source extends string> = Simplify<Omit<ParseParams<Split<source>>, '*'>>\n\n// prettier-ignore\nexport type ParseParams<split extends SplitPattern> =\n  split extends { hostname: infer hostname, pathname: infer pathname } ?\n    & (hostname extends string ? ParsePartParams<hostname> : {})\n    & (pathname extends string ? ParsePartParams<pathname> : {})\n  :\n  never\n\n// prettier-ignore\ntype ParsePartParams<source, stack extends Array<null> = []> =\n  string extends source ? Record<string, string | undefined> :\n  source extends `${infer char extends '\\\\' | ':' | '*' | '(' | ')'}${infer rest}` ?\n    char extends '\\\\' ?\n      rest extends `${string}${infer rest}` ?\n        ParsePartParams<rest, stack> :\n        never\n    :\n    char extends ':' ?\n      IdentifierParse<rest> extends { identifier: infer name extends string, rest: infer rest } ?\n        Param<name, stack> & ParsePartParams<rest, stack>\n      :\n      never\n    :\n    char extends '*' ?\n      IdentifierParse<rest> extends { identifier: infer name extends string, rest: infer rest } ?\n        Param<name extends '' ? '*' : name, stack> & ParsePartParams<rest, stack>\n      :\n      never\n    :\n    char extends '(' ? ParsePartParams<rest, [...stack, null]> :\n    char extends ')' ?\n      stack extends [null, ...infer tail extends Array<null>] ?\n        ParsePartParams<rest, tail> :\n        never\n    :\n    never\n  :\n  source extends `${string}${infer rest}` ? ParsePartParams<rest, stack> :\n  {}\n\ntype Param<name extends string, stack extends Array<null>> = stack extends []\n  ? { [key in name]: string }\n  : { [key in name]: string | undefined }\n\n// Identifier --------------------------------------------------------------------------------------\n\ntype IdentifierHead = a_z | A_Z | '_' | '$'\ntype IdentifierTail = IdentifierHead | _0_9\n\ntype IdentifierParse<source extends string> = _IdentifierParse<{ identifier: ''; rest: source }>\n\n// prettier-ignore\ntype _IdentifierParse<state extends { identifier: string, rest: string }> =\n  state extends { identifier: '', rest: `${infer head extends IdentifierHead}${infer tail}` } ?\n    _IdentifierParse<{ identifier: head, rest: tail }>\n  :\n  state extends { identifier: string, rest: `${infer head extends IdentifierTail}${infer rest}`} ?\n    _IdentifierParse<{ identifier: `${state['identifier']}${head}`, rest: rest }>\n  :\n  state\n\n// Character classes -------------------------------------------------------------------------------\n\n// prettier-ignore\ntype a_z = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'\ntype A_Z = Uppercase<a_z>\ntype _0_9 = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/parse.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport dedent from 'dedent'\n\nimport { ParseError } from './parse.ts'\n\ndescribe('ParseError', () => {\n  it('exposes type, source, and index properties', () => {\n    let error = new ParseError('unmatched (', 'foo(bar', 3)\n    assert.equal(error.type, 'unmatched (')\n    assert.equal(error.source, 'foo(bar')\n    assert.equal(error.index, 3)\n  })\n\n  it('underlines the error indices', () => {\n    let error = new ParseError('unmatched (', 'api/(v:major', 4)\n    assert.equal(\n      error.toString(),\n      dedent`\n        ParseError: unmatched (\n\n        api/(v:major\n            ^\n      `,\n    )\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/parse.ts",
    "content": "import { PartPattern } from './part-pattern.ts'\nimport type { Span } from './split.ts'\nimport type { RoutePattern } from '../route-pattern.ts'\n\nexport function parseProtocol(source: string, span: Span | null): RoutePattern['ast']['protocol'] {\n  if (!span) return null\n  let protocol = source.slice(...span)\n  if (protocol === '' || protocol === 'http' || protocol === 'https' || protocol === 'http(s)') {\n    return protocol === '' ? null : protocol\n  }\n  throw new ParseError('invalid protocol', source, span[0])\n}\n\nexport function parseHostname(\n  source: string,\n  span: Span | null,\n): RoutePattern['ast']['hostname'] | null {\n  if (!span) return null\n  let part = PartPattern.parse(source, { span, type: 'hostname' })\n  if (isNamelessWildcard(part)) return null\n  return part\n}\n\nfunction isNamelessWildcard(part: PartPattern): boolean {\n  if (part.tokens.length !== 1) return false\n  let token = part.tokens[0]\n  if (token.type !== '*') return false\n  return token.name === '*'\n}\n\n/**\n * Parse a search string into search constraints.\n *\n * Search constraints define what query params must be present:\n * - `null`: param must be present (e.g., `?q`, `?q=`, `?q=1`)\n * - Empty `Set`: param must be present with a value (e.g., `?q=1`)\n * - Non-empty `Set`: param must be present with all these values (e.g., `?q=x&q=y`)\n *\n * Examples:\n * ```ts\n * parse('q')       // -> Map([['q', null]])\n * parse('q=')      // -> Map([['q', new Set()]])\n * parse('q=x&q=y') // -> Map([['q', new Set(['x', 'y'])]])\n * ```\n *\n * @param source the search string to parse (without leading `?`)\n * @returns the parsed search constraints\n */\nexport function parseSearch(source: string): RoutePattern['ast']['search'] {\n  let constraints: Map<string, Set<string> | null> = new Map()\n\n  for (let param of source.split('&')) {\n    if (param === '') continue\n    let equalIndex = param.indexOf('=')\n\n    // `?q`\n    if (equalIndex === -1) {\n      let name = decodeURIComponent(param)\n      if (!constraints.get(name)) {\n        constraints.set(name, null)\n      }\n      continue\n    }\n\n    let name = decodeURIComponent(param.slice(0, equalIndex))\n    let value = decodeURIComponent(param.slice(equalIndex + 1))\n\n    // `?q=`\n    if (value.length === 0) {\n      if (!constraints.get(name)) {\n        constraints.set(name, new Set())\n      }\n      continue\n    }\n\n    // `?q=1`\n    let constraint = constraints.get(name)\n    constraints.set(name, constraint ? constraint.add(value) : new Set([value]))\n  }\n\n  return constraints\n}\n\ntype ParseErrorType =\n  | 'unmatched ('\n  | 'unmatched )'\n  | 'missing variable name'\n  | 'dangling escape'\n  | 'invalid protocol'\n\n/**\n * Error thrown when a route pattern cannot be parsed.\n */\nexport class ParseError extends Error {\n  /**\n   * The parse failure category.\n   */\n  type: ParseErrorType\n\n  /**\n   * Original pattern source being parsed.\n   */\n  source: string\n\n  /**\n   * Character index where parsing failed.\n   */\n  index: number\n\n  constructor(type: ParseErrorType, source: string, index: number) {\n    let underline = ' '.repeat(index) + '^'\n    let message = `${type}\\n\\n${source}\\n${underline}`\n\n    super(message)\n    this.name = 'ParseError'\n    this.type = type\n    this.source = source\n    this.index = index\n  }\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/part-pattern.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { ParseError } from './parse.ts'\nimport { PartPattern } from './part-pattern.ts'\n\ndescribe('PartPattern', () => {\n  describe('parse', () => {\n    type AST = ConstructorParameters<typeof PartPattern>[0]\n    function assertParse(source: string, ast: AST) {\n      assert.deepEqual(\n        PartPattern.parse(source, { type: 'pathname' }),\n        new PartPattern(ast, { type: 'pathname' }),\n      )\n    }\n\n    function assertParseError(source: string, type: ParseError['type'], index: number) {\n      assert.throws(\n        () => PartPattern.parse(source, { type: 'pathname' }),\n        new ParseError(type, source, index),\n      )\n    }\n\n    it('parses static text', () => {\n      assertParse('abc', {\n        tokens: [{ type: 'text', text: 'abc' }],\n        optionals: new Map(),\n      })\n    })\n\n    it('parses a variable', () => {\n      assertParse(':abc', {\n        tokens: [{ type: ':', name: 'abc' }],\n        optionals: new Map(),\n      })\n      assertParse(':_hello_WORLD', {\n        tokens: [{ type: ':', name: '_hello_WORLD' }],\n        optionals: new Map(),\n      })\n      assertParse(':$_hello_WORLD$123$', {\n        tokens: [{ type: ':', name: '$_hello_WORLD$123$' }],\n        optionals: new Map(),\n      })\n    })\n\n    it('parses a wildcard', () => {\n      assertParse('*', {\n        tokens: [{ type: '*', name: '*' }],\n        optionals: new Map(),\n      })\n      assertParse('*abc', {\n        tokens: [{ type: '*', name: 'abc' }],\n        optionals: new Map(),\n      })\n      assertParse('*_hello_WORLD', {\n        tokens: [{ type: '*', name: '_hello_WORLD' }],\n        optionals: new Map(),\n      })\n      assertParse('*$_hello_WORLD$123$', {\n        tokens: [{ type: '*', name: '$_hello_WORLD$123$' }],\n        optionals: new Map(),\n      })\n    })\n\n    it('parses an optional', () => {\n      assertParse('aa(bb)cc', {\n        tokens: [\n          { type: 'text', text: 'aa' },\n          { type: '(' },\n          { type: 'text', text: 'bb' },\n          { type: ')' },\n          { type: 'text', text: 'cc' },\n        ],\n        optionals: new Map([[1, 3]]),\n      })\n      assertParse('(aa(bb)cc)', {\n        tokens: [\n          { type: '(' },\n          { type: 'text', text: 'aa' },\n          { type: '(' },\n          { type: 'text', text: 'bb' },\n          { type: ')' },\n          { type: 'text', text: 'cc' },\n          { type: ')' },\n        ],\n        optionals: new Map([\n          [0, 6],\n          [2, 4],\n        ]),\n      })\n    })\n\n    it('parses combinations of text, variables, wildcards, optionals', () => {\n      assertParse('api/(v:major(.:minor)/)run', {\n        tokens: [\n          { type: 'text', text: 'api' },\n          { type: 'separator' },\n          { type: '(' },\n          { type: 'text', text: 'v' },\n          { type: ':', name: 'major' },\n          { type: '(' },\n          { type: 'text', text: '.' },\n          { type: ':', name: 'minor' },\n          { type: ')' },\n          { type: 'separator' },\n          { type: ')' },\n          { type: 'text', text: 'run' },\n        ],\n        optionals: new Map([\n          [2, 10],\n          [5, 8],\n        ]),\n      })\n\n      assertParse('*/node_modules/(*path/):package/dist/index.:ext', {\n        tokens: [\n          { type: '*', name: '*' },\n          { type: 'separator' },\n          { type: 'text', text: 'node_modules' },\n          { type: 'separator' },\n          { type: '(' },\n          { type: '*', name: 'path' },\n          { type: 'separator' },\n          { type: ')' },\n          { type: ':', name: 'package' },\n          { type: 'separator' },\n          { type: 'text', text: 'dist' },\n          { type: 'separator' },\n          { type: 'text', text: 'index.' },\n          { type: ':', name: 'ext' },\n        ],\n        optionals: new Map([[4, 7]]),\n      })\n    })\n\n    it('parses repeated param names', () => {\n      assertParse(':id/:id', {\n        tokens: [{ type: ':', name: 'id' }, { type: 'separator' }, { type: ':', name: 'id' }],\n        optionals: new Map(),\n      })\n      assertParse('*id/*id', {\n        tokens: [{ type: '*', name: 'id' }, { type: 'separator' }, { type: '*', name: 'id' }],\n        optionals: new Map(),\n      })\n      assertParse('*/*', {\n        tokens: [{ type: '*', name: '*' }, { type: 'separator' }, { type: '*', name: '*' }],\n        optionals: new Map(),\n      })\n      assertParse(':a/*a/:b/*b/:b/*a/:a', {\n        tokens: [\n          { type: ':', name: 'a' },\n          { type: 'separator' },\n          { type: '*', name: 'a' },\n          { type: 'separator' },\n          { type: ':', name: 'b' },\n          { type: 'separator' },\n          { type: '*', name: 'b' },\n          { type: 'separator' },\n          { type: ':', name: 'b' },\n          { type: 'separator' },\n          { type: '*', name: 'a' },\n          { type: 'separator' },\n          { type: ':', name: 'a' },\n        ],\n        optionals: new Map(),\n      })\n    })\n\n    it(\"throws 'unmatched ('\", () => {\n      assertParseError('(', 'unmatched (', 0)\n      assertParseError('(()', 'unmatched (', 0)\n      assertParseError('()(', 'unmatched (', 2)\n    })\n    it(\"throws 'unmatched )'\", () => {\n      assertParseError(')', 'unmatched )', 0)\n      assertParseError(')()', 'unmatched )', 0)\n      assertParseError('())', 'unmatched )', 2)\n    })\n    it(\"throws 'missing variable name'\", () => {\n      assertParseError(':', 'missing variable name', 0)\n      assertParseError('a:', 'missing variable name', 1)\n      assertParseError('(a:)', 'missing variable name', 2)\n      assertParseError(':(a)', 'missing variable name', 0)\n      assertParseError(':123', 'missing variable name', 0)\n      assertParseError('::', 'missing variable name', 0)\n    })\n    it(\"throws 'dangling escape'\", () => {\n      assertParseError('\\\\', 'dangling escape', 0)\n    })\n  })\n\n  describe('source', () => {\n    function assertSource(expected: string) {\n      let partPattern = PartPattern.parse(expected, { type: 'pathname' })\n      assert.equal(partPattern.source, expected)\n    }\n\n    it('returns source representation of pattern', () => {\n      assertSource('api/(v:major(.:minor)/)run')\n      assertSource('*/node_modules/(*path/):package/dist/index.:ext')\n    })\n  })\n\n  describe('match', () => {\n    type MatchParam = { type: ':' | '*'; name: string; value: string; begin: number; end: number }\n    function assertMatch(pattern: string, part: string, expected: Array<MatchParam>) {\n      let result = PartPattern.parse(pattern, { type: 'pathname' }).match(part)\n      assert.deepEqual(result, expected)\n    }\n\n    it('matches variable', () => {\n      assertMatch('posts/:id', 'posts/123', [\n        { type: ':', name: 'id', value: '123', begin: 6, end: 9 },\n      ])\n    })\n\n    it('matches multiple variables', () => {\n      assertMatch('posts/:id/comments/:commentId', 'posts/123/comments/456', [\n        { type: ':', name: 'id', value: '123', begin: 6, end: 9 },\n        { type: ':', name: 'commentId', value: '456', begin: 19, end: 22 },\n      ])\n    })\n\n    it('matches multiple variables with repeated names', () => {\n      assertMatch(':id/:id', '123/456', [\n        { type: ':', name: 'id', value: '123', begin: 0, end: 3 },\n        { type: ':', name: 'id', value: '456', begin: 4, end: 7 },\n      ])\n    })\n\n    it('matches wildcard', () => {\n      assertMatch('files/*path', 'files/a/b/c', [\n        { type: '*', name: 'path', value: 'a/b/c', begin: 6, end: 11 },\n      ])\n    })\n\n    it('matches multiple wildcards', () => {\n      assertMatch('*prefix/middle/*suffix', 'a/b/middle/c/d', [\n        { type: '*', name: 'prefix', value: 'a/b', begin: 0, end: 3 },\n        { type: '*', name: 'suffix', value: 'c/d', begin: 11, end: 14 },\n      ])\n    })\n\n    it('matches multiple wildcards with repeated names', () => {\n      assertMatch('*path/*path', 'a/b/c/d', [\n        { type: '*', name: 'path', value: 'a/b/c', begin: 0, end: 5 },\n        { type: '*', name: 'path', value: 'd', begin: 6, end: 7 },\n      ])\n    })\n\n    it('matches optional parameter when present', () => {\n      assertMatch('api(/:version)', 'api/v1', [\n        { type: ':', name: 'version', value: 'v1', begin: 4, end: 6 },\n      ])\n    })\n\n    it('matches optional parameter when absent', () => {\n      assertMatch('api(/:version)', 'api', [])\n    })\n\n    it('matches nested optional parameters when partially present', () => {\n      assertMatch('api(/:major(/:minor))', 'api/v2', [\n        { type: ':', name: 'major', value: 'v2', begin: 4, end: 6 },\n      ])\n    })\n\n    it('matches nested optional parameters when all absent', () => {\n      assertMatch('api(/:major(/:minor))', 'api', [])\n    })\n\n    it('matches pattern with case-sensitivity determined by ignoreCase option', () => {\n      let pattern = PartPattern.parse('Posts/:id', { type: 'pathname' })\n      assert.equal(pattern.match('posts/123'), null)\n      assert.notEqual(pattern.match('Posts/123', { ignoreCase: false }), null)\n      assert.notEqual(pattern.match('posts/123', { ignoreCase: true }), null)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/part-pattern.ts",
    "content": "import { ParseError } from './parse.ts'\nimport { unreachable } from '../unreachable.ts'\nimport * as RE from '../regexp.ts'\nimport type { Span } from './split.ts'\nimport { HrefError } from './href.ts'\nimport type { RoutePattern } from '../route-pattern.ts'\n\ntype MatchParam = {\n  type: ':' | '*'\n  name: string\n  value: string\n  begin: number\n  end: number\n}\n\nexport type PartPatternMatch = Array<MatchParam>\nexport type PartPatternToken =\n  | { type: 'text'; text: string }\n  | { type: 'separator' }\n  | { type: '(' | ')' }\n  | { type: ':' | '*'; name: string }\n\nconst IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z_$0-9]*/\n\nexport class PartPattern {\n  readonly tokens: ReadonlyArray<PartPatternToken>\n  readonly optionals: ReadonlyMap<number, number>\n  readonly type: 'hostname' | 'pathname'\n\n  #regexp: { caseSensitive: RegExp; caseInsensitive: RegExp } | undefined\n\n  constructor(\n    args: {\n      tokens: ReadonlyArray<PartPatternToken>\n      optionals: ReadonlyMap<number, number>\n    },\n    options: { type: 'hostname' | 'pathname' },\n  ) {\n    this.tokens = args.tokens\n    this.optionals = args.optionals\n    this.type = options.type\n  }\n\n  get params(): Array<Extract<PartPatternToken, { type: ':' | '*' }>> {\n    let result: Array<Extract<PartPatternToken, { type: ':' | '*' }>> = []\n    for (let token of this.tokens) {\n      if (token.type === ':' || token.type === '*') {\n        result.push(token)\n      }\n    }\n    return result\n  }\n\n  get separator(): '.' | '/' {\n    return separatorForType(this.type)\n  }\n\n  static parse(\n    source: string,\n    options: { span?: Span; type: 'hostname' | 'pathname' },\n  ): PartPattern {\n    let span = options.span ?? [0, source.length]\n    let separator = separatorForType(options.type)\n\n    let tokens: Array<PartPatternToken> = []\n    let optionals: Map<number, number> = new Map()\n\n    let appendText = (text: string) => {\n      let currentToken = tokens.at(-1)\n      if (currentToken?.type === 'text') {\n        currentToken.text += text\n      } else {\n        tokens.push({ type: 'text', text })\n      }\n    }\n\n    let i = span[0]\n    let optionalStack: Array<number> = []\n    while (i < span[1]) {\n      let char = source[i]\n\n      // optional begin\n      if (char === '(') {\n        optionalStack.push(tokens.length)\n        tokens.push({ type: char })\n        i += 1\n        continue\n      }\n\n      // optional end\n      if (char === ')') {\n        let begin = optionalStack.pop()\n        if (begin === undefined) {\n          throw new ParseError('unmatched )', source, i)\n        }\n        optionals.set(begin, tokens.length)\n        tokens.push({ type: char })\n        i += 1\n        continue\n      }\n\n      // variable\n      if (char === ':') {\n        i += 1\n        let name = IDENTIFIER_RE.exec(source.slice(i, span[1]))?.[0]\n        if (!name) {\n          throw new ParseError('missing variable name', source, i - 1)\n        }\n        tokens.push({ type: ':', name })\n        i += name.length\n        continue\n      }\n\n      // wildcard\n      if (char === '*') {\n        i += 1\n        let name = IDENTIFIER_RE.exec(source.slice(i, span[1]))?.[0]\n        tokens.push({ type: '*', name: name ?? '*' })\n        i += name?.length ?? 0\n        continue\n      }\n\n      if (separator && char === separator) {\n        tokens.push({ type: 'separator' })\n        i += 1\n        continue\n      }\n\n      // escaped char\n      if (char === '\\\\') {\n        if (i + 1 === span[1]) {\n          throw new ParseError('dangling escape', source, i)\n        }\n        let text = source.slice(i, i + 2)\n        appendText(text)\n        i += text.length\n        continue\n      }\n\n      // text\n      appendText(char)\n      i += 1\n    }\n    if (optionalStack.length > 0) {\n      throw new ParseError('unmatched (', source, optionalStack.at(-1)!)\n    }\n\n    return new PartPattern({ tokens, optionals }, { type: options.type })\n  }\n\n  get source(): string {\n    let result = ''\n    for (let token of this.tokens) {\n      if (token.type === '(' || token.type === ')') {\n        result += token.type\n        continue\n      }\n\n      if (token.type === 'text') {\n        result += token.text\n        continue\n      }\n\n      if (token.type === ':' || token.type === '*') {\n        let name = token.name === '*' ? '' : token.name\n        result += `${token.type}${name}`\n        continue\n      }\n\n      if (token.type === 'separator') {\n        result += this.separator\n        continue\n      }\n\n      unreachable(token.type)\n    }\n\n    return result\n  }\n\n  /**\n   * Generate a partial href from a part pattern and params.\n   *\n   * @param pattern The route pattern containing the part pattern.\n   * @param params The parameters to substitute into the pattern.\n   * @returns The partial href for the given params\n   */\n  href(pattern: RoutePattern, params: Record<string, unknown>): string {\n    let missingParams: Array<string> = []\n\n    let stack: Array<{ begin?: number; href: string }> = [{ href: '' }]\n    let i = 0\n    while (i < this.tokens.length) {\n      let token = this.tokens[i]\n      if (token.type === 'text') {\n        stack[stack.length - 1].href += token.text\n        i += 1\n        continue\n      }\n      if (token.type === 'separator') {\n        stack[stack.length - 1].href += this.separator\n        i += 1\n        continue\n      }\n      if (token.type === '(') {\n        stack.push({ begin: i, href: '' })\n        i += 1\n        continue\n      }\n      if (token.type === ')') {\n        let frame = stack.pop()!\n        stack[stack.length - 1].href += frame.href\n        i += 1\n        continue\n      }\n      if (token.type === ':' || token.type === '*') {\n        let value = params[token.name]\n        if (value === undefined) {\n          if (stack.length <= 1) {\n            if (token.name === '*') {\n              throw new HrefError({\n                type: 'nameless-wildcard',\n                pattern,\n              })\n            }\n            missingParams.push(token.name)\n          }\n          let frame = stack.pop()!\n          i = this.optionals.get(frame.begin!)! + 1\n          continue\n        }\n        stack[stack.length - 1].href += typeof value === 'string' ? value : String(value)\n        i += 1\n        continue\n      }\n      unreachable(token.type)\n    }\n    if (missingParams.length > 0) {\n      throw new HrefError({\n        type: 'missing-params',\n        pattern,\n        partPattern: this,\n        missingParams,\n        params,\n      })\n    }\n    if (stack.length !== 1) unreachable()\n    return stack[0].href\n  }\n\n  match(part: string, options?: { ignoreCase?: boolean }): PartPatternMatch | null {\n    let ignoreCase = options?.ignoreCase ?? false\n    if (this.#regexp === undefined) {\n      this.#regexp = this.#toRegExp()\n    }\n    let regexp = ignoreCase ? this.#regexp.caseInsensitive : this.#regexp.caseSensitive\n    let reMatch = regexp.exec(part)\n    if (reMatch === null) return null\n    let match: PartPatternMatch = []\n    let params = this.params\n    for (let i = 0; i < params.length; i++) {\n      let param = params[i]\n      let captureIndex = i + 1\n      let span = reMatch.indices?.[captureIndex]\n      if (span === undefined) continue\n      match.push({\n        type: param.type,\n        name: param.name,\n        begin: span[0],\n        end: span[1],\n        value: reMatch[captureIndex],\n      })\n    }\n    return match\n  }\n\n  #toRegExp(): { caseSensitive: RegExp; caseInsensitive: RegExp } {\n    if (this.#regexp !== undefined) return this.#regexp\n    let result = ''\n    for (let token of this.tokens) {\n      if (token.type === 'text') {\n        result += RE.escape(token.text)\n        continue\n      }\n\n      if (token.type === ':') {\n        result += this.separator ? `([^${this.separator}]+?)` : `(.+?)`\n        continue\n      }\n\n      if (token.type === '*') {\n        result += `(.*)`\n        continue\n      }\n\n      if (token.type === '(') {\n        result += '(?:'\n        continue\n      }\n\n      if (token.type === ')') {\n        result += ')?'\n        continue\n      }\n\n      if (token.type === 'separator') {\n        result += RE.escape(this.separator ?? '')\n        continue\n      }\n\n      unreachable(token.type)\n    }\n    let source = `^${result}$`\n    this.#regexp = {\n      caseSensitive: new RegExp(source, 'd'),\n      caseInsensitive: new RegExp(source, 'di'),\n    }\n    return this.#regexp\n  }\n}\n\nfunction separatorForType(type: 'hostname' | 'pathname'): '.' | '/' {\n  if (type === 'hostname') return '.'\n  return '/'\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/serialize.ts",
    "content": "import type { RoutePattern } from '../route-pattern.ts'\n\n/**\n * Serialize search constraints to a query string.\n *\n * @param constraints the search constraints to convert\n * @returns the query string (without leading `?`), or undefined if empty\n */\nexport function serializeSearch(constraints: RoutePattern['ast']['search']): string | undefined {\n  if (constraints.size === 0) {\n    return undefined\n  }\n\n  let parts: Array<string> = []\n\n  for (let [key, constraint] of constraints) {\n    if (constraint === null) {\n      parts.push(encodeURIComponent(key))\n    } else if (constraint.size === 0) {\n      parts.push(`${encodeURIComponent(key)}=`)\n    } else {\n      for (let value of constraint) {\n        parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n      }\n    }\n  }\n\n  let result = parts.join('&')\n  return result || undefined\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/split.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { split, type SplitResult } from './split.ts'\n\nfunction assertSplit(source: string, expected: Partial<SplitResult>) {\n  expected.protocol = expected.protocol ?? null\n  expected.hostname = expected.hostname ?? null\n  expected.port = expected.port ?? null\n  expected.pathname = expected.pathname ?? null\n  expected.search = expected.search ?? null\n\n  assert.deepEqual(split(source), expected)\n}\n\ndescribe('split', () => {\n  it('extracts protocol', () => {\n    assertSplit('http://', { protocol: [0, 4] })\n  })\n\n  it('extracts hostname', () => {\n    assertSplit('://example.com', { hostname: [3, 14] })\n  })\n\n  it('extracts port', () => {\n    assertSplit('://example.com:8000', { hostname: [3, 14], port: [15, 19] })\n  })\n\n  it('extracts pathname', () => {\n    assertSplit('pathname', { pathname: [0, 8] })\n    assertSplit('/pathname', { pathname: [1, 9] })\n    assertSplit('//pathname', { pathname: [1, 10] })\n  })\n\n  it('returns null for empty pathname', () => {\n    assertSplit('/', { pathname: null })\n    assertSplit('http:///', { protocol: [0, 4], pathname: null })\n    assertSplit('://example/', { hostname: [3, 10], pathname: null })\n  })\n\n  it('extracts search', () => {\n    assertSplit('?q=1', { search: [1, 4] })\n  })\n\n  it('extracts protocol + hostname', () => {\n    assertSplit('http://example.com', { protocol: [0, 4], hostname: [7, 18] })\n  })\n\n  it('extracts protocol + pathname', () => {\n    assertSplit('http:///pathname', { protocol: [0, 4], pathname: [8, 16] })\n  })\n\n  it('extracts hostname + pathname', () => {\n    assertSplit('://example.com/pathname', { hostname: [3, 14], pathname: [15, 23] })\n  })\n\n  it('extracts protocol + hostname + pathname', () => {\n    assertSplit('http://example.com/pathname', {\n      protocol: [0, 4],\n      hostname: [7, 18],\n      pathname: [19, 27],\n    })\n  })\n\n  it('extracts protocol + hostname + port + pathname + search', () => {\n    assertSplit('http://example.com:8000/pathname?q=1', {\n      protocol: [0, 4],\n      hostname: [7, 18],\n      port: [19, 23],\n      pathname: [24, 32],\n      search: [33, 36],\n    })\n  })\n\n  it('treats / before :// as pathname', () => {\n    assertSplit('pathname/then://solidus', { pathname: [0, 23] })\n    assertSplit('/pathname/then://solidus', { pathname: [1, 24] })\n  })\n\n  it('treats ? before :// as search', () => {\n    assertSplit('?search://solidus', { search: [1, 17] })\n  })\n\n  it('treats ? before / as search', () => {\n    assertSplit('?search/slash', { search: [1, 13] })\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern/split.ts",
    "content": "export type Span = [begin: number, end: number]\n\nexport type SplitResult = {\n  protocol: Span | null\n  hostname: Span | null\n  port: Span | null\n  pathname: Span | null\n  search: Span | null\n}\n\n/**\n * Split a route pattern into protocol, hostname, port, pathname, and search\n * spans delimited as `protocol://hostname:port/pathname?search`.\n *\n * Delimiters are not included in the spans with the exception of the leading `/` for pathname.\n * Spans are [begin (inclusive), end (exclusive)].\n *\n * @param source the route pattern string to split\n * @returns an object containing spans for each URL component\n */\nexport function split(source: string): SplitResult {\n  let result: SplitResult = {\n    protocol: null,\n    hostname: null,\n    port: null,\n    pathname: null,\n    search: null,\n  }\n\n  let questionMarkIndex = source.indexOf('?')\n  if (questionMarkIndex !== -1) {\n    result.search = span(questionMarkIndex + 1, source.length)\n    source = source.slice(0, questionMarkIndex)\n  }\n\n  let solidusIndex = source.indexOf('://')\n\n  if (solidusIndex === -1) {\n    // path/without/solidus\n    result.pathname = pathnameSpan(source, 0, source.length)\n    return result\n  }\n\n  let slashIndex = source.indexOf('/')\n  if (slashIndex === solidusIndex + 1) {\n    // first slash is from solidus, find next slash\n    slashIndex = source.indexOf('/', solidusIndex + 3)\n  }\n\n  if (slashIndex === -1) {\n    // (protocol)://(host)\n    result.protocol = span(0, solidusIndex)\n    let host = span(solidusIndex + 3, source.length)\n    if (host) {\n      let { hostname, port } = hostSpans(source, host)\n      result.hostname = hostname\n      result.port = port\n    }\n    return result\n  }\n\n  if (slashIndex < solidusIndex) {\n    // pathname/with://solidus\n    result.pathname = pathnameSpan(source, 0, source.length)\n    return result\n  }\n\n  // (protocol)://(host)/(pathname)\n  result.protocol = span(0, solidusIndex)\n  let host = span(solidusIndex + 3, slashIndex)\n  if (host) {\n    let { hostname, port } = hostSpans(source, host)\n    result.hostname = hostname\n    result.port = port\n  }\n  result.pathname = pathnameSpan(source, slashIndex, source.length)\n  return result\n}\n\nfunction span(start: number, end: number): Span | null {\n  if (start === end) return null\n  return [start, end]\n}\n\nfunction hostSpans(source: string, host: Span): { hostname: Span | null; port: Span | null } {\n  let lastColonIndex = source.slice(0, host[1]).lastIndexOf(':')\n  if (lastColonIndex === -1 || lastColonIndex < host[0]) return { hostname: host, port: null }\n\n  if (source.slice(lastColonIndex + 1, host[1]).match(/^\\d+$/)) {\n    return { hostname: span(host[0], lastColonIndex), port: span(lastColonIndex + 1, host[1]) }\n  }\n  return { hostname: host, port: null }\n}\n\nfunction pathnameSpan(source: string, begin: number, end: number): Span | null {\n  if (source[begin] === '/') begin += 1\n  return span(begin, end)\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { RoutePattern } from './route-pattern.ts'\nimport { HrefError } from './route-pattern/href.ts'\n\ndescribe('RoutePattern', () => {\n  describe('parse', () => {\n    function assertParse(\n      source: string,\n      expected: { [K in Exclude<keyof RoutePattern['ast'], 'search'>]?: string } & {\n        search?: Record<string, Array<string> | null>\n      },\n    ) {\n      let pattern = new RoutePattern(source)\n      let expectedSearch = new Map()\n      if (expected.search) {\n        for (let name in expected.search) {\n          let value = expected.search[name]\n          expectedSearch.set(name, value ? new Set(expected.search[name]) : null)\n        }\n      }\n      assert.deepEqual(\n        {\n          protocol: pattern.ast.protocol,\n          hostname: pattern.ast.hostname?.source ?? null,\n          port: pattern.ast.port,\n          pathname: pattern.ast.pathname.source,\n          search: pattern.ast.search,\n        },\n        {\n          // explicitly set each prop so that we can omitted keys from `expected` to set them as defaults\n          protocol: expected.protocol ?? null,\n          hostname: expected.hostname ?? null,\n          port: expected.port ?? null,\n          pathname: expected.pathname ?? '',\n          search: expectedSearch,\n        },\n      )\n    }\n\n    it('parses hostname', () => {\n      assertParse('://example.com', { hostname: 'example.com' })\n    })\n\n    it('parses port', () => {\n      assertParse('://example.com:8000', { hostname: 'example.com', port: '8000' })\n    })\n\n    it('parses pathname', () => {\n      assertParse('products/:id', { pathname: 'products/:id' })\n    })\n\n    it('parses search', () => {\n      assertParse('?q', { search: { q: null } })\n      assertParse('?q=', { search: { q: [] } })\n      assertParse('?q=1', { search: { q: ['1'] } })\n    })\n\n    it('parses protocol + hostname', () => {\n      assertParse('https://example.com', {\n        protocol: 'https',\n        hostname: 'example.com',\n      })\n    })\n\n    it('parses protocol + pathname', () => {\n      assertParse('http:///dir/file', {\n        protocol: 'http',\n        pathname: 'dir/file',\n      })\n    })\n\n    it('parses hostname + pathname', () => {\n      assertParse('://example.com/about', {\n        hostname: 'example.com',\n        pathname: 'about',\n      })\n    })\n\n    it('parses protocol + hostname + pathname', () => {\n      assertParse('https://example.com/about', {\n        protocol: 'https',\n        hostname: 'example.com',\n        pathname: 'about',\n      })\n    })\n\n    it('parses protocol + hostname + search', () => {\n      assertParse('https://example.com?q=1', {\n        protocol: 'https',\n        hostname: 'example.com',\n        search: { q: ['1'] },\n      })\n    })\n\n    it('parses protocol + pathname + search', () => {\n      assertParse('http:///dir/file?q=1', {\n        protocol: 'http',\n        pathname: 'dir/file',\n        search: { q: ['1'] },\n      })\n    })\n\n    it('parses hostname + pathname + search', () => {\n      assertParse('://example.com/about?q=1', {\n        hostname: 'example.com',\n        pathname: 'about',\n        search: { q: ['1'] },\n      })\n    })\n\n    it('parses protocol + hostname + pathname + search', () => {\n      assertParse('https://example.com/about?q=1', {\n        protocol: 'https',\n        hostname: 'example.com',\n        pathname: 'about',\n        search: { q: ['1'] },\n      })\n    })\n\n    it('parses search params into constraints grouped by param name', () => {\n      assertParse('?q&q', { search: { q: null } })\n      assertParse('?q&q=', { search: { q: [] } })\n      assertParse('?q&q=1', { search: { q: ['1'] } })\n      assertParse('?q=&q=1', { search: { q: ['1'] } })\n      assertParse('?q=1&q=2', { search: { q: ['1', '2'] } })\n      assertParse('?q&q=&q=1&q=2', { search: { q: ['1', '2'] } })\n    })\n\n    it('throws on invalid protocol', () => {\n      assert.throws(() => new RoutePattern('ftp://example.com'), {\n        name: 'ParseError',\n        type: 'invalid protocol',\n      })\n      assert.throws(() => new RoutePattern('ws://example.com/path'), {\n        name: 'ParseError',\n        type: 'invalid protocol',\n      })\n      assert.throws(() => new RoutePattern('httpx://example.com'), {\n        name: 'ParseError',\n        type: 'invalid protocol',\n      })\n      assert.throws(() => new RoutePattern('http(s)x://example.com'), {\n        name: 'ParseError',\n        type: 'invalid protocol',\n      })\n    })\n  })\n\n  describe('part accessors', () => {\n    it('returns protocol', () => {\n      assert.equal(new RoutePattern('http://example.com').protocol, 'http')\n      assert.equal(new RoutePattern('https://example.com').protocol, 'https')\n      assert.equal(new RoutePattern('http(s)://example.com').protocol, 'http(s)')\n      assert.equal(new RoutePattern('/pathname').protocol, '')\n      assert.equal(new RoutePattern('://example.com').protocol, '')\n    })\n\n    it('returns hostname', () => {\n      assert.equal(new RoutePattern('://example.com').hostname, 'example.com')\n      assert.equal(new RoutePattern('://:host').hostname, ':host')\n      assert.equal(new RoutePattern('://api.example.com').hostname, 'api.example.com')\n      assert.equal(new RoutePattern('/pathname').hostname, '')\n      assert.equal(new RoutePattern('http://').hostname, '')\n    })\n\n    it('returns port', () => {\n      assert.equal(new RoutePattern('://example.com:8000').port, '8000')\n      assert.equal(new RoutePattern('://example.com:3000').port, '3000')\n      assert.equal(new RoutePattern('://example.com').port, '')\n      assert.equal(new RoutePattern('/pathname').port, '')\n    })\n\n    it('returns pathname', () => {\n      assert.equal(new RoutePattern('/posts/:id').pathname, 'posts/:id')\n      assert.equal(new RoutePattern('posts/:id').pathname, 'posts/:id')\n      assert.equal(new RoutePattern('/posts(/:id)').pathname, 'posts(/:id)')\n      assert.equal(new RoutePattern('://example.com').pathname, '')\n      assert.equal(new RoutePattern('/').pathname, '')\n      assert.equal(new RoutePattern('').pathname, '')\n    })\n\n    it('returns search', () => {\n      assert.equal(new RoutePattern('?q').search, 'q')\n      assert.equal(new RoutePattern('?q=').search, 'q=')\n      assert.equal(new RoutePattern('?q=1').search, 'q=1')\n      assert.equal(new RoutePattern('?q=1&q=2').search, 'q=1&q=2')\n      assert.equal(new RoutePattern('/posts?filter').search, 'filter')\n      assert.equal(new RoutePattern('/posts?sort=asc').search, 'sort=asc')\n      assert.equal(new RoutePattern('/posts').search, '')\n      assert.equal(new RoutePattern('').search, '')\n    })\n\n    it('returns all parts together', () => {\n      let pattern = new RoutePattern('https://api.example.com:8000/v1/:resource?filter=active')\n      assert.equal(pattern.protocol, 'https')\n      assert.equal(pattern.hostname, 'api.example.com')\n      assert.equal(pattern.port, '8000')\n      assert.equal(pattern.pathname, 'v1/:resource')\n      assert.equal(pattern.search, 'filter=active')\n    })\n  })\n\n  describe('source', () => {\n    function assertSource(source: string, expected?: string) {\n      assert.equal(new RoutePattern(source).source, expected ?? source)\n    }\n\n    it('reconstructs pathname only', () => {\n      assertSource('/posts/:id')\n      assertSource('posts/:id', '/posts/:id')\n      assertSource('/posts(/:id)')\n      assertSource('/', '/')\n      assertSource('', '/')\n    })\n\n    it('reconstructs hostname only', () => {\n      assertSource('://example.com', '://example.com/')\n      assertSource('://:host', '://:host/')\n    })\n\n    it('reconstructs port', () => {\n      assertSource('://example.com:8000', '://example.com:8000/')\n      assertSource('://example.com:3000', '://example.com:3000/')\n      assertSource('://:host:8080', '://:host:8080/')\n    })\n\n    it('reconstructs protocol', () => {\n      assertSource('http://', 'http:///')\n      assertSource('https://', 'https:///')\n      assertSource('http(s)://', 'http(s):///')\n    })\n\n    it('reconstructs protocol + hostname', () => {\n      assertSource('https://example.com', 'https://example.com/')\n      assertSource('http://example.com', 'http://example.com/')\n      assertSource('http(s)://*host', 'http(s)://*host/')\n    })\n\n    it('reconstructs protocol + hostname + pathname', () => {\n      assertSource('https://example.com/about')\n      assertSource('http://example.com/products/:id')\n      assertSource('http(s)://*host/path')\n    })\n\n    it('reconstructs protocol + hostname + port + pathname', () => {\n      assertSource('https://example.com:8000/about')\n      assertSource('http://localhost:3000/posts/:id')\n      assertSource('http(s)://example.com:8000/path')\n    })\n\n    it('reconstructs search params', () => {\n      assertSource('?q', '/?q')\n      assertSource('?q=', '/?q=')\n      assertSource('?q=1', '/?q=1')\n      assertSource('?q=1&q=2', '/?q=1&q=2')\n      assertSource('/posts?filter')\n      assertSource('/posts?sort=asc')\n      assertSource('/posts?tag=foo&tag=bar')\n      assertSource('https://example.com/posts?q=1')\n    })\n\n    it('reconstructs complex patterns with optionals', () => {\n      assertSource('/posts(/:id)')\n      assertSource('://(staging.)example.com', '://(staging.)example.com/')\n      assertSource(\n        '://(staging.)example.com/api(/:version)',\n        '://(staging.)example.com/api(/:version)',\n      )\n      assertSource(\n        '://(staging.)example.com/api(/:version)/resources/:id(.json)',\n        '://(staging.)example.com/api(/:version)/resources/:id(.json)',\n      )\n      assertSource('http(s)://*host/path')\n    })\n\n    it('reconstructs full patterns', () => {\n      assertSource('https://api.example.com:8000/v1/:resource')\n      assertSource('http(s)://example.com/base')\n      assertSource('http://old.com:3000/keep/this')\n      assertSource('users/:id?tab=profile', '/users/:id?tab=profile')\n      assertSource('://example.com/path?q=1&q=2&filter')\n    })\n  })\n\n  describe('join', () => {\n    function assertJoin(a: string, b: string, expected: string) {\n      assert.deepEqual(new RoutePattern(a).join(new RoutePattern(b)), new RoutePattern(expected))\n    }\n\n    it('joins protocol', () => {\n      assertJoin('http://', '://', 'http://')\n      assertJoin('://', '://', '://')\n      assertJoin('://', 'http://', 'http://')\n\n      assertJoin('http://', '://example.com', 'http://example.com')\n      assertJoin('://example.com', 'http://', 'http://example.com')\n\n      assertJoin('http://', 'https://', 'https://')\n      assertJoin('://example.com', 'https://', 'https://example.com')\n      assertJoin('http://example.com', 'https://', 'https://example.com')\n\n      assertJoin('http(s)://', 'https://', 'https://')\n      assertJoin('https://', 'http(s)://', 'http(s)://')\n    })\n\n    it('joins hostname', () => {\n      assertJoin('://example.com', '://*', '://example.com')\n      assertJoin('://*', '://*', '://*')\n      assertJoin('://*', '://example.com', '://example.com')\n\n      assertJoin('://example.com', '://*host', '://*host')\n      assertJoin('://*host', '://example.com', '://example.com')\n      assertJoin('://*host', '://*other', '://*other')\n      assertJoin('://*', '://*host', '://*host')\n\n      assertJoin('://example.com', '://other.com', '://other.com')\n      assertJoin('://', '://other.com', '://other.com')\n      assertJoin('http://example.com', '://other.com', 'http://other.com')\n      assertJoin('://example.com/pathname', '://other.com', '://other.com/pathname')\n      assertJoin('/pathname', '://other.com', '://other.com/pathname')\n    })\n\n    it('joins port', () => {\n      assertJoin('://:8000', '://', '://:8000')\n      assertJoin('://', '://:8000', '://:8000')\n      assertJoin('://:8000', '://:3000', '://:3000')\n      assertJoin('://example.com', '://example.com:8000', '://example.com:8000')\n      assertJoin('http://example.com:4321', '://example.com:8000', 'http://example.com:8000')\n    })\n\n    it('joins pathname', () => {\n      assertJoin('', '', '')\n      assertJoin('', 'b', 'b')\n      assertJoin('a', '', 'a')\n\n      assertJoin('a', 'b', 'a/b')\n      assertJoin('a/', 'b', 'a/b')\n      assertJoin('a', '/b', 'a/b')\n      assertJoin('a/', '/b', 'a/b')\n\n      assertJoin('(a)', '(b)', '(a)/(b)')\n      assertJoin('(a/)', '(b)', '(a)/(b)')\n      assertJoin('(a)', '(/b)', '(a)(/b)')\n      assertJoin('(a/)', '(/b)', '(a)(/b)')\n    })\n\n    it('joins search', () => {\n      assertJoin('path', '?a', 'path?a')\n      assertJoin('?a', '?b=1', '?a&b=1')\n      assertJoin('?a=1', '?b=2', '?a=1&b=2')\n    })\n\n    it('joins complex combinations', () => {\n      assertJoin('http://example.com/a', 'http(s)://*host/b', 'http(s)://*host/a/b')\n      assertJoin('http://example.com:8000/a', 'https:///b', 'https://example.com:8000/a/b')\n      assertJoin('http://example.com:8000/a', '://other.com/b', 'http://other.com:8000/a/b')\n\n      assertJoin(\n        'https://api.example.com:8000/v1/:resource',\n        '/users/(admin/)posts?filter&sort=asc',\n        'https://api.example.com:8000/v1/:resource/users/(admin/)posts?filter&sort=asc',\n      )\n\n      assertJoin(\n        'http(s)://example.com/base',\n        'http(s)://other.com/path',\n        'http(s)://other.com/base/path',\n      )\n\n      assertJoin(\n        'http://old.com:3000/keep/this',\n        'https://new.com:8080',\n        'https://new.com:8080/keep/this',\n      )\n\n      assertJoin(\n        'users/:id?tab=profile',\n        'posts/:postId?sort=recent',\n        'users/:id/posts/:postId?tab=profile&sort=recent',\n      )\n\n      assertJoin(\n        '://(staging.)example.com/api(/:version)',\n        '://*/resources/:id(.json)',\n        '://(staging.)example.com/api(/:version)/resources/:id(.json)',\n      )\n    })\n  })\n\n  describe('href', () => {\n    function hrefError(type: HrefError['details']['type']) {\n      return (error: unknown) => error instanceof HrefError && error.details.type === type\n    }\n\n    describe('protocol', () => {\n      it('defaults omitted protocol (://) to https', () => {\n        let pattern = new RoutePattern('://example.com/path')\n        let result = pattern.href()\n        assert.equal(result, 'https://example.com/path')\n      })\n\n      it('defaults http(s) to https', () => {\n        let pattern = new RoutePattern('http(s)://example.com/path')\n        let result = pattern.href()\n        assert.equal(result, 'https://example.com/path')\n      })\n\n      it('supports explicit http', () => {\n        let pattern = new RoutePattern('http://example.com/path')\n        let result = pattern.href()\n        assert.equal(result, 'http://example.com/path')\n      })\n\n      it('supports explicit https', () => {\n        let pattern = new RoutePattern('https://example.com/path')\n        let result = pattern.href()\n        assert.equal(result, 'https://example.com/path')\n      })\n    })\n\n    describe('hostname', () => {\n      describe('when origin is present', () => {\n        it('throws when protocol specified', () => {\n          let pattern = new RoutePattern('https://*/path')\n          // @ts-expect-error - missing hostname\n          assert.throws(() => pattern.href(), hrefError('missing-hostname'))\n        })\n\n        it('throws when protocol with named wildcard missing param', () => {\n          let pattern = new RoutePattern('http://*host/path')\n          // @ts-expect-error - missing required param\n          assert.throws(() => pattern.href(), hrefError('missing-params'))\n        })\n\n        it('throws when port specified', () => {\n          let pattern = new RoutePattern('://:8080/path')\n          assert.throws(() => pattern.href(), hrefError('missing-hostname'))\n        })\n      })\n\n      it('supports static hostname', () => {\n        let pattern = new RoutePattern('://example.com/path')\n        assert.equal(pattern.href(), 'https://example.com/path')\n        assert.equal(pattern.href({}), 'https://example.com/path')\n        assert.equal(pattern.href(null), 'https://example.com/path')\n        assert.equal(pattern.href(undefined), 'https://example.com/path')\n      })\n\n      describe('with dynamic segment', () => {\n        it('works when provided', () => {\n          let pattern = new RoutePattern('://:host.com/path')\n          let result = pattern.href({ host: 'example' })\n          assert.equal(result, 'https://example.com/path')\n        })\n\n        it('throws when missing', () => {\n          let pattern = new RoutePattern('://:host/path')\n          // @ts-expect-error - missing required param\n          assert.throws(() => pattern.href(), hrefError('missing-params'))\n        })\n      })\n\n      it('supports multiple dynamic segments', () => {\n        let pattern = new RoutePattern('://:subdomain.:domain.com/path')\n        let result = pattern.href({ subdomain: 'api', domain: 'example' })\n        assert.equal(result, 'https://api.example.com/path')\n      })\n\n      it('supports named wildcard', () => {\n        let pattern = new RoutePattern('://*env.example.com/path')\n        let result = pattern.href({ env: 'staging' })\n        assert.equal(result, 'https://staging.example.com/path')\n      })\n\n      it('throws for nameless wildcard', () => {\n        let pattern = new RoutePattern('://*.example.com/path')\n        // @ts-expect-error - nameless wildcard\n        assert.throws(() => pattern.href(), hrefError('nameless-wildcard'))\n      })\n\n      it('includes optional with static content', () => {\n        let pattern = new RoutePattern('://(www.)example.com/path')\n        let result = pattern.href()\n        assert.equal(result, 'https://www.example.com/path')\n      })\n    })\n\n    describe('port', () => {\n      it('supports static port', () => {\n        let pattern = new RoutePattern('://example.com:8080/path')\n        let result = pattern.href()\n        assert.equal(result, 'https://example.com:8080/path')\n      })\n\n      it('works with hostname params', () => {\n        let pattern = new RoutePattern('://:host:8080/path')\n        let result = pattern.href({ host: 'localhost' })\n        assert.equal(result, 'https://localhost:8080/path')\n      })\n    })\n\n    describe('pathname', () => {\n      it('supports static pathname', () => {\n        let pattern = new RoutePattern('/posts')\n        assert.equal(pattern.href(), '/posts')\n        assert.equal(pattern.href({}), '/posts')\n        assert.equal(pattern.href(null), '/posts')\n        assert.equal(pattern.href(undefined), '/posts')\n      })\n\n      it('normalizes static pathname without leading slash', () => {\n        let pattern = new RoutePattern('posts')\n        let result = pattern.href()\n        assert.equal(result, '/posts')\n      })\n\n      describe('with dynamic segment', () => {\n        it('works when provided', () => {\n          let pattern = new RoutePattern('/posts/:id')\n          let result = pattern.href({ id: '123' })\n          assert.equal(result, '/posts/123')\n        })\n\n        it('works with number params', () => {\n          let pattern = new RoutePattern('/posts/:id')\n          let result = pattern.href({ id: 123 })\n          assert.equal(result, '/posts/123')\n        })\n\n        it('ignores extra params', () => {\n          let pattern = new RoutePattern('/posts/:id')\n          let result = pattern.href({ id: '123', page: '2', sort: 'desc' })\n          assert.equal(result, '/posts/123')\n        })\n\n        it('throws when missing', () => {\n          let pattern = new RoutePattern('/posts/:id')\n          // @ts-expect-error - missing required param\n          assert.throws(() => pattern.href(), hrefError('missing-params'))\n        })\n\n        it('throws when params is null (required params)', () => {\n          let pattern = new RoutePattern('/posts/:id')\n          // @ts-expect-error - null not allowed when required params\n          assert.throws(() => pattern.href(null), hrefError('missing-params'))\n        })\n\n        it('throws when params is undefined (required params)', () => {\n          let pattern = new RoutePattern('/posts/:id')\n          // @ts-expect-error - undefined not allowed when required params\n          assert.throws(() => pattern.href(undefined), hrefError('missing-params'))\n        })\n      })\n\n      it('supports multiple dynamic segments', () => {\n        let pattern = new RoutePattern('/users/:userId/posts/:postId')\n        let result = pattern.href({ userId: '42', postId: '123' })\n        assert.equal(result, '/users/42/posts/123')\n      })\n\n      it('supports named wildcard', () => {\n        assert.equal(\n          new RoutePattern('/files/*path').href({ path: 'docs/readme.md' }),\n          '/files/docs/readme.md',\n        )\n        assert.equal(\n          new RoutePattern('images/*path.png').href({ path: 'images/hero' }),\n          '/images/images/hero.png',\n        )\n      })\n\n      it('supports wildcard with number param', () => {\n        let pattern = new RoutePattern('/files/*path')\n        let result = pattern.href({ path: 123 })\n        assert.equal(result, '/files/123')\n      })\n\n      it('throws for unnamed wildcard', () => {\n        let pattern = new RoutePattern('/files/*')\n        // @ts-expect-error - nameless wildcard\n        assert.throws(() => pattern.href(), hrefError('nameless-wildcard'))\n      })\n\n      it('supports repeated params', () => {\n        let pattern = new RoutePattern('/:lang/users/:userId/:lang/posts/:postId')\n        let result = pattern.href({ lang: 'en', userId: '42', postId: '123' })\n        assert.equal(result, '/en/users/42/en/posts/123')\n      })\n    })\n\n    describe('pattern with optionals', () => {\n      it('includes optional with static content', () => {\n        assert.equal(new RoutePattern('/posts(/edit)').href(), '/posts/edit')\n        assert.equal(new RoutePattern('products(.md)').href(), '/products.md')\n      })\n\n      it('includes optional with variable when provided', () => {\n        let pattern = new RoutePattern('/posts(/:id)')\n        let result = pattern.href({ id: '123' })\n        assert.equal(result, '/posts/123')\n      })\n\n      it('omits optional with variable when omitted', () => {\n        let pattern = new RoutePattern('/posts(/:id)')\n        assert.equal(pattern.href(), '/posts')\n        assert.equal(pattern.href({}), '/posts')\n        assert.equal(pattern.href(null), '/posts')\n        assert.equal(pattern.href(undefined), '/posts')\n      })\n\n      it('includes optional with wildcard when provided', () => {\n        let pattern = new RoutePattern('/files(/*path)')\n        let result = pattern.href({ path: 'docs/readme.md' })\n        assert.equal(result, '/files/docs/readme.md')\n      })\n\n      it('omits optional with wildcard when omitted', () => {\n        let pattern = new RoutePattern('/files(/*path)')\n        assert.equal(pattern.href(), '/files')\n        assert.equal(pattern.href({}), '/files')\n        assert.equal(pattern.href(null), '/files')\n        assert.equal(pattern.href(undefined), '/files')\n      })\n\n      it('omits optional with nameless wildcard', () => {\n        let pattern = new RoutePattern('/files(/*)')\n        assert.equal(pattern.href(), '/files')\n        assert.equal(pattern.href({}), '/files')\n        assert.equal(pattern.href(null), '/files')\n        assert.equal(pattern.href(undefined), '/files')\n      })\n\n      describe('with nested optionals', () => {\n        it('includes all when all provided', () => {\n          let pattern = new RoutePattern('/blog/:year(/:month(/:day))')\n          let result = pattern.href({ year: '2024', month: '01', day: '15' })\n          assert.equal(result, '/blog/2024/01/15')\n        })\n\n        it('includes only outer when inner omitted', () => {\n          let pattern = new RoutePattern('/blog/:year(/:month(/:day))')\n          let result = pattern.href({ year: '2024', month: '01' })\n          assert.equal(result, '/blog/2024/01')\n        })\n\n        it('omits both when only inner provided', () => {\n          let pattern = new RoutePattern('/blog/:year(/:month(/:day))')\n          let result = pattern.href({ year: '2024', day: '15' })\n          assert.equal(result, '/blog/2024')\n        })\n\n        it('omits both when neither provided', () => {\n          let pattern = new RoutePattern('/blog/:year(/:month(/:day))')\n          let result = pattern.href({ year: '2024' })\n          assert.equal(result, '/blog/2024')\n        })\n      })\n\n      describe('with multiple optionals', () => {\n        it('includes both when both provided', () => {\n          let pattern = new RoutePattern('/posts(/:id)(/:action)')\n          let result = pattern.href({ id: '123', action: 'edit' })\n          assert.equal(result, '/posts/123/edit')\n        })\n\n        it('includes only first when second omitted', () => {\n          let pattern = new RoutePattern('/posts(/:id)(/:action)')\n          let result = pattern.href({ id: '123' })\n          assert.equal(result, '/posts/123')\n        })\n\n        it('includes only second when first omitted', () => {\n          let pattern = new RoutePattern('/posts(/:id)(/:action)')\n          let result = pattern.href({ action: 'edit' })\n          assert.equal(result, '/posts/edit')\n        })\n\n        it('omits both when neither provided', () => {\n          let pattern = new RoutePattern('/posts(/:id)(/:action)')\n          assert.equal(pattern.href(), '/posts')\n        })\n      })\n\n      it('normalizes to slash when entire pattern is omitted optional', () => {\n        let pattern = new RoutePattern('(/:locale)(/:page)')\n        assert.equal(pattern.href(), '/')\n      })\n    })\n\n    describe('search params', () => {\n      it('works with no constraints', () => {\n        let pattern = new RoutePattern('/posts')\n        let result = pattern.href(undefined, { category: ['books', 'electronics'] })\n        assert.equal(result, '/posts?category=books&category=electronics')\n      })\n\n      describe('with bare constraint (?q)', () => {\n        it('includes empty value without user param', () => {\n          let pattern = new RoutePattern('/posts?filter')\n          let result = pattern.href()\n          assert.equal(result, '/posts?filter=')\n        })\n\n        it('uses user param value', () => {\n          let pattern = new RoutePattern('/posts?filter')\n          let result = pattern.href(undefined, { filter: 'active' })\n          assert.equal(result, '/posts?filter=active')\n        })\n      })\n\n      describe('with empty-value constraint (?q=)', () => {\n        it('throws without user param', () => {\n          let pattern = new RoutePattern('/posts?filter=')\n          assert.throws(() => pattern.href(), hrefError('missing-search-params'))\n        })\n\n        it('uses user param value', () => {\n          let pattern = new RoutePattern('/posts?filter=')\n          let result = pattern.href(undefined, { filter: 'active' })\n          assert.equal(result, '/posts?filter=active')\n        })\n      })\n\n      describe('with specific-value constraint (?q=foo)', () => {\n        it('uses pattern value only', () => {\n          let pattern = new RoutePattern('/posts?sort=asc')\n          let result = pattern.href()\n          assert.equal(result, '/posts?sort=asc')\n        })\n\n        it('preserves pattern search when only path params passed', () => {\n          assert.equal(\n            new RoutePattern('products?sort=asc&limit').href(),\n            '/products?sort=asc&limit=',\n          )\n          assert.equal(\n            new RoutePattern('products/:id?sort=asc&limit').href({ id: '1' }),\n            '/products/1?sort=asc&limit=',\n          )\n        })\n\n        it('prepends user params', () => {\n          let pattern = new RoutePattern('/posts?sort=asc')\n          let result = pattern.href(undefined, { sort: 'desc' })\n          assert.equal(result, '/posts?sort=desc&sort=asc')\n        })\n\n        it('deduplicates when user matches pattern', () => {\n          let pattern = new RoutePattern('/posts?tag=featured')\n          let result = pattern.href(undefined, { tag: 'featured' })\n          assert.equal(result, '/posts?tag=featured')\n        })\n\n        it('deduplicates when user matches one of multiple pattern values', () => {\n          let pattern = new RoutePattern('/posts?tag=featured&tag=popular')\n          let result = pattern.href(undefined, { tag: 'featured' })\n          assert.equal(result, '/posts?tag=featured&tag=popular')\n        })\n\n        it('handles array values', () => {\n          let pattern = new RoutePattern('/posts?tag=featured&tag=popular')\n          let result = pattern.href(undefined, { tag: ['tutorial', 'beginner'] })\n          assert.equal(result, '/posts?tag=tutorial&tag=beginner&tag=featured&tag=popular')\n        })\n      })\n\n      it('supports additional user params', () => {\n        let pattern = new RoutePattern('/posts?sort=asc')\n        let result = pattern.href(undefined, { page: '2' })\n        assert.equal(result, '/posts?page=2&sort=asc')\n      })\n    })\n\n    describe('format', () => {\n      it('returns relative URL for pathname only', () => {\n        let pattern = new RoutePattern('/posts/:id')\n        let result = pattern.href({ id: '123' })\n        assert.equal(result, '/posts/123')\n      })\n\n      it('returns absolute URL with protocol', () => {\n        let pattern = new RoutePattern('https://example.com/path')\n        let result = pattern.href()\n        assert.equal(result, 'https://example.com/path')\n      })\n\n      it('returns absolute URL with hostname', () => {\n        let pattern = new RoutePattern('://example.com/path')\n        let result = pattern.href()\n        assert.equal(result, 'https://example.com/path')\n      })\n\n      it('returns absolute URL with port', () => {\n        let pattern = new RoutePattern('://example.com:8080/path')\n        let result = pattern.href()\n        assert.equal(result, 'https://example.com:8080/path')\n      })\n    })\n  })\n\n  describe('match', () => {\n    function assertMatch(\n      pattern: string,\n      url: string,\n      expected: {\n        params?: Record<string, string | undefined>\n      } | null,\n    ) {\n      let match = new RoutePattern(pattern).match(url)\n\n      if (expected === null) {\n        assert.equal(match, null, `Expected pattern \"${pattern}\" to not match URL \"${url}\"`)\n        return\n      }\n\n      assert.notEqual(match, null, `Expected pattern \"${pattern}\" to match URL \"${url}\"`)\n      assert.deepEqual(match?.params, expected.params ?? {})\n    }\n\n    describe('protocol', () => {\n      it('matches http', () => {\n        assertMatch('http://example.com/path', 'http://example.com/path', {})\n      })\n\n      it('matches https', () => {\n        assertMatch('https://example.com/path', 'https://example.com/path', {})\n      })\n\n      it('matches http(s) optional', () => {\n        assertMatch('http(s)://example.com/path', 'http://example.com/path', {})\n        assertMatch('http(s)://example.com/path', 'https://example.com/path', {})\n      })\n    })\n\n    describe('hostname', () => {\n      it('matches one variable', () => {\n        assertMatch('://:host.com/path', 'https://example.com/path', {\n          params: { host: 'example' },\n        })\n      })\n\n      it('matches multiple variables', () => {\n        assertMatch('://:subdomain.:domain.com/path', 'https://api.example.com/path', {\n          params: { subdomain: 'api', domain: 'example' },\n        })\n      })\n\n      it('matches multiple variables with repeated names', () => {\n        assertMatch('://:part.:part.com/path', 'https://api.example.com/path', {\n          params: { part: 'example' },\n        })\n      })\n\n      it('excludes nameless wildcard from params', () => {\n        assertMatch('://*.example.com/path', 'https://api.example.com/path', {})\n      })\n    })\n\n    describe('port', () => {\n      it('matches when port is equal', () => {\n        assertMatch('://example.com:8080/path', 'https://example.com:8080/path', {})\n      })\n\n      it('does not match when port differs', () => {\n        assertMatch('://example.com:8080/path', 'https://example.com:3000/path', null)\n      })\n    })\n\n    describe('pathname', () => {\n      it('matches one variable', () => {\n        assertMatch('/posts/:id', 'https://example.com/posts/123', { params: { id: '123' } })\n      })\n\n      it('matches multiple variables', () => {\n        assertMatch('/users/:userId/posts/:postId', 'https://example.com/users/42/posts/123', {\n          params: { userId: '42', postId: '123' },\n        })\n      })\n\n      it('matches multiple variables with repeated names', () => {\n        assertMatch('/:id/nested/:id', 'https://example.com/first/nested/second', {\n          params: { id: 'second' },\n        })\n      })\n\n      it('excludes nameless wildcard from params', () => {\n        assertMatch('/posts/*/comments', 'https://example.com/posts/123/comments', {})\n      })\n    })\n\n    describe('search', () => {\n      it('matches bare parameter for presence only', () => {\n        assertMatch('?q', 'https://example.com?q', {})\n      })\n\n      it('matches bare parameter when URL has value', () => {\n        assertMatch('?q', 'https://example.com?q=search', {})\n      })\n\n      it('requires non-empty value when pattern has empty value', () => {\n        assertMatch('?q=', 'https://example.com?q=search', {})\n        assertMatch('?q=', 'https://example.com?q=', null)\n        assertMatch('?q=', 'https://example.com?q', null)\n      })\n\n      it('matches parameter with specific value', () => {\n        assertMatch('?sort=asc', 'https://example.com?sort=asc', {})\n      })\n\n      it('matches parameter with multiple values', () => {\n        assertMatch('?tag=foo&tag=bar', 'https://example.com?tag=foo&tag=bar', {})\n      })\n\n      it('matches multiple parameters', () => {\n        assertMatch('?filter&sort=asc', 'https://example.com?filter=active&sort=asc', {})\n      })\n\n      it('allows extra parameters with bare constraint', () => {\n        assertMatch('?q', 'https://example.com?q=search&page=2&limit=10', {})\n      })\n\n      it('allows extra parameters with empty value constraint', () => {\n        assertMatch('?filter=', 'https://example.com?filter=active&sort=asc&page=1', {})\n      })\n\n      it('allows extra parameters with specific value', () => {\n        assertMatch('?sort=asc', 'https://example.com?sort=asc&filter=active&page=2', {})\n      })\n\n      it('allows extra parameters with multiple constraints', () => {\n        assertMatch(\n          '?filter&sort=asc',\n          'https://example.com?filter=active&sort=asc&page=1&limit=20',\n          {},\n        )\n      })\n\n      it('allows extra values for constrained parameter', () => {\n        assertMatch('?tag=foo', 'https://example.com?tag=foo&tag=bar&tag=baz', {})\n      })\n\n      it('does not match when required parameter is missing', () => {\n        assertMatch('?filter', 'https://example.com?sort=asc', null)\n      })\n\n      it('does not match when required value is missing', () => {\n        assertMatch('?sort=asc', 'https://example.com?sort=desc', null)\n      })\n\n      it('matches any search params when no constraints specified', () => {\n        assertMatch('/posts', 'https://example.com/posts?q=search&page=2', {})\n        assertMatch('/posts', 'https://example.com/posts', {})\n      })\n    })\n  })\n\n  describe('ignoreCase', () => {\n    it('pathname is case-sensitive by default', () => {\n      let pattern = new RoutePattern('/Posts/:id')\n      assert.equal(pattern.match('https://example.com/posts/123'), null)\n      assert.equal(pattern.match('https://example.com/POSTS/123'), null)\n      assert.notEqual(pattern.match('https://example.com/Posts/123'), null)\n    })\n\n    it('pathname is case-insensitive when match(url, { ignoreCase: true })', () => {\n      let pattern = new RoutePattern('/Posts/:id')\n      assert.notEqual(pattern.match('https://example.com/posts/123', { ignoreCase: true }), null)\n      assert.notEqual(pattern.match('https://example.com/POSTS/123', { ignoreCase: true }), null)\n      assert.notEqual(pattern.match('https://example.com/PoStS/123', { ignoreCase: true }), null)\n    })\n\n    it('preserves original casing in params for case-insensitive pathname match', () => {\n      let pattern = new RoutePattern('/posts/:id')\n      let match = pattern.match('https://example.com/POSTS/ABC', { ignoreCase: true })\n      assert.notEqual(match, null)\n      assert.equal(match!.params.id, 'ABC')\n    })\n\n    it('search is always case-sensitive', () => {\n      let pattern = new RoutePattern('?Sort')\n      assert.notEqual(pattern.match('https://example.com?Sort'), null)\n      assert.equal(pattern.match('https://example.com?sort'), null)\n      assert.equal(pattern.match('https://example.com?sort', { ignoreCase: true }), null)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/src/lib/route-pattern.ts",
    "content": "import { split } from './route-pattern/split.ts'\nimport { PartPattern, type PartPatternMatch } from './route-pattern/part-pattern.ts'\nimport type { Join } from './types/index.ts'\nimport { parseHostname, parseProtocol, parseSearch } from './route-pattern/parse.ts'\nimport { serializeSearch } from './route-pattern/serialize.ts'\nimport { joinPathname, joinSearch } from './route-pattern/join.ts'\nimport { HrefError, hrefSearch, type HrefArgs } from './route-pattern/href.ts'\nimport { matchSearch } from './route-pattern/match.ts'\nimport type { Params } from './route-pattern/params.ts'\n\ntype AST = {\n  readonly protocol: 'http' | 'https' | 'http(s)' | null\n  readonly hostname: PartPattern | null\n  readonly port: string | null\n  readonly pathname: PartPattern\n  /**\n   * - `null`: key must be present\n   * - Empty `Set`: key must be present with a value\n   * - Non-empty `Set`: key must be present with all these values\n   *\n   * ```ts\n   * new Map([['q', null]])                // -> ?q, ?q=, ?q=1\n   * new Map([['q', new Set()]])           // -> ?q=1\n   * new Map([['q', new Set(['x', 'y'])]]) // -> ?q=x&q=y\n   * ```\n   */\n  readonly search: ReadonlyMap<string, ReadonlySet<string> | null>\n}\n\n/**\n * Result returned when a URL matches a route pattern.\n */\nexport type RoutePatternMatch<source extends string = string> = {\n  pattern: RoutePattern\n  url: URL\n  params: Params<source>\n\n  /**\n   * Rich information about matched params (variables and wildcards) in the hostname and pathname,\n   * analogous to RegExp groups/indices.\n   */\n  paramsMeta: {\n    hostname: PartPatternMatch\n    pathname: PartPatternMatch\n  }\n}\n\n/**\n * A class for matching and generating URLs based on a defined pattern.\n */\nexport class RoutePattern<source extends string = string> {\n  /**\n   * Parsed route-pattern AST used for matching and href generation.\n   */\n  readonly ast: AST\n\n  // The `join()` method bypasses the constructor and creates a new instance directly\n  // using `Object.create()`. This means that the constructor will only run for instances\n  // that are instantiated directly with a source string, not for all instances of `RoutePattern`.\n  // This also means that we cannot use JavaScript features like `#private` fields/methods and\n  // class field initializers that rely on the constructor being run.\n  constructor(source: source) {\n    let spans = split(source)\n\n    this.ast = {\n      protocol: parseProtocol(source, spans.protocol),\n      hostname: parseHostname(source, spans.hostname),\n      port: spans.port ? source.slice(...spans.port) : null,\n      pathname: spans.pathname\n        ? PartPattern.parse(source, { span: spans.pathname, type: 'pathname' })\n        : PartPattern.parse('', { span: [0, 0], type: 'pathname' }),\n      search: spans.search ? parseSearch(source.slice(...spans.search)) : new Map(),\n    }\n  }\n\n  // eslint-disable-next-line no-restricted-syntax\n  private get hasOrigin(): boolean {\n    return this.ast.protocol !== null || this.ast.hostname !== null || this.ast.port !== null\n  }\n\n  /**\n   * The protocol portion of the pattern without the trailing colon.\n   */\n  get protocol(): string {\n    return this.ast.protocol ?? ''\n  }\n\n  /**\n   * The hostname portion of the pattern.\n   */\n  get hostname(): string {\n    return this.ast.hostname?.source ?? ''\n  }\n\n  /**\n   * The explicit port portion of the pattern.\n   */\n  get port(): string {\n    return this.ast.port ?? ''\n  }\n\n  /**\n   * The pathname portion of the pattern without a leading slash.\n   */\n  get pathname(): string {\n    return this.ast.pathname.source\n  }\n\n  /**\n   * The serialized search constraints without a leading `?`.\n   */\n  get search(): string {\n    return serializeSearch(this.ast.search) ?? ''\n  }\n\n  /**\n   * The serialized route-pattern source string.\n   */\n  get source(): string {\n    let result = ''\n\n    if (this.hasOrigin) {\n      let protocol = this.protocol\n      let hostname = this.hostname\n      let port = this.port === '' ? '' : `:${this.port}`\n      result += `${protocol}://${hostname}${port}`\n    }\n\n    result += '/' + this.pathname\n\n    let search = this.search\n    if (search) result += `?${search}`\n\n    return result\n  }\n\n  /**\n   * Returns the serialized route-pattern source string.\n   *\n   * @returns The pattern source.\n   */\n  toString(): string {\n    return this.source\n  }\n\n  /**\n   * Joins this pattern with another pathname or route pattern.\n   *\n   * @param other Pattern or pathname to append.\n   * @returns A new route pattern representing the joined path.\n   */\n  join<other extends string>(\n    other: other | RoutePattern<other>,\n  ): RoutePattern<Join<source, other>> {\n    other = typeof other === 'string' ? new RoutePattern(other) : other\n\n    return Object.create(RoutePattern.prototype, {\n      ast: {\n        enumerable: true,\n        value: {\n          protocol: other.ast.protocol ?? this.ast.protocol,\n          hostname: other.ast.hostname ?? this.ast.hostname,\n          port: other.ast.port ?? this.ast.port,\n          pathname: joinPathname(this.ast.pathname, other.ast.pathname),\n          search: joinSearch(this.ast.search, other.ast.search),\n        },\n      },\n    })\n  }\n\n  /**\n   * Builds an href from this pattern and the supplied params.\n   *\n   * @param args Path params and optional search params.\n   * @returns The generated href string.\n   */\n  href(...args: HrefArgs<source>): string {\n    let [params, searchParams] = args\n    searchParams ??= {}\n\n    let result = ''\n\n    if (this.hasOrigin) {\n      // protocol: null defaults to 'https', 'http(s)' defaults to 'https'\n      let protocol =\n        this.ast.protocol === null || this.ast.protocol === 'http(s)' ? 'https' : this.ast.protocol\n\n      // hostname\n      if (this.ast.hostname === null) {\n        throw new HrefError({ type: 'missing-hostname', pattern: this })\n      }\n      let hostname = this.ast.hostname.href(this, params ?? {})\n\n      // port\n      let port = this.ast.port === null ? '' : `:${this.ast.port}`\n      result += `${protocol}://${hostname}${port}`\n    }\n\n    // pathname\n    let pathname = this.ast.pathname.href(this, params ?? {})\n    result += '/' + pathname\n\n    // search\n    let search = hrefSearch(this, searchParams)\n    if (search) result += `?${search}`\n\n    return result\n  }\n\n  /**\n   * Match a URL against this pattern.\n   *\n   * @param url The URL to match\n   * @param options Match options\n   * @param options.ignoreCase When `true`, pathname matching is case-insensitive. Defaults to `false`. Hostname is always case-insensitive; search remains case-sensitive.\n   * @returns The match result, or `null` if no match\n   */\n  match(url: string | URL, options?: { ignoreCase?: boolean }): RoutePatternMatch<source> | null {\n    url = typeof url === 'string' ? new URL(url) : url\n\n    let hostname: PartPatternMatch | null = null\n    if (this.hasOrigin) {\n      // protocol: null matches http or https, 'http(s)' matches http or https\n      if (this.ast.protocol === 'http(s)') {\n        if (url.protocol !== 'http:' && url.protocol !== 'https:') return null\n      } else if (this.ast.protocol !== null) {\n        let expectedProtocol = `${this.ast.protocol}:`\n        if (url.protocol !== expectedProtocol) return null\n      }\n\n      // hostname: null matches any hostname\n      if (this.ast.hostname !== null) {\n        hostname = this.ast.hostname.match(url.hostname, { ignoreCase: true })\n        if (hostname === null) return null\n      }\n\n      // port: null matches empty port\n      if (this.ast.port === null && url.port !== '') return null\n      if (this.ast.port !== null && url.port !== this.ast.port) return null\n    }\n\n    if (this.ast.hostname === null) {\n      // Pathname-only pattern - treat hostname as wildcard match\n      hostname = [{ type: '*', name: '*', begin: 0, end: url.hostname.length, value: url.hostname }]\n    }\n\n    // url.pathname: remove leading slash\n    let pathname = this.ast.pathname.match(url.pathname.slice(1), options)\n    if (pathname === null) return null\n\n    if (!matchSearch(url.searchParams, this.ast.search)) return null\n\n    let params: Record<string, string | undefined> = {}\n\n    // hostname params\n    this.ast.hostname?.params.forEach((param) => {\n      if (param.name === '*') return\n      params[param.name] = undefined\n    })\n    hostname?.forEach((param) => {\n      if (param.name === '*') return\n      params[param.name] = param.value\n    })\n\n    // pathname params\n    this.ast.pathname.params.forEach((param) => {\n      if (param.name === '*') return\n      params[param.name] = undefined\n    })\n    pathname.forEach((param) => {\n      if (param.name === '*') return\n      params[param.name] = param.value\n    })\n\n    return {\n      pattern: this,\n      url,\n      params: params as Params<source>,\n      paramsMeta: { hostname: hostname ?? [], pathname },\n    }\n  }\n\n  /**\n   * Tests whether a URL matches this route pattern.\n   *\n   * @param url URL to test.\n   * @returns `true` when the URL matches the pattern.\n   */\n  test(url: string | URL): boolean {\n    return this.match(url) !== null\n  }\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/specificity.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { RoutePattern } from './route-pattern.ts'\nimport * as Specificity from './specificity.ts'\n\ndescribe('specificity', () => {\n  describe('compare', () => {\n    function assertCompare(patterns: [string, string], url: URL | string, expected: -1 | 0 | 1) {\n      url = typeof url === 'string' ? new URL(url) : url\n      let a = new RoutePattern(patterns[0])\n      let b = new RoutePattern(patterns[1])\n      let matchA = a.match(url)\n      let matchB = b.match(url)\n\n      assert.notEqual(matchA, null, `Pattern A \"${patterns[0]}\" should match URL \"${url}\"`)\n      assert.notEqual(matchB, null, `Pattern B \"${patterns[1]}\" should match URL \"${url}\"`)\n\n      assert.equal(Specificity.compare(matchA!, matchB!), expected)\n    }\n\n    describe('hostname', () => {\n      it('ranks static higher than variable', () => {\n        assertCompare(['https://example.com', 'https://:subdomain.com'], 'https://example.com', 1)\n      })\n\n      it('ranks variable higher than wildcard', () => {\n        assertCompare(['https://:subdomain.com', 'https://*.com'], 'https://example.com', 1)\n      })\n\n      it('ties when variables have same range', () => {\n        assertCompare(['https://:subdomain.com', 'https://:other.com'], 'https://example.com', 0)\n      })\n\n      it('ties when wildcards have same range', () => {\n        assertCompare(['https://*.com', 'https://*.com'], 'https://example.com', 0)\n      })\n\n      it('ranks variable with prefix/suffix higher than bare variable', () => {\n        assertCompare(['https://e:sub.com', 'https://:subdomain.com'], 'https://example.com', 1)\n      })\n\n      it('ranks wildcard with prefix/suffix higher than bare wildcard', () => {\n        assertCompare(['https://e*.com', 'https://*.com'], 'https://example.com', 1)\n      })\n\n      it('breaks tie on variables and wildcards by subsequent characters', () => {\n        assertCompare(\n          ['https://a*.cd.:ef.*.com', 'https://*.cd.:ef.*.com'],\n          'https://ab.cd.ef.gh.com',\n          1,\n        )\n      })\n\n      it('ranks segments right-to-left but chars within segments left-to-right', () => {\n        assertCompare(['https://a.*b-c.d', 'https://a.b-*c.d'], 'https://a.b-xxx.yyy-c.d', 1)\n      })\n\n      it('ranks back-to-back variables with static content higher', () => {\n        assertCompare(['https://:a-:b.com', 'https://:sub.com'], 'https://ab-cd.com', 1)\n      })\n\n      it('ranks back-to-back wildcards with static content higher', () => {\n        assertCompare(['https://*-*.com', 'https://*.com'], 'https://ab-cd.com', 1)\n      })\n    })\n\n    describe('pathname', () => {\n      it('ranks static higher than variable', () => {\n        assertCompare(\n          ['https://example.com/posts/123', 'https://example.com/posts/:id'],\n          'https://example.com/posts/123',\n          1,\n        )\n      })\n\n      it('ranks variable higher than wildcard', () => {\n        assertCompare(\n          ['https://example.com/posts/:id', 'https://example.com/posts/*'],\n          'https://example.com/posts/123',\n          1,\n        )\n      })\n\n      it('ties when variables have same range', () => {\n        assertCompare(\n          ['https://example.com/posts/:id', 'https://example.com/posts/:other'],\n          'https://example.com/posts/123',\n          0,\n        )\n      })\n\n      it('ties when wildcards have same range', () => {\n        assertCompare(\n          ['https://example.com/posts/*', 'https://example.com/posts/*'],\n          'https://example.com/posts/123',\n          0,\n        )\n      })\n\n      it('ranks variable with prefix/suffix higher than bare variable', () => {\n        assertCompare(\n          ['https://example.com/posts-:id', 'https://example.com/:segment'],\n          'https://example.com/posts-123',\n          1,\n        )\n      })\n\n      it('ranks wildcard with prefix/suffix higher than bare wildcard', () => {\n        assertCompare(\n          ['https://example.com/p*', 'https://example.com/*/:id'],\n          'https://example.com/posts/123',\n          1,\n        )\n      })\n\n      it('breaks tie on variables and wildcards by subsequent characters', () => {\n        assertCompare(\n          ['https://example.com/*/123/:id/7*', 'https://example.com/*/123/:id/*'],\n          'https://example.com/posts/123/456/789',\n          1,\n        )\n      })\n\n      it('ranks back-to-back variables with static content higher', () => {\n        assertCompare(\n          ['https://example.com/:a/:b', 'https://example.com/*'],\n          'https://example.com/posts/123',\n          1,\n        )\n      })\n\n      it('ranks back-to-back wildcards with static content higher', () => {\n        assertCompare(\n          ['https://example.com/*/*', 'https://example.com/*'],\n          'https://example.com/posts/123',\n          1,\n        )\n      })\n    })\n\n    describe('search', () => {\n      it('ranks exact value higher than any value', () => {\n        assertCompare(\n          ['https://example.com/posts?q=hello', 'https://example.com/posts?q'],\n          'https://example.com/posts?q=hello',\n          1,\n        )\n      })\n\n      it('ranks any value higher than key only', () => {\n        assertCompare(\n          ['https://example.com/posts?q=', 'https://example.com/posts?q'],\n          'https://example.com/posts?q=hello',\n          1,\n        )\n      })\n\n      it('ranks more constraints higher', () => {\n        assertCompare(\n          ['https://example.com/posts?q=&a=', 'https://example.com/posts?q='],\n          'https://example.com/posts?q=hello&a=1',\n          1,\n        )\n      })\n\n      it('ties on different keys with same count', () => {\n        assertCompare(\n          ['https://example.com/posts?q=', 'https://example.com/posts?a='],\n          'https://example.com/posts?q=hello&a=1',\n          0,\n        )\n      })\n\n      it('ranks multiple exact values higher than single any value', () => {\n        assertCompare(\n          ['https://example.com/posts?q=hello&q=world', 'https://example.com/posts?q'],\n          'https://example.com/posts?q=hello&q=world',\n          1,\n        )\n      })\n\n      it('ranks exact values higher than any values', () => {\n        assertCompare(\n          ['https://example.com/posts?q=hello&a=1', 'https://example.com/posts?q&a'],\n          'https://example.com/posts?q=hello&a=1&b=2',\n          1,\n        )\n      })\n    })\n\n    it('throws when comparing matches for different URLs', () => {\n      let pattern = new RoutePattern('https://example.com/:path')\n      let match1 = pattern.match('https://example.com/foo')\n      let match2 = pattern.match('https://example.com/bar')\n\n      assert.notEqual(match1, null)\n      assert.notEqual(match2, null)\n\n      assert.throws(\n        () => Specificity.compare(match1!, match2!),\n        /Cannot compare matches for different URLs/,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/src/lib/specificity.ts",
    "content": "import type { RoutePattern, RoutePatternMatch } from './route-pattern.ts'\n\n/**\n * Returns true if match `a` is less specific than match `b`.\n *\n * @param a the first match to compare\n * @param b the second match to compare\n * @returns true if `a` is less specific than `b`\n */\nexport function lessThan(a: RoutePatternMatch, b: RoutePatternMatch): boolean {\n  return compare(a, b) === -1\n}\n\n/**\n * Returns true if match `a` is more specific than match `b`.\n *\n * @param a the first match to compare\n * @param b the second match to compare\n * @returns true if `a` is more specific than `b`\n */\nexport function greaterThan(a: RoutePatternMatch, b: RoutePatternMatch): boolean {\n  return compare(a, b) === 1\n}\n\n/**\n * Returns true if matches `a` and `b` have equal specificity.\n *\n * @param a the first match to compare\n * @param b the second match to compare\n * @returns true if `a` and `b` have equal specificity\n */\nexport function equal(a: RoutePatternMatch, b: RoutePatternMatch): boolean {\n  return compare(a, b) === 0\n}\n\n/**\n * Comparator function for sorting matches from least specific to most specific.\n *\n * @param a the first match to compare\n * @param b the second match to compare\n * @returns negative if `a` is less specific, positive if more specific, 0 if equal\n */\nexport let ascending = (a: RoutePatternMatch, b: RoutePatternMatch): number => compare(a, b)\n\n/**\n * Comparator function for sorting matches from most specific to least specific.\n *\n * @param a the first match to compare\n * @param b the second match to compare\n * @returns positive if `a` is less specific, negative if more specific, 0 if equal\n */\nexport let descending = (a: RoutePatternMatch, b: RoutePatternMatch): number => compare(a, b) * -1\n\n/**\n * Compare two matches by specificity.\n * Passing to `.sort()` will sort matches from least specific to most specific.\n *\n * @param a the first match to compare\n * @param b the second match to compare\n * @returns -1 if `a` is less specific, 1 if `a` is more specific, 0 if tied.\n */\nexport function compare(a: RoutePatternMatch, b: RoutePatternMatch): -1 | 0 | 1 {\n  if (a.url.href !== b.url.href) {\n    throw new Error(`Cannot compare matches for different URLs: ${a.url.href} vs ${b.url.href}`)\n  }\n\n  // Hostname comparison\n  let hostnameResult = compareHostname(a.url.hostname, a.paramsMeta.hostname, b.paramsMeta.hostname)\n  if (hostnameResult !== 0) return hostnameResult\n\n  // Pathname comparison\n  let pathnameResult = comparePathname(a.paramsMeta.pathname, b.paramsMeta.pathname)\n  if (pathnameResult !== 0) return pathnameResult\n\n  // Search comparison\n  let searchResult = compareSearch(a.pattern.ast.search, b.pattern.ast.search)\n  if (searchResult !== 0) return searchResult\n\n  return 0\n}\n\nfunction compareHostname(\n  hostname: string,\n  a: RoutePatternMatch['paramsMeta']['hostname'],\n  b: RoutePatternMatch['paramsMeta']['hostname'],\n): -1 | 0 | 1 {\n  if (a.length === 0 && b.length === 0) return 0\n  if (a.length === 0 && b.length > 0) return 1\n  if (a.length > 0 && b.length === 0) return -1\n\n  // Encoding of hostname chars: 0 = static, 1 = variable (:), 2 = wildcard (*)\n  // Note: `Int8Array` defaults to 0 for all indices not explicitly set.\n\n  let aEncoding = new Int8Array(hostname.length)\n  for (let range of a) {\n    aEncoding.fill(range.type === ':' ? 1 : 2, range.begin, range.end)\n  }\n\n  let bEncoding = new Int8Array(hostname.length)\n  for (let range of b) {\n    bEncoding.fill(range.type === ':' ? 1 : 2, range.begin, range.end)\n  }\n\n  // Build segments right-to-left: desc order by begin\n  let segments: Array<{ begin: number; end: number }> = []\n  let end = hostname.length\n  for (let i = hostname.length - 1; i >= 0; i--) {\n    if (hostname[i] === '.') {\n      segments.push({ begin: i + 1, end })\n      end = i\n    }\n  }\n  segments.push({ begin: 0, end }) // leftmost segment\n\n  for (let segment of segments) {\n    for (let j = segment.begin; j < segment.end; j++) {\n      if (aEncoding[j] < bEncoding[j]) return 1 // a is more specific\n      if (aEncoding[j] > bEncoding[j]) return -1 // b is more specific\n    }\n  }\n\n  return 0\n}\n\nfunction comparePathname(\n  a: RoutePatternMatch['paramsMeta']['pathname'],\n  b: RoutePatternMatch['paramsMeta']['pathname'],\n): -1 | 0 | 1 {\n  if (a.length === 0 && b.length === 0) return 0\n  if (a.length === 0 && b.length > 0) return 1\n  if (a.length > 0 && b.length === 0) return -1\n\n  let i = 0\n  let aIndex = 0\n  let bIndex = 0\n\n  while (aIndex < a.length || bIndex < b.length) {\n    let aRange = a[aIndex]\n    let bRange = b[bIndex]\n\n    if (aRange === undefined) return 1 // a is fully static from here\n    if (bRange === undefined) return -1 // b is fully static from here\n\n    // Skip to the minimum begin of the two current ranges\n    i = Math.min(aRange.begin, bRange.begin)\n\n    if (i < aRange.begin) return 1 // a has static content at i\n    if (i < bRange.begin) return -1 // b has static content at i\n\n    if (aRange.type === ':' && bRange.type === '*') return 1 // a is more specific\n    if (aRange.type === '*' && bRange.type === ':') return -1 // b is more specific\n\n    let minEnd = Math.min(aRange.end, bRange.end)\n    i = minEnd\n\n    // Advance range indices if we've reached their ends\n    if (i >= aRange.end) aIndex += 1\n    if (i >= bRange.end) bIndex += 1\n  }\n\n  return 0\n}\n\nfunction compareSearch(\n  a: RoutePattern['ast']['search'],\n  b: RoutePattern['ast']['search'],\n): -1 | 0 | 1 {\n  let aSpecificity = searchSpecificity(a)\n  let bSpecificity = searchSpecificity(b)\n\n  if (aSpecificity.keyAndExactValue > bSpecificity.keyAndExactValue) return 1\n  if (aSpecificity.keyAndExactValue < bSpecificity.keyAndExactValue) return -1\n\n  if (aSpecificity.keyAndAnyValue > bSpecificity.keyAndAnyValue) return 1\n  if (aSpecificity.keyAndAnyValue < bSpecificity.keyAndAnyValue) return -1\n\n  if (aSpecificity.key > bSpecificity.key) return 1\n  if (aSpecificity.key < bSpecificity.key) return -1\n\n  return 0\n}\n\nfunction searchSpecificity(constraints: RoutePattern['ast']['search']): {\n  keyAndExactValue: number\n  keyAndAnyValue: number\n  key: number\n} {\n  let exactValue = 0\n  let anyValue = 0\n  let key = 0\n\n  for (let constraint of constraints.values()) {\n    if (constraint === null) {\n      key += 1\n      continue\n    }\n    if (constraint.size === 0) {\n      anyValue += 1\n      continue\n    }\n    exactValue += constraint.size\n  }\n\n  return { keyAndExactValue: exactValue, keyAndAnyValue: anyValue, key }\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/trie-matcher/variant.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { PartPattern } from '../route-pattern/part-pattern.ts'\nimport { PartPatternVariant } from './variant.ts'\n\ndescribe('Variant', () => {\n  describe('generate', () => {\n    function assertVariants(source: string, expected: Array<string>) {\n      let pattern = PartPattern.parse(source, { type: 'pathname' })\n      let actual = PartPatternVariant.generate(pattern).map((variant) =>\n        variant.toString(pattern.separator),\n      )\n      assert.deepEqual(actual, expected)\n    }\n\n    it('produces all possible combinations of optionals', () => {\n      assertVariants('a.:b.c', ['a.{:b}.c'])\n      assertVariants('a(:b)*c', ['a{*c}', 'a{:b}{*c}'])\n      assertVariants('a(:b)c(*d)e', ['ace', 'ac{*d}e', 'a{:b}ce', 'a{:b}c{*d}e'])\n      assertVariants('a(:b(*c):d)e', ['ae', 'a{:b}{:d}e', 'a{:b}{*c}{:d}e'])\n      assertVariants('a(:b(*c):d)e(*f)g', [\n        'aeg',\n        'ae{*f}g',\n        'a{:b}{:d}eg',\n        'a{:b}{:d}e{*f}g',\n        'a{:b}{*c}{:d}eg',\n        'a{:b}{*c}{:d}e{*f}g',\n      ])\n    })\n  })\n})\n"
  },
  {
    "path": "packages/route-pattern/src/lib/trie-matcher/variant.ts",
    "content": "import { unreachable } from '../unreachable.ts'\nimport type { PartPattern, PartPatternToken } from '../route-pattern/part-pattern.ts'\nimport type { RoutePattern } from '../route-pattern.ts'\nimport * as RE from '../regexp.ts'\n\ntype Variant = {\n  protocol: 'http' | 'https'\n  hostname:\n    | { type: 'static'; value: string }\n    | { type: 'dynamic'; value: PartPattern }\n    | { type: 'any' }\n  port: string\n  pathname: PartPatternVariant\n}\n\ntype Segment =\n  | { type: 'static'; key: string }\n  | { type: 'variable'; key: string; regexp: RegExp }\n  | { type: 'wildcard'; key: string; regexp: RegExp }\n\nexport function generate(pattern: RoutePattern): Array<Variant> {\n  // prettier-ignore\n  let protocols =\n    pattern.ast.protocol === null ? ['http', 'https'] as const :\n    pattern.ast.protocol === 'http(s)' ? ['http', 'https'] as const :\n    [pattern.ast.protocol]\n\n  // prettier-ignore\n  let hostnames =\n    pattern.ast.hostname === null ? [{ type: 'any' as const }] :\n    pattern.ast.hostname.params.length === 0 ?\n      PartPatternVariant.generate(pattern.ast.hostname).map((variant) => ({ type: 'static' as const, value: variant.toString('.') })) :\n      [{ type: 'dynamic' as const, value: pattern.ast.hostname }]\n\n  let pathnames = PartPatternVariant.generate(pattern.ast.pathname)\n\n  let result: Array<Variant> = []\n  for (let protocol of protocols) {\n    for (let hostname of hostnames) {\n      for (let pathname of pathnames) {\n        result.push({ protocol, hostname, port: pattern.ast.port ?? '', pathname })\n      }\n    }\n  }\n\n  return result\n}\n\ntype Token = Extract<PartPatternToken, { type: 'text' | ':' | '*' | 'separator' }>\ntype Param = Extract<PartPatternToken, { type: ':' | '*' }>\n\nexport class PartPatternVariant {\n  tokens: Array<Token>\n\n  constructor(tokens: Array<Token>) {\n    this.tokens = tokens\n  }\n\n  static generate(pattern: PartPattern): Array<PartPatternVariant> {\n    let result: Array<PartPatternVariant> = []\n\n    let stack: Array<{ index: number; tokens: Array<Token> }> = [{ index: 0, tokens: [] }]\n\n    while (stack.length > 0) {\n      let { index, tokens } = stack.pop()!\n\n      if (index === pattern.tokens.length) {\n        result.push(new PartPatternVariant(tokens))\n        continue\n      }\n\n      let token = pattern.tokens[index]\n\n      if (token.type === '(') {\n        stack.push(\n          { index: index + 1, tokens },\n          { index: pattern.optionals.get(index)! + 1, tokens: tokens.slice() },\n        )\n        continue\n      }\n\n      if (token.type === ')') {\n        stack.push({ index: index + 1, tokens })\n        continue\n      }\n\n      if (\n        token.type === ':' ||\n        token.type === '*' ||\n        token.type === 'text' ||\n        token.type === 'separator'\n      ) {\n        tokens.push(token)\n        stack.push({ index: index + 1, tokens })\n        continue\n      }\n      unreachable(token.type)\n    }\n\n    return result\n  }\n\n  params(): Array<Param> {\n    let result = []\n    for (let token of this.tokens) {\n      if (token.type === ':' || token.type === '*') {\n        result.push(token)\n      }\n    }\n    return result\n  }\n\n  toString(separator: string): string {\n    let result = ''\n\n    for (let token of this.tokens) {\n      if (token.type === 'text') {\n        result += token.text\n        continue\n      }\n\n      if (token.type === ':' || token.type === '*') {\n        let name = token.name === '*' ? '' : token.name\n        result += `{${token.type}${name}}`\n        continue\n      }\n\n      if (token.type === 'separator') {\n        result += separator\n        continue\n      }\n\n      unreachable(token.type)\n    }\n\n    return result\n  }\n\n  segments(options?: { ignoreCase?: boolean }): Array<Segment> {\n    let ignoreCase = options?.ignoreCase ?? false\n    let result: Array<Segment> = []\n\n    let key = ''\n    let reSource = ''\n    let reFlags = ignoreCase ? 'di' : 'd'\n    let type: 'static' | 'variable' | 'wildcard' = 'static'\n\n    for (let token of this.tokens) {\n      if (token.type === 'separator') {\n        if (type === 'static') {\n          result.push({ type: 'static', key: ignoreCase ? key.toLowerCase() : key })\n          key = ''\n          reSource = ''\n          continue\n        }\n        if (type === 'variable') {\n          result.push({ type: 'variable', key, regexp: new RegExp(reSource, reFlags) })\n          key = ''\n          reSource = ''\n          type = 'static'\n          continue\n        }\n        if (type === 'wildcard') {\n          key += '/'\n          reSource += RE.escape('/')\n          continue\n        }\n        unreachable(type)\n      }\n\n      if (token.type === 'text') {\n        key += token.text\n        reSource += RE.escape(token.text)\n        continue\n      }\n\n      if (token.type === ':') {\n        key += '{:}'\n        reSource += `([^/]+)`\n        if (type === 'static') type = 'variable'\n        continue\n      }\n\n      if (token.type === '*') {\n        key += '{*}'\n        reSource += `(.*)`\n        type = 'wildcard'\n        continue\n      }\n\n      unreachable(token.type)\n    }\n\n    if (type === 'static') {\n      result.push({ type: 'static', key: ignoreCase ? key.toLowerCase() : key })\n    }\n    if (type === 'variable' || type === 'wildcard') {\n      result.push({ type, key, regexp: new RegExp(reSource, reFlags) })\n    }\n    return result\n  }\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/trie-matcher.ts",
    "content": "import type {\n  PartPattern,\n  PartPatternMatch,\n  PartPatternToken,\n} from './route-pattern/part-pattern.ts'\nimport { RoutePattern } from './route-pattern.ts'\nimport * as Variant from './trie-matcher/variant.ts'\nimport { unreachable } from './unreachable.ts'\nimport type { Match, Matcher } from './matcher.ts'\nimport * as Specificity from './specificity.ts'\nimport { matchSearch } from './route-pattern/match.ts'\n\ntype Param = Extract<PartPatternToken, { type: ':' | '*' }>\n\n/**\n * Trie-based matcher optimized for repeated route lookups.\n */\nexport class TrieMatcher<data = unknown> implements Matcher<data> {\n  /**\n   * Whether pathname matching is case-insensitive.\n   */\n  readonly ignoreCase: boolean\n\n  /**\n   * Trie storage used to index registered patterns.\n   */\n  trie: Trie<data>\n\n  /**\n   * @param options Constructor options\n   * @param options.ignoreCase When `true`, pathname matching is case-insensitive for all patterns. Defaults to `false`.\n   */\n  constructor(options?: { ignoreCase?: boolean }) {\n    this.ignoreCase = options?.ignoreCase ?? false\n    this.trie = new Trie({ ignoreCase: this.ignoreCase })\n  }\n\n  /**\n   * Adds a pattern and associated data to the trie.\n   *\n   * @param pattern Pattern to register.\n   * @param data Data returned when the pattern matches.\n   */\n  add(pattern: string | RoutePattern, data: data): void {\n    pattern = typeof pattern === 'string' ? new RoutePattern(pattern) : pattern\n    this.trie.insert(pattern, data)\n  }\n\n  /**\n   * Returns the best matching pattern for a URL.\n   *\n   * @param url URL to match.\n   * @param compareFn Specificity comparer used to rank matches.\n   * @returns The best match, or `null` when nothing matches.\n   */\n  match(url: string | URL, compareFn = Specificity.descending): Match<string, data> | null {\n    url = typeof url === 'string' ? new URL(url) : url\n    let matches = this.matchAll(url, compareFn)\n    return matches[0] ?? null\n  }\n\n  /**\n   * Returns every pattern that matches a URL.\n   *\n   * @param url URL to match.\n   * @param compareFn Specificity comparer used to sort matches.\n   * @returns All matching routes sorted by specificity.\n   */\n  matchAll(url: string | URL, compareFn = Specificity.descending): Array<Match<string, data>> {\n    url = typeof url === 'string' ? new URL(url) : url\n    let matches = this.trie.search(url)\n    return matches\n      .map((match) => ({\n        pattern: match.pattern,\n        url,\n        params: match.params,\n        paramsMeta: { hostname: match.hostname, pathname: match.pathname },\n        data: match.data,\n      }))\n      .sort(compareFn)\n  }\n}\n\ntype ProtocolNode<data> = {\n  http: HostnameNode<data>\n  https: HostnameNode<data>\n}\n\ntype HostnameNode<data> = {\n  static: Map<string, PortNode<data>>\n  dynamic: Array<{ part: PartPattern; portNode: PortNode<data> }>\n  any: PortNode<data>\n}\n\nfunction createHostnameNode<data>(): HostnameNode<data> {\n  return {\n    static: new Map(),\n    dynamic: [],\n    any: new Map(),\n  }\n}\n\ntype PortNode<data> = Map<string, PathnameNode<data>>\n\ntype PathnameNode<data> = {\n  static: Map<string, PathnameNode<data>>\n  variable: Map<string, { regexp: RegExp; pathnameNode: PathnameNode<data> }>\n  wildcard: Map<string, { regexp: RegExp; pathnameNode: PathnameNode<data> }>\n  values: Array<{\n    pattern: RoutePattern\n    data: data\n    requiredParams: Array<Param>\n    undefinedParams: Array<Param>\n  }>\n}\n\nfunction createPathnameNode<data>(): PathnameNode<data> {\n  return {\n    static: new Map(),\n    variable: new Map(),\n    wildcard: new Map(),\n    values: [],\n  }\n}\n\ntype SearchResult<data> = Array<{\n  pattern: RoutePattern\n  data: data\n  hostname: PartPatternMatch\n  pathname: PartPatternMatch\n  params: Record<string, string | undefined>\n}>\n\nexport class Trie<data = unknown> {\n  #ignoreCase: boolean\n  protocolNode: ProtocolNode<data>\n\n  constructor(options?: { ignoreCase?: boolean }) {\n    this.#ignoreCase = options?.ignoreCase ?? false\n    this.protocolNode = {\n      http: createHostnameNode(),\n      https: createHostnameNode(),\n    }\n  }\n\n  insert(pattern: RoutePattern, data: data): void {\n    for (let variant of Variant.generate(pattern)) {\n      // protocol -> hostname\n      let hostnameNode = this.protocolNode[variant.protocol]\n\n      // hostname -> port\n      let portNode: PortNode<data> | undefined = undefined\n      if (variant.hostname.type === 'any') {\n        portNode = hostnameNode.any\n      } else if (variant.hostname.type === 'static') {\n        let key = variant.hostname.value.toLowerCase()\n        portNode = hostnameNode.static.get(key)\n        if (portNode === undefined) {\n          portNode = new Map()\n          hostnameNode.static.set(key, portNode)\n        }\n      } else {\n        portNode = new Map()\n        hostnameNode.dynamic.push({ part: variant.hostname.value, portNode })\n      }\n\n      // port -> pathname\n      let pathnameRoot = portNode?.get(variant.port)\n      if (pathnameRoot === undefined) {\n        pathnameRoot = createPathnameNode()\n        portNode.set(variant.port, pathnameRoot)\n      }\n\n      // pathname segments\n      let pathnameNode = pathnameRoot\n      let segments = variant.pathname.segments({ ignoreCase: this.#ignoreCase })\n      for (let segment of segments) {\n        if (segment.type === 'static') {\n          let next = pathnameNode.static.get(segment.key)\n          if (next === undefined) {\n            next = createPathnameNode()\n            pathnameNode.static.set(segment.key, next)\n          }\n          pathnameNode = next\n          continue\n        }\n        if (segment.type === 'variable') {\n          let next = pathnameNode.variable.get(segment.key)\n          if (next === undefined) {\n            next = { regexp: segment.regexp, pathnameNode: createPathnameNode() }\n            pathnameNode.variable.set(segment.key, next)\n          }\n          pathnameNode = next.pathnameNode\n          continue\n        }\n        if (segment.type === 'wildcard') {\n          let next = pathnameNode.wildcard.get(segment.key)\n          if (next === undefined) {\n            next = { regexp: segment.regexp, pathnameNode: createPathnameNode() }\n            pathnameNode.wildcard.set(segment.key, next)\n          }\n          pathnameNode = next.pathnameNode\n          continue\n        }\n        unreachable(segment)\n      }\n\n      let requiredParams = variant.pathname.params()\n      let undefinedParams: Array<Param> = []\n      for (let param of pattern.ast.pathname.params) {\n        if (\n          !requiredParams.some((p) => p.name === param.name) &&\n          !undefinedParams.some((p) => p.name === param.name)\n        ) {\n          undefinedParams.push(param)\n        }\n      }\n      pathnameNode.values.push({\n        pattern,\n        data,\n        requiredParams,\n        undefinedParams,\n      })\n    }\n  }\n\n  search(url: URL): SearchResult<data> {\n    let origins: Array<{ hostnameMatch: PartPatternMatch; pathnameNode: PathnameNode<data> }> = []\n\n    // protocol -> hostname\n    let protocol = url.protocol.slice(0, -1)\n    if (protocol !== 'http' && protocol !== 'https') return []\n    let hostNameNode = this.protocolNode[protocol]\n\n    // any hostname + port -> pathname\n    let anyHostname = hostNameNode.any.get(url.port)\n    if (anyHostname) {\n      origins.push({\n        hostnameMatch: [\n          { type: '*', name: '*', begin: 0, end: url.hostname.length, value: url.hostname },\n        ],\n        pathnameNode: anyHostname,\n      })\n    }\n\n    // static hostname + port -> pathname (hostname case-insensitive)\n    let staticHostname = hostNameNode.static.get(url.hostname.toLowerCase())\n    if (staticHostname) {\n      let pathnameNode = staticHostname.get(url.port)\n      if (pathnameNode) {\n        origins.push({ hostnameMatch: [], pathnameNode })\n      }\n    }\n    // dynamic hostname + port -> pathname\n    hostNameNode.dynamic.forEach(({ part, portNode }) => {\n      let match = part.match(url.hostname, { ignoreCase: true })\n      if (match) {\n        let pathnameNode = portNode.get(url.port)\n        if (pathnameNode) {\n          origins.push({ hostnameMatch: match, pathnameNode })\n        }\n      }\n    })\n\n    let results: SearchResult<data> = []\n\n    // pathname\n    let urlSegments = url.pathname.slice(1).split('/')\n    for (let origin of origins) {\n      let stack: Array<{\n        segmentIndex: number\n        pathnameNode: PathnameNode<data>\n        charOffset: number\n        pathnameMatch: Array<{\n          value: string\n          begin: number\n          end: number\n        }>\n      }> = [\n        { segmentIndex: 0, pathnameNode: origin.pathnameNode, charOffset: 0, pathnameMatch: [] },\n      ]\n      while (stack.length > 0) {\n        let current = stack.pop()!\n\n        if (current.segmentIndex === urlSegments.length) {\n          for (let value of current.pathnameNode.values) {\n            if (!matchSearch(url.searchParams, value.pattern.ast.search)) {\n              continue\n            }\n\n            let pathnameMatch: PartPatternMatch = []\n            for (let i = 0; i < value.requiredParams.length; i++) {\n              let param = value.requiredParams[i]\n              let rest = current.pathnameMatch[i]\n              pathnameMatch.push({\n                ...rest,\n                ...param,\n              })\n            }\n\n            let params: Record<string, string | undefined> = {}\n            // Start with all params from the original pattern set to undefined\n            for (let param of value.pattern.ast.hostname?.params ?? []) {\n              if (param.name !== '*') {\n                params[param.name] = undefined\n              }\n            }\n            for (let param of value.pattern.ast.pathname.params) {\n              if (param.name !== '*') {\n                params[param.name] = undefined\n              }\n            }\n            // Then overwrite with actual matched values\n            for (let param of origin.hostnameMatch) {\n              if (param.name === '*') continue\n              params[param.name] = param.value\n            }\n            for (let param of pathnameMatch) {\n              if (param.name === '*') continue\n              params[param.name] = param.value\n            }\n\n            results.push({\n              pattern: value.pattern,\n              data: value.data,\n              hostname: origin.hostnameMatch,\n              pathname: pathnameMatch,\n              params,\n            })\n          }\n          continue\n        }\n\n        let urlSegment = urlSegments[current.segmentIndex]\n        let staticKey = this.#ignoreCase ? urlSegment.toLowerCase() : urlSegment\n        let nextStatic = current.pathnameNode.static.get(staticKey)\n        if (nextStatic) {\n          stack.push({\n            segmentIndex: current.segmentIndex + 1,\n            pathnameNode: nextStatic,\n            charOffset: current.charOffset + urlSegment.length + 1,\n            pathnameMatch: current.pathnameMatch,\n          })\n        }\n\n        for (let { regexp, pathnameNode } of current.pathnameNode.variable.values()) {\n          let match = regexp.exec(urlSegment)\n          if (match) {\n            let pathnameMatch = current.pathnameMatch.slice()\n            for (let i = 1; i < match.indices!.length; i++) {\n              let span = match.indices![i]\n              if (span === undefined) unreachable()\n              pathnameMatch.push({\n                begin: current.charOffset + span[0],\n                end: current.charOffset + span[1],\n                value: match[i],\n              })\n            }\n            stack.push({\n              segmentIndex: current.segmentIndex + 1,\n              pathnameNode,\n              charOffset: current.charOffset + match.index + match[0].length + 1,\n              pathnameMatch,\n            })\n          }\n        }\n\n        for (let { regexp, pathnameNode } of current.pathnameNode.wildcard.values()) {\n          let remaining = urlSegments.slice(current.segmentIndex).join('/')\n          let match = regexp.exec(remaining)\n          if (match) {\n            let pathnameMatch = current.pathnameMatch.slice()\n            for (let i = 1; i < match.indices!.length; i++) {\n              let span = match.indices![i]\n              if (span === undefined) continue\n              pathnameMatch.push({\n                begin: current.charOffset + span[0],\n                end: current.charOffset + span[1],\n                value: match[i],\n              })\n            }\n            stack.push({\n              segmentIndex: urlSegments.length,\n              pathnameNode,\n              charOffset: current.charOffset + remaining.length,\n              pathnameMatch,\n            })\n          }\n        }\n      }\n    }\n\n    return results\n  }\n}\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/index.ts",
    "content": "export type { Join } from './join.ts'\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/join.test.ts",
    "content": "import type { Assert, IsEqual } from './utils.ts'\nimport type { Join } from './join.ts'\n\n// prettier-ignore\nexport type Tests = [\n  // empty input/base\n  Assert<IsEqual<Join<'', ''>, '/'>>,\n  Assert<IsEqual<Join<'/', '/'>, '/'>>,\n  Assert<IsEqual<Join<'hello', ''>, '/hello'>>,\n  Assert<IsEqual<Join<'', 'hello'>, '/hello'>>,\n\n  // input origin overrides base origin\n  Assert<IsEqual<Join<'http://example.com:8080', 'https://remix.run'>, 'https://remix.run/'>>,\n  Assert<IsEqual<Join<'http://example.com:8080', '://remix.run'>, '://remix.run/'>>,\n  Assert<IsEqual<Join<'http://example.com', '://remix.run:8080'>, '://remix.run:8080/'>>,\n\n  // base origin used when input has none\n  Assert<IsEqual<Join<'https://remix.run', 'api'>, 'https://remix.run/api'>>,\n  Assert<IsEqual<Join<'https://remix.run/', 'api'>, 'https://remix.run/api'>>,\n  Assert<IsEqual<Join<'https://remix.run:8080', 'api'>, 'https://remix.run:8080/api'>>,\n  Assert<IsEqual<Join<'https://remix.run:8080/', 'api'>, 'https://remix.run:8080/api'>>,\n\n  // root pathname join\n  Assert<IsEqual<Join<'/', 'hello'>, '/hello'>>,\n  Assert<IsEqual<Join<'/', '/hello'>, '/hello'>>,\n\n  // root input with existing pathname\n  Assert<IsEqual<Join<'hello', '/'>, '/hello'>>,\n  Assert<IsEqual<Join<'/hello', '/'>, '/hello'>>,\n\n  // absolute pathname join\n  Assert<IsEqual<Join<'hello', '/world'>, '/hello/world'>>,\n  Assert<IsEqual<Join<'/hello', '/world'>, '/hello/world'>>,\n  Assert<IsEqual<Join<'hello/', '/world'>, '/hello/world'>>,\n  Assert<IsEqual<Join<'/hello/', '/world'>, '/hello/world'>>,\n\n  // relative pathname join\n  Assert<IsEqual<Join<'hello', 'world'>, '/hello/world'>>,\n  Assert<IsEqual<Join<'/hello', 'world'>, '/hello/world'>>,\n  Assert<IsEqual<Join<'hello/', 'world'>, '/hello/world'>>,\n  Assert<IsEqual<Join<'/hello/', 'world'>, '/hello/world'>>,\n\n  // optional pathname join\n  Assert<IsEqual<Join<'', '(/:lang)/world'>, '(/:lang)/world'>>,\n  Assert<IsEqual<Join<'/', '(/:lang)/world'>, '(/:lang)/world'>>,\n  Assert<IsEqual<Join<'hello', '(/:lang)/world'>, '/hello(/:lang)/world'>>,\n  Assert<IsEqual<Join<'hello/', '(/:lang)/world'>, '/hello(/:lang)/world'>>,\n\n  // search params\n  Assert<IsEqual<Join<'https://remix.run', '?q=1'>, 'https://remix.run/?q=1'>>,\n  Assert<IsEqual<Join<'https://remix.run?q=1', '?q=2'>, 'https://remix.run/?q=1&q=2'>>,\n]\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/join.ts",
    "content": "import type { Parse, ParsedPattern, Separator, Token } from './parse'\nimport type { StartsWithSeparator, Stringify } from './stringify'\n\n/**\n * Join two pattern strings together.\n */\nexport type Join<A extends string, B extends string> = _Join<Parse<A>, Parse<B>>\n\ntype _Join<A extends ParsedPattern, B extends ParsedPattern> = Stringify<{\n  protocol: JoinOriginField<A, B, 'protocol'>\n  hostname: JoinOriginField<A, B, 'hostname'>\n  port: JoinOriginField<A, B, 'port'>\n  pathname: JoinPathnames<A['pathname'], B['pathname']>\n  search: JoinSearch<A['search'], B['search']>\n}>\n\n// prettier-ignore\ntype JoinOriginField<\n  A extends ParsedPattern,\n  B extends ParsedPattern,\n  Field extends 'protocol' | 'hostname' | 'port'\n> = B['hostname'] extends Token[] ? B[Field] : A[Field]\n\n// prettier-ignore\ntype JoinPathnames<A extends Token[] | undefined, B extends Token[] | undefined> =\n  B extends undefined ? A :\n  B extends [] ? A :\n  A extends undefined ? B :\n  A extends [] ? B :\n  A extends Token[] ?\n    B extends Token[] ? JoinPathnameTokens<RemoveTrailingSeparator<A>, B> :\n    never :\n  never\n\n// prettier-ignore\ntype RemoveTrailingSeparator<T extends Token[]> =\n  T extends [...infer Rest extends Token[], Separator] ? Rest : T\n\n// prettier-ignore\ntype JoinPathnameTokens<\n  A extends Token[],\n  B extends Token[]\n> = B extends [Separator] ?\n    A :\n    StartsWithSeparator<B> extends true ?\n      [...A, ...B] :\n      [...A, Separator, ...B]\n\n// prettier-ignore\ntype JoinSearch<\n  A extends string | undefined,\n  B extends string | undefined\n> = B extends undefined ? A :\n    A extends undefined ? B :\n    `${A}&${B}`\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/parse.test.ts",
    "content": "import type { Assert, IsEqual } from './utils.ts'\nimport type { Parse } from './parse.ts'\n\nexport type Tests = [\n  // empty string\n  Assert<\n    IsEqual<\n      Parse<''>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: undefined\n        search: undefined\n      }\n    >\n  >,\n\n  // pathname only patterns\n  Assert<\n    IsEqual<\n      Parse<'hello'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: 'hello' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'hello world'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: 'hello world' }]\n        search: undefined\n      }\n    >\n  >,\n\n  // variables\n  Assert<\n    IsEqual<\n      Parse<':id'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'variable'; name: 'id' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<':user_id'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'variable'; name: 'user_id' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<':$special'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'variable'; name: '$special' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'users/:id'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [\n          { type: 'text'; value: 'users' },\n          { type: 'separator' },\n          { type: 'variable'; name: 'id' },\n        ]\n        search: undefined\n      }\n    >\n  >,\n\n  // wildcard\n  Assert<\n    IsEqual<\n      Parse<'*files'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'wildcard'; name: 'files' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'*'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'wildcard' }]\n        search: undefined\n      }\n    >\n  >,\n\n  // optional\n  Assert<\n    IsEqual<\n      Parse<'(hello)'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'optional'; tokens: [{ type: 'text'; value: 'hello' }] }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'(:id)'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'optional'; tokens: [{ type: 'variable'; name: 'id' }] }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'(users/:id)'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [\n          {\n            type: 'optional'\n            tokens: [\n              { type: 'text'; value: 'users' },\n              { type: 'separator' },\n              { type: 'variable'; name: 'id' },\n            ]\n          },\n        ]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'api(/:version)'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [\n          { type: 'text'; value: 'api' },\n          {\n            type: 'optional'\n            tokens: [{ type: 'separator' }, { type: 'variable'; name: 'version' }]\n          },\n        ]\n        search: undefined\n      }\n    >\n  >,\n\n  // escaping\n  Assert<\n    IsEqual<\n      Parse<'\\\\:'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: ':' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'\\\\*'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: '*' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'\\\\('>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: '(' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'\\\\{'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: '{' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'\\\\\\\\'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: '\\\\' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'hello\\\\:world\\\\*test'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: 'hello:world*test' }]\n        search: undefined\n      }\n    >\n  >,\n\n  // complex combinations\n  Assert<\n    IsEqual<\n      Parse<'api/v:version/users/:id(*rest.:format)'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [\n          { type: 'text'; value: 'api' },\n          { type: 'separator' },\n          { type: 'text'; value: 'v' },\n          { type: 'variable'; name: 'version' },\n          { type: 'separator' },\n          { type: 'text'; value: 'users' },\n          { type: 'separator' },\n          { type: 'variable'; name: 'id' },\n          {\n            type: 'optional'\n            tokens: [\n              { type: 'wildcard'; name: 'rest' },\n              { type: 'text'; value: '.' },\n              { type: 'variable'; name: 'format' },\n            ]\n          },\n        ]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'/path/:id'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [\n          { type: 'text'; value: 'path' },\n          { type: 'separator' },\n          { type: 'variable'; name: 'id' },\n        ]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'/path/:id?q=1'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [\n          { type: 'text'; value: 'path' },\n          { type: 'separator' },\n          { type: 'variable'; name: 'id' },\n        ]\n        search: 'q=1'\n      }\n    >\n  >,\n\n  // full URL patterns\n  Assert<\n    IsEqual<\n      Parse<'https\\\\:'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: 'https:' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<':protocol\\\\:'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'variable'; name: 'protocol' }, { type: 'text'; value: ':' }]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'://example.com'>,\n      {\n        protocol: undefined\n        hostname: [\n          { type: 'text'; value: 'example' },\n          { type: 'separator' },\n          { type: 'text'; value: 'com' },\n        ]\n        port: undefined\n        pathname: undefined\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'://:subdomain.example.com'>,\n      {\n        protocol: undefined\n        hostname: [\n          { type: 'variable'; name: 'subdomain' },\n          { type: 'separator' },\n          { type: 'text'; value: 'example' },\n          { type: 'separator' },\n          { type: 'text'; value: 'com' },\n        ]\n        port: undefined\n        pathname: undefined\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'https://example.com'>,\n      {\n        protocol: [{ type: 'text'; value: 'https' }]\n        hostname: [\n          { type: 'text'; value: 'example' },\n          { type: 'separator' },\n          { type: 'text'; value: 'com' },\n        ]\n        port: undefined\n        pathname: undefined\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'://example.com:8080'>,\n      {\n        protocol: undefined\n        hostname: [\n          { type: 'text'; value: 'example' },\n          { type: 'separator' },\n          { type: 'text'; value: 'com' },\n        ]\n        port: '8080'\n        pathname: undefined\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'://*.example.com'>,\n      {\n        protocol: undefined\n        hostname: [\n          { type: 'wildcard' },\n          { type: 'separator' },\n          { type: 'text'; value: 'example' },\n          { type: 'separator' },\n          { type: 'text'; value: 'com' },\n        ]\n        port: undefined\n        pathname: undefined\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'://example.com:8080/api/:id'>,\n      {\n        protocol: undefined\n        hostname: [\n          { type: 'text'; value: 'example' },\n          { type: 'separator' },\n          { type: 'text'; value: 'com' },\n        ]\n        port: '8080'\n        pathname: [\n          { type: 'text'; value: 'api' },\n          { type: 'separator' },\n          { type: 'variable'; name: 'id' },\n        ]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'https://example.com/api/:id'>,\n      {\n        protocol: [{ type: 'text'; value: 'https' }]\n        hostname: [\n          { type: 'text'; value: 'example' },\n          { type: 'separator' },\n          { type: 'text'; value: 'com' },\n        ]\n        port: undefined\n        pathname: [\n          { type: 'text'; value: 'api' },\n          { type: 'separator' },\n          { type: 'variable'; name: 'id' },\n        ]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'search?q=:query'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [{ type: 'text'; value: 'search' }]\n        search: 'q=:query'\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<':protocol://:subdomain.example.com:8080/api/v:version/users/:id?format=json'>,\n      {\n        protocol: [{ type: 'variable'; name: 'protocol' }]\n        hostname: [\n          { type: 'variable'; name: 'subdomain' },\n          { type: 'separator' },\n          { type: 'text'; value: 'example' },\n          { type: 'separator' },\n          { type: 'text'; value: 'com' },\n        ]\n        port: '8080'\n        pathname: [\n          { type: 'text'; value: 'api' },\n          { type: 'separator' },\n          { type: 'text'; value: 'v' },\n          { type: 'variable'; name: 'version' },\n          { type: 'separator' },\n          { type: 'text'; value: 'users' },\n          { type: 'separator' },\n          { type: 'variable'; name: 'id' },\n        ]\n        search: 'format=json'\n      }\n    >\n  >,\n\n  // nested optionals (successful parses)\n  Assert<\n    IsEqual<\n      Parse<'(nested(test))'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [\n          {\n            type: 'optional'\n            tokens: [\n              { type: 'text'; value: 'nested' },\n              { type: 'optional'; tokens: [{ type: 'text'; value: 'test' }] },\n            ]\n          },\n        ]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'files(/*path(.:ext))'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [\n          { type: 'text'; value: 'files' },\n          {\n            type: 'optional'\n            tokens: [\n              { type: 'separator' },\n              { type: 'wildcard'; name: 'path' },\n              {\n                type: 'optional'\n                tokens: [{ type: 'text'; value: '.' }, { type: 'variable'; name: 'ext' }]\n              },\n            ]\n          },\n        ]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'files(/*(.:ext))'>,\n      {\n        protocol: undefined\n        hostname: undefined\n        port: undefined\n        pathname: [\n          { type: 'text'; value: 'files' },\n          {\n            type: 'optional'\n            tokens: [\n              { type: 'separator' },\n              { type: 'wildcard' },\n              {\n                type: 'optional'\n                tokens: [{ type: 'text'; value: '.' }, { type: 'variable'; name: 'ext' }]\n              },\n            ]\n          },\n        ]\n        search: undefined\n      }\n    >\n  >,\n  Assert<\n    IsEqual<\n      Parse<'users/:id' | 'api/(v:major(.:minor))'>,\n      | {\n          protocol: undefined\n          hostname: undefined\n          port: undefined\n          pathname: [\n            { type: 'text'; value: 'users' },\n            { type: 'separator' },\n            { type: 'variable'; name: 'id' },\n          ]\n          search: undefined\n        }\n      | {\n          protocol: undefined\n          hostname: undefined\n          port: undefined\n          pathname: [\n            { type: 'text'; value: 'api' },\n            { type: 'separator' },\n            {\n              type: 'optional'\n              tokens: [\n                { type: 'text'; value: 'v' },\n                { type: 'variable'; name: 'major' },\n                {\n                  type: 'optional'\n                  tokens: [{ type: 'text'; value: '.' }, { type: 'variable'; name: 'minor' }]\n                },\n              ]\n            },\n          ]\n          search: undefined\n        }\n    >\n  >,\n]\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/parse.ts",
    "content": "import type { Split, SplitPattern } from './split'\nimport type { ForceDistributive } from './utils'\n\nexport interface ParsedPattern {\n  protocol: Token[] | undefined\n  hostname: Token[] | undefined\n  port: string | undefined\n  pathname: Token[] | undefined\n  search: string | undefined\n}\n\n// prettier-ignore\nexport type Parse<T extends string> =\n  T extends ForceDistributive ?\n    Split<T> extends infer S extends SplitPattern ?\n      {\n        protocol: S['protocol'] extends string ? ParsePart<S['protocol']> : undefined\n        hostname: S['hostname'] extends string ? ParsePart<S['hostname'], '.'> : undefined\n        port: S['port'] extends string ? S['port'] : undefined\n        pathname: S['pathname'] extends string ? ParsePart<S['pathname'], '/'> : undefined\n        search: S['search'] extends string ? S['search'] : undefined\n      } :\n      never :\n    never\n\nexport type Variable = { type: 'variable'; name: string }\nexport type Wildcard = { type: 'wildcard'; name?: string }\nexport type Text = { type: 'text'; value: string }\nexport type Separator = { type: 'separator' }\nexport type Optional = { type: 'optional'; tokens: Token[] }\n\nexport type Token = Variable | Wildcard | Text | Separator | Optional\n\ntype ParsePartState = {\n  tokens: Token[]\n  optionals: Array<Token[]>\n  rest: string\n}\n\ntype ParsePart<T extends string, Sep extends string = ''> = _ParsePart<\n  {\n    tokens: []\n    optionals: []\n    rest: T\n  },\n  Sep\n>\n\n// prettier-ignore\ntype _ParsePart<S extends ParsePartState, Sep extends string = ''> =\n  S extends { rest: `${infer Head}${infer Tail}` } ?\n    Head extends Sep ? _ParsePart<AppendToken<S, { type: 'separator' }, Tail>, Sep> :\n    Head extends ':' ?\n      IdentifierParse<Tail> extends { identifier: infer name extends string, rest: infer rest extends string } ?\n        (name extends '' ? never : _ParsePart<AppendToken<S, { type: 'variable', name: name }, rest>, Sep>) :\n      never : // this should never happen\n    Head extends '*' ?\n      IdentifierParse<Tail> extends { identifier: infer name extends string, rest: infer rest extends string } ?\n        _ParsePart<AppendToken<S, (name extends '' ? { type: 'wildcard' } : { type: 'wildcard', name: name }), rest>, Sep> :\n      never : // this should never happen\n    Head extends '(' ? _ParsePart<PushOptional<S, Tail>, Sep> :\n    Head extends ')' ?\n      PopOptional<S, Tail> extends infer next extends ParsePartState ? _ParsePart<next, Sep> :\n      never : // unmatched `)` handled in PopOptional\n    Head extends '\\\\' ?\n      Tail extends `${infer L}${infer R}` ? _ParsePart<AppendText<S, L, R>, Sep> :\n      never : // dangling escape\n    _ParsePart<AppendText<S, Head, Tail>, Sep> :\n  S['optionals'] extends [] ? S['tokens'] :\n  never // unmatched `(`\n\n// prettier-ignore\ntype AppendToken<S extends ParsePartState, token extends Token, rest extends string> =\n  S['optionals'] extends [...infer O extends Array<Token[]>, infer Top extends Token[]] ?\n    {\n      tokens: S['tokens']\n      optionals: [...O, [...Top, token]]\n      rest: rest\n    } :\n    {\n      tokens: [...S['tokens'], token]\n      optionals: S['optionals']\n      rest: rest;\n    }\n\n// prettier-ignore\ntype AppendText<S extends ParsePartState, text extends string, rest extends string> =\n  S['optionals'] extends [...infer O extends Array<Token[]>, infer Top extends Token[]] ?\n    (\n      Top extends [...infer Tokens extends Array<Token>, { type: 'text', value: infer value extends string }] ?\n        { tokens: S['tokens']; optionals: [...O, [...Tokens, { type: 'text', value: `${value}${text}` }]]; rest: rest } :\n        { tokens: S['tokens']; optionals: [...O, [...Top, { type: 'text', value: text }]]; rest: rest }\n    ) :\n    (\n      S['tokens'] extends [...infer Tokens extends Array<Token>, { type: 'text', value: infer value extends string }] ?\n        { tokens: [...Tokens, { type: 'text', value: `${value}${text}` }]; optionals: S['optionals']; rest: rest } :\n        { tokens: [...S['tokens'], { type: 'text', value: text }]; optionals: S['optionals']; rest: rest }\n    )\n\n// Optional stack helpers ---------------------------------------------------------------------------\n\ntype PushOptional<S extends ParsePartState, rest extends string> = {\n  tokens: S['tokens']\n  optionals: [...S['optionals'], []]\n  rest: rest\n}\n\n// If stack is empty -> unmatched ')', return never\n// Else pop and wrap tokens into an Optional token; append to parent or part\ntype PopOptional<S extends ParsePartState, R extends string> = S['optionals'] extends [\n  ...infer O extends Array<Token[]>,\n  infer Top extends Array<Token>,\n]\n  ? O extends [...infer OO extends Array<Token[]>, infer Parent extends Token[]]\n    ? {\n        tokens: S['tokens']\n        optionals: [...OO, [...Parent, { type: 'optional'; tokens: Top }]]\n        rest: R\n      }\n    : { tokens: [...S['tokens'], { type: 'optional'; tokens: Top }]; optionals: []; rest: R }\n  : never\n\n// Identifier --------------------------------------------------------------------------------------\n\n// prettier-ignore\ntype _a_z = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'\ntype _A_Z = Uppercase<_a_z>\ntype _0_9 = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'\n\ntype IdentifierHead = _a_z | _A_Z | '_' | '$'\ntype IdentifierTail = IdentifierHead | _0_9\n\ntype IdentifierParse<T extends string> = _IdentifierParse<{ identifier: ''; rest: T }>\n\n// prettier-ignore\ntype _IdentifierParse<S extends { identifier: string, rest: string }> =\n  S extends { identifier: '', rest: `${infer Head extends IdentifierHead}${infer Tail}` } ?\n    _IdentifierParse<{ identifier: Head, rest: Tail }> :\n    S extends { identifier: string, rest: `${infer Head extends IdentifierTail}${infer Tail}`} ?\n      _IdentifierParse<{ identifier: `${S['identifier']}${Head}`, rest: Tail }> :\n      S\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/split.test.ts",
    "content": "import type { Assert, IsEqual } from './utils.ts'\nimport type { Split } from './split.ts'\n\n// prettier-ignore\nexport type Tests = [\n  // empty string\n  Assert<IsEqual<\n    Split<''>,\n    { protocol: undefined; hostname: undefined; port: undefined; pathname: undefined; search: undefined }\n  >>,\n\n  // complex URL\n  Assert<IsEqual<\n    Split<'http(s)://(*host.:sub.)remix.run:8080/products(/:id/v:version)/*?q=1'>,\n    { protocol: 'http(s)'; hostname: '(*host.:sub.)remix.run'; port: '8080'; pathname: 'products(/:id/v:version)/*'; search: 'q=1' }\n  >>,\n\n  // protocol + ...\n  Assert<IsEqual<\n    Split<'http://host.com'>,\n    { protocol: 'http'; hostname: 'host.com'; port: undefined; pathname: undefined; search: undefined }\n  >>,\n  Assert<IsEqual<\n    Split<'http://host.com:8080'>,\n    { protocol: 'http'; hostname: 'host.com'; port: '8080'; pathname: undefined; search: undefined }\n  >>,\n  Assert<IsEqual<\n    Split<'http://host.com/path/:id'>,\n    { protocol: 'http'; hostname: 'host.com'; port: undefined; pathname: 'path/:id'; search: undefined }\n  >>,\n  Assert<IsEqual<\n    Split<'http://host.com:8080/path/:id'>,\n    { protocol: 'http'; hostname: 'host.com'; port: '8080'; pathname: 'path/:id'; search: undefined }\n  >>,\n  Assert<IsEqual<\n    Split<'http://host.com?q=1'>,\n    { protocol: 'http'; hostname: 'host.com'; port: undefined; pathname: undefined; search: 'q=1' }\n  >>,\n  Assert<IsEqual<\n    Split<'http://host.com/path/:id?q=1'>,\n    { protocol: 'http'; hostname: 'host.com'; port: undefined; pathname: 'path/:id'; search: 'q=1' }\n  >>,\n\n  // hostname + ...\n  Assert<IsEqual<\n    Split<'://host.com'>,\n    { protocol: undefined; hostname: 'host.com'; port: undefined; pathname: undefined; search: undefined }\n  >>,\n  Assert<IsEqual<\n    Split<'://host.com:3000'>,\n    { protocol: undefined; hostname: 'host.com'; port: '3000'; pathname: undefined; search: undefined }\n  >>,\n  Assert<IsEqual<\n    Split<'://host.com/path/:id'>,\n    { protocol: undefined; hostname: 'host.com'; port: undefined; pathname: 'path/:id'; search: undefined }\n  >>,\n  Assert<IsEqual<\n    Split<'://host.com:3000/path/:id'>,\n    { protocol: undefined; hostname: 'host.com'; port: '3000'; pathname: 'path/:id'; search: undefined }\n  >>,\n  Assert<IsEqual<\n    Split<'://host.com?q=1'>,\n    { protocol: undefined; hostname: 'host.com'; port: undefined; pathname: undefined; search: 'q=1' }\n  >>,\n  Assert<IsEqual<\n    Split<'://host.com/path/:id?q=1'>,\n    { protocol: undefined; hostname: 'host.com'; port: undefined; pathname: 'path/:id'; search: 'q=1' }\n  >>,\n\n  // pathname + ...\n  Assert<IsEqual<\n    Split<'path/:id'>,\n    { protocol: undefined; hostname: undefined; port: undefined; pathname: 'path/:id'; search: undefined }\n  >>,\n  Assert<IsEqual<\n    Split<'path/:id?q=1'>,\n    { protocol: undefined; hostname: undefined; port: undefined; pathname: 'path/:id'; search: 'q=1' }\n  >>,\n  Assert<IsEqual<\n    Split<'/path/:id'>,\n    { protocol: undefined; hostname: undefined; port: undefined; pathname: 'path/:id'; search: undefined }\n  >>,\n\n  // search + ...\n  Assert<IsEqual<\n    Split<'?q=1'>,\n    { protocol: undefined; hostname: undefined; port: undefined; pathname: undefined; search: 'q=1' }\n  >>,\n  Assert<IsEqual<\n    Split<'/path/:id?q=1'>,\n    { protocol: undefined; hostname: undefined; port: undefined; pathname: 'path/:id'; search: 'q=1' }\n  >>,\n]\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/split.ts",
    "content": "export interface SplitPattern {\n  protocol: string | undefined\n  hostname: string | undefined\n  port: string | undefined\n  pathname: string | undefined\n  search: string | undefined\n}\n\n// prettier-ignore\nexport type Split<T extends string> =\n  _Split<T> extends infer S extends Partial<SplitPattern> ? {\n    protocol: S['protocol'] extends string ? S['protocol'] : undefined\n    hostname: S['hostname'] extends string ? S['hostname'] : undefined\n    port: S['port'] extends string ? S['port'] : undefined\n    pathname: S['pathname'] extends string ? S['pathname'] : undefined\n    search: S['search'] extends string ? S['search'] : undefined\n  } :\n  never\n\n// prettier-ignore\ntype _Split<T extends string> =\n  T extends '' ? {} :\n  T extends `${infer L}?${infer R}` ? _Split<L> & { search: R } :\n  T extends `${infer Protocol}://${infer R}` ?\n    Protocol extends '' ? (\n      R extends `${infer Host}/${infer Pathname}` ? SplitHost<Host> & { pathname: Pathname } :\n      SplitHost<R>\n    ) :\n    Protocol extends `${string}/${string}` ? { pathname: T } :\n    R extends `${infer Host}/${infer Pathname}` ? SplitHost<Host> & { protocol: Protocol; pathname: Pathname } :\n    SplitHost<R> & { protocol: Protocol } :\n  T extends `/${infer Pathname}` ? { pathname: Pathname } :\n  { pathname: T }\n\n// prettier-ignore\ntype SplitHost<T extends string> =\n  T extends `${infer L}:${infer R}` ?\n    IsDigits<R> extends true ? { hostname: L; port: R} :\n    SplitHost<R> extends { hostname: infer H extends string; port: infer P extends string } ? { hostname: `${L}:${H}`; port: P } :\n    { hostname: T } :\n  { hostname: T }\n\ntype _0_9 = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'\n\n// prettier-ignore\ntype IsDigits<S extends string> =\n  S extends `${_0_9}${infer T}` ?\n    T extends '' ? true :\n    IsDigits<T> :\n  false\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/stringify.test.ts",
    "content": "import type { Assert, IsEqual } from './utils.ts'\nimport type { Parse } from './parse.ts'\nimport type { Stringify } from './stringify.ts'\n\nexport type Tests = [\n  Assert<IsEqual<Stringify<Parse<''>>, '/'>>,\n  Assert<IsEqual<Stringify<Parse<'http'>>, '/http'>>,\n  Assert<IsEqual<Stringify<Parse<'hello/world'>>, '/hello/world'>>,\n  Assert<IsEqual<Stringify<Parse<'hello/world?q=1'>>, '/hello/world?q=1'>>,\n  Assert<IsEqual<Stringify<Parse<'hello/world?q=1&q=2'>>, '/hello/world?q=1&q=2'>>,\n  Assert<IsEqual<Stringify<Parse<'hello/world?q=1&a=2'>>, '/hello/world?q=1&a=2'>>,\n\n  Assert<IsEqual<Stringify<Parse<'http://example.com'>>, 'http://example.com/'>>,\n  Assert<IsEqual<Stringify<Parse<'https://example.com/path'>>, 'https://example.com/path'>>,\n  Assert<IsEqual<Stringify<Parse<'https://example.com/path?q=1'>>, 'https://example.com/path?q=1'>>,\n  Assert<\n    IsEqual<\n      Stringify<Parse<'https://example.com/path?q=1&q=2'>>,\n      'https://example.com/path?q=1&q=2'\n    >\n  >,\n  Assert<\n    IsEqual<\n      Stringify<Parse<'https://example.com/path?q=1&a=2'>>,\n      'https://example.com/path?q=1&a=2'\n    >\n  >,\n]\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/stringify.ts",
    "content": "import type { ParsedPattern, Token } from './parse'\n\n// prettier-ignore\nexport type Stringify<T extends ParsedPattern> =\n  T['hostname'] extends Token[] ?\n    `${StringifyTokens<T['protocol'], ''>}://${StringifyTokens<T['hostname'], '.'>}${StringifyPort<T['port']>}${StringifyPathname<T['pathname']>}${StringifySearch<T['search']>}` :\n    `${StringifyPathname<T['pathname']>}${StringifySearch<T['search']>}`\n\n// prettier-ignore\ntype StringifyTokens<T extends Token[] | undefined, Sep extends string> =\n  T extends undefined ? '' :\n  T extends [] ? '' :\n  T extends [infer Head extends Token, ...infer Tail extends Token[]] ?\n    `${StringifyToken<Head, Sep>}${StringifyTokens<Tail, Sep>}` :\n    never\n\n// prettier-ignore\ntype StringifyToken<T extends Token, Sep extends string> =\n  T extends { type: 'text', value: infer V extends string } ? V :\n  T extends { type: 'variable', name: infer N extends string } ? `:${N}` :\n  T extends { type: 'wildcard', name: infer N extends string } ? `*${N}` :\n  T extends { type: 'wildcard' } ? '*' :\n  T extends { type: 'separator' } ? Sep :\n  T extends { type: 'optional', tokens: infer Tokens extends Token[] } ? `(${StringifyTokens<Tokens, Sep>})` :\n  never\n\n// prettier-ignore\ntype StringifyPathname<T extends Token[] | undefined> =\n  T extends undefined ? '/' :\n  T extends [] ? '/' :\n  T extends Token[] ?\n    StartsWithSeparator<T> extends true ?\n      `${StringifyTokens<T, '/'>}` :\n      `/${StringifyTokens<T, '/'>}` :\n    never\n\ntype StringifyPort<T extends string | undefined> = T extends string ? `:${T}` : ''\n\ntype StringifySearch<T extends string | undefined> = T extends string ? `?${T}` : ''\n\n// prettier-ignore\nexport type StartsWithSeparator<T extends Token[]> =\n  T extends [] ? false :\n  T extends [{ type: 'separator' }, ...Token[]] ? true :\n  T extends [{ type: 'optional', tokens: infer Tokens extends Token[] }, ...Token[]] ?\n    StartsWithSeparator<Tokens> :\n    false\n"
  },
  {
    "path": "packages/route-pattern/src/lib/types/utils.ts",
    "content": "export type Assert<T extends true> = T\n\nexport type IsEqual<A, B> =\n  (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false\n\nexport type Simplify<T> = { [K in keyof T]: T[K] } & {}\n\n/**\n * Function arguments are contravariant, so unknown args must be typed as `Array<any>`\n *\n * Usage:\n *\n * ```ts\n * type UnknownFunction = (args: UnknownArgs) => unknown\n * ```\n */\nexport type UnknownArgs = Array<any>\n\n/**\n * Force TS to distribute a union with `T extends ForceDistributeUnion ? ... : ...`\n * as a more explicit alias of the common `T extends any ? ... : ...` pattern.\n *\n * See: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types\n *\n * Usage:\n *\n * ```ts\n * type Stuff<T> =\n *   T extends ForceDistributive ?\n *     // Now, operate on each member of the union separately\n *     string extends T ? 'string' :\n *     T\n *   :\n *   never\n * ```\n */\nexport type ForceDistributive = any\n"
  },
  {
    "path": "packages/route-pattern/src/lib/unreachable.ts",
    "content": "/**\n * An internal error the should never happen.\n *\n * @param value Typed as `never` to ensure exhaustiveness for discriminated unions.\n */\nexport function unreachable(value?: never): never {\n  let message = value === undefined ? 'Unreachable' : `Unreachable: ${value}`\n  throw new Error(message)\n}\n"
  },
  {
    "path": "packages/route-pattern/src/specificity.ts",
    "content": "export { lessThan, greaterThan, equal, ascending, descending, compare } from './lib/specificity.ts'\n"
  },
  {
    "path": "packages/route-pattern/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/route-pattern/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"vendor\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/session/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/session/CHANGELOG.md",
    "content": "# `session` CHANGELOG\n\nThis is the changelog for [`session`](https://github.com/remix-run/remix/tree/main/packages/session). It follows [semantic versioning](https://semver.org/).\n\n## v0.4.1 (2025-12-06)\n\n- 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.\n\n## v0.4.0 (2025-11-25)\n\n- Add `Session` class. The `createSession` function now returns an instance of the `Session` class.\n\n  ```ts\n  // You can now create sessions using either approach:\n  import { createSession, Session } from '@remix-run/session'\n\n  // Factory function\n  let session = createSession()\n\n  // Or use the class directly\n  let session = new Session()\n  ```\n\n- BREAKING CHANGE: Rename `createFileSessionStorage` to `createFsSessionStorage` and export from `@remix-run/session/fs-storage`\n\n  ```ts\n  // before\n  import { createFileSessionStorage } from '@remix-run/session/file-storage'\n  let storage = createFileSessionStorage('/tmp/sessions')\n\n  // after\n  import { createFsSessionStorage } from '@remix-run/session/fs-storage'\n  let storage = createFsSessionStorage('/tmp/sessions')\n  ```\n\n## v0.3.0 (2025-11-21)\n\n- BREAKING CHANGE: Rename `createFileStorage` to `createFileSessionStorage`\n- BREAKING CHANGE: Rename `createMemoryStorage` to `createMemorySessionStorage`\n- BREAKING CHANGE: Rename `createCookieStorage` to `createCookieSessionStorage`\n\n## v0.2.1 (2025-11-19)\n\n- 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\n\n## v0.2.0 (2025-11-18)\n\n- BREAKING CHANGE: Remove `Session` class, use `createSession` instead\n- BREAKING CHANGE: Remove class versions of session storage, use the factory functions instead\n\n  ```tsx\n  // before\n  import { FileSessionStorage } from '@remix-run/session/file-storage'\n  let storage = new FileSessionStorage(/* ... */)\n\n  // after\n  import { createFileStorage } from '@remix-run/session/file-storage'\n  let storage = createFileStorage(/* ... */)\n  ```\n\n- Add `session.regenerateId(deleteOldSession?: boolean)` to purge old session data when the session ID is regenerated. This is useful for preventing session fixation attacks.\n\n## v0.1.0 (2025-11-08)\n\nThis is the initial release of `@remix-run/session`.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/session/README.md) for more details.\n"
  },
  {
    "path": "packages/session/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/session/README.md",
    "content": "# session\n\nA 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.\n\n## Features\n\n- **Multiple Storage Strategies:** Includes memory, cookie, and file-based [session storage strategies](#storage-strategies) for different use cases\n- **Flash Messages:** Support for [flash data](#flash-messages) that persists only for the next request\n- **Session Security:** Built-in protection against [session fixation attacks](#regenerating-session-ids)\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nThe following example shows how to use a session to persist data across requests.\n\nThe 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.\n\n```ts\nimport { createCookieSessionStorage } from 'remix/session/cookie-storage'\n\n// Create a session storage. This is used to store session data across requests.\nlet storage = createCookieSessionStorage()\n\n// This function simulates a typical request flow where the session is read from\n// the request cookie, modified, and the new cookie is returned in the response.\nasync function handleRequest(cookie: string | null) {\n  let session = await storage.read(cookie)\n  session.set('count', Number(session.get('count') ?? 0) + 1)\n  return {\n    session, // The session data from this \"request\"\n    cookie: await storage.save(session), // The cookie to use on the next request\n  }\n}\n\nlet response1 = await handleRequest(null)\nassert.equal(response1.session.get('count'), 1)\n\nlet response2 = await handleRequest(response1.cookie)\nassert.equal(response2.session.get('count'), 2)\n\nlet response3 = await handleRequest(response2.cookie)\nassert.equal(response3.session.get('count'), 3)\n```\n\nThe 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.\n\n### Flash Messages\n\nFlash messages are values that persist only for the next request, perfect for displaying one-time notifications:\n\n```ts\nasync function requestIndex(cookie: string | null) {\n  let session = await storage.read(cookie)\n  return { session, cookie: await storage.save(session) }\n}\n\nasync function requestSubmit(cookie: string | null) {\n  let session = await storage.read(cookie)\n  session.flash('message', 'success!')\n  return { session, cookie: await storage.save(session) }\n}\n\n// Flash data is undefined on the first request\nlet response1 = await requestIndex(null)\nassert.equal(response1.session.get('message'), undefined)\n\n// Flash data is undefined on the same request it is set. This response\n// is typically a redirect to a route that displays the flash data.\nlet response2 = await requestSubmit(response1.cookie)\nassert.equal(response2.session.get('message'), undefined)\n\n// Flash data is available on the next request\nlet response3 = await requestIndex(response2.cookie)\nassert.equal(response3.session.get('message'), 'success!')\n\n// Flash data is not available on subsequent requests\nlet response4 = await requestIndex(response3.cookie)\nassert.equal(response4.session.get('message'), undefined)\n```\n\n### Regenerating Session IDs\n\nFor 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.\n\n```ts\nimport { createFsSessionStorage } from 'remix/session/fs-storage'\n\nlet sessionStorage = createFsSessionStorage('/tmp/sessions')\n\nasync function requestIndex(cookie: string | null) {\n  let session = await sessionStorage.read(cookie)\n  return { session, cookie: await sessionStorage.save(session) }\n}\n\nasync function requestLogin(cookie: string | null) {\n  let session = await sessionStorage.read(cookie)\n  session.set('userId', 'mj')\n  session.regenerateId()\n  return { session, cookie: await sessionStorage.save(session) }\n}\n\nlet response1 = await requestIndex(null)\nassert.equal(response1.session.get('userId'), undefined)\n\nlet response2 = await requestLogin(response1.cookie)\nassert.notEqual(response2.session.id, response1.session.id)\n\nlet response3 = await requestIndex(response2.cookie)\nassert.equal(response3.session.get('userId'), 'mj')\n```\n\nTo 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.\n\n### Destroying Sessions\n\nWhen a user logs out, you should destroy the session using `session.destroy()`.\n\nThis 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.\n\n### Storage Strategies\n\nSeveral strategies are provided out of the box for storing session data across requests, depending on your needs.\n\nA 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.\n\n#### Filesystem Storage\n\nFilesystem 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.\n\n```ts\nimport { createFsSessionStorage } from 'remix/session/fs-storage'\n\nlet sessionStorage = createFsSessionStorage('/tmp/sessions')\n```\n\n#### Cookie Storage\n\nCookie 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.\n\nThe 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.\n\n```ts\nimport { createCookieSessionStorage } from 'remix/session/cookie-storage'\n\nlet sessionStorage = createCookieSessionStorage()\n```\n\n#### Memory Storage\n\nMemory 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.\n\n```ts\nimport { createMemorySessionStorage } from 'remix/session/memory-storage'\n\nlet sessionStorage = createMemorySessionStorage()\n```\n\n## Related Packages\n\n- [`@remix-run/cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie) - Cookie parsing and serialization\n- [`@remix-run/fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router with built-in session middleware\n- [`@remix-run/session-storage-memcache`](https://github.com/remix-run/remix/tree/main/packages/session-storage-memcache) - Memcache-backed session storage\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/session/package.json",
    "content": "{\n  \"name\": \"@remix-run/session\",\n  \"version\": \"0.4.1\",\n  \"description\": \"Session management for JavaScript\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/session\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/session#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./cookie-storage\": \"./src/cookie-storage.ts\",\n    \"./fs-storage\": \"./src/fs-storage.ts\",\n    \"./memory-storage\": \"./src/memory-storage.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./cookie-storage\": {\n        \"types\": \"./dist/cookie-storage.d.ts\",\n        \"default\": \"./dist/cookie-storage.js\"\n      },\n      \"./fs-storage\": {\n        \"types\": \"./dist/fs-storage.d.ts\",\n        \"default\": \"./dist/fs-storage.js\"\n      },\n      \"./memory-storage\": {\n        \"types\": \"./dist/memory-storage.d.ts\",\n        \"default\": \"./dist/memory-storage.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"session\",\n    \"session-management\",\n    \"session-storage\",\n    \"fetch\",\n    \"cookie\",\n    \"storage\"\n  ]\n}\n"
  },
  {
    "path": "packages/session/src/cookie-storage.ts",
    "content": "export { createCookieSessionStorage } from './lib/session-storage/cookie.ts'\n"
  },
  {
    "path": "packages/session/src/fs-storage.ts",
    "content": "export { type FsSessionStorageOptions, createFsSessionStorage } from './lib/session-storage/fs.ts'\n"
  },
  {
    "path": "packages/session/src/index.ts",
    "content": "export { Session, createSessionId, createSession } from './lib/session.ts'\nexport type { SessionStorage } from './lib/session-storage.ts'\n"
  },
  {
    "path": "packages/session/src/lib/session-storage/cookie.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it, mock } from 'node:test'\n\nimport { createCookieSessionStorage } from './cookie.ts'\n\ndescribe('cookie session storage', () => {\n  it('persists session data across requests', async () => {\n    let storage = createCookieSessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 3)\n  })\n\n  it('clears session data when the session is destroyed', async () => {\n    let storage = createCookieSessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestDestroy(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.destroy()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestDestroy(response2.cookie)\n    assert.ok(response3.session.destroyed)\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('count'), 1)\n  })\n\n  it('does not set a cookie when session data is not changed', async () => {\n    let storage = createCookieSessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response = await requestIndex()\n    assert.equal(response.session.dirty, false)\n    assert.equal(response.cookie, null)\n  })\n\n  it('makes flash data available only on the next request', async () => {\n    let storage = createCookieSessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestFlash(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.flash('message', 'success!')\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('message'), undefined)\n\n    let response2 = await requestFlash(response1.cookie)\n    assert.equal(response2.session.get('message'), undefined)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('message'), 'success!')\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('message'), undefined)\n  })\n\n  it('leaves old session data in storage by default when the id is regenerated', async () => {\n    let storage = createCookieSessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestLogin(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestLogin(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 2, 'old session data should still be in storage')\n  })\n\n  it('logs a warning when the id is regenerated and the deleteOldSession option is true', async () => {\n    let consoleWarn = mock.method(console, 'warn', () => {})\n\n    let storage = createCookieSessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestLoginAndDeleteOldSession(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId(true)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestLoginAndDeleteOldSession(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    assert.equal(consoleWarn.mock.calls.length, 1)\n    let warning = consoleWarn.mock.calls[0].arguments[0] as string\n    assert.match(\n      warning,\n      /Session ID [\\w-]+ was regenerated, but the old session cannot be deleted when using cookie storage/,\n    )\n\n    consoleWarn.mock.restore()\n  })\n})\n"
  },
  {
    "path": "packages/session/src/lib/session-storage/cookie.ts",
    "content": "import { createSession, type SessionData } from '../session.ts'\nimport type { SessionStorage } from '../session-storage.ts'\n\n/**\n * Creates a session storage that stores all session data in the session cookie itself.\n *\n * Note: This is suitable for use in production. However, the total size of the session cookie is limited\n * to the browser's maximum cookie size, typically 4096 bytes.\n *\n * @returns The session storage\n */\nexport function createCookieSessionStorage(): SessionStorage {\n  return {\n    async read(cookie) {\n      if (cookie) {\n        try {\n          let parsed = JSON.parse(cookie) as { i: string; d: SessionData }\n          return createSession(parsed.i, parsed.d)\n        } catch {\n          // Invalid JSON, fall through to create new session\n        }\n      }\n\n      return createSession()\n    },\n    async save(session) {\n      if (session.deleteId) {\n        console.warn(\n          `Session ID ${session.deleteId} was regenerated, but the old session cannot ` +\n            'be deleted when using cookie storage',\n        )\n      }\n\n      if (session.destroyed) {\n        return ''\n      }\n      if (session.dirty) {\n        return JSON.stringify({ i: session.id, d: session.data })\n      }\n\n      return null\n    },\n  }\n}\n"
  },
  {
    "path": "packages/session/src/lib/session-storage/fs.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { afterEach, beforeEach, describe, it } from 'node:test'\nimport * as fsp from 'node:fs/promises'\nimport * as path from 'node:path'\nimport * as os from 'node:os'\n\nimport { createFsSessionStorage } from './fs.ts'\n\ndescribe('fs session storage', () => {\n  let tmpDir: string\n  beforeEach(async () => {\n    tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'file-session-storage-test-'))\n  })\n\n  afterEach(async () => {\n    await fsp.rm(tmpDir, { recursive: true, force: true })\n  })\n\n  it('does not use unknown session IDs by default', async () => {\n    let storage = createFsSessionStorage(tmpDir)\n    let session = await storage.read('unknown')\n    assert.notEqual(session.id, 'unknown')\n  })\n\n  it('uses unknown session IDs if enabled', async () => {\n    let storage = createFsSessionStorage(tmpDir, { useUnknownIds: true })\n    let session = await storage.read('unknown')\n    assert.equal(session.id, 'unknown')\n  })\n\n  it('persists session data across requests', async () => {\n    let storage = createFsSessionStorage(tmpDir)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 3)\n  })\n\n  it('clears session data when the session is destroyed', async () => {\n    let storage = createFsSessionStorage(tmpDir)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestDestroy(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.destroy()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestDestroy(response2.cookie)\n    assert.ok(response3.session.destroyed)\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('count'), 1)\n  })\n\n  it('does not set a cookie when session data is not changed', async () => {\n    let storage = createFsSessionStorage(tmpDir)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response = await requestIndex()\n    assert.equal(response.session.dirty, false)\n    assert.equal(response.cookie, null)\n  })\n\n  it('makes flash data available only on the next request', async () => {\n    let storage = createFsSessionStorage(tmpDir)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestFlash(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.flash('message', 'success!')\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('message'), undefined)\n\n    let response2 = await requestFlash(response1.cookie)\n    assert.equal(response2.session.get('message'), undefined)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('message'), 'success!')\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('message'), undefined)\n  })\n\n  it('leaves old session data in storage by default when the id is regenerated', async () => {\n    let storage = createFsSessionStorage(tmpDir)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', ((session.get('count') as number | undefined) ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestLogin(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestLogin(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 2, 'old session should still be in storage')\n  })\n\n  it('deletes old session data when the id is regenerated and the deleteOldSession option is true', async () => {\n    let storage = createFsSessionStorage(tmpDir)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestLoginAndDeleteOldSession(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId(true)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestLoginAndDeleteOldSession(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 1, 'old session should be deleted')\n  })\n\n  it('throws error if session directory is a file', async () => {\n    let filePath = path.join(tmpDir, 'not-a-directory')\n    await fsp.writeFile(filePath, 'I am a file, not a directory.')\n\n    assert.throws(\n      () => createFsSessionStorage(filePath),\n      new Error(`Path \"${filePath}\" is not a directory`),\n    )\n  })\n})\n"
  },
  {
    "path": "packages/session/src/lib/session-storage/fs.ts",
    "content": "import * as fs from 'node:fs'\nimport * as fsp from 'node:fs/promises'\nimport * as path from 'node:path'\n\nimport { createSession, type SessionData } from '../session.ts'\nimport type { SessionStorage } from '../session-storage.ts'\n\n/**\n * Options for filesystem-backed session storage.\n */\nexport interface FsSessionStorageOptions {\n  /**\n   * Whether to reuse session IDs sent from the client that are not found in storage.\n   * Default is `false`.\n   */\n  useUnknownIds?: boolean\n}\n\n/**\n * Creates a session storage that stores all session data in a filesystem directory using\n * Node's fs module.\n *\n * Note: No attempt is made to avoid overwriting existing files, so the directory used should\n * be a new directory solely dedicated to this storage object.\n *\n * @param directory The directory to store the session files in\n * @param options (optional) The options for the session storage\n * @returns The session storage\n */\nexport function createFsSessionStorage(\n  directory: string,\n  options?: FsSessionStorageOptions,\n): SessionStorage {\n  let root = path.resolve(directory)\n  let useUnknownIds = options?.useUnknownIds ?? false\n\n  try {\n    let stats = fs.statSync(root)\n    if (!stats.isDirectory()) {\n      throw new Error(`Path \"${root}\" is not a directory`)\n    }\n  } catch (error) {\n    if (!isNoEntityError(error)) {\n      throw error\n    }\n\n    fs.mkdirSync(root, { recursive: true })\n  }\n\n  async function getFilePath(id: string): Promise<string> {\n    let hash = await computeHash(id)\n    let subdir = hash.slice(0, 2)\n    let filename = hash.slice(2)\n    return path.join(root, subdir, filename)\n  }\n\n  async function deleteFile(id: string): Promise<void> {\n    try {\n      await fsp.unlink(await getFilePath(id))\n    } catch (error) {\n      if (!isNoEntityError(error)) {\n        throw error\n      }\n    }\n  }\n\n  return {\n    async read(cookie) {\n      let id = cookie\n\n      if (id) {\n        try {\n          let file = await getFilePath(id)\n          let content = await fsp.readFile(file, 'utf-8')\n          let data = JSON.parse(content) as SessionData\n          return createSession(id, data)\n        } catch (error) {\n          if (!isNoEntityError(error)) {\n            throw error\n          }\n          // File doesn't exist, fall through to create new session\n        }\n      }\n\n      return createSession(useUnknownIds && id ? id : undefined)\n    },\n    async save(session) {\n      if (session.deleteId) {\n        await deleteFile(session.deleteId)\n      }\n\n      if (session.destroyed) {\n        await deleteFile(session.id)\n        return ''\n      }\n      if (session.dirty) {\n        let file = await getFilePath(session.id)\n        await fsp.mkdir(path.dirname(file), { recursive: true })\n        await fsp.writeFile(file, JSON.stringify(session.data), 'utf-8')\n        return session.id\n      }\n\n      return null\n    },\n  }\n}\n\nfunction isNoEntityError(error: unknown): error is NodeJS.ErrnoException & { code: 'ENOENT' } {\n  return (\n    error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT'\n  )\n}\n\nasync function computeHash(id: string, algorithm = 'SHA-256'): Promise<string> {\n  let encoder = new TextEncoder()\n  let data = encoder.encode(id)\n  let hashBuffer = await crypto.subtle.digest(algorithm, data)\n  let hashArray = Array.from(new Uint8Array(hashBuffer))\n  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')\n}\n"
  },
  {
    "path": "packages/session/src/lib/session-storage/memory.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createMemorySessionStorage } from './memory.ts'\n\ndescribe('memory session storage', () => {\n  it('does not use unknown session IDs by default', async () => {\n    let storage = createMemorySessionStorage()\n    let session = await storage.read('unknown')\n    assert.notEqual(session.id, 'unknown')\n  })\n\n  it('uses unknown session IDs if enabled', async () => {\n    let storage = createMemorySessionStorage({ useUnknownIds: true })\n    let session = await storage.read('unknown')\n    assert.equal(session.id, 'unknown')\n  })\n\n  it('persists session data across requests', async () => {\n    let storage = createMemorySessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 3)\n  })\n\n  it('clears session data when the session is destroyed', async () => {\n    let storage = createMemorySessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestDestroy(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.destroy()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestDestroy(response2.cookie)\n    assert.ok(response3.session.destroyed)\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('count'), 1)\n    assert.notEqual(response4.session.id, response3.session.id)\n  })\n\n  it('does not set a cookie when session data is not changed', async () => {\n    let storage = createMemorySessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response = await requestIndex()\n    assert.equal(response.session.dirty, false)\n    assert.equal(response.cookie, null)\n  })\n\n  it('makes flash data available only on the next request', async () => {\n    let storage = createMemorySessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestFlash(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.flash('message', 'success!')\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('message'), undefined)\n\n    let response2 = await requestFlash(response1.cookie)\n    assert.equal(response2.session.get('message'), undefined)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('message'), 'success!')\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('message'), undefined)\n  })\n\n  it('leaves old session data in storage by default when the id is regenerated', async () => {\n    let storage = createMemorySessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestLogin(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestLogin(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 2, 'old session data should still be in storage')\n  })\n\n  it('deletes old session data when the id is regenerated and the deleteOldSession option is true', async () => {\n    let storage = createMemorySessionStorage()\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestLoginAndDeleteOldSession(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId(true)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestLoginAndDeleteOldSession(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 1, 'old session data should be deleted')\n  })\n})\n"
  },
  {
    "path": "packages/session/src/lib/session-storage/memory.ts",
    "content": "import { createSession, type SessionData } from '../session.ts'\nimport type { SessionStorage } from '../session-storage.ts'\n\nexport interface MemorySessionStorageOptions {\n  /**\n   * Whether to reuse session IDs sent from the client that are not found in storage.\n   * Default is `false`.\n   */\n  useUnknownIds?: boolean\n}\n\n/**\n * Creates a session storage that stores all session data in memory.\n *\n * Note: This is useful for testing and development. All session data is lost when the\n * server restarts.\n *\n * @param options The options for the session storage\n * @returns The session storage\n */\nexport function createMemorySessionStorage(options?: MemorySessionStorageOptions): SessionStorage {\n  let useUnknownIds = options?.useUnknownIds ?? false\n  let map = new Map<string, SessionData>()\n\n  return {\n    async read(cookie) {\n      let id = cookie\n\n      if (id == null) {\n        return createSession()\n      }\n      if (id !== '' && map.has(id)) {\n        return createSession(id, map.get(id))\n      }\n\n      return createSession(useUnknownIds && id !== '' ? id : undefined)\n    },\n    async save(session) {\n      if (session.deleteId) {\n        map.delete(session.deleteId)\n      }\n\n      if (session.destroyed) {\n        map.delete(session.id)\n        return ''\n      }\n      if (session.dirty) {\n        map.set(session.id, session.data)\n        return session.id\n      }\n\n      return null\n    },\n  }\n}\n"
  },
  {
    "path": "packages/session/src/lib/session-storage.ts",
    "content": "import type { Session } from './session.ts'\n\n/**\n * An interface for storing and retrieving session data.\n */\nexport interface SessionStorage {\n  /**\n   * Retrieve a new session from storage based on the session cookie.\n   *\n   * @param cookie The session cookie value, or `null` if no session cookie is available\n   * @returns The session\n   */\n  read(cookie: string | null): Promise<Session>\n  /**\n   * Save session data in storage and return the session cookie.\n   *\n   * Note: If no session cookie should be set, this method returns `null`.\n   *\n   * @param session The session to save\n   * @returns The session cookie value, or `null` if no session cookie should be set\n   */\n  save(session: Session): Promise<string | null>\n}\n"
  },
  {
    "path": "packages/session/src/lib/session.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createSession } from './session.ts'\n\ndescribe('Session', () => {\n  it('creates a new session', () => {\n    let session = createSession()\n    assert.ok(session)\n    assert.ok(session.id)\n    assert.equal(session.destroyed, false)\n    assert.equal(session.dirty, false)\n    assert.equal(session.size, 0)\n  })\n\n  it('creates a new session with a custom ID', () => {\n    let session = createSession('custom-id')\n    assert.equal(session.id, 'custom-id')\n  })\n\n  it('creates a new session with initial data', () => {\n    let session = createSession(undefined, [{ hello: 'world' }, {}])\n    assert.equal(session.size, 1)\n    assert.equal(session.get('hello'), 'world')\n    assert.equal(session.dirty, false)\n  })\n\n  it('creates a new session with initial flash data', () => {\n    let session = createSession(undefined, [{}, { hello: 'world' }])\n    assert.equal(session.get('hello'), 'world')\n    assert.equal(session.dirty, true)\n  })\n\n  it('sets and gets values', () => {\n    let session = createSession()\n    assert.equal(session.has('hello'), false)\n    assert.equal(session.get('hello'), undefined)\n    assert.equal(session.dirty, false)\n\n    session.set('hello', 'world')\n    assert.equal(session.has('hello'), true)\n    assert.equal(session.get('hello'), 'world')\n    assert.equal(session.dirty, true)\n\n    session.unset('hello')\n    assert.equal(session.has('hello'), false)\n    assert.equal(session.get('hello'), undefined)\n    assert.equal(session.dirty, true)\n  })\n\n  it('sets and gets values with complex types', () => {\n    let session = createSession(undefined, [{ user: { id: 123, name: 'alice' } }, {}])\n\n    assert.deepEqual(session.get('user'), { id: 123, name: 'alice' })\n\n    session.set('user', { id: 456, name: 'bob' })\n    assert.deepEqual(session.get('user'), { id: 456, name: 'bob' })\n\n    // @ts-expect-error - should not allow different type for user\n    session.set('user', { id: 456, name: 'bob', roles: ['admin'] })\n  })\n\n  it('unsets values when set to null', () => {\n    let session = createSession()\n    session.set('hello', 'world')\n    assert.equal(session.has('hello'), true)\n    assert.equal(session.get('hello'), 'world')\n    assert.equal(session.dirty, true)\n\n    session.set('hello', null)\n    assert.equal(session.has('hello'), false)\n    assert.equal(session.get('hello'), undefined)\n    assert.equal(session.dirty, true)\n  })\n\n  it('unsets values when set to undefined', () => {\n    let session = createSession()\n    session.set('hello', 'world')\n    assert.equal(session.has('hello'), true)\n    assert.equal(session.get('hello'), 'world')\n    assert.equal(session.dirty, true)\n\n    session.set('hello', undefined)\n    assert.equal(session.has('hello'), false)\n    assert.equal(session.get('hello'), undefined)\n    assert.equal(session.dirty, true)\n  })\n\n  it('regenerates the session ID', () => {\n    let session = createSession()\n    let originalId = session.id\n    assert.equal(session.dirty, false)\n\n    session.regenerateId()\n    assert.notEqual(session.id, originalId)\n    assert.equal(session.deleteId, undefined)\n    assert.equal(session.dirty, true)\n  })\n\n  it('deletes the old session ID', () => {\n    let session = createSession()\n    let originalId = session.id\n    assert.equal(session.dirty, false)\n\n    session.regenerateId(true)\n    assert.notEqual(session.id, originalId)\n    assert.equal(session.deleteId, originalId)\n    assert.equal(session.dirty, true)\n  })\n\n  it('destroys the session', () => {\n    let session = createSession()\n    assert.equal(session.destroyed, false)\n    session.destroy()\n    assert.equal(session.destroyed, true)\n  })\n\n  it('deletes the original session when the session id is regenerated more than once', () => {\n    let session = createSession()\n    let originalId = session.id\n\n    session.regenerateId(true)\n    assert.notEqual(session.id, originalId)\n    assert.equal(session.deleteId, originalId)\n\n    session.regenerateId(true)\n    assert.notEqual(session.id, originalId)\n    assert.equal(session.deleteId, originalId)\n  })\n\n  describe('a destroyed session', () => {\n    it('flash() throws an error', () => {\n      let session = createSession()\n      session.destroy()\n      assert.throws(() => session.flash('hello', 'world'), new Error('Session has been destroyed'))\n    })\n\n    it('regenerateId() throws an error', () => {\n      let session = createSession()\n      session.destroy()\n      assert.throws(() => session.regenerateId(), new Error('Session has been destroyed'))\n    })\n\n    it('set() throws an error', () => {\n      let session = createSession()\n      session.destroy()\n      assert.throws(() => session.set('hello', 'world'), new Error('Session has been destroyed'))\n    })\n\n    it('unset() throws an error', () => {\n      let session = createSession()\n      session.destroy()\n      assert.throws(() => session.unset('hello'), new Error('Session has been destroyed'))\n    })\n\n    it('get() returns undefined', () => {\n      let session = createSession()\n      session.set('hello', 'world')\n      assert.equal(session.get('hello'), 'world')\n      session.destroy()\n      assert.equal(session.get('hello'), undefined)\n    })\n\n    it('has() returns false', () => {\n      let session = createSession()\n      session.set('hello', 'world')\n      assert.equal(session.has('hello'), true)\n      session.destroy()\n      assert.equal(session.has('hello'), false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/session/src/lib/session.ts",
    "content": "type Data = Record<string, unknown>\n\nexport type SessionData<valueData extends Data = Data, flashData extends Data = Data> = [\n  valueData,\n  flashData,\n]\n\n/**\n * A session persists data for a specific user across multiple requests to a server.\n */\nexport class Session<valueData extends Data = Data, flashData extends Data = Data> {\n  #originalId: string\n  #currentId: string\n  #deleteId: string | undefined = undefined\n  #valueMap: Map<keyof valueData, valueData[keyof valueData]>\n  #flashMap: Map<keyof flashData, flashData[keyof flashData]>\n  #nextMap: Map<keyof flashData, flashData[keyof flashData]>\n  #destroyed = false\n  #dirty: boolean\n\n  /**\n   * @param id The session ID\n   * @param initialData The initial session data\n   */\n  constructor(id = createSessionId(), initialData?: SessionData<valueData, flashData>) {\n    this.#originalId = id\n    this.#currentId = id\n    this.#valueMap = toMap(initialData?.[0])\n    this.#flashMap = toMap(initialData?.[1])\n    this.#nextMap = new Map<keyof flashData, flashData[keyof flashData]>()\n    // Mark as dirty if flash data exists so it gets cleared on save\n    this.#dirty = this.#flashMap.size > 0\n  }\n\n  #checkDestroyed() {\n    if (this.#destroyed) throw new Error('Session has been destroyed')\n  }\n\n  /**\n   * The raw session data in a format suitable for storage.\n   *\n   * Note: Do not use this for normal reading of session data. Use the `get` method instead.\n   */\n  get data(): SessionData<valueData, flashData> {\n    return (\n      this.#destroyed\n        ? [{}, {}]\n        : [Object.fromEntries(this.#valueMap), Object.fromEntries(this.#nextMap)]\n    ) as SessionData<valueData, flashData>\n  }\n\n  /**\n   * The session ID that will be deleted when the session is saved. This is set to the original\n   * session ID when the session ID is regenerated with the `deleteOldSession` option.\n   */\n  get deleteId(): string | undefined {\n    return this.#deleteId\n  }\n\n  /**\n   * Mark this session as destroyed.\n   *\n   * This prevents all further modifications to the session.\n   */\n  destroy(): void {\n    this.#destroyed = true\n  }\n\n  /**\n   * Whether this session has been destroyed.\n   */\n  get destroyed(): boolean {\n    return this.#destroyed\n  }\n\n  /**\n   * Whether this session has been modified since it was created.\n   */\n  get dirty(): boolean {\n    return this.#dirty\n  }\n\n  /**\n   * Set a value in the session that will be available only during the next request.\n   * @param key The key of the value to flash\n   * @param value The value to flash\n   */\n  flash<key extends keyof flashData>(key: key, value: flashData[key]): void {\n    this.#checkDestroyed()\n    this.#nextMap.set(key, value)\n    this.#dirty = true\n  }\n\n  /**\n   * Get a value from the session.\n   *\n   * @param key The key of the value to get\n   * @returns The value for the given key\n   */\n  get<key extends keyof valueData>(key: key): valueData[key] | undefined\n  get<key extends keyof flashData>(key: key): flashData[key] | undefined\n  get(key: string): undefined\n  get(key: string) {\n    if (this.#destroyed) return undefined as any\n    return this.#valueMap.get(key as any) ?? this.#flashMap.get(key as any)\n  }\n\n  /**\n   * Check if a value is stored for the given key.\n   *\n   * @param key The key to check\n   * @returns `true` if a value is stored for the given key, `false` otherwise\n   */\n  has(key: keyof valueData | keyof flashData): boolean {\n    if (this.#destroyed) return false\n    return this.#valueMap.has(key as any) || this.#flashMap.has(key as any)\n  }\n\n  /**\n   * The unique identifier for this session.\n   */\n  get id(): string {\n    return this.#currentId\n  }\n\n  /**\n   * Regenerate the session ID while preserving the session data. This should be called after login\n   * or other privilege changes.\n   *\n   * @param deleteOldSession Whether to delete the old session data when the session is saved (default: `false`)\n   */\n  regenerateId(deleteOldSession = false): void {\n    this.#checkDestroyed()\n    if (deleteOldSession) this.#deleteId = this.#originalId\n    this.#currentId = createSessionId()\n    this.#dirty = true\n  }\n\n  /**\n   * Set a value in the session.\n   * @param key The key of the value to set\n   * @param value The value to set\n   */\n  set<key extends keyof valueData>(key: key, value: valueData[key]): void {\n    this.#checkDestroyed()\n    if (value == null) {\n      this.#valueMap.delete(key as any)\n    } else {\n      this.#valueMap.set(key as any, value)\n    }\n    this.#dirty = true\n  }\n\n  /**\n   * The number of key/value pairs in the session.\n   */\n  get size(): number {\n    if (this.#destroyed) return 0\n    return this.#valueMap.size + this.#flashMap.size\n  }\n\n  /**\n   * Remove a value from the session.\n   * @param key The key of the value to remove\n   */\n  unset(key: keyof valueData): void {\n    this.#checkDestroyed()\n    this.#valueMap.delete(key as any)\n    this.#dirty = true\n  }\n}\n\nfunction toMap<data extends Data>(data?: data): Map<keyof data, data[keyof data]> {\n  if (!data) return new Map()\n  return new Map(Object.entries(data) as [keyof data, data[keyof data]][])\n}\n\n/**\n * Create a new session.\n *\n * @param id The ID of the session\n * @param initialData The initial data for the session\n * @returns The new session\n */\nexport function createSession<valueData extends Data = Data, flashData extends Data = Data>(\n  id = createSessionId(),\n  initialData?: SessionData<valueData, flashData>,\n): Session<valueData, flashData> {\n  return new Session(id, initialData)\n}\n\n/**\n * Create a new cryptographically secure session ID.\n *\n * @returns A new session ID\n */\nexport function createSessionId(): string {\n  return crypto.randomUUID()\n}\n"
  },
  {
    "path": "packages/session/src/memory-storage.ts",
    "content": "export { createMemorySessionStorage } from './lib/session-storage/memory.ts'\n"
  },
  {
    "path": "packages/session/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/session/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/session-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/session-middleware/.changes/minor.session-context-key.md",
    "content": "BREAKING CHANGE: Session middleware no longer reads/writes `context.session`.\n\nSession state is now stored on request context using the `Session` class itself as the context key and accessed with `context.get(Session)`.\n"
  },
  {
    "path": "packages/session-middleware/CHANGELOG.md",
    "content": "# `session-middleware` CHANGELOG\n\nThis is the changelog for [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware). It follows [semantic versioning](https://semver.org/).\n\n## v0.1.4\n\n### Patch Changes\n\n- Ensure response is mutable before modifying.\n\n- Bumped `@remix-run/*` dependencies:\n  - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0)\n\n## v0.1.3\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0)\n\n## v0.1.2\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.1.1 (2025-12-06)\n\n- Use `response.headers.append('Set-Cookie', ...)` instead of `response.headers.set('Set-Cookie', ...)` to not overwrite cookies set by other middleware/handlers\n\n## v0.1.0 (2025-11-19)\n\nInitial release extracted from `@remix-run/fetch-router` v0.9.0.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/session-middleware/README.md) for more details.\n"
  },
  {
    "path": "packages/session-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/session-middleware/README.md",
    "content": "# session-middleware\n\nSession middleware for Remix using signed cookies. It loads session state from incoming requests, stores it in request context using `Session`, and persists updates automatically.\n\n## Features\n\n- **Session Lifecycle Handling** - Reads and saves session state per request\n- **Context Integration** - Exposes session APIs directly on request context\n- **Secure Cookie Support** - Designed for signed session cookies\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { createCookie } from 'remix/cookie'\nimport { Session } from 'remix/session'\nimport { createCookieSessionStorage } from 'remix/session/cookie-storage'\nimport { session } from 'remix/session-middleware'\n\nlet sessionCookie = createCookie('__session', {\n  secrets: ['s3cr3t'], // session cookies must be signed!\n  httpOnly: true,\n  secure: true,\n  sameSite: 'lax',\n})\n\nlet sessionStorage = createCookieSessionStorage()\n\nlet router = createRouter({\n  middleware: [session(sessionCookie, sessionStorage)],\n})\n\nrouter.get('/', (context) => {\n  let session = context.get(Session)\n  session.set('count', Number(session.get('count') ?? 0) + 1)\n  return new Response(`Count: ${session.get('count')}`)\n})\n```\n\nThe middleware:\n\n- Reads the session from the cookie on incoming requests\n- Makes it available as `context.get(Session)`\n- Automatically saves session changes and sets the cookie on responses\n\nNote: The session cookie must be signed for security. This prevents tampering with the session data on the client.\n\n### Login/Logout Flow\n\nA basic login/logout flow could look like this:\n\n```ts\nimport * as res from 'remix/fetch-router/response-helpers'\nimport { Session } from 'remix/session'\n\nrouter.get('/login', ({ get }) => {\n  let session = get(Session)\n  let error = session.get('error')\n  return res.html(`\n    <html>\n      <body>\n        <h1>Login</h1>\n        ${typeof error === 'string' ? <div class=\"error\">${error}</div> : null}\n        <form method=\"POST\" action=\"/login\">\n          <input type=\"text\" name=\"username\" placeholder=\"Username\" />\n          <input type=\"password\" name=\"password\" placeholder=\"Password\" />\n          <button type=\"submit\">Login</button>\n        </form>\n      </body>\n    </html>\n  `)\n})\n\nrouter.post('/login', ({ get }) => {\n  let session = get(Session)\n  let formData = get(FormData)\n  let username = formData.get('username')\n  let password = formData.get('password')\n\n  let user = authenticateUser(username, password)\n  if (!user) {\n    session.flash('error', 'Invalid username or password')\n    return res.redirect('/login')\n  }\n\n  session.regenerateId()\n  session.set('userId', user.id)\n\n  return res.redirect('/dashboard')\n})\n\nrouter.post('/logout', ({ get }) => {\n  let session = get(Session)\n  session.destroy()\n  return res.redirect('/')\n})\n```\n\n## Related Packages\n\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n- [`session`](https://github.com/remix-run/remix/tree/main/packages/session) - Session management and storage\n- [`cookie`](https://github.com/remix-run/remix/tree/main/packages/cookie) - Cookie parsing and serialization\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/session-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/session-middleware\",\n  \"version\": \"0.1.4\",\n  \"description\": \"Middleware for managing sessions with cookie-based storage\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/session-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/session-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/cookie\": \"workspace:*\",\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/headers\": \"workspace:*\",\n    \"@remix-run/session\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/cookie\": \"workspace:^\",\n    \"@remix-run/fetch-router\": \"workspace:^\",\n    \"@remix-run/session\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"session\",\n    \"cookie\",\n    \"session-management\"\n  ]\n}\n"
  },
  {
    "path": "packages/session-middleware/src/index.ts",
    "content": "export { session } from './lib/session.ts'\n"
  },
  {
    "path": "packages/session-middleware/src/lib/session.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createCookie } from '@remix-run/cookie'\nimport { SetCookie } from '@remix-run/headers'\nimport { createSession, Session } from '@remix-run/session'\nimport { createCookieSessionStorage } from '@remix-run/session/cookie-storage'\nimport { createRouter } from '@remix-run/fetch-router'\n\nimport { session as sessionMiddleware } from './session.ts'\n\n// Create a new request using the cookie in the given response\nfunction createRequest(fromResponse?: Response): Request {\n  let headers = new Headers()\n  if (fromResponse) {\n    let setCookie = fromResponse.headers.getSetCookie()\n    if (setCookie.length > 0) {\n      let cookie = new SetCookie(setCookie[0])\n      headers.append('Cookie', `${cookie.name}=${cookie.value}`)\n    }\n  }\n  return new Request('https://remix.run', { headers })\n}\n\ndescribe('session middleware', () => {\n  it('persists session data across requests', async () => {\n    let cookie = createCookie('__sess', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [sessionMiddleware(cookie, storage)],\n    })\n\n    router.map('/', ({ get }) => {\n      let session = get(Session)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return new Response(`Count: ${session.get('count')}`)\n    })\n\n    let response1 = await router.fetch('https://remix.run')\n    assert.equal(await response1.text(), 'Count: 1')\n\n    let response2 = await router.fetch(createRequest(response1))\n    assert.equal(await response2.text(), 'Count: 2')\n\n    let response3 = await router.fetch(createRequest(response2))\n    assert.equal(await response3.text(), 'Count: 3')\n  })\n\n  it('allows for direct return of fetch() call', async () => {\n    let cookie = createCookie('__sess', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [sessionMiddleware(cookie, storage)],\n    })\n\n    router.map('/', ({ get }) => {\n      let session = get(Session)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return fetch('http://example.com')\n    })\n\n    let response = await router.fetch('https://remix.run')\n\n    assert.equal(response.headers.get('Set-Cookie')?.length != null, true)\n  })\n\n  it('throws if the session cookie is not signed', async () => {\n    let cookie = createCookie('__sess', { secrets: [] })\n    let storage = createCookieSessionStorage()\n\n    assert.throws(() => {\n      sessionMiddleware(cookie, storage)\n    }, new Error('Session cookie must be signed'))\n  })\n\n  it('throws at request time if the session is already started', async () => {\n    let cookie = createCookie('__sess', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [\n        sessionMiddleware(cookie, storage),\n        // The second session middleware should throw an error\n        sessionMiddleware(cookie, storage),\n      ],\n    })\n\n    router.map('/', () => new Response('Home'))\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run')\n    }, new Error('Existing session found, refusing to overwrite'))\n  })\n\n  it('throws at request time if the session is modified by another middleware/handler', async () => {\n    let cookie = createCookie('__sess', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [sessionMiddleware(cookie, storage)],\n    })\n\n    router.map('/', (context) => {\n      context.set(Session, createSession())\n      return new Response('Home')\n    })\n\n    await assert.rejects(async () => {\n      await router.fetch('https://remix.run')\n    }, new Error('Cannot save session that was initialized by another middleware/handler'))\n  })\n\n  it('does not overwrite cookies set by other middleware/handlers', async () => {\n    let cookie = createCookie('__sess', { secrets: ['secret1'] })\n    let storage = createCookieSessionStorage()\n\n    let router = createRouter({\n      middleware: [sessionMiddleware(cookie, storage)],\n    })\n\n    router.map('/', ({ get }) => {\n      let session = get(Session)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return new Response(`Count: ${session.get('count')}`, {\n        headers: {\n          'Set-Cookie': `count=${session.get('count')}`,\n        },\n      })\n    })\n\n    let response = await router.fetch('https://remix.run')\n\n    let setCookie = response.headers.getSetCookie()\n    assert.equal(setCookie.length, 2)\n  })\n})\n"
  },
  {
    "path": "packages/session-middleware/src/lib/session.ts",
    "content": "import type { Cookie } from '@remix-run/cookie'\nimport type { Middleware } from '@remix-run/fetch-router'\nimport { Session, type SessionStorage } from '@remix-run/session'\n\n/**\n * Middleware that manages request session state on request context.\n *\n * @param sessionCookie The session cookie to use\n * @param sessionStorage The storage backend for session data\n * @returns The session middleware\n */\nexport function session(sessionCookie: Cookie, sessionStorage: SessionStorage): Middleware {\n  if (!sessionCookie.signed) {\n    throw new Error('Session cookie must be signed')\n  }\n\n  return async (context, next) => {\n    if (context.has(Session)) {\n      throw new Error('Existing session found, refusing to overwrite')\n    }\n\n    let cookieValue = await sessionCookie.parse(context.headers.get('Cookie'))\n    let session = await sessionStorage.read(cookieValue)\n\n    context.set(Session, session)\n\n    let response = await next()\n\n    if (session !== context.get(Session)) {\n      throw new Error('Cannot save session that was initialized by another middleware/handler')\n    }\n\n    let setCookieValue = await sessionStorage.save(session)\n    if (setCookieValue != null) {\n      // make sure the response is mutable\n      response = new Response(response.body, response)\n      response.headers.append('Set-Cookie', await sessionCookie.serialize(setCookieValue))\n    }\n\n    return response\n  }\n}\n"
  },
  {
    "path": "packages/session-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/session-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/session-storage-memcache/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/session-storage-memcache/CHANGELOG.md",
    "content": "# `session-storage-memcache` CHANGELOG\n\nThis 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/).\n\n## v0.1.0\n\n### Minor Changes\n\n- Add Memcache session storage with `createMemcacheSessionStorage(server, options)`.\n\n  This adds a Node.js Memcache backend with support for `useUnknownIds`, `keyPrefix`, and `ttlSeconds`, along with integration tests that run against Memcached in CI.\n\n## Unreleased\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/session-storage-memcache/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/session-storage-memcache/README.md",
    "content": "# session-storage-memcache\n\nMemcache session storage for [`@remix-run/session`](https://github.com/remix-run/remix/tree/main/packages/session).\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\n```ts\nimport { createMemcacheSessionStorage } from 'remix/session-storage-memcache'\n\nlet sessionStorage = createMemcacheSessionStorage('127.0.0.1:11211', {\n  keyPrefix: 'my-app:session:',\n  ttlSeconds: 60 * 60 * 24 * 7,\n})\n```\n\nAvailable options:\n\n- `useUnknownIds` (default: `false`) - reuse unknown session IDs sent by the client\n- `keyPrefix` (default: `'remix:session:'`) - prefix for all Memcache keys\n- `ttlSeconds` (default: `0`) - session expiration in seconds (`0` means no expiration)\n\nNote: Memcache storage uses TCP sockets and requires a Node.js runtime.\n\n## Related Packages\n\n- [`@remix-run/session`](https://github.com/remix-run/remix/tree/main/packages/session) - Core session primitives and storage interface\n- [`@remix-run/session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) - Middleware for wiring session storage into request handling\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/session-storage-memcache/package.json",
    "content": "{\n  \"name\": \"@remix-run/session-storage-memcache\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Memcache session storage for remix/session\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/session-storage-memcache\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/session-storage-memcache#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"dependencies\": {\n    \"@remix-run/session\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"session\",\n    \"session-management\",\n    \"session-storage\",\n    \"memcache\"\n  ]\n}\n"
  },
  {
    "path": "packages/session-storage-memcache/src/index.ts",
    "content": "export {\n  type MemcacheSessionStorageOptions,\n  createMemcacheSessionStorage,\n} from './lib/memcache-storage.ts'\n"
  },
  {
    "path": "packages/session-storage-memcache/src/lib/memcache-client.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport * as net from 'node:net'\nimport { afterEach, beforeEach, describe, it } from 'node:test'\n\nimport { createMemcacheClient } from './memcache-client.ts'\n\ntype FakeMemcacheServer = {\n  address: string\n  close: () => Promise<void>\n}\n\ntype FakeMemcacheServerOptions = {\n  malformedGetResponse?: string\n  getResponseKey?: string\n  setResponse?: string\n  deleteResponse?: string\n}\n\ndescribe('memcache client', () => {\n  let server: FakeMemcacheServer\n\n  beforeEach(async () => {\n    server = await startFakeMemcacheServer()\n  })\n\n  afterEach(async () => {\n    await server.close()\n  })\n\n  it('reads and writes values', async () => {\n    let client = createMemcacheClient(server.address)\n\n    await client.set('test-key', 'hello', 0)\n    let value = await client.get('test-key')\n\n    assert.equal(value, 'hello')\n  })\n\n  it('returns null for unknown keys', async () => {\n    let client = createMemcacheClient(server.address)\n    let value = await client.get('missing-key')\n    assert.equal(value, null)\n  })\n\n  it('deletes keys', async () => {\n    let client = createMemcacheClient(server.address)\n\n    await client.set('test-key', 'hello', 0)\n    await client.delete('test-key')\n    let value = await client.get('test-key')\n\n    assert.equal(value, null)\n  })\n\n  it('treats missing keys as a successful delete', async () => {\n    let client = createMemcacheClient(server.address)\n    await client.delete('missing-key')\n  })\n\n  it('throws for invalid server addresses', () => {\n    assert.throws(() => createMemcacheClient('127.0.0.1/path'), /Expected format \"host:port\"/)\n  })\n\n  it('throws when memcache returns an invalid get response', async () => {\n    await server.close()\n    server = await startFakeMemcacheServer({ malformedGetResponse: 'BROKEN\\r\\n' })\n\n    let client = createMemcacheClient(server.address)\n\n    await assert.rejects(() => client.get('test-key'), /Invalid Memcache get response: BROKEN/)\n  })\n\n  it('throws when get returns data for an unexpected key', async () => {\n    await server.close()\n    server = await startFakeMemcacheServer({ getResponseKey: 'different-key' })\n\n    let client = createMemcacheClient(server.address)\n    await client.set('test-key', 'hello', 0)\n\n    await assert.rejects(\n      () => client.get('test-key'),\n      /Memcache returned value for unexpected key: different-key/,\n    )\n  })\n\n  it('throws when set fails', async () => {\n    await server.close()\n    server = await startFakeMemcacheServer({ setResponse: 'NOT_STORED' })\n\n    let client = createMemcacheClient(server.address)\n\n    await assert.rejects(\n      () => client.set('test-key', 'hello', 0),\n      /Memcache set failed: NOT_STORED/,\n    )\n  })\n\n  it('throws when delete fails', async () => {\n    await server.close()\n    server = await startFakeMemcacheServer({ deleteResponse: 'ERROR' })\n\n    let client = createMemcacheClient(server.address)\n\n    await assert.rejects(() => client.delete('test-key'), /Memcache delete failed: ERROR/)\n  })\n})\n\nasync function startFakeMemcacheServer(\n  options?: FakeMemcacheServerOptions,\n): Promise<FakeMemcacheServer> {\n  return await new Promise((resolve, reject) => {\n    let store = new Map<string, Buffer>()\n\n    let server = net.createServer((socket) => {\n      let buffer = Buffer.alloc(0)\n      let pendingSet: { key: string; bytes: number } | undefined\n\n      socket.on('data', (chunk) => {\n        buffer = Buffer.concat([buffer, chunk])\n\n        while (true) {\n          if (pendingSet) {\n            let expected = pendingSet.bytes + 2\n\n            if (buffer.length < expected) {\n              return\n            }\n\n            let value = buffer.subarray(0, pendingSet.bytes)\n            let suffix = buffer.subarray(pendingSet.bytes, expected)\n            buffer = buffer.subarray(expected)\n\n            if (!suffix.equals(Buffer.from('\\r\\n'))) {\n              socket.write('CLIENT_ERROR bad data chunk\\r\\n')\n              socket.end()\n              return\n            }\n\n            store.set(pendingSet.key, Buffer.from(value))\n            pendingSet = undefined\n            socket.write(`${options?.setResponse ?? 'STORED'}\\r\\n`)\n            continue\n          }\n\n          let lineEnd = buffer.indexOf('\\r\\n')\n          if (lineEnd === -1) {\n            return\n          }\n\n          let line = buffer.subarray(0, lineEnd).toString('utf8')\n          buffer = buffer.subarray(lineEnd + 2)\n\n          let getMatch = /^get (\\S+)$/.exec(line)\n          if (getMatch) {\n            if (options?.malformedGetResponse) {\n              socket.write(options.malformedGetResponse)\n              continue\n            }\n\n            let key = getMatch[1]\n            let value = store.get(key)\n\n            if (value == null) {\n              socket.write('END\\r\\n')\n              continue\n            }\n\n            let responseKey = options?.getResponseKey ?? key\n            let header = `VALUE ${responseKey} 0 ${value.byteLength}\\r\\n`\n            socket.write(Buffer.concat([Buffer.from(header), value, Buffer.from('\\r\\nEND\\r\\n')]))\n            continue\n          }\n\n          let setMatch = /^set (\\S+) (\\d+) (\\d+) (\\d+)$/.exec(line)\n          if (setMatch) {\n            let bytes = Number(setMatch[4])\n\n            if (!Number.isInteger(bytes) || bytes < 0) {\n              socket.write('CLIENT_ERROR bad command line format\\r\\n')\n              continue\n            }\n\n            pendingSet = {\n              key: setMatch[1],\n              bytes,\n            }\n            continue\n          }\n\n          let deleteMatch = /^delete (\\S+)$/.exec(line)\n          if (deleteMatch) {\n            if (options?.deleteResponse) {\n              socket.write(`${options.deleteResponse}\\r\\n`)\n              continue\n            }\n\n            let deleted = store.delete(deleteMatch[1])\n            socket.write(deleted ? 'DELETED\\r\\n' : 'NOT_FOUND\\r\\n')\n            continue\n          }\n\n          socket.write('ERROR\\r\\n')\n        }\n      })\n    })\n\n    server.once('error', (error) => {\n      reject(error)\n    })\n\n    server.listen(0, '127.0.0.1', () => {\n      let address = server.address()\n\n      if (address == null || typeof address === 'string') {\n        reject(new Error('Failed to resolve fake Memcache server address'))\n        return\n      }\n\n      resolve({\n        address: `127.0.0.1:${address.port}`,\n        close: async () => {\n          await new Promise<void>((resolveClose, rejectClose) => {\n            server.close((error) => {\n              if (error) {\n                rejectClose(error)\n                return\n              }\n\n              resolveClose()\n            })\n          })\n        },\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "packages/session-storage-memcache/src/lib/memcache-client.ts",
    "content": "import * as net from 'node:net'\n\nconst CRLF = '\\r\\n'\nconst CRLF_BUFFER = Buffer.from(CRLF)\nconst END_RESPONSE = Buffer.from(`END${CRLF}`)\nconst DEFAULT_PORT = 11211\nconst SOCKET_TIMEOUT_MS = 5_000\n\ntype MemcacheAddress = {\n  host: string\n  port: number\n}\n\nexport interface MemcacheClient {\n  get: (key: string) => Promise<string | null>\n  set: (key: string, value: string, ttlSeconds: number) => Promise<void>\n  delete: (key: string) => Promise<void>\n}\n\nexport function createMemcacheClient(server: string): MemcacheClient {\n  let address = parseMemcacheServer(server)\n\n  return {\n    async get(key) {\n      return await getMemcacheValue(address, key)\n    },\n    async set(key, value, ttlSeconds) {\n      await setMemcacheValue(address, key, value, ttlSeconds)\n    },\n    async delete(key) {\n      await deleteMemcacheValue(address, key)\n    },\n  }\n}\n\nasync function getMemcacheValue(address: MemcacheAddress, key: string): Promise<string | null> {\n  let response = await sendMemcacheCommand(address, `get ${key}${CRLF}`, isGetResponseComplete)\n\n  if (response.equals(END_RESPONSE)) {\n    return null\n  }\n\n  let lineEnd = response.indexOf(CRLF)\n  if (lineEnd === -1) {\n    throw new Error(`Invalid Memcache get response: ${response.toString('utf8')}`)\n  }\n\n  let firstLine = response.subarray(0, lineEnd).toString('utf8')\n  let match = /^VALUE (\\S+) (\\d+) (\\d+)$/.exec(firstLine)\n  if (match == null) {\n    throw new Error(`Invalid Memcache get response: ${response.toString('utf8')}`)\n  }\n  if (match[1] !== key) {\n    throw new Error(`Memcache returned value for unexpected key: ${match[1]}`)\n  }\n\n  let bytes = Number(match[3])\n  if (!Number.isInteger(bytes) || bytes < 0) {\n    throw new Error(`Invalid Memcache value length: ${match[3]}`)\n  }\n\n  let valueStart = lineEnd + CRLF_BUFFER.length\n  let valueEnd = valueStart + bytes\n  let expectedLength = valueEnd + CRLF_BUFFER.length + END_RESPONSE.length\n\n  if (response.length !== expectedLength) {\n    throw new Error(`Invalid Memcache get response length: ${response.toString('utf8')}`)\n  }\n  if (!response.subarray(valueEnd, valueEnd + CRLF_BUFFER.length).equals(CRLF_BUFFER)) {\n    throw new Error('Invalid Memcache get response: missing value terminator')\n  }\n  if (!response.subarray(valueEnd + CRLF_BUFFER.length).equals(END_RESPONSE)) {\n    throw new Error('Invalid Memcache get response: missing END terminator')\n  }\n\n  return response.subarray(valueStart, valueEnd).toString('utf8')\n}\n\nasync function setMemcacheValue(\n  address: MemcacheAddress,\n  key: string,\n  value: string,\n  ttlSeconds: number,\n): Promise<void> {\n  let valueBuffer = Buffer.from(value, 'utf8')\n  let commandBuffer = Buffer.from(\n    `set ${key} 0 ${ttlSeconds} ${valueBuffer.byteLength}${CRLF}`,\n    'utf8',\n  )\n  let payload = Buffer.concat([commandBuffer, valueBuffer, CRLF_BUFFER])\n\n  let response = await sendMemcacheCommand(address, payload, isLineResponseComplete)\n  let status = parseSingleLineResponse(response, 'set')\n\n  if (status !== 'STORED') {\n    throw new Error(`Memcache set failed: ${status}`)\n  }\n}\n\nasync function deleteMemcacheValue(address: MemcacheAddress, key: string): Promise<void> {\n  let response = await sendMemcacheCommand(address, `delete ${key}${CRLF}`, isLineResponseComplete)\n  let status = parseSingleLineResponse(response, 'delete')\n\n  if (status === 'DELETED' || status === 'NOT_FOUND') {\n    return\n  }\n\n  throw new Error(`Memcache delete failed: ${status}`)\n}\n\nasync function sendMemcacheCommand(\n  address: MemcacheAddress,\n  command: string | Buffer,\n  isComplete: (response: Buffer) => boolean,\n): Promise<Buffer> {\n  let socket = await connectToMemcache(address)\n\n  try {\n    await writeToSocket(socket, command)\n    return await readFromSocket(socket, isComplete)\n  } finally {\n    socket.destroy()\n  }\n}\n\nasync function connectToMemcache(address: MemcacheAddress): Promise<net.Socket> {\n  return await new Promise((resolve, reject) => {\n    let socket = net.createConnection({ host: address.host, port: address.port })\n    let settled = false\n\n    socket.setNoDelay(true)\n    socket.setTimeout(SOCKET_TIMEOUT_MS)\n\n    function finalize(error?: Error): void {\n      if (settled) {\n        return\n      }\n\n      settled = true\n      socket.off('connect', onConnect)\n      socket.off('error', onError)\n      socket.off('timeout', onTimeout)\n\n      if (error) {\n        socket.destroy()\n        reject(error)\n        return\n      }\n\n      resolve(socket)\n    }\n\n    function onConnect(): void {\n      finalize()\n    }\n\n    function onError(error: Error): void {\n      finalize(\n        new Error(\n          `Failed to connect to Memcache ${address.host}:${address.port}: ${error.message}`,\n        ),\n      )\n    }\n\n    function onTimeout(): void {\n      finalize(new Error(`Timed out connecting to Memcache ${address.host}:${address.port}`))\n    }\n\n    socket.on('connect', onConnect)\n    socket.on('error', onError)\n    socket.on('timeout', onTimeout)\n  })\n}\n\nasync function writeToSocket(socket: net.Socket, command: string | Buffer): Promise<void> {\n  await new Promise((resolve, reject) => {\n    socket.write(command, (error) => {\n      if (error) {\n        reject(new Error(`Failed to write Memcache command: ${error.message}`))\n        return\n      }\n\n      resolve(undefined)\n    })\n  })\n}\n\nasync function readFromSocket(\n  socket: net.Socket,\n  isComplete: (response: Buffer) => boolean,\n): Promise<Buffer> {\n  return await new Promise((resolve, reject) => {\n    let chunks: Buffer[] = []\n    let totalLength = 0\n    let settled = false\n\n    function finalize(response?: Buffer, error?: Error): void {\n      if (settled) {\n        return\n      }\n\n      settled = true\n      socket.off('data', onData)\n      socket.off('error', onError)\n      socket.off('end', onEnd)\n      socket.off('timeout', onTimeout)\n\n      if (error) {\n        reject(error)\n        return\n      }\n\n      resolve(response as Buffer)\n    }\n\n    function onData(chunk: Buffer): void {\n      chunks.push(chunk)\n      totalLength += chunk.length\n\n      let response = Buffer.concat(chunks, totalLength)\n      if (isComplete(response)) {\n        finalize(response)\n      }\n    }\n\n    function onError(error: Error): void {\n      finalize(undefined, new Error(`Memcache request failed: ${error.message}`))\n    }\n\n    function onEnd(): void {\n      finalize(\n        undefined,\n        new Error('Memcache server closed the connection before response completed'),\n      )\n    }\n\n    function onTimeout(): void {\n      finalize(undefined, new Error('Timed out waiting for Memcache response'))\n    }\n\n    socket.on('data', onData)\n    socket.on('error', onError)\n    socket.on('end', onEnd)\n    socket.on('timeout', onTimeout)\n  })\n}\n\nfunction isLineResponseComplete(response: Buffer): boolean {\n  return response.indexOf(CRLF) !== -1\n}\n\nfunction parseSingleLineResponse(response: Buffer, command: string): string {\n  let lineEnd = response.indexOf(CRLF)\n  if (lineEnd === -1) {\n    throw new Error(`Invalid Memcache ${command} response: ${response.toString('utf8')}`)\n  }\n  if (lineEnd + CRLF_BUFFER.length !== response.length) {\n    throw new Error(`Invalid Memcache ${command} response: ${response.toString('utf8')}`)\n  }\n\n  return response.subarray(0, lineEnd).toString('utf8')\n}\n\nfunction isGetResponseComplete(response: Buffer): boolean {\n  if (response.length < END_RESPONSE.length) {\n    return false\n  }\n\n  if (response.equals(END_RESPONSE)) {\n    return true\n  }\n\n  let lineEnd = response.indexOf(CRLF)\n  if (lineEnd === -1) {\n    return false\n  }\n\n  let firstLine = response.subarray(0, lineEnd).toString('utf8')\n  let match = /^VALUE \\S+ \\d+ (\\d+)$/.exec(firstLine)\n  if (match == null) {\n    return true\n  }\n\n  let bytes = Number(match[1])\n  if (!Number.isInteger(bytes) || bytes < 0) {\n    return true\n  }\n\n  let expectedLength =\n    lineEnd + CRLF_BUFFER.length + bytes + CRLF_BUFFER.length + END_RESPONSE.length\n  return response.length >= expectedLength\n}\n\nfunction parseMemcacheServer(server: string): MemcacheAddress {\n  let url: URL\n\n  try {\n    url = new URL(`memcache://${server}`)\n  } catch {\n    throw new Error(`Invalid Memcache server \"${server}\". Expected format \"host:port\".`)\n  }\n\n  if (url.hostname === '') {\n    throw new Error(`Invalid Memcache server \"${server}\": host is required.`)\n  }\n  if (\n    url.username ||\n    url.password ||\n    (url.pathname !== '' && url.pathname !== '/') ||\n    url.search ||\n    url.hash\n  ) {\n    throw new Error(`Invalid Memcache server \"${server}\". Expected format \"host:port\".`)\n  }\n\n  let port = url.port === '' ? DEFAULT_PORT : Number(url.port)\n\n  if (!Number.isInteger(port) || port < 1 || port > 65_535) {\n    throw new Error(`Invalid Memcache port in \"${server}\": ${url.port || '(empty)'}`)\n  }\n\n  return {\n    host: url.hostname,\n    port,\n  }\n}\n"
  },
  {
    "path": "packages/session-storage-memcache/src/lib/memcache-storage.integration.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport type { SessionStorage } from '@remix-run/session'\nimport {\n  createMemcacheSessionStorage,\n  type MemcacheSessionStorageOptions,\n} from './memcache-storage.ts'\n\nlet integrationEnabled =\n  process.env.SESSION_MEMCACHE_INTEGRATION === '1' &&\n  typeof process.env.SESSION_MEMCACHE_SERVER === 'string'\n\ndescribe('memcache session storage integration', () => {\n  it('does not use unknown session IDs by default', { skip: !integrationEnabled }, async () => {\n    let storage = createIntegrationStorage()\n    let session = await storage.read('unknown')\n    assert.notEqual(session.id, 'unknown')\n  })\n\n  it('uses unknown session IDs if enabled', { skip: !integrationEnabled }, async () => {\n    let storage = createIntegrationStorage({ useUnknownIds: true })\n    let session = await storage.read('unknown')\n    assert.equal(session.id, 'unknown')\n  })\n\n  it('persists session data across requests', { skip: !integrationEnabled }, async () => {\n    let storage = createIntegrationStorage()\n    let requests = createRequestHelpers(storage)\n\n    let response1 = await requests.requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requests.requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requests.requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 3)\n  })\n\n  it(\n    'clears session data when the session is destroyed',\n    { skip: !integrationEnabled },\n    async () => {\n      let storage = createIntegrationStorage()\n      let requests = createRequestHelpers(storage)\n\n      let response1 = await requests.requestIndex()\n      assert.equal(response1.session.get('count'), 1)\n\n      let response2 = await requests.requestIndex(response1.cookie)\n      assert.equal(response2.session.get('count'), 2)\n\n      let response3 = await requests.requestDestroy(response2.cookie)\n      assert.ok(response3.session.destroyed)\n\n      let response4 = await requests.requestIndex(response3.cookie)\n      assert.equal(response4.session.get('count'), 1)\n      assert.notEqual(response4.session.id, response3.session.id)\n    },\n  )\n\n  it(\n    'does not set a cookie when session data is not changed',\n    { skip: !integrationEnabled },\n    async () => {\n      let storage = createIntegrationStorage()\n      let requests = createRequestHelpers(storage)\n\n      let response = await requests.requestSession()\n      assert.equal(response.session.dirty, false)\n      assert.equal(response.cookie, null)\n    },\n  )\n\n  it(\n    'makes flash data available only on the next request',\n    { skip: !integrationEnabled },\n    async () => {\n      let storage = createIntegrationStorage()\n      let requests = createRequestHelpers(storage)\n\n      let response1 = await requests.requestSession()\n      assert.equal(response1.session.get('message'), undefined)\n\n      let response2 = await requests.requestFlash(response1.cookie)\n      assert.equal(response2.session.get('message'), undefined)\n\n      let response3 = await requests.requestSession(response2.cookie)\n      assert.equal(response3.session.get('message'), 'success!')\n\n      let response4 = await requests.requestSession(response3.cookie)\n      assert.equal(response4.session.get('message'), undefined)\n    },\n  )\n\n  it(\n    'leaves old session data in storage by default when the id is regenerated',\n    { skip: !integrationEnabled },\n    async () => {\n      let storage = createIntegrationStorage()\n      let requests = createRequestHelpers(storage)\n\n      let response1 = await requests.requestIndex()\n      assert.equal(response1.session.get('count'), 1)\n\n      let response2 = await requests.requestLogin(response1.cookie)\n      assert.notEqual(response2.session.id, response1.session.id)\n\n      let response3 = await requests.requestIndex(response2.cookie)\n      assert.equal(response3.session.get('count'), 2)\n\n      let response4 = await requests.requestIndex(response1.cookie)\n      assert.equal(response4.session.get('count'), 2, 'old session data should still be in storage')\n    },\n  )\n\n  it(\n    'deletes old session data when the id is regenerated and the deleteOldSession option is true',\n    { skip: !integrationEnabled },\n    async () => {\n      let storage = createIntegrationStorage()\n      let requests = createRequestHelpers(storage)\n\n      let response1 = await requests.requestIndex()\n      assert.equal(response1.session.get('count'), 1)\n\n      let response2 = await requests.requestLoginAndDeleteOldSession(response1.cookie)\n      assert.notEqual(response2.session.id, response1.session.id)\n\n      let response3 = await requests.requestIndex(response2.cookie)\n      assert.equal(response3.session.get('count'), 2)\n\n      let response4 = await requests.requestIndex(response1.cookie)\n      assert.equal(response4.session.get('count'), 1, 'old session data should be deleted')\n    },\n  )\n})\n\nfunction createIntegrationStorage(options?: MemcacheSessionStorageOptions): SessionStorage {\n  return createMemcacheSessionStorage(process.env.SESSION_MEMCACHE_SERVER as string, {\n    ...options,\n    keyPrefix: `remix:session:integration:${crypto.randomUUID()}:`,\n  })\n}\n\nfunction createRequestHelpers(storage: SessionStorage) {\n  return {\n    async requestSession(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestFlash(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.flash('message', 'success!')\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestDestroy(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.destroy()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestLogin(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestLoginAndDeleteOldSession(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId(true)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n  }\n}\n"
  },
  {
    "path": "packages/session-storage-memcache/src/lib/memcache-storage.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport * as net from 'node:net'\nimport { afterEach, beforeEach, describe, it } from 'node:test'\n\nimport type { SessionStorage } from '@remix-run/session'\nimport { createMemcacheSessionStorage } from './memcache-storage.ts'\n\ntype FakeMemcacheServer = {\n  address: string\n  close: () => Promise<void>\n}\n\ndescribe('memcache session storage', () => {\n  let server: FakeMemcacheServer\n\n  beforeEach(async () => {\n    server = await startFakeMemcacheServer()\n  })\n\n  afterEach(async () => {\n    await server.close()\n  })\n\n  it('does not use unknown session IDs by default', async () => {\n    let storage = createMemcacheSessionStorage(server.address)\n    let session = await storage.read('unknown')\n    assert.notEqual(session.id, 'unknown')\n  })\n\n  it('uses unknown session IDs if enabled', async () => {\n    let storage = createMemcacheSessionStorage(server.address, { useUnknownIds: true })\n    let session = await storage.read('unknown')\n    assert.equal(session.id, 'unknown')\n  })\n\n  it('persists session data across requests', async () => {\n    let storage = createMemcacheSessionStorage(server.address)\n    let requests = createRequestHelpers(storage)\n\n    let response1 = await requests.requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requests.requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requests.requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 3)\n  })\n\n  it('clears session data when the session is destroyed', async () => {\n    let storage = createMemcacheSessionStorage(server.address)\n    let requests = createRequestHelpers(storage)\n\n    let response1 = await requests.requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requests.requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requests.requestDestroy(response2.cookie)\n    assert.ok(response3.session.destroyed)\n\n    let response4 = await requests.requestIndex(response3.cookie)\n    assert.equal(response4.session.get('count'), 1)\n    assert.notEqual(response4.session.id, response3.session.id)\n  })\n\n  it('does not set a cookie when session data is not changed', async () => {\n    let storage = createMemcacheSessionStorage(server.address)\n    let requests = createRequestHelpers(storage)\n\n    let response = await requests.requestSession()\n    assert.equal(response.session.dirty, false)\n    assert.equal(response.cookie, null)\n  })\n\n  it('makes flash data available only on the next request', async () => {\n    let storage = createMemcacheSessionStorage(server.address)\n    let requests = createRequestHelpers(storage)\n\n    let response1 = await requests.requestSession()\n    assert.equal(response1.session.get('message'), undefined)\n\n    let response2 = await requests.requestFlash(response1.cookie)\n    assert.equal(response2.session.get('message'), undefined)\n\n    let response3 = await requests.requestSession(response2.cookie)\n    assert.equal(response3.session.get('message'), 'success!')\n\n    let response4 = await requests.requestSession(response3.cookie)\n    assert.equal(response4.session.get('message'), undefined)\n  })\n\n  it('leaves old session data in storage by default when the id is regenerated', async () => {\n    let storage = createMemcacheSessionStorage(server.address)\n    let requests = createRequestHelpers(storage)\n\n    let response1 = await requests.requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requests.requestLogin(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requests.requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requests.requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 2, 'old session data should still be in storage')\n  })\n\n  it('deletes old session data when the id is regenerated and the deleteOldSession option is true', async () => {\n    let storage = createMemcacheSessionStorage(server.address)\n    let requests = createRequestHelpers(storage)\n\n    let response1 = await requests.requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requests.requestLoginAndDeleteOldSession(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requests.requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requests.requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 1, 'old session data should be deleted')\n  })\n\n  it('throws for invalid configuration', () => {\n    assert.throws(\n      () => createMemcacheSessionStorage(server.address, { ttlSeconds: -1 }),\n      /ttlSeconds must be a non-negative integer/,\n    )\n\n    assert.throws(\n      () => createMemcacheSessionStorage(server.address, { keyPrefix: 'invalid prefix' }),\n      /keyPrefix may only contain printable ASCII characters without spaces/,\n    )\n  })\n})\n\nfunction createRequestHelpers(storage: SessionStorage) {\n  return {\n    async requestSession(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestFlash(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.flash('message', 'success!')\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestDestroy(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.destroy()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestLogin(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId()\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n    async requestLoginAndDeleteOldSession(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId(true)\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    },\n  }\n}\n\nasync function startFakeMemcacheServer(): Promise<FakeMemcacheServer> {\n  return await new Promise((resolve, reject) => {\n    let store = new Map<string, Buffer>()\n\n    let server = net.createServer((socket) => {\n      let buffer = Buffer.alloc(0)\n      let pendingSet: { key: string; bytes: number } | undefined\n\n      socket.on('data', (chunk) => {\n        buffer = Buffer.concat([buffer, chunk])\n\n        while (true) {\n          if (pendingSet) {\n            let expected = pendingSet.bytes + 2\n\n            if (buffer.length < expected) {\n              return\n            }\n\n            let value = buffer.subarray(0, pendingSet.bytes)\n            let suffix = buffer.subarray(pendingSet.bytes, expected)\n            buffer = buffer.subarray(expected)\n\n            if (!suffix.equals(Buffer.from('\\r\\n'))) {\n              socket.write('CLIENT_ERROR bad data chunk\\r\\n')\n              socket.end()\n              return\n            }\n\n            store.set(pendingSet.key, Buffer.from(value))\n            pendingSet = undefined\n            socket.write('STORED\\r\\n')\n            continue\n          }\n\n          let lineEnd = buffer.indexOf('\\r\\n')\n          if (lineEnd === -1) {\n            return\n          }\n\n          let line = buffer.subarray(0, lineEnd).toString('utf8')\n          buffer = buffer.subarray(lineEnd + 2)\n\n          let getMatch = /^get (\\S+)$/.exec(line)\n          if (getMatch) {\n            let key = getMatch[1]\n            let value = store.get(key)\n\n            if (value == null) {\n              socket.write('END\\r\\n')\n              continue\n            }\n\n            let header = `VALUE ${key} 0 ${value.byteLength}\\r\\n`\n            socket.write(Buffer.concat([Buffer.from(header), value, Buffer.from('\\r\\nEND\\r\\n')]))\n            continue\n          }\n\n          let setMatch = /^set (\\S+) (\\d+) (\\d+) (\\d+)$/.exec(line)\n          if (setMatch) {\n            let bytes = Number(setMatch[4])\n\n            if (!Number.isInteger(bytes) || bytes < 0) {\n              socket.write('CLIENT_ERROR bad command line format\\r\\n')\n              continue\n            }\n\n            pendingSet = {\n              key: setMatch[1],\n              bytes,\n            }\n            continue\n          }\n\n          let deleteMatch = /^delete (\\S+)$/.exec(line)\n          if (deleteMatch) {\n            let deleted = store.delete(deleteMatch[1])\n            socket.write(deleted ? 'DELETED\\r\\n' : 'NOT_FOUND\\r\\n')\n            continue\n          }\n\n          socket.write('ERROR\\r\\n')\n        }\n      })\n    })\n\n    server.once('error', (error) => {\n      reject(error)\n    })\n\n    server.listen(0, '127.0.0.1', () => {\n      let address = server.address()\n\n      if (address == null || typeof address === 'string') {\n        reject(new Error('Failed to resolve fake Memcache server address'))\n        return\n      }\n\n      resolve({\n        address: `127.0.0.1:${address.port}`,\n        close: async () => {\n          await new Promise<void>((resolveClose, rejectClose) => {\n            server.close((error) => {\n              if (error) {\n                rejectClose(error)\n                return\n              }\n\n              resolveClose()\n            })\n          })\n        },\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "packages/session-storage-memcache/src/lib/memcache-storage.ts",
    "content": "import { createSession } from '@remix-run/session'\nimport type { SessionStorage } from '@remix-run/session'\nimport { createMemcacheClient } from './memcache-client.ts'\n\nconst DEFAULT_KEY_PREFIX = 'remix:session:'\nconst MAX_MEMCACHE_KEY_LENGTH = 250\nconst HASH_LENGTH = 64\ntype SessionData = ReturnType<typeof createSession>['data']\n\n/**\n * Options for Memcache-backed session storage.\n */\nexport interface MemcacheSessionStorageOptions {\n  /**\n   * Whether to reuse session IDs sent from the client that are not found in storage.\n   * Default is `false`.\n   */\n  useUnknownIds?: boolean\n\n  /**\n   * Prefix prepended to all session keys in Memcache.\n   * Default is `'remix:session:'`.\n   */\n  keyPrefix?: string\n\n  /**\n   * Session TTL in seconds.\n   * Default is `0` (never expire).\n   */\n  ttlSeconds?: number\n}\n\n/**\n * Creates a session storage that stores all session data in Memcache.\n *\n * Note: This storage requires a Node.js runtime with TCP socket support.\n *\n * @param server The Memcache server in `host:port` format\n * @param options (optional) The options for the session storage\n * @returns The session storage\n */\nexport function createMemcacheSessionStorage(\n  server: string,\n  options?: MemcacheSessionStorageOptions,\n): SessionStorage {\n  let client = createMemcacheClient(server)\n  let useUnknownIds = options?.useUnknownIds ?? false\n  let keyPrefix = options?.keyPrefix ?? DEFAULT_KEY_PREFIX\n  let ttlSeconds = options?.ttlSeconds ?? 0\n\n  assertValidKeyPrefix(keyPrefix)\n  assertValidTtl(ttlSeconds)\n\n  return {\n    async read(cookie) {\n      let id = cookie\n\n      if (id) {\n        let data = await readSessionData(id)\n        if (data != null) {\n          return createSession(id, data)\n        }\n      }\n\n      return createSession(useUnknownIds && id ? id : undefined)\n    },\n    async save(session) {\n      if (session.deleteId) {\n        await deleteSessionData(session.deleteId)\n      }\n\n      if (session.destroyed) {\n        await deleteSessionData(session.id)\n        return ''\n      }\n      if (session.dirty) {\n        await writeSessionData(session.id, session.data)\n        return session.id\n      }\n\n      return null\n    },\n  }\n\n  async function readSessionData(id: string): Promise<SessionData | null> {\n    let key = await getMemcacheKey(keyPrefix, id)\n    let value = await client.get(key)\n\n    if (value == null) {\n      return null\n    }\n\n    try {\n      return JSON.parse(value) as SessionData\n    } catch (error) {\n      throw new Error(\n        `Failed to parse session data for session ID ${id}: ${getErrorMessage(error)}`,\n      )\n    }\n  }\n\n  async function writeSessionData(id: string, data: SessionData): Promise<void> {\n    let key = await getMemcacheKey(keyPrefix, id)\n    await client.set(key, JSON.stringify(data), ttlSeconds)\n  }\n\n  async function deleteSessionData(id: string): Promise<void> {\n    let key = await getMemcacheKey(keyPrefix, id)\n    await client.delete(key)\n  }\n}\n\nasync function getMemcacheKey(prefix: string, id: string): Promise<string> {\n  return `${prefix}${await computeHash(id)}`\n}\n\nfunction assertValidKeyPrefix(keyPrefix: string): void {\n  if (!/^[\\x21-\\x7e]*$/.test(keyPrefix)) {\n    throw new Error('Memcache keyPrefix may only contain printable ASCII characters without spaces')\n  }\n\n  let keyPrefixBytes = Buffer.byteLength(keyPrefix, 'utf8')\n  if (keyPrefixBytes + HASH_LENGTH > MAX_MEMCACHE_KEY_LENGTH) {\n    throw new Error(\n      `Memcache keyPrefix is too long. Maximum length is ${MAX_MEMCACHE_KEY_LENGTH - HASH_LENGTH} bytes`,\n    )\n  }\n}\n\nfunction assertValidTtl(ttlSeconds: number): void {\n  if (!Number.isInteger(ttlSeconds) || ttlSeconds < 0) {\n    throw new Error(`Memcache ttlSeconds must be a non-negative integer. Received: ${ttlSeconds}`)\n  }\n}\n\nfunction getErrorMessage(error: unknown): string {\n  return error instanceof Error ? error.message : String(error)\n}\n\nasync function computeHash(id: string, algorithm = 'SHA-256'): Promise<string> {\n  let encoder = new TextEncoder()\n  let data = encoder.encode(id)\n  let hashBuffer = await crypto.subtle.digest(algorithm, data)\n  let hashArray = Array.from(new Uint8Array(hashBuffer))\n  return hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('')\n}\n"
  },
  {
    "path": "packages/session-storage-memcache/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/session-storage-memcache/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/session-storage-redis/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/session-storage-redis/CHANGELOG.md",
    "content": "# `session-storage-redis` CHANGELOG\n\nThis 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/).\n\n## v0.1.0\n\n### Minor Changes\n\n- Initial release of `@remix-run/session-storage-redis` with `createRedisSessionStorage()`.\n\n## Unreleased\n\n### Minor Changes\n\n- Initial release.\n"
  },
  {
    "path": "packages/session-storage-redis/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/session-storage-redis/README.md",
    "content": "# session-storage-redis\n\nRedis-backed session storage for [`@remix-run/session`](https://github.com/remix-run/remix/tree/main/packages/session).\nUse this package when app servers need to share session state through Redis.\n\n## Installation\n\n```sh\nnpm i @remix-run/session @remix-run/session-storage-redis redis\n```\n\n## Usage\n\n```ts\nimport { createClient } from 'redis'\nimport { createRedisSessionStorage } from '@remix-run/session-storage-redis'\n\nlet redis = createClient({ url: process.env.REDIS_URL })\nawait redis.connect()\n\nlet sessionStorage = createRedisSessionStorage(redis, {\n  keyPrefix: 'session:',\n  ttl: 60 * 60 * 24,\n})\n```\n\n## Options\n\n`createRedisSessionStorage(client, options)` supports:\n\n- `keyPrefix` (`string`, default: `'session:'`)\n- `ttl` (`number` seconds)\n- `useUnknownIds` (`boolean`, default: `false`)\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/session-storage-redis/package.json",
    "content": "{\n  \"name\": \"@remix-run/session-storage-redis\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Redis session storage for remix/session\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/session-storage-redis\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/session-storage-redis#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\",\n    \"redis\": \"^5.10.0\"\n  },\n  \"dependencies\": {\n    \"@remix-run/session\": \"workspace:^\"\n  },\n  \"peerDependencies\": {\n    \"redis\": \"^5.10.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"redis\": {\n      \"optional\": true\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"session\",\n    \"session-storage\",\n    \"redis\",\n    \"storage\"\n  ]\n}\n"
  },
  {
    "path": "packages/session-storage-redis/src/index.ts",
    "content": "export {\n  createRedisSessionStorage,\n  type RedisSessionStorageClient,\n  type RedisSessionStorageOptions,\n} from './lib/redis-storage.ts'\n"
  },
  {
    "path": "packages/session-storage-redis/src/lib/redis-storage.integration.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { after, before, beforeEach, describe, it } from 'node:test'\n\nimport { createClient } from 'redis'\n\nimport { createRedisSessionStorage } from './redis-storage.ts'\n\nlet redisUrl = process.env.SESSION_REDIS_URL\nlet integrationEnabled = process.env.SESSION_REDIS_INTEGRATION === '1' && redisUrl != null\n\ndescribe('redis session storage integration', { skip: !integrationEnabled }, () => {\n  let client: ReturnType<typeof createClient>\n\n  before(async () => {\n    client = createClient({ url: redisUrl })\n    await client.connect()\n  })\n\n  beforeEach(async () => {\n    await client.flushDb()\n  })\n\n  after(async () => {\n    await client.quit()\n  })\n\n  it('does not use unknown session IDs by default', async () => {\n    let storage = createRedisSessionStorage(client)\n    let session = await storage.read('unknown')\n\n    assert.notEqual(session.id, 'unknown')\n  })\n\n  it('uses unknown session IDs if enabled', async () => {\n    let storage = createRedisSessionStorage(client, { useUnknownIds: true })\n    let session = await storage.read('unknown')\n\n    assert.equal(session.id, 'unknown')\n  })\n\n  it('persists session data across requests', async () => {\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 3)\n  })\n\n  it('clears session data when the session is destroyed', async () => {\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestDestroy(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.destroy()\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestDestroy(response2.cookie)\n    assert.ok(response3.session.destroyed)\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('count'), 1)\n    assert.notEqual(response4.session.id, response3.session.id)\n  })\n\n  it('does not set a cookie when session data is not changed', async () => {\n    let storage = createRedisSessionStorage(client)\n\n    let session = await storage.read(null)\n    let cookie = await storage.save(session)\n\n    assert.equal(session.dirty, false)\n    assert.equal(cookie, null)\n  })\n\n  it('makes flash data available only on the next request', async () => {\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestFlash(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.flash('message', 'success!')\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('message'), undefined)\n\n    let response2 = await requestFlash(response1.cookie)\n    assert.equal(response2.session.get('message'), undefined)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('message'), 'success!')\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('message'), undefined)\n  })\n\n  it('deletes old session data when the id is regenerated and the deleteOldSession option is true', async () => {\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestLoginAndDeleteOldSession(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId(true)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestLoginAndDeleteOldSession(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 1, 'old session data should be deleted')\n  })\n\n  it('sets key expiration when ttl is configured', async () => {\n    let storage = createRedisSessionStorage(client, { ttl: 300, keyPrefix: 'session:' })\n\n    let session = await storage.read(null)\n    session.set('count', 1)\n    let cookie = await storage.save(session)\n\n    assert.equal(cookie, session.id)\n\n    let ttl = await client.ttl('session:' + session.id)\n    assert.ok(ttl > 0)\n  })\n})\n"
  },
  {
    "path": "packages/session-storage-redis/src/lib/redis-storage.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { createRedisSessionStorage, type RedisSessionStorageClient } from './redis-storage.ts'\n\nfunction createMapRedisClient() {\n  let values = new Map<string, string>()\n\n  let client: RedisSessionStorageClient = {\n    async get(key) {\n      return values.get(key) ?? null\n    },\n    async set(key, value) {\n      values.set(key, value)\n    },\n    async del(key) {\n      values.delete(key)\n    },\n  }\n\n  return { client, values }\n}\n\ndescribe('redis session storage', () => {\n  it('does not use unknown session IDs by default', async () => {\n    let { client } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client)\n    let session = await storage.read('unknown')\n\n    assert.notEqual(session.id, 'unknown')\n  })\n\n  it('uses unknown session IDs if enabled', async () => {\n    let { client } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client, { useUnknownIds: true })\n    let session = await storage.read('unknown')\n\n    assert.equal(session.id, 'unknown')\n  })\n\n  it('persists session data across requests', async () => {\n    let { client } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 3)\n  })\n\n  it('clears session data when the session is destroyed', async () => {\n    let { client } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestDestroy(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.destroy()\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestIndex(response1.cookie)\n    assert.equal(response2.session.get('count'), 2)\n\n    let response3 = await requestDestroy(response2.cookie)\n    assert.ok(response3.session.destroyed)\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('count'), 1)\n    assert.notEqual(response4.session.id, response3.session.id)\n  })\n\n  it('does not set a cookie when session data is not changed', async () => {\n    let { client } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response = await requestIndex()\n    assert.equal(response.session.dirty, false)\n    assert.equal(response.cookie, null)\n  })\n\n  it('makes flash data available only on the next request', async () => {\n    let { client } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestFlash(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.flash('message', 'success!')\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('message'), undefined)\n\n    let response2 = await requestFlash(response1.cookie)\n    assert.equal(response2.session.get('message'), undefined)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('message'), 'success!')\n\n    let response4 = await requestIndex(response3.cookie)\n    assert.equal(response4.session.get('message'), undefined)\n  })\n\n  it('leaves old session data in storage by default when the id is regenerated', async () => {\n    let { client } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestLogin(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId()\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestLogin(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 2, 'old session data should still be in storage')\n  })\n\n  it('deletes old session data when the id is regenerated and the deleteOldSession option is true', async () => {\n    let { client } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client)\n\n    async function requestIndex(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.set('count', Number(session.get('count') ?? 0) + 1)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    async function requestLoginAndDeleteOldSession(cookie: string | null = null) {\n      let session = await storage.read(cookie)\n      session.regenerateId(true)\n\n      return {\n        cookie: await storage.save(session),\n        session,\n      }\n    }\n\n    let response1 = await requestIndex()\n    assert.equal(response1.session.get('count'), 1)\n\n    let response2 = await requestLoginAndDeleteOldSession(response1.cookie)\n    assert.notEqual(response2.session.id, response1.session.id)\n\n    let response3 = await requestIndex(response2.cookie)\n    assert.equal(response3.session.get('count'), 2)\n\n    let response4 = await requestIndex(response1.cookie)\n    assert.equal(response4.session.get('count'), 1, 'old session data should be deleted')\n  })\n\n  it('uses keyPrefix for redis keys', async () => {\n    let { client, values } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client, { keyPrefix: 'my-app:' })\n\n    let session = await storage.read(null)\n    session.set('count', 1)\n    let cookie = await storage.save(session)\n\n    assert.equal(cookie, session.id)\n    assert.ok(values.has('my-app:' + session.id))\n  })\n\n  it('uses setEx when ttl is configured and the client supports setEx', async () => {\n    let setCallCount = 0\n    let setExArgs: [string, number, string] | undefined\n\n    let client: RedisSessionStorageClient = {\n      async get() {\n        return null\n      },\n      async set() {\n        setCallCount += 1\n      },\n      async del() {},\n      async setEx(key, ttlSeconds, value) {\n        setExArgs = [key, ttlSeconds, value]\n      },\n    }\n\n    let storage = createRedisSessionStorage(client, { ttl: 60.9, keyPrefix: 'session:' })\n    let session = await storage.read(null)\n    session.set('count', 1)\n    let cookie = await storage.save(session)\n\n    assert.equal(cookie, session.id)\n    assert.equal(setCallCount, 0)\n    assert.deepEqual(setExArgs, ['session:' + session.id, 60, JSON.stringify(session.data)])\n  })\n\n  it('uses expire when ttl is configured and setEx is unavailable', async () => {\n    let setCalls: Array<[string, string]> = []\n    let expireCalls: Array<[string, number]> = []\n\n    let client: RedisSessionStorageClient = {\n      async get() {\n        return null\n      },\n      async set(key, value) {\n        setCalls.push([key, value])\n      },\n      async del() {},\n      async expire(key, ttlSeconds) {\n        expireCalls.push([key, ttlSeconds])\n      },\n    }\n\n    let storage = createRedisSessionStorage(client, { ttl: 180 })\n    let session = await storage.read(null)\n    session.set('count', 1)\n    let cookie = await storage.save(session)\n\n    assert.equal(cookie, session.id)\n    assert.deepEqual(setCalls, [['session:' + session.id, JSON.stringify(session.data)]])\n    assert.deepEqual(expireCalls, [['session:' + session.id, 180]])\n  })\n\n  it('throws when ttl is configured and the client does not support expiration', () => {\n    let client: RedisSessionStorageClient = {\n      async get() {\n        return null\n      },\n      async set() {},\n      async del() {},\n    }\n\n    assert.throws(\n      () => createRedisSessionStorage(client, { ttl: 60 }),\n      new Error('Redis client must implement setEx() or expire() when ttl is configured'),\n    )\n  })\n\n  it('throws if session data in redis is invalid JSON', async () => {\n    let { client, values } = createMapRedisClient()\n    let storage = createRedisSessionStorage(client)\n\n    values.set('session:bad', '{invalid-json')\n\n    await assert.rejects(\n      async () => {\n        await storage.read('bad')\n      },\n      {\n        name: 'SyntaxError',\n      },\n    )\n  })\n})\n"
  },
  {
    "path": "packages/session-storage-redis/src/lib/redis-storage.ts",
    "content": "import { createSession } from '@remix-run/session'\nimport type { SessionStorage } from '@remix-run/session'\n\ntype SessionData = ReturnType<typeof createSession>['data']\n\n/**\n * Minimal Redis client contract required by {@link createRedisSessionStorage}.\n */\nexport interface RedisSessionStorageClient {\n  /**\n   * Reads a serialized session value.\n   */\n  get(key: string): Promise<string | null> | string | null\n\n  /**\n   * Stores a serialized session value.\n   */\n  set(key: string, value: string): Promise<unknown> | unknown\n\n  /**\n   * Deletes a stored session value.\n   */\n  del(key: string): Promise<unknown> | unknown\n\n  /**\n   * Stores a serialized session value with a TTL in seconds.\n   */\n  setEx?(key: string, ttlSeconds: number, value: string): Promise<unknown> | unknown\n\n  /**\n   * Updates the TTL for an existing session value in seconds.\n   */\n  expire?(key: string, ttlSeconds: number): Promise<unknown> | unknown\n}\n\n/**\n * Options for Redis-backed session storage created by {@link createRedisSessionStorage}.\n */\nexport interface RedisSessionStorageOptions {\n  /**\n   * Prefix for session keys in Redis.\n   *\n   * @default 'session:'\n   */\n  keyPrefix?: string\n\n  /**\n   * Session TTL in seconds. If set, the session key expires automatically.\n   *\n   * @default undefined\n   */\n  ttl?: number\n\n  /**\n   * Whether to reuse session IDs sent from the client that are not found in storage.\n   *\n   * @default false\n   */\n  useUnknownIds?: boolean\n}\n\n/**\n * Creates a session storage backed by Redis.\n *\n * @param client Redis client with get/set/del methods\n * @param options Session storage options\n * @returns The session storage\n */\nexport function createRedisSessionStorage(\n  client: RedisSessionStorageClient,\n  options?: RedisSessionStorageOptions,\n): SessionStorage {\n  let keyPrefix = options?.keyPrefix ?? 'session:'\n  let useUnknownIds = options?.useUnknownIds ?? false\n  let ttl = normalizeTtl(options?.ttl)\n\n  if (ttl != null && client.setEx == null && client.expire == null) {\n    throw new Error('Redis client must implement setEx() or expire() when ttl is configured')\n  }\n\n  function keyForSession(id: string): string {\n    return keyPrefix + id\n  }\n\n  return {\n    async read(cookie) {\n      let id = cookie\n\n      if (id != null && id !== '') {\n        let value = await client.get(keyForSession(id))\n\n        if (value != null) {\n          let data = JSON.parse(value) as SessionData\n          return createSession(id, data)\n        }\n      }\n\n      return createSession(useUnknownIds && id != null && id !== '' ? id : undefined)\n    },\n    async save(session) {\n      if (session.deleteId) {\n        await client.del(keyForSession(session.deleteId))\n      }\n\n      if (session.destroyed) {\n        await client.del(keyForSession(session.id))\n        return ''\n      }\n      if (session.dirty) {\n        let key = keyForSession(session.id)\n        let value = JSON.stringify(session.data)\n\n        if (ttl == null) {\n          await client.set(key, value)\n        } else if (client.setEx) {\n          await client.setEx(key, ttl, value)\n        } else {\n          let expire = client.expire\n\n          if (expire == null) {\n            throw new Error(\n              'Redis client must implement setEx() or expire() when ttl is configured',\n            )\n          }\n\n          await client.set(key, value)\n          await expire.call(client, key, ttl)\n        }\n\n        return session.id\n      }\n\n      return null\n    },\n  }\n}\n\nfunction normalizeTtl(value: number | undefined): number | undefined {\n  if (value == null) {\n    return undefined\n  }\n\n  return Math.max(1, Math.floor(value))\n}\n"
  },
  {
    "path": "packages/session-storage-redis/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/session-storage-redis/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"types\": [\"node\"],\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/static-middleware/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/static-middleware/CHANGELOG.md",
    "content": "# `static-middleware` CHANGELOG\n\nThis is the changelog for [`static-middleware`](https://github.com/remix-run/remix/tree/main/packages/static-middleware). It follows [semantic versioning](https://semver.org/).\n\n## v0.4.4\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`fetch-router@0.17.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.17.0)\n  - [`fs@0.4.2`](https://github.com/remix-run/remix/releases/tag/fs@0.4.2)\n  - [`mime@0.4.0`](https://github.com/remix-run/remix/releases/tag/mime@0.4.0)\n  - [`response@0.3.2`](https://github.com/remix-run/remix/releases/tag/response@0.3.2)\n\n## v0.4.3\n\n### Patch Changes\n\n- Bumped `@remix-run/*` dependencies:\n  - [`@remix-run/fetch-router@0.16.0`](https://github.com/remix-run/remix/releases/tag/fetch-router@0.16.0)\n\n## v0.4.2\n\n### Patch Changes\n\n- Changed `@remix-run/*` peer dependencies to regular dependencies\n\n## v0.4.1\n\n### Patch Changes\n\n- Update `@remix-run/fs` peer dependency to use new `openLazyFile()` API\n\n## v0.4.0 (2025-11-25)\n\n- BREAKING CHANGE: Replace `mrmime` dependency with `@remix-run/mime` for MIME type detection which is now a peer dependency.\n\n- Add support for `acceptRanges` function to conditionally enable HTTP Range requests based on the file being served:\n\n  ```ts\n  // Enable ranges only for large files\n  staticFiles('./public', {\n    acceptRanges: (file) => file.size > 10 * 1024 * 1024,\n  })\n\n  // Enable ranges only for videos\n  staticFiles('./public', {\n    acceptRanges: (file) => file.type.startsWith('video/'),\n  })\n  ```\n\n## v0.3.0 (2025-11-25)\n\n- 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.\n- Add `listFiles` option to generate a directory listing when a directory is requested.\n\n  ```ts\n  staticFiles('./public', { listFiles: true })\n  ```\n\n## v0.2.0 (2025-11-20)\n\n- 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)\n- Add `@remix-run/fs` as a peer dependency. This package now imports from `@remix-run/fs` instead of `@remix-run/lazy-file/fs`.\n- 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.\n\n  ```ts\n  // Serve index.html from directories by default\n  staticFiles('./public')\n\n  // Custom index files\n  staticFiles('./public', {\n    index: ['default.html', 'home.html'],\n  })\n\n  // Disable index file serving\n  staticFiles('./public', { index: false })\n  staticFiles('./public', { index: [] })\n  ```\n\n## v0.1.0 (2025-11-19)\n\nInitial release extracted from `@remix-run/fetch-router` v0.9.0.\n\nSee the [README](https://github.com/remix-run/remix/blob/main/packages/static-middleware/README.md) for more details.\n"
  },
  {
    "path": "packages/static-middleware/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/static-middleware/README.md",
    "content": "# static-middleware\n\nStatic file serving middleware for Remix. Serves static files from a directory with support for ETags, range requests, and conditional requests.\n\n## Features\n\n- [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) support (weak and strong)\n- [Range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests) support (HTTP 206 Partial Content)\n- [Conditional request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) support (If-None-Match, If-Modified-Since)\n- Path traversal protection\n- Automatic fallback to next middleware/handler if file not found\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nStatic middleware is useful for serving static files from a directory.\n\n```ts\nimport { createRouter } from 'remix/fetch-router'\nimport { staticFiles } from 'remix/static-middleware'\n\nlet router = createRouter({\n  middleware: [staticFiles('./public')],\n})\n\nrouter.get('/', () => new Response('Home'))\n```\n\n### With Cache Control\n\nInternally, 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) to send files with full HTTP semantics. This means it also accepts the same options as the `createFileResponse()` helper.\n\n```ts\nlet router = createRouter({\n  middleware: [\n    staticFiles('./public', {\n      cacheControl: 'public, max-age=31536000, immutable', // 1 year\n    }),\n  ],\n})\n```\n\n### Filter Files\n\n```ts\nlet router = createRouter({\n  middleware: [\n    staticFiles('./public', {\n      filter(path) {\n        // Don't serve hidden files\n        return !path.startsWith('.')\n      },\n    }),\n  ],\n})\n```\n\n### Multiple Directories\n\n```ts\nlet router = createRouter({\n  middleware: [\n    staticFiles('./public'),\n    staticFiles('./assets', {\n      cacheControl: 'public, max-age=31536000',\n    }),\n  ],\n})\n```\n\n## Security\n\n- Prevents path traversal attacks (e.g., `../../../etc/passwd`)\n- Only serves files with GET and HEAD requests\n- Respects the configured root directory boundary\n\n## Related Packages\n\n- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API\n- [`lazy-file`](https://github.com/remix-run/remix/tree/main/packages/lazy-file) - Used internally for streaming file contents\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/static-middleware/demos/list-files/README.md",
    "content": "# List Files Demo\n\nThis demo shows how to use the `listFiles` option in the `@remix-run/static-middleware` package to display a directory listing when a directory is requested.\n\n## Running the Demo\n\n```bash\ncd packages/static-middleware/demos/list-files\nnode server.js\n```\n\nThen visit `http://localhost:44100` in your browser to see the directory listing.\n\n## What It Does\n\nThe demo serves files from the monorepo root directory and displays a nice-looking HTML table when you navigate to a directory. The table shows:\n\n- Folder icon (📁) for directories\n- File icon (📄) for files\n- File size (formatted in KB, MB, etc.)\n- File type (based on extension)\n- Parent directory link (..) to navigate up\n\nThe page is responsive and adapts to different screen sizes using CSS media queries.\n\n## Configuration\n\nThe demo uses these options:\n\n- `listFiles: true` - Enable directory listing\n- `index: false` - Disable index file serving so directories always show the listing\n\nTry visiting:\n\n- `http://localhost:44100/` - Root directory\n- `http://localhost:44100/packages` - Packages directory\n- `http://localhost:44100/packages/static-middleware` - This package\n"
  },
  {
    "path": "packages/static-middleware/demos/list-files/package.json",
    "content": "{\n  \"name\": \"static-middleware-list-files-demo\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\",\n    \"@remix-run/node-fetch-server\": \"workspace:^\",\n    \"@remix-run/static-middleware\": \"workspace:^\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"dev\": \"node --watch server.js\",\n    \"start\": \"node server.js\",\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "packages/static-middleware/demos/list-files/server.js",
    "content": "import * as http from 'node:http'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { createRequestListener } from '@remix-run/node-fetch-server'\nimport { createRouter } from '@remix-run/fetch-router'\nimport { staticFiles } from '@remix-run/static-middleware'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\nconst PORT = 44100\n\n// Point to the monorepo root (two levels up from demos/list-files)\nlet root = path.resolve(__dirname, '..', '..', '..', '..')\n\nlet router = createRouter({\n  middleware: [\n    staticFiles(root, {\n      listFiles: true,\n      // Disable index file serving so directories always show the file listing\n      index: false,\n    }),\n  ],\n})\n\nlet server = http.createServer(createRequestListener((request) => router.fetch(request)))\n\nserver.listen(PORT, () => {\n  console.log(`Server running at http://localhost:${PORT}`)\n  console.log(`Serving files from: ${root}`)\n})\n"
  },
  {
    "path": "packages/static-middleware/demos/list-files/tsconfig.json",
    "content": "{\n  \"include\": [\"server.js\"],\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\"],\n    \"noEmit\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/static-middleware/package.json",
    "content": "{\n  \"name\": \"@remix-run/static-middleware\",\n  \"version\": \"0.4.4\",\n  \"description\": \"Middleware for serving static files from the filesystem\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/static-middleware\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/static-middleware#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:*\",\n    \"@remix-run/form-data-middleware\": \"workspace:*\",\n    \"@remix-run/method-override-middleware\": \"workspace:*\",\n    \"@remix-run/mime\": \"workspace:*\",\n    \"@remix-run/response\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"dependencies\": {\n    \"@remix-run/fetch-router\": \"workspace:^\",\n    \"@remix-run/fs\": \"workspace:^\",\n    \"@remix-run/html-template\": \"workspace:^\",\n    \"@remix-run/mime\": \"workspace:^\",\n    \"@remix-run/response\": \"workspace:^\"\n  },\n  \"scripts\": {\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"fetch\",\n    \"router\",\n    \"middleware\",\n    \"static-files\",\n    \"file-server\"\n  ]\n}\n"
  },
  {
    "path": "packages/static-middleware/src/index.ts",
    "content": "export { type StaticFilesOptions, staticFiles } from './lib/static.ts'\n"
  },
  {
    "path": "packages/static-middleware/src/lib/directory-listing.ts",
    "content": "import * as path from 'node:path'\nimport * as fsp from 'node:fs/promises'\nimport { html } from '@remix-run/html-template'\nimport { createHtmlResponse } from '@remix-run/response/html'\nimport { detectMimeType } from '@remix-run/mime'\n\ninterface DirectoryEntry {\n  name: string\n  isDirectory: boolean\n  size: number\n  type: string\n}\n\nexport async function generateDirectoryListing(\n  dirPath: string,\n  pathname: string,\n): Promise<Response> {\n  let entries: DirectoryEntry[] = []\n\n  try {\n    let dirents = await fsp.readdir(dirPath, { withFileTypes: true })\n\n    for (let dirent of dirents) {\n      let fullPath = path.join(dirPath, dirent.name)\n      let isDirectory = dirent.isDirectory()\n      let size = 0\n      let type = ''\n\n      if (isDirectory) {\n        size = await calculateDirectorySize(fullPath)\n      } else {\n        try {\n          let stats = await fsp.stat(fullPath)\n          size = stats.size\n          let mimeType = detectMimeType(dirent.name)\n          type = mimeType || 'application/octet-stream'\n        } catch {\n          // Unable to stat file, use defaults\n        }\n      }\n\n      entries.push({\n        name: dirent.name,\n        isDirectory,\n        size,\n        type,\n      })\n    }\n  } catch {\n    return new Response('Error reading directory', { status: 500 })\n  }\n\n  // Sort: directories first, then alphabetically\n  entries.sort((a, b) => {\n    if (a.isDirectory && !b.isDirectory) return -1\n    if (!a.isDirectory && b.isDirectory) return 1\n    return a.name.localeCompare(b.name, undefined, { numeric: true })\n  })\n\n  // Build table rows\n  let tableRows = []\n\n  let folderIcon = html.raw`<svg class=\"icon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M2 3.5h4.5l1.5 1.5h6v8h-12z\"/></svg>`\n  let fileIcon = html.raw`<svg class=\"icon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M3 2h7l3 3v9H3z\"/><path d=\"M10 2v3h3\"/></svg>`\n\n  // Add parent directory link if not at root\n  if (pathname !== '/' && pathname !== '') {\n    let parentPath = pathname.replace(/\\/$/, '').split('/').slice(0, -1).join('/') || '/'\n    tableRows.push(html`\n      <tr class=\"file-row\">\n        <td class=\"name-cell\">\n          <a href=\"${parentPath}\">${folderIcon} ..</a>\n        </td>\n        <td class=\"size-cell\"></td>\n        <td class=\"type-cell\"></td>\n      </tr>\n    `)\n  }\n\n  for (let entry of entries) {\n    let icon = entry.isDirectory ? folderIcon : fileIcon\n    let href = pathname.endsWith('/') ? pathname + entry.name : pathname + '/' + entry.name\n    let sizeDisplay = formatFileSize(entry.size)\n    let typeDisplay = entry.isDirectory ? 'Folder' : entry.type\n\n    tableRows.push(html`\n      <tr class=\"file-row\">\n        <td class=\"name-cell\">\n          <a href=\"${href}\">${icon} ${entry.name}</a>\n        </td>\n        <td class=\"size-cell\">${sizeDisplay}</td>\n        <td class=\"type-cell\">${typeDisplay}</td>\n      </tr>\n    `)\n  }\n\n  return createHtmlResponse(html`\n    <html lang=\"en\">\n      <head>\n        <meta charset=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <title>Index of ${pathname}</title>\n        <style>\n          * {\n            box-sizing: border-box;\n          }\n          body {\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,\n              Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n            margin: 0;\n            padding: 2rem 1rem;\n            background: #fff;\n            color: #333;\n            font-size: 14px;\n          }\n          .container {\n            max-width: 1200px;\n            margin: 0 auto;\n          }\n          h1 {\n            margin: 0 0 1rem 0;\n            padding: 0;\n            font-size: 1.25rem;\n            font-weight: 600;\n            color: #333;\n          }\n          .icon {\n            width: 14px;\n            height: 14px;\n            display: inline-block;\n            vertical-align: text-top;\n            margin-right: 0.5rem;\n            color: #666;\n          }\n          table {\n            width: 100%;\n            border-collapse: collapse;\n            border: 1px solid #e0e0e0;\n          }\n          thead tr {\n            background: #fafafa;\n          }\n          th {\n            padding: 0.5rem 1rem;\n            text-align: left;\n            font-weight: 600;\n            font-size: 0.8125rem;\n            color: #666;\n            border-bottom: 1px solid #e0e0e0;\n          }\n          td {\n            border-bottom: 1px solid #f0f0f0;\n          }\n          tr:last-child td {\n            border-bottom: none;\n          }\n          .file-row {\n            position: relative;\n          }\n          .file-row:hover {\n            background: #fafafa;\n          }\n          .name-cell {\n            width: 50%;\n            padding: 0;\n          }\n          .name-cell a {\n            display: block;\n            padding: 0.375rem 1rem;\n            color: #0066cc;\n            text-decoration: none;\n          }\n          .name-cell a::after {\n            content: '';\n            position: absolute;\n            top: 0;\n            left: 0;\n            right: 0;\n            bottom: 0;\n          }\n          .file-row:hover .name-cell a {\n            text-decoration: underline;\n          }\n          .size-cell {\n            width: 25%;\n            padding: 0.375rem 1rem;\n            text-align: left;\n            color: #666;\n            font-variant-numeric: tabular-nums;\n          }\n          .type-cell {\n            width: 25%;\n            padding: 0.375rem 1rem;\n            color: #666;\n            white-space: nowrap;\n          }\n          @media (max-width: 768px) {\n            body {\n              padding: 1rem 0.5rem;\n            }\n            h1 {\n              font-size: 1.125rem;\n              margin-bottom: 0.75rem;\n            }\n            thead {\n              display: none;\n            }\n            .name-cell a,\n            .size-cell,\n            .type-cell {\n              padding: 0.375rem 0.75rem;\n            }\n            .type-cell {\n              display: none;\n            }\n            .name-cell {\n              width: 60%;\n            }\n            .size-cell {\n              width: 40%;\n              text-align: right;\n            }\n          }\n          @media (max-width: 480px) {\n            body {\n              font-size: 13px;\n            }\n            .name-cell a,\n            .size-cell,\n            .type-cell {\n              padding: 0.375rem 0.5rem;\n            }\n            h1 {\n              font-size: 1rem;\n            }\n            .icon {\n              width: 12px;\n              height: 12px;\n            }\n          }\n        </style>\n      </head>\n      <body>\n        <div class=\"container\">\n          <h1>Index of ${pathname}</h1>\n          <table>\n            <thead>\n              <tr>\n                <th class=\"name\">Name</th>\n                <th class=\"size\">Size</th>\n                <th class=\"type\">Type</th>\n              </tr>\n            </thead>\n            <tbody>\n              ${tableRows}\n            </tbody>\n          </table>\n        </div>\n      </body>\n    </html>\n  `)\n}\n\nasync function calculateDirectorySize(dirPath: string): Promise<number> {\n  let totalSize = 0\n\n  try {\n    let dirents = await fsp.readdir(dirPath, { withFileTypes: true })\n\n    for (let dirent of dirents) {\n      let fullPath = path.join(dirPath, dirent.name)\n\n      try {\n        if (dirent.isDirectory()) {\n          totalSize += await calculateDirectorySize(fullPath)\n        } else if (dirent.isFile()) {\n          let stats = await fsp.stat(fullPath)\n          totalSize += stats.size\n        }\n      } catch {\n        // Skip files/folders we can't access\n      }\n    }\n  } catch {\n    // If we can't read the directory, return 0\n  }\n\n  return totalSize\n}\n\nfunction formatFileSize(bytes: number): string {\n  if (bytes === 0) return '0 B'\n  let units = ['B', 'kB', 'MB', 'GB', 'TB']\n  let i = Math.floor(Math.log(bytes) / Math.log(1024))\n  let size = bytes / Math.pow(1024, i)\n  return size.toFixed(i === 0 ? 0 : 1) + ' ' + units[i]\n}\n"
  },
  {
    "path": "packages/static-middleware/src/lib/static.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport * as fs from 'node:fs'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\nimport { describe, it, beforeEach, afterEach } from 'node:test'\n\nimport { createRouter } from '@remix-run/fetch-router'\nimport { formData } from '@remix-run/form-data-middleware'\nimport { methodOverride } from '@remix-run/method-override-middleware'\n\nimport { staticFiles } from './static.ts'\n\ndescribe('staticFiles middleware', () => {\n  let tmpDir: string\n  beforeEach(() => {\n    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'static-middleware-test-'))\n  })\n\n  afterEach(() => {\n    fs.rmSync(tmpDir, { recursive: true, force: true })\n  })\n\n  function createTestFile(filename: string, content: string, date?: Date) {\n    let filePath = path.join(tmpDir, filename)\n    let dir = path.dirname(filePath)\n\n    if (!fs.existsSync(dir)) {\n      fs.mkdirSync(dir, { recursive: true })\n    }\n\n    fs.writeFileSync(filePath, content)\n\n    if (date) {\n      fs.utimesSync(filePath, date, date)\n    }\n\n    return filePath\n  }\n\n  describe('basic functionality', () => {\n    it('serves a file', async () => {\n      createTestFile('test.txt', 'Hello, World!')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/test.txt')\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), 'Hello, World!')\n      assert.equal(response.headers.get('Content-Type'), 'text/plain; charset=utf-8')\n    })\n\n    it('serves a file with HEAD request', async () => {\n      createTestFile('test.txt', 'Hello, World!')\n\n      let router = createRouter()\n      router.head('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/test.txt', { method: 'HEAD' })\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '')\n      assert.equal(response.headers.get('Content-Type'), 'text/plain; charset=utf-8')\n    })\n\n    it('serves files from nested directories', async () => {\n      createTestFile('dir/subdir/file.txt', 'Nested file')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/dir/subdir/file.txt')\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), 'Nested file')\n    })\n\n    it('falls through to handler for non-existent file', async () => {\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Custom Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/nonexistent.txt')\n\n      assert.equal(response.status, 404)\n      assert.equal(await response.text(), 'Custom Fallback Handler')\n    })\n\n    it('falls through to handler when requesting a directory', async () => {\n      let dirPath = path.join(tmpDir, 'subdir')\n      fs.mkdirSync(dirPath)\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir')\n\n      assert.equal(response.status, 404)\n      assert.equal(await response.text(), 'Fallback Handler')\n    })\n  })\n\n  it('supports etag by default', async () => {\n    let lastModified = new Date('2025-01-01')\n    createTestFile('test.txt', 'Hello, World!', lastModified)\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [staticFiles(tmpDir)],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/test.txt')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, World!')\n    let etag = response.headers.get('ETag')\n    assert.ok(etag)\n    assert.equal(etag, 'W/\"13-1735689600000\"')\n\n    let response2 = await router.fetch('https://remix.run/test.txt', {\n      headers: { 'If-None-Match': etag },\n    })\n    assert.equal(response2.status, 304)\n    assert.equal(await response2.text(), '')\n  })\n\n  it('does not send etag if etag is disabled', async () => {\n    let lastModified = new Date('2025-01-01')\n    createTestFile('test.txt', 'Hello, World!', lastModified)\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [staticFiles(tmpDir, { etag: false })],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/test.txt')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, World!')\n    assert.equal(response.headers.get('ETag'), null)\n  })\n\n  it('supports last-modified by default', async () => {\n    let lastModified = new Date('2025-01-01')\n    createTestFile('test.txt', 'Hello, World!', lastModified)\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [staticFiles(tmpDir)],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/test.txt')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, World!')\n    assert.equal(response.headers.get('Last-Modified'), lastModified.toUTCString())\n  })\n\n  it('does not send last-modified if lastModified is disabled', async () => {\n    let lastModified = new Date('2025-01-01')\n    createTestFile('test.txt', 'Hello, World!', lastModified)\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [staticFiles(tmpDir, { lastModified: false })],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/test.txt')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, World!')\n    assert.equal(response.headers.get('Last-Modified'), null)\n  })\n\n  it('does not support accept-ranges by default for compressible files', async () => {\n    let lastModified = new Date('2025-01-01')\n    createTestFile('test.txt', 'Hello, World!', lastModified)\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [staticFiles(tmpDir)],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/test.txt')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, World!')\n    assert.equal(response.headers.get('Accept-Ranges'), null)\n\n    let response2 = await router.fetch('https://remix.run/test.txt', {\n      headers: { Range: 'bytes=0-4' },\n    })\n    assert.equal(response2.status, 200)\n    assert.equal(await response2.text(), 'Hello, World!')\n  })\n\n  it('supports range requests when acceptRanges is explicitly enabled', async () => {\n    let lastModified = new Date('2025-01-01')\n    createTestFile('test.txt', 'Hello, World!', lastModified)\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [staticFiles(tmpDir, { acceptRanges: true })],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/test.txt')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, World!')\n    assert.equal(response.headers.get('Accept-Ranges'), 'bytes')\n\n    let response2 = await router.fetch('https://remix.run/test.txt', {\n      headers: { Range: 'bytes=0-4' },\n    })\n    assert.equal(response2.status, 206)\n    assert.equal(await response2.text(), 'Hello')\n    assert.equal(response2.headers.get('Content-Range'), 'bytes 0-4/13')\n    assert.equal(response2.headers.get('Content-Length'), '5')\n    assert.equal(response2.headers.get('Accept-Ranges'), 'bytes')\n  })\n\n  it('supports range requests with acceptRanges function', async () => {\n    createTestFile('test.txt', 'Hello, World!')\n    createTestFile('video.dat', 'fake video data')\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [\n        staticFiles(tmpDir, {\n          acceptRanges: (file) => file.type.startsWith('video/'),\n        }),\n      ],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    // test.txt should not have ranges (text/plain doesn't match video/* filter)\n    let response = await router.fetch('https://remix.run/test.txt')\n    assert.equal(response.status, 200)\n    assert.equal(await response.text(), 'Hello, World!')\n    assert.equal(response.headers.get('Accept-Ranges'), null)\n\n    let response2 = await router.fetch('https://remix.run/test.txt', {\n      headers: { Range: 'bytes=0-4' },\n    })\n    assert.equal(response2.status, 200)\n    assert.equal(await response2.text(), 'Hello, World!')\n  })\n\n  it('supports cache-control', async () => {\n    createTestFile('test.txt', 'Hello, World!')\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [staticFiles(tmpDir, { cacheControl: 'public, max-age=3600' })],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let response = await router.fetch('https://remix.run/test.txt')\n    assert.equal(response.headers.get('Cache-Control'), 'public, max-age=3600')\n  })\n\n  it('works with multiple static middleware instances', async () => {\n    createTestFile('assets/style.css', 'body {}')\n    createTestFile('images/logo.png', 'PNG data')\n\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [staticFiles(tmpDir)],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let response1 = await router.fetch('https://remix.run/assets/style.css')\n    assert.equal(response1.status, 200)\n    assert.equal(await response1.text(), 'body {}')\n\n    let response2 = await router.fetch('https://remix.run/images/logo.png')\n    assert.equal(response2.status, 200)\n    assert.equal(await response2.text(), 'PNG data')\n  })\n\n  it('works as fallback middleware', async () => {\n    createTestFile('index.html', '<h1>Fallback Handler</h1>')\n\n    let router = createRouter()\n    router.get('/api/users', () => new Response('Users API'))\n    router.get('*path', {\n      middleware: [staticFiles(tmpDir)],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let response1 = await router.fetch('https://remix.run/api/users')\n    assert.equal(await response1.text(), 'Users API')\n\n    let response2 = await router.fetch('https://remix.run/index.html')\n    assert.equal(await response2.text(), '<h1>Fallback Handler</h1>')\n  })\n\n  for (let method of ['POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'] as const) {\n    it(`ignores ${method} requests`, async () => {\n      createTestFile('test.txt', 'Hello, World!')\n\n      let router = createRouter()\n      router.route(method, '/*path', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/test.txt', { method })\n\n      assert.equal(response.status, 404)\n      assert.equal(await response.text(), 'Fallback Handler')\n    })\n  }\n\n  it('prevents path traversal with .. in pathname', async () => {\n    createTestFile('secret.txt', 'Secret content')\n\n    let publicDirName = 'public'\n    createTestFile(`${publicDirName}/allowed.txt`, 'Allowed content')\n\n    let router = createRouter()\n    router.get('/*', {\n      middleware: [staticFiles(path.join(tmpDir, publicDirName))],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    let allowedResponse = await router.fetch('https://remix.run/allowed.txt')\n    assert.equal(allowedResponse.status, 200)\n    assert.equal(await allowedResponse.text(), 'Allowed content')\n\n    let traversalResponse = await router.fetch('https://remix.run/../secret.txt')\n    assert.equal(traversalResponse.status, 404)\n  })\n\n  it('does not support absolute paths in the URL', async () => {\n    let parentDir = path.dirname(tmpDir)\n    let secretFileName = 'secret-outside-root.txt'\n    let secretPath = path.join(parentDir, secretFileName)\n    fs.writeFileSync(secretPath, 'Secret content')\n\n    let router = createRouter()\n    router.get('*path', {\n      middleware: [staticFiles(tmpDir)],\n      action() {\n        return new Response('Fallback Handler', { status: 404 })\n      },\n    })\n\n    try {\n      let response = await router.fetch(`https://remix.run/${secretPath}`)\n      assert.equal(response.status, 404)\n    } finally {\n      fs.unlinkSync(secretPath)\n    }\n  })\n\n  describe('filter option', () => {\n    it('filters files based on custom filter function', async () => {\n      createTestFile('index.html', '<h1>Home</h1>')\n      createTestFile('secret.txt', 'Secret')\n      createTestFile('public.txt', 'Public')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [\n          staticFiles(tmpDir, {\n            filter: (path) => !path.includes('secret'),\n          }),\n        ],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let secretResponse = await router.fetch('https://remix.run/secret.txt')\n      assert.equal(secretResponse.status, 404)\n      assert.equal(await secretResponse.text(), 'Fallback Handler')\n\n      let publicResponse = await router.fetch('https://remix.run/public.txt')\n      assert.equal(publicResponse.status, 200)\n      assert.equal(await publicResponse.text(), 'Public')\n    })\n  })\n\n  describe('index option', () => {\n    it('serves default index.html when requesting a directory', async () => {\n      createTestFile('subdir/index.html', '<h1>Index Page</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir/')\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '<h1>Index Page</h1>')\n      assert.equal(response.headers.get('Content-Type'), 'text/html; charset=utf-8')\n    })\n\n    it('serves default index.html when requesting a directory without trailing slash', async () => {\n      createTestFile('subdir/index.html', '<h1>Index Page</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir')\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '<h1>Index Page</h1>')\n      assert.equal(response.headers.get('Content-Type'), 'text/html; charset=utf-8')\n    })\n\n    it('serves default index.htm when index.html does not exist', async () => {\n      createTestFile('subdir/index.htm', '<h1>HTM Index Page</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir/')\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '<h1>HTM Index Page</h1>')\n      assert.equal(response.headers.get('Content-Type'), 'text/html; charset=utf-8')\n    })\n\n    it('prefers index.html over index.htm when both exist', async () => {\n      createTestFile('subdir/index.html', '<h1>HTML Index</h1>')\n      createTestFile('subdir/index.htm', '<h1>HTM Index</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir/')\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '<h1>HTML Index</h1>')\n    })\n\n    it('falls through when directory has no index file', async () => {\n      let dirPath = path.join(tmpDir, 'subdir')\n      fs.mkdirSync(dirPath)\n      createTestFile('subdir/other.txt', 'Not an index file')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir/')\n      assert.equal(response.status, 404)\n      assert.equal(await response.text(), 'Fallback Handler')\n    })\n\n    it('serves custom index file when specified', async () => {\n      createTestFile('subdir/default.html', '<h1>Custom Default Page</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir, { index: ['default.html'] })],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir/')\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '<h1>Custom Default Page</h1>')\n    })\n\n    it('tries custom index files in order', async () => {\n      createTestFile('subdir/home.html', '<h1>Home Page</h1>')\n      createTestFile('subdir/default.html', '<h1>Default Page</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir, { index: ['index.html', 'home.html', 'default.html'] })],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir/')\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '<h1>Home Page</h1>')\n    })\n\n    it('serves root directory index file', async () => {\n      createTestFile('index.html', '<h1>Root Index</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/')\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '<h1>Root Index</h1>')\n    })\n\n    it('supports empty index array to disable index file serving', async () => {\n      createTestFile('subdir/index.html', '<h1>Index Page</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir, { index: [] })],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir/')\n      assert.equal(response.status, 404)\n      assert.equal(await response.text(), 'Fallback Handler')\n    })\n\n    it('supports index: false to disable index file serving', async () => {\n      createTestFile('subdir/index.html', '<h1>Index Page</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir, { index: false })],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir/')\n      assert.equal(response.status, 404)\n      assert.equal(await response.text(), 'Fallback Handler')\n    })\n\n    it('supports index: true to use default index files', async () => {\n      createTestFile('subdir/index.html', '<h1>Index Page</h1>')\n\n      let router = createRouter()\n      router.get('/*', {\n        middleware: [staticFiles(tmpDir, { index: true })],\n        action() {\n          return new Response('Fallback Handler', { status: 404 })\n        },\n      })\n\n      let response = await router.fetch('https://remix.run/subdir/')\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), '<h1>Index Page</h1>')\n    })\n  })\n\n  describe('works with method-override middleware', () => {\n    it('ignores overridden POST requests', async () => {\n      createTestFile('test.txt', 'Hello, World!')\n\n      let router = createRouter({\n        middleware: [formData(), methodOverride()],\n      })\n      router.post('/*path', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('POST handler called', { status: 200 })\n        },\n      })\n\n      let formDataPayload = new FormData()\n      formDataPayload.append('_method', 'POST')\n      formDataPayload.append('name', 'test')\n\n      let response = await router.fetch('https://remix.run/test.txt', {\n        method: 'POST',\n        body: formDataPayload,\n      })\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), 'POST handler called')\n    })\n\n    it('serves files with overridden GET requests', async () => {\n      createTestFile('test.txt', 'Hello, World!')\n\n      let router = createRouter({\n        middleware: [formData(), methodOverride()],\n      })\n      router.get('/*path', {\n        middleware: [staticFiles(tmpDir)],\n        action() {\n          return new Response('GET handler fallback', { status: 404 })\n        },\n      })\n\n      let formDataPayload = new FormData()\n      formDataPayload.append('_method', 'GET')\n\n      let response = await router.fetch('https://remix.run/test.txt', {\n        method: 'POST',\n        body: formDataPayload,\n      })\n\n      assert.equal(response.status, 200)\n      assert.equal(await response.text(), 'Hello, World!')\n      assert.equal(response.headers.get('Content-Type'), 'text/plain; charset=utf-8')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/static-middleware/src/lib/static.ts",
    "content": "import * as path from 'node:path'\nimport * as fsp from 'node:fs/promises'\nimport { openLazyFile } from '@remix-run/fs'\nimport type { Middleware } from '@remix-run/fetch-router'\nimport { createFileResponse as sendFile, type FileResponseOptions } from '@remix-run/response/file'\n\nimport { generateDirectoryListing } from './directory-listing.ts'\n\n/**\n * Function that determines if HTTP Range requests should be supported for a given file.\n *\n * @param file The File object being served\n * @returns true if range requests should be supported\n */\nexport type AcceptRangesFunction = (file: File) => boolean\n\n/**\n * Options for the {@link staticFiles} middleware in addition to {@link FileResponseOptions}.\n */\nexport interface StaticFilesOptions extends Omit<FileResponseOptions, 'acceptRanges'> {\n  /**\n   * Filter function to determine which files should be served.\n   *\n   * @param path The relative path being requested\n   * @returns Whether to serve the file\n   */\n  filter?: (path: string) => boolean\n\n  /**\n   * Whether to support HTTP Range requests for partial content.\n   *\n   * Can be a boolean or a function that receives the file.\n   * When enabled, includes Accept-Ranges header and handles Range requests\n   * with 206 Partial Content responses.\n   *\n   * Defaults to enabling ranges only for non-compressible MIME types,\n   * as defined by `isCompressibleMimeType()` from `@remix-run/mime`.\n   *\n   * Note: Range requests and compression are mutually exclusive. When\n   * `Accept-Ranges: bytes` is present in the response headers, the compression\n   * middleware will not compress the response. This is why the default behavior\n   * enables ranges only for non-compressible types.\n   *\n   * @example\n   * // Force range request support for all files\n   * acceptRanges: true\n   *\n   * @example\n   * // Enable ranges for videos only\n   * acceptRanges: (file) => file.type.startsWith('video/')\n   */\n  acceptRanges?: boolean | AcceptRangesFunction\n\n  /**\n   * Files to try and serve as the index file when the request path targets a directory.\n   *\n   * - `true`: Use default index files `['index.html', 'index.htm']`\n   * - `false`: Disable index file serving\n   * - `string[]`: Custom list of index files to try in order\n   *\n   * @default true\n   */\n  index?: boolean | string[]\n  /**\n   * Whether to return an HTML page listing the files in a directory when the request path\n   * targets a directory. If both this and `index` are set, `index` takes precedence.\n   *\n   * @default false\n   */\n  listFiles?: boolean\n}\n\n/**\n * Creates a middleware that serves static files from the filesystem.\n *\n * Uses the URL pathname to resolve files, removing the leading slash to make it a relative path.\n * The middleware always falls through to the handler if the file is not found or an error occurs.\n *\n * @param root The root directory to serve files from (absolute or relative to cwd)\n * @param options Configuration for file responses\n * @returns The static files middleware\n */\nexport function staticFiles(root: string, options: StaticFilesOptions = {}): Middleware {\n  // Ensure root is an absolute path\n  root = path.resolve(root)\n\n  let { acceptRanges, filter, index: indexOption, listFiles, ...fileOptions } = options\n\n  // Normalize index option\n  let index: string[]\n  if (indexOption === false) {\n    index = []\n  } else if (indexOption === true || indexOption === undefined) {\n    index = ['index.html', 'index.htm']\n  } else {\n    index = indexOption\n  }\n\n  return async (context, next) => {\n    if (context.method !== 'GET' && context.method !== 'HEAD') {\n      return next()\n    }\n\n    let relativePath = context.url.pathname.replace(/^\\/+/, '')\n\n    if (filter && !filter(relativePath)) {\n      return next()\n    }\n\n    let targetPath = path.join(root, relativePath)\n    let filePath: string | undefined\n\n    try {\n      let stats = await fsp.stat(targetPath)\n\n      if (stats.isFile()) {\n        filePath = targetPath\n      } else if (stats.isDirectory()) {\n        // Try each index file in turn\n        for (let indexFile of index) {\n          let indexPath = path.join(targetPath, indexFile)\n          try {\n            let indexStats = await fsp.stat(indexPath)\n            if (indexStats.isFile()) {\n              filePath = indexPath\n              break\n            }\n          } catch {\n            // Index file doesn't exist, continue to next\n          }\n        }\n\n        // If no index file found and listFiles is enabled, show directory listing\n        if (!filePath && listFiles) {\n          return generateDirectoryListing(targetPath, context.url.pathname)\n        }\n      }\n    } catch {\n      // Path doesn't exist or isn't accessible, fall through\n    }\n\n    if (filePath) {\n      let fileName = path.relative(root, filePath)\n      let lazyFile = openLazyFile(filePath, { name: fileName })\n\n      let finalFileOptions: FileResponseOptions = { ...fileOptions }\n\n      // If acceptRanges is a function, evaluate it with the lazyFile\n      // Otherwise, pass it directly to sendFile\n      if (typeof acceptRanges === 'function') {\n        finalFileOptions.acceptRanges = acceptRanges(lazyFile)\n      } else if (acceptRanges !== undefined) {\n        finalFileOptions.acceptRanges = acceptRanges\n      }\n\n      return sendFile(lazyFile, context.request, finalFileOptions)\n    }\n\n    return next()\n  }\n}\n"
  },
  {
    "path": "packages/static-middleware/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"global.d.ts\", \"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/static-middleware/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/tar-parser/.changes/README.md",
    "content": "# Changes Directory\n\nSee the [Contributing Guide](../../../CONTRIBUTING.md#adding-a-change-file) for documentation on how to add change files.\n"
  },
  {
    "path": "packages/tar-parser/CHANGELOG.md",
    "content": "# `tar-parser` CHANGELOG\n\nThis is the changelog for [`tar-parser`](https://github.com/remix-run/remix/tree/main/packages/tar-parser). It follows [semantic versioning](https://semver.org/).\n\n## v0.7.0 (2025-11-20)\n\n- Update dev dependencies to use `@remix-run/fs` instead of `@remix-run/lazy-file/fs`.\n\n## v0.6.0 (2025-11-04)\n\n- Build using `tsc` instead of `esbuild`. This means modules in the `dist` directory now mirror the layout of modules in the `src` directory.\n\n## v0.5.0 (2025-10-22)\n\n- 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()`.\n\n## v0.4.0 (2025-07-24)\n\n- Renamed package from `@mjackson/tar-parser` to `@remix-run/tar-parser`\n\n## v0.3.0 (2025-06-06)\n\n- Add `/src` to npm package, so \"go to definition\" goes to the actual source\n- Use one set of types for all built files, instead of separate types for ESM and CJS\n- Build using esbuild directly instead of tsup\n\n## v0.2.2 (2025-02-04)\n\n- Add `Promise<void>` to `TarEntryHandler` return type\n\n## v0.2.1 (2025-01-24)\n\n- Add support for environments that do not support `ReadableStream.prototype[Symbol.asyncIterator]` (i.e. Safari), see #46\n\n## v0.2.0 (2025-01-07)\n\n- Fix a bug that hangs the process when trying to read zero-length entries.\n\n## v0.1.0 (2024-12-06)\n\n- Initial release\n"
  },
  {
    "path": "packages/tar-parser/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/tar-parser/README.md",
    "content": "# tar-parser\n\nStreaming [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.\n\n## Features\n\n- **Universal Runtime** - Runs anywhere JavaScript runs\n- **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\n- **Format Support** - Supports POSIX, GNU, and PAX tar formats\n- **Memory Efficient** - Does not buffer anything in normal usage\n- **Zero Dependencies** - No external dependencies\n\n## Installation\n\n```sh\nnpm i remix\n```\n\n## Usage\n\nThe main parser interface is the `parseTar(archive, handler)` function:\n\n```ts\nimport { parseTar } from 'remix/tar-parser'\n\nlet response = await fetch('https://github.com/remix-run/remix/archive/refs/heads/main.tar.gz')\n\nawait parseTar(response.body.pipeThrough(new DecompressionStream('gzip')), (entry) => {\n  console.log(entry.name, entry.size)\n})\n```\n\nIf you're parsing an archive with filename encodings other than UTF-8, use the `filenameEncoding` option:\n\n```ts\nlet response = await fetch(/* ... */)\n\nawait parseTar(response.body, { filenameEncoding: 'latin1' }, (entry) => {\n  console.log(entry.name, entry.size)\n})\n```\n\n## Benchmark\n\n`tar-parser` performs on par with other popular tar parsing libraries on Node.js.\n\n```\n> @remix-run/tar-parser@0.0.0 bench /Users/michael/Projects/remix-the-web/packages/tar-parser\n> node ./bench/runner.ts\n\nPlatform: Darwin (24.0.0)\nCPU: Apple M1 Pro\nDate: 12/6/2024, 11:00:55 AM\nNode.js v22.8.0\n┌────────────┬────────────────────┐\n│ (index)    │ lodash npm package │\n├────────────┼────────────────────┤\n│ tar-parser │ '6.23 ms ± 0.58'   │\n│ tar-stream │ '6.72 ms ± 2.24'   │\n│ node-tar   │ '6.49 ms ± 0.44'   │\n└────────────┴────────────────────┘\n```\n\n## Related Packages\n\n- [`multipart-parser`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser) - Fast, streaming multipart parser for JavaScript\n\n## Credits\n\n`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.\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/tar-parser/bench/package.json",
    "content": "{\n  \"name\": \"tar-parser-bench\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@remix-run/fs\": \"workspace:^\",\n    \"@remix-run/tar-parser\": \"workspace:^\",\n    \"gunzip-maybe\": \"^1.4.2\",\n    \"tar\": \"^7.4.3\",\n    \"tar-stream\": \"^3.1.7\"\n  },\n  \"devDependencies\": {\n    \"@types/gunzip-maybe\": \"^1.4.2\",\n    \"@types/tar\": \"^6.1.13\",\n    \"@types/tar-stream\": \"^3.1.3\"\n  }\n}\n"
  },
  {
    "path": "packages/tar-parser/bench/parsers/node-tar.ts",
    "content": "import * as fs from 'node:fs'\nimport * as tar from 'tar'\n\nexport async function parse(filename: string): Promise<number> {\n  let stream = fs.createReadStream(filename)\n\n  let start = performance.now()\n\n  await new Promise<void>((resolve, reject) => {\n    stream\n      .pipe(tar.t())\n      .on('entry', (entry) => {\n        entry.resume()\n      })\n      .on('finish', () => {\n        resolve()\n      })\n  })\n\n  return performance.now() - start\n}\n"
  },
  {
    "path": "packages/tar-parser/bench/parsers/tar-parser.ts",
    "content": "import { parseTar } from '@remix-run/tar-parser'\nimport { openLazyFile } from '@remix-run/fs'\n\nexport async function parse(filename: string): Promise<number> {\n  let stream = openLazyFile(filename).stream().pipeThrough(new DecompressionStream('gzip'))\n\n  let start = performance.now()\n\n  await parseTar(stream, (_entry) => {\n    // Do nothing\n  })\n\n  return performance.now() - start\n}\n"
  },
  {
    "path": "packages/tar-parser/bench/parsers/tar-stream.ts",
    "content": "import * as fs from 'node:fs'\nimport gunzip from 'gunzip-maybe'\nimport tar from 'tar-stream'\n\nexport async function parse(filename: string): Promise<number> {\n  let stream = fs.createReadStream(filename).pipe(gunzip())\n\n  let start = performance.now()\n\n  await new Promise<void>((resolve, reject) => {\n    let extract = tar.extract()\n\n    extract.on('error', reject)\n\n    extract.on('entry', function (_header, stream, next) {\n      stream.on('end', function () {\n        next() // ready for next entry\n      })\n\n      stream.resume() // just auto drain the stream\n    })\n\n    extract.on('finish', function () {\n      resolve()\n    })\n\n    stream.pipe(extract)\n  })\n\n  return performance.now() - start\n}\n"
  },
  {
    "path": "packages/tar-parser/bench/runner.ts",
    "content": "import * as os from 'node:os'\nimport * as process from 'node:process'\n\nimport { fixtures } from '../test/utils.ts'\n\nimport * as nodeTar from './parsers/node-tar.ts'\nimport * as tarParser from './parsers/tar-parser.ts'\nimport * as tarStream from './parsers/tar-stream.ts'\n\nconst benchmarks = [{ name: 'lodash npm package', filename: fixtures.lodashNpmPackage }]\n\ninterface Parser {\n  parse(filename: string): Promise<number>\n}\n\nasync function runParserBenchmarks(\n  parser: Parser,\n  times = 1000,\n): Promise<BenchmarkResults[string]> {\n  let results: BenchmarkResults[string] = {}\n\n  for (let benchmark of benchmarks) {\n    let measurements: number[] = []\n    for (let i = 0; i < times; ++i) {\n      measurements.push(await parser.parse(benchmark.filename))\n    }\n\n    results[benchmark.name] = getMeanAndStdDev(measurements)\n  }\n\n  return results\n}\n\nfunction getMeanAndStdDev(measurements: number[]): string {\n  let mean = measurements.reduce((a, b) => a + b, 0) / measurements.length\n  let variance = measurements.reduce((a, b) => a + (b - mean) ** 2, 0) / measurements.length\n  let stdDev = Math.sqrt(variance)\n  return mean.toFixed(2) + ' ms ± ' + stdDev.toFixed(2)\n}\n\ninterface BenchmarkResults {\n  [parserName: string]: {\n    [benchmarkName: string]: string\n  }\n}\n\nasync function runBenchmarks(parserName?: string): Promise<BenchmarkResults> {\n  let results: BenchmarkResults = {}\n\n  if (parserName === 'tar-parser' || parserName === undefined) {\n    results['tar-parser'] = await runParserBenchmarks(tarParser)\n  }\n  if (parserName === 'tar-stream' || parserName === undefined) {\n    results['tar-stream'] = await runParserBenchmarks(tarStream)\n  }\n  if (parserName === 'node-tar' || parserName === undefined) {\n    results['node-tar'] = await runParserBenchmarks(nodeTar)\n  }\n\n  return results\n}\n\nfunction printResults(results: BenchmarkResults) {\n  console.log(`Platform: ${os.type()} (${os.release()})`)\n  console.log(`CPU: ${os.cpus()[0].model}`)\n  console.log(`Date: ${new Date().toLocaleString()}`)\n  console.log(`Node.js ${process.version}`)\n\n  console.table(results)\n}\n\nrunBenchmarks(process.argv[2]).then(printResults, (error) => {\n  console.error(error)\n  process.exit(1)\n})\n"
  },
  {
    "path": "packages/tar-parser/package.json",
    "content": "{\n  \"name\": \"@remix-run/tar-parser\",\n  \"version\": \"0.7.0\",\n  \"description\": \"A fast, efficient parser for tar streams in any JavaScript environment\",\n  \"author\": \"Michael Jackson <mjijackson@gmail.com>\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/remix-run/remix.git\",\n    \"directory\": \"packages/tar-parser\"\n  },\n  \"homepage\": \"https://github.com/remix-run/remix/tree/main/packages/tar-parser#readme\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"dist\",\n    \"src\",\n    \"!src/**/*.test.ts\"\n  ],\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./src/index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"publishConfig\": {\n    \"exports\": {\n      \".\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"./package.json\": \"./package.json\"\n    }\n  },\n  \"devDependencies\": {\n    \"@remix-run/fs\": \"workspace:*\",\n    \"@types/node\": \"catalog:\",\n    \"@typescript/native-preview\": \"catalog:\"\n  },\n  \"scripts\": {\n    \"bench\": \"node ./bench/runner.ts\",\n    \"build\": \"tsgo -p tsconfig.build.json\",\n    \"clean\": \"git clean -fdX\",\n    \"prepublishOnly\": \"pnpm run build\",\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"keywords\": [\n    \"tar\",\n    \"archive\",\n    \"parser\",\n    \"stream\"\n  ]\n}\n"
  },
  {
    "path": "packages/tar-parser/src/globals.ts",
    "content": "// This file provides global type augmentation for ReadableStream async iteration.\n// See https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/62651\n\ndeclare global {\n  interface ReadableStream<R = any> {\n    values(options?: { preventCancel?: boolean }): AsyncIterableIterator<R>\n    [Symbol.asyncIterator](): AsyncIterableIterator<R>\n  }\n}\n\nexport {}\n"
  },
  {
    "path": "packages/tar-parser/src/index.ts",
    "content": "import './globals.ts'\n\nexport {\n  TarParseError,\n  type TarHeader,\n  type ParseTarHeaderOptions,\n  parseTarHeader,\n  type ParseTarOptions,\n  parseTar,\n  type TarParserOptions,\n  TarParser,\n  TarEntry,\n} from './lib/tar.ts'\n"
  },
  {
    "path": "packages/tar-parser/src/lib/read-stream.ts",
    "content": "// We need this little helper for environments that do not support\n// ReadableStream.prototype[Symbol.asyncIterator] yet. See #46\nexport async function* readStream(stream: ReadableStream<Uint8Array>): AsyncIterable<Uint8Array> {\n  let reader = stream.getReader()\n\n  while (true) {\n    let result = await reader.read()\n    if (result.done) break\n    yield result.value\n  }\n}\n"
  },
  {
    "path": "packages/tar-parser/src/lib/tar.test.ts",
    "content": "import * as assert from 'node:assert/strict'\nimport { describe, it } from 'node:test'\n\nimport { fixtures, readFixture } from '../../test/utils.ts'\n\nimport { type TarHeader, parseTar } from './tar.ts'\n\nasync function bufferBytes(stream: ReadableStream<Uint8Array>): Promise<Uint8Array<ArrayBuffer>> {\n  let chunks: Uint8Array[] = []\n  let length = 0\n\n  for await (let chunk of stream) {\n    chunks.push(chunk)\n    length += chunk.byteLength\n  }\n\n  let result = new Uint8Array(length)\n  let offset = 0\n\n  for (let chunk of chunks) {\n    result.set(chunk, offset)\n    offset += chunk.byteLength\n  }\n\n  return result\n}\n\nasync function bufferString(\n  stream: ReadableStream<Uint8Array>,\n  encoding = 'utf-8',\n): Promise<string> {\n  let decoder = new TextDecoder(encoding)\n  let string = ''\n\n  for await (let chunk of stream) {\n    string += decoder.decode(chunk, { stream: true })\n  }\n\n  string += decoder.decode()\n\n  return string\n}\n\nasync function computeHash(\n  buffer: Uint8Array<ArrayBuffer>,\n  algorithm = 'SHA-256',\n): Promise<string> {\n  let digest = await crypto.subtle.digest(algorithm, buffer)\n  return Array.from(new Uint8Array(digest))\n    .map((byte) => byte.toString(16).padStart(2, '0'))\n    .join('')\n    .slice(0, 8)\n}\n\ndescribe('TarParser', () => {\n  it('parses express-4.21.1.tgz', async () => {\n    let entries: Record<string, string> = {}\n    await parseTar(readFixture(fixtures.expressNpmPackage), async (entry) => {\n      let hash = await computeHash(await bufferBytes(entry.body))\n      entries[entry.name] = hash\n    })\n\n    assert.deepEqual(entries, {\n      'package/LICENSE': '95a57628',\n      'package/lib/application.js': '5901b32f',\n      'package/lib/express.js': '2f25585c',\n      'package/index.js': '4d2f5afc',\n      'package/lib/router/index.js': '19c5ca9b',\n      'package/lib/middleware/init.js': '48c1d12f',\n      'package/lib/router/layer.js': 'c90709dc',\n      'package/lib/middleware/query.js': '6edce396',\n      'package/lib/request.js': '64ac1075',\n      'package/lib/response.js': '4b5c338c',\n      'package/lib/router/route.js': '86db1235',\n      'package/lib/utils.js': '9035c6d9',\n      'package/lib/view.js': 'ec627880',\n      'package/package.json': '774eaac2',\n      'package/History.md': 'ca257313',\n      'package/Readme.md': '016f344e',\n    })\n  })\n\n  it('parses fetch-proxy-0.1.0.tar.gz', async () => {\n    let count = 0\n    await parseTar(readFixture(fixtures.fetchProxyGithubArchive), async (entry) => {\n      // Drain the body stream to avoid memory accumulation\n      for await (let _ of entry.body) {\n      }\n      count++\n    })\n\n    assert.equal(count, 192)\n  })\n\n  it('parses lodash-4.17.21.tgz', async () => {\n    let count = 0\n    await parseTar(readFixture(fixtures.lodashNpmPackage), async (entry) => {\n      // Drain the body stream to avoid memory accumulation\n      for await (let _ of entry.body) {\n      }\n      count++\n    })\n\n    assert.equal(count, 1054)\n  })\n\n  it('parses npm-11.0.0.tgz', async () => {\n    let count = 0\n    await parseTar(readFixture(fixtures.npmNpmPackage), async (entry) => {\n      // Drain the body stream to avoid memory accumulation\n      for await (let _ of entry.body) {\n      }\n      count++\n    })\n\n    assert.equal(count, 2368)\n  })\n})\n\ndescribe('tar-stream test cases', () => {\n  it('parses one-file.tar', async () => {\n    let entries: [TarHeader, string][] = []\n    await parseTar(readFixture(fixtures.oneFile), async (entry) => {\n      entries.push([entry.header, await bufferString(entry.body)])\n    })\n\n    assert.deepEqual(entries, [\n      [\n        {\n          name: 'test.txt',\n          mode: 0o644,\n          uid: 501,\n          gid: 20,\n          size: 12,\n          mtime: 1387580181,\n          type: 'file',\n          linkname: null,\n          uname: 'maf',\n          gname: 'staff',\n          devmajor: 0,\n          devminor: 0,\n          pax: null,\n        },\n        'hello world\\n',\n      ],\n    ])\n  })\n\n  it('parses multi-file.tar', async () => {\n    let entries: [TarHeader, string][] = []\n    await parseTar(readFixture(fixtures.multiFile), async (entry) => {\n      entries.push([entry.header, await bufferString(entry.body)])\n    })\n\n    assert.deepEqual(entries, [\n      [\n        {\n          name: 'file-1.txt',\n          mode: 0o644,\n          uid: 501,\n          gid: 20,\n          size: 12,\n          mtime: 1387580181,\n          type: 'file',\n          linkname: null,\n          uname: 'maf',\n          gname: 'staff',\n          devmajor: 0,\n          devminor: 0,\n          pax: null,\n        },\n        'i am file-1\\n',\n      ],\n      [\n        {\n          name: 'file-2.txt',\n          mode: 0o644,\n          uid: 501,\n          gid: 20,\n          size: 12,\n          mtime: 1387580181,\n          type: 'file',\n          linkname: null,\n          uname: 'maf',\n          gname: 'staff',\n          devmajor: 0,\n          devminor: 0,\n          pax: null,\n        },\n        'i am file-2\\n',\n      ],\n    ])\n  })\n\n  it('parses pax.tar', async () => {\n    let entries: [TarHeader, string][] = []\n    await parseTar(readFixture(fixtures.pax), async (entry) => {\n      entries.push([entry.header, await bufferString(entry.body)])\n    })\n\n    assert.deepEqual(entries, [\n      [\n        {\n          name: 'pax.txt',\n          mode: 0o644,\n          uid: 501,\n          gid: 20,\n          size: 12,\n          mtime: 1387580181,\n          type: 'file',\n          linkname: null,\n          uname: 'maf',\n          gname: 'staff',\n          devmajor: 0,\n          devminor: 0,\n          pax: {\n            path: 'pax.txt',\n            special: 'sauce',\n          },\n        },\n        'hello world\\n',\n      ],\n    ])\n  })\n\n  it('parses types.tar', async () => {\n    let headers: TarHeader[] = []\n    await parseTar(readFixture(fixtures.types), async (entry) => {\n      headers.push(entry.header)\n    })\n\n    assert.deepEqual(headers, [\n      {\n        name: 'directory',\n        mode: 0o755,\n        uid: 501,\n        gid: 20,\n        size: 0,\n        mtime: 1387580181,\n        type: 'directory',\n        linkname: null,\n        uname: 'maf',\n        gname: 'staff',\n        devmajor: 0,\n        devminor: 0,\n        pax: null,\n      },\n      {\n        name: 'directory-link',\n        mode: 0o755,\n        uid: 501,\n        gid: 20,\n        size: 0,\n        mtime: 1387580181,\n        type: 'symlink',\n        linkname: 'directory',\n        uname: 'maf',\n        gname: 'staff',\n        devmajor: 0,\n        devminor: 0,\n        pax: null,\n      },\n    ])\n  })\n\n  it('parses long-name.tar', async () => {\n    let entries: [TarHeader, string][] = []\n    await parseTar(readFixture(fixtures.longName), async (entry) => {\n      entries.push([entry.header, await bufferString(entry.body)])\n    })\n\n    assert.deepEqual(entries, [\n      [\n        {\n          name: 'my/file/is/longer/than/100/characters/and/should/use/the/prefix/header/foobarbaz/foobarbaz/foobarbaz/foobarbaz/foobarbaz/foobarbaz/filename.txt',\n          mode: 0o644,\n          uid: 501,\n          gid: 20,\n          size: 16,\n          mtime: 1387580181,\n          type: 'file',\n          linkname: null,\n          uname: 'maf',\n          gname: 'staff',\n          devmajor: 0,\n          devminor: 0,\n          pax: null,\n        },\n        'hello long name\\n',\n      ],\n    ])\n  })\n\n  it('parses unicode-bsd.tar', async () => {\n    let headers: TarHeader[] = []\n    await parseTar(readFixture(fixtures.unicodeBsd), async (entry) => {\n      headers.push(entry.header)\n    })\n\n    assert.deepEqual(headers, [\n      {\n        name: 'høllø.txt',\n        mode: 0o644,\n        uid: 501,\n        gid: 20,\n        size: 4,\n        mtime: 1387588646,\n        type: 'file',\n        linkname: null,\n        uname: 'maf',\n        gname: 'staff',\n        devmajor: 0,\n        devminor: 0,\n        pax: {\n          'SCHILY.dev': '16777217',\n          'SCHILY.ino': '3599143',\n          'SCHILY.nlink': '1',\n          atime: '1387589077',\n          ctime: '1387588646',\n          path: 'høllø.txt',\n        },\n      },\n    ])\n  })\n\n  it('parses unicode.tar', async () => {\n    let headers: TarHeader[] = []\n    await parseTar(readFixture(fixtures.unicode), async (entry) => {\n      headers.push(entry.header)\n    })\n\n    assert.deepEqual(headers, [\n      {\n        name: 'høstål.txt',\n        mode: 0o644,\n        uid: 501,\n        gid: 20,\n        size: 8,\n        mtime: 1387580181,\n        type: 'file',\n        linkname: null,\n        uname: 'maf',\n        gname: 'staff',\n        devmajor: 0,\n        devminor: 0,\n        pax: { path: 'høstål.txt' },\n      },\n    ])\n  })\n\n  it('parses name-is-100.tar', async () => {\n    let entries: [number, string][] = []\n    await parseTar(readFixture(fixtures.nameIs100), async (entry) => {\n      entries.push([entry.header.name.length, await bufferString(entry.body)])\n    })\n\n    assert.deepEqual(entries, [[100, 'hello\\n']])\n  })\n\n  it('parses space.tar', async () => {\n    let entries: [TarHeader, string][] = []\n    await parseTar(readFixture(fixtures.space), async (entry) => {\n      entries.push([entry.header, await bufferString(entry.body)])\n    })\n\n    assert.equal(entries.length, 4)\n  })\n\n  it('parses gnu-long-path.tar', async () => {\n    let entries: [TarHeader, string][] = []\n    await parseTar(readFixture(fixtures.gnuLongPath), async (entry) => {\n      entries.push([entry.header, await bufferString(entry.body)])\n    })\n\n    assert.equal(entries.length, 1)\n  })\n\n  it('parses base-256-uid-gid.tar', async () => {\n    let headers: TarHeader[] = []\n    await parseTar(readFixture(fixtures.base256UidGid), async (entry) => {\n      headers.push(entry.header)\n    })\n\n    assert.equal(headers.length, 1)\n    assert.equal(headers[0].uid, 116435139)\n    assert.equal(headers[0].gid, 1876110778)\n  })\n\n  it('parses base-256-size.tar', async () => {\n    let headers: TarHeader[] = []\n    await parseTar(readFixture(fixtures.base256Size), async (entry) => {\n      headers.push(entry.header)\n    })\n\n    assert.deepEqual(headers, [\n      {\n        name: 'test.txt',\n        mode: 0o644,\n        uid: 501,\n        gid: 20,\n        size: 12,\n        mtime: 1387580181,\n        type: 'file',\n        linkname: null,\n        uname: 'maf',\n        gname: 'staff',\n        devmajor: 0,\n        devminor: 0,\n        pax: null,\n      },\n    ])\n  })\n\n  it('parses latin1.tar', async () => {\n    let entries: [TarHeader, string][] = []\n    await parseTar(readFixture(fixtures.latin1), { filenameEncoding: 'latin1' }, async (entry) => {\n      entries.push([entry.header, await bufferString(entry.body)])\n    })\n\n    assert.deepEqual(entries, [\n      [\n        {\n          name: \"En français, s'il vous plaît?.txt\",\n          mode: 0o644,\n          uid: 0,\n          gid: 0,\n          size: 14,\n          mtime: 1495941034,\n          type: 'file',\n          linkname: null,\n          uname: 'root',\n          gname: 'root',\n          devmajor: 0,\n          devminor: 0,\n          pax: null,\n        },\n        'Hello, world!\\n',\n      ],\n    ])\n  })\n\n  it('throws when parsing incomplete.tar', async () => {\n    await assert.rejects(\n      async () => {\n        await parseTar(readFixture(fixtures.incomplete), () => {})\n      },\n      {\n        name: 'TarParseError',\n        message: 'Unexpected end of archive',\n      },\n    )\n  })\n\n  it('parses gnu.tar', async () => {\n    let entries: [TarHeader, string][] = []\n    await parseTar(readFixture(fixtures.gnu), async (entry) => {\n      entries.push([entry.header, await bufferString(entry.body)])\n    })\n\n    assert.deepEqual(entries, [\n      [\n        {\n          name: 'test.txt',\n          mode: 0o644,\n          uid: 12345,\n          gid: 67890,\n          size: 14,\n          mtime: 1559239869,\n          type: 'file',\n          linkname: null,\n          uname: 'myuser',\n          gname: 'mygroup',\n          devmajor: 0,\n          devminor: 0,\n          pax: null,\n        },\n        'Hello, world!\\n',\n      ],\n    ])\n  })\n\n  it('parses gnu-incremental.tar', async () => {\n    let entries: [TarHeader, string][] = []\n    await parseTar(readFixture(fixtures.gnuIncremental), async (entry) => {\n      entries.push([entry.header, await bufferString(entry.body)])\n    })\n\n    assert.deepEqual(entries, [\n      [\n        {\n          name: 'test.txt',\n          mode: 0o644,\n          uid: 12345,\n          gid: 67890,\n          size: 14,\n          mtime: 1559239869,\n          type: 'file',\n          linkname: null,\n          uname: 'myuser',\n          gname: 'mygroup',\n          devmajor: 0,\n          devminor: 0,\n          pax: null,\n        },\n        'Hello, world!\\n',\n      ],\n    ])\n  })\n})\n"
  },
  {
    "path": "packages/tar-parser/src/lib/tar.ts",
    "content": "import { readStream } from './read-stream.ts'\nimport {\n  buffersEqual,\n  concatChunks,\n  computeChecksum,\n  decodeLongPath,\n  decodePax,\n  getOctal,\n  getString,\n  overflow,\n} from './utils.ts'\n\nconst TarBlockSize = 512\n\n/**\n * An error thrown when parsing a tar archive fails.\n */\nexport class TarParseError extends Error {\n  /**\n   * @param message The error message\n   */\n  constructor(message: string) {\n    super(message)\n    this.name = 'TarParseError'\n  }\n}\n\n/**\n * The parsed header of a tar entry.\n */\nexport interface TarHeader {\n  /**\n   * Entry path stored in the archive.\n   */\n  name: string\n\n  /**\n   * File mode parsed from the header, or `null` when unavailable.\n   */\n  mode: number | null\n\n  /**\n   * Numeric user ID parsed from the header, or `null` when unavailable.\n   */\n  uid: number | null\n\n  /**\n   * Numeric group ID parsed from the header, or `null` when unavailable.\n   */\n  gid: number | null\n\n  /**\n   * Entry size in bytes.\n   */\n  size: number\n\n  /**\n   * Last modification time parsed from the header, or `null` when unavailable.\n   */\n  mtime: number | null\n\n  /**\n   * Normalized entry type such as `file` or `directory`.\n   */\n  type: string\n\n  /**\n   * Linked path target for link entries, or `null` when not present.\n   */\n  linkname: string | null\n\n  /**\n   * User name parsed from the header.\n   */\n  uname: string\n\n  /**\n   * Group name parsed from the header.\n   */\n  gname: string\n\n  /**\n   * Major device number for device entries, or `null` when unavailable.\n   */\n  devmajor: number | null\n\n  /**\n   * Minor device number for device entries, or `null` when unavailable.\n   */\n  devminor: number | null\n\n  /**\n   * Decoded PAX metadata attached to the entry, or `null` when none is present.\n   */\n  pax: Record<string, string> | null\n}\n\nconst TarFileTypes: Record<string, string> = {\n  '0': 'file',\n  '1': 'link',\n  '2': 'symlink',\n  '3': 'character-device',\n  '4': 'block-device',\n  '5': 'directory',\n  '6': 'fifo',\n  '7': 'contiguous-file',\n  '27': 'gnu-long-link-path',\n  '28': 'gnu-long-path',\n  '30': 'gnu-long-path',\n  '55': 'pax-global-header',\n  '72': 'pax-header',\n}\n\nconst ZeroOffset = '0'.charCodeAt(0)\nconst UstarMagic = new Uint8Array([0x75, 0x73, 0x74, 0x61, 0x72, 0x00]) // \"ustar\\0\"\nconst UstarVersion = new Uint8Array([ZeroOffset, ZeroOffset]) // \"00\"\nconst GnuMagic = new Uint8Array([0x75, 0x73, 0x74, 0x61, 0x72, 0x20]) // \"ustar \"\nconst GnuVersion = new Uint8Array([0x20, 0x00]) // \" \\0\"\n\n/**\n * Options for parsing tar headers.\n */\nexport interface ParseTarHeaderOptions {\n  /**\n   * Set `false` to disallow unknown header formats.\n   *\n   * @default true\n   */\n  allowUnknownFormat?: boolean\n  /**\n   * The label (encoding) for filenames.\n   *\n   * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings)\n   *\n   * @default 'utf-8'\n   */\n  filenameEncoding?: string\n}\n\n/**\n * Parses a tar header block.\n *\n * @param block The tar header block\n * @param options Options that control how the header is parsed\n * @returns The parsed tar header\n */\nexport function parseTarHeader(block: Uint8Array, options?: ParseTarHeaderOptions): TarHeader {\n  if (block.length !== TarBlockSize) {\n    throw new TarParseError('Invalid tar header size')\n  }\n\n  let allowUnknownFormat = options?.allowUnknownFormat ?? true\n  let filenameEncoding = options?.filenameEncoding ?? 'utf-8'\n\n  // Tar header format\n  // Offset  Size    Field\n  // 0       100     Filename\n  // 100     8       File mode (octal)\n  // 108     8       Owner's numeric user ID (octal)\n  // 116     8       Group's numeric user ID (octal)\n  // 124     12      File size in bytes (octal)\n  // 136     12      Last modification time (octal)\n  // 148     8       Checksum for header block (octal)\n  // 156     1       Type flag\n  // 157     100     Name of linked file\n  // 257     6       Magic string \"ustar\\0\" or \"ustar \"\n  // 263     2       Version \"00\" or \" \\0\"\n  // 265     32      Owner username\n  // 297     32      Owner groupname\n  // 329     8       Device major number (octal)\n  // 337     8       Device minor number (octal)\n  // 345     155     Filename prefix (ustar only)\n\n  let checksum = getOctal(block, 148, 8)\n  if (checksum !== computeChecksum(block)) {\n    throw new TarParseError(\n      'Invalid tar header. Maybe the tar is corrupted or needs to be gunzipped?',\n    )\n  }\n\n  let typeFlag = block[156] === 0 ? 0 : block[156] - ZeroOffset\n  let header: TarHeader = {\n    name: getString(block, 0, 100, filenameEncoding),\n    mode: getOctal(block, 100, 8),\n    uid: getOctal(block, 108, 8),\n    gid: getOctal(block, 116, 8),\n    size: getOctal(block, 124, 12) ?? 0,\n    mtime: getOctal(block, 136, 12),\n    type: TarFileTypes[typeFlag] ?? 'unknown',\n    linkname: block[157] === 0 ? null : getString(block, 157, 100, filenameEncoding),\n    uname: getString(block, 265, 32),\n    gname: getString(block, 297, 32),\n    devmajor: getOctal(block, 329, 8),\n    devminor: getOctal(block, 337, 8),\n    pax: null,\n  }\n\n  let magic = block.subarray(257, 263)\n  let version = block.subarray(263, 265)\n  if (buffersEqual(magic, UstarMagic) && buffersEqual(version, UstarVersion)) {\n    // UStar (posix) format\n    if (block[345] !== 0) {\n      let prefix = getString(block, 345, 155)\n      header.name = prefix + '/' + header.name\n    }\n  } else if (buffersEqual(magic, GnuMagic) && buffersEqual(version, GnuVersion)) {\n    // GNU format\n  } else if (!allowUnknownFormat) {\n    throw new TarParseError('Invalid tar header, unknown format')\n  }\n\n  return header\n}\n\ntype TarArchiveSource =\n  | ReadableStream<Uint8Array>\n  | Uint8Array\n  | Iterable<Uint8Array>\n  | AsyncIterable<Uint8Array>\n\ntype TarEntryHandler = (entry: TarEntry) => void | Promise<void>\n\n/**\n * Options for parsing a tar archive.\n */\nexport type ParseTarOptions = ParseTarHeaderOptions\n\n/**\n * Parse a tar archive and call the given handler for each entry it contains.\n *\n * ```ts\n * import { parseTar } from 'remix/tar-parser';\n *\n * await parseTar(archive, (entry) => {\n *  console.log(entry.name);\n * });\n * ```\n *\n * @param archive The tar archive source data\n * @param handler A function to call for each entry in the archive\n * @returns A promise that resolves when the parse is finished\n */\nexport async function parseTar(archive: TarArchiveSource, handler: TarEntryHandler): Promise<void>\nexport async function parseTar(\n  archive: TarArchiveSource,\n  options: ParseTarOptions,\n  handler: TarEntryHandler,\n): Promise<void>\nexport async function parseTar(\n  archive: TarArchiveSource,\n  options: ParseTarOptions | TarEntryHandler,\n  handler?: TarEntryHandler,\n): Promise<void> {\n  let opts: ParseTarOptions | undefined\n  if (typeof options === 'function') {\n    handler = options\n  } else {\n    opts = options\n  }\n\n  let parser = new TarParser(opts)\n  await parser.parse(archive, handler!)\n}\n\n/**\n * Options for configuring a {@link TarParser}.\n */\nexport type TarParserOptions = ParseTarHeaderOptions\n\n/**\n * A parser for tar archives.\n */\nexport class TarParser {\n  #buffer: Uint8Array | null = null\n  #missing = 0\n  #header: TarHeader | null = null\n  #bodyController: ReadableStreamDefaultController<Uint8Array> | null = null\n  #longHeader = false\n  #gnuLongPath: string | null = null\n  #gnuLongLinkPath: string | null = null\n  #paxGlobal: Record<string, string> | null = null\n  #pax: Record<string, string> | null = null\n\n  #options?: TarParserOptions\n\n  /**\n   * @param options Options that control how the tar archive is parsed\n   */\n  constructor(options?: TarParserOptions) {\n    this.#options = options\n  }\n\n  /**\n   * Parse a stream/buffer tar archive and call the given handler for each entry it contains.\n   * Resolves when the parse is finished and all handlers resolve.\n   *\n   * @param archive The tar archive source data\n   * @param handler A function to call for each entry in the archive\n   * @returns A promise that resolves when the parse is finished\n   */\n  async parse(archive: TarArchiveSource, handler: TarEntryHandler): Promise<void> {\n    this.#reset()\n\n    let results: unknown[] = []\n\n    function handleEntry(entry: TarEntry): void {\n      results.push(handler(entry))\n    }\n\n    if (archive instanceof ReadableStream) {\n      for await (let chunk of readStream(archive)) {\n        this.#write(chunk, handleEntry)\n      }\n    } else if (isAsyncIterable(archive)) {\n      for await (let chunk of archive) {\n        this.#write(chunk, handleEntry)\n      }\n    } else if (archive instanceof Uint8Array) {\n      this.#write(archive, handleEntry)\n    } else if (isIterable(archive)) {\n      for (let chunk of archive) {\n        this.#write(chunk, handleEntry)\n      }\n    } else {\n      throw new TypeError('Cannot parse tar archive; expected a stream or buffer')\n    }\n\n    if (this.#missing !== 0) {\n      throw new TarParseError('Unexpected end of archive')\n    }\n\n    await Promise.all(results)\n  }\n\n  #reset(): void {\n    this.#buffer = null\n    this.#missing = 0\n    this.#header = null\n    this.#bodyController = null\n    this.#longHeader = false\n    this.#gnuLongPath = null\n    this.#gnuLongLinkPath = null\n    this.#paxGlobal = null\n    this.#pax = null\n  }\n\n  #write(chunk: Uint8Array, handler: TarEntryHandler): void {\n    if (this.#buffer !== null) {\n      this.#buffer = concatChunks(this.#buffer, chunk)\n    } else {\n      this.#buffer = chunk\n    }\n\n    while (this.#buffer !== null && this.#buffer.length > 0) {\n      if (this.#missing > 0) {\n        if (this.#bodyController !== null) {\n          this.#parseBody()\n          continue\n        }\n\n        if (this.#longHeader) {\n          if (this.#missing > this.#buffer.length) break\n          this.#parseLongHeader()\n          continue\n        }\n\n        if (this.#missing >= this.#buffer.length) {\n          this.#missing -= this.#buffer.length\n          this.#buffer = null\n          break\n        }\n\n        this.#buffer = this.#buffer.subarray(this.#missing)\n        this.#missing = 0\n      }\n\n      if (this.#buffer.length < TarBlockSize) break\n      this.#parseHeader(handler)\n    }\n  }\n\n  #parseHeader(handler: TarEntryHandler): void {\n    let block = this.#read(TarBlockSize)\n\n    if (isZeroBlock(block)) {\n      this.#header = null\n      return\n    }\n\n    this.#header = parseTarHeader(block, this.#options)\n\n    switch (this.#header.type) {\n      case 'gnu-long-path':\n      case 'gnu-long-link-path':\n      case 'pax-global-header':\n      case 'pax-header':\n        this.#longHeader = true\n        this.#missing = this.#header.size\n        return\n    }\n\n    if (this.#gnuLongPath) {\n      this.#header.name = this.#gnuLongPath\n      this.#gnuLongPath = null\n    }\n\n    if (this.#gnuLongLinkPath) {\n      this.#header.linkname = this.#gnuLongLinkPath\n      this.#gnuLongLinkPath = null\n    }\n\n    if (this.#pax) {\n      if (this.#pax.path) this.#header.name = this.#pax.path\n      if (this.#pax.linkpath) this.#header.linkname = this.#pax.linkpath\n      if (this.#pax.size) this.#header.size = parseInt(this.#pax.size, 10)\n      this.#header.pax = this.#pax\n      this.#pax = null\n    }\n\n    if (this.#header.size === 0 || this.#header.type === 'directory') {\n      let emptyBody = new ReadableStream({\n        start(controller) {\n          controller.close()\n        },\n      })\n\n      handler(new TarEntry(this.#header, emptyBody))\n      this.#bodyController = null\n      this.#missing = 0\n      return\n    }\n\n    let body = new ReadableStream({\n      start: (controller) => {\n        this.#bodyController = controller\n      },\n    })\n\n    handler(new TarEntry(this.#header, body))\n\n    this.#missing = this.#header.size\n  }\n\n  #parseLongHeader(): void {\n    this.#longHeader = false\n\n    let buffer = this.#read(this.#header!.size)\n\n    switch (this.#header!.type) {\n      case 'gnu-long-path':\n        this.#gnuLongPath = decodeLongPath(buffer)\n        break\n      case 'gnu-long-link-path':\n        this.#gnuLongLinkPath = decodeLongPath(buffer)\n        break\n      case 'pax-global-header':\n        this.#paxGlobal = decodePax(buffer)\n        break\n      case 'pax-header':\n        this.#pax =\n          this.#paxGlobal !== null\n            ? Object.assign({}, this.#paxGlobal, decodePax(buffer))\n            : decodePax(buffer)\n        break\n    }\n\n    this.#missing = overflow(this.#header!.size)\n  }\n\n  #parseBody(): void {\n    if (this.#missing >= this.#buffer!.length) {\n      this.#bodyController!.enqueue(this.#buffer!)\n      this.#missing -= this.#buffer!.length\n      this.#buffer = null\n    } else {\n      this.#bodyController!.enqueue(this.#read(this.#missing))\n      this.#bodyController!.close()\n      this.#bodyController = null\n      this.#missing = overflow(this.#header!.size)\n    }\n  }\n\n  #read(size: number): Uint8Array {\n    let result = this.#buffer!.subarray(0, size)\n    this.#buffer = this.#buffer!.subarray(size)\n    return result\n  }\n}\n\nfunction isIterable<T>(value: unknown): value is Iterable<T> {\n  return typeof value === 'object' && value != null && Symbol.iterator in value\n}\n\nfunction isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> {\n  return typeof value === 'object' && value != null && Symbol.asyncIterator in value\n}\n\nfunction isZeroBlock(buffer: Uint8Array): boolean {\n  return buffer.every((byte) => byte === 0)\n}\n\n/**\n * An entry in a tar archive.\n */\nexport class TarEntry {\n  #header: TarHeader\n  #body: ReadableStream<Uint8Array>\n  #bodyUsed = false\n\n  /**\n   * @param header The header info for this entry\n   * @param body The entry's content as a stream\n   */\n  constructor(header: TarHeader, body: ReadableStream<Uint8Array>) {\n    this.#header = header\n    this.#body = body\n  }\n\n  /**\n   * The content of this entry as an [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer).\n   *\n   * @returns A promise that resolves to an `ArrayBuffer`\n   */\n  async arrayBuffer(): Promise<ArrayBuffer> {\n    return (await this.bytes()).buffer as ArrayBuffer\n  }\n\n  /**\n   * The content of this entry as a `ReadableStream<Uint8Array>`.\n   */\n  get body(): ReadableStream<Uint8Array> {\n    return this.#body\n  }\n\n  /**\n   * Whether the body of this entry has been consumed.\n   */\n  get bodyUsed(): boolean {\n    return this.#bodyUsed\n  }\n\n  /**\n   * The content of this entry buffered into a single typed array.\n   *\n   * @returns A promise that resolves to a `Uint8Array`\n   */\n  async bytes(): Promise<Uint8Array> {\n    if (this.#bodyUsed) {\n      throw new Error('Body is already consumed or is being consumed')\n    }\n\n    this.#bodyUsed = true\n\n    let result = new Uint8Array(this.size)\n    let offset = 0\n    for await (let chunk of readStream(this.#body)) {\n      result.set(chunk, offset)\n      offset += chunk.length\n    }\n\n    return result\n  }\n\n  /**\n   * The raw header info associated with this entry.\n   */\n  get header(): TarHeader {\n    return this.#header\n  }\n\n  /**\n   * The name of this entry.\n   */\n  get name(): string {\n    return this.header.name\n  }\n\n  /**\n   * The size of this entry in bytes.\n   */\n  get size(): number {\n    return this.header.size\n  }\n\n  /**\n   * The content of this entry as a string.\n   *\n   * Note: Do not use this for binary data, use `await entry.bytes()` or stream `entry.body` directly instead.\n   *\n   * @returns A promise that resolves to the entry's content as a string\n   */\n  async text(): Promise<string> {\n    return new TextDecoder().decode(await this.bytes())\n  }\n}\n"
  },
  {
    "path": "packages/tar-parser/src/lib/utils.ts",
    "content": "export function buffersEqual(a: Uint8Array, b: Uint8Array): boolean {\n  if (a.length !== b.length) return false\n  for (let i = 0; i < a.length; i++) {\n    if (a[i] !== b[i]) return false\n  }\n  return true\n}\n\nexport function concatChunks(a: Uint8Array, b: Uint8Array): Uint8Array {\n  let result = new Uint8Array(a.length + b.length)\n  result.set(a)\n  result.set(b, a.length)\n  return result\n}\n\nexport function computeChecksum(block: Uint8Array): number {\n  let sum = 8 * 32\n  for (let i = 0; i < 148; i++) sum += block[i]\n  for (let i = 156; i < 512; i++) sum += block[i]\n  return sum\n}\n\nexport function decodeLongPath(buffer: Uint8Array): string {\n  return Utf8Decoder.decode(buffer)\n}\n\nexport function decodePax(buffer: Uint8Array): Record<string, string> {\n  let pax: Record<string, string> = {}\n\n  while (buffer.length) {\n    let i = 0\n    while (i < buffer.length && buffer[i] !== 32) i++\n\n    let len = parseInt(Utf8Decoder.decode(buffer.subarray(0, i)), 10)\n    if (!len) break\n\n    let val = Utf8Decoder.decode(buffer.subarray(i + 1, len - 1))\n    let eq = val.indexOf('=')\n    if (eq === -1) break\n\n    pax[val.slice(0, eq)] = val.slice(eq + 1)\n\n    buffer = buffer.subarray(len)\n  }\n\n  return pax\n}\n\nexport function indexOf(buffer: Uint8Array, value: number, offset: number, end: number): number {\n  for (; offset < end; offset++) {\n    if (buffer[offset] === value) return offset\n  }\n  return end\n}\n\nexport function getString(buffer: Uint8Array, offset: number, size: number, label = 'utf-8') {\n  return new TextDecoder(label).decode(\n    buffer.subarray(offset, indexOf(buffer, 0, offset, offset + size)),\n  )\n}\n\nconst Utf8Decoder = new TextDecoder()\n\nexport function getOctal(buffer: Uint8Array, offset: number, size: number) {\n  let value = buffer.subarray(offset, offset + size)\n  offset = 0\n\n  if (value[offset] & 0x80) return parse256(value)\n\n  // Older versions of tar can prefix with spaces\n  while (offset < value.length && value[offset] === 32) offset++\n  let end = clamp(indexOf(value, 32, offset, value.length), value.length, value.length)\n  while (offset < end && value[offset] === 0) offset++\n  if (end === offset) return 0\n\n  return parseInt(Utf8Decoder.decode(value.subarray(offset, end)), 8)\n}\n\nfunction clamp(index: number, len: number, defaultValue: number): number {\n  if (typeof index !== 'number') return defaultValue\n  index = ~~index // Coerce to integer.\n  if (index >= len) return len\n  if (index >= 0) return index\n  index += len\n  if (index >= 0) return index\n  return 0\n}\n\n/* Copied from the tar-stream repo who copied it from the node-tar repo.\n */\nfunction parse256(buf: Uint8Array): number | null {\n  // first byte MUST be either 80 or FF\n  // 80 for positive, FF for 2's comp\n  let positive\n  if (buf[0] === 0x80) positive = true\n  else if (buf[0] === 0xff) positive = false\n  else return null\n\n  // build up a base-256 tuple from the least sig to the highest\n  let tuple = []\n  let i\n  for (i = buf.length - 1; i > 0; i--) {\n    let byte = buf[i]\n    if (positive) tuple.push(byte)\n    else tuple.push(0xff - byte)\n  }\n\n  let sum = 0\n  let len = tuple.length\n  for (i = 0; i < len; i++) {\n    sum += tuple[i] * Math.pow(256, i)\n  }\n\n  return positive ? sum : -1 * sum\n}\n\nexport function overflow(size: number): number {\n  size &= 511\n  return size && 512 - size\n}\n"
  },
  {
    "path": "packages/tar-parser/test/utils.ts",
    "content": "import * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { openLazyFile } from '@remix-run/fs'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\nconst fixturesDir = path.resolve(__dirname, 'fixtures')\n\nexport const fixtures = {\n  base256Size: path.resolve(fixturesDir, 'base-256-size.tar'),\n  base256UidGid: path.resolve(fixturesDir, 'base-256-uid-gid.tar'),\n  expressNpmPackage: path.resolve(fixturesDir, 'express-4.21.1.tgz'),\n  fetchProxyGithubArchive: path.resolve(fixturesDir, 'fetch-proxy-0.1.0.tar.gz'),\n  gnuIncremental: path.resolve(fixturesDir, 'gnu-incremental.tar'),\n  gnuLongPath: path.resolve(fixturesDir, 'gnu-long-path.tar'),\n  gnu: path.resolve(fixturesDir, 'gnu.tar'),\n  incomplete: path.resolve(fixturesDir, 'incomplete.tar'),\n  latin1: path.resolve(fixturesDir, 'latin1.tar'),\n  lodashNpmPackage: path.resolve(fixturesDir, 'lodash-4.17.21.tgz'),\n  longName: path.resolve(fixturesDir, 'long-name.tar'),\n  multiFile: path.resolve(fixturesDir, 'multi-file.tar'),\n  npmNpmPackage: path.resolve(fixturesDir, 'npm-11.0.0.tgz'),\n  nameIs100: path.resolve(fixturesDir, 'name-is-100.tar'),\n  oneFile: path.resolve(fixturesDir, 'one-file.tar'),\n  pax: path.resolve(fixturesDir, 'pax.tar'),\n  space: path.resolve(fixturesDir, 'space.tar'),\n  types: path.resolve(fixturesDir, 'types.tar'),\n  unicodeBsd: path.resolve(fixturesDir, 'unicode-bsd.tar'),\n  unicode: path.resolve(fixturesDir, 'unicode.tar'),\n}\n\nexport function readFixture(filename: string): ReadableStream<Uint8Array> {\n  let stream = openLazyFile(filename).stream()\n  return filename.endsWith('.tar.gz') || filename.endsWith('.tgz')\n    ? stream.pipeThrough(new DecompressionStream('gzip'))\n    : stream\n}\n"
  },
  {
    "path": "packages/tar-parser/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/tar-parser/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ESNext\",\n    \"allowImportingTsExtensions\": true,\n    \"rewriteRelativeImportExtensions\": true,\n    \"verbatimModuleSyntax\": true\n  },\n  \"exclude\": [\"bench\", \"dist\"]\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "catalog:\n  '@types/node': ^24.6.0\n  '@typescript/native-preview': 7.0.0-dev.20251125.1\n  playwright: ^1.53.1\n  tsx: ^4.20.6\n  typescript: ^5.9.3\n\npackages:\n  - demos/*\n  - docs\n  - packages/*\n  - packages/fetch-router/demos/*\n  - packages/form-data-parser/demos/*\n  - packages/component/demos\n  - packages/component/bench\n  - packages/component/bench/frameworks/*\n  - packages/lazy-file/scripts\n  - packages/multipart-parser/bench\n  - packages/multipart-parser/demos/*\n  - packages/node-fetch-server/bench\n  - packages/node-fetch-server/demos/*\n  - packages/route-pattern/bench\n  - packages/static-middleware/demos/*\n  - packages/tar-parser/bench\n  - scripts\n\nshellEmulator: true\n"
  },
  {
    "path": "scripts/changes-preview.ts",
    "content": "import {\n  parseAllChangeFiles,\n  formatValidationErrors,\n  generateChangelogContent,\n  generateCommitMessage,\n} from './utils/changes.ts'\nimport { colors, colorize } from './utils/color.ts'\n\n/**\n * Main preview function\n */\nfunction main() {\n  let result = parseAllChangeFiles()\n\n  if (!result.valid) {\n    console.error(colorize('Validation failed', colors.red) + '\\n')\n    console.error(formatValidationErrors(result.errors))\n    console.error()\n    process.exit(1)\n  }\n\n  let { releases } = result\n\n  if (releases.length === 0) {\n    console.log('No packages have changes to release.\\n')\n    process.exit(0)\n  }\n\n  console.log(colorize('CHANGES', colors.lightBlue))\n  console.log()\n  console.log(`${releases.length} package${releases.length === 1 ? '' : 's'} with changes:\\n`)\n\n  for (let release of releases) {\n    console.log(\n      `  • ${release.packageName}: ${release.currentVersion} → ${release.nextVersion} (${release.bump} bump)`,\n    )\n    for (let change of release.changes) {\n      console.log(`    - ${change.file}`)\n    }\n    console.log()\n  }\n\n  console.log(colorize('CHANGELOG PREVIEW', colors.lightBlue))\n  console.log()\n\n  for (let release of releases) {\n    console.log(colorize(`${release.packageDirName}/CHANGELOG.md:`, colors.gray))\n    console.log()\n    console.log(generateChangelogContent(release))\n  }\n\n  console.log(colorize('COMMIT MESSAGE', colors.lightBlue))\n  console.log()\n  console.log(generateCommitMessage(releases))\n  console.log()\n\n  console.log(colorize('VERSION COMMAND', colors.lightBlue))\n  console.log()\n  console.log('pnpm changes:version')\n  console.log()\n}\n\nmain()\n"
  },
  {
    "path": "scripts/changes-validate.ts",
    "content": "import * as fs from 'node:fs'\nimport { parseAllChangeFiles, formatValidationErrors } from './utils/changes.ts'\nimport { colors, colorize } from './utils/color.ts'\nimport { getAllPackageDirNames, getPackageFile } from './utils/packages.ts'\n\nfunction getMissingChangelogPackageDirNames(): string[] {\n  let packageDirNames = getAllPackageDirNames()\n  let missing: string[] = []\n\n  for (let packageDirName of packageDirNames) {\n    let changelogPath = getPackageFile(packageDirName, 'CHANGELOG.md')\n    if (!fs.existsSync(changelogPath)) {\n      missing.push(packageDirName)\n    }\n  }\n\n  return missing\n}\n\n/**\n * Validates all change files in the repository\n * Exits with code 1 if any validation errors are found\n */\nfunction main() {\n  let hasErrors = false\n\n  // Validate all packages have changelogs\n  console.log('Validating package changelogs...\\n')\n  let missingChangelogPackageDirNames = getMissingChangelogPackageDirNames()\n\n  if (missingChangelogPackageDirNames.length > 0) {\n    hasErrors = true\n    console.error(colorize('Missing changelogs', colors.red) + '\\n')\n    for (let packageDirName of missingChangelogPackageDirNames) {\n      console.error(`📦 ${packageDirName}: Missing CHANGELOG.md file`)\n    }\n    console.error()\n  }\n\n  // Validate change files\n  console.log('Validating change files...\\n')\n  let result = parseAllChangeFiles()\n\n  if (!result.valid) {\n    hasErrors = true\n    console.error(colorize('Invalid change files', colors.red) + '\\n')\n    console.error(formatValidationErrors(result.errors))\n    console.error()\n  } else {\n    console.log(colorize('All change files are valid!', colors.lightGreen) + '\\n')\n  }\n\n  if (hasErrors) {\n    process.exit(1)\n  }\n}\n\nmain()\n"
  },
  {
    "path": "scripts/changes-version.ts",
    "content": "/**\n * Updates package.json versions, CHANGELOG.md files, and creates a release commit.\n *\n * Usage:\n *   pnpm changes:version [--no-commit]\n *\n * Options:\n *   --no-commit  Only update files, don't commit (for manual review)\n */\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport {\n  parseAllChangeFiles,\n  formatValidationErrors,\n  generateChangelogContent,\n  generateCommitMessage,\n} from './utils/changes.ts'\nimport { colors, colorize } from './utils/color.ts'\nimport { getPackageFile, getPackagePath } from './utils/packages.ts'\nimport { readJson, writeJson, readFile, writeFile } from './utils/fs.ts'\nimport { logAndExec } from './utils/process.ts'\n\n/**\n * Updates package.json version\n */\nfunction updatePackageJson(packageDirName: string, newVersion: string) {\n  let packageJsonPath = getPackageFile(packageDirName, 'package.json')\n  let packageJson = readJson(packageJsonPath)\n  packageJson.version = newVersion\n  writeJson(packageJsonPath, packageJson)\n  console.log(`  ✓ Updated package.json to ${newVersion}`)\n}\n\n/**\n * Updates CHANGELOG.md with new content\n */\nfunction updateChangelog(packageDirName: string, newContent: string) {\n  let changelogPath = getPackageFile(packageDirName, 'CHANGELOG.md')\n  let existingChangelog = readFile(changelogPath)\n\n  let lines = existingChangelog.split('\\n')\n\n  // Find the first ## version entry\n  let firstVersionIndex = lines.findIndex((line) => line.startsWith('## '))\n\n  let updatedChangelog: string\n  if (firstVersionIndex !== -1) {\n    // Insert before the first version entry\n    lines.splice(firstVersionIndex, 0, newContent)\n    updatedChangelog = lines.join('\\n')\n  } else {\n    // No version entries yet - append to the end\n    updatedChangelog = existingChangelog.trimEnd() + '\\n\\n' + newContent + '\\n'\n  }\n\n  writeFile(changelogPath, updatedChangelog)\n  console.log(`  ✓ Updated CHANGELOG.md`)\n}\n\n/**\n * Deletes all change files (except README.md)\n */\nfunction deleteChangeFiles(packageDirName: string) {\n  let changesDir = path.join(getPackagePath(packageDirName), '.changes')\n  let files = fs.readdirSync(changesDir)\n  let changeFiles = files.filter((file) => file !== 'README.md' && file.endsWith('.md'))\n\n  for (let file of changeFiles) {\n    let filePath = path.join(changesDir, file)\n    fs.unlinkSync(filePath)\n  }\n\n  console.log(`  ✓ Deleted ${changeFiles.length} change file${changeFiles.length === 1 ? '' : 's'}`)\n}\n\n/**\n * Main version function\n */\nfunction main() {\n  let skipCommit = process.argv.includes('--no-commit')\n\n  console.log('Validating change files...\\n')\n\n  let result = parseAllChangeFiles()\n\n  if (!result.valid) {\n    console.error(colorize('Validation failed', colors.red) + '\\n')\n    console.error(formatValidationErrors(result.errors))\n    console.error()\n    process.exit(1)\n  }\n\n  let { releases } = result\n\n  if (releases.length === 0) {\n    console.log('No packages have changes to release.\\n')\n    process.exit(0)\n  }\n\n  console.log(colorize('Validation passed!', colors.lightGreen) + '\\n')\n  console.log('═'.repeat(80))\n  console.log(colorize(skipCommit ? 'UPDATING VERSION' : 'PREPARING RELEASE', colors.lightBlue))\n  console.log('═'.repeat(80))\n  console.log()\n\n  // Process each package\n  for (let release of releases) {\n    console.log(\n      colorize(`${release.packageName}:`, colors.gray) +\n        ` ${release.currentVersion} → ${release.nextVersion}`,\n    )\n\n    // Update package.json\n    updatePackageJson(release.packageDirName, release.nextVersion)\n\n    // Update CHANGELOG.md\n    let changelogContent = generateChangelogContent(release)\n    updateChangelog(release.packageDirName, changelogContent)\n\n    // Delete change files\n    deleteChangeFiles(release.packageDirName)\n\n    console.log()\n  }\n\n  if (skipCommit) {\n    // Success message for --no-commit\n    console.log('═'.repeat(80))\n    console.log(colorize('VERSION UPDATED', colors.lightGreen))\n    console.log('═'.repeat(80))\n    console.log()\n    console.log('Files have been updated. Review the changes, then manually commit:')\n    console.log()\n    console.log('```sh')\n    let commitMessage = generateCommitMessage(releases)\n    console.log(`git add .`)\n    console.log()\n    console.log(`git commit -m \"${commitMessage}\"`)\n    console.log('```')\n    console.log()\n  } else {\n    // Stage all changes\n    console.log('Staging changes...')\n    logAndExec('git add .')\n    console.log()\n\n    // Create commit\n    let commitMessage = generateCommitMessage(releases)\n    console.log('Creating commit...')\n    logAndExec(`git commit -m \"${commitMessage}\"`)\n    console.log()\n\n    // Success message (skip in CI since the workflow handles the rest)\n    if (!process.env.CI) {\n      console.log('═'.repeat(80))\n      console.log('✅ RELEASE PREPARED')\n      console.log('═'.repeat(80))\n      console.log()\n      console.log('Release commit has been created locally.')\n      console.log()\n      console.log('To publish, push and the publish workflow will handle the rest:')\n      console.log()\n      console.log('  git push')\n      console.log()\n    }\n  }\n}\n\nmain()\n"
  },
  {
    "path": "scripts/detect-changed-packages.ts",
    "content": "import * as cp from 'node:child_process'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\n\ntype PackageInfo = {\n  dirName: string\n  name: string\n  dependencies: string[]\n}\n\ntype CliOptions = {\n  baseRef: string\n  headRef: string\n  listOnly: boolean\n}\n\nfunction main() {\n  let options = parseArgs(process.argv.slice(2))\n  let packageInfos = getPackageInfos()\n  let changedPackages = getChangedPackageNames(options.baseRef, options.headRef, packageInfos)\n  let selectedPackages = getSelectedPackageNames(changedPackages, packageInfos)\n\n  if (options.listOnly) {\n    console.log(JSON.stringify(selectedPackages, null, 2))\n    return\n  }\n\n  if (selectedPackages.length === 0) {\n    console.log(\n      `No package changes detected between ${options.baseRef} and ${options.headRef}. Skipping package tests.`,\n    )\n    return\n  }\n\n  console.log(\n    `Running tests for packages changed between ${options.baseRef} and ${options.headRef}:`,\n  )\n  for (let packageName of selectedPackages) {\n    console.log(`- ${packageName}`)\n  }\n  console.log()\n\n  let args = selectedPackages.flatMap((packageName) => ['--filter', packageName])\n  args.push('run', 'test')\n\n  let result = cp.spawnSync('pnpm', args, {\n    stdio: 'inherit',\n    shell: process.platform === 'win32',\n  })\n\n  if (result.status !== 0) {\n    process.exit(result.status ?? 1)\n  }\n}\n\nfunction parseArgs(args: string[]): CliOptions {\n  let baseRef = ''\n  let headRef = 'HEAD'\n  let listOnly = false\n\n  for (let arg of args) {\n    if (arg === '--list') {\n      listOnly = true\n      continue\n    }\n\n    if (baseRef === '') {\n      baseRef = arg\n      continue\n    }\n\n    if (headRef === 'HEAD') {\n      headRef = arg\n      continue\n    }\n\n    throw new Error(`Unexpected argument: ${arg}`)\n  }\n\n  if (baseRef === '') {\n    throw new Error(\n      'Usage: node ./scripts/detect-changed-packages.ts <base-ref> [head-ref] [--list]',\n    )\n  }\n\n  return { baseRef, headRef, listOnly }\n}\n\nfunction getPackageInfos(): PackageInfo[] {\n  let packagesDir = path.join(process.cwd(), 'packages')\n  let infos: PackageInfo[] = []\n\n  for (let dirName of fs.readdirSync(packagesDir)) {\n    let packageJsonPath = path.join(packagesDir, dirName, 'package.json')\n    if (!fs.existsSync(packageJsonPath)) {\n      continue\n    }\n\n    let packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as {\n      name?: string\n      dependencies?: Record<string, string>\n      devDependencies?: Record<string, string>\n      optionalDependencies?: Record<string, string>\n      peerDependencies?: Record<string, string>\n    }\n\n    if (typeof packageJson.name !== 'string') {\n      continue\n    }\n\n    let dependencyNames = new Set<string>()\n\n    for (let field of [\n      packageJson.dependencies,\n      packageJson.devDependencies,\n      packageJson.optionalDependencies,\n      packageJson.peerDependencies,\n    ]) {\n      for (let dependencyName of Object.keys(field ?? {})) {\n        dependencyNames.add(dependencyName)\n      }\n    }\n\n    infos.push({\n      dirName,\n      name: packageJson.name,\n      dependencies: [...dependencyNames],\n    })\n  }\n\n  return infos\n}\n\nfunction getChangedPackageNames(\n  baseRef: string,\n  headRef: string,\n  packageInfos: PackageInfo[],\n): Set<string> {\n  let diffOutput = cp.execFileSync(\n    'git',\n    ['diff', '--name-only', `${baseRef}...${headRef}`, '--', 'packages/*'],\n    { encoding: 'utf8' },\n  )\n\n  let dirNameToPackageName = new Map(packageInfos.map((info) => [info.dirName, info.name]))\n  let changedPackages = new Set<string>()\n\n  for (let file of diffOutput.split('\\n')) {\n    if (!file.startsWith('packages/')) {\n      continue\n    }\n\n    let [, dirName] = file.split('/', 3)\n    let packageName = dirNameToPackageName.get(dirName)\n    if (packageName != null) {\n      changedPackages.add(packageName)\n    }\n  }\n\n  return changedPackages\n}\n\nfunction getSelectedPackageNames(\n  changedPackages: Set<string>,\n  packageInfos: PackageInfo[],\n): string[] {\n  if (changedPackages.size === 0) {\n    return []\n  }\n\n  let knownPackageNames = new Set(packageInfos.map((info) => info.name))\n  let reverseDependencies = new Map<string, Set<string>>()\n\n  for (let info of packageInfos) {\n    reverseDependencies.set(info.name, new Set())\n  }\n\n  for (let info of packageInfos) {\n    for (let dependencyName of info.dependencies) {\n      if (!knownPackageNames.has(dependencyName)) {\n        continue\n      }\n\n      reverseDependencies.get(dependencyName)?.add(info.name)\n    }\n  }\n\n  let selectedPackages = new Set(changedPackages)\n  let queue = [...changedPackages]\n\n  while (queue.length > 0) {\n    let packageName = queue.shift()\n    if (packageName == null) {\n      continue\n    }\n\n    for (let dependentName of reverseDependencies.get(packageName) ?? []) {\n      if (selectedPackages.has(dependentName)) {\n        continue\n      }\n\n      selectedPackages.add(dependentName)\n      queue.push(dependentName)\n    }\n  }\n\n  return packageInfos\n    .map((info) => info.name)\n    .filter((packageName) => selectedPackages.has(packageName))\n}\n\nmain()\n"
  },
  {
    "path": "scripts/generate-remix.ts",
    "content": "/**\n * Auto-generates the remix umbrella package by:\n * 1. Scanning all @remix-run/* packages in packages/ directory\n * 2. Creating source files that re-export from each package and sub-export\n * 3. Generating exports configuration in package.json\n * 4. Setting up dependencies for all referenced packages\n *\n * Run: node docs/generate-remix.ts\n */\n\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\nimport url from 'node:url'\nimport * as semver from 'semver'\nimport { logAndExec } from './utils/process.ts'\n\nlet __dirname = path.dirname(url.fileURLToPath(import.meta.url))\nlet packagesDir = path.resolve(__dirname, '../packages')\nlet remixDir = path.join(packagesDir, 'remix')\nlet remixChangesDir = path.join(remixDir, '.changes')\nlet remixPackageJsonPath = path.join(remixDir, 'package.json')\n\nconst SOURCE_FOLDER = 'src'\n\ntype RemixRunPackage = {\n  name: string\n  version: string\n  exports: ExportEntry[]\n}\n\ntype ExportEntry = {\n  // The source file path relative to src: `headers.ts`, `headers/cookie-storage.ts`\n  sourceFile: string\n  // The export path in package.json exports: `./headers`, `./headers/cookie-storage`1\n  exportPath: string\n  // The package/sub-export to re-export from: `@remix-run/headers`, `@remix-run/headers/cookie-storage`\n  reExportFrom: string\n}\n\nlet { remixRunPackages, allExports } = await getRemixRunPackages()\nlet remixPackageJson = JSON.parse(await fs.readFile(remixPackageJsonPath, 'utf-8'))\n\n// Track existing exports for comparison\nlet existingExports = new Set<string>(\n  Object.keys(remixPackageJson.exports || {}).filter(\n    (key) => key !== '.' && key !== './package.json',\n  ),\n)\n\n// Update remixPackageJson in place and output to disk\nawait updateRemixPackage()\n\n// Generate change files\nawait outputExportsChangeFiles(remixPackageJson.exports)\n\n// Implementations\nasync function getRemixRunPackages() {\n  console.log('Scanning packages...')\n\n  // Get all packages except remix itself\n  let packageDirNames = (await fs.readdir(packagesDir, { withFileTypes: true }))\n    .filter((dirent) => dirent.isDirectory() && dirent.name !== 'remix')\n    .map((dirent) => dirent.name)\n\n  let remixRunPackages: RemixRunPackage[] = []\n\n  // Scan each package for its exports\n  for (let packageDirName of packageDirNames) {\n    let packageJsonPath = path.join(packagesDir, packageDirName, 'package.json')\n    let packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'))\n    let packageName = packageJson.name as string\n\n    // Skip if not a @remix-run package\n    if (!packageName.startsWith('@remix-run/')) {\n      continue\n    }\n\n    let remixRunPackage: RemixRunPackage = {\n      name: packageName,\n      version: packageJson.version,\n      exports: [],\n    }\n    remixRunPackages.push(remixRunPackage)\n\n    let shortName = packageName.replace('@remix-run/', '')\n\n    // Get all exports except package.json\n    let packageExports = packageJson.exports\n    if (packageExports && typeof packageExports === 'object') {\n      for (let [exportPath, _] of Object.entries(packageExports)) {\n        if (exportPath === './package.json') continue\n\n        if (exportPath === '.') {\n          // Main export\n          remixRunPackage.exports.push({\n            sourceFile: `${shortName}.ts`,\n            exportPath: `./${shortName}`,\n            reExportFrom: packageName,\n          })\n        } else {\n          // Sub-export (e.g., \"./cookie-storage\")\n          let subExport = exportPath.replace('./', '')\n          remixRunPackage.exports.push({\n            sourceFile: `${shortName}/${subExport}.ts`,\n            exportPath: `./${shortName}/${subExport}`,\n            reExportFrom: `${packageName}/${subExport}`,\n          })\n        }\n      }\n    }\n  }\n\n  // Sort exports by export path for consistent ordering\n  let allExports = remixRunPackages.flatMap((pkg) => pkg.exports)\n  allExports.sort((a, b) => a.exportPath.localeCompare(b.exportPath))\n\n  console.log(\n    `Found ${remixRunPackages.length} @remix-run packages with a total of ${allExports.length} exports.`,\n  )\n\n  return { remixRunPackages, allExports }\n}\n\nasync function updateRemixPackage() {\n  // Ensure we have a passing linter before generating code\n  logAndExec(`npx eslint packages/remix/ --max-warnings=0`)\n\n  // Clear existing source files\n  let sourceFolderPath = path.join(remixDir, SOURCE_FOLDER)\n  await fs.rm(sourceFolderPath, { recursive: true, force: true })\n  await fs.mkdir(sourceFolderPath, { recursive: true })\n\n  // Generate fresh source files\n  console.log('Generating Remix source files...')\n  for (let entry of allExports) {\n    let sourceFilePath = path.join(remixDir, SOURCE_FOLDER, entry.sourceFile)\n    // Create subdirectory if needed\n    let sourceFileDir = path.dirname(sourceFilePath)\n    await fs.mkdir(sourceFileDir, { recursive: true })\n    let content = [\n      `// IMPORTANT: This file is auto-generated, please do not edit manually.`,\n      `export * from '${entry.reExportFrom}'\\n`,\n    ].join('\\n')\n    await fs.writeFile(sourceFilePath, content, 'utf-8')\n  }\n\n  // Run linter against generated code with --fix\n  logAndExec(`npx eslint packages/remix/ --max-warnings=0 --fix`)\n\n  // Update package.json\n  console.log('Updating Remix package.json...')\n  remixPackageJson.exports = {}\n  remixPackageJson.publishConfig.exports = {}\n\n  for (let entry of allExports) {\n    let exportPath = path.join(SOURCE_FOLDER, entry.sourceFile)\n    remixPackageJson.exports[entry.exportPath] = `./${exportPath}`\n\n    let distFile = path.join(entry.sourceFile.replace(/\\.ts$/, ''))\n    remixPackageJson.publishConfig.exports[entry.exportPath] = {\n      types: `./dist/${distFile}.d.ts`,\n      default: `./dist/${distFile}.js`,\n    }\n  }\n\n  remixPackageJson.exports['./package.json'] = './package.json'\n  remixPackageJson.publishConfig.exports['./package.json'] = './package.json'\n\n  for (let packageInfo of remixRunPackages) {\n    remixPackageJson.dependencies[packageInfo.name] = 'workspace:^'\n  }\n\n  await fs.writeFile(\n    remixPackageJsonPath,\n    JSON.stringify(remixPackageJson, null, 2) + '\\n',\n    'utf-8',\n  )\n}\n\n// Build exports change summary\nasync function outputExportsChangeFiles(exportsConfig: Record<string, string>) {\n  let newExportsSet = new Set<string>(\n    Object.keys(exportsConfig).filter((key) => key !== '.' && key !== './package.json'),\n  )\n  let addedExports = Array.from(newExportsSet).filter((key) => !existingExports.has(key))\n  let removedExports = Array.from(existingExports).filter((key) => !newExportsSet.has(key))\n\n  if (addedExports.length === 0 && removedExports.length === 0) {\n    return\n  }\n\n  let semverType = removedExports.length > 0 ? 'major' : 'minor'\n  let changeFileBaseName = 'remix.update-exports.md'\n  let changeFile = path.join(remixChangesDir, `${semverType}.${changeFileBaseName}`)\n  let alternateSemverType = semverType === 'major' ? 'minor' : 'major'\n  let alternateChangeFile = path.join(\n    remixChangesDir,\n    `${alternateSemverType}.${changeFileBaseName}`,\n  )\n  let legacyChangeFilePattern = /^(major|minor)\\.remix\\.update-exports-\\d+\\.md$/\n  let changes = ''\n\n  // Remove any old timestamped exports change files from prior runs.\n  for (let fileName of await fs.readdir(remixChangesDir)) {\n    if (!legacyChangeFilePattern.test(fileName)) {\n      continue\n    }\n    await fs.unlink(path.join(remixChangesDir, fileName))\n  }\n\n  // Remove the alternate semver deterministic file if present so we only keep one.\n  try {\n    await fs.unlink(alternateChangeFile)\n  } catch (e) {\n    if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n      throw e\n    }\n  }\n\n  if (removedExports.length > 0) {\n    console.log()\n    console.log('Removed package.json exports:')\n    changes += 'Removed `package.json` `exports`:\\n'\n    for (let exportPath of removedExports) {\n      exportPath = exportPath.replace('./', '')\n      let exportName = `remix/${exportPath}`\n      console.log(`   - ${exportName}`)\n      changes += ` - \\`${exportName}\\`\\n`\n\n      // Remove re-export file\n      let srcFile = path.join(remixDir, SOURCE_FOLDER, exportPath + '.ts')\n      try {\n        await fs.unlink(srcFile)\n      } catch (e) {\n        if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n          throw e\n        }\n      }\n    }\n  }\n\n  if (addedExports.length > 0) {\n    console.log()\n    console.log('Added package.json exports:')\n    changes += 'Added `package.json` `exports`:\\n'\n    for (let exportPath of addedExports) {\n      let entry = allExports.find((e) => e.exportPath === exportPath)\n      exportPath = `remix/${exportPath.replace('./', '')}`\n      if (entry) {\n        console.log(`   - ${exportPath} → ${entry.reExportFrom}`)\n        changes += ` - \\`${exportPath}\\` to re-export APIs from \\`${entry.reExportFrom}\\`\\n`\n      }\n    }\n  }\n\n  await fs.writeFile(changeFile, changes, 'utf-8')\n  console.log()\n  console.log('Created exports change file:')\n  console.log(`   - ${path.relative(process.cwd(), changeFile)}`)\n}\n"
  },
  {
    "path": "scripts/package.json",
    "content": "{\n  \"name\": \"remix-the-scripts\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@octokit/request\": \"^9.1.3\",\n    \"@types/node\": \"catalog:\",\n    \"@types/semver\": \"^7.5.8\",\n    \"semver\": \"^7.6.3\"\n  },\n  \"scripts\": {\n    \"test\": \"node --test\",\n    \"typecheck\": \"tsgo --noEmit\"\n  }\n}\n"
  },
  {
    "path": "scripts/pr-preview.ts",
    "content": "/**\n * PR Preview Script\n *\n * This script manages preview builds for pull requests by:\n * - Creating comments on PRs with installation instructions for preview builds\n * - Cleaning up preview branches when PRs are merged or closed\n *\n * Commands:\n * - `comment <pr-number>`: Adds a comment to the specified PR with instructions\n *   to install the preview build. Updates any existing preview comments.\n * - `cleanup <pr-number>`: Deletes the preview branch from the remote repository\n *   and adds a cleanup notification comment to the PR.\n *\n * Usage: `node pr-preview.ts <command> <pr-number>`\n */\n\nimport { parseArgs } from 'node:util'\n\nimport { createPrComment, deletePrComment, getPrComments, updatePrComment } from './utils/github.ts'\nimport { logAndExec } from './utils/process.ts'\n\nconst STICKY_MARKER = '<!-- pr-preview-comment-sticky -->'\nconst CLEANUP_MARKER = '<!-- pr-preview-comment-cleanup -->'\n\nlet { positionals } = parseArgs({\n  allowPositionals: true,\n  strict: true,\n})\n\nif (positionals.length !== 3) {\n  printUsage()\n  process.exit(1)\n}\n\nlet [command, prNumberString, branch] = positionals\nlet prNumber = parseInt(prNumberString, 10)\nif (isNaN(prNumber) || prNumber <= 0) {\n  printUsage()\n  throw new Error(`Invalid PR number: ${prNumberString}`)\n}\n\nlet commands: Record<string, () => Promise<void>> = {\n  comment,\n  cleanup,\n}\n\nif (commands[command]) {\n  await commands[command]()\n} else {\n  printUsage()\n  throw new Error(`Unknown command: ${command}`)\n}\n\nfunction printUsage() {\n  console.error('Usage: node pr-preview.ts <command> <args>')\n  console.error('  comment <pr-number> <preview-branch> - Add preview comment to PR')\n  console.error('  cleanup <pr-number> <preview-branch> - Delete branch from origin')\n}\n\nasync function comment() {\n  let commentBody = `\\\n${STICKY_MARKER}\n### Preview Build Available\n\nA preview build has been created for this PR. You can install it using:\n\n\\`\\`\\`sh\npnpm install \"remix-run/remix#${branch}&path:packages/remix\"\n\\`\\`\\`\n\nThis preview build will be updated automatically as you push new commits.`\n\n  // Get existing comments\n  let comments = await getPrComments(prNumber)\n\n  // Only add a comment if one doesn't already exist\n  let stickyComment = comments.find((comment) => comment.body?.includes(STICKY_MARKER))\n  if (stickyComment) {\n    console.log('Updating preview comment on PR')\n    await updatePrComment(stickyComment.id, commentBody)\n  } else {\n    console.log('Adding preview comment to PR')\n    await createPrComment(prNumber, commentBody)\n  }\n\n  // Delete cleanup comment if it exists\n  let cleanupComment = comments.find((comment) => comment.body?.includes(CLEANUP_MARKER))\n  if (cleanupComment) {\n    console.log('Deleting existing cleanup comment')\n    await deletePrComment(cleanupComment.id)\n  }\n}\n\nasync function cleanup() {\n  console.log(`Deleted branch: ${branch}`)\n  await logAndExec(`git push --delete origin ${branch}`)\n\n  let commentBody = `\\\n${CLEANUP_MARKER}\nThe preview branch \\`${branch}\\` has been deleted now that this PR is merged/closed.`\n\n  console.log('Adding cleanup comment to PR')\n  await createPrComment(prNumber, commentBody)\n}\n"
  },
  {
    "path": "scripts/publish.ts",
    "content": "/**\n * Publishes packages to npm and creates tags/releases for what was published.\n *\n * This script uses pnpm publish with --report-summary, reads the summary file,\n * and creates Git tags + GitHub releases. When the remix package is in prerelease\n * mode (has .changes/config.json with prereleaseChannel), it publishes in two phases:\n * all other packages as \"latest\", then remix with the \"next\" tag.\n *\n * This script is designed for CI use. For previewing releases, use `pnpm changes:preview`.\n *\n * Usage:\n *   node scripts/publish.ts [--skip-ci-check] [--dry-run]\n *\n * Options:\n *   --skip-ci-check  Bypass the CI environment check\n *   --dry-run        Show what would be published without actually publishing.\n *                    Queries npm to determine unpublished packages and previews\n *                    what the GitHub releases would look like.\n */\nimport * as cp from 'node:child_process'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\n\nimport {\n  findVersionIntroductionCommit,\n  getLocalTagTarget,\n  getRemoteTagTarget,\n  tagExists,\n} from './utils/git.ts'\nimport { createRelease, releaseExists } from './utils/github.ts'\nimport { getRootDir, logAndExec } from './utils/process.ts'\nimport { readChangesConfig, getChangelogEntry } from './utils/changes.ts'\nimport {\n  getAllPackageDirNames,\n  getPackageFile,\n  getGitTag,\n  packageNameToDirectoryName,\n  getPackageShortName,\n} from './utils/packages.ts'\nimport { readJson, fileExists } from './utils/fs.ts'\n\nlet rootDir = getRootDir()\n\nlet args = process.argv.slice(2)\nlet skipCiCheck = args.includes('--skip-ci-check')\nlet dryRun = args.includes('--dry-run')\n\ninterface PublishedPackage {\n  packageName: string\n  version: string\n  tag: string\n}\n\ninterface PublishSummary {\n  publishedPackages: Array<{\n    name: string\n    version: string\n  }>\n}\n\ninterface LocalPackage {\n  dirName: string\n  npmName: string\n  localVersion: string\n}\n\ninterface TagPlan {\n  pkg: PublishedPackage\n  targetCommit: string\n}\n\n/**\n * Read published packages from pnpm's publish summary file.\n * See https://pnpm.io/cli/publish#--report-summary\n */\nfunction readPublishSummary(): PublishedPackage[] {\n  let summaryPath = path.join(rootDir, 'pnpm-publish-summary.json')\n\n  if (!fs.existsSync(summaryPath)) {\n    throw new Error(\n      `pnpm-publish-summary.json not found. This is unexpected after a successful publish.`,\n    )\n  }\n\n  let summary: PublishSummary = JSON.parse(fs.readFileSync(summaryPath, 'utf-8'))\n\n  return summary.publishedPackages.map((pkg) => ({\n    packageName: pkg.name,\n    version: pkg.version,\n    tag: getGitTag(pkg.name, pkg.version),\n  }))\n}\n\n/**\n * Get local package metadata from the workspace.\n */\nfunction getLocalPackages(): LocalPackage[] {\n  let packageDirNames = getAllPackageDirNames()\n  let localPackages: LocalPackage[] = []\n\n  for (let packageDirName of packageDirNames) {\n    let packageJsonPath = getPackageFile(packageDirName, 'package.json')\n\n    // Skip directories without a package.json\n    if (!fileExists(packageJsonPath)) {\n      continue\n    }\n\n    let packageJson = readJson(packageJsonPath)\n    localPackages.push({\n      dirName: packageDirName,\n      npmName: packageJson.name as string,\n      localVersion: packageJson.version as string,\n    })\n  }\n\n  return localPackages\n}\n\n/**\n * Check if a specific version of a package is published on npm.\n */\nasync function isVersionPublished(packageName: string, version: string): Promise<boolean> {\n  return new Promise((resolve) => {\n    cp.exec(\n      `npm view ${packageName}@${version} version`,\n      { encoding: 'utf-8' },\n      (_error, stdout) => {\n        // If we get output that matches the version, it exists\n        resolve(stdout.trim() === version)\n      },\n    )\n  })\n}\n\n/**\n * Get all packages that have versions not yet published to npm.\n */\nasync function getUnpublishedPackages(): Promise<PublishedPackage[]> {\n  let localPackages = getLocalPackages()\n\n  // Query npm for all packages in parallel\n  let npmResults = await Promise.all(\n    localPackages.map(async (pkg) => ({\n      pkg,\n      isPublished: await isVersionPublished(pkg.npmName, pkg.localVersion),\n    })),\n  )\n\n  // Filter to unpublished packages\n  let unpublished: PublishedPackage[] = []\n  for (let { pkg, isPublished } of npmResults) {\n    if (!isPublished) {\n      unpublished.push({\n        packageName: pkg.npmName,\n        version: pkg.localVersion,\n        tag: getGitTag(pkg.npmName, pkg.localVersion),\n      })\n    }\n  }\n\n  return unpublished\n}\n\n/**\n * Find package versions that are already published to npm but missing git tags.\n * This enables release recovery after partial publish failures.\n */\nasync function getPublishedPackagesMissingTags(): Promise<PublishedPackage[]> {\n  let localPackages = getLocalPackages()\n\n  let npmResults = await Promise.all(\n    localPackages.map(async (pkg) => ({\n      pkg,\n      isPublished: await isVersionPublished(pkg.npmName, pkg.localVersion),\n    })),\n  )\n\n  let missingTags: PublishedPackage[] = []\n  for (let { pkg, isPublished } of npmResults) {\n    if (!isPublished) {\n      continue\n    }\n\n    let tag = getGitTag(pkg.npmName, pkg.localVersion)\n    if (!tagExists(tag)) {\n      missingTags.push({\n        packageName: pkg.npmName,\n        version: pkg.localVersion,\n        tag,\n      })\n    }\n  }\n\n  return missingTags\n}\n\n/**\n * Find package versions that are already published and tagged but missing GitHub releases.\n * This enables release recovery after tags were pushed successfully.\n */\nasync function getPublishedPackagesMissingReleases(): Promise<PublishedPackage[]> {\n  let localPackages = getLocalPackages()\n\n  let npmResults = await Promise.all(\n    localPackages.map(async (pkg) => ({\n      pkg,\n      isPublished: await isVersionPublished(pkg.npmName, pkg.localVersion),\n    })),\n  )\n\n  let missingReleases: PublishedPackage[] = []\n  for (let { pkg, isPublished } of npmResults) {\n    if (!isPublished) {\n      continue\n    }\n\n    let tag = getGitTag(pkg.npmName, pkg.localVersion)\n    if (!tagExists(tag)) {\n      continue\n    }\n\n    if (!(await releaseExists(tag))) {\n      missingReleases.push({\n        packageName: pkg.npmName,\n        version: pkg.localVersion,\n        tag,\n      })\n    }\n  }\n\n  return missingReleases\n}\n\nfunction dedupePublishedPackages(packages: PublishedPackage[]): PublishedPackage[] {\n  let seenTags = new Set<string>()\n  let deduped: PublishedPackage[] = []\n\n  for (let pkg of packages) {\n    if (seenTags.has(pkg.tag)) {\n      continue\n    }\n    seenTags.add(pkg.tag)\n    deduped.push(pkg)\n  }\n\n  return deduped\n}\n\nfunction isShallowRepository(): boolean {\n  try {\n    let output = cp.execFileSync('git', ['rev-parse', '--is-shallow-repository'], {\n      stdio: 'pipe',\n      encoding: 'utf-8',\n    })\n    return output.trim() === 'true'\n  } catch {\n    return false\n  }\n}\n\nfunction ensureGitHistoryForVersionLookup() {\n  if (isShallowRepository()) {\n    console.log('\\nRepository is shallow, fetching full history for release tag anchoring...')\n    logAndExec('git fetch --unshallow --tags origin')\n    return\n  }\n\n  console.log('\\nFetching tags from origin...')\n  logAndExec('git fetch --tags origin')\n}\n\nfunction resolveTagPlans(packages: PublishedPackage[]): TagPlan[] {\n  let plans: TagPlan[] = []\n\n  for (let pkg of packages) {\n    let packageDirName = packageNameToDirectoryName(pkg.packageName)\n    if (packageDirName === null) {\n      throw new Error(\n        `Could not map package \"${pkg.packageName}\" to a workspace directory for tag anchoring.`,\n      )\n    }\n\n    let packageJsonPath = path\n      .relative(rootDir, getPackageFile(packageDirName, 'package.json'))\n      .replaceAll('\\\\', '/')\n\n    let targetCommit = findVersionIntroductionCommit(packageJsonPath, pkg.version)\n    if (targetCommit === null) {\n      throw new Error(\n        `Could not find commit that introduced ${pkg.packageName}@${pkg.version} from ${packageJsonPath}. Ensure full git history is available.`,\n      )\n    }\n\n    plans.push({ pkg, targetCommit })\n  }\n\n  return plans\n}\n\nfunction verifyTagTargets(tagPlans: TagPlan[]) {\n  let mismatches: Array<{\n    tag: string\n    scope: 'local' | 'remote'\n    actual: string\n    expected: string\n  }> = []\n\n  for (let plan of tagPlans) {\n    let localTarget = getLocalTagTarget(plan.pkg.tag)\n    if (localTarget !== null && localTarget !== plan.targetCommit) {\n      mismatches.push({\n        tag: plan.pkg.tag,\n        scope: 'local',\n        actual: localTarget,\n        expected: plan.targetCommit,\n      })\n    }\n\n    let remoteTarget = getRemoteTagTarget(plan.pkg.tag)\n    if (remoteTarget !== null && remoteTarget !== plan.targetCommit) {\n      mismatches.push({\n        tag: plan.pkg.tag,\n        scope: 'remote',\n        actual: remoteTarget,\n        expected: plan.targetCommit,\n      })\n    }\n  }\n\n  if (mismatches.length > 0) {\n    let lines = ['Detected existing tags pointing at unexpected commits:']\n    for (let mismatch of mismatches) {\n      lines.push(\n        `  • ${mismatch.tag} (${mismatch.scope}) expected ${mismatch.expected.slice(0, 12)} but found ${mismatch.actual.slice(0, 12)}`,\n      )\n    }\n    lines.push('Refusing to continue to avoid creating inconsistent release metadata.')\n    throw new Error(lines.join('\\n'))\n  }\n}\n\ninterface ChangelogWarning {\n  packageName: string\n  version: string\n}\n\n/**\n * Preview GitHub releases for packages that would be published.\n * Returns warnings for packages with missing changelog entries.\n */\nfunction previewGitHubReleases(packages: PublishedPackage[]): { warnings: ChangelogWarning[] } {\n  let warnings: ChangelogWarning[] = []\n\n  console.log('GitHub Release Preview')\n  console.log('═'.repeat(60))\n  console.log()\n\n  for (let pkg of packages) {\n    let tagName = getGitTag(pkg.packageName, pkg.version)\n    let releaseName = `${getPackageShortName(pkg.packageName)} v${pkg.version}`\n    let changes = getChangelogEntry({ packageName: pkg.packageName, version: pkg.version })\n    let body = changes?.body ?? 'No changelog entry found for this version.'\n\n    if (changes === null) {\n      warnings.push({ packageName: pkg.packageName, version: pkg.version })\n    }\n\n    console.log(`📦 ${releaseName}`)\n    console.log(`   Tag: ${tagName}`)\n    console.log()\n    console.log('   Release notes:')\n    console.log()\n    for (let line of body.split('\\n')) {\n      console.log(`   ${line}`)\n    }\n    console.log()\n    console.log('─'.repeat(60))\n    console.log()\n  }\n\n  return { warnings }\n}\n\nasync function main() {\n  // Safety check: this script should only run in CI when not in dry run mode\n  if (!process.env.CI && !skipCiCheck && !dryRun) {\n    console.error('The publish script is designed for CI use only.')\n    console.error('Use --skip-ci-check to bypass this check for local use.')\n    console.error('Use --dry-run to preview the publish process.')\n    console.error('\\nFor previewing releases, use: pnpm changes:preview')\n    process.exit(1)\n  }\n\n  if (dryRun) {\n    console.log('🔍 DRY RUN MODE - No packages will be published\\n')\n  }\n\n  // Check if remix is in prerelease mode\n  let remixChangesConfig = readChangesConfig('remix')\n  let remixPrereleaseChannel: string | null = null\n\n  if (remixChangesConfig.exists) {\n    if (!remixChangesConfig.valid) {\n      console.error('Error reading remix changes config:', remixChangesConfig.error)\n      process.exit(1)\n    }\n    remixPrereleaseChannel = remixChangesConfig.config.prereleaseChannel || null\n    if (remixPrereleaseChannel) {\n      console.log(`Remix is in prerelease mode (channel: ${remixPrereleaseChannel})`)\n      console.log('Publishing in two phases: other packages as \"latest\", then remix as \"next\"\\n')\n    }\n  }\n\n  // Publish packages to npm\n  console.log('Publishing packages to npm...\\n')\n\n  let published: PublishedPackage[] = []\n\n  if (remixPrereleaseChannel) {\n    let publishCommands = [\n      // Phase 1: Publish everything in `packages` except remix (with --report-summary so we know what was published)\n      'pnpm publish --recursive --filter \"./packages/*\" --filter \"!remix\" --access public --no-git-checks --report-summary',\n      // Phase 2: Publish remix with \"next\" tag (with --report-summary so we know if remix was published)\n      'pnpm publish --filter remix --tag next --access public --no-git-checks --report-summary',\n    ]\n\n    if (dryRun) {\n      console.log('Would run:')\n      for (let publishCommand of publishCommands) {\n        console.log(`  $ ${publishCommand}`)\n      }\n      console.log()\n    } else {\n      for (let publishCommand of publishCommands) {\n        logAndExec(publishCommand)\n        published.push(...readPublishSummary())\n      }\n    }\n  } else {\n    // Single-phase publish: everything as latest\n    let publishCommand =\n      'pnpm publish --recursive --filter \"./packages/*\" --access public --no-git-checks --report-summary'\n\n    if (dryRun) {\n      console.log('Would run:')\n      console.log(`  $ ${publishCommand}`)\n      console.log()\n    } else {\n      logAndExec(publishCommand)\n      published.push(...readPublishSummary())\n    }\n  }\n\n  // In dry run mode, query npm to determine what would be published\n  // and preview the GitHub releases. This is designed to be run against\n  // the contents of the \"Release\" PR / `pnpm changes:version` output.\n  if (dryRun) {\n    console.log('Checking npm for unpublished versions...\\n')\n\n    let unpublished = await getUnpublishedPackages()\n\n    if (unpublished.length === 0) {\n      console.log('All package versions are already published to npm.')\n      console.log('\\n🔍 Dry run complete.')\n      return\n    }\n\n    console.log(\n      `${unpublished.length} package${unpublished.length === 1 ? '' : 's'} would be published:\\n`,\n    )\n    for (let pkg of unpublished) {\n      console.log(`  • ${pkg.packageName}@${pkg.version}`)\n    }\n    console.log()\n\n    let { warnings } = previewGitHubReleases(unpublished)\n\n    if (warnings.length > 0) {\n      console.log('⚠️  WARNINGS')\n      console.log('═'.repeat(60))\n      console.log()\n      console.log('The following packages have no changelog entry for their version:')\n      console.log()\n      for (let warning of warnings) {\n        console.log(`  • ${warning.packageName} v${warning.version}`)\n      }\n      console.log()\n      console.log('Their GitHub releases will show \"No changelog entry found for this version.\"')\n      console.log('This may indicate a missing or malformed CHANGELOG.md entry.')\n      console.log()\n    }\n\n    console.log(\n      '🔍 Dry run complete. No packages published, no git tags or GitHub releases created.',\n    )\n    return\n  }\n\n  if (published.length > 0) {\n    console.log(`\\n${published.length} package${published.length === 1 ? '' : 's'} published:`)\n    for (let pkg of published) {\n      console.log(`  • ${pkg.packageName}@${pkg.version}`)\n    }\n  } else {\n    console.log('\\nNo new packages were published.')\n  }\n\n  let packagesNeedingTagsOrReleases = dedupePublishedPackages([\n    ...published,\n    ...(await getPublishedPackagesMissingTags()),\n    ...(await getPublishedPackagesMissingReleases()),\n  ])\n\n  if (packagesNeedingTagsOrReleases.length === 0) {\n    console.log('\\nNo packages need git tags or GitHub releases.')\n    return\n  }\n\n  ensureGitHistoryForVersionLookup()\n\n  console.log('\\nResolving release tag targets...')\n  let tagPlans = resolveTagPlans(packagesNeedingTagsOrReleases)\n\n  for (let plan of tagPlans) {\n    console.log(`  • ${plan.pkg.tag} -> ${plan.targetCommit.slice(0, 12)}`)\n  }\n\n  verifyTagTargets(tagPlans)\n\n  // Configure git\n  console.log('\\nConfiguring git...')\n  logAndExec('git config user.name \"Remix Run Bot\"')\n  logAndExec('git config user.email \"hello@remix.run\"')\n\n  // Create tags (skip if already exist)\n  console.log(`\\nCreating tag${tagPlans.length === 1 ? '' : 's'} for published packages...`)\n  let createdTags: TagPlan[] = []\n  for (let plan of tagPlans) {\n    let localTarget = getLocalTagTarget(plan.pkg.tag)\n    let remoteTarget = getRemoteTagTarget(plan.pkg.tag)\n\n    if (localTarget !== null || remoteTarget !== null) {\n      let existingTarget = localTarget ?? remoteTarget\n      console.log(`  ⊘ ${plan.pkg.tag} (already exists at ${existingTarget?.slice(0, 12)})`)\n      continue\n    }\n\n    cp.execFileSync('git', ['tag', plan.pkg.tag, plan.targetCommit], { stdio: 'pipe' })\n    console.log(`  ✓ ${plan.pkg.tag} -> ${plan.targetCommit.slice(0, 12)}`)\n    createdTags.push(plan)\n  }\n\n  // Push tags if any were created\n  if (createdTags.length > 0) {\n    console.log(`\\nPushing tag${createdTags.length === 1 ? '' : 's'}...`)\n    for (let plan of createdTags) {\n      let ref = `refs/tags/${plan.pkg.tag}`\n      process.stdout.write(`  $ git push origin ${ref}\\n`)\n      try {\n        cp.execFileSync('git', ['push', 'origin', ref], { stdio: 'inherit' })\n      } catch {\n        let remoteTarget = getRemoteTagTarget(plan.pkg.tag)\n        if (remoteTarget === plan.targetCommit) {\n          console.log(\n            `  ⊘ ${plan.pkg.tag} (already exists remotely at ${plan.targetCommit.slice(0, 12)})`,\n          )\n          continue\n        }\n        throw new Error(\n          `Failed to push ${plan.pkg.tag}, and remote target does not match expected commit ${plan.targetCommit.slice(0, 12)}.`,\n        )\n      }\n    }\n  } else {\n    console.log('\\nNo new tags to push.')\n  }\n\n  // Create GitHub releases (skip if already exists)\n  console.log('\\nCreating GitHub releases...')\n  let failedReleases: Array<{ pkg: PublishedPackage; error: string }> = []\n\n  for (let pkg of packagesNeedingTagsOrReleases) {\n    let result = await createRelease(pkg.packageName, pkg.version)\n    if (result.status === 'created') {\n      console.log(`  ✓ ${pkg.packageName} v${pkg.version}`)\n    } else if (result.status === 'skipped') {\n      console.log(`  ⊘ ${pkg.packageName} v${pkg.version} (${result.reason.toLowerCase()})`)\n    } else {\n      console.log(`  ✗ ${pkg.packageName} v${pkg.version} (failed)`)\n      failedReleases.push({ pkg, error: result.error })\n    }\n  }\n\n  // Report any failures\n  if (failedReleases.length > 0) {\n    console.error('\\n⚠️  Some GitHub releases failed to create:')\n    for (let { pkg, error } of failedReleases) {\n      console.error(`  • ${pkg.packageName} v${pkg.version}: ${error}`)\n    }\n    console.error('\\nYou may need to create these releases manually.')\n    process.exit(1)\n  }\n\n  console.log('\\n✅ Done.')\n}\n\nmain()\n"
  },
  {
    "path": "scripts/release-pr.ts",
    "content": "/**\n * Opens or updates the release PR.\n *\n * Usage:\n *   node scripts/release-pr.ts [--preview]\n *\n * Environment:\n *   GITHUB_TOKEN - Required (unless --preview)\n */\nimport { parseAllChangeFiles, generateCommitMessage } from './utils/changes.ts'\nimport { generatePrBody } from './utils/release-pr.ts'\nimport { logAndExec } from './utils/process.ts'\nimport { findOpenPr, createPr, updatePr, closePr } from './utils/github.ts'\n\nlet args = process.argv.slice(2)\nlet preview = args.includes('--preview')\n\nlet baseBranch = 'main'\nlet prBranch = 'release-pr/main'\nlet prTitle = 'Release'\n\nasync function main() {\n  console.log(preview ? '🔍 PREVIEW MODE\\n' : '')\n\n  // Parse and validate changes\n  console.log('Validating change files...')\n  let result = parseAllChangeFiles()\n\n  if (!result.valid) {\n    console.error('Validation errors found:')\n    for (let error of result.errors) {\n      console.error(`  ${error.packageDirName}/${error.file}: ${error.error}`)\n    }\n    process.exit(1)\n  }\n\n  let { releases } = result\n\n  if (releases.length === 0) {\n    console.log('No pending changes to release.')\n\n    // Check if there's a stale PR that should be closed\n    if (!preview && process.env.GITHUB_TOKEN) {\n      let existingPr = await findOpenPr(prBranch, baseBranch)\n      if (existingPr) {\n        console.log(`\\nClosing stale PR #${existingPr.number}...`)\n        await closePr(\n          existingPr.number,\n          'Closing automatically — all change files have been removed or released.',\n        )\n        console.log(`✅ Closed PR: ${existingPr.html_url}`)\n      }\n    }\n\n    process.exit(0)\n  }\n\n  console.log(`\\nFound ${releases.length} package${releases.length === 1 ? '' : 's'} with changes:`)\n  for (let release of releases) {\n    console.log(`  • ${release.packageName}: ${release.currentVersion} → ${release.nextVersion}`)\n  }\n  console.log()\n\n  // Generate content\n  let commitMessage = generateCommitMessage(releases)\n  let prBody = generatePrBody(releases)\n\n  if (preview) {\n    console.log('Would create/update PR with:')\n    console.log(`  Branch: ${prBranch}`)\n    console.log(`  Title: ${prTitle}`)\n    console.log(`  Commit: ${commitMessage.split('\\n')[0]}`)\n    console.log('\\nPR Body:')\n    console.log('─'.repeat(60))\n    console.log(prBody)\n    console.log('─'.repeat(60))\n    console.log('\\nPreview complete. No changes made.')\n    process.exit(0)\n  }\n\n  // Require token for non-preview\n  if (!process.env.GITHUB_TOKEN) {\n    console.error('GITHUB_TOKEN environment variable is required')\n    process.exit(1)\n  }\n\n  // Configure git\n  console.log('Configuring git...')\n  logAndExec('git config user.name \"Remix Run Bot\"')\n  logAndExec('git config user.email \"hello@remix.run\"')\n\n  // Create or switch to PR branch\n  console.log(`\\nSwitching to branch: ${prBranch}`)\n  logAndExec(`git checkout -B ${prBranch}`)\n\n  // Reset to base branch\n  logAndExec(`git reset --hard origin/${baseBranch}`)\n\n  // Run version command\n  console.log('\\nRunning pnpm changes:version...')\n  logAndExec('pnpm changes:version')\n\n  console.log('\\nPushing branch...')\n  logAndExec(`git push origin ${prBranch} --force`)\n\n  // Create or update PR\n  console.log('\\nChecking for existing PR...')\n  let existingPr = await findOpenPr(prBranch, baseBranch)\n\n  if (existingPr) {\n    console.log(`Updating existing PR #${existingPr.number}...`)\n    await updatePr(existingPr.number, { title: prTitle, body: prBody })\n    console.log(`\\n✅ Updated PR: ${existingPr.html_url}`)\n  } else {\n    console.log('Creating new PR...')\n    let newPr = await createPr({ title: prTitle, body: prBody, head: prBranch, base: baseBranch })\n    console.log(`\\n✅ Created PR #${newPr.number}: ${newPr.html_url}`)\n  }\n}\n\nmain().catch((error) => {\n  console.error('Error:', error.message)\n  process.exit(1)\n})\n"
  },
  {
    "path": "scripts/setup-installable-branch.ts",
    "content": "import * as fsp from 'node:fs/promises'\nimport * as path from 'node:path'\nimport * as util from 'node:util'\nimport { logAndExec } from './utils/process.ts'\n\n/**\n * This script prepares a base branch (usually `main`) to be PNPM-installable\n * directly from GitHub via a new branch (usually `preview/main`):\n *\n *   pnpm install \"remix-run/remix#preview/main&path:packages/remix\"\n *\n * To do this, we can run a build, make some minor changes to the repo, and\n * commit the build + changes to the new branch. These changes would never be\n * down-merged back to the source branch.\n *\n * This script does the following:\n *  - Checks out the new branch and resets it to the base (current) branch\n *  - Runs a build\n *  - Removes `dist/` from `.gitignore`\n *  - Updates all internal `@remix-run/*` deps to use the github format for the\n *    given installable branch\n *  - Copies all `publishConfig`'s down so we get `exports` from `dist/` instead of `src/`\n *  - Commits the changes\n *\n * Then, after pushing, `pnpm install \"remix-run/remix#preview/main&path:packages/remix\"`\n * sees the `remix` nested deps and they all point to github with similar URLs so\n * they install as nested deps the same way.\n */\n\nlet { positionals } = util.parseArgs({\n  allowPositionals: true,\n})\n\n// Use first positional argument or fall back to --branch flag or default\nlet installableBranch = positionals[0]\nif (!installableBranch) {\n  throw new Error('Error: You must provide an installable branch name')\n}\n\n// Error if git status is not clean\nlet gitStatus = logAndExec('git status --porcelain', true)\nif (gitStatus) {\n  throw new Error('Error: Git working directory is not clean. Commit or stash changes first.')\n}\n\n// Capture the current branch name\nlet sha = logAndExec('git rev-parse --short HEAD ', true).trim()\n\nconsole.log(`Preparing installable branch \\`${installableBranch}\\` from sha ${sha}`)\n\n// Switch to new branch and reset to current commit on base branch\nlogAndExec(`git checkout -B ${installableBranch}`)\n\n// Build dist/ folders\nlogAndExec('pnpm build')\n\nawait updateGitignore()\nawait updatePackageDependencies()\n\nlogAndExec('git add .')\nlogAndExec(`git commit -a -m \"installable build from ${sha}\"`)\n\nconsole.log(\n  [\n    '',\n    `✅ Done!`,\n    '',\n    `You can now push the \\`${installableBranch}\\` branch to GitHub and install via:`,\n    '',\n    `  pnpm install \"remix-run/remix#${installableBranch}&path:packages/remix\"`,\n  ].join('\\n'),\n)\n\n// Remove `dist` from gitignore so we include built code in the repo\nasync function updateGitignore() {\n  let gitignorePath = path.join(process.cwd(), '.gitignore')\n  let content = await fsp.readFile(gitignorePath, 'utf-8')\n  let filtered = content\n    .split('\\n')\n    .filter((line) => !line.trim().startsWith('dist'))\n    .join('\\n')\n  await fsp.writeFile(gitignorePath, filtered)\n  console.log('Updated .gitignore')\n}\n\n// Update `package.json` files to point to this branch on github\nasync function updatePackageDependencies() {\n  let packagesDir = path.join(process.cwd(), 'packages')\n\n  let packageDirNames = await fsp.readdir(packagesDir, { withFileTypes: true })\n\n  for (let dir of packageDirNames) {\n    if (!dir.isDirectory()) continue\n\n    let packageJsonPath = path.join(packagesDir, dir.name, 'package.json')\n    let content = await fsp.readFile(packageJsonPath, 'utf-8')\n    let pkg = JSON.parse(content)\n\n    // Point all `@remix-run/` dependencies to this branch on github\n    if (pkg.dependencies) {\n      for (let name of Object.keys(pkg.dependencies)) {\n        if (name.startsWith('@remix-run/')) {\n          let packageDirName = name.replace('@remix-run/', '')\n          pkg.dependencies[name] =\n            `remix-run/remix#${installableBranch}&path:packages/${packageDirName}`\n        }\n      }\n    }\n\n    // Apply `publishConfig` overrides\n    if (pkg.publishConfig) {\n      Object.assign(pkg, pkg.publishConfig)\n      delete pkg.publishConfig\n    }\n\n    await fsp.writeFile(packageJsonPath, JSON.stringify(pkg, null, 2) + '\\n')\n    console.log(`Updated ${dir.name}`)\n  }\n\n  console.log('Done')\n}\n\nfunction commitChanges() {}\n"
  },
  {
    "path": "scripts/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"ES2024\"],\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"noEmit\": true,\n    \"allowImportingTsExtensions\": true\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "scripts/utils/changes.test.ts",
    "content": "import assert from 'node:assert/strict'\nimport { test } from 'node:test'\nimport type { PackageRelease } from './changes.ts'\nimport { generateChangelogContent } from './changes.ts'\n\nfunction makeRelease(overrides: Partial<PackageRelease> = {}): PackageRelease {\n  return {\n    packageDirName: 'remix',\n    packageName: 'remix',\n    currentVersion: '3.0.0-alpha.3',\n    nextVersion: '3.0.0-alpha.4',\n    bump: 'patch',\n    changes: [],\n    dependencyBumps: [],\n    ...overrides,\n  }\n}\n\ntest('generateChangelogContent groups prerelease changes into a single section', () => {\n  let content = generateChangelogContent(\n    makeRelease({\n      changes: [\n        {\n          file: 'minor.alpha.md',\n          bump: 'minor',\n          content: 'Alpha change',\n        },\n        {\n          file: 'major.breaking.md',\n          bump: 'major',\n          content: 'BREAKING CHANGE: Breaking change',\n        },\n        {\n          file: 'patch.fix.md',\n          bump: 'patch',\n          content: 'Patch change',\n        },\n      ],\n      dependencyBumps: [\n        {\n          packageName: '@remix-run/router',\n          version: '1.2.3',\n          releaseUrl: 'https://example.com/router',\n        },\n      ],\n    }),\n  )\n\n  assert.match(content, /^## v3\\.0\\.0-alpha\\.4/m)\n  assert.match(content, /^### Pre-release Changes$/m)\n  assert.doesNotMatch(content, /^### Major Changes$/m)\n  assert.doesNotMatch(content, /^### Minor Changes$/m)\n  assert.doesNotMatch(content, /^### Patch Changes$/m)\n  assert.match(content, /- BREAKING CHANGE: Breaking change/)\n  assert.match(content, /- Alpha change/)\n  assert.match(content, /- Patch change/)\n  assert.match(content, /- Bumped `@remix-run\\/\\*` dependencies:/)\n  assert.match(content, /\\[`router@1\\.2\\.3`\\]\\(https:\\/\\/example\\.com\\/router\\)/)\n})\n\ntest('generateChangelogContent keeps stable releases grouped by bump type', () => {\n  let content = generateChangelogContent(\n    makeRelease({\n      currentVersion: '3.0.0',\n      nextVersion: '3.0.1',\n      changes: [\n        {\n          file: 'minor.feature.md',\n          bump: 'minor',\n          content: 'Feature change',\n        },\n      ],\n      dependencyBumps: [\n        {\n          packageName: '@remix-run/router',\n          version: '1.2.3',\n          releaseUrl: 'https://example.com/router',\n        },\n      ],\n    }),\n  )\n\n  assert.match(content, /^## v3\\.0\\.1/m)\n  assert.match(content, /^### Minor Changes$/m)\n  assert.match(content, /^### Patch Changes$/m)\n  assert.doesNotMatch(content, /^### Pre-release Changes$/m)\n})\n"
  },
  {
    "path": "scripts/utils/changes.ts",
    "content": "import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport {\n  getAllPackageDirNames,\n  getPackageFile,\n  getPackagePath,\n  packageNameToDirectoryName,\n  getTransitiveDependents,\n  getGitHubReleaseUrl,\n  getPackageDependencies,\n  getGitTag,\n} from './packages.ts'\nimport { fileExists, readFile, readJson } from './fs.ts'\nimport { inc, major, prerelease, type ReleaseType } from './semver.ts'\n\nconst bumpTypes = ['major', 'minor', 'patch'] as const\ntype BumpType = (typeof bumpTypes)[number]\n\n// Changes configuration (from packages/remix/.changes/config.json)\n// Only the remix package supports changes config.\nexport interface ChangesConfig {\n  prereleaseChannel: string\n}\n\nexport type ParsedChangesConfig =\n  | { exists: false }\n  | { exists: true; valid: true; config: ChangesConfig }\n  | { exists: true; valid: false; error: string }\n\n/**\n * Reads and validates a package's .changes/config.json.\n */\nexport function readChangesConfig(packageDirName: string): ParsedChangesConfig {\n  let packagePath = getPackagePath(packageDirName)\n  let configJsonPath = path.join(packagePath, '.changes', 'config.json')\n\n  if (!fs.existsSync(configJsonPath)) {\n    return { exists: false }\n  }\n\n  let content: unknown\n  try {\n    content = JSON.parse(fs.readFileSync(configJsonPath, 'utf-8'))\n  } catch {\n    return { exists: true, valid: false, error: 'Invalid JSON in .changes/config.json' }\n  }\n\n  if (typeof content !== 'object' || content === null) {\n    return {\n      exists: true,\n      valid: false,\n      error: '.changes/config.json must be an object',\n    }\n  }\n\n  let obj = content as Record<string, unknown>\n\n  if ('prereleaseChannel' in obj) {\n    if (typeof obj.prereleaseChannel !== 'string' || obj.prereleaseChannel.trim().length === 0) {\n      return {\n        exists: true,\n        valid: false,\n        error: '.changes/config.json \"prereleaseChannel\" must be a non-empty string',\n      }\n    }\n    return {\n      exists: true,\n      valid: true,\n      config: { prereleaseChannel: obj.prereleaseChannel.trim() },\n    }\n  }\n\n  return { exists: true, valid: true, config: { prereleaseChannel: '' } }\n}\n\n/**\n * Extracts the prerelease identifier from a version string (e.g., \"alpha\" from \"3.0.0-alpha.5\")\n */\nfunction getPrereleaseIdentifier(version: string): string | null {\n  let parts = prerelease(version)\n  if (parts === null || parts.length === 0) {\n    return null\n  }\n  return typeof parts[0] === 'string' ? parts[0] : null\n}\n\n/**\n * Calculates the next version based on current version, bump type, and changes config.\n */\nfunction getNextVersion(\n  currentVersion: string,\n  bumpType: BumpType,\n  changesConfig: ChangesConfig | null,\n): string {\n  let currentPrereleaseId = getPrereleaseIdentifier(currentVersion)\n  let isCurrentPrerelease = currentPrereleaseId !== null\n\n  if (changesConfig !== null && changesConfig.prereleaseChannel) {\n    // In prerelease mode\n    let targetChannel = changesConfig.prereleaseChannel\n\n    if (currentPrereleaseId === targetChannel) {\n      // Same channel - just bump the counter\n      let nextVersion = inc(currentVersion, 'prerelease', targetChannel)\n      if (nextVersion == null) {\n        throw new Error(`Invalid prerelease increment: ${currentVersion}`)\n      }\n      return nextVersion\n    } else {\n      // Entering prerelease or transitioning to a new channel (e.g., stable → alpha, or alpha → beta)\n      // Apply the bump type to get the base version, then add prerelease suffix\n      let baseVersion = isCurrentPrerelease\n        ? currentVersion.replace(/-.*$/, '') // Strip existing prerelease suffix\n        : inc(currentVersion, bumpType as ReleaseType)\n\n      if (baseVersion == null) {\n        throw new Error(`Invalid version increment: ${currentVersion} + ${bumpType}`)\n      }\n\n      return `${baseVersion}-${targetChannel}.0`\n    }\n  } else {\n    // Not in prerelease mode\n    if (isCurrentPrerelease) {\n      // Graduating from prerelease to stable - strip the prerelease suffix\n      let baseVersion = currentVersion.replace(/-.*$/, '')\n      return baseVersion\n    } else {\n      // Normal stable release\n      let nextVersion = inc(currentVersion, bumpType as ReleaseType)\n      if (nextVersion == null) {\n        throw new Error(`Invalid version increment: ${currentVersion} + ${bumpType}`)\n      }\n      return nextVersion\n    }\n  }\n}\n\ninterface ChangeFile {\n  file: string\n  bump: BumpType\n  content: string\n}\n\ninterface ValidationError {\n  packageDirName: string\n  file: string\n  error: string\n}\n\ntype ParsedPackageChanges =\n  | { valid: true; changes: ChangeFile[]; changesConfig: ChangesConfig | null }\n  | { valid: false; errors: ValidationError[] }\n\n/**\n * Parses and validates all change files for a package.\n * Returns changes if valid, or errors if invalid.\n */\nfunction parsePackageChanges(packageDirName: string): ParsedPackageChanges {\n  let packagePath = getPackagePath(packageDirName)\n  let changesDir = path.join(packagePath, '.changes')\n  let changes: ChangeFile[] = []\n  let errors: ValidationError[] = []\n\n  // Changes directory should exist (with at least README.md)\n  if (!fs.existsSync(changesDir)) {\n    return {\n      valid: false,\n      errors: [\n        {\n          packageDirName,\n          file: '.changes/',\n          error: 'Changes directory does not exist',\n        },\n      ],\n    }\n  }\n\n  // README.md should exist in .changes directory so it persists between releases\n  let readmePath = path.join(changesDir, 'README.md')\n  if (!fs.existsSync(readmePath)) {\n    errors.push({\n      packageDirName,\n      file: '.changes/README.md',\n      error: 'README.md is missing from .changes directory',\n    })\n  }\n\n  // Get package version to determine validation rules\n  let packageJsonPath = getPackageFile(packageDirName, 'package.json')\n  let packageJson = readJson(packageJsonPath)\n  let currentVersion = packageJson.version as string\n  let majorVersion = major(currentVersion)\n  let isV1Plus = majorVersion >= 1\n  let currentVersionPrereleaseId = getPrereleaseIdentifier(currentVersion)\n  let isCurrentVersionPrerelease = currentVersionPrereleaseId !== null\n\n  // Handle .changes/config.json - only supported for remix package\n  let changesConfig: ChangesConfig | null = null\n  let configJsonPath = path.join(changesDir, 'config.json')\n\n  if (packageDirName === 'remix') {\n    // For remix, read and validate the changes config\n    let parsedChangesConfig = readChangesConfig(packageDirName)\n    if (parsedChangesConfig.exists) {\n      if (!parsedChangesConfig.valid) {\n        errors.push({\n          packageDirName,\n          file: '.changes/config.json',\n          error: parsedChangesConfig.error,\n        })\n        return { valid: false, errors }\n      }\n      changesConfig = parsedChangesConfig.config\n    }\n  } else {\n    // For non-remix packages, error if config.json exists\n    if (fs.existsSync(configJsonPath)) {\n      errors.push({\n        packageDirName,\n        file: '.changes/config.json',\n        error: '.changes/config.json is only supported for the \"remix\" package. Remove this file.',\n      })\n      return { valid: false, errors }\n    }\n  }\n\n  // Read all files in .changes directory\n  let files = fs.readdirSync(changesDir)\n  let changeFileNames = files.filter((file) => file !== 'README.md' && file !== 'config.json')\n  let hasChangeFiles = changeFileNames.filter((f) => f.endsWith('.md')).length > 0\n\n  // Validate changes config / version consistency\n  let configPrereleaseChannel = changesConfig?.prereleaseChannel ?? null\n  let isActivePrereleaseMode = Boolean(configPrereleaseChannel) && isCurrentVersionPrerelease\n  if (configPrereleaseChannel) {\n    // Config has prerelease channel\n    if (\n      currentVersionPrereleaseId !== null &&\n      currentVersionPrereleaseId !== configPrereleaseChannel\n    ) {\n      // Channel mismatch (e.g., version is alpha but config says beta) - need change files to transition\n      if (!hasChangeFiles) {\n        errors.push({\n          packageDirName,\n          file: '.changes/config.json',\n          error: `prereleaseChannel '${configPrereleaseChannel}' doesn't match version's prerelease identifier '${currentVersionPrereleaseId}'. Add a change file to transition to ${configPrereleaseChannel}.`,\n        })\n      }\n    } else if (!isCurrentVersionPrerelease && !hasChangeFiles) {\n      // Config says prerelease but version is stable AND no change files - need change files to enter prerelease\n      errors.push({\n        packageDirName,\n        file: '.changes/config.json',\n        error: `prereleaseChannel exists but version ${currentVersion} is stable. Add a change file to enter prerelease mode, or remove prereleaseChannel if this package should not be in prerelease.`,\n      })\n    }\n  } else {\n    // No prerelease channel - validate version is stable (unless graduating with change files)\n    if (isCurrentVersionPrerelease && !hasChangeFiles) {\n      errors.push({\n        packageDirName,\n        file: '.changes/',\n        error: `Version ${currentVersion} is a prerelease but no prereleaseChannel exists. Either add .changes/config.json with { \"prereleaseChannel\": \"${currentVersionPrereleaseId}\" }, or add a change file to graduate to stable.`,\n      })\n    }\n  }\n\n  for (let file of changeFileNames) {\n    // Skip non-.md files\n    if (!file.endsWith('.md')) {\n      continue\n    }\n\n    // Parse filename format when it follows bump naming (e.g. \"minor.add-feature.md\")\n    let bump: BumpType | null = null\n\n    let withoutExt = file.slice(0, -3)\n    let dotIndex = withoutExt.indexOf('.')\n    if (dotIndex !== -1) {\n      let bumpStr = withoutExt.slice(0, dotIndex)\n      let name = withoutExt.slice(dotIndex + 1)\n      if (bumpTypes.includes(bumpStr as BumpType) && name.length > 0) {\n        bump = bumpStr as BumpType\n      }\n    }\n\n    if (bump == null && isActivePrereleaseMode) {\n      // In prerelease mode, bump type does not affect versioning, so any filename is allowed.\n      bump = 'patch'\n    }\n\n    if (bump == null) {\n      errors.push({\n        packageDirName,\n        file,\n        error:\n          'Change file must be a \".md\" file starting with \"major.\", \"minor.\", or \"patch.\" (e.g. \"minor.add-feature.md\")',\n      })\n      continue\n    }\n\n    // Read file content\n    let filePath = path.join(changesDir, file)\n    let content = fs.readFileSync(filePath, 'utf-8').trim()\n\n    // Check if file is not empty\n    if (content.length === 0) {\n      errors.push({\n        packageDirName,\n        file,\n        error: 'Change file cannot be empty',\n      })\n      continue\n    }\n\n    // Check if first line starts with a bullet point\n    let firstLine = content.split('\\n')[0].trim()\n    if (firstLine.startsWith('- ') || firstLine.startsWith('* ')) {\n      errors.push({\n        packageDirName,\n        file,\n        error:\n          'Change file should not start with a bullet point (- or *). The bullet will be added automatically in the CHANGELOG. Just write the text directly.',\n      })\n      continue\n    }\n\n    // Check for headings that aren't level 4, 5, or 6\n    let invalidHeadingMatch = content.match(/^(#{1,3}|#{7,})\\s+/m)\n    if (invalidHeadingMatch) {\n      let headingLevel = invalidHeadingMatch[1].length\n      errors.push({\n        packageDirName,\n        file,\n        error: `Headings in change files must be level 4 (####), 5 (#####), or 6 (######), but found level ${headingLevel}. This is because change files are nested within the changelog which already uses heading levels 1-3.`,\n      })\n      continue\n    }\n\n    // Validate breaking change prefix matches the correct bump type (only for stable releases)\n    // In prerelease mode, breaking changes don't need special handling since we're just bumping counter\n    if (!configPrereleaseChannel) {\n      let isBreakingChange = hasBreakingChangePrefix(content)\n\n      if (isBreakingChange) {\n        if (isV1Plus && bump !== 'major') {\n          errors.push({\n            packageDirName,\n            file,\n            error: `Breaking changes in v1+ packages must use \"major.\" prefix (current version: ${currentVersion}). Rename to \"major.${file.slice(file.indexOf('.') + 1)}\"`,\n          })\n          continue\n        } else if (!isV1Plus && !isCurrentVersionPrerelease && bump !== 'minor') {\n          errors.push({\n            packageDirName,\n            file,\n            error: `Breaking changes in v0.x packages must use \"minor.\" prefix (current version: ${currentVersion}). Rename to \"minor.${file.slice(file.indexOf('.') + 1)}\"`,\n          })\n          continue\n        }\n      }\n    }\n\n    // File is valid, add to changes\n    changes.push({ file, bump, content })\n  }\n\n  // Validate entering prerelease requires a major bump\n  if (configPrereleaseChannel && !isCurrentVersionPrerelease && changes.length > 0) {\n    let hasMajorBump = changes.some((c) => c.bump === 'major')\n    if (!hasMajorBump) {\n      errors.push({\n        packageDirName,\n        file: '.changes/config.json',\n        error:\n          'Entering prerelease mode requires a major version bump. Add a change file with \"major.\" prefix (e.g. \"major.release-v2-alpha.md\").',\n      })\n    }\n  }\n\n  if (errors.length > 0) {\n    return { valid: false, errors }\n  }\n\n  return { valid: true, changes, changesConfig }\n}\n\n/**\n * Represents a dependency that was bumped, triggering this release.\n */\nexport interface DependencyBump {\n  packageName: string\n  version: string\n  releaseUrl: string\n}\n\nexport interface PackageRelease {\n  packageDirName: string\n  packageName: string\n  currentVersion: string\n  nextVersion: string\n  bump: BumpType\n  changes: ChangeFile[]\n  /** Dependencies that were bumped, triggering this release (if any) */\n  dependencyBumps: DependencyBump[]\n}\n\ntype ParsedChanges =\n  | { valid: true; releases: PackageRelease[] }\n  | { valid: false; errors: ValidationError[] }\n\n/**\n * Parses and validates all change files across all packages.\n * Also includes packages that need to be released due to dependency changes.\n * Returns releases if valid, or errors if invalid.\n */\nexport function parseAllChangeFiles(): ParsedChanges {\n  let packageDirNames = getAllPackageDirNames()\n  let errors: ValidationError[] = []\n\n  // Build maps for lookup\n  let dirNameToPackageName = new Map<string, string>()\n  let packageNameToDirName = new Map<string, string>()\n\n  // First pass: collect package info and validate change files\n  interface ParsedPackageInfo {\n    packageDirName: string\n    packageName: string\n    currentVersion: string\n    changes: ChangeFile[]\n    changesConfig: ChangesConfig | null\n  }\n  let parsedPackages: ParsedPackageInfo[] = []\n\n  // Read the remix changes config once (only remix supports changes config)\n  let remixChangesConfig = readChangesConfig('remix')\n  let validRemixChangesConfig: ChangesConfig | null = null\n  if (remixChangesConfig.exists && remixChangesConfig.valid) {\n    validRemixChangesConfig = remixChangesConfig.config\n  }\n\n  for (let packageDirName of packageDirNames) {\n    let parsed = parsePackageChanges(packageDirName)\n\n    if (!parsed.valid) {\n      errors.push(...parsed.errors)\n      continue\n    }\n\n    let packageJsonPath = getPackageFile(packageDirName, 'package.json')\n    let packageJson = readJson(packageJsonPath)\n    let packageName = packageJson.name as string\n    let currentVersion = packageJson.version as string\n\n    dirNameToPackageName.set(packageDirName, packageName)\n    packageNameToDirName.set(packageName, packageDirName)\n\n    // For remix package, use the changes config even if there are no change files\n    // (to correctly bump prerelease counter for dependency-triggered releases)\n    let changesConfig = parsed.changesConfig\n    if (packageDirName === 'remix' && changesConfig === null && validRemixChangesConfig) {\n      changesConfig = validRemixChangesConfig\n    }\n\n    parsedPackages.push({\n      packageDirName,\n      packageName,\n      currentVersion,\n      changes: parsed.changes,\n      changesConfig,\n    })\n  }\n\n  if (errors.length > 0) {\n    return { valid: false, errors }\n  }\n\n  // Find packages with direct changes\n  let directlyChangedPackages = new Set<string>()\n  for (let pkg of parsedPackages) {\n    if (pkg.changes.length > 0) {\n      directlyChangedPackages.add(pkg.packageName)\n    }\n  }\n\n  // Find all packages that transitively depend on changed packages\n  let transitiveDependents = getTransitiveDependents(directlyChangedPackages)\n\n  // Determine all packages that will be released\n  let allReleasingPackages = new Set<string>([\n    ...directlyChangedPackages,\n    ...transitiveDependents.keys(),\n  ])\n\n  // Compute next versions for all releasing packages\n  // We need to do this in dependency order to correctly compute dependency bumps\n  let packageVersions = new Map<string, string>() // packageName -> nextVersion\n\n  // First, compute versions for directly changed packages\n  for (let pkg of parsedPackages) {\n    if (pkg.changes.length > 0) {\n      let bump = getHighestBump(pkg.changes.map((c) => c.bump))\n      if (bump == null) continue\n      let nextVersion = getNextVersion(pkg.currentVersion, bump, pkg.changesConfig)\n      packageVersions.set(pkg.packageName, nextVersion)\n    }\n  }\n\n  // Then, compute versions for dependency-triggered releases\n  // We need to do this iteratively because a package's version depends on knowing\n  // which of its dependencies are being released\n  for (let pkg of parsedPackages) {\n    if (\n      !directlyChangedPackages.has(pkg.packageName) &&\n      allReleasingPackages.has(pkg.packageName)\n    ) {\n      // This package is being released due to dependency changes\n      // Use the package's changes config if it has one (e.g., remix in prerelease mode)\n      let nextVersion = getNextVersion(pkg.currentVersion, 'patch', pkg.changesConfig)\n      packageVersions.set(pkg.packageName, nextVersion)\n    }\n  }\n\n  // Now build the final releases with dependency bumps\n  let releases: PackageRelease[] = []\n\n  for (let pkg of parsedPackages) {\n    if (!allReleasingPackages.has(pkg.packageName)) {\n      continue\n    }\n\n    let nextVersion = packageVersions.get(pkg.packageName)\n    if (nextVersion == null) continue\n\n    // Compute dependency bumps: which of this package's direct dependencies are being released?\n    let dependencyBumps: DependencyBump[] = []\n    let deps = getPackageDependencies(pkg.packageName)\n\n    for (let depName of deps) {\n      if (allReleasingPackages.has(depName)) {\n        let depVersion = packageVersions.get(depName)\n        if (depVersion) {\n          dependencyBumps.push({\n            packageName: depName,\n            version: depVersion,\n            releaseUrl: getGitHubReleaseUrl(depName, depVersion),\n          })\n        }\n      }\n    }\n\n    // Sort dependency bumps alphabetically by package name\n    dependencyBumps.sort((a, b) => a.packageName.localeCompare(b.packageName))\n\n    let bump: BumpType = 'patch'\n    if (pkg.changes.length > 0) {\n      bump = getHighestBump(pkg.changes.map((c) => c.bump)) ?? 'patch'\n    }\n\n    releases.push({\n      packageDirName: pkg.packageDirName,\n      packageName: pkg.packageName,\n      currentVersion: pkg.currentVersion,\n      nextVersion,\n      bump,\n      changes: pkg.changes,\n      dependencyBumps,\n    })\n  }\n\n  // Sort by package name for consistency\n  releases.sort((a, b) => a.packageName.localeCompare(b.packageName))\n\n  return { valid: true, releases }\n}\n\n/**\n * Formats validation errors for display\n */\nexport function formatValidationErrors(errors: ValidationError[]): string {\n  let errorsByPackageDirName: Record<string, ValidationError[]> = {}\n  for (let error of errors) {\n    if (!errorsByPackageDirName[error.packageDirName]) {\n      errorsByPackageDirName[error.packageDirName] = []\n    }\n    errorsByPackageDirName[error.packageDirName].push(error)\n  }\n\n  let lines: string[] = []\n\n  for (let [packageDirName, packageErrors] of Object.entries(errorsByPackageDirName)) {\n    lines.push(`📦 ${packageDirName}:`)\n    for (let error of packageErrors) {\n      lines.push(`   ${error.file}: ${error.error}`)\n    }\n    lines.push('')\n  }\n\n  let packageCount = Object.keys(errorsByPackageDirName).length\n  lines.push(\n    `Found ${errors.length} error${errors.length === 1 ? '' : 's'} in ${packageCount} package${packageCount === 1 ? '' : 's'}`,\n  )\n\n  return lines.join('\\n')\n}\n\n/**\n * Determines the highest severity bump type from an array of bump types.\n */\nfunction getHighestBump(bumps: BumpType[]): BumpType | null {\n  if (bumps.includes('major')) return 'major'\n  if (bumps.includes('minor')) return 'minor'\n  if (bumps.includes('patch')) return 'patch'\n  return null\n}\n\n/**\n * Checks if content starts with \"BREAKING CHANGE: \" (case-insensitive,\n * ignoring markdown formatting and leading whitespace)\n */\nfunction hasBreakingChangePrefix(content: string): boolean {\n  return content\n    .trimStart()\n    .replace(/^[*_]+/, '')\n    .toLowerCase()\n    .startsWith('breaking change: ')\n}\n\n/**\n * Formats a changelog entry from change file content\n */\nfunction formatChangelogEntry(content: string): string {\n  let lines = content.trim().split('\\n')\n\n  if (lines.length === 1) {\n    return `- ${lines[0]}`\n  }\n\n  // Multi-line: first line is bullet, rest are indented\n  let [firstLine, ...restLines] = lines\n  let formatted = [`- ${firstLine}`]\n\n  for (let line of restLines) {\n    // Add proper indentation for continuation lines\n    formatted.push(line ? `  ${line}` : '')\n  }\n\n  return formatted.join('\\n')\n}\n\nfunction sortChangelogChanges(changes: PackageRelease['changes']): PackageRelease['changes'] {\n  // Sort with breaking changes hoisted to top, then alphabetically by filename\n  return [...changes].sort((a, b) => {\n    let aBreaking = hasBreakingChangePrefix(a.content)\n    let bBreaking = hasBreakingChangePrefix(b.content)\n    if (aBreaking !== bBreaking) return aBreaking ? -1 : 1\n    return a.file.localeCompare(b.file)\n  })\n}\n\n/**\n * Generates a section for a specific bump type (e.g., \"### Major Changes\")\n */\nconst sectionTitles: Record<BumpType, string> = {\n  major: 'Major Changes',\n  minor: 'Minor Changes',\n  patch: 'Patch Changes',\n}\n\nfunction generateBumpTypeSection(\n  changes: PackageRelease['changes'],\n  bumpType: BumpType,\n  subheadingLevel: number,\n): string | null {\n  let filtered = changes.filter((c) => c.bump === bumpType)\n\n  if (filtered.length === 0) {\n    return null\n  }\n\n  let sorted = sortChangelogChanges(filtered)\n\n  let lines: string[] = []\n  let subheadingPrefix = '#'.repeat(subheadingLevel)\n\n  lines.push(`${subheadingPrefix} ${sectionTitles[bumpType]}`)\n  lines.push('')\n\n  for (let change of sorted) {\n    lines.push(formatChangelogEntry(change.content))\n    lines.push('')\n  }\n\n  return lines.join('\\n')\n}\n\nfunction generatePrereleaseChangesSection(\n  changes: PackageRelease['changes'],\n  dependencyBumps: DependencyBump[],\n  subheadingLevel: number,\n): string | null {\n  if (changes.length === 0 && dependencyBumps.length === 0) {\n    return null\n  }\n\n  let lines: string[] = []\n  let subheadingPrefix = '#'.repeat(subheadingLevel)\n  lines.push(`${subheadingPrefix} Pre-release Changes`)\n  lines.push('')\n\n  let sortedChanges = sortChangelogChanges(changes)\n  for (let change of sortedChanges) {\n    lines.push(formatChangelogEntry(change.content))\n    lines.push('')\n  }\n\n  if (dependencyBumps.length > 0) {\n    lines.push('- Bumped `@remix-run/*` dependencies:')\n    for (let dep of dependencyBumps) {\n      let tag = getGitTag(dep.packageName, dep.version)\n      lines.push(`  - [\\`${tag}\\`](${dep.releaseUrl})`)\n    }\n    lines.push('')\n  }\n\n  return lines.join('\\n')\n}\n\n/**\n * Generates the dependency bumps section for a changelog entry\n */\nfunction generateDependencyBumpsSection(\n  dependencyBumps: DependencyBump[],\n  subheadingLevel: number,\n): string | null {\n  if (dependencyBumps.length === 0) {\n    return null\n  }\n\n  let lines: string[] = []\n  let subheadingPrefix = '#'.repeat(subheadingLevel)\n\n  lines.push(`${subheadingPrefix} Patch Changes`)\n  lines.push('')\n  lines.push('- Bumped `@remix-run/*` dependencies:')\n\n  for (let dep of dependencyBumps) {\n    let tag = getGitTag(dep.packageName, dep.version)\n    lines.push(`  - [\\`${tag}\\`](${dep.releaseUrl})`)\n  }\n\n  lines.push('')\n\n  return lines.join('\\n')\n}\n\n/**\n * Generates changelog content for a package release\n */\nexport function generateChangelogContent(\n  release: PackageRelease,\n  options: {\n    /** Whether to include package name in heading. Default: false */\n    includePackageName?: boolean\n    /** Markdown heading level (2 = ##, 3 = ###). Default: 2 */\n    headingLevel?: 2 | 3\n  } = {},\n): string {\n  let { includePackageName = false, headingLevel = 2 } = options\n  let lines: string[] = []\n\n  let headingPrefix = '#'.repeat(headingLevel)\n  let packagePart = includePackageName ? `${release.packageName} ` : ''\n  lines.push(`${headingPrefix} ${packagePart}v${release.nextVersion}`)\n  lines.push('')\n\n  let subheadingLevel = headingLevel + 1\n\n  // In prerelease mode, all change-file entries are grouped into a single section.\n  let isPrereleaseRelease = getPrereleaseIdentifier(release.nextVersion) !== null\n  if (isPrereleaseRelease) {\n    let prereleaseSection = generatePrereleaseChangesSection(\n      release.changes,\n      release.dependencyBumps,\n      subheadingLevel,\n    )\n    if (prereleaseSection) {\n      lines.push(prereleaseSection)\n    }\n    return lines.join('\\n')\n  }\n\n  // Generate sections in order: major, minor, patch (skipping empty sections)\n  for (let bumpType of bumpTypes) {\n    let section = generateBumpTypeSection(release.changes, bumpType, subheadingLevel)\n    if (section) {\n      lines.push(section)\n    }\n  }\n\n  // Add dependency bumps section if there are any\n  // Only add if there are no other patch changes (to avoid duplicate \"Patch Changes\" heading)\n  if (release.dependencyBumps.length > 0) {\n    let hasPatchChanges = release.changes.some((c) => c.bump === 'patch')\n    if (hasPatchChanges) {\n      // Append to existing patch section (without heading)\n      lines.push('- Bumped `@remix-run/*` dependencies:')\n      for (let dep of release.dependencyBumps) {\n        let tag = getGitTag(dep.packageName, dep.version)\n        lines.push(`  - [\\`${tag}\\`](${dep.releaseUrl})`)\n      }\n      lines.push('')\n    } else {\n      // Create new patch section with heading\n      let section = generateDependencyBumpsSection(release.dependencyBumps, subheadingLevel)\n      if (section) {\n        lines.push(section)\n      }\n    }\n  }\n\n  return lines.join('\\n')\n}\n\n/**\n * Generates the commit message for all releases\n */\nexport function generateCommitMessage(releases: PackageRelease[]): string {\n  let subject = 'Release'\n  let body = releases\n    .map((r) => `- ${r.packageName}: ${r.currentVersion} -> ${r.nextVersion}`)\n    .join('\\n')\n\n  return `${subject}\\n\\n${body}`\n}\n\n// =============================================================================\n// CHANGELOG.md parsing utilities (for reading already-released changes)\n// =============================================================================\n\ninterface ChangelogEntry {\n  version: string\n  date?: Date\n  body: string\n}\n\ntype AllChangelogEntries = Record<string, ChangelogEntry>\n\n/**\n * Parses a package's CHANGELOG.md and returns all version entries\n */\nfunction parseChangelog(packageDirName: string): AllChangelogEntries | null {\n  let changelogPath = getPackageFile(packageDirName, 'CHANGELOG.md')\n\n  if (!fileExists(changelogPath)) {\n    return null\n  }\n\n  let changelog = readFile(changelogPath)\n  let parser = /^## ([a-z\\d\\.\\-]+)(?: \\(([^)]+)\\))?$/gim\n\n  let result: AllChangelogEntries = {}\n\n  let match\n  while ((match = parser.exec(changelog))) {\n    let [_, versionString, dateString] = match\n    let lastIndex = parser.lastIndex\n    let version = versionString.startsWith('v') ? versionString.slice(1) : versionString\n    let date = dateString ? new Date(dateString) : undefined\n    let nextMatch = parser.exec(changelog)\n    let body = changelog.slice(lastIndex, nextMatch ? nextMatch.index : undefined).trim()\n    result[version] = { version, date, body }\n    parser.lastIndex = lastIndex\n  }\n\n  return result\n}\n\n/**\n * Gets a specific version's entry from a package's CHANGELOG.md.\n * Accepts an npm package name (e.g., \"@remix-run/static-middleware\" or \"remix\").\n */\nexport function getChangelogEntry({\n  packageName,\n  version,\n}: {\n  packageName: string\n  version: string\n}): ChangelogEntry | null {\n  let dirName = packageNameToDirectoryName(packageName)\n  if (dirName === null) {\n    return null\n  }\n\n  let allEntries = parseChangelog(dirName)\n  if (allEntries !== null) {\n    return allEntries[version] ?? null\n  }\n\n  return null\n}\n"
  },
  {
    "path": "scripts/utils/color.ts",
    "content": "export let colors = {\n  // Regular colors\n  black: '\\x1b[30m',\n  red: '\\x1b[31m',\n  green: '\\x1b[32m',\n  yellow: '\\x1b[33m',\n  blue: '\\x1b[34m',\n  magenta: '\\x1b[35m',\n  cyan: '\\x1b[36m',\n  white: '\\x1b[37m',\n\n  // Bright colors\n  gray: '\\x1b[90m',\n  lightRed: '\\x1b[91m',\n  lightGreen: '\\x1b[92m',\n  lightYellow: '\\x1b[93m',\n  lightBlue: '\\x1b[94m',\n  lightMagenta: '\\x1b[95m',\n  lightCyan: '\\x1b[96m',\n  lightWhite: '\\x1b[97m',\n\n  // Styles\n  bold: '\\x1b[1m',\n  dim: '\\x1b[2m',\n  underline: '\\x1b[4m',\n\n  reset: '\\x1b[0m',\n}\n\nexport function colorize(text: string, color: string): string {\n  if (process.env.NO_COLOR) {\n    return text\n  }\n  return `${color}${text}${colors.reset}`\n}\n"
  },
  {
    "path": "scripts/utils/fs.ts",
    "content": "import * as fs from 'node:fs'\n\nexport function fileExists(filename: string): boolean {\n  return fs.existsSync(filename)\n}\n\nexport function readFile(filename: string, encoding: BufferEncoding = 'utf-8'): string {\n  try {\n    return fs.readFileSync(filename, encoding)\n  } catch (error) {\n    if (isFsError(error) && error.code === 'ENOENT') {\n      console.error(`Not found: \"${filename}\"`)\n      process.exit(1)\n    } else {\n      throw error\n    }\n  }\n}\n\nfunction isFsError(error: unknown): error is { code: string } {\n  return (\n    typeof error === 'object' && error != null && 'code' in error && typeof error.code === 'string'\n  )\n}\n\nexport function writeFile(filename: string, data: string): void {\n  fs.writeFileSync(filename, data)\n}\n\nexport function readJson(filename: string): any {\n  return JSON.parse(readFile(filename))\n}\n\nexport function writeJson(filename: string, data: any): void {\n  writeFile(filename, JSON.stringify(data, null, 2) + '\\n')\n}\n"
  },
  {
    "path": "scripts/utils/git.test.ts",
    "content": "import assert from 'node:assert/strict'\nimport * as cp from 'node:child_process'\nimport * as fs from 'node:fs'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\nimport { test } from 'node:test'\nimport { findVersionIntroductionCommit, getLocalTagTarget } from './git.ts'\n\nfunction execGit(args: string[], cwd: string): string {\n  return cp.execFileSync('git', args, { cwd, stdio: 'pipe', encoding: 'utf-8' }).trim()\n}\n\ntest('findVersionIntroductionCommit returns the commit where the target version was introduced', () => {\n  let tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'remix-git-test-'))\n  let originalCwd = process.cwd()\n  let packageJsonPath = 'packages/example/package.json'\n\n  try {\n    process.chdir(tempDir)\n    execGit(['init'], tempDir)\n    execGit(['config', 'user.name', 'Test Bot'], tempDir)\n    execGit(['config', 'user.email', 'test@example.com'], tempDir)\n\n    fs.mkdirSync(path.dirname(packageJsonPath), { recursive: true })\n    fs.writeFileSync(\n      packageJsonPath,\n      `${JSON.stringify({ name: '@remix-run/example', version: '1.0.0' }, null, 2)}\\n`,\n    )\n    execGit(['add', '.'], tempDir)\n    execGit(['commit', '-m', 'initial release'], tempDir)\n    let initialCommit = execGit(['rev-parse', 'HEAD'], tempDir)\n\n    fs.writeFileSync('README.md', 'hello\\n')\n    execGit(['add', 'README.md'], tempDir)\n    execGit(['commit', '-m', 'docs change'], tempDir)\n\n    fs.writeFileSync(\n      packageJsonPath,\n      `${JSON.stringify({ name: '@remix-run/example', version: '1.1.0' }, null, 2)}\\n`,\n    )\n    execGit(['add', packageJsonPath], tempDir)\n    execGit(['commit', '-m', 'bump version'], tempDir)\n    let versionBumpCommit = execGit(['rev-parse', 'HEAD'], tempDir)\n\n    fs.writeFileSync(\n      packageJsonPath,\n      `${JSON.stringify(\n        {\n          name: '@remix-run/example',\n          version: '1.1.0',\n          description: 'metadata only',\n        },\n        null,\n        2,\n      )}\\n`,\n    )\n    execGit(['add', packageJsonPath], tempDir)\n    execGit(['commit', '-m', 'metadata tweak'], tempDir)\n\n    assert.equal(findVersionIntroductionCommit(packageJsonPath, '1.1.0'), versionBumpCommit)\n    assert.equal(findVersionIntroductionCommit(packageJsonPath, '1.0.0'), initialCommit)\n    assert.equal(findVersionIntroductionCommit(packageJsonPath, '9.9.9'), null)\n\n    execGit(['tag', 'example@1.1.0', versionBumpCommit], tempDir)\n    assert.equal(getLocalTagTarget('example@1.1.0'), versionBumpCommit)\n  } finally {\n    process.chdir(originalCwd)\n    fs.rmSync(tempDir, { recursive: true, force: true })\n  }\n})\n"
  },
  {
    "path": "scripts/utils/git.ts",
    "content": "import * as cp from 'node:child_process'\n\nfunction execGit(args: string[]): string {\n  return cp.execFileSync('git', args, { stdio: 'pipe', encoding: 'utf-8' }).trim()\n}\n\nfunction parseVersionFromPackageJson(content: string): string | null {\n  try {\n    let parsed = JSON.parse(content) as Record<string, unknown>\n    return typeof parsed.version === 'string' ? parsed.version : null\n  } catch {\n    return null\n  }\n}\n\nfunction getPackageVersionAtRef(ref: string, packageJsonPath: string): string | null {\n  try {\n    let content = execGit(['show', `${ref}:${packageJsonPath}`])\n    return parseVersionFromPackageJson(content)\n  } catch {\n    return null\n  }\n}\n\n/**\n * Finds the commit that introduced a specific version in a package.json file.\n * Returns null when the version can't be found in the available git history.\n */\nexport function findVersionIntroductionCommit(\n  packageJsonPath: string,\n  version: string,\n): string | null {\n  let normalizedPath = packageJsonPath.replaceAll('\\\\', '/')\n\n  let output: string\n  try {\n    output = execGit(['log', '--format=%H', '--', normalizedPath])\n  } catch {\n    return null\n  }\n\n  if (output.length === 0) {\n    return null\n  }\n\n  let commits = output.split('\\n').filter((line) => line.length > 0)\n\n  for (let commit of commits) {\n    let commitVersion = getPackageVersionAtRef(commit, normalizedPath)\n    if (commitVersion !== version) {\n      continue\n    }\n\n    let parentLine = execGit(['rev-list', '--parents', '-n', '1', commit])\n    let [_commit, ...parents] = parentLine.split(' ').filter((line) => line.length > 0)\n\n    if (parents.length === 0) {\n      return commit\n    }\n\n    let introducedInCommit = false\n    for (let parent of parents) {\n      let parentVersion = getPackageVersionAtRef(parent, normalizedPath)\n      if (parentVersion !== version) {\n        introducedInCommit = true\n        break\n      }\n    }\n\n    if (introducedInCommit) {\n      return commit\n    }\n  }\n\n  return null\n}\n\n/**\n * Gets the local commit target for a tag.\n * Returns null when the tag does not exist locally.\n */\nexport function getLocalTagTarget(tag: string): string | null {\n  try {\n    return execGit(['rev-parse', '--verify', `refs/tags/${tag}^{commit}`])\n  } catch {\n    return null\n  }\n}\n\n/**\n * Gets the remote commit target for a tag from origin.\n * Returns null when the tag does not exist remotely.\n */\nexport function getRemoteTagTarget(tag: string): string | null {\n  try {\n    let output = execGit([\n      'ls-remote',\n      '--tags',\n      'origin',\n      `refs/tags/${tag}`,\n      `refs/tags/${tag}^{}`,\n    ])\n    let lines = output.split('\\n').filter((line) => line.length > 0)\n    if (lines.length === 0) {\n      return null\n    }\n\n    let peeledLine = lines.find((line) => line.endsWith(`refs/tags/${tag}^{}`))\n    if (peeledLine) {\n      return peeledLine.split('\\t')[0]\n    }\n\n    return lines[0].split('\\t')[0]\n  } catch {\n    return null\n  }\n}\n\n/**\n * Check if a git tag exists\n */\nexport function tagExists(tag: string): boolean {\n  return getLocalTagTarget(tag) !== null || getRemoteTagTarget(tag) !== null\n}\n"
  },
  {
    "path": "scripts/utils/github.ts",
    "content": "import { request } from '@octokit/request'\n\nimport { getChangelogEntry } from './changes.ts'\nimport { getGitTag, getPackageShortName } from './packages.ts'\n\nlet owner = 'remix-run'\nlet repo = 'remix'\n\nfunction getToken(): string {\n  let token = process.env.GITHUB_TOKEN\n  if (!token) {\n    throw new Error('GITHUB_TOKEN environment variable is required')\n  }\n  return token\n}\n\nfunction auth() {\n  return { headers: { authorization: `token ${getToken()}` } }\n}\n\nexport type CreateReleaseResult =\n  | { status: 'created'; url: string }\n  | { status: 'skipped'; reason: string }\n  | { status: 'error'; error: string }\n\n/**\n * Check if a GitHub release exists for a tag.\n */\nexport async function releaseExists(tag: string): Promise<boolean> {\n  try {\n    await request('GET /repos/{owner}/{repo}/releases/tags/{tag}', {\n      ...auth(),\n      owner,\n      repo,\n      tag,\n    })\n    return true\n  } catch (error) {\n    if (error instanceof Error && 'status' in error && error.status === 404) {\n      return false\n    }\n    throw error\n  }\n}\n\n/**\n * Creates a GitHub release for a package version.\n * Returns a result object indicating success, already exists, or error.\n */\nexport async function createRelease(\n  packageName: string,\n  version: string,\n  options: {\n    preview?: boolean\n  } = {},\n): Promise<CreateReleaseResult> {\n  let { preview = false } = options\n\n  let tagName = getGitTag(packageName, version)\n  let releaseName = `${getPackageShortName(packageName)} v${version}`\n  let changes = getChangelogEntry({ packageName, version })\n  let body = changes?.body ?? 'No changelog entry found for this version.'\n\n  if (preview) {\n    console.log(`  Tag:  ${tagName}`)\n    console.log(`  Name: ${releaseName}`)\n    console.log()\n    console.log('  Body:')\n    console.log()\n    for (let line of body.split('\\n')) {\n      console.log(`    ${line}`)\n    }\n    console.log()\n    return { status: 'skipped', reason: 'Preview mode' }\n  }\n\n  try {\n    if (await releaseExists(tagName)) {\n      return { status: 'skipped', reason: 'Already exists' }\n    }\n  } catch (error) {\n    let message = error instanceof Error ? error.message : String(error)\n    return { status: 'error', error: message }\n  }\n\n  try {\n    let response = await request('POST /repos/{owner}/{repo}/releases', {\n      ...auth(),\n      owner,\n      repo,\n      tag_name: tagName,\n      name: releaseName,\n      body,\n    })\n\n    return { status: 'created', url: response.data.html_url }\n  } catch (error) {\n    let message = error instanceof Error ? error.message : String(error)\n    return { status: 'error', error: message }\n  }\n}\n\n/**\n * Find an open PR from a specific branch to a base branch\n */\nexport async function findOpenPr(head: string, base: string) {\n  let response = await request('GET /repos/{owner}/{repo}/pulls', {\n    ...auth(),\n    owner,\n    repo,\n    state: 'open',\n    head: `${owner}:${head}`,\n    base,\n  })\n\n  return response.data.length > 0 ? response.data[0] : null\n}\n\n/**\n * Create a new PR\n */\nexport async function createPr(options: {\n  title: string\n  body: string\n  head: string\n  base: string\n}) {\n  let response = await request('POST /repos/{owner}/{repo}/pulls', {\n    ...auth(),\n    owner,\n    repo,\n    title: options.title,\n    body: options.body,\n    head: options.head,\n    base: options.base,\n  })\n\n  return response.data\n}\n\n/**\n * Update an existing PR\n */\nexport async function updatePr(prNumber: number, options: { title?: string; body?: string }) {\n  await request('PATCH /repos/{owner}/{repo}/pulls/{pull_number}', {\n    ...auth(),\n    owner,\n    repo,\n    pull_number: prNumber,\n    ...options,\n  })\n}\n\n/**\n * Close a PR with an optional comment\n */\nexport async function closePr(prNumber: number, comment?: string) {\n  if (comment) {\n    await request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {\n      ...auth(),\n      owner,\n      repo,\n      issue_number: prNumber,\n      body: comment,\n    })\n  }\n\n  await request('PATCH /repos/{owner}/{repo}/pulls/{pull_number}', {\n    ...auth(),\n    owner,\n    repo,\n    pull_number: prNumber,\n    state: 'closed',\n  })\n}\n\n/**\n * Get all comments on a PR\n */\nexport async function getPrComments(prNumber: number) {\n  let response = await request('GET /repos/{owner}/{repo}/issues/{issue_number}/comments', {\n    ...auth(),\n    owner,\n    repo,\n    issue_number: prNumber,\n  })\n\n  return response.data\n}\n\n/**\n * Create a comment on a PR\n */\nexport async function createPrComment(prNumber: number, body: string) {\n  let response = await request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {\n    ...auth(),\n    owner,\n    repo,\n    issue_number: prNumber,\n    body,\n  })\n\n  return response.data\n}\n\n/**\n * Update a comment on a PR\n */\nexport async function updatePrComment(commentId: number, body: string) {\n  await request('PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}', {\n    ...auth(),\n    owner,\n    repo,\n    comment_id: commentId,\n    body,\n  })\n}\n\n/**\n * Delete a comment on a PR\n */\nexport async function deletePrComment(commentId: number) {\n  await request('DELETE /repos/{owner}/{repo}/issues/comments/{comment_id}', {\n    ...auth(),\n    owner,\n    repo,\n    comment_id: commentId,\n  })\n}\n"
  },
  {
    "path": "scripts/utils/packages.ts",
    "content": "import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\n\nexport const packagesDir = path.relative(\n  process.cwd(),\n  path.resolve(__dirname, '..', '..', 'packages'),\n)\n\nexport const GITHUB_REPO_URL = 'https://github.com/remix-run/remix'\n\nexport function getAllPackageDirNames(): string[] {\n  return fs.readdirSync(packagesDir).filter((name) => {\n    let packagePath = getPackagePath(name)\n    return fs.existsSync(packagePath) && fs.statSync(packagePath).isDirectory()\n  })\n}\n\nexport function getPackagePath(packageDirName: string): string {\n  return path.resolve(packagesDir, packageDirName)\n}\n\nexport function getPackageFile(packageDirName: string, filename: string): string {\n  return path.join(getPackagePath(packageDirName), filename)\n}\n\n/**\n * Builds a mapping from npm package names to directory names by reading\n * all package.json files in the packages directory.\n */\nlet getNpmPackageNameToDirectoryMap = (() => {\n  let map: Map<string, string> | null = null\n\n  return function getNpmPackageNameToDirectoryMap(): Map<string, string> {\n    if (map !== null) {\n      return map\n    }\n\n    map = new Map()\n    let dirNames = getAllPackageDirNames()\n\n    for (let dirName of dirNames) {\n      let packageJsonPath = getPackageFile(dirName, 'package.json')\n      if (fs.existsSync(packageJsonPath)) {\n        try {\n          let packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))\n          if (typeof packageJson.name === 'string') {\n            map.set(packageJson.name, dirName)\n          }\n        } catch {\n          // Skip invalid package.json files\n        }\n      }\n    }\n\n    return map\n  }\n})()\n\n/**\n * Converts an npm package name to the directory name in the packages folder.\n * Returns null if no mapping is found.\n *\n * Examples:\n *   \"@remix-run/static-middleware\" -> \"static-middleware\"\n *   \"remix\" -> \"remix\"\n */\nexport function packageNameToDirectoryName(packageName: string): string | null {\n  return getNpmPackageNameToDirectoryMap().get(packageName) ?? null\n}\n\n/**\n * Returns the short name used in git tags for a package.\n * For @remix-run/* packages, strips the scope. For \"remix\", returns \"remix\".\n *\n * Examples:\n *   \"@remix-run/headers\" -> \"headers\"\n *   \"remix\" -> \"remix\"\n */\nexport function getPackageShortName(packageName: string): string {\n  if (packageName.startsWith('@remix-run/')) {\n    return packageName.slice('@remix-run/'.length)\n  }\n  return packageName\n}\n\n/**\n * Generates the git tag for a package release.\n *\n * Examples:\n *   (\"@remix-run/headers\", \"0.11.0\") -> \"headers@0.11.0\"\n *   (\"remix\", \"3.0.0\") -> \"remix@3.0.0\"\n */\nexport function getGitTag(packageName: string, version: string): string {\n  return `${getPackageShortName(packageName)}@${version}`\n}\n\n/**\n * Generates the GitHub release URL for a package release.\n */\nexport function getGitHubReleaseUrl(packageName: string, version: string): string {\n  let tag = getGitTag(packageName, version)\n  return `${GITHUB_REPO_URL}/releases/tag/${tag}`\n}\n\ninterface PackageInfo {\n  name: string\n  version: string\n  dirName: string\n  dependencies: string[] // Only @remix-run/* dependencies\n}\n\n/**\n * Gets information about all packages in the monorepo, including their\n * @remix-run/* dependencies.\n */\nlet getPackageInfoMap = (() => {\n  let map: Map<string, PackageInfo> | null = null\n\n  return function getPackageInfoMap(): Map<string, PackageInfo> {\n    if (map !== null) {\n      return map\n    }\n\n    map = new Map()\n    let dirNames = getAllPackageDirNames()\n\n    for (let dirName of dirNames) {\n      let packageJsonPath = getPackageFile(dirName, 'package.json')\n      if (fs.existsSync(packageJsonPath)) {\n        try {\n          let packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))\n          let name = packageJson.name as string\n          let version = packageJson.version as string\n\n          // Collect @remix-run/* dependencies from the dependencies field\n          let dependencies: string[] = []\n          let deps = packageJson.dependencies as Record<string, string> | undefined\n          if (deps) {\n            for (let depName of Object.keys(deps)) {\n              if (depName.startsWith('@remix-run/') || depName === 'remix') {\n                dependencies.push(depName)\n              }\n            }\n          }\n\n          map.set(name, { name, version, dirName, dependencies })\n        } catch {\n          // Skip invalid package.json files\n        }\n      }\n    }\n\n    return map\n  }\n})()\n\n/**\n * Gets the @remix-run/* dependencies for a package.\n */\nexport function getPackageDependencies(packageName: string): string[] {\n  let info = getPackageInfoMap().get(packageName)\n  return info?.dependencies ?? []\n}\n\n/**\n * Builds a reverse dependency graph: maps each package to the set of packages\n * that depend on it.\n */\nexport function buildReverseDependencyGraph(): Map<string, Set<string>> {\n  let graph = new Map<string, Set<string>>()\n  let packageInfoMap = getPackageInfoMap()\n\n  // Initialize empty sets for all packages\n  for (let packageName of packageInfoMap.keys()) {\n    graph.set(packageName, new Set())\n  }\n\n  // Build reverse edges\n  for (let [packageName, info] of packageInfoMap) {\n    for (let dep of info.dependencies) {\n      let dependents = graph.get(dep)\n      if (dependents) {\n        dependents.add(packageName)\n      }\n    }\n  }\n\n  return graph\n}\n\n/**\n * Gets all packages that transitively depend on any of the given packages.\n * Returns a map from dependent package name to the set of changed packages it depends on.\n */\nexport function getTransitiveDependents(changedPackages: Set<string>): Map<string, Set<string>> {\n  let reverseGraph = buildReverseDependencyGraph()\n  let result = new Map<string, Set<string>>()\n\n  // For each changed package, find all its transitive dependents\n  function addDependents(changedPackage: string, originalChangedPackage: string) {\n    let directDependents = reverseGraph.get(changedPackage)\n    if (!directDependents) return\n\n    for (let dependent of directDependents) {\n      // Skip if this is one of the originally changed packages\n      if (changedPackages.has(dependent)) continue\n\n      // Track which changed packages this dependent needs\n      let changedDeps = result.get(dependent)\n      if (!changedDeps) {\n        changedDeps = new Set()\n        result.set(dependent, changedDeps)\n      }\n      changedDeps.add(originalChangedPackage)\n\n      // Recursively process dependents\n      addDependents(dependent, originalChangedPackage)\n    }\n  }\n\n  for (let changedPackage of changedPackages) {\n    addDependents(changedPackage, changedPackage)\n  }\n\n  return result\n}\n"
  },
  {
    "path": "scripts/utils/process.ts",
    "content": "import * as cp from 'node:child_process'\nimport * as path from 'node:path'\n\n/**\n * Get the root directory of the monorepo (parent of scripts/).\n * Works whether called directly or via node --eval.\n */\nexport function getRootDir(): string {\n  // import.meta.dirname is the directory containing this file (scripts/utils/)\n  // Go up two levels to get the repo root\n  if (import.meta.dirname) {\n    return path.join(import.meta.dirname, '..', '..')\n  }\n  // Fallback for environments where import.meta.dirname isn't available\n  return process.cwd()\n}\n\nexport function logAndExec(command: string, captureOutput = false): string {\n  console.log(`$ ${command}`)\n  if (captureOutput) {\n    return cp.execSync(command, { stdio: 'pipe', encoding: 'utf-8' }).trim()\n  } else {\n    cp.execSync(command, { stdio: 'inherit' })\n    return ''\n  }\n}\n"
  },
  {
    "path": "scripts/utils/release-pr.test.ts",
    "content": "import assert from 'node:assert/strict'\nimport { test } from 'node:test'\nimport type { PackageRelease } from './changes.ts'\nimport { generatePrBody } from './release-pr.ts'\n\nfunction makeRelease({\n  packageDirName,\n  packageName,\n  nextVersion,\n}: {\n  packageDirName: string\n  packageName: string\n  nextVersion: string\n}): PackageRelease {\n  return {\n    packageDirName,\n    packageName,\n    currentVersion: '1.0.0',\n    nextVersion,\n    bump: 'patch',\n    changes: [{ file: 'patch.test-change.md', bump: 'patch', content: 'Test change' }],\n    dependencyBumps: [],\n  }\n}\n\ntest('generatePrBody puts remix first and sorts remaining packages alphabetically', () => {\n  let body = generatePrBody([\n    makeRelease({\n      packageDirName: 'zeta',\n      packageName: '@remix-run/zeta',\n      nextVersion: '1.0.1',\n    }),\n    makeRelease({\n      packageDirName: 'remix',\n      packageName: 'remix',\n      nextVersion: '3.0.0',\n    }),\n    makeRelease({\n      packageDirName: 'beta',\n      packageName: '@remix-run/beta',\n      nextVersion: '1.0.1',\n    }),\n    makeRelease({\n      packageDirName: 'alpha',\n      packageName: '@remix-run/alpha',\n      nextVersion: '1.0.1',\n    }),\n  ])\n\n  let remixTableIndex = body.indexOf('| remix |')\n  let alphaTableIndex = body.indexOf('| @remix-run/alpha |')\n  let betaTableIndex = body.indexOf('| @remix-run/beta |')\n  let zetaTableIndex = body.indexOf('| @remix-run/zeta |')\n\n  assert.notEqual(remixTableIndex, -1)\n  assert.notEqual(alphaTableIndex, -1)\n  assert.notEqual(betaTableIndex, -1)\n  assert.notEqual(zetaTableIndex, -1)\n  assert.ok(remixTableIndex < alphaTableIndex)\n  assert.ok(alphaTableIndex < betaTableIndex)\n  assert.ok(betaTableIndex < zetaTableIndex)\n\n  let remixChangelogIndex = body.indexOf('## remix v3.0.0')\n  let alphaChangelogIndex = body.indexOf('## @remix-run/alpha v1.0.1')\n  let betaChangelogIndex = body.indexOf('## @remix-run/beta v1.0.1')\n  let zetaChangelogIndex = body.indexOf('## @remix-run/zeta v1.0.1')\n\n  assert.notEqual(remixChangelogIndex, -1)\n  assert.notEqual(alphaChangelogIndex, -1)\n  assert.notEqual(betaChangelogIndex, -1)\n  assert.notEqual(zetaChangelogIndex, -1)\n  assert.ok(remixChangelogIndex < alphaChangelogIndex)\n  assert.ok(alphaChangelogIndex < betaChangelogIndex)\n  assert.ok(betaChangelogIndex < zetaChangelogIndex)\n})\n"
  },
  {
    "path": "scripts/utils/release-pr.ts",
    "content": "import type { PackageRelease } from './changes.ts'\nimport { generateChangelogContent } from './changes.ts'\n\n// GitHub has a 65,536 character limit for PR body. We use 60,000 to be safe.\nlet maxBodyLength = 60_000\n\n/**\n * Generates the PR body for a release PR\n */\nexport function generatePrBody(releases: PackageRelease[]): string {\n  let orderedReleases = sortReleasesForDisplay(releases)\n  let header = generateHeader()\n  let releasesTable = generateReleasesTable(orderedReleases)\n  let changelogs = generateChangelogs(orderedReleases)\n\n  let fullBody = [header, releasesTable, changelogs].join('\\n\\n')\n\n  // If under limit, return full body\n  if (fullBody.length <= maxBodyLength) {\n    return fullBody\n  }\n\n  // Truncate changelogs section to fit\n  let baseLength = header.length + releasesTable.length + 100 // buffer for truncation notice\n  let availableForChangelogs = maxBodyLength - baseLength\n  let truncatedChangelogs = truncateChangelogs(orderedReleases, availableForChangelogs)\n\n  return [header, releasesTable, truncatedChangelogs].join('\\n\\n')\n}\n\nfunction sortReleasesForDisplay(releases: PackageRelease[]): PackageRelease[] {\n  return [...releases].sort((a, b) => {\n    let aIsRemix = a.packageDirName === 'remix'\n    let bIsRemix = b.packageDirName === 'remix'\n\n    if (aIsRemix && !bIsRemix) {\n      return -1\n    }\n\n    if (!aIsRemix && bIsRemix) {\n      return 1\n    }\n\n    return a.packageName.localeCompare(b.packageName)\n  })\n}\n\nfunction generateHeader(): string {\n  return [\n    'This PR is managed by the [`release-pr`](https://github.com/remix-run/remix/blob/main/.github/workflows/release-pr.yaml) workflow. ' +\n      'Do not edit it manually. ' +\n      'See [CONTRIBUTING.md](https://github.com/remix-run/remix/blob/main/CONTRIBUTING.md#releases) for more.',\n  ].join('\\n')\n}\n\nfunction generateReleasesTable(releases: PackageRelease[]): string {\n  let lines = ['# Releases', '', '| Package | Version |', '|---------|---------|']\n\n  for (let release of releases) {\n    lines.push(\n      `| ${release.packageName} | \\`${release.currentVersion}\\` → \\`${release.nextVersion}\\` |`,\n    )\n  }\n\n  return lines.join('\\n')\n}\n\nfunction generateChangelogs(releases: PackageRelease[]): string {\n  let lines = ['# Changelogs']\n\n  for (let release of releases) {\n    lines.push('')\n    lines.push(generatePackageChangelog(release))\n  }\n\n  return lines.join('\\n')\n}\n\nfunction generatePackageChangelog(release: PackageRelease): string {\n  return generateChangelogContent(release, {\n    includePackageName: true,\n    headingLevel: 2,\n  })\n}\n\nfunction truncateChangelogs(releases: PackageRelease[], maxLength: number): string {\n  let lines = ['# Changelogs']\n  let currentLength = lines.join('\\n').length\n  let includedCount = 0\n\n  for (let release of releases) {\n    let changelog = '\\n\\n' + generatePackageChangelog(release)\n    if (currentLength + changelog.length <= maxLength) {\n      lines.push('')\n      lines.push(generatePackageChangelog(release))\n      currentLength += changelog.length\n      includedCount++\n    } else {\n      break\n    }\n  }\n\n  let omittedCount = releases.length - includedCount\n\n  if (omittedCount > 0) {\n    lines.push('')\n    lines.push(\n      `> ⚠️ ${omittedCount} changelog${omittedCount === 1 ? '' : 's'} omitted due to size limits. See the PR diff for full details.`,\n    )\n  }\n\n  return lines.join('\\n')\n}\n"
  },
  {
    "path": "scripts/utils/semver.ts",
    "content": "export type ReleaseType = 'major' | 'minor' | 'patch' | 'prerelease'\n\ntype ParsedVersion = {\n  major: number\n  minor: number\n  patch: number\n  prerelease: Array<string | number>\n}\n\nconst semverPattern =\n  /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/\n\nfunction parse(version: string): ParsedVersion | null {\n  let match = semverPattern.exec(version)\n  if (match == null) {\n    return null\n  }\n\n  let prerelease = match[4]\n    ? match[4]\n        .split('.')\n        .filter(Boolean)\n        .map((part) => (/^\\d+$/.test(part) ? Number(part) : part))\n    : []\n\n  return {\n    major: Number(match[1]),\n    minor: Number(match[2]),\n    patch: Number(match[3]),\n    prerelease,\n  }\n}\n\nfunction format(version: ParsedVersion): string {\n  let base = `${version.major}.${version.minor}.${version.patch}`\n\n  if (version.prerelease.length === 0) {\n    return base\n  }\n\n  return `${base}-${version.prerelease.join('.')}`\n}\n\nexport function major(version: string): number {\n  let parsed = parse(version)\n  if (parsed == null) {\n    throw new Error(`Invalid semver version: ${version}`)\n  }\n\n  return parsed.major\n}\n\nexport function prerelease(version: string): Array<string | number> | null {\n  let parsed = parse(version)\n  if (parsed == null) {\n    throw new Error(`Invalid semver version: ${version}`)\n  }\n\n  return parsed.prerelease.length > 0 ? parsed.prerelease : null\n}\n\nexport function inc(version: string, releaseType: ReleaseType, identifier?: string): string | null {\n  let parsed = parse(version)\n  if (parsed == null) {\n    return null\n  }\n\n  if (releaseType === 'major') {\n    return format({\n      major: parsed.major + 1,\n      minor: 0,\n      patch: 0,\n      prerelease: [],\n    })\n  }\n\n  if (releaseType === 'minor') {\n    return format({\n      major: parsed.major,\n      minor: parsed.minor + 1,\n      patch: 0,\n      prerelease: [],\n    })\n  }\n\n  if (releaseType === 'patch') {\n    return format({\n      major: parsed.major,\n      minor: parsed.minor,\n      patch: parsed.patch + 1,\n      prerelease: [],\n    })\n  }\n\n  if (identifier == null || identifier.trim() === '') {\n    return null\n  }\n\n  let nextPrerelease = [...parsed.prerelease]\n\n  if (nextPrerelease.length === 0 || nextPrerelease[0] !== identifier) {\n    nextPrerelease = [identifier, 0]\n  } else {\n    let lastIndex = nextPrerelease.length - 1\n    let lastPart = nextPrerelease[lastIndex]\n\n    if (typeof lastPart === 'number') {\n      nextPrerelease[lastIndex] = lastPart + 1\n    } else {\n      nextPrerelease.push(0)\n    }\n  }\n\n  return format({\n    major: parsed.major,\n    minor: parsed.minor,\n    patch: parsed.patch,\n    prerelease: nextPrerelease,\n  })\n}\n"
  }
]