diff --git a/.github/workflows/ci-rsc.yml b/.github/workflows/ci-rsc.yml new file mode 100644 index 00000000..46fc7ea5 --- /dev/null +++ b/.github/workflows/ci-rsc.yml @@ -0,0 +1,61 @@ +name: ci-rsc +on: + push: + branches: + - main + pull_request: + paths: + - "packages/plugin-rsc/**" + - "pnpm-lock.yaml" + - ".github/workflows/ci-rsc.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - run: pnpm i + - run: pnpm build + - run: pnpm -C packages/plugin-rsc tsc + - run: pnpm -C packages/plugin-rsc test + + test-e2e: + name: test-rsc (${{ matrix.os }} / ${{ matrix.browser }}) + runs-on: ${{ matrix.os }} + strategy: + # TODO: shard? + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + browser: [chromium] + include: + - os: ubuntu-latest + browser: firefox + - os: macos-latest + browser: webkit + fail-fast: false + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - run: pnpm i + - run: pnpm build + - run: pnpm -C packages/plugin-rsc exec playwright install ${{ matrix.browser }} + - run: pnpm -C packages/plugin-rsc test-e2e-ci --project=${{ matrix.browser }} + env: + TEST_ISOLATED: true + - uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-${{ matrix.os }}-${{ matrix.browser }} + path: | + packages/plugin-rsc/test-results diff --git a/README.md b/README.md index 96f4281a..a2589146 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ See [`@vitejs/plugin-react` documentation](packages/plugin-react/README.md) and [`@vitejs/plugin-react-swc` documentation](packages/plugin-react-swc/README.md) +# Vite Plugin RSC + +See [`@vitejs/plugin-rsc` documentation](packages/plugin-rsc/README.md) + ## Packages | Package | Version (click for changelogs) | @@ -22,6 +26,7 @@ See [`@vitejs/plugin-react` documentation](packages/plugin-react/README.md) and | [@vitejs/plugin-react](packages/plugin-react) | [![plugin-react version](https://img.shields.io/npm/v/@vitejs/plugin-react.svg?label=%20)](packages/plugin-react/CHANGELOG.md) | | [@vitejs/plugin-react-oxc](packages/plugin-react-oxc) | [![plugin-react-oxc version](https://img.shields.io/npm/v/@vitejs/plugin-react-oxc.svg?label=%20)](packages/plugin-react-oxc/CHANGELOG.md) | | [@vitejs/plugin-react-swc](packages/plugin-react-swc) | [![plugin-react-swc version](https://img.shields.io/npm/v/@vitejs/plugin-react-swc.svg?label=%20)](packages/plugin-react-swc/CHANGELOG.md) | +| [@vitejs/plugin-rsc](packages/plugin-rsc) | [![plugin-rsc version](https://img.shields.io/npm/v/@vitejs/plugin-rsc.svg?label=%20)](packages/plugin-rsc/CHANGELOG.md) | ## License diff --git a/eslint.config.js b/eslint.config.js index ae5f6db8..1b2ff21d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -9,7 +9,12 @@ import globals from 'globals' export default tseslint.config( { - ignores: ['**/dist/**', '**/playground-temp/**', '**/temp/**'], + ignores: [ + '**/dist/**', + '**/playground-temp/**', + '**/temp/**', + 'packages/plugin-rsc/**', + ], }, eslint.configs.recommended, ...tseslint.configs.recommended, diff --git a/package.json b/package.json index 95e06ace..e01ef4a3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@eslint/js": "^9.30.1", "@types/fs-extra": "^11.0.4", "@types/node": "^22.16.0", - "@vitejs/release-scripts": "^1.5.0", + "@vitejs/release-scripts": "^1.6.0", "eslint": "^9.30.1", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-n": "^17.20.0", @@ -72,6 +72,9 @@ ] }, "pnpm": { + "overrides": { + "@vitejs/plugin-rsc": "workspace:*" + }, "packageExtensions": { "generouted": { "peerDependencies": { diff --git a/packages/plugin-rsc/.gitignore b/packages/plugin-rsc/.gitignore new file mode 100644 index 00000000..416330c3 --- /dev/null +++ b/packages/plugin-rsc/.gitignore @@ -0,0 +1,13 @@ +node_modules +dist +.vercel +.vite-node +.wrangler +.netlify +*.log +*.tgz +test-results +*.tsbuildinfo +.debug +.vite-inspect +.claude diff --git a/packages/plugin-rsc/CHANGELOG.md b/packages/plugin-rsc/CHANGELOG.md new file mode 100644 index 00000000..88290a61 --- /dev/null +++ b/packages/plugin-rsc/CHANGELOG.md @@ -0,0 +1,172 @@ +# Changelog + +## v0.4.10 (2025-07-04) + +- feat: add `@vitejs/plugin-rsc` ([#521](https://github.com/vitejs/vite-plugin-react/pull/521)) + +--- + +Older versions were released as [`@hi-ogawa/vite-rsc`](https://github.com/hi-ogawa/vite-plugins/tree/main/packages/rsc). + +## v0.4.9 (2025-07-03) + +- feat: re-export plugin from base exports entry ([#1125](https://github.com/hi-ogawa/vite-plugins/pull/1125)) +- feat: re-export `transformHoistInlineDirective` ([#1122](https://github.com/hi-ogawa/vite-plugins/pull/1122)) +- fix: don't copy vite manifest from rsc to client ([#1118](https://github.com/hi-ogawa/vite-plugins/pull/1118)) + +## v0.4.8 (2025-07-01) + +- fix: copy all server assets to client by default and output `__vite_rsc_encryption_key` to fs directly ([#1102](https://github.com/hi-ogawa/vite-plugins/pull/1102)) +- fix: stable client build ([#1094](https://github.com/hi-ogawa/vite-plugins/pull/1094)) + +## v0.4.7 (2025-06-28) + +- feat: re-export `encodeReply` and `createTemporaryReferenceSet` from `react-server-dom/client` in `rsc` ([#1089](https://github.com/hi-ogawa/vite-plugins/pull/1089)) +- chore: add `use cache` example ([#1089](https://github.com/hi-ogawa/vite-plugins/pull/1089)) +- refactor: output code without indent ([#1087](https://github.com/hi-ogawa/vite-plugins/pull/1087)) + +## v0.4.6 (2025-06-27) + +- fix: correctly resolve server function created by 3rd party package during dev ([#1067](https://github.com/hi-ogawa/vite-plugins/pull/1067)) +- fix: correctly resolve client boundary created by server package during dev ([#1050](https://github.com/hi-ogawa/vite-plugins/pull/1050)) +- fix: copy only css assets from server build to client build by default ([#1072](https://github.com/hi-ogawa/vite-plugins/pull/1072)) +- fix: fix single quote string in `loadModule('ssr', 'index')` ([#1064](https://github.com/hi-ogawa/vite-plugins/pull/1064)) +- fix: stabilize server build by externalizing encryption key file ([#1069](https://github.com/hi-ogawa/vite-plugins/pull/1069)) +- fix: check build instead of `import.meta.env.DEV` ([#1083](https://github.com/hi-ogawa/vite-plugins/pull/1083)) +- perf: strip code during scan build ([#1066](https://github.com/hi-ogawa/vite-plugins/pull/1066)) +- feat: support preserving client reference original value ([#1078](https://github.com/hi-ogawa/vite-plugins/pull/1078)) +- feat: add `enableActionEncryption` option for debugging purpose ([#1084](https://github.com/hi-ogawa/vite-plugins/pull/1084)) +- feat: add `ignoredClientInServerPackageWarning` option ([#1065](https://github.com/hi-ogawa/vite-plugins/pull/1065)) + +## v0.4.5 (2025-06-22) + +- feat: rsc css transform for default export identifier ([#1046](https://github.com/hi-ogawa/vite-plugins/pull/1046)) +- feat: add `import.meta.viteRsc.loadBootstrapScriptContent` ([#1042](https://github.com/hi-ogawa/vite-plugins/pull/1042)) +- fix: only include jsx/tsx for rsc css export transform ([#1034](https://github.com/hi-ogawa/vite-plugins/pull/1034)) +- fix: ensure server-only and client-only not externalized ([#1045](https://github.com/hi-ogawa/vite-plugins/pull/1045)) +- fix: use static import for `loadCss` virtuals during build ([#1043](https://github.com/hi-ogawa/vite-plugins/pull/1043)) + +## v0.4.4 (2025-06-20) + +- feat: automatic rsc css export transform ([#1030](https://github.com/hi-ogawa/vite-plugins/pull/1030)) +- feat: add plugin to workaround cloudflare error ([#1014](https://github.com/hi-ogawa/vite-plugins/pull/1014)) +- feat: add load module dev proxy ([#1012](https://github.com/hi-ogawa/vite-plugins/pull/1012)) +- feat: add `serverHandler` option to allow using ssr environment as main handler ([#1008](https://github.com/hi-ogawa/vite-plugins/pull/1008)) +- feat: support `loadModule(environment, entry)` ([#1007](https://github.com/hi-ogawa/vite-plugins/pull/1007)) +- refactor: tweak renderHtml types and naming ([#1029](https://github.com/hi-ogawa/vite-plugins/pull/1029)) + +## v0.4.3 (2025-06-18) + +- feat: add rsc css export transform helper ([#1002](https://github.com/hi-ogawa/vite-plugins/pull/1002)) +- feat: support `loadCss(importer)` ([#1001](https://github.com/hi-ogawa/vite-plugins/pull/1001)) + +## v0.4.2 (2025-06-17) + +- fix: allow custom `outDir` + chore: cloudflare single worker setup ([#990](https://github.com/hi-ogawa/vite-plugins/pull/990)) +- fix: transform `__webpack_require__` global ([#980](https://github.com/hi-ogawa/vite-plugins/pull/980)) +- fix: inline and optimize react deps in ssr environment ([#982](https://github.com/hi-ogawa/vite-plugins/pull/982)) +- refactor: resolve self runtime import instead of `dedupe` ([#975](https://github.com/hi-ogawa/vite-plugins/pull/975)) +- refactor: emit assets manifest during `writeBundle` ([#972](https://github.com/hi-ogawa/vite-plugins/pull/972)) +- refactor: use `../` instead of `./../` path in output ([#963](https://github.com/hi-ogawa/vite-plugins/pull/963)) + +## v0.4.1 (2025-06-15) + +- fix: re-publish to fix vendored dependency + +## v0.4.0 (2025-06-15) + +- refactor!: rework multi environment API (bootstrap script) ([#958](https://github.com/hi-ogawa/vite-plugins/pull/958)) +- refactor!: rework multi environment API (ssr module) ([#957](https://github.com/hi-ogawa/vite-plugins/pull/957)) +- refactor!: simplify plugin options in favor of `rollupOptions.input` ([#956](https://github.com/hi-ogawa/vite-plugins/pull/956)) +- feat: expose `rsc-html-stream` utils ([#950](https://github.com/hi-ogawa/vite-plugins/pull/950)) +- fix: fix missing rsc css on build ([#949](https://github.com/hi-ogawa/vite-plugins/pull/949)) + +## v0.3.4 (2025-06-12) + +- fix: fix internal import to allow stable react vendor chunk ([#824](https://github.com/hi-ogawa/vite-plugins/pull/824)) +- fix: compat for old react plugin ([#939](https://github.com/hi-ogawa/vite-plugins/pull/939)) + +## v0.3.3 (2025-06-12) + +- feat: support rolldown-vite ([#931](https://github.com/hi-ogawa/vite-plugins/pull/931)) +- fix: allow usage without react plugin ([#934](https://github.com/hi-ogawa/vite-plugins/pull/934)) +- chore: docs ([#921](https://github.com/hi-ogawa/vite-plugins/pull/921)) + +## v0.3.2 (2025-06-10) + +- feat: auto initialize ([#925](https://github.com/hi-ogawa/vite-plugins/pull/925)) +- fix: emit assets manifest only in server build ([#929](https://github.com/hi-ogawa/vite-plugins/pull/929)) +- refactor: inline react-server-dom in ssr (2) ([#927](https://github.com/hi-ogawa/vite-plugins/pull/927)) +- chore: add `@cloudflare/vite-plugin` example ([#926](https://github.com/hi-ogawa/vite-plugins/pull/926)) + +## v0.3.1 (2025-06-06) + +- refactor: vendor react-server-dom ([#854](https://github.com/hi-ogawa/vite-plugins/pull/854)) + +## v0.3.0 (2025-06-05) + +- feat!: rsc css code split ([#876](https://github.com/hi-ogawa/vite-plugins/pull/876)) +- feat: encrypt closure bind values ([#897](https://github.com/hi-ogawa/vite-plugins/pull/897)) +- fix: client element as bound arg encryption ([#905](https://github.com/hi-ogawa/vite-plugins/pull/905)) +- fix: throw on client reference call on server ([#900](https://github.com/hi-ogawa/vite-plugins/pull/900)) + +## v0.2.4 (2025-05-26) + +- fix: fix stale css import in non-boundary client module ([#887](https://github.com/hi-ogawa/vite-plugins/pull/887)) +- fix: fix non-client-boundary client module hmr in tailwind example ([#886](https://github.com/hi-ogawa/vite-plugins/pull/886)) + +## v0.2.3 (2025-05-22) + +- fix: support Windows ([#884](https://github.com/hi-ogawa/vite-plugins/pull/884)) +- fix: remove stale ssr styles during dev ([#879](https://github.com/hi-ogawa/vite-plugins/pull/879)) +- fix: add `vary` header to avoid rsc payload on tab re-open ([#877](https://github.com/hi-ogawa/vite-plugins/pull/877)) + +## v0.2.2 (2025-05-18) + +- fix: emit server assets and copy to client ([#861](https://github.com/hi-ogawa/vite-plugins/pull/861)) +- fix: css modules hmr ([#860](https://github.com/hi-ogawa/vite-plugins/pull/860)) +- fix: fix `collectCssByUrl` error ([#856](https://github.com/hi-ogawa/vite-plugins/pull/856)) +- fix: show invalid transform error with code frame ([#871](https://github.com/hi-ogawa/vite-plugins/pull/871)) +- perf: preload client reference deps before non-cached import ([#850](https://github.com/hi-ogawa/vite-plugins/pull/850)) + +## v0.2.1 (2025-05-13) + +- feat: automatic client package heuristics ([#830](https://github.com/hi-ogawa/vite-plugins/pull/830)) +- fix: add browser entry to `optimizeDeps.entries` ([#846](https://github.com/hi-ogawa/vite-plugins/pull/846)) +- fix: resolve self package from project root ([#845](https://github.com/hi-ogawa/vite-plugins/pull/845)) +- refactor: use `rsc-html-stream` ([#843](https://github.com/hi-ogawa/vite-plugins/pull/843)) + +## v0.2.0 (2025-05-12) + +- feat: apply tree-shaking to all client references (2nd approach) ([#838](https://github.com/hi-ogawa/vite-plugins/pull/838)) +- feat: support nonce ([#813](https://github.com/hi-ogawa/vite-plugins/pull/813)) +- feat: support css in rsc environment ([#825](https://github.com/hi-ogawa/vite-plugins/pull/825)) +- feat: support css in client references ([#823](https://github.com/hi-ogawa/vite-plugins/pull/823)) +- fix: handle html escape and binary data in ssr rsc payload ([#839](https://github.com/hi-ogawa/vite-plugins/pull/839)) +- fix: wrap virtual to workaround module runner entry issues ([#832](https://github.com/hi-ogawa/vite-plugins/pull/832)) +- fix: scan build in two environments ([#820](https://github.com/hi-ogawa/vite-plugins/pull/820)) +- refactor: simplify client reference mapping ([#836](https://github.com/hi-ogawa/vite-plugins/pull/836)) +- refactor!: remove `entries.css` ([#831](https://github.com/hi-ogawa/vite-plugins/pull/831)) +- refactor: client reference ssr preinit/preload via proxy and remove `prepareDestination` ([#828](https://github.com/hi-ogawa/vite-plugins/pull/828)) +- refactor: tweak asset links api ([#826](https://github.com/hi-ogawa/vite-plugins/pull/826)) + +## v0.1.1 (2025-05-07) + +- fix: statically import client references virtual ([#815](https://github.com/hi-ogawa/vite-plugins/pull/815)) +- fix: fix base for findSourceMapURL ([#812](https://github.com/hi-ogawa/vite-plugins/pull/812)) +- fix: fix module runner line offset in `findSourceMapURL` ([#810](https://github.com/hi-ogawa/vite-plugins/pull/810)) + +## v0.1.0 (2025-05-01) + +- feat: support `findSourceMapURL` for `createServerReference` ([#796](https://github.com/hi-ogawa/vite-plugins/pull/796)) +- feat: support `findSourceMapURL` for component stack and replay logs ([#779](https://github.com/hi-ogawa/vite-plugins/pull/779)) +- feat: support temporary references ([#776](https://github.com/hi-ogawa/vite-plugins/pull/776)) +- feat: support custom base ([#775](https://github.com/hi-ogawa/vite-plugins/pull/775)) +- feat: refactor assets manifest and expose it to rsc build ([#767](https://github.com/hi-ogawa/vite-plugins/pull/767)) +- feat: ssr modulepreload only for build ([#763](https://github.com/hi-ogawa/vite-plugins/pull/763)) +- feat: tree shake unused reference exports ([#761](https://github.com/hi-ogawa/vite-plugins/pull/761)) +- feat: re-export react-server-dom ([#744](https://github.com/hi-ogawa/vite-plugins/pull/744)) +- feat: support css entry ([#737](https://github.com/hi-ogawa/vite-plugins/pull/737)) +- feat wrap client packages in virtual (support `clientPackages` options) ([#718](https://github.com/hi-ogawa/vite-plugins/pull/718)) +- feat: modulepreload client reference on ssr ([#703](https://github.com/hi-ogawa/vite-plugins/pull/703)) +- feat: create vite-rsc ([#692](https://github.com/hi-ogawa/vite-plugins/pull/692)) diff --git a/packages/plugin-rsc/README.md b/packages/plugin-rsc/README.md new file mode 100644 index 00000000..cca3a4d0 --- /dev/null +++ b/packages/plugin-rsc/README.md @@ -0,0 +1,447 @@ +# @vitejs/plugin-rsc + +This package provides [React Server Components](https://react.dev/reference/rsc/server-components) (RSC) support for Vite. + +## Features + +- **Framework-less RSC experience**: The plugin implements [RSC conventions](https://react.dev/reference/rsc/server-components) and provides low level `react-server-dom` runtime API without framework-specific abstractions. +- **CSS support**: CSS is automatically code-split both at client and server components and they are injected upon rendering. +- **HMR support**: Enables editing both client and server components without full page reloads. +- **Runtime agnostic**: Built on [Vite environment API](https://vite.dev/guide/api-environment.html) and works with other runtimes (e.g., [`@cloudflare/vite-plugin`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare)). + +## Getting Started + +You can start a project by copying an example locally by: + +```sh +npx degit vitejs/vite-plugin-react/packages/plugin-rsc/examples/starter my-app +``` + +## Examples + +- [`./examples/starter`](./examples/starter) + - This example provides an in-depth overview of API with inline comments to explain how they function within RSC-powered React application. +- [`./examples/react-router`](./examples/react-router) + - This demonstrates how to integrate [experimental React Router RSC API](https://remix.run/blog/rsc-preview) with this plugin. + It also includes `@cloudflare/vite-plugin` integration. +- [`./examples/basic`](./examples/basic) + - This is mainly used for e2e testing and include various advanced RSC usages (e.g. `"use cache"` example). + It also uses a high level `@vitejs/plugin-rsc/extra/{rsc,ssr,browser}` API for quick setup. +- [`./examples/ssg`](./examples/ssg) + - Static site generation (SSG) example with MDX and client components for interactivity. + +## Basic Concepts + +This example is a simplified version of [`./examples/starter`](./examples/starter). You can read [`./examples/starter/src/framework/entry.{rsc,ssr,browser}.tsx`](./examples/starter/src/framework) for more in-depth commentary, which includes server function handling and client-side RSC re-fetching/re-rendering. + +This is the diagram to show the basic flow of RSC rendering process. See also https://github.com/hi-ogawa/vite-plugins/discussions/606. + +```mermaid +graph TD + + subgraph "rsc environment" + A["React virtual dom tree"] --> |"[@vitejs/plugin-rsc/rsc]
renderToReadableStream"| B1["RSC Stream"]; + end + + B1 --> B2 + B1 --> B3 + + subgraph "ssr environment" + B2["RSC Stream"] --> |"[@vitejs/plugin-rsc/ssr]
createFromReadableStream"| C1["React virtual dom tree"]; + C1 --> |"[react-dom/server]
SSR"| E["HTML String/Stream"]; + end + + subgraph "client environment" + B3["RSC Stream"] --> |"[@vitejs/plugin-rsc/browser]
createFromReadableStream"| C2["React virtual dom tree"]; + C2 --> |"[react-dom/client]
CSR: mount, hydration"| D["DOM Elements"]; + end + + style A fill:#D6EAF8,stroke:#333,stroke-width:2px + style B1 fill:#FEF9E7,stroke:#333,stroke-width:2px + style B2 fill:#FEF9E7,stroke:#333,stroke-width:2px + style B3 fill:#FEF9E7,stroke:#333,stroke-width:2px + style C1 fill:#D6EAF8,stroke:#333,stroke-width:2px + style C2 fill:#D6EAF8,stroke:#333,stroke-width:2px + style D fill:#D5F5E3,stroke:#333,stroke-width:2px + style E fill:#FADBD8,stroke:#333,stroke-width:2px +``` + +- [`vite.config.ts`](./examples/starter/vite.config.ts) + +```js +import rsc from '@vitejs/plugin-rsc' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + // add plugin + rsc(), + ], + + // specify entry point for each environment. + environments: { + // `rsc` environment loads modules with `react-server` condition. + // this environment is responsible for: + // - RSC stream serialization (React VDOM -> RSC stream) + // - server functions handling + rsc: { + build: { + rollupOptions: { + input: { + index: './src/framework/entry.rsc.tsx', + }, + }, + }, + }, + + // `ssr` environment loads modules without `react-server` condition. + // this environment is responsible for: + // - RSC stream deserialization (RSC stream -> React VDOM) + // - traditional SSR (React VDOM -> HTML string/stream) + // (NOTE: as it can be seen in the above diagram. SSR is technically an optional mechanism.) + ssr: { + build: { + rollupOptions: { + input: { + index: './src/framework/entry.ssr.tsx', + }, + }, + }, + }, + + // client environment is used for hydration and client-side rendering + // this environment is responsible for: + // - RSC stream deserialization (RSC stream -> React VDOM) + // - traditional CSR (React VDOM -> Browser DOM tree mount/hydration) + // - refetch and re-render RSC + // - calling server functions + client: { + build: { + rollupOptions: { + input: { + index: './src/framework/entry.browser.tsx', + }, + }, + }, + }, + }, +}) +``` + +- [`entry.rsc.tsx`](./examples/starter/src/framework/entry.rsc.tsx) + +```tsx +import * as ReactServer from '@vitejs/plugin-rsc/rsc' // re-export of react-server-dom/server.edge + +// the plugin assumes `rsc` entry having default export of request handler +export default async function handler(request: Request): Promise { + // serialization React VDOM to RSC stream + const root = ( + + +

Test

+ + + ) + const rscStream = ReactServer.renderToReadableStream(root) + + // respond direct RSC stream request based on framework's convention + if (request.url.endsWith('.rsc')) { + return new Response(rscStream, { + headers: { + 'Content-type': 'text/x-component;charset=utf-8', + }, + }) + } + + // delegate to SSR environment for html rendering + // `loadModule` is a helper API provided by the plugin for multi environment interaction. + const ssrEntry = await import.meta.viteRsc.loadModule< + typeof import('./entry.ssr.tsx') + >('ssr', 'index') + const htmlStream = await ssrEntry.handleSsr(rscStream) + + // respond html + return new Response(htmlStream, { + headers: { + 'Content-type': 'text/html', + }, + }) +} +``` + +- [`entry.ssr.tsx`](./examples/starter/src/framework/entry.ssr.tsx) + +```tsx +import * as ReactClient from '@vitejs/plugin-rsc/ssr' // re-export of react-server-dom/client.edge +import * as ReactDOMServer from 'react-dom/server.edge' + +export async function handleSsr(rscStream: ReadableStream) { + // deserialize RSC stream back to React VDOM + const root = await ReactClient.createFromReadableStream(rscStream) + + // helper API to allow referencing browser entry content from SSR environment + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index') + + // render html (traditional SSR) + const htmlStream = ReactDOMServer.renderToReadableStream(root, { + bootstrapScriptContent, + }) + + return htmlStream +} +``` + +- [`entry.browser.tsx`](./examples/starter/src/framework/entry.browser.tsx) + +```tsx +import * as ReactClient from "@vitejs/plugin-rsc/browser"; // re-export of react-server-dom/client.browser +import * as ReactDOMClient from "react-dom/client"; + +async function main() { + // fetch and deserialize RSC stream back to React VDOM + const rscResponse = await fetch(window.location.href + ".rsc); + const root = await ReactClient.createFromReadableStream(rscResponse.body); + + // hydration (traditional CSR) + ReactDOMClient.hydrateRoot(document, root); +} + +main(); +``` + +## `react-server-dom` API + +### `@vitejs/plugin-rsc/rsc` + +This module re-exports RSC runtime API provided by `react-server-dom/server.edge` + +- `renderToReadableStream`: RSC serialization (React VDOM -> RSC stream) +- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM). This is also available on rsc environment itself. For example, it allows saving serailized RSC and deserializing it for later use. +- `decodeAction/decodeReply/loadServerAction`: server function related... + +### `@vitejs/plugin-rsc/ssr` + +This module re-exports RSC runtime API provided by `react-server-dom/client.edge` + +- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM) + +### `@vitejs/plugin-rsc/browser` + +This module re-exports RSC runtime API provided by `react-server-dom/client.browser` + +- `createFromReadableStream`: RSC deserialization (RSC stream -> React VDOM) +- `createFromFetch`: a robust way of `createFromReadableStream((await fetch("...")).body)` +- `encodeReply/setServerCallback`: server function related... + +## Environment helper API + +The plugin provides an additional helper for multi environment interaction. + +### available on `rsc` or `ssr` environment + +#### `import.meta.viteRsc.loadModule` + +- Type: `(environmentName: "ssr" | "rsc", entryName: string) => Promise` + +This allows importing `ssr` environment module specified by `environments.ssr.build.rollupOptions.input[entryName]` inside `rsc` environment and vice versa. + +During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process. When enabling `rsc({ loadModuleDevProxy: true })` plugin option, the loaded module is implemented as a proxy with `fetch`-based RPC to call in node environment on the main Vite process, which for example, allows `rsc` environment inside cloudflare workers to access `ssr` environment on the main Vite process. + +During production build, this API will be rewritten into a static import of the specified entry of other environment build and the modules are executed inside the same runtime. + +For example, + +```js +// ./entry.rsc.tsx +const ssrModule = await import.meta.viteRsc.loadModule("ssr", "index"); +ssrModule.renderHTML(...); + +// ./entry.ssr.tsx (with environments.ssr.build.rollupOptions.input.index = "./entry.ssr.tsx") +export function renderHTML(...) {} +``` + +### available on `rsc` environment + +#### `import.meta.viteRsc.loadCss` + +- Type: `(importer?: string) => React.ReactNode` + +This allows collecting css which is imported through a current server module and injecting them inside server components. + +```tsx +import './test.css' +import dep from './dep.tsx' + +export function ServerPage() { + // this will include css assets for "test.css" + // and any css transitively imported through "dep.tsx" + return ( + <> + {import.meta.viteRsc.loadCss()} + ... + + ) +} +``` + +Where specifying `loadCss()`, it will collect css through the server module resolved by ``. + +```tsx +// virtual:my-framework-helper +export function Assets() { + return <> + {import.meta.viteRsc.loadCss("/routes/home.tsx")} + {import.meta.viteRsc.loadCss("/routes/about.tsx")} + {...} + +} + +// user-app.tsx +import { Assets } from "virtual:my-framework-helper"; + +export function UserApp() { + return + + + + ... + +} +``` + +#### `?vite-rsc-css-export=` + +This special query convention provides automatic injection of `import.meta.viteRsc.loadCss`. + +For example, + +```tsx +// my-route.tsx +export function Page(props) { + return
...
+} + +// my-route.css?vite-rsc-css-export=Page +function Page(props) { + return
...
+} + +function __Page(props) { + return ( + <> + {import.meta.viteRsc.loadCss()} + + + ) +} + +export { __Page as Page } +``` + +### available on `ssr` environment + +#### `import.meta.viteRsc.loadBootstrapScriptContent("index")` + +This provides a raw js code to execute a browser entry file specified by `environments.client.build.rollupOptions.input.index`. This is intended to be used with React DOM SSR API, such as [`renderToReadableStream`](https://react.dev/reference/react-dom/server/renderToReadableStream) + +```js +import bootstrapScriptContent from 'virtual:vite-rsc/bootstrap-script-content' +import { renderToReadableStream } from 'react-dom/server.edge' + +const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index') +const htmlStream = await renderToReadableStream(reactNode, { + bootstrapScriptContent, +}) +``` + +### available on `client` environment + +#### `rsc:update` event + +This event is fired when server modules are updated, which can be used to trigger re-fetching and re-rendering of RSC components on browser. + +```js +import * as ReactClient from '@vitejs/plugin-rsc/browser' + +import.meta.hot.on('rsc:update', async () => { + // re-fetch RSC stream + const rscPayload = await ReactClient.createFromFetch( + fetch(window.location.href + '.rsc'), + ) + // re-render ... +}) +``` + +## Plugin API + +### `@vitejs/plugin-rsc` + +```js +import rsc from '@vitejs/plugin-rsc' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + rsc({ + // this is only a shorthand of specifying each rollup input via + // `environments[name].build.rollupOptions.input.index` + entries: { + rsc: '...', + ssr: '...', + client: '...', + }, + + // by default, the plugin sets up middleware + // using `default` export of `rsc` environment `index` entry. + // this behavior can be customized by `serverHandler` option. + serverHandler: false, + + // when `loadModuleDevProxy: true`, `import.meta.viteRsc.loadModule` is implemented + // through `fetch` based RPC, which allows, for example, rsc environment inside + // cloudflare workers to communicate with node ssr environment on main Vite process. + loadModuleDevProxy: true, + + // by default, `loadCss()` helper is injected based on certain heuristics. + // if it breaks, it can be opt-out or selectively applied based on files. + rscCssTransform: { filter: (id) => id.includes('/my-app/') }, + + // by default, the plugin uses a build-time generated encryption key for + // "use server" closure argument binding. + // This can be overwritten by configuring `defineEncryptionKey` option, + // for example, to obtain a key through environment variable during runtime. + // cf. https://nextjs.org/docs/app/guides/data-security#overwriting-encryption-keys-advanced + defineEncryptionKey: 'process.env.MY_ENCRYPTION_KEY', + }), + ], +}) +``` + +## Higher level API + +This is a wrapper of `react-server-dom` API and helper API to setup a minimal RSC app without writing own framework code like [`./examples/starter/src/framework`](./examples/starter/src/framework/). See [`./examples/basic`](./examples/basic/) for how this API is used. + +### `@vitejs/plugin-rsc/extra/rsc` + +- `renderRequest` + +### `@vitejs/plugin-rsc/extra/ssr` + +- `renderHtml` + +### `@vitejs/plugin-rsc/extra/browser` + +- `hydrate` + +## Credits + +This project builds on fundamental techniques and insights from pioneering Vite RSC implementations. +Additionally, Parcel and React Router's work on standardizing the RSC bundler/app responsibility has guided this plugin's API design: + +- [Waku](https://github.com/wakujs/waku) +- [@lazarv/react-server](https://github.com/lazarv/react-server) +- [@jacob-ebey/vite-react-server-dom](https://github.com/jacob-ebey/vite-plugins/tree/main/packages/vite-react-server-dom) +- [React Router RSC](https://remix.run/blog/rsc-preview) +- [Parcel RSC](https://parceljs.org/recipes/rsc) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts new file mode 100644 index 00000000..9b98ee9f --- /dev/null +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -0,0 +1,931 @@ +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' +import { type Page, expect, test } from '@playwright/test' +import { type Fixture, setupIsolatedFixture, useFixture } from './fixture' +import { expectNoReload, testNoJs, waitForHydration } from './helper' + +// TODO: parallel? + +test.describe('dev-default', () => { + const f = useFixture({ root: 'examples/basic', mode: 'dev' }) + defineTest(f) +}) + +test.describe('build-default', () => { + const f = useFixture({ root: 'examples/basic', mode: 'build' }) + defineTest(f) +}) + +test.describe('dev-base', () => { + const f = useFixture({ + root: 'examples/basic', + mode: 'dev', + cliOptions: { + env: { + TEST_BASE: 'true', + }, + }, + }) + defineTest(f) +}) + +test.describe('build-base', () => { + const f = useFixture({ + root: 'examples/basic', + mode: 'build', + cliOptions: { + env: { + TEST_BASE: 'true', + }, + }, + }) + defineTest(f) +}) + +test.describe(() => { + // disabled by default + if (!process.env.TEST_ISOLATED) return + + let tmpRoot = '/tmp/test-vite-rsc' + test.beforeAll(async () => { + await setupIsolatedFixture({ src: 'examples/basic', dest: tmpRoot }) + }) + + test.describe('dev-isolated', () => { + const f = useFixture({ root: tmpRoot, mode: 'dev' }) + defineTest(f) + }) + + test.describe('build-isolated', () => { + const f = useFixture({ root: tmpRoot, mode: 'build' }) + defineTest(f) + }) +}) + +function defineTest(f: Fixture) { + test('basic', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + }) + + test('client component', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await page.getByRole('button', { name: 'client-counter: 0' }).click() + await page.getByRole('button', { name: 'client-counter: 1' }).click() + }) + + test('server action @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await testAction(page) + }) + + testNoJs('server action @nojs', async ({ page }) => { + await page.goto(f.url()) + await testAction(page) + }) + + async function testAction(page: Page) { + await page.getByRole('button', { name: 'server-counter: 0' }).click() + await page.getByRole('button', { name: 'server-counter: 1' }).click() + await expect( + page.getByRole('button', { name: 'server-counter: 2' }), + ).toBeVisible() + await page.getByRole('button', { name: 'server-counter-reset' }).click() + await expect( + page.getByRole('button', { name: 'server-counter: 0' }), + ).toBeVisible() + } + + test('useActionState @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await testUseActionState(page) + }) + + testNoJs('useActionState @nojs', async ({ page }) => { + await page.goto(f.url()) + await testUseActionState(page) + }) + + async function testUseActionState(page: Page) { + await expect(page.getByTestId('use-action-state')).toContainText( + 'test-useActionState: 0', + ) + await page.getByTestId('use-action-state').click() + await expect(page.getByTestId('use-action-state')).toContainText( + 'test-useActionState: 1', + ) + await page.getByTestId('use-action-state').click() + await expect(page.getByTestId('use-action-state')).toContainText( + 'test-useActionState: 2', + ) + } + + test('useActionState with jsx @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await testUseActionStateJsx(page) + }) + + testNoJs('useActionState with jsx @nojs', async ({ page }) => { + await page.goto(f.url()) + await testUseActionStateJsx(page, { js: false }) + }) + + async function testUseActionStateJsx(page: Page, options?: { js?: boolean }) { + await page.getByTestId('use-action-state-jsx').getByRole('button').click() + await expect(page.getByTestId('use-action-state-jsx')).toContainText( + /\(ok\)/, + ) + + // 1st call "works" but it shows an error during reponse and it breaks 2nd call. + // Failed to serialize an action for progressive enhancement: + // Error: React Element cannot be passed to Server Functions from the Client without a temporary reference set. Pass a TemporaryReferenceSet to the options. + // [Promise, ] + if (!options?.js) return + + await page.getByTestId('use-action-state-jsx').getByRole('button').click() + await expect(page.getByTestId('use-action-state-jsx')).toContainText( + /\(ok\).*\(ok\)/, + ) + } + + test.describe(() => { + test.skip(f.mode !== 'build') + + testNoJs('module preload on ssr', async ({ page }) => { + await page.goto(f.url()) + const srcs = await page + .locator(`head >> link[rel="modulepreload"]`) + .evaluateAll((elements) => + elements.map((el) => el.getAttribute('href')), + ) + const manifest = JSON.parse( + readFileSync( + f.root + '/dist/ssr/__vite_rsc_assets_manifest.js', + 'utf-8', + ).slice('export default '.length), + ) + const hashString = (v: string) => + createHash('sha256').update(v).digest().toString('hex').slice(0, 12) + const deps = + manifest.clientReferenceDeps[hashString('src/routes/client.tsx')] + expect(srcs).toEqual(expect.arrayContaining(deps.js)) + }) + }) + + test.describe(() => { + test.skip(f.mode !== 'dev') + + test('server reference update @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await testServerActionUpdate(page, { js: true }) + }) + + test('server reference update @nojs', async ({ page }) => { + await page.goto(f.url()) + await testServerActionUpdate(page, { js: false }) + }) + }) + + async function testServerActionUpdate(page: Page, options: { js: boolean }) { + await page.getByRole('button', { name: 'server-counter: 0' }).click() + await expect( + page.getByRole('button', { name: 'server-counter: 1' }), + ).toBeVisible() + + // update server code + const editor = f.createEditor('src/routes/action/action.tsx') + editor.edit((s) => + s.replace('const TEST_UPDATE = 1\n', 'const TEST_UPDATE = 10\n'), + ) + await expect(async () => { + if (!options.js) await page.goto(f.url()) + await expect( + page.getByRole('button', { name: 'server-counter: 0' }), + ).toBeVisible({ timeout: 10 }) + }).toPass() + + await page.getByRole('button', { name: 'server-counter: 0' }).click() + await expect( + page.getByRole('button', { name: 'server-counter: 10' }), + ).toBeVisible() + + editor.reset() + await expect(async () => { + if (!options.js) await page.goto(f.url()) + await expect( + page.getByRole('button', { name: 'server-counter: 0' }), + ).toBeVisible({ timeout: 10 }) + }).toPass() + } + + test.describe(() => { + test.skip(f.mode !== 'dev') + + test('client hmr', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await page.getByRole('button', { name: 'client-counter: 0' }).click() + await expect( + page.getByRole('button', { name: 'client-counter: 1' }), + ).toBeVisible() + + const editor = f.createEditor('src/routes/client.tsx') + editor.edit((s) => s.replace('client-counter', 'client-[edit]-counter')) + await expect( + page.getByRole('button', { name: 'client-[edit]-counter: 1' }), + ).toBeVisible() + + // check next ssr is also updated + const res = await page.goto(f.url()) + expect(await res?.text()).toContain('client-[edit]-counter') + await waitForHydration(page) + editor.reset() + await page.getByRole('button', { name: 'client-counter: 0' }).click() + }) + + test('non-boundary client hmr', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + + const locator = page.getByTestId('test-hmr-client-dep') + await expect(locator).toHaveText('test-hmr-client-dep: 0[ok]') + await locator.locator('button').click() + await expect(locator).toHaveText('test-hmr-client-dep: 1[ok]') + + const editor = f.createEditor('src/routes/hmr-client-dep/client-dep.tsx') + editor.edit((s) => s.replace('[ok]', '[ok-edit]')) + await expect(locator).toHaveText('test-hmr-client-dep: 1[ok-edit]') + + // check next ssr is also updated + const res = await page.reload() + expect(await res?.text()).toContain('[ok-edit]') + + await waitForHydration(page) + editor.reset() + await expect(locator).toHaveText('test-hmr-client-dep: 0[ok]') + }) + + test('server hmr', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + const editor = f.createEditor('src/routes/action/server.tsx') + editor.edit((s) => s.replace('server-counter', 'server-[edit]-counter')) + await expect( + page.getByRole('button', { name: 'server-[edit]-counter: 0' }), + ).toBeVisible() + editor.reset() + await expect( + page.getByRole('button', { name: 'server-counter: 0' }), + ).toBeVisible() + }) + + test('module invalidation', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + + // change child module state + const locator = page.getByTestId('test-module-invalidation-server') + await expect(locator).toContainText('[dep: 0]') + locator.getByRole('button').click() + await expect(locator).toContainText('[dep: 1]') + + // change parent module + const editor = f.createEditor('src/routes/module-invalidation/server.tsx') + editor.edit((s) => s.replace('[dep:', '[dep-edit:')) + + // preserve child module state + await expect(locator).toContainText('[dep-edit: 1]') + editor.reset() + await expect(locator).toContainText('[dep: 1]') + }) + }) + + test('css @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await testCss(page) + }) + + testNoJs('css @nojs', async ({ page }) => { + await page.goto(f.url()) + await testCss(page) + }) + + async function testCss(page: Page, color = 'rgb(255, 165, 0)') { + await expect(page.locator('.test-style-client')).toHaveCSS('color', color) + await expect(page.locator('.test-style-server')).toHaveCSS('color', color) + } + + test.describe(() => { + test.skip(f.mode !== 'dev') + + test('css hmr client', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + + await using _ = await expectNoReload(page) + const editor = f.createEditor('src/routes/style-client/client.css') + editor.edit((s) => s.replaceAll('rgb(255, 165, 0)', 'rgb(0, 165, 255)')) + await expect(page.locator('.test-style-client')).toHaveCSS( + 'color', + 'rgb(0, 165, 255)', + ) + editor.edit((s) => + s.replaceAll( + `color: rgb(0, 165, 255);`, + `/* color: rgb(0, 165, 255); */`, + ), + ) + await expect(page.locator('.test-style-client')).toHaveCSS( + 'color', + 'rgb(0, 0, 0)', + ) + // wait longer for multiple edits + await page.waitForTimeout(100) + editor.reset() + await expect(page.locator('.test-style-client')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + }) + + test('adding/removing css client @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await testAddRemoveCssClient(page, { js: true }) + }) + + testNoJs('adding/removing css client @nojs', async ({ page }) => { + await page.goto(f.url()) + await testAddRemoveCssClient(page, { js: false }) + }) + + async function testAddRemoveCssClient( + page: Page, + options: { js: boolean }, + ) { + await expect(page.locator('.test-style-client-dep')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + + // remove css import + const editor = f.createEditor('src/routes/style-client/client-dep.tsx') + editor.edit((s) => + s.replaceAll( + `import './client-dep.css'`, + `/* import './client-dep.css' */`, + ), + ) + await page.waitForTimeout(100) + await expect(async () => { + if (!options.js) await page.reload() + await expect(page.locator('.test-style-client-dep')).toHaveCSS( + 'color', + 'rgb(0, 0, 0)', + { timeout: 10 }, + ) + }).toPass() + + // add back css import + editor.reset() + await page.waitForTimeout(100) + await expect(async () => { + if (!options.js) await page.reload() + await expect(page.locator('.test-style-client-dep')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + { timeout: 10 }, + ) + }).toPass() + } + + test('css hmr server', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + + await using _ = await expectNoReload(page) + const editor = f.createEditor('src/routes/style-server/server.css') + editor.edit((s) => s.replaceAll('rgb(255, 165, 0)', 'rgb(0, 165, 255)')) + await expect(page.locator('.test-style-server')).toHaveCSS( + 'color', + 'rgb(0, 165, 255)', + ) + editor.edit((s) => + s.replaceAll( + `color: rgb(0, 165, 255);`, + `/* color: rgb(0, 165, 255); */`, + ), + ) + await expect(page.locator('.test-style-server')).toHaveCSS( + 'color', + 'rgb(0, 0, 0)', + ) + editor.reset() + await expect(page.locator('.test-style-server')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + }) + + // TODO: need a way to add/remove links on server hmr. for now, it requires a manually reload. + test.skip('adding/removing css server @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await testAddRemoveCssServer(page, { js: true }) + }) + + testNoJs('adding/removing css server @nojs', async ({ page }) => { + await page.goto(f.url()) + await testAddRemoveCssServer(page, { js: false }) + }) + + async function testAddRemoveCssServer( + page: Page, + options: { js: boolean }, + ) { + await expect(page.locator('.test-style-server')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + + // remove css import + const editor = f.createEditor('src/routes/style-server/server.tsx') + editor.edit((s) => + s.replaceAll(`import './server.css'`, `/* import './server.css' */`), + ) + await page.waitForTimeout(100) + await expect(async () => { + if (!options.js) await page.reload() + await expect(page.locator('.test-style-server')).toHaveCSS( + 'color', + 'rgb(0, 0, 0)', + { timeout: 10 }, + ) + }).toPass() + + // add back css import + editor.reset() + await page.waitForTimeout(100) + await expect(async () => { + if (!options.js) await page.reload() + await expect(page.locator('.test-style-server')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + { timeout: 10 }, + ) + }).toPass() + } + }) + + test('css client no ssr', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await page.locator("a[href='?test-client-style-no-ssr']").click() + await expect(page.locator('.test-style-client-no-ssr')).toHaveCSS( + 'color', + 'rgb(0, 200, 100)', + ) + }) + + test('css module client @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByTestId('css-module-client')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + + if (f.mode !== 'dev') return + + // test client css module HMR + await using _ = await expectNoReload(page) + const editor = f.createEditor('src/routes/style-client/client.module.css') + editor.edit((s) => s.replaceAll('rgb(255, 165, 0)', 'rgb(0, 165, 255)')) + await expect(page.getByTestId('css-module-client')).toHaveCSS( + 'color', + 'rgb(0, 165, 255)', + ) + editor.reset() + await expect(page.getByTestId('css-module-client')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + }) + + test('css module server @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByTestId('css-module-server')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + + if (f.mode !== 'dev') return + + // test server css module HMR + await using _ = await expectNoReload(page) + const editor = f.createEditor('src/routes/style-server/server.module.css') + editor.edit((s) => s.replaceAll('rgb(255, 165, 0)', 'rgb(0, 165, 255)')) + await expect(page.getByTestId('css-module-server')).toHaveCSS( + 'color', + 'rgb(0, 165, 255)', + ) + editor.reset() + await expect(page.getByTestId('css-module-server')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + }) + + testNoJs('css module @nojs', async ({ page }) => { + await page.goto(f.url()) + await expect(page.getByTestId('css-module-client')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + await expect(page.getByTestId('css-module-server')).toHaveCSS( + 'color', + 'rgb(255, 165, 0)', + ) + }) + + test('tailwind @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await testTailwind(page) + }) + + testNoJs('tailwind @nojs', async ({ page }) => { + await page.goto(f.url()) + await testTailwind(page) + }) + + async function testTailwind(page: Page) { + await expect(page.locator('.test-tw-client')).toHaveCSS( + 'color', + // blue-500 + 'rgb(0, 0, 255)', + ) + await expect(page.locator('.test-tw-server')).toHaveCSS( + 'color', + // red-500 + 'rgb(255, 0, 0)', + ) + } + + test.describe(() => { + test.skip(f.mode !== 'dev') + + test('tailwind hmr', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await testTailwind(page) + + await using _ = await expectNoReload(page) + + const clientFile = f.createEditor('src/routes/tailwind/client.tsx') + clientFile.edit((s) => s.replaceAll('text-[#00f]', 'text-[#88f]')) + await expect(page.locator('.test-tw-client')).toHaveCSS( + 'color', + 'rgb(136, 136, 255)', + ) + clientFile.reset() + await expect(page.locator('.test-tw-client')).toHaveCSS( + 'color', + 'rgb(0, 0, 255)', + ) + + const serverFile = f.createEditor('src/routes/tailwind/server.tsx') + serverFile.edit((s) => s.replaceAll('text-[#f00]', 'text-[#f88]')) + await expect(page.locator('.test-tw-server')).toHaveCSS( + 'color', + 'rgb(255, 136, 136)', + ) + serverFile.reset() + await expect(page.locator('.test-tw-server')).toHaveCSS( + 'color', + 'rgb(255, 0, 0)', + ) + }) + + testNoJs('no FOUC after server restart @nojs', async ({ page }) => { + const res = await page.request.get(f.url('/__test_restart')) + expect(await res.text()).toBe('ok') + await new Promise((r) => setTimeout(r, 100)) + await page.goto(f.url('./')) + await testCss(page) + await testTailwind(page) + }) + }) + + test('temporary references @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await page.getByRole('button', { name: 'test-temporary-reference' }).click() + await expect(page.getByTestId('temporary-reference')).toContainText( + 'result: [server [client]]', + ) + }) + + test('server action error @js', async ({ page }) => { + // it doesn't seem possible to assert react error stack mapping on playwright. + // this need to be verified manually on browser devtools console. + await page.goto(f.url()) + await waitForHydration(page) + await page.getByRole('button', { name: 'test-server-action-error' }).click() + await expect(page.getByText('ErrorBoundary caught')).toBeVisible() + await page.getByRole('button', { name: 'reset-error' }).click() + await expect( + page.getByRole('button', { name: 'test-server-action-error' }), + ).toBeVisible() + }) + + test('hydrate while streaming @js', async ({ page }) => { + // client is interactive before suspense is resolved + await page.goto(f.url('./?test-suspense=1000'), { waitUntil: 'commit' }) + await waitForHydration(page) + await expect(page.getByTestId('suspense')).toContainText( + 'suspense-fallback', + ) + await expect(page.getByTestId('suspense')).toContainText( + 'suspense-resolved', + ) + }) + + test('ssr rsc payload encoding', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByTestId('ssr-rsc-payload')).toHaveText( + 'test1: true, test2: true, test3: false, test4: true', + ) + + await page.goto(f.url('./?test-payload-binary')) + await waitForHydration(page) + await expect(page.getByTestId('ssr-rsc-payload')).toHaveText( + 'test1: true, test2: true, test3: true, test4: true', + ) + }) + + test('action bind simple @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await testActionBindSimple(page) + }) + + testNoJs('action bind simple @nojs', async ({ page }) => { + await page.goto(f.url()) + await testActionBindSimple(page) + }) + + async function testActionBindSimple(page: Page) { + await expect(page.getByTestId('test-server-action-bind-simple')).toHaveText( + '[?]', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-simple' }) + .click() + await expect(page.getByTestId('test-server-action-bind-simple')).toHaveText( + 'true', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-reset' }) + .click() + } + + test('action bind client @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await testActionBindClient(page) + }) + + // this doesn't work on Next either https://github.com/hi-ogawa/reproductions/tree/main/next-rsc-client-action-bind + testNoJs.skip('action bind client @nojs', async ({ page }) => { + await page.goto(f.url()) + await testActionBindClient(page) + }) + + async function testActionBindClient(page: Page) { + await expect(page.getByTestId('test-server-action-bind-client')).toHaveText( + '[?]', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-client' }) + .click() + await expect(page.getByTestId('test-server-action-bind-client')).toHaveText( + 'true', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-reset' }) + .click() + } + + test('action bind action @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await testActionBindAction(page) + }) + + testNoJs('action bind action @nojs', async ({ page }) => { + await page.goto(f.url()) + await testActionBindAction(page) + }) + + async function testActionBindAction(page: Page) { + await expect(page.getByTestId('test-server-action-bind-action')).toHaveText( + '[?]', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-action' }) + .click() + await expect(page.getByTestId('test-server-action-bind-action')).toHaveText( + '[true,true]', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-reset' }) + .click() + } + + test('test serialization @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByTestId('serialization')).toHaveText('?') + await page.getByTestId('serialization').click() + await expect(page.getByTestId('serialization')).toHaveText('ok') + }) + + test('client-in-server package', async ({ page }) => { + await page.goto(f.url()) + await expect(page.getByTestId('client-in-server')).toHaveText( + '[test-client-in-server-dep: true]', + ) + await expect(page.getByTestId('provider-in-server')).toHaveText( + '[test-provider-in-server-dep: true]', + ) + }) + + test('server-in-server package', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByTestId('server-in-server')).toHaveText( + 'server-in-server: 0', + ) + await page.getByTestId('server-in-server').click() + await expect(page.getByTestId('server-in-server')).toHaveText( + 'server-in-server: 1', + ) + await page.reload() + await expect(page.getByTestId('server-in-server')).toHaveText( + 'server-in-server: 1', + ) + }) + + test('server-in-client package', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByTestId('server-in-client')).toHaveText( + 'server-in-client: ?', + ) + await page.getByTestId('server-in-client').click() + await expect(page.getByTestId('server-in-client')).toHaveText( + 'server-in-client: 1', + ) + await page.reload() + await waitForHydration(page) + await expect(page.getByTestId('server-in-client')).toHaveText( + 'server-in-client: ?', + ) + await page.getByTestId('server-in-client').click() + await expect(page.getByTestId('server-in-client')).toHaveText( + 'server-in-client: 2', + ) + }) + + test('use cache function', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + const locator = page.getByTestId('test-use-cache-fn') + await expect(locator.locator('span')).toHaveText( + '(actionCount: 0, cacheFnCount: 0)', + ) + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 1, cacheFnCount: 1)', + ) + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 2, cacheFnCount: 1)', + ) + await locator.getByRole('textbox').fill('test') + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 3, cacheFnCount: 2)', + ) + await locator.getByRole('textbox').fill('test') + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 4, cacheFnCount: 2)', + ) + + // revalidate cache + await locator.getByRole('textbox').fill('revalidate') + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 5, cacheFnCount: 3)', + ) + await locator.getByRole('textbox').fill('test') + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 6, cacheFnCount: 4)', + ) + }) + + test('use cache component', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + const static1 = await page + .getByTestId('test-use-cache-component-static') + .textContent() + const dynamic1 = await page + .getByTestId('test-use-cache-component-dynamic') + .textContent() + await page.waitForTimeout(100) + await page.reload() + const static2 = await page + .getByTestId('test-use-cache-component-static') + .textContent() + const dynamic2 = await page + .getByTestId('test-use-cache-component-dynamic') + .textContent() + expect({ static2, dynamic2 }).toEqual({ + static2: expect.stringMatching(static1!), + dynamic2: expect.not.stringMatching(dynamic1!), + }) + }) + + test('use cache closure', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + const locator = page.getByTestId('test-use-cache-closure') + await expect(locator.locator('span')).toHaveText( + '(actionCount: 0, innerFnCount: 0)', + ) + + // (x, y) + await locator.getByPlaceholder('outer').fill('x') + await locator.getByPlaceholder('inner').fill('y') + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 1, innerFnCount: 1)', + ) + + // (x, y) + await locator.getByPlaceholder('outer').fill('x') + await locator.getByPlaceholder('inner').fill('y') + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 2, innerFnCount: 1)', + ) + + // (xx, y) + await locator.getByPlaceholder('outer').fill('xx') + await locator.getByPlaceholder('inner').fill('y') + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 3, innerFnCount: 2)', + ) + + // (xx, y) + await locator.getByPlaceholder('outer').fill('xx') + await locator.getByPlaceholder('inner').fill('y') + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 4, innerFnCount: 2)', + ) + + // (xx, yy) + await locator.getByPlaceholder('outer').fill('xx') + await locator.getByPlaceholder('inner').fill('yy') + await locator.getByRole('button').click() + await expect(locator.locator('span')).toHaveText( + '(actionCount: 5, innerFnCount: 3)', + ) + }) +} diff --git a/packages/plugin-rsc/e2e/fixture.ts b/packages/plugin-rsc/e2e/fixture.ts new file mode 100644 index 00000000..7d99f99d --- /dev/null +++ b/packages/plugin-rsc/e2e/fixture.ts @@ -0,0 +1,191 @@ +import assert from 'node:assert' +import { type SpawnOptions, spawn } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import { stripVTControlCharacters, styleText } from 'node:util' +import test from '@playwright/test' +import { x } from 'tinyexec' + +function runCli(options: { command: string; label?: string } & SpawnOptions) { + const [name, ...args] = options.command.split(' ') + const child = x(name!, args, { nodeOptions: options }).process! + const label = `[${options.label ?? 'cli'}]` + child.stdout!.on('data', (data) => { + if (process.env.TEST_DEBUG) { + console.log(styleText('cyan', label), data.toString()) + } + }) + child.stderr!.on('data', (data) => { + console.log(styleText('magenta', label), data.toString()) + }) + const done = new Promise((resolve) => { + child.on('exit', (code) => { + if (code !== 0 && code !== 143 && process.platform !== 'win32') { + console.log(styleText('magenta', `${label}`), `exit code ${code}`) + } + resolve() + }) + }) + + async function findPort(): Promise { + let stdout = '' + return new Promise((resolve) => { + child.stdout!.on('data', (data) => { + stdout += stripVTControlCharacters(String(data)) + const match = stdout.match(/http:\/\/localhost:(\d+)/) + if (match) { + resolve(Number(match[1])) + } + }) + }) + } + + function kill() { + if (process.platform === 'win32') { + spawn('taskkill', ['/pid', String(child.pid), '/t', '/f']) + } else { + child.kill() + } + } + + return { proc: child, done, findPort, kill } +} + +export type Fixture = ReturnType + +export function useFixture(options: { + root: string + mode?: 'dev' | 'build' + command?: string + buildCommand?: string + cliOptions?: SpawnOptions +}) { + let cleanup: (() => Promise) | undefined + let baseURL!: string + + const cwd = path.resolve(options.root) + + // TODO: `beforeAll` is called again on any test failure. + // https://playwright.dev/docs/test-retries + test.beforeAll(async () => { + if (options.mode === 'dev') { + const proc = runCli({ + command: options.command ?? `pnpm dev`, + label: `${options.root}:dev`, + cwd, + ...options.cliOptions, + }) + const port = await proc.findPort() + // TODO: use `test.extend` to set `baseURL`? + baseURL = `http://localhost:${port}` + cleanup = async () => { + proc.kill() + await proc.done + } + } + if (options.mode === 'build') { + if (!process.env.TEST_SKIP_BUILD) { + const proc = runCli({ + command: options.buildCommand ?? `pnpm build`, + label: `${options.root}:build`, + cwd, + ...options.cliOptions, + }) + await proc.done + assert(proc.proc.exitCode === 0) + } + const proc = runCli({ + command: options.command ?? `pnpm preview`, + label: `${options.root}:preview`, + cwd, + ...options.cliOptions, + }) + const port = await proc.findPort() + baseURL = `http://localhost:${port}` + cleanup = async () => { + proc.kill() + await proc.done + } + } + }) + + test.afterAll(async () => { + await cleanup?.() + }) + + const originalFiles: Record = {} + + function createEditor(filepath: string) { + filepath = path.resolve(cwd, filepath) + const init = fs.readFileSync(filepath, 'utf-8') + originalFiles[filepath] ??= init + let current = init + return { + edit(editFn: (data: string) => string): void { + const next = editFn(current) + assert(next !== current, 'Edit function did not change the content') + current = next + fs.writeFileSync(filepath, next) + }, + reset(): void { + fs.writeFileSync(filepath, originalFiles[filepath]!) + }, + } + } + + test.afterAll(async () => { + for (const [filepath, content] of Object.entries(originalFiles)) { + fs.writeFileSync(filepath, content) + } + }) + + return { + mode: options.mode, + root: cwd, + url: (url: string = './') => new URL(url, baseURL).href, + createEditor, + } +} + +export async function setupIsolatedFixture(options: { + src: string + dest: string +}) { + // copy fixture + fs.rmSync(options.dest, { recursive: true, force: true }) + fs.cpSync(options.src, options.dest, { recursive: true }) + fs.rmSync(path.join(options.dest, 'node_modules'), { + recursive: true, + force: true, + }) + + // setup package.json overrides + const packagesDir = path.join(import.meta.dirname, '..', '..') + const overrides = { + '@vitejs/plugin-rsc': `file:${path.join(packagesDir, 'plugin-rsc')}`, + } + editFileJson(path.join(options.dest, 'package.json'), (pkg: any) => { + Object.assign(((pkg.pnpm ??= {}).overrides ??= {}), overrides) + return pkg + }) + + // install + await x('pnpm', ['i'], { + throwOnError: true, + nodeOptions: { + cwd: options.dest, + stdio: process.env.TEST_DEBUG ? 'inherit' : undefined, + }, + }) +} + +function editFileJson(filepath: string, edit: (s: string) => string) { + fs.writeFileSync( + filepath, + JSON.stringify( + edit(JSON.parse(fs.readFileSync(filepath, 'utf-8'))), + null, + 2, + ), + ) +} diff --git a/packages/plugin-rsc/e2e/helper.ts b/packages/plugin-rsc/e2e/helper.ts new file mode 100644 index 00000000..fdb36606 --- /dev/null +++ b/packages/plugin-rsc/e2e/helper.ts @@ -0,0 +1,44 @@ +import test, { type Page, expect } from '@playwright/test' + +export const testNoJs = test.extend({ + javaScriptEnabled: ({}, use) => use(false), +}) + +export async function waitForHydration(page: Page) { + await expect + .poll( + () => + page + .locator('body') + .evaluate( + (el) => + el && + Object.keys(el).some((key) => key.startsWith('__reactFiber')), + ), + { timeout: 3000 }, + ) + .toBeTruthy() +} + +export async function expectNoReload(page: Page) { + // inject custom meta + await page.evaluate(() => { + const el = document.createElement('meta') + el.setAttribute('name', 'x-reload-check') + document.head.append(el) + }) + + // TODO: playwright prints a weird error on dispose error, + // so maybe we shouldn't abuse this pattern :( + return { + [Symbol.asyncDispose]: async () => { + // check if meta is preserved + await expect(page.locator(`meta[name="x-reload-check"]`)).toBeAttached({ + timeout: 1, + }) + await page.evaluate(() => { + document.querySelector(`meta[name="x-reload-check"]`)!.remove() + }) + }, + } +} diff --git a/packages/plugin-rsc/e2e/react-router.test.ts b/packages/plugin-rsc/e2e/react-router.test.ts new file mode 100644 index 00000000..9a8f6e78 --- /dev/null +++ b/packages/plugin-rsc/e2e/react-router.test.ts @@ -0,0 +1,179 @@ +import { createHash } from 'node:crypto' +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { type Fixture, useFixture } from './fixture' +import { expectNoReload, testNoJs, waitForHydration } from './helper' + +test.describe('dev-default', () => { + const f = useFixture({ root: 'examples/react-router', mode: 'dev' }) + defineTest(f) +}) + +test.describe('build-default', () => { + const f = useFixture({ root: 'examples/react-router', mode: 'build' }) + defineTest(f) +}) + +test.describe('dev-cloudflare', () => { + const f = useFixture({ + root: 'examples/react-router', + mode: 'dev', + command: 'pnpm cf-dev', + }) + defineTest(f) +}) + +test.describe('build-cloudflare', () => { + const f = useFixture({ + root: 'examples/react-router', + mode: 'build', + buildCommand: 'pnpm cf-build', + command: 'pnpm cf-preview', + }) + defineTest(f) +}) + +function defineTest(f: Fixture) { + test('loader', async ({ page }) => { + await page.goto(f.url()) + await expect(page.getByText(`loaderData: {"name":"Unknown"}`)).toBeVisible() + }) + + test('client', async ({ page }) => { + await page.goto(f.url('./about')) + await waitForHydration(page) + await page.getByRole('button', { name: 'Client counter: 0' }).click() + await expect( + page.getByRole('button', { name: 'Client counter: 1' }), + ).toBeVisible() + }) + + test('navigation', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + + await page.getByText('This is the home page.').click() + + await page.getByRole('link', { name: 'About' }).click() + await page.waitForURL(f.url('./about')) + await page.getByText('This is the about page.').click() + + await page.getByRole('link', { name: 'Home' }).click() + await page.waitForURL(f.url()) + await page.getByText('This is the home page.').click() + }) + + test.describe(() => { + test.skip(f.mode !== 'build') + + testNoJs('ssr modulepreload', async ({ page }) => { + await page.goto(f.url()) + const srcs = await page + .locator(`head >> link[rel="modulepreload"]`) + .evaluateAll((elements) => + elements.map((el) => el.getAttribute('href')), + ) + const { default: manifest } = await import( + path.resolve(f.root, 'dist/ssr/__vite_rsc_assets_manifest.js') + ) + const hashString = (v: string) => + createHash('sha256').update(v).digest().toString('hex').slice(0, 12) + const deps = + manifest.clientReferenceDeps[hashString('app/routes/home.client.tsx')] + expect(srcs).toEqual(expect.arrayContaining(deps.js)) + }) + }) + + test.describe(() => { + test.skip(f.mode !== 'dev') + + test('client hmr', async ({ page }) => { + await page.goto(f.url('./about')) + await waitForHydration(page) + await using _ = await expectNoReload(page) + + await page.getByRole('button', { name: 'Client counter: 0' }).click() + await expect( + page.getByRole('button', { name: 'Client counter: 1' }), + ).toBeVisible() + + const editor = f.createEditor('app/routes/about.tsx') + editor.edit((s) => s.replace('Client counter:', 'Client [edit] counter:')) + + await expect( + page.getByRole('button', { name: 'Client [edit] counter: 1' }), + ).toBeVisible() + }) + + test('server hmr', async ({ page }) => { + await page.goto(f.url('/')) + await waitForHydration(page) + await using _ = await expectNoReload(page) + + await page.getByText('This is the home page.').click() + + const editor = f.createEditor('app/routes/home.tsx') + editor.edit((s) => + s.replace('This is the home page.', 'This is the home [edit] page.'), + ) + + await page.getByText('This is the home [edit] page.').click() + }) + }) + + test('server css code split', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.locator('.test-style-home')).toHaveCSS( + 'color', + 'rgb(250, 150, 0)', + ) + + // client side navigation to "/about" keeps "/" styles + await page.getByRole('link', { name: 'About' }).click() + await page.waitForURL(f.url('./about')) + await expect(page.locator('.test-style-home')).toHaveCSS( + 'color', + 'rgb(250, 150, 0)', + ) + + // SSR of "/about" doesn't include "/" styles + await page.goto(f.url('./about')) + await waitForHydration(page) + await expect(page.locator('.test-style-home')).not.toHaveCSS( + 'color', + 'rgb(250, 150, 0)', + ) + + // client side navigation to "/" loads "/" styles + await page.getByRole('link', { name: 'Home' }).click() + await page.waitForURL(f.url()) + await expect(page.locator('.test-style-home')).toHaveCSS( + 'color', + 'rgb(250, 150, 0)', + ) + }) + + test('vite-rsc-css-export', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByTestId('root-style')).toHaveCSS( + 'color', + 'rgb(0, 0, 255)', + ) + }) + + test('useActionState', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await page.getByTestId('use-action-state-jsx').getByRole('button').click() + await expect(page.getByTestId('use-action-state-jsx')).toContainText( + /\(ok\)/, + ) + await page.getByTestId('use-action-state-jsx').getByRole('button').click() + await expect(page.getByTestId('use-action-state-jsx')).toContainText( + /\(ok\).*\(ok\)/, + ) + }) +} diff --git a/packages/plugin-rsc/e2e/ssg.test.ts b/packages/plugin-rsc/e2e/ssg.test.ts new file mode 100644 index 00000000..b1b7d4fb --- /dev/null +++ b/packages/plugin-rsc/e2e/ssg.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test' +import { type Fixture, useFixture } from './fixture' +import { waitForHydration } from './helper' + +test.describe('dev', () => { + const f = useFixture({ + root: 'examples/ssg', + mode: 'dev', + }) + defineTestSsg(f) +}) + +test.describe('build', () => { + const f = useFixture({ + root: 'examples/ssg', + mode: 'build', + }) + defineTestSsg(f) +}) + +function defineTestSsg(f: Fixture) { + test('basic', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + + if (f.mode === 'build') { + const t1 = await page.getByTestId('timestamp').textContent() + await page.waitForTimeout(100) + await page.reload() + await waitForHydration(page) + const t2 = await page.getByTestId('timestamp').textContent() + expect(t2).toBe(t1) + } + }) +} diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts new file mode 100644 index 00000000..f0d6aa23 --- /dev/null +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -0,0 +1,94 @@ +import { expect, test } from '@playwright/test' +import { type Fixture, useFixture } from './fixture' +import { expectNoReload, testNoJs, waitForHydration } from './helper' + +test.describe('dev-default', () => { + const f = useFixture({ root: 'examples/starter', mode: 'dev' }) + defineTest(f) +}) + +test.describe('build-default', () => { + const f = useFixture({ root: 'examples/starter', mode: 'build' }) + defineTest(f) +}) + +test.describe('dev-cloudflare', () => { + const f = useFixture({ root: 'examples/starter-cf-single', mode: 'dev' }) + defineTest(f) +}) + +test.describe('build-cloudflare', () => { + const f = useFixture({ root: 'examples/starter-cf-single', mode: 'build' }) + defineTest(f) +}) + +function defineTest(f: Fixture) { + test('basic', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + }) + + test('client component', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await page.getByRole('button', { name: 'Client Counter: 0' }).click() + await expect( + page.getByRole('button', { name: 'Client Counter: 1' }), + ).toBeVisible() + }) + + test('server action @js', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await using _ = await expectNoReload(page) + await page.getByRole('button', { name: 'Server Counter: 0' }).click() + await expect( + page.getByRole('button', { name: 'Server Counter: 1' }), + ).toBeVisible() + }) + + testNoJs('server action @nojs', async ({ page }) => { + await page.goto(f.url()) + await page.getByRole('button', { name: 'Server Counter: 1' }).click() + await expect( + page.getByRole('button', { name: 'Server Counter: 2' }), + ).toBeVisible() + }) + + test('client hmr', async ({ page }) => { + test.skip(f.mode === 'build') + + await page.goto(f.url()) + await waitForHydration(page) + await page.getByRole('button', { name: 'Client Counter: 0' }).click() + await expect( + page.getByRole('button', { name: 'Client Counter: 1' }), + ).toBeVisible() + + const editor = f.createEditor(`src/client.tsx`) + editor.edit((s) => s.replace('Client Counter', 'Client [edit] Counter')) + await expect( + page.getByRole('button', { name: 'Client [edit] Counter: 1' }), + ).toBeVisible() + + // check next ssr is also updated + const res = await page.goto(f.url()) + expect(await res?.text()).toContain('Client [edit] Counter') + await waitForHydration(page) + editor.reset() + await page.getByRole('button', { name: 'Client Counter: 0' }).click() + }) + + test('image assets', async ({ page }) => { + await page.goto(f.url()) + await waitForHydration(page) + await expect(page.getByAltText('Vite logo')).not.toHaveJSProperty( + 'naturalWidth', + 0, + ) + await expect(page.getByAltText('React logo')).not.toHaveJSProperty( + 'naturalWidth', + 0, + ) + }) +} diff --git a/packages/plugin-rsc/e2e/tsconfig.json b/packages/plugin-rsc/e2e/tsconfig.json new file mode 100644 index 00000000..fbf20fed --- /dev/null +++ b/packages/plugin-rsc/e2e/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "noImplicitReturns": false, + "checkJs": false + } +} diff --git a/packages/plugin-rsc/examples/basic/README.md b/packages/plugin-rsc/examples/basic/README.md new file mode 100644 index 00000000..f9608323 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/README.md @@ -0,0 +1,9 @@ +# rsc basic + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/hi-ogawa/vite-plugins/tree/main/packages/rsc/examples/basic) + +https://vite-rsc-basic.hiro18181.workers.dev + +```sh +npx giget gh:hi-ogawa/vite-plugins/packages/rsc/examples/basic my-app +``` diff --git a/packages/plugin-rsc/examples/basic/package.json b/packages/plugin-rsc/examples/basic/package.json new file mode 100644 index 00000000..7db4bcf4 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/package.json @@ -0,0 +1,32 @@ +{ + "name": "@vitejs/plugin-rsc-examples-basic", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --app", + "preview": "vite preview", + "cf-build": "CF_BUILD=1 pnpm build", + "cf-preview": "wrangler dev", + "cf-release": "wrangler deploy" + }, + "dependencies": { + "@vitejs/plugin-rsc": "latest", + "react": "latest", + "react-dom": "latest" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.4", + "@types/react": "latest", + "@types/react-dom": "latest", + "@vitejs/plugin-react": "latest", + "@vitejs/test-dep-client-in-server": "file:./test-dep/client-in-server", + "@vitejs/test-dep-client-in-server2": "file:./test-dep/client-in-server2", + "@vitejs/test-dep-server-in-client": "file:./test-dep/server-in-client", + "@vitejs/test-dep-server-in-server": "file:./test-dep/server-in-server", + "tailwindcss": "^4.1.4", + "vite": "^7.0.2", + "vite-plugin-inspect": "^11.2.0" + } +} diff --git a/packages/plugin-rsc/examples/basic/public/favicon.ico b/packages/plugin-rsc/examples/basic/public/favicon.ico new file mode 100644 index 00000000..4aff0766 Binary files /dev/null and b/packages/plugin-rsc/examples/basic/public/favicon.ico differ diff --git a/packages/plugin-rsc/examples/basic/src/client.tsx b/packages/plugin-rsc/examples/basic/src/client.tsx new file mode 100644 index 00000000..ebbd4335 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/client.tsx @@ -0,0 +1,3 @@ +import { hydrate } from '@vitejs/plugin-rsc/extra/browser' + +hydrate() diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-bind/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-bind/client.tsx new file mode 100644 index 00000000..2fe0c81c --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action-bind/client.tsx @@ -0,0 +1,12 @@ +'use client' + +import React from 'react' + +export function ActionBindClient() { + const hydrated = React.useSyncExternalStore( + React.useCallback(() => () => {}, []), + () => true, + () => false, + ) + return <>{String(hydrated)} +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-bind/form.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-bind/form.tsx new file mode 100644 index 00000000..1b1675c3 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action-bind/form.tsx @@ -0,0 +1,16 @@ +'use client' + +import React from 'react' + +export function TestServerActionBindClientForm(props: { + action: () => Promise +}) { + const [result, formAction] = React.useActionState(props.action, '[?]') + + return ( +
+ + {result} +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-bind/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-bind/server.tsx new file mode 100644 index 00000000..2de0f294 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action-bind/server.tsx @@ -0,0 +1,96 @@ +// based on test cases in +// https://github.com/vercel/next.js/blob/ad898de735c393d98960a68c8d9eaeee32206c57/test/e2e/app-dir/actions/app/encryption/page.js + +import { ActionBindClient } from './client' +import { TestServerActionBindClientForm } from './form' + +export function TestServerActionBindReset() { + return ( +
{ + 'use server' + testServerActionBindSimpleState = '[?]' + testServerActionBindActionState = '[?]' + testServerActionBindClientState++ + }} + > + +
+ ) +} + +let testServerActionBindSimpleState = '[?]' + +export function TestServerActionBindSimple() { + const outerValue = 'outerValue' + + return ( +
{ + 'use server' + const result = String(formData.get('value')) === outerValue + testServerActionBindSimpleState = JSON.stringify(result) + }} + > + + + + {testServerActionBindSimpleState} + +
+ ) +} + +let testServerActionBindClientState = 0 + +export function TestServerActionBindClient() { + // client element as server action bound argument + const client = + + const action = async () => { + 'use server' + return client + } + + return ( + + ) +} + +let testServerActionBindActionState = '[?]' + +export function TestServerActionBindAction() { + async function otherAction() { + 'use server' + return 'otherActionValue' + } + + function wrapAction(value: string, action: () => Promise) { + return async function (formValue: string) { + 'use server' + const actionValue = await action() + return [actionValue === 'otherActionValue', formValue === value] + } + } + + const action = wrapAction('ok', otherAction) + + return ( +
{ + 'use server' + const result = await action(String(formData.get('value'))) + testServerActionBindActionState = JSON.stringify(result) + }} + > + + + + {testServerActionBindActionState} + +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-error/error-boundary.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-error/error-boundary.tsx new file mode 100644 index 00000000..9b06078c --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action-error/error-boundary.tsx @@ -0,0 +1,40 @@ +'use client' + +import * as React from 'react' + +interface Props { + children?: React.ReactNode +} + +interface State { + error: Error | null +} + +export default class ErrorBoundary extends React.Component { + constructor(props: Props) { + super(props) + this.state = { error: null } + } + + static getDerivedStateFromError(error: Error) { + return { error } + } + + render() { + if (this.state.error) { + return ( +
+ ErrorBoundary caught '{this.state.error.message}' + +
+ ) + } + return this.props.children + } +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-error/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-error/server.tsx new file mode 100644 index 00000000..07647569 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action-error/server.tsx @@ -0,0 +1,19 @@ +import ErrorBoundary from './error-boundary' + +// see browser console to verify that server action error shows +// server component stack with correct source map + +export function TestServerActionError() { + return ( + +
{ + 'use server' + throw new Error('boom!') + }} + > + +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-from-client/action.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-from-client/action.tsx new file mode 100644 index 00000000..d40fa1db --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action-from-client/action.tsx @@ -0,0 +1,27 @@ +'use server' + +// test findSourceMapURL for server action imported from client + +export async function notThis() { + // + // + // + notThis2() +} + +export async function testAction() { + console.log('[test-action-from-client]') +} + +function notThis2() { + // + // +} + +export async function testAction2() { + console.log('[test-action-from-client-2]') +} + +export async function testActionState(prev: number) { + return prev + 1 +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-from-client/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-from-client/client.tsx new file mode 100644 index 00000000..8f0bc236 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action-from-client/client.tsx @@ -0,0 +1,25 @@ +'use client' + +import React from 'react' +import { testAction, testAction2, testActionState } from './action' + +export function TestActionFromClient() { + return ( +
+ + +
+ ) +} + +export function TestUseActionState() { + const [state, formAction] = React.useActionState(testActionState, 0) + + return ( +
+ +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-state/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-state/client.tsx new file mode 100644 index 00000000..5c6b6e68 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action-state/client.tsx @@ -0,0 +1,19 @@ +'use client' + +import React from 'react' + +export function TestActionStateClient(props: { + action: (prev: React.ReactNode) => Promise +}) { + const [state, formAction, isPending] = React.useActionState( + props.action, + null, + ) + + return ( +
+ + {isPending ? 'pending...' : state} +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action-state/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/action-state/server.tsx new file mode 100644 index 00000000..fb728831 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action-state/server.tsx @@ -0,0 +1,21 @@ +import { TestActionStateClient } from './client' + +// Test case based on +// https://github.com/remix-run/react-router/issues/13882 + +export function TestActionStateServer() { + const time = new Date().toISOString() // test closure encryption + return ( + { + 'use server' + await new Promise((resolve) => setTimeout(resolve, 500)) + return ( + + [(ok) (time: {time})] {prev} + + ) + }} + /> + ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action/action.tsx b/packages/plugin-rsc/examples/basic/src/routes/action/action.tsx new file mode 100644 index 00000000..5c6769a3 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action/action.tsx @@ -0,0 +1,16 @@ +'use server' + +let serverCounter = 0 + +export async function getServerCounter(): Promise { + return serverCounter +} + +export async function changeServerCounter(formData: FormData): Promise { + const TEST_UPDATE = 1 + serverCounter += Number(formData.get('change')) * TEST_UPDATE +} + +export async function resetServerCounter(): Promise { + serverCounter = 0 +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/action/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/action/server.tsx new file mode 100644 index 00000000..6bb0646f --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/action/server.tsx @@ -0,0 +1,15 @@ +import { + changeServerCounter, + getServerCounter, + resetServerCounter, +} from './action' + +export function ServerCounter() { + return ( +
+ + + +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/client.tsx new file mode 100644 index 00000000..237ff422 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/client.tsx @@ -0,0 +1,26 @@ +'use client' + +import React from 'react' + +export function ClientCounter(): React.ReactElement { + const [count, setCount] = React.useState(0) + return ( + + ) +} + +const noop = () => () => {} +export function Hydrated() { + const hydrated = React.useSyncExternalStore( + noop, + () => true, + () => false, + ) + return [hydrated: {hydrated ? 1 : 0}] +} + +export function UnusedClientReference() { + console.log('__unused_client_reference__') +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/deps/client-in-server/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/deps/client-in-server/client.tsx new file mode 100644 index 00000000..2e7dc9f2 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/deps/client-in-server/client.tsx @@ -0,0 +1,8 @@ +'use client' + +// @ts-ignore +import { TestContextValue } from '@vitejs/test-dep-client-in-server2/client' + +export function TestContextValueIndirect() { + return +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/deps/client-in-server/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/deps/client-in-server/server.tsx new file mode 100644 index 00000000..cb029357 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/deps/client-in-server/server.tsx @@ -0,0 +1,22 @@ +// @ts-ignore +import { TestClientInServerDep } from '@vitejs/test-dep-client-in-server/server' +// @ts-ignore +import { TestContextProviderInServer } from '@vitejs/test-dep-client-in-server2/server' +import { TestContextValueIndirect } from './client' + +export function TestClientInServer() { + return ( +
+
+ [test-client-in-server-dep: ] +
+
+ [test-provider-in-server-dep:{' '} + + + + ] +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/deps/server-in-client/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/deps/server-in-client/client.tsx new file mode 100644 index 00000000..0a0c7335 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/deps/server-in-client/client.tsx @@ -0,0 +1,6 @@ +// @ts-ignore +import { TestClient } from '@vitejs/test-dep-server-in-client/client' + +export function TestServerInClient() { + return +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/deps/server-in-server/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/deps/server-in-server/server.tsx new file mode 100644 index 00000000..5d15747b --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/deps/server-in-server/server.tsx @@ -0,0 +1,6 @@ +// @ts-ignore +import { ServerCounter } from '@vitejs/test-dep-server-in-server/server' + +export function TestServerInServer() { + return +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/hmr-client-dep/client-dep.tsx b/packages/plugin-rsc/examples/basic/src/routes/hmr-client-dep/client-dep.tsx new file mode 100644 index 00000000..adb03ac2 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/hmr-client-dep/client-dep.tsx @@ -0,0 +1,3 @@ +export function ClientDep() { + return <>[ok] +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/hmr-client-dep/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/hmr-client-dep/client.tsx new file mode 100644 index 00000000..1812aadb --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/hmr-client-dep/client.tsx @@ -0,0 +1,16 @@ +'use client' + +import React from 'react' +import { ClientDep } from './client-dep' + +export function TestHmrClientDep() { + const [count, setCount] = React.useState(0) + return ( +
+ + +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/module-invalidation/server-dep.tsx b/packages/plugin-rsc/examples/basic/src/routes/module-invalidation/server-dep.tsx new file mode 100644 index 00000000..64c930ab --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/module-invalidation/server-dep.tsx @@ -0,0 +1,3 @@ +export const dep = { + value: 0, +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/module-invalidation/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/module-invalidation/server.tsx new file mode 100644 index 00000000..90062572 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/module-invalidation/server.tsx @@ -0,0 +1,18 @@ +import { dep } from './server-dep' + +export function TestModuleInvalidationServer() { + return ( +
+
{ + 'use server' + dep.value ^= 1 + }} + > + + [dep: {dep.value}] +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/payload/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/payload/client.tsx new file mode 100644 index 00000000..788de21f --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/payload/client.tsx @@ -0,0 +1,29 @@ +'use client' + +export function TestPayloadClient(props: { + test1?: any + test2?: any + test3?: any + test4?: any +}) { + const results = { + test1: props.test1 === '🙂', + test2: props.test2 === "", + test3: + props.test3 instanceof Uint8Array && + isSameArray(props.test3, new TextEncoder().encode('🔥').reverse()), + test4: props.test4 === '&><\u2028\u2029', + } + const formatted = Object.entries(results) + .map(([k, v]) => `${k}: ${String(v)}`) + .join(', ') + return <>{formatted} +} + +function isSameArray(x: Uint8Array, y: Uint8Array) { + if (x.length !== y.length) return false + for (let i = 0; i < x.length; i++) { + if (x[i] !== y[i]) return false + } + return true +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/payload/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/payload/server.tsx new file mode 100644 index 00000000..253a6b71 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/payload/server.tsx @@ -0,0 +1,24 @@ +import { TestPayloadClient } from './client' + +export function TestPayloadServer(props: { url: URL }) { + return ( +
+ test-payload (binary):{' '} + + throw new Error('boom')"} + test3={ + // disabled by default so that it won't break Stackblitz demo + // https://github.com/stackblitz/webcontainer-core/issues/1861 + props.url.searchParams.has('test-payload-binary') + ? // reverse to have non-utf8 binary data + new TextEncoder().encode('🔥').reverse() + : null + } + test4={'&><\u2028\u2029'} + /> + +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/root.tsx b/packages/plugin-rsc/examples/basic/src/routes/root.tsx new file mode 100644 index 00000000..352d97ad --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/root.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { + TestServerActionBindAction, + TestServerActionBindClient, + TestServerActionBindReset, + TestServerActionBindSimple, +} from './action-bind/server' +import { TestServerActionError } from './action-error/server' +import { + TestActionFromClient, + TestUseActionState, +} from './action-from-client/client' +import { TestActionStateServer } from './action-state/server' +import { ServerCounter } from './action/server' +import { ClientCounter, Hydrated } from './client' +import { TestClientInServer } from './deps/client-in-server/server' +import { TestServerInClient } from './deps/server-in-client/client' +import { TestServerInServer } from './deps/server-in-server/server' +import { TestHmrClientDep } from './hmr-client-dep/client' +import { TestModuleInvalidationServer } from './module-invalidation/server' +import { TestPayloadServer } from './payload/server' +import { TestSerializationServer } from './serialization/server' +import { TestCssClientNoSsr } from './style-client-no-ssr/server' +import { TestStyleClient } from './style-client/client' +import { TestStyleServer } from './style-server/server' +import { TestTailwindClient } from './tailwind/client' +import { TestTailwindServer } from './tailwind/server' +import { TestTemporaryReference } from './temporary-reference/client' +import { TestUseCache } from './use-cache/server' + +export function Root(props: { url: URL }) { + return ( + + + vite-rsc + {import.meta.viteRsc.loadCss('/src/routes/root.tsx')} + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +function TestReplayConsoleLogs(props: { url: URL }) { + if (props.url.search.includes('test-replay-console-logs')) { + console.log('[test-replay-console-logs]') + } + return test-replayConsoleLogs +} + +function TestSuspense(props: { url: URL }) { + if (props.url.search.includes('test-suspense')) { + const ms = Number(props.url.searchParams.get('test-suspense')) || 1000 + async function Inner() { + await new Promise((resolve) => setTimeout(resolve, ms)) + return
suspense-resolved
+ } + return ( +
+ suspense-fallback
}> + + + + ) + } + return test-suspense +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/serialization/action.tsx b/packages/plugin-rsc/examples/basic/src/routes/serialization/action.tsx new file mode 100644 index 00000000..69e96d66 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/serialization/action.tsx @@ -0,0 +1,6 @@ +'use server' + +export async function testSerializationAction() { + console.log('[test-serialization-action]') + return 'ok' +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx new file mode 100644 index 00000000..7815f807 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx @@ -0,0 +1,18 @@ +'use client' + +import React from 'react' + +export function TestSerializationClient(props: { action: () => Promise }) { + const [state, setState] = React.useState('?') + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx new file mode 100644 index 00000000..7930bf7a --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx @@ -0,0 +1,27 @@ +import { + createFromReadableStream, + renderToReadableStream, +} from '@vitejs/plugin-rsc/rsc' +import { testSerializationAction } from './action' +import { TestSerializationClient } from './client' + +export function TestSerializationServer() { + const original = + let serialized = renderToReadableStream(original) + // debug serialization + if (0) { + serialized = serialized + .pipeThrough(new TextDecoderStream()) + .pipeThrough( + new TransformStream({ + transform(chunk, controller) { + console.log('[test-serialization]', { chunk }) + controller.enqueue(chunk) + }, + }), + ) + .pipeThrough(new TextEncoderStream()) + } + const deserialized = createFromReadableStream(serialized) + return
test-serialization:{deserialized}
+} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client-no-ssr/client.css b/packages/plugin-rsc/examples/basic/src/routes/style-client-no-ssr/client.css new file mode 100644 index 00000000..96c36325 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client-no-ssr/client.css @@ -0,0 +1,3 @@ +.test-style-client-no-ssr { + color: rgb(0, 200, 100); +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client-no-ssr/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/style-client-no-ssr/client.tsx new file mode 100644 index 00000000..85754d47 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client-no-ssr/client.tsx @@ -0,0 +1,7 @@ +'use client' + +import './client.css' + +export function TestClient() { + return [test] +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client-no-ssr/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/style-client-no-ssr/server.tsx new file mode 100644 index 00000000..8e0f7304 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client-no-ssr/server.tsx @@ -0,0 +1,11 @@ +import { TestClient } from './client' + +export function TestCssClientNoSsr(props: { url: URL }) { + return ( +
+ test-client-style-no-ssr{' '} + show hide{' '} + {props.url.searchParams.has('test-client-style-no-ssr') && } +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client/client-dep.css b/packages/plugin-rsc/examples/basic/src/routes/style-client/client-dep.css new file mode 100644 index 00000000..a58f3cfc --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client/client-dep.css @@ -0,0 +1,3 @@ +.test-style-client-dep { + color: rgb(255, 165, 0); +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client/client-dep.tsx b/packages/plugin-rsc/examples/basic/src/routes/style-client/client-dep.tsx new file mode 100644 index 00000000..8dcd56b1 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client/client-dep.tsx @@ -0,0 +1,5 @@ +import './client-dep.css' + +export function TestClientDep() { + return
test-style-client-dep
+} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client/client.css b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.css new file mode 100644 index 00000000..bd95cc0f --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.css @@ -0,0 +1,5 @@ +/* css imported by client references */ + +.test-style-client { + color: rgb(255, 165, 0); +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client/client.module.css b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.module.css new file mode 100644 index 00000000..7b8fea47 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.module.css @@ -0,0 +1,3 @@ +.client { + color: rgb(255, 165, 0); +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx new file mode 100644 index 00000000..bc715d67 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-client/client.tsx @@ -0,0 +1,17 @@ +'use client' + +import './client.css' +import { TestClientDep } from './client-dep' +import styles from './client.module.css' + +export function TestStyleClient() { + return ( + <> +
test-style-client
+
+ test-css-module-client +
+ + + ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-server/server.css b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.css new file mode 100644 index 00000000..480fa138 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.css @@ -0,0 +1,3 @@ +.test-style-server { + color: rgb(255, 165, 0); +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-server/server.module.css b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.module.css new file mode 100644 index 00000000..a391a735 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.module.css @@ -0,0 +1,3 @@ +.server { + color: rgb(255, 165, 0); +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx new file mode 100644 index 00000000..6684e6b5 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx @@ -0,0 +1,13 @@ +import './server.css' +import styles from './server.module.css' + +export function TestStyleServer() { + return ( + <> +
test-style-server
+
+ test-css-module-server +
+ + ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/tailwind/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/tailwind/client.tsx new file mode 100644 index 00000000..868bc0bb --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/tailwind/client.tsx @@ -0,0 +1,5 @@ +'use client' + +export function TestTailwindClient() { + return
test-tw-client
+} diff --git a/packages/plugin-rsc/examples/basic/src/routes/tailwind/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/tailwind/server.tsx new file mode 100644 index 00000000..b130e1ca --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/tailwind/server.tsx @@ -0,0 +1,3 @@ +export function TestTailwindServer() { + return
test-tw-server
+} diff --git a/packages/plugin-rsc/examples/basic/src/routes/temporary-reference/action.tsx b/packages/plugin-rsc/examples/basic/src/routes/temporary-reference/action.tsx new file mode 100644 index 00000000..c855de52 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/temporary-reference/action.tsx @@ -0,0 +1,10 @@ +'use server' + +export async function action(node: React.ReactNode) { + 'use server' + return ( + + [server {node}] + + ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/temporary-reference/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/temporary-reference/client.tsx new file mode 100644 index 00000000..69dd7e9f --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/temporary-reference/client.tsx @@ -0,0 +1,21 @@ +'use client' + +import React from 'react' +import { action } from './action' + +export function TestTemporaryReference() { + const [result, setResult] = React.useState('(none)') + + return ( +
+
{ + setResult(await action([client])) + }} + > + +
+
result: {result}
+
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx new file mode 100644 index 00000000..d90f7496 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx @@ -0,0 +1,105 @@ +import { revalidateCache } from '../../use-cache-runtime' + +export function TestUseCache() { + return ( + <> + + + + + ) +} + +function TestUseCacheFn() { + return ( +
{ + 'use server' + actionCount++ + const argument = formData.get('argument') + await testFn(argument) + if (argument === 'revalidate') { + revalidateCache(testFn) + } + }} + > + + + + (actionCount: {actionCount}, cacheFnCount: {cacheFnCount}) + +
+ ) +} + +let actionCount = 0 +let cacheFnCount = 0 + +async function testFn(..._args: unknown[]) { + 'use cache' + cacheFnCount++ +} + +function TestUseCacheComponent() { + // NOTE: wrapping with `span` (or any jsx) is crucial because + // raw string `children` would get included as cache key + // and thus causes `TestComponent` to be evaluated in each render. + return ( + + {new Date().toISOString()} + + ) +} + +async function TestComponent(props: { children?: React.ReactNode }) { + 'use cache' + return ( +
+ [test-use-cache-component]{' '} + + (static: {new Date().toISOString()}) + {' '} + + (dynamic: {props.children}) + +
+ ) +} + +async function TestUseCacheClosure() { + return ( +
+
{ + 'use server' + actionCount2++ + outerFnArg = formData.get('outer') as string + innerFnArg = formData.get('inner') as string + await outerFn(outerFnArg)(innerFnArg) + }} + > + + + +
+ + (actionCount: {actionCount2}, innerFnCount: {innerFnCount}) + +
+ ) +} + +function outerFn(outer: string) { + async function innerFn(inner: string) { + 'use cache' + innerFnCount++ + console.log({ outer, inner }) + } + return innerFn +} + +let outerFnArg = '' +let innerFnArg = '' +let innerFnCount = 0 +let actionCount2 = 0 diff --git a/packages/plugin-rsc/examples/basic/src/server.ssr.tsx b/packages/plugin-rsc/examples/basic/src/server.ssr.tsx new file mode 100644 index 00000000..65176949 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/server.ssr.tsx @@ -0,0 +1 @@ +export * from '@vitejs/plugin-rsc/extra/ssr' diff --git a/packages/plugin-rsc/examples/basic/src/server.tsx b/packages/plugin-rsc/examples/basic/src/server.tsx new file mode 100644 index 00000000..86b9234d --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/server.tsx @@ -0,0 +1,31 @@ +import './styles.css' +import { renderRequest } from '@vitejs/plugin-rsc/extra/rsc' + +export default async function handler(request: Request): Promise { + const url = new URL(request.url) + const { Root } = await import('./routes/root.tsx') + const root = ( + <> + {import.meta.viteRsc.loadCss()} + + + ) + const nonce = !process.env.NO_CSP ? crypto.randomUUID() : undefined + const response = await renderRequest(request, root, { nonce }) + if (nonce) { + response.headers.set( + 'content-security-policy', + `default-src 'self'; ` + + // `unsafe-eval` is required during dev since React uses eval for findSourceMapURL feature + `script-src 'self' 'nonce-${nonce}' ${ + import.meta.env.DEV ? `'unsafe-eval'` : `` + } ; ` + + `style-src 'self' 'nonce-${nonce}'; `, + ) + } + return response +} + +if (import.meta.hot) { + import.meta.hot.accept() +} diff --git a/packages/plugin-rsc/examples/basic/src/styles.css b/packages/plugin-rsc/examples/basic/src/styles.css new file mode 100644 index 00000000..9ea32262 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/styles.css @@ -0,0 +1,13 @@ +@import 'tailwindcss' source('./'); + +button { + @apply bg-gray-100 mx-1 px-2 border hover:bg-gray-200 active:bg-gray-300; +} + +input { + @apply mx-1 px-2 border; +} + +a { + @apply text-gray-500 underline hover:text-gray-700; +} diff --git a/packages/plugin-rsc/examples/basic/src/use-cache-runtime.tsx b/packages/plugin-rsc/examples/basic/src/use-cache-runtime.tsx new file mode 100644 index 00000000..44e01105 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/use-cache-runtime.tsx @@ -0,0 +1,94 @@ +import * as ReactRsc from '@vitejs/plugin-rsc/rsc' + +// based on +// https://github.com/vercel/next.js/pull/70435 +// https://github.com/vercel/next.js/blob/09a2167b0a970757606b7f91ff2d470f77f13f8c/packages/next/src/server/use-cache/use-cache-wrapper.ts + +const cachedFnMap = new WeakMap() +const cachedFnCacheEntries = new WeakMap< + Function, + Record> +>() + +export default function cacheWrapper(fn: (...args: any[]) => Promise) { + if (cachedFnMap.has(fn)) { + return cachedFnMap.get(fn)! + } + + async function cachedFn(...args: any[]): Promise { + let cacheEntries = cachedFnCacheEntries.get(cachedFn) + if (!cacheEntries) { + cacheEntries = {} + cachedFnCacheEntries.set(cachedFn, cacheEntries) + } + + // Serialize arguments to a cache key via `encodeReply` from `react-server-dom/client`. + // NOTE: using `renderToReadableStream` here for arguments serialization would end up + // serializing react elements (e.g. children props), which causes + // those arguments to be included as a cache key and it doesn't achieve + // "use cache static shell + dynamic children props" pattern. + // cf. https://nextjs.org/docs/app/api-reference/directives/use-cache#non-serializable-arguments + const clientTemporaryReferences = + ReactRsc.createClientTemporaryReferenceSet() + const encodedArguments = await ReactRsc.encodeReply(args, { + temporaryReferences: clientTemporaryReferences, + }) + const serializedCacheKey = await replyToCacheKey(encodedArguments) + + // cache `fn` result as stream + // (cache value is promise so that it dedupes concurrent async calls) + const entryPromise = (cacheEntries[serializedCacheKey] ??= (async () => { + const temporaryReferences = ReactRsc.createTemporaryReferenceSet() + const decodedArgs = await ReactRsc.decodeReply(encodedArguments, { + temporaryReferences, + }) + + // run the original function + const result = await fn(...decodedArgs) + + // serialize result to a ReadableStream + const stream = ReactRsc.renderToReadableStream(result, { + environmentName: 'Cache', + temporaryReferences, + }) + return new StreamCacher(stream) + })()) + + // deserialized cached stream + const stream = (await entryPromise).get() + const result = ReactRsc.createFromReadableStream(stream, { + environmentName: 'Cache', + replayConsoleLogs: true, + temporaryReferences: clientTemporaryReferences, + }) + return result + } + + cachedFnMap.set(fn, cachedFn) + + return cachedFn +} + +export function revalidateCache(cachedFn: Function) { + cachedFnCacheEntries.delete(cachedFn) +} + +class StreamCacher { + constructor(private stream: ReadableStream) {} + get(): ReadableStream { + const [returnStream, savedStream] = this.stream.tee() + this.stream = savedStream + return returnStream + } +} + +async function replyToCacheKey(reply: string | FormData) { + if (typeof reply === 'string') { + return reply + } + const buffer = await crypto.subtle.digest( + 'SHA-256', + await new Response(reply).arrayBuffer(), + ) + return btoa(String.fromCharCode(...new Uint8Array(buffer))) +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/client-in-server/client.js b/packages/plugin-rsc/examples/basic/test-dep/client-in-server/client.js new file mode 100644 index 00000000..6a7a8d1b --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/client-in-server/client.js @@ -0,0 +1,8 @@ +'use client' + +import React from 'react' + +export function TestClient() { + const [ok] = React.useState(() => true) + return React.createElement('span', null, String(ok)) +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/client-in-server/package.json b/packages/plugin-rsc/examples/basic/test-dep/client-in-server/package.json new file mode 100644 index 00000000..68ab7795 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/client-in-server/package.json @@ -0,0 +1,11 @@ +{ + "name": "@vitejs/test-dep-client-in-server", + "private": true, + "type": "module", + "exports": { + "./server": "./server.js" + }, + "peerDependencies": { + "react": "*" + } +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/client-in-server/server.js b/packages/plugin-rsc/examples/basic/test-dep/client-in-server/server.js new file mode 100644 index 00000000..e145d3c5 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/client-in-server/server.js @@ -0,0 +1,6 @@ +import React from 'react' +import { TestClient } from './client.js' + +export async function TestClientInServerDep() { + return React.createElement(TestClient) +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/client-in-server2/client.js b/packages/plugin-rsc/examples/basic/test-dep/client-in-server2/client.js new file mode 100644 index 00000000..8a09b673 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/client-in-server2/client.js @@ -0,0 +1,18 @@ +'use client' + +import React from 'react' + +const testContext = React.createContext() + +export function TestContextProvider(props) { + return React.createElement( + testContext.Provider, + { value: props.value }, + props.children, + ) +} + +export function TestContextValue() { + const value = React.useContext(testContext) + return React.createElement('span', null, String(value)) +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/client-in-server2/package.json b/packages/plugin-rsc/examples/basic/test-dep/client-in-server2/package.json new file mode 100644 index 00000000..fbc55fef --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/client-in-server2/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-dep-client-in-server2", + "private": true, + "type": "module", + "exports": { + "./server": "./server.js", + "./client": "./client.js" + }, + "peerDependencies": { + "react": "*" + } +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/client-in-server2/server.js b/packages/plugin-rsc/examples/basic/test-dep/client-in-server2/server.js new file mode 100644 index 00000000..323d0e03 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/client-in-server2/server.js @@ -0,0 +1,10 @@ +import React from 'react' +import { TestContextProvider } from './client.js' + +export function TestContextProviderInServer(props) { + return React.createElement( + TestContextProvider, + { value: props.value }, + props.children, + ) +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/server-in-client/client.js b/packages/plugin-rsc/examples/basic/test-dep/server-in-client/client.js new file mode 100644 index 00000000..bdb6c459 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/server-in-client/client.js @@ -0,0 +1,21 @@ +'use client' + +import React from 'react' +import { changeCounter } from './server.js' + +const h = React.createElement + +export function TestClient() { + const [count, setCount] = React.useState(() => '?') + + return h( + 'button', + { + 'data-testid': 'server-in-client', + onClick: async () => { + setCount(await changeCounter(1)) + }, + }, + `server-in-client: ${count}`, + ) +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/server-in-client/package.json b/packages/plugin-rsc/examples/basic/test-dep/server-in-client/package.json new file mode 100644 index 00000000..d172984a --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/server-in-client/package.json @@ -0,0 +1,11 @@ +{ + "name": "@vitejs/test-dep-server-in-client", + "private": true, + "type": "module", + "exports": { + "./client": "./client.js" + }, + "peerDependencies": { + "react": "*" + } +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/server-in-client/server.js b/packages/plugin-rsc/examples/basic/test-dep/server-in-client/server.js new file mode 100644 index 00000000..3e76f153 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/server-in-client/server.js @@ -0,0 +1,12 @@ +'use server' + +let counter = 0 + +export async function getCounter() { + return counter +} + +export async function changeCounter(change) { + counter += change + return counter +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/server-in-server/package.json b/packages/plugin-rsc/examples/basic/test-dep/server-in-server/package.json new file mode 100644 index 00000000..7a84ef53 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/server-in-server/package.json @@ -0,0 +1,11 @@ +{ + "name": "@vitejs/test-dep-server-in-server", + "private": true, + "type": "module", + "exports": { + "./server": "./server.js" + }, + "peerDependencies": { + "react": "*" + } +} diff --git a/packages/plugin-rsc/examples/basic/test-dep/server-in-server/server.js b/packages/plugin-rsc/examples/basic/test-dep/server-in-server/server.js new file mode 100644 index 00000000..bc553464 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/test-dep/server-in-server/server.js @@ -0,0 +1,19 @@ +import React from 'react' + +const h = React.createElement + +let counter = 0 + +export function ServerCounter() { + return h( + 'form', + { + 'data-testid': 'server-in-server', + action: async () => { + 'use server' + counter++ + }, + }, + h('button', null, `server-in-server: ${counter}`), + ) +} diff --git a/packages/plugin-rsc/examples/basic/tsconfig.json b/packages/plugin-rsc/examples/basic/tsconfig.json new file mode 100644 index 00000000..77438d9d --- /dev/null +++ b/packages/plugin-rsc/examples/basic/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client", "@vitejs/plugin-rsc/types"], + "jsx": "react-jsx" + } +} diff --git a/packages/plugin-rsc/examples/basic/vite.config.ts b/packages/plugin-rsc/examples/basic/vite.config.ts new file mode 100644 index 00000000..7eff64dc --- /dev/null +++ b/packages/plugin-rsc/examples/basic/vite.config.ts @@ -0,0 +1,178 @@ +import assert from 'node:assert' +import rsc, { transformHoistInlineDirective } from '@vitejs/plugin-rsc' +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' +import { type Plugin, defineConfig, parseAstAsync } from 'vite' +import inspect from 'vite-plugin-inspect' + +// log unhandled rejection to debug e2e failures +if (!(globalThis as any).__debugHandlerRegisterd) { + process.on('uncaughtException', (err) => { + console.error('⚠️⚠️⚠️ uncaughtException ⚠️⚠️⚠️', err) + }) + process.on('unhandledRejection', (err) => { + console.error('⚠️⚠️⚠️ unhandledRejection ⚠️⚠️⚠️', err) + }) + ;(globalThis as any).__debugHandlerRegisterd = true +} + +export default defineConfig({ + base: process.env.TEST_BASE ? '/custom-base/' : undefined, + clearScreen: false, + plugins: [ + tailwindcss(), + react(), + vitePluginUseCache(), + rsc({ + entries: { + client: './src/client.tsx', + ssr: './src/server.ssr.tsx', + rsc: './src/server.tsx', + }, + // disable auto css injection to manually test `loadCss` feature. + rscCssTransform: false, + ignoredPackageWarnings: [/@vitejs\/test-dep-/], + copyServerAssetsToClient: (fileName) => + fileName !== '__server_secret.txt', + }), + // avoid ecosystem CI fail due to vite-plugin-inspect compatibility + !process.env.ECOSYSTEM_CI && inspect(), + { + // test server restart scenario on e2e + name: 'test-api', + configureServer(server) { + server.middlewares.use((req, res, next) => { + const url = new URL(req.url!, 'http://localhost') + if (url.pathname === '/__test_restart') { + setTimeout(() => { + server.restart() + }, 10) + res.end('ok') + return + } + next() + }) + }, + }, + { + name: 'test-client-reference-tree-shaking', + enforce: 'post', + writeBundle(_options, bundle) { + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk') { + assert(!chunk.code.includes('__unused_client_reference__')) + } + } + }, + }, + { + name: 'test-server-assets-security', + buildStart() { + if (this.environment.name === 'rsc') { + this.emitFile({ + type: 'asset', + fileName: '__server_secret.txt', + source: '__server_secret', + }) + } + }, + writeBundle(_options, bundle) { + if (this.environment.name === 'rsc') { + assert(Object.keys(bundle).includes('__server_secret.txt')) + } else { + assert(!Object.keys(bundle).includes('__server_secret.txt')) + } + + const viteManifest = bundle['.vite/manifest.json'] + assert(viteManifest.type === 'asset') + assert(typeof viteManifest.source === 'string') + if (this.environment.name === 'rsc') { + assert(viteManifest.source.includes('src/server.tsx')) + assert(!viteManifest.source.includes('src/client.tsx')) + } + if (this.environment.name === 'client') { + assert(!viteManifest.source.includes('src/server.tsx')) + assert(viteManifest.source.includes('src/client.tsx')) + } + }, + }, + { + name: 'cf-build', + enforce: 'post', + apply: () => !!process.env.CF_BUILD, + configEnvironment() { + return { + keepProcessEnv: false, + define: { + 'process.env.NO_CSP': 'false', + }, + resolve: { + noExternal: true, + }, + } + }, + generateBundle() { + if (this.environment.name === 'rsc') { + this.emitFile({ + type: 'asset', + fileName: 'cloudflare.js', + source: `\ +import handler from './index.js'; +export default { fetch: handler }; +`, + }) + } + if (this.environment.name === 'client') { + // https://developers.cloudflare.com/workers/static-assets/headers/#custom-headers + this.emitFile({ + type: 'asset', + fileName: '_headers', + source: `\ +/favicon.ico + Cache-Control: public, max-age=3600, s-maxage=3600 +/assets/* + Cache-Control: public, max-age=31536000, immutable +`, + }) + } + }, + }, + ], + build: { + minify: false, + manifest: true, + }, + optimizeDeps: { + exclude: [ + '@vitejs/test-dep-client-in-server/client', + '@vitejs/test-dep-client-in-server2/client', + '@vitejs/test-dep-server-in-client/client', + ], + }, +}) as any + +function vitePluginUseCache(): Plugin[] { + return [ + { + name: 'use-cache', + async transform(code) { + if (!code.includes('use cache')) return + const ast = await parseAstAsync(code) + const result = transformHoistInlineDirective(code, ast, { + runtime: (value) => `__vite_rsc_cache(${value})`, + directive: 'use cache', + rejectNonAsyncFunction: true, + noExport: true, + }) + if (!result.output.hasChanged()) return + result.output.prepend( + `import __vite_rsc_cache from "/src/use-cache-runtime";`, + ) + return { + code: result.output.toString(), + map: result.output.generateMap({ hires: 'boundary' }), + } + }, + }, + ] +} diff --git a/packages/plugin-rsc/examples/basic/wrangler.jsonc b/packages/plugin-rsc/examples/basic/wrangler.jsonc new file mode 100644 index 00000000..d5cb469b --- /dev/null +++ b/packages/plugin-rsc/examples/basic/wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "https://www.unpkg.com/wrangler@4.13.0/config-schema.json", + "name": "vite-rsc-basic", + "main": "dist/rsc/cloudflare.js", + "assets": { + "directory": "dist/client" + }, + "workers_dev": true, + "compatibility_date": "2025-04-01", + "compatibility_flags": ["nodejs_als"] +} diff --git a/packages/plugin-rsc/examples/hono/package.json b/packages/plugin-rsc/examples/hono/package.json new file mode 100644 index 00000000..a33afc4c --- /dev/null +++ b/packages/plugin-rsc/examples/hono/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vitejs/plugin-rsc-examples-hono", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --app", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/plugin-rsc": "workspace:*", + "hono": "^4.7.5" + } +} diff --git a/packages/plugin-rsc/examples/hono/public/favicon.ico b/packages/plugin-rsc/examples/hono/public/favicon.ico new file mode 100644 index 00000000..4aff0766 Binary files /dev/null and b/packages/plugin-rsc/examples/hono/public/favicon.ico differ diff --git a/packages/plugin-rsc/examples/hono/src/client.tsx b/packages/plugin-rsc/examples/hono/src/client.tsx new file mode 100644 index 00000000..c734dae7 --- /dev/null +++ b/packages/plugin-rsc/examples/hono/src/client.tsx @@ -0,0 +1,36 @@ +import { fetchRSC } from '@vitejs/plugin-rsc/extra/browser' +import React from 'react' +import ReactDOM from 'react-dom/client' + +function main() { + const dom = document.getElementById('root')! + ReactDOM.createRoot(dom).render() +} + +function App() { + return ( +
+

hello client

+ +
+ ) +} + +function FetchRsc() { + const [rsc, setRsc] = React.useState(null) + + return ( +
+ + {rsc} +
+ ) +} + +main() diff --git a/packages/plugin-rsc/examples/hono/src/server.tsx b/packages/plugin-rsc/examples/hono/src/server.tsx new file mode 100644 index 00000000..3499a18c --- /dev/null +++ b/packages/plugin-rsc/examples/hono/src/server.tsx @@ -0,0 +1,34 @@ +import { renderRequest } from '@vitejs/plugin-rsc/extra/rsc' +import { Hono } from 'hono' + +const app = new Hono() + +app.get('/api/rsc', (c) => { + const el = ( +
+
Hono!
+
random: ${Math.random().toString(36).slice(2)}
+
+ ) + // TODO: request is irrelevant + return renderRequest(c.req.raw, el) +}) + +app.all('/', (c) => { + return renderRequest(c.req.raw, ) +}) + +function Document() { + return ( + + + vite-rsc + + +
+ + + ) +} + +export default app.fetch diff --git a/packages/plugin-rsc/examples/hono/tsconfig.json b/packages/plugin-rsc/examples/hono/tsconfig.json new file mode 100644 index 00000000..eeb2d95d --- /dev/null +++ b/packages/plugin-rsc/examples/hono/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "jsx": "react-jsx" + } +} diff --git a/packages/plugin-rsc/examples/hono/vite.config.ts b/packages/plugin-rsc/examples/hono/vite.config.ts new file mode 100644 index 00000000..fb5b0950 --- /dev/null +++ b/packages/plugin-rsc/examples/hono/vite.config.ts @@ -0,0 +1,20 @@ +import rsc from '@vitejs/plugin-rsc' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig({ + clearScreen: false, + plugins: [ + react(), + rsc({ + entries: { + client: './src/client.tsx', + rsc: './src/server.tsx', + ssr: '@vitejs/plugin-rsc/extra/ssr', + }, + }), + ], + build: { + minify: false, + }, +}) as any diff --git a/packages/plugin-rsc/examples/react-router/README.md b/packages/plugin-rsc/examples/react-router/README.md new file mode 100644 index 00000000..d560eb65 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/README.md @@ -0,0 +1,31 @@ +# rsc react-router + +https://vite-rsc-react-router.hiro18181.workers.dev + +Vite RSC example based on demo made by React router team with Parcel: + +- https://github.com/jacob-ebey/parcel-plugin-react-router/ +- https://github.com/jacob-ebey/experimental-parcel-react-router-starter +- https://github.com/remix-run/react-router/tree/rsc/playground/rsc-vite + +See also [`rsc-movies`](https://github.com/hi-ogawa/rsc-movies/). + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/hi-ogawa/vite-plugins/tree/main/packages/rsc/examples/react-router?file=src%2Froutes%2Froot.tsx) + +Or try it locally by: + +```sh +npx giget gh:hi-ogawa/vite-plugins/packages/rsc/examples/react-router my-app +cd my-app +npm i +npm run dev +npm run build +npm run preview + +# run on @cloudflare/vite-plugin and deploy. +# a separate configuration is found in ./cf/vite.config.ts +npm run cf-dev +npm run cf-build +npm run cf-preview +npm run cf-release +``` diff --git a/packages/plugin-rsc/examples/react-router/app/paper.css b/packages/plugin-rsc/examples/react-router/app/paper.css new file mode 100644 index 00000000..a4087ac5 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/paper.css @@ -0,0 +1,150 @@ +@theme { + --default-font-family: 'Patrick Hand SC', sans-serif; + --default-mono-font-family: 'Patrick Hand SC', sans-serif; + + --color-foreground: black; + --color-danger: rgb(167, 52, 45); + --color-secondary: rgb(11, 116, 213); + --color-success: rgb(134, 163, 97); + --color-warning: rgb(221, 205, 69); + --color-border: #cdcccb; + --color-border-active: rgba(0, 0, 0, 0.2); + + --color-paper-background: white; + --color-paper-border: #cdcccb; + --shadow-paper: -1px 5px 35px -9px rgba(0, 0, 0, 0.2); + + --shadow-btn: 15px 28px 25px -18px rgba(0, 0, 0, 0.2); + --shadow-btn-hover: 2px 8px 8px -5px rgba(0, 0, 0, 0.3); + --color-btn-border: black; + --btn-color-danger: var(--color-danger); + --btn-color-secondary: var(--color-secondary); + --btn-color-success: var(--color-success); + --btn-color-warning: var(--color-warning); +} + +@utility paper-border { + @apply border-2 border-border; + border-bottom-left-radius: 25px 115px; + border-bottom-right-radius: 155px 25px; + border-top-left-radius: 15px 225px; + border-top-right-radius: 25px 150px; +} + +@utility no-paper-border { + @apply border-0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +@utility paper-underline { + @apply border-b-3 border-[currentcolor]; + border-bottom-left-radius: 15px 3px; + border-bottom-right-radius: 15px 5px; + border-bottom-style: solid; +} + +@utility paper-underline-hover { + @apply paper-underline border-transparent; + @variant hover { + @apply border-[currentcolor]; + } +} + +@utility paper { + @apply border border-paper-border bg-paper-background p-8 shadow-paper; +} + +@utility breadcrumbs { + @apply flex flex-wrap gap-2; + & > * { + @apply inline-block after:text-lg after:content-[""] not-last:after:ml-2 not-last:after:text-foreground not-last:after:content-["/"]; + } + & > a { + @apply text-secondary; + } +} + +@utility btn { + @apply inline-block cursor-pointer bg-paper-background paper-border px-4 py-2 text-lg shadow-btn transition-[shadow_transition]; + + @variant active { + @apply border-border-active; + } + @variant hover { + @apply translate-y-1 shadow-btn-hover; + } + + &.btn-icon { + @apply aspect-square px-2 py-2; + & img, + & svg { + @apply h-7 w-7; + } + } +} + +@utility btn-* { + border-color: --value(--btn-color- *); + color: --value(--btn-color- *); +} + +@utility btn-sm { + @apply px-2 py-1 text-base; +} + +@utility btn-lg { + @apply px-6 py-3 text-2xl; +} + +@utility label { + @apply mb-1 block font-semibold; +} + +@utility input { + @apply paper-border px-3 py-2; + + @variant disabled { + @apply border-border-active; + } +} + +@utility checkbox { + @apply h-6 w-6 paper-border; + + @variant disabled { + @apply border-border-active; + } +} + +@utility select { + @apply paper-border px-3 py-2; + + @variant disabled { + @apply border-border-active; + } +} + +@layer base { + body { + @apply text-foreground; + } + + * { + @apply outline-secondary; + } +} + +@layer utilities { + .prose { + :where(u):not(:where([class~='not-prose'], [class~='not-prose'] *)) { + @apply paper-underline no-underline; + } + + :where(a):not(:where([class~='not-prose'], [class~='not-prose'] *)) { + @apply paper-underline-hover no-underline text-secondary; + } + } +} diff --git a/packages/plugin-rsc/examples/react-router/app/root.tsx b/packages/plugin-rsc/examples/react-router/app/root.tsx new file mode 100644 index 00000000..0c356b69 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/root.tsx @@ -0,0 +1,56 @@ +import './styles.css' +import { Link, Outlet } from 'react-router' +import { ServerHmr } from '../react-router-vite/server-hmr' +import { TestClientState, TestHydrated } from './routes/client' +import { DumpError, GlobalNavigationLoadingBar } from './routes/root.client' + +export function Layout({ children }: { children: React.ReactNode }) { + console.log('Layout') + return ( + + + + + React Router Vite + + +
+ +
+ + + {children} + + + ) +} + +export default function Component() { + console.log('Root') + return ( + <> + + + ) +} + +export function ErrorBoundary() { + return +} diff --git a/packages/plugin-rsc/examples/react-router/app/routes.ts b/packages/plugin-rsc/examples/react-router/app/routes.ts new file mode 100644 index 00000000..cd308775 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes.ts @@ -0,0 +1,6 @@ +import { type RouteConfig, index, route } from '@react-router/dev/routes' + +export default [ + index('routes/home.tsx'), + route('about', 'routes/about.tsx'), +] satisfies RouteConfig diff --git a/packages/plugin-rsc/examples/react-router/app/routes/about.tsx b/packages/plugin-rsc/examples/react-router/app/routes/about.tsx new file mode 100644 index 00000000..a4a076ca --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes/about.tsx @@ -0,0 +1,20 @@ +'use client' + +import React from 'react' + +export function Component() { + const [count, setCount] = React.useState(0) + + return ( +
+
+

About

+

This is the about page.

+

[test-style-home]

+ +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/react-router/app/routes/client.tsx b/packages/plugin-rsc/examples/react-router/app/routes/client.tsx new file mode 100644 index 00000000..679c9938 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes/client.tsx @@ -0,0 +1,22 @@ +'use client' + +import React from 'react' + +export function TestHydrated() { + const hydrated = React.useSyncExternalStore( + React.useCallback(() => () => {}, []), + () => true, + () => false, + ) + return [hydrated: {hydrated ? 1 : 0}] +} + +export function TestClientState() { + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/react-router/app/routes/home.actions.ts b/packages/plugin-rsc/examples/react-router/app/routes/home.actions.ts new file mode 100644 index 00000000..94e68629 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes/home.actions.ts @@ -0,0 +1,7 @@ +'use server' + +export async function sayHello(defaultName: string, formData: FormData) { + await new Promise((resolve) => setTimeout(resolve, 500)) + const name = formData.get('name') || defaultName + console.log(`Hello, ${name}`) +} diff --git a/packages/plugin-rsc/examples/react-router/app/routes/home.client.tsx b/packages/plugin-rsc/examples/react-router/app/routes/home.client.tsx new file mode 100644 index 00000000..6da32f10 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes/home.client.tsx @@ -0,0 +1,12 @@ +'use client' + +import { useFormStatus } from 'react-dom' + +export function PendingButton() { + const status = useFormStatus() + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/react-router/app/routes/home.css b/packages/plugin-rsc/examples/react-router/app/routes/home.css new file mode 100644 index 00000000..7204e2fd --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes/home.css @@ -0,0 +1,3 @@ +.test-style-home { + color: rgb(250, 150, 0); +} diff --git a/packages/plugin-rsc/examples/react-router/app/routes/home.tsx b/packages/plugin-rsc/examples/react-router/app/routes/home.tsx new file mode 100644 index 00000000..d632fe29 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes/home.tsx @@ -0,0 +1,56 @@ +namespace Route { + export type LoaderArgs = any + export type ComponentProps = any +} + +import { sayHello } from './home.actions.ts' +import { PendingButton } from './home.client.tsx' +import './home.css' +import { TestActionStateServer } from './test-action-state/server.tsx' + +export function loader({ request }: Route.LoaderArgs) { + const url = new URL(request.url) + const name = url.searchParams.get('name') + return { name: name || 'Unknown' } +} + +const Component = ({ loaderData }: Route.ComponentProps) => { + return ( +
+
+

Home

+

This is the home page.

+ [test-style-home] +
+          loaderData: {JSON.stringify(loaderData)}
+        
+

Server Action

+
+
+ + +
+
+ +
+
+
+ +
+
+
+ ) +} + +export default Component diff --git a/packages/plugin-rsc/examples/react-router/app/routes/root.client.tsx b/packages/plugin-rsc/examples/react-router/app/routes/root.client.tsx new file mode 100644 index 00000000..be3a8e3d --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes/root.client.tsx @@ -0,0 +1,44 @@ +'use client' + +import { useNavigation, useRouteError } from 'react-router' + +export function GlobalNavigationLoadingBar() { + const navigation = useNavigation() + + if (navigation.state === 'idle') return null + + return ( +
+
+
+ ) +} + +export function DumpError() { + const error = useRouteError() + const message = + error instanceof Error ? ( +
+
+          {JSON.stringify(
+            {
+              ...error,
+              name: error.name,
+              message: error.message,
+            },
+            null,
+            2,
+          )}
+        
+ {error.stack &&
{error.stack}
} +
+ ) : ( +
Unknown Error
+ ) + return ( + <> +

Oooops

+
{message}
+ + ) +} diff --git a/packages/plugin-rsc/examples/react-router/app/routes/test-action-state/client.tsx b/packages/plugin-rsc/examples/react-router/app/routes/test-action-state/client.tsx new file mode 100644 index 00000000..520dab49 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes/test-action-state/client.tsx @@ -0,0 +1,19 @@ +'use client' + +import React from 'react' + +export function TestActionStateClient(props: { + action: (prev: React.ReactNode) => Promise +}) { + const [state, formAction, isPending] = React.useActionState( + props.action, + null, + ) + + return ( +
+ + {isPending ? 'pending...' : state} +
+ ) +} diff --git a/packages/plugin-rsc/examples/react-router/app/routes/test-action-state/server.tsx b/packages/plugin-rsc/examples/react-router/app/routes/test-action-state/server.tsx new file mode 100644 index 00000000..128186e2 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/routes/test-action-state/server.tsx @@ -0,0 +1,20 @@ +import { TestActionStateClient } from './client' + +// Test case based on +// https://github.com/remix-run/react-router/issues/13882 + +export function TestActionStateServer({ message }: { message: string }) { + return ( + { + 'use server' + await new Promise((resolve) => setTimeout(resolve, 200)) + return ( + + [(ok) ({message})] {prev} + + ) + }} + /> + ) +} diff --git a/packages/plugin-rsc/examples/react-router/app/styles.css b/packages/plugin-rsc/examples/react-router/app/styles.css new file mode 100644 index 00000000..e1a22e4e --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/app/styles.css @@ -0,0 +1,32 @@ +@import 'tailwindcss'; +@plugin "@tailwindcss/typography"; + +@import './paper.css'; + +@theme { + --animate-progress: progress 1s infinite linear; + + @keyframes progress { + 0% { + transform: translateX(0) scaleX(0); + } + 40% { + transform: translateX(0) scaleX(0.4); + } + 100% { + transform: translateX(100%) scaleX(0.5); + } + } +} + +@utility vt-name { + view-transition-name: var(--vt-name); +} + +@utility no-vt { + view-transition-name: none; +} + +@view-transition { + navigation: auto; +} diff --git a/packages/plugin-rsc/examples/react-router/cf/entry.rsc.tsx b/packages/plugin-rsc/examples/react-router/cf/entry.rsc.tsx new file mode 100644 index 00000000..b194dfeb --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/cf/entry.rsc.tsx @@ -0,0 +1,13 @@ +import { fetchServer } from '../react-router-vite/entry.rsc' + +console.log('[debug:cf-rsc-entry]') + +export default { + fetch(request: Request) { + return fetchServer(request) + }, +} + +if (import.meta.hot) { + import.meta.hot.accept() +} diff --git a/packages/plugin-rsc/examples/react-router/cf/entry.ssr.tsx b/packages/plugin-rsc/examples/react-router/cf/entry.ssr.tsx new file mode 100644 index 00000000..9a570e70 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/cf/entry.ssr.tsx @@ -0,0 +1,13 @@ +import handler from '../react-router-vite/entry.ssr' + +console.log('[debug:cf-ssr-entry]') + +// TODO: +// shouldn't "entry.rsc.tsx" be the main server entry +// and optionally call "entry.ssr.tsx" only for rendering html? + +export default { + fetch(request: Request, env: any) { + return handler(request, (request) => env.RSC.fetch(request)) + }, +} diff --git a/packages/plugin-rsc/examples/react-router/cf/vite.config.ts b/packages/plugin-rsc/examples/react-router/cf/vite.config.ts new file mode 100644 index 00000000..c4348cec --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/cf/vite.config.ts @@ -0,0 +1,72 @@ +import { cloudflare } from '@cloudflare/vite-plugin' +import rsc from '@vitejs/plugin-rsc' +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' +import inspect from 'vite-plugin-inspect' +import { reactRouter } from '../react-router-vite/plugin' + +export default defineConfig({ + clearScreen: false, + build: { + minify: false, + }, + plugins: [ + tailwindcss(), + react(), + reactRouter(), + rsc({ + entries: { + client: './react-router-vite/entry.browser.tsx', + }, + serverHandler: false, + }), + inspect(), + cloudflare({ + configPath: './cf/wrangler.ssr.jsonc', + viteEnvironment: { + name: 'ssr', + }, + auxiliaryWorkers: [ + { + configPath: './cf/wrangler.rsc.jsonc', + viteEnvironment: { + name: 'rsc', + }, + }, + ], + }), + { + name: 'react-router-fixup', + transform(code) { + if (code.includes(`import { AsyncLocalStorage } from 'async_hooks';`)) { + code = code.replaceAll('async_hooks', 'node:async_hooks') + code = code.replaceAll( + `global.___reactRouterServerStorage___`, + `globalThis.___reactRouterServerStorage___`, + ) + return code + } + }, + }, + ], + environments: { + client: { + optimizeDeps: { + include: ['react-router', 'react-router/internal/react-server-client'], + }, + }, + ssr: { + optimizeDeps: { + include: ['react-router > cookie', 'react-router > set-cookie-parser'], + exclude: ['react-router'], + }, + }, + rsc: { + optimizeDeps: { + include: ['react-router > cookie', 'react-router > set-cookie-parser'], + exclude: ['react-router'], + }, + }, + }, +}) diff --git a/packages/plugin-rsc/examples/react-router/cf/wrangler.rsc.jsonc b/packages/plugin-rsc/examples/react-router/cf/wrangler.rsc.jsonc new file mode 100644 index 00000000..68270a8b --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/cf/wrangler.rsc.jsonc @@ -0,0 +1,8 @@ +{ + "$schema": "https://www.unpkg.com/wrangler@4.19.1/config-schema.json", + "name": "vite-rsc-react-router-rsc", + "main": "./entry.rsc.tsx", + "workers_dev": true, + "compatibility_date": "2025-04-01", + "compatibility_flags": ["nodejs_als"] +} diff --git a/packages/plugin-rsc/examples/react-router/cf/wrangler.ssr.jsonc b/packages/plugin-rsc/examples/react-router/cf/wrangler.ssr.jsonc new file mode 100644 index 00000000..bfc7066b --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/cf/wrangler.ssr.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "https://www.unpkg.com/wrangler@4.19.1/config-schema.json", + "name": "vite-rsc-react-router", + "main": "./entry.ssr.tsx", + "workers_dev": true, + "services": [{ "binding": "RSC", "service": "vite-rsc-react-router-rsc" }], + "compatibility_date": "2025-04-01", + "compatibility_flags": ["nodejs_als"] +} diff --git a/packages/plugin-rsc/examples/react-router/package.json b/packages/plugin-rsc/examples/react-router/package.json new file mode 100644 index 00000000..c3e66589 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/package.json @@ -0,0 +1,34 @@ +{ + "name": "@vitejs/plugin-rsc-examples-react-router", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --app", + "preview": "vite preview", + "cf-dev": "vite -c ./cf/vite.config.ts", + "cf-build": "vite -c ./cf/vite.config.ts build", + "cf-preview": "vite -c ./cf/vite.config.ts preview", + "cf-release": "wrangler deploy -c dist/rsc/wrangler.json && wrangler deploy" + }, + "dependencies": { + "@vitejs/plugin-rsc": "latest", + "react": "latest", + "react-dom": "latest", + "react-router": "0.0.0-experimental-23decd7bc" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.8.0", + "@react-router/dev": "0.0.0-experimental-23decd7bc", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.1.4", + "@types/react": "latest", + "@types/react-dom": "latest", + "@vitejs/plugin-react": "latest", + "tailwindcss": "^4.1.4", + "vite": "^7.0.2", + "vite-plugin-inspect": "^11.2.0", + "wrangler": "^4.22.0" + } +} diff --git a/packages/plugin-rsc/examples/react-router/public/favicon.ico b/packages/plugin-rsc/examples/react-router/public/favicon.ico new file mode 100644 index 00000000..5dbdfcdd Binary files /dev/null and b/packages/plugin-rsc/examples/react-router/public/favicon.ico differ diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.browser.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.browser.tsx new file mode 100644 index 00000000..fcc8b3cc --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.browser.tsx @@ -0,0 +1,38 @@ +import { + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + setServerCallback, +} from '@vitejs/plugin-rsc/browser' +import * as React from 'react' +import { hydrateRoot } from 'react-dom/client' +import { + unstable_RSCHydratedRouter as RSCHydratedRouter, + type unstable_RSCPayload as RSCPayload, + unstable_createCallServer as createCallServer, + unstable_getRSCStream as getRSCStream, +} from 'react-router' + +setServerCallback( + createCallServer({ + createFromReadableStream, + encodeReply, + createTemporaryReferenceSet, + }), +) + +createFromReadableStream(getRSCStream()).then( + (payload: RSCPayload) => { + React.startTransition(() => { + hydrateRoot( + document, + + + , + ) + }) + }, +) diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx new file mode 100644 index 00000000..8583030c --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx @@ -0,0 +1,31 @@ +import { + createTemporaryReferenceSet, + decodeAction, + decodeReply, + loadServerAction, + renderToReadableStream, +} from '@vitejs/plugin-rsc/rsc' +import { unstable_matchRSCServerRequest as matchRSCServerRequest } from 'react-router' + +import routes from 'virtual:react-router-routes' + +export async function fetchServer(request: Request): Promise { + return await matchRSCServerRequest({ + createTemporaryReferenceSet, + decodeReply, + decodeAction, + loadServerAction, + request, + routes, + generateResponse(match, options) { + return new Response(renderToReadableStream(match.payload, options), { + status: match.statusCode, + headers: match.headers, + }) + }, + }) +} + +if (import.meta.hot) { + import.meta.hot.accept() +} diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.single.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.single.tsx new file mode 100644 index 00000000..5aff3e4e --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.single.tsx @@ -0,0 +1,8 @@ +import baseHandler from './entry.ssr' + +export default async function handler(request: Request) { + const rsc = await import.meta.viteRsc.loadModule< + typeof import('./entry.rsc') + >('rsc', 'index') + return baseHandler(request, rsc.fetchServer) +} diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx new file mode 100644 index 00000000..4e226dc1 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx @@ -0,0 +1,27 @@ +import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr' +import * as ReactDomServer from 'react-dom/server.edge' +import { + unstable_RSCStaticRouter as RSCStaticRouter, + unstable_routeRSCServerRequest as routeRSCServerRequest, +} from 'react-router' + +export default async function handler( + request: Request, + fetchServer: (request: Request) => Promise, +): Promise { + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index') + return routeRSCServerRequest({ + request, + fetchServer, + createFromReadableStream: (body) => createFromReadableStream(body), + renderHTML(getPayload) { + return ReactDomServer.renderToReadableStream( + , + { + bootstrapScriptContent, + }, + ) + }, + }) +} diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/plugin.ts b/packages/plugin-rsc/examples/react-router/react-router-vite/plugin.ts new file mode 100644 index 00000000..b9850bc1 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/plugin.ts @@ -0,0 +1,123 @@ +import assert from 'node:assert/strict' +import path from 'node:path' +import type { Config } from '@react-router/dev/config' +import type { RouteConfigEntry } from '@react-router/dev/routes' +import { type Plugin, createIdResolver, runnerImport } from 'vite' + +export function reactRouter(): Plugin[] { + let idResolver: ReturnType + + return [ + { + name: 'react-router:config', + configResolved(config) { + idResolver = createIdResolver(config) + }, + resolveId(source) { + if (source === 'virtual:react-router-routes') { + return '\0' + source + } + }, + async load(id) { + if (id === '\0virtual:react-router-routes') { + const findFile = (id: string) => idResolver(this.environment, id) + const config = await readReactRouterConfig(findFile) + this.addWatchFile(config.configFile) + this.addWatchFile(config.routesFile) + const code = generateRoutesCode(config) + return code + } + }, + }, + ] +} + +async function readReactRouterConfig( + findFile: (id: string) => Promise, +) { + // find react-router.config.ts + const configFile = await findFile('./react-router.config') + assert(configFile, "Cannot find 'react-router.config' file") + const configImport = await runnerImport<{ default: Config }>(configFile) + const appDirectory = path.resolve( + configImport.module.default.appDirectory ?? 'app', + ) + + // find routes.ts + const routesFile = await findFile(path.join(appDirectory, 'routes')) + assert(routesFile, "Cannot find 'routes' file") + const routesImport = await runnerImport<{ + default: RouteConfigEntry[] + }>(routesFile) + + // find root.tsx + const rootFile = await findFile(path.join(appDirectory, 'root')) + assert(rootFile, "Cannot find 'root' file") + + const routes = [ + { + id: 'root', + path: '', + file: rootFile, + children: routesImport.module.default, + }, + ] + + return { configFile, routesFile, appDirectory, routes } +} + +// copied from +// https://github.com/jacob-ebey/parcel-plugin-react-router/blob/9385be813534537dfb0fe640a3e5c5607be3b61d/packages/resolver/src/resolver.ts + +function generateRoutesCode(config: { + appDirectory: string + routes: RouteConfigEntry[] +}) { + let code = 'export default [' + const closeRouteSymbol = Symbol('CLOSE_ROUTE') + let stack: Array = [ + ...config.routes, + ] + while (stack.length > 0) { + const route = stack.pop() + if (!route) break + if (route === closeRouteSymbol) { + code += ']},' + continue + } + code += '{' + // TODO: route-module transform + code += `lazy: () => import(${JSON.stringify( + path.resolve(config.appDirectory, route.file), + )}),` + code += `id: ${JSON.stringify( + route.id || createRouteId(route.file, config.appDirectory), + )},` + if (typeof route.path === 'string') { + code += `path: ${JSON.stringify(route.path)},` + } + if (route.index) { + code += `index: true,` + } + if (route.caseSensitive) { + code += `caseSensitive: true,` + } + if (route.children) { + code += ['children:['] + stack.push(closeRouteSymbol) + stack.push(...[...route.children].reverse()) + } else { + code += '},' + } + } + code += '];\n' + + return code +} + +function createRouteId(file: string, appDirectory: string) { + return path + .relative(appDirectory, file) + .replace(/\\+/, '/') + .slice(0, -path.extname(file).length) +} diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/server-hmr.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/server-hmr.tsx new file mode 100644 index 00000000..0b1e799a --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/server-hmr.tsx @@ -0,0 +1,19 @@ +'use client' + +import React from 'react' +import { useNavigate } from 'react-router' + +export function ServerHmr() { + if (import.meta.hot) { + const navigate = useNavigate() + React.useEffect(() => { + const refetch = () => + navigate(window.location.pathname, { replace: true }) + import.meta.hot!.on('rsc:update', refetch) + return () => { + import.meta.hot!.off('rsc:update', refetch) + } + }, [navigate]) + } + return null +} diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/types.d.ts b/packages/plugin-rsc/examples/react-router/react-router-vite/types.d.ts new file mode 100644 index 00000000..2982ced9 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/types.d.ts @@ -0,0 +1,11 @@ +/// +/// + +declare module 'react-dom/server.edge' { + export * from 'react-dom/server' +} + +declare module 'virtual:react-router-routes' { + const routes: any + export default routes +} diff --git a/packages/plugin-rsc/examples/react-router/react-router.config.ts b/packages/plugin-rsc/examples/react-router/react-router.config.ts new file mode 100644 index 00000000..c7029a54 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/react-router.config.ts @@ -0,0 +1,5 @@ +import type { Config } from '@react-router/dev/config' + +export default { + appDirectory: './app', +} satisfies Config diff --git a/packages/plugin-rsc/examples/react-router/tsconfig.json b/packages/plugin-rsc/examples/react-router/tsconfig.json new file mode 100644 index 00000000..20b648a3 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "jsx": "react-jsx" + } +} diff --git a/packages/plugin-rsc/examples/react-router/vite.config.ts b/packages/plugin-rsc/examples/react-router/vite.config.ts new file mode 100644 index 00000000..d0245181 --- /dev/null +++ b/packages/plugin-rsc/examples/react-router/vite.config.ts @@ -0,0 +1,33 @@ +import rsc from '@vitejs/plugin-rsc' +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' +import inspect from 'vite-plugin-inspect' +import { reactRouter } from './react-router-vite/plugin' + +export default defineConfig({ + clearScreen: false, + build: { + minify: false, + }, + plugins: [ + tailwindcss(), + react(), + reactRouter(), + rsc({ + entries: { + client: './react-router-vite/entry.browser.tsx', + ssr: './react-router-vite/entry.ssr.single.tsx', + rsc: './react-router-vite/entry.rsc.tsx', + }, + serverHandler: { + environmentName: 'ssr', + entryName: 'index', + }, + }), + inspect(), + ], + optimizeDeps: { + include: ['react-router', 'react-router/internal/react-server-client'], + }, +}) as any diff --git a/packages/plugin-rsc/examples/ssg/README.md b/packages/plugin-rsc/examples/ssg/README.md new file mode 100644 index 00000000..c5da5218 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/README.md @@ -0,0 +1,15 @@ +# SSG + MDX example + +This example demonstrates: + +- Client component inside MDX +- MDX HMR +- Static site generation + +## usage + +```js +pnpm dev +pnpm build +pnpm preview +``` diff --git a/packages/plugin-rsc/examples/ssg/package.json b/packages/plugin-rsc/examples/ssg/package.json new file mode 100644 index 00000000..13e508cb --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/package.json @@ -0,0 +1,24 @@ +{ + "name": "@vitejs/plugin-rsc-examples-ssg", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/plugin-rsc": "latest", + "react": "latest", + "react-dom": "latest" + }, + "devDependencies": { + "@mdx-js/rollup": "^3.1.0", + "@types/react": "latest", + "@types/react-dom": "latest", + "@vitejs/plugin-react": "latest", + "vite-plugin-inspect": "latest" + } +} diff --git a/packages/plugin-rsc/examples/ssg/public/favicon.ico b/packages/plugin-rsc/examples/ssg/public/favicon.ico new file mode 100644 index 00000000..4aff0766 Binary files /dev/null and b/packages/plugin-rsc/examples/ssg/public/favicon.ico differ diff --git a/packages/plugin-rsc/examples/ssg/src/counter.tsx b/packages/plugin-rsc/examples/ssg/src/counter.tsx new file mode 100644 index 00000000..79444524 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/counter.tsx @@ -0,0 +1,11 @@ +'use client' + +import React from 'react' + +export function Counter() { + const [count, setCount] = React.useState(0) + + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/ssg/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/ssg/src/framework/entry.browser.tsx new file mode 100644 index 00000000..6ef285d8 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/framework/entry.browser.tsx @@ -0,0 +1,97 @@ +import * as ReactClient from '@vitejs/plugin-rsc/browser' +import { getRscStreamFromHtml } from '@vitejs/plugin-rsc/rsc-html-stream/browser' +import React from 'react' +import ReactDomClient from 'react-dom/client' +import { RSC_POSTFIX, type RscPayload } from './shared' + +async function hydrate(): Promise { + async function onNavigation() { + const url = new URL(window.location.href) + url.pathname = url.pathname + RSC_POSTFIX + const payload = await ReactClient.createFromFetch(fetch(url)) + setPayload(payload) + } + + const initialPayload = await ReactClient.createFromReadableStream( + getRscStreamFromHtml(), + ) + + let setPayload: (v: RscPayload) => void + + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload) + + React.useEffect(() => { + setPayload = (v) => React.startTransition(() => setPayload_(v)) + }, [setPayload_]) + + React.useEffect(() => { + return listenNavigation(() => onNavigation()) + }, []) + + return payload.root + } + + const browserRoot = ( + + + + ) + + ReactDomClient.hydrateRoot(document, browserRoot) + + if (import.meta.hot) { + import.meta.hot.on('rsc:update', () => { + window.history.replaceState({}, '', window.location.href) + }) + } +} + +function listenNavigation(onNavigation: () => void): () => void { + window.addEventListener('popstate', onNavigation) + + const oldPushState = window.history.pushState + window.history.pushState = function (...args) { + const res = oldPushState.apply(this, args) + onNavigation() + return res + } + + const oldReplaceState = window.history.replaceState + window.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args) + onNavigation() + return res + } + + function onClick(e: MouseEvent) { + let link = (e.target as Element).closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault() + history.pushState(null, '', link.href) + } + } + document.addEventListener('click', onClick) + + return () => { + document.removeEventListener('click', onClick) + window.removeEventListener('popstate', onNavigation) + window.history.pushState = oldPushState + window.history.replaceState = oldReplaceState + } +} + +hydrate() diff --git a/packages/plugin-rsc/examples/ssg/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/ssg/src/framework/entry.rsc.tsx new file mode 100644 index 00000000..d0d9d235 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/framework/entry.rsc.tsx @@ -0,0 +1,38 @@ +import * as ReactServer from '@vitejs/plugin-rsc/rsc' +import { Root, getStaticPaths } from '../root' +import { RSC_POSTFIX, type RscPayload } from './shared' + +export { getStaticPaths } + +export default async function handler(request: Request): Promise { + let url = new URL(request.url) + let isRscRequest = false + if (url.pathname.endsWith(RSC_POSTFIX)) { + isRscRequest = true + url.pathname = url.pathname.slice(0, -RSC_POSTFIX.length) + } + + const rscPayload: RscPayload = { root: } + const rscStream = ReactServer.renderToReadableStream(rscPayload) + + if (isRscRequest) { + return new Response(rscStream, { + headers: { + 'content-type': 'text/x-component;charset=utf-8', + vary: 'accept', + }, + }) + } + + const ssr = await import.meta.viteRsc.loadModule< + typeof import('./entry.ssr') + >('ssr', 'index') + const htmlStream = await ssr.renderHtml(rscStream) + + return new Response(htmlStream, { + headers: { + 'content-type': 'text/html;charset=utf-8', + vary: 'accept', + }, + }) +} diff --git a/packages/plugin-rsc/examples/ssg/src/framework/entry.ssr.tsx b/packages/plugin-rsc/examples/ssg/src/framework/entry.ssr.tsx new file mode 100644 index 00000000..45297825 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/framework/entry.ssr.tsx @@ -0,0 +1,29 @@ +import { injectRscStreamToHtml } from '@vitejs/plugin-rsc/rsc-html-stream/ssr' +import * as ReactClient from '@vitejs/plugin-rsc/ssr' +import React from 'react' +import * as ReactDomServer from 'react-dom/server.edge' +import type { RscPayload } from './shared' + +export async function renderHtml(rscStream: ReadableStream) { + const [rscStream1, rscStream2] = rscStream.tee() + + let payload: Promise + function SsrRoot() { + payload ??= ReactClient.createFromReadableStream(rscStream1) + const root = React.use(payload).root + return root + } + + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index') + + const htmlStream = await ReactDomServer.renderToReadableStream(, { + bootstrapScriptContent, + }) + // for SSG + await htmlStream.allReady + + let responseStream: ReadableStream = htmlStream + responseStream = responseStream.pipeThrough(injectRscStreamToHtml(rscStream2)) + return responseStream +} diff --git a/packages/plugin-rsc/examples/ssg/src/framework/shared.tsx b/packages/plugin-rsc/examples/ssg/src/framework/shared.tsx new file mode 100644 index 00000000..e602b35d --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/framework/shared.tsx @@ -0,0 +1,7 @@ +import type React from 'react' + +export const RSC_POSTFIX = '_.rsc' + +export type RscPayload = { + root: React.ReactNode +} diff --git a/packages/plugin-rsc/examples/ssg/src/posts/counter.mdx b/packages/plugin-rsc/examples/ssg/src/posts/counter.mdx new file mode 100644 index 00000000..1654ee8b --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/posts/counter.mdx @@ -0,0 +1,7 @@ +export const title = 'Counter in MDX' + +import { Counter } from '../counter' + +# Counter in MDX + + diff --git a/packages/plugin-rsc/examples/ssg/src/posts/oxc.mdx b/packages/plugin-rsc/examples/ssg/src/posts/oxc.mdx new file mode 100644 index 00000000..5cff86b8 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/posts/oxc.mdx @@ -0,0 +1,3 @@ +# Oxc + +The fastest JavaScript language toolchain! diff --git a/packages/plugin-rsc/examples/ssg/src/posts/rolldown.mdx b/packages/plugin-rsc/examples/ssg/src/posts/rolldown.mdx new file mode 100644 index 00000000..71e2931a --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/posts/rolldown.mdx @@ -0,0 +1,3 @@ +# Rolldown + +The fastest JavaScript bundler! diff --git a/packages/plugin-rsc/examples/ssg/src/posts/vite.mdx b/packages/plugin-rsc/examples/ssg/src/posts/vite.mdx new file mode 100644 index 00000000..b510d386 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/posts/vite.mdx @@ -0,0 +1,3 @@ +# Vite + +The build tool for the web! diff --git a/packages/plugin-rsc/examples/ssg/src/posts/vitest.mdx b/packages/plugin-rsc/examples/ssg/src/posts/vitest.mdx new file mode 100644 index 00000000..9b534e10 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/posts/vitest.mdx @@ -0,0 +1,3 @@ +# Vitest + +Next-generation test runner! diff --git a/packages/plugin-rsc/examples/ssg/src/react.d.ts b/packages/plugin-rsc/examples/ssg/src/react.d.ts new file mode 100644 index 00000000..d92ea675 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/react.d.ts @@ -0,0 +1,3 @@ +declare module 'react-dom/server.edge' { + export * from 'react-dom/server' +} diff --git a/packages/plugin-rsc/examples/ssg/src/root.tsx b/packages/plugin-rsc/examples/ssg/src/root.tsx new file mode 100644 index 00000000..cb3ecb12 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/src/root.tsx @@ -0,0 +1,70 @@ +import { Counter } from './counter' + +async function getPosts() { + let glob = import.meta.glob('./posts/*.mdx', { eager: true }) + glob = Object.fromEntries( + Object.entries(glob).map(([k, v]) => [ + k.slice('./posts'.length, -'.mdx'.length), + v, + ]), + ) + return glob +} + +export async function getStaticPaths() { + const posts = await getPosts() + return ['/', ...Object.keys(posts)] +} + +export async function Root({ url }: { url: URL }) { + const posts = await getPosts() + + async function RootContent() { + if (url.pathname === '/') { + return ( + + ) + } + + const module = posts[url.pathname] + if (!!module) { + const Component = (module as any).default + return + } + + // TODO: how to 404? + return

Not found

+ } + + return ( + + + + + RSC MDX SSG + + +
+

+ RSC + MDX + SSG +

+ + + Rendered at {new Date().toISOString()} + +
+
+ +
+ + + ) +} diff --git a/packages/plugin-rsc/examples/ssg/tsconfig.json b/packages/plugin-rsc/examples/ssg/tsconfig.json new file mode 100644 index 00000000..4c355ed3 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "erasableSyntaxOnly": true, + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client", "@vitejs/plugin-rsc/types"], + "jsx": "react-jsx" + } +} diff --git a/packages/plugin-rsc/examples/ssg/vite.config.ts b/packages/plugin-rsc/examples/ssg/vite.config.ts new file mode 100644 index 00000000..05807fd2 --- /dev/null +++ b/packages/plugin-rsc/examples/ssg/vite.config.ts @@ -0,0 +1,96 @@ +import assert from 'node:assert' +import fs from 'node:fs' +import path from 'node:path' +import { Readable } from 'node:stream' +import { pathToFileURL } from 'node:url' +import rsc from '@vitejs/plugin-rsc' +import mdx from '@mdx-js/rollup' +import react from '@vitejs/plugin-react' +import { type Plugin, type ResolvedConfig, defineConfig } from 'vite' +import inspect from 'vite-plugin-inspect' +import { RSC_POSTFIX } from './src/framework/shared' + +export default defineConfig((env) => ({ + plugins: [ + mdx(), + react(), + rsc({ + entries: { + client: './src/framework/entry.browser.tsx', + rsc: './src/framework/entry.rsc.tsx', + ssr: './src/framework/entry.ssr.tsx', + }, + serverHandler: env.isPreview ? false : undefined, + }), + rscSsgPlugin(), + inspect(), + ], +})) + +function rscSsgPlugin(): Plugin[] { + return [ + { + name: 'rsc-ssg', + config(_config, env) { + if (env.isPreview) { + return { + appType: 'mpa', + } + } + }, + // Use post ssr writeBundle to wait for app is fully built. + // On Vite 7, you can use `buildApp` hook instead. + writeBundle: { + order: 'post', + async handler() { + if (this.environment.name === 'ssr') { + const config = this.environment.getTopLevelConfig() + await renderStatic(config) + } + }, + }, + }, + ] +} + +async function renderStatic(config: ResolvedConfig) { + // import server entry + const entryPath = path.join(config.environments.rsc.build.outDir, 'index.js') + const entry: typeof import('./src/framework/entry.rsc') = await import( + pathToFileURL(entryPath).href + ) + + // entry provides a list of static paths + const staticPaths = await entry.getStaticPaths() + + // render rsc and html + const baseDir = config.environments.client.build.outDir + for (const htmlPath of staticPaths) { + config.logger.info('[vite-rsc:ssg] -> ' + htmlPath) + const rscPath = htmlPath + RSC_POSTFIX + const htmlResponse = await entry.default( + new Request(new URL(htmlPath, 'http://ssg.local')), + ) + assert.equal(htmlResponse.status, 200) + await fs.promises.writeFile( + path.join(baseDir, normalizeHtmlFilePath(htmlPath)), + Readable.fromWeb(htmlResponse.body as any), + ) + + const rscResponse = await entry.default( + new Request(new URL(rscPath, 'http://ssg.local')), + ) + assert.equal(rscResponse.status, 200) + await fs.promises.writeFile( + path.join(baseDir, rscPath), + Readable.fromWeb(rscResponse.body as any), + ) + } +} + +function normalizeHtmlFilePath(p: string) { + if (p.endsWith('/')) { + return p + 'index.html' + } + return p + '.html' +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/README.md b/packages/plugin-rsc/examples/starter-cf-single/README.md new file mode 100644 index 00000000..78d862b6 --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/README.md @@ -0,0 +1,23 @@ +# Vite + RSC + Cloudflare Workers + +https://vite-rsc-starter.hiro18181.workers.dev + +[examples/starter](https://github.com/hi-ogawa/vite-plugins/tree/main/packages/rsc/examples/starter) integrated with [`@cloudflare/vite-plugin`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare). + +The difference from [examples/react-router](https://github.com/hi-ogawa/vite-plugins/tree/main/packages/rsc/examples/react-router) is that this doesn't require two workers. + +- RSC environment always runs on Cloudflare Workers. +- During development, SSR environment runs as Vite's deafult Node environment. +- During production, SSR environment build output is directly imported into RSC environment build and both codes run on the same worker. + +Such communication mechanism is enabled via `rsc({ loadModuleDevProxy: true })` plugin option. + +```sh +# run dev server +npm run dev + +# build for production and preview +npm run build +npm run preview +npm run release +``` diff --git a/packages/plugin-rsc/examples/starter-cf-single/package.json b/packages/plugin-rsc/examples/starter-cf-single/package.json new file mode 100644 index 00000000..4398bd68 --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/package.json @@ -0,0 +1,24 @@ +{ + "name": "@vitejs/plugin-rsc-examples-starter-cf-single", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "release": "wrangler deploy" + }, + "dependencies": { + "@vitejs/plugin-rsc": "latest", + "react": "latest", + "react-dom": "latest" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.8.0", + "@types/react": "latest", + "@types/react-dom": "latest", + "@vitejs/plugin-react": "latest" + } +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/public/vite.svg b/packages/plugin-rsc/examples/starter-cf-single/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugin-rsc/examples/starter-cf-single/src/action.tsx b/packages/plugin-rsc/examples/starter-cf-single/src/action.tsx new file mode 100644 index 00000000..4fc55d65 --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/src/action.tsx @@ -0,0 +1,11 @@ +'use server' + +let serverCounter = 0 + +export async function getServerCounter() { + return serverCounter +} + +export async function updateServerCounter(change: number) { + serverCounter += change +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/src/assets/react.svg b/packages/plugin-rsc/examples/starter-cf-single/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugin-rsc/examples/starter-cf-single/src/client.tsx b/packages/plugin-rsc/examples/starter-cf-single/src/client.tsx new file mode 100644 index 00000000..29bb5d36 --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/src/client.tsx @@ -0,0 +1,13 @@ +'use client' + +import React from 'react' + +export function ClientCounter() { + const [count, setCount] = React.useState(0) + + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.browser.tsx new file mode 100644 index 00000000..9ba4ae4e --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.browser.tsx @@ -0,0 +1,127 @@ +import * as ReactClient from '@vitejs/plugin-rsc/browser' +import { getRscStreamFromHtml } from '@vitejs/plugin-rsc/rsc-html-stream/browser' +import React from 'react' +import * as ReactDOMClient from 'react-dom/client' +import type { RscPayload } from './entry.rsc' + +async function main() { + // stash `setPayload` function to trigger re-rendering + // from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr) + let setPayload: (v: RscPayload) => void + + // deserialize RSC stream back to React VDOM for CSR + const initialPayload = await ReactClient.createFromReadableStream( + // initial RSC stream is injected in SSR stream as + getRscStreamFromHtml(), + ) + + // browser root component to (re-)render RSC payload as state + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload) + + React.useEffect(() => { + setPayload = (v) => React.startTransition(() => setPayload_(v)) + }, [setPayload_]) + + // re-fetch/render on client side navigation + React.useEffect(() => { + return listenNavigation(() => fetchRscPayload()) + }, []) + + return payload.root + } + + // re-fetch RSC and trigger re-rendering + async function fetchRscPayload() { + const payload = await ReactClient.createFromFetch( + fetch(window.location.href), + ) + setPayload(payload) + } + + // register a handler which will be internally called by React + // on server function request after hydration. + ReactClient.setServerCallback(async (id, args) => { + const url = new URL(window.location.href) + const temporaryReferences = ReactClient.createTemporaryReferenceSet() + const payload = await ReactClient.createFromFetch( + fetch(url, { + method: 'POST', + body: await ReactClient.encodeReply(args, { temporaryReferences }), + headers: { + 'x-rsc-action': id, + }, + }), + { temporaryReferences }, + ) + setPayload(payload) + return payload.returnValue + }) + + // hydration + const browserRoot = ( + + + + ) + ReactDOMClient.hydrateRoot(document, browserRoot, { + formState: initialPayload.formState, + }) + + // implement server HMR by trigering re-fetch/render of RSC upon server code change + if (import.meta.hot) { + import.meta.hot.on('rsc:update', () => { + fetchRscPayload() + }) + } +} + +// a little helper to setup events interception for client side navigation +function listenNavigation(onNavigation: () => void) { + window.addEventListener('popstate', onNavigation) + + const oldPushState = window.history.pushState + window.history.pushState = function (...args) { + const res = oldPushState.apply(this, args) + onNavigation() + return res + } + + const oldReplaceState = window.history.replaceState + window.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args) + onNavigation() + return res + } + + function onClick(e: MouseEvent) { + let link = (e.target as Element).closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault() + history.pushState(null, '', link.href) + } + } + document.addEventListener('click', onClick) + + return () => { + document.removeEventListener('click', onClick) + window.removeEventListener('popstate', onNavigation) + window.history.pushState = oldPushState + window.history.replaceState = oldReplaceState + } +} + +main() diff --git a/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.rsc.tsx new file mode 100644 index 00000000..a3060d2f --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.rsc.tsx @@ -0,0 +1,95 @@ +import * as ReactServer from '@vitejs/plugin-rsc/rsc' +import type { ReactFormState } from 'react-dom/client' +import { Root } from '../root.tsx' + +export type RscPayload = { + root: React.ReactNode + returnValue?: unknown + formState?: ReactFormState +} + +async function handler(request: Request): Promise { + // handle server function request + const isAction = request.method === 'POST' + let returnValue: unknown | undefined + let formState: ReactFormState | undefined + let temporaryReferences: unknown | undefined + if (isAction) { + // x-rsc-action header exists when action is called via `ReactClient.setServerCallback`. + const actionId = request.headers.get('x-rsc-action') + if (actionId) { + const contentType = request.headers.get('content-type') + const body = contentType?.startsWith('multipart/form-data') + ? await request.formData() + : await request.text() + temporaryReferences = ReactServer.createTemporaryReferenceSet() + const args = await ReactServer.decodeReply(body, { temporaryReferences }) + const action = await ReactServer.loadServerAction(actionId) + returnValue = await action.apply(null, args) + } else { + // otherwise server function is called via `
` + // before hydration (e.g. when javascript is disabled). + // aka progressive enhancement. + const formData = await request.formData() + const decodedAction = await ReactServer.decodeAction(formData) + const result = await decodedAction() + formState = await ReactServer.decodeFormState(result, formData) + } + } + + // serialization from React VDOM tree to RSC stream. + // we render RSC stream after handling server function request + // so that new render reflects updated state from server function call + // to achieve single round trip to mutate and fetch from server. + const rscStream = ReactServer.renderToReadableStream({ + // in this example, we always render the same `` + root: , + returnValue, + formState, + }) + + // respond RSC stream without HTML rendering based on framework's convention. + // here we use request header `content-type`. + // additionally we allow `?__rsc` and `?__html` to easily view payload directly. + const url = new URL(request.url) + const isRscRequest = + (!request.headers.get('accept')?.includes('text/html') && + !url.searchParams.has('__html')) || + url.searchParams.has('__rsc') + + if (isRscRequest) { + return new Response(rscStream, { + headers: { + 'content-type': 'text/x-component;charset=utf-8', + vary: 'accept', + }, + }) + } + + const { renderHTML } = await import.meta.viteRsc.loadModule< + typeof import('./entry.ssr.tsx') + >('ssr', 'index') + const htmlStream = await renderHTML(rscStream, { + formState, + // allow quick simulation of javscript disabled browser + debugNojs: url.searchParams.has('__nojs'), + }) + + // respond html + return new Response(htmlStream, { + headers: { + 'Content-type': 'text/html', + vary: 'accept', + }, + }) +} + +export default { + fetch(request: Request) { + return handler(request) + }, +} + +if (import.meta.hot) { + import.meta.hot.accept() +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.ssr.tsx b/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.ssr.tsx new file mode 100644 index 00000000..c9e0fab9 --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/src/framework/entry.ssr.tsx @@ -0,0 +1,55 @@ +import { injectRscStreamToHtml } from '@vitejs/plugin-rsc/rsc-html-stream/ssr' // helper API +import * as ReactClient from '@vitejs/plugin-rsc/ssr' // RSC API +import React from 'react' +import type { ReactFormState } from 'react-dom/client' +import * as ReactDOMServer from 'react-dom/server.edge' +import type { RscPayload } from './entry.rsc' + +export type RenderHTML = typeof renderHTML + +export async function renderHTML( + rscStream: ReadableStream, + options?: { + formState?: ReactFormState + nonce?: string + debugNojs?: boolean + }, +) { + // duplicate one RSC stream into two. + // - one for SSR (ReactClient.createFromReadableStream below) + // - another for browser hydration payload by injecting . + const [rscStream1, rscStream2] = rscStream.tee() + + // deserialize RSC stream back to React VDOM + let payload: Promise + function SsrRoot() { + // deserialization needs to be kicked off inside ReactDOMServer context + // for ReactDomServer preinit/preloading to work + payload ??= ReactClient.createFromReadableStream(rscStream1) + return React.use(payload).root + } + + // render html (traditional SSR) + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index') + const htmlStream = await ReactDOMServer.renderToReadableStream(, { + bootstrapScriptContent: options?.debugNojs + ? undefined + : bootstrapScriptContent, + nonce: options?.nonce, + // no types + ...{ formState: options?.formState }, + }) + + let responseStream: ReadableStream = htmlStream + if (!options?.debugNojs) { + // initial RSC stream is injected in HTML stream as + responseStream = responseStream.pipeThrough( + injectRscStreamToHtml(rscStream2, { + nonce: options?.nonce, + }), + ) + } + + return responseStream +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/src/framework/react.d.ts b/packages/plugin-rsc/examples/starter-cf-single/src/framework/react.d.ts new file mode 100644 index 00000000..d92ea675 --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/src/framework/react.d.ts @@ -0,0 +1,3 @@ +declare module 'react-dom/server.edge' { + export * from 'react-dom/server' +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/src/index.css b/packages/plugin-rsc/examples/starter-cf-single/src/index.css new file mode 100644 index 00000000..f4d2128c --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/src/index.css @@ -0,0 +1,112 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 1rem; +} + +.read-the-docs { + color: #888; + text-align: left; +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/src/root.tsx b/packages/plugin-rsc/examples/starter-cf-single/src/root.tsx new file mode 100644 index 00000000..694d3fe7 --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/src/root.tsx @@ -0,0 +1,70 @@ +import './index.css' +import viteLogo from '/vite.svg' +import { getServerCounter, updateServerCounter } from './action.tsx' +import reactLogo from './assets/react.svg' +import { ClientCounter } from './client.tsx' + +export function Root() { + return ( + + + + + + Vite + RSC + + + + + + ) +} + +function App() { + return ( +
+ +

Vite + RSC

+
+ +
+
+ + + +
+
    +
  • + Edit src/client.tsx to test client HMR. +
  • +
  • + Edit src/root.tsx to test server HMR. +
  • +
  • + Visit{' '} + + /?__rsc + {' '} + to view RSC stream payload. +
  • +
  • + Visit{' '} + + /?__nojs + {' '} + to test server action without js enabled. +
  • +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/tsconfig.json b/packages/plugin-rsc/examples/starter-cf-single/tsconfig.json new file mode 100644 index 00000000..4c355ed3 --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "erasableSyntaxOnly": true, + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client", "@vitejs/plugin-rsc/types"], + "jsx": "react-jsx" + } +} diff --git a/packages/plugin-rsc/examples/starter-cf-single/vite.config.ts b/packages/plugin-rsc/examples/starter-cf-single/vite.config.ts new file mode 100644 index 00000000..4ca6437b --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/vite.config.ts @@ -0,0 +1,46 @@ +import { cloudflare } from '@cloudflare/vite-plugin' +import rsc from '@vitejs/plugin-rsc' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig((_env) => ({ + clearScreen: false, + build: { + minify: false, + }, + plugins: [ + react(), + rsc({ + entries: { + client: './src/framework/entry.browser.tsx', + ssr: './src/framework/entry.ssr.tsx', + }, + serverHandler: false, + loadModuleDevProxy: true, + }), + cloudflare({ + configPath: './wrangler.jsonc', + viteEnvironment: { + name: 'rsc', + }, + }), + ], + environments: { + rsc: { + build: { + rollupOptions: { + // ensure `default` export only in cloudflare entry output + preserveEntrySignatures: 'exports-only', + }, + }, + }, + ssr: { + keepProcessEnv: false, + build: { + // build `ssr` inside `rsc` directory so that + // wrangler can deploy self-contained `dist/rsc` + outDir: './dist/rsc/ssr', + }, + }, + }, +})) diff --git a/packages/plugin-rsc/examples/starter-cf-single/wrangler.jsonc b/packages/plugin-rsc/examples/starter-cf-single/wrangler.jsonc new file mode 100644 index 00000000..bbd479cb --- /dev/null +++ b/packages/plugin-rsc/examples/starter-cf-single/wrangler.jsonc @@ -0,0 +1,8 @@ +{ + "$schema": "https://www.unpkg.com/wrangler@4.19.1/config-schema.json", + "name": "vite-rsc-starter", + "main": "./src/framework/entry.rsc.tsx", + "workers_dev": true, + "compatibility_date": "2025-04-01", + "compatibility_flags": ["nodejs_als"] +} diff --git a/packages/plugin-rsc/examples/starter/README.md b/packages/plugin-rsc/examples/starter/README.md new file mode 100644 index 00000000..8031f926 --- /dev/null +++ b/packages/plugin-rsc/examples/starter/README.md @@ -0,0 +1,36 @@ +# Vite + RSC + +This example shows how to setup a React application with [Server Component](https://react.dev/reference/rsc/server-components) features on Vite using [`@vitejs/plugin-rsc`](https://github.com/hi-ogawa/vite-plugins/tree/main/packages/rsc). + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/hi-ogawa/vite-plugins/tree/main/packages/rsc/examples/starter) + +```sh +# run dev server +npm run dev + +# build for production and preview +npm run build +npm run preview +``` + +## API usages + +See [`@vitejs/plugin-rsc`](https://github.com/hi-ogawa/vite-plugins/tree/main/packages/rsc) for the documentation. + +- [`vite.config.ts`](./vite.config.ts) + - `@higoawa/vite-rsc/plugin` +- [`./src/framework/entry.rsc.tsx`](./src/framework/entry.rsc.tsx) + - `@vitejs/plugin-rsc/rsc` + - `import.meta.viteRsc.loadModule` +- [`./src/framework/entry.ssr.tsx`](./src/framework/entry.ssr.tsx) + - `@vitejs/plugin-rsc/ssr` + - `@vitejs/plugin-rsc/rsc-html-stream/ssr` + - `import.meta.viteRsc.loadBootstrapScriptContent` +- [`./src/framework/entry.browser.tsx`](./src/framework/entry.browser.tsx) + - `@vitejs/plugin-rsc/browser` + - `@vitejs/plugin-rsc/rsc-html-stream/browser` + +## Notes + +- [`./src/framework/entry.{browser,rsc,ssr}.tsx`](./src/framework) (with inline comments) provides an overview of how low level RSC (React flight) API can be used to build RSC framework. +- You can use [`vite-plugin-inspect`](https://github.com/antfu-collective/vite-plugin-inspect) to understand how `"use client"` and `"use server"` directives are transformed internally. diff --git a/packages/plugin-rsc/examples/starter/package.json b/packages/plugin-rsc/examples/starter/package.json new file mode 100644 index 00000000..91f3871a --- /dev/null +++ b/packages/plugin-rsc/examples/starter/package.json @@ -0,0 +1,23 @@ +{ + "name": "@vitejs/plugin-rsc-examples-starter", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/plugin-rsc": "latest", + "react": "latest", + "react-dom": "latest" + }, + "devDependencies": { + "@types/react": "latest", + "@types/react-dom": "latest", + "@vitejs/plugin-react": "latest", + "vite-plugin-inspect": "latest" + } +} diff --git a/packages/plugin-rsc/examples/starter/public/vite.svg b/packages/plugin-rsc/examples/starter/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/packages/plugin-rsc/examples/starter/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugin-rsc/examples/starter/src/action.tsx b/packages/plugin-rsc/examples/starter/src/action.tsx new file mode 100644 index 00000000..4fc55d65 --- /dev/null +++ b/packages/plugin-rsc/examples/starter/src/action.tsx @@ -0,0 +1,11 @@ +'use server' + +let serverCounter = 0 + +export async function getServerCounter() { + return serverCounter +} + +export async function updateServerCounter(change: number) { + serverCounter += change +} diff --git a/packages/plugin-rsc/examples/starter/src/assets/react.svg b/packages/plugin-rsc/examples/starter/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/packages/plugin-rsc/examples/starter/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugin-rsc/examples/starter/src/client.tsx b/packages/plugin-rsc/examples/starter/src/client.tsx new file mode 100644 index 00000000..29bb5d36 --- /dev/null +++ b/packages/plugin-rsc/examples/starter/src/client.tsx @@ -0,0 +1,13 @@ +'use client' + +import React from 'react' + +export function ClientCounter() { + const [count, setCount] = React.useState(0) + + return ( + + ) +} diff --git a/packages/plugin-rsc/examples/starter/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/starter/src/framework/entry.browser.tsx new file mode 100644 index 00000000..9ba4ae4e --- /dev/null +++ b/packages/plugin-rsc/examples/starter/src/framework/entry.browser.tsx @@ -0,0 +1,127 @@ +import * as ReactClient from '@vitejs/plugin-rsc/browser' +import { getRscStreamFromHtml } from '@vitejs/plugin-rsc/rsc-html-stream/browser' +import React from 'react' +import * as ReactDOMClient from 'react-dom/client' +import type { RscPayload } from './entry.rsc' + +async function main() { + // stash `setPayload` function to trigger re-rendering + // from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr) + let setPayload: (v: RscPayload) => void + + // deserialize RSC stream back to React VDOM for CSR + const initialPayload = await ReactClient.createFromReadableStream( + // initial RSC stream is injected in SSR stream as + getRscStreamFromHtml(), + ) + + // browser root component to (re-)render RSC payload as state + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload) + + React.useEffect(() => { + setPayload = (v) => React.startTransition(() => setPayload_(v)) + }, [setPayload_]) + + // re-fetch/render on client side navigation + React.useEffect(() => { + return listenNavigation(() => fetchRscPayload()) + }, []) + + return payload.root + } + + // re-fetch RSC and trigger re-rendering + async function fetchRscPayload() { + const payload = await ReactClient.createFromFetch( + fetch(window.location.href), + ) + setPayload(payload) + } + + // register a handler which will be internally called by React + // on server function request after hydration. + ReactClient.setServerCallback(async (id, args) => { + const url = new URL(window.location.href) + const temporaryReferences = ReactClient.createTemporaryReferenceSet() + const payload = await ReactClient.createFromFetch( + fetch(url, { + method: 'POST', + body: await ReactClient.encodeReply(args, { temporaryReferences }), + headers: { + 'x-rsc-action': id, + }, + }), + { temporaryReferences }, + ) + setPayload(payload) + return payload.returnValue + }) + + // hydration + const browserRoot = ( + + + + ) + ReactDOMClient.hydrateRoot(document, browserRoot, { + formState: initialPayload.formState, + }) + + // implement server HMR by trigering re-fetch/render of RSC upon server code change + if (import.meta.hot) { + import.meta.hot.on('rsc:update', () => { + fetchRscPayload() + }) + } +} + +// a little helper to setup events interception for client side navigation +function listenNavigation(onNavigation: () => void) { + window.addEventListener('popstate', onNavigation) + + const oldPushState = window.history.pushState + window.history.pushState = function (...args) { + const res = oldPushState.apply(this, args) + onNavigation() + return res + } + + const oldReplaceState = window.history.replaceState + window.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args) + onNavigation() + return res + } + + function onClick(e: MouseEvent) { + let link = (e.target as Element).closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault() + history.pushState(null, '', link.href) + } + } + document.addEventListener('click', onClick) + + return () => { + document.removeEventListener('click', onClick) + window.removeEventListener('popstate', onNavigation) + window.history.pushState = oldPushState + window.history.replaceState = oldReplaceState + } +} + +main() diff --git a/packages/plugin-rsc/examples/starter/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/starter/src/framework/entry.rsc.tsx new file mode 100644 index 00000000..b45e053e --- /dev/null +++ b/packages/plugin-rsc/examples/starter/src/framework/entry.rsc.tsx @@ -0,0 +1,103 @@ +import * as ReactServer from '@vitejs/plugin-rsc/rsc' +import type { ReactFormState } from 'react-dom/client' +import { Root } from '../root.tsx' + +// The schema of payload which is serialized into RSC stream on rsc environment +// and deserialized on ssr/client environments. +export type RscPayload = { + // this demo renders/serializes/deserizlies entire root html element + // but this mechanism can be changed to render/fetch different parts of components + // based on your own route conventions. + root: React.ReactNode + // server action return value of non-progressive enhancement case + returnValue?: unknown + // server action form state (e.g. useActionState) of progressive enhancement case + formState?: ReactFormState +} + +// the plugin by default assumes `rsc` entry having default export of request handler. +// however, how server entries are executed can be customized by registering +// own server handler e.g. `@cloudflare/vite-plugin`. +export default async function handler(request: Request): Promise { + // handle server function request + const isAction = request.method === 'POST' + let returnValue: unknown | undefined + let formState: ReactFormState | undefined + let temporaryReferences: unknown | undefined + if (isAction) { + // x-rsc-action header exists when action is called via `ReactClient.setServerCallback`. + const actionId = request.headers.get('x-rsc-action') + if (actionId) { + const contentType = request.headers.get('content-type') + const body = contentType?.startsWith('multipart/form-data') + ? await request.formData() + : await request.text() + temporaryReferences = ReactServer.createTemporaryReferenceSet() + const args = await ReactServer.decodeReply(body, { temporaryReferences }) + const action = await ReactServer.loadServerAction(actionId) + returnValue = await action.apply(null, args) + } else { + // otherwise server function is called via `
` + // before hydration (e.g. when javascript is disabled). + // aka progressive enhancement. + const formData = await request.formData() + const decodedAction = await ReactServer.decodeAction(formData) + const result = await decodedAction() + formState = await ReactServer.decodeFormState(result, formData) + } + } + + // serialization from React VDOM tree to RSC stream. + // we render RSC stream after handling server function request + // so that new render reflects updated state from server function call + // to achieve single round trip to mutate and fetch from server. + const rscStream = ReactServer.renderToReadableStream({ + // in this example, we always render the same `` + root: , + returnValue, + formState, + }) + + // respond RSC stream without HTML rendering based on framework's convention. + // here we use request header `content-type`. + // additionally we allow `?__rsc` and `?__html` to easily view payload directly. + const url = new URL(request.url) + const isRscRequest = + (!request.headers.get('accept')?.includes('text/html') && + !url.searchParams.has('__html')) || + url.searchParams.has('__rsc') + + if (isRscRequest) { + return new Response(rscStream, { + headers: { + 'content-type': 'text/x-component;charset=utf-8', + vary: 'accept', + }, + }) + } + + // Delegate to SSR environment for html rendering. + // The plugin provides `loadSsrModule` helper to allow loading SSR environment entry module + // in RSC environment. however this can be customized by implementing own runtime communication + // e.g. `@cloudflare/vite-plugin`'s service binding. + const ssrEntryModule = await import.meta.viteRsc.loadModule< + typeof import('./entry.ssr.tsx') + >('ssr', 'index') + const htmlStream = await ssrEntryModule.renderHTML(rscStream, { + formState, + // allow quick simulation of javscript disabled browser + debugNojs: url.searchParams.has('__nojs'), + }) + + // respond html + return new Response(htmlStream, { + headers: { + 'Content-type': 'text/html', + vary: 'accept', + }, + }) +} + +if (import.meta.hot) { + import.meta.hot.accept() +} diff --git a/packages/plugin-rsc/examples/starter/src/framework/entry.ssr.tsx b/packages/plugin-rsc/examples/starter/src/framework/entry.ssr.tsx new file mode 100644 index 00000000..129dbadf --- /dev/null +++ b/packages/plugin-rsc/examples/starter/src/framework/entry.ssr.tsx @@ -0,0 +1,53 @@ +import { injectRscStreamToHtml } from '@vitejs/plugin-rsc/rsc-html-stream/ssr' // helper API +import * as ReactClient from '@vitejs/plugin-rsc/ssr' // RSC API +import React from 'react' +import type { ReactFormState } from 'react-dom/client' +import * as ReactDOMServer from 'react-dom/server.edge' +import type { RscPayload } from './entry.rsc' + +export async function renderHTML( + rscStream: ReadableStream, + options: { + formState?: ReactFormState + nonce?: string + debugNojs?: boolean + }, +) { + // duplicate one RSC stream into two. + // - one for SSR (ReactClient.createFromReadableStream below) + // - another for browser hydration payload by injecting . + const [rscStream1, rscStream2] = rscStream.tee() + + // deserialize RSC stream back to React VDOM + let payload: Promise + function SsrRoot() { + // deserialization needs to be kicked off inside ReactDOMServer context + // for ReactDomServer preinit/preloading to work + payload ??= ReactClient.createFromReadableStream(rscStream1) + return React.use(payload).root + } + + // render html (traditional SSR) + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index') + const htmlStream = await ReactDOMServer.renderToReadableStream(, { + bootstrapScriptContent: options?.debugNojs + ? undefined + : bootstrapScriptContent, + nonce: options?.nonce, + // no types + ...{ formState: options?.formState }, + }) + + let responseStream: ReadableStream = htmlStream + if (!options?.debugNojs) { + // initial RSC stream is injected in HTML stream as + responseStream = responseStream.pipeThrough( + injectRscStreamToHtml(rscStream2, { + nonce: options?.nonce, + }), + ) + } + + return responseStream +} diff --git a/packages/plugin-rsc/examples/starter/src/framework/react.d.ts b/packages/plugin-rsc/examples/starter/src/framework/react.d.ts new file mode 100644 index 00000000..d92ea675 --- /dev/null +++ b/packages/plugin-rsc/examples/starter/src/framework/react.d.ts @@ -0,0 +1,3 @@ +declare module 'react-dom/server.edge' { + export * from 'react-dom/server' +} diff --git a/packages/plugin-rsc/examples/starter/src/index.css b/packages/plugin-rsc/examples/starter/src/index.css new file mode 100644 index 00000000..f4d2128c --- /dev/null +++ b/packages/plugin-rsc/examples/starter/src/index.css @@ -0,0 +1,112 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 1rem; +} + +.read-the-docs { + color: #888; + text-align: left; +} diff --git a/packages/plugin-rsc/examples/starter/src/root.tsx b/packages/plugin-rsc/examples/starter/src/root.tsx new file mode 100644 index 00000000..a42c6a92 --- /dev/null +++ b/packages/plugin-rsc/examples/starter/src/root.tsx @@ -0,0 +1,70 @@ +import './index.css' // css import is automatically injected in exported server components +import viteLogo from '/vite.svg' +import { getServerCounter, updateServerCounter } from './action.tsx' +import reactLogo from './assets/react.svg' +import { ClientCounter } from './client.tsx' + +export function Root() { + return ( + + + + + + Vite + RSC + + + + + + ) +} + +function App() { + return ( +
+ +

Vite + RSC

+
+ +
+
+ + + +
+
    +
  • + Edit src/client.tsx to test client HMR. +
  • +
  • + Edit src/root.tsx to test server HMR. +
  • +
  • + Visit{' '} + + /?__rsc + {' '} + to view RSC stream payload. +
  • +
  • + Visit{' '} + + /?__nojs + {' '} + to test server action without js enabled. +
  • +
+
+ ) +} diff --git a/packages/plugin-rsc/examples/starter/tsconfig.json b/packages/plugin-rsc/examples/starter/tsconfig.json new file mode 100644 index 00000000..4c355ed3 --- /dev/null +++ b/packages/plugin-rsc/examples/starter/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "erasableSyntaxOnly": true, + "allowImportingTsExtensions": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["vite/client", "@vitejs/plugin-rsc/types"], + "jsx": "react-jsx" + } +} diff --git a/packages/plugin-rsc/examples/starter/vite.config.ts b/packages/plugin-rsc/examples/starter/vite.config.ts new file mode 100644 index 00000000..99837202 --- /dev/null +++ b/packages/plugin-rsc/examples/starter/vite.config.ts @@ -0,0 +1,73 @@ +import rsc from '@vitejs/plugin-rsc' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' +// import inspect from "vite-plugin-inspect"; + +export default defineConfig({ + plugins: [ + rsc({ + // `entries` option is only a shorthand for specifying each `rollupOptions.input` below + // > entries: { rsc, ssr, client }, + // + // by default, the plugin setup request handler based on `default export` of `rsc` environment `rollupOptions.input.index`. + // This can be disabled when setting up own server handler e.g. `@cloudflare/vite-plugin`. + // > serverHandler: false + }), + + // use any of react plugins https://github.com/vitejs/vite-plugin-react + // to enable client component HMR + react(), + + // use https://github.com/antfu-collective/vite-plugin-inspect + // to understand internal transforms required for RSC. + // inspect(), + ], + + // specify entry point for each environment. + // (currently the plugin assumes `rollupOptions.input.index` for some features.) + environments: { + // `rsc` environment loads modules with `react-server` condition. + // this environment is responsible for: + // - RSC stream serialization (React VDOM -> RSC stream) + // - server functions handling + rsc: { + build: { + rollupOptions: { + input: { + index: './src/framework/entry.rsc.tsx', + }, + }, + }, + }, + + // `ssr` environment loads modules without `react-server` condition. + // this environment is responsible for: + // - RSC stream deserialization (RSC stream -> React VDOM) + // - traditional SSR (React VDOM -> HTML string/stream) + ssr: { + build: { + rollupOptions: { + input: { + index: './src/framework/entry.ssr.tsx', + }, + }, + }, + }, + + // client environment is used for hydration and client-side rendering + // this environment is responsible for: + // - RSC stream deserialization (RSC stream -> React VDOM) + // - traditional CSR (React VDOM -> Browser DOM tree mount/hydration) + // - refetch and re-render RSC + // - calling server functions + client: { + build: { + rollupOptions: { + input: { + index: './src/framework/entry.browser.tsx', + }, + }, + }, + }, + }, +}) diff --git a/packages/plugin-rsc/package.json b/packages/plugin-rsc/package.json new file mode 100644 index 00000000..cfb754b6 --- /dev/null +++ b/packages/plugin-rsc/package.json @@ -0,0 +1,70 @@ +{ + "name": "@vitejs/plugin-rsc", + "version": "0.4.10-alpha.1", + "description": "React Server Components (RSC) support for Vite.", + "keywords": [ + "vite", + "vite-plugin", + "react", + "react-server-components", + "rsc" + ], + "homepage": "https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc", + "repository": { + "type": "git", + "url": "git+https://github.com/vitejs/vite-plugin-react.git", + "directory": "packages/plugin-rsc" + }, + "license": "MIT", + "type": "module", + "exports": { + "./package.json": "./package.json", + "./types": "./types/index.d.ts", + ".": "./dist/index.js", + "./*": "./dist/*.js" + }, + "files": [ + "dist", + "types" + ], + "scripts": { + "test": "vitest", + "test-e2e": "playwright test --project=chromium", + "test-e2e-ci": "playwright test", + "tsc": "tsc -b ./tsconfig.json ./e2e/tsconfig.json ./examples/*/tsconfig.json", + "tsc-dev": "pnpm tsc --watch --preserveWatchOutput", + "dev": "tsdown --sourcemap --watch src", + "build": "tsdown", + "prepack": "tsdown --clean" + }, + "dependencies": { + "@mjackson/node-fetch-server": "^0.6.1", + "es-module-lexer": "^1.6.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17", + "periscopic": "^4.0.2", + "turbo-stream": "^3.1.0", + "vitefu": "^1.0.5" + }, + "devDependencies": { + "@hiogawa/utils": "^1.7.0", + "@playwright/test": "^1.53.1", + "@tsconfig/strictest": "^2.0.5", + "@types/estree": "^1.0.8", + "@types/node": "^22.14.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "workspace:*", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-server-dom-webpack": "^19.1.0", + "rsc-html-stream": "^0.0.6", + "tinyexec": "^1.0.1", + "tsdown": "^0.12.9" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*", + "vite": "*" + } +} diff --git a/packages/plugin-rsc/playwright.config.ts b/packages/plugin-rsc/playwright.config.ts new file mode 100644 index 00000000..29a7d0c6 --- /dev/null +++ b/packages/plugin-rsc/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: 'e2e', + use: { + trace: 'on-first-retry', + }, + expect: { + toPass: { timeout: 5000 }, + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + viewport: null, + deviceScaleFactor: undefined, + }, + }, + { + name: 'firefox', + use: devices['Desktop Firefox'], + }, + { + name: 'webkit', + use: devices['Desktop Safari'], + }, + ], + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: ['list', process.env.CI && 'github'] + .filter(Boolean) + .map((name) => [name] as any), +}) as any diff --git a/packages/plugin-rsc/src/browser.ts b/packages/plugin-rsc/src/browser.ts new file mode 100644 index 00000000..e5a52f94 --- /dev/null +++ b/packages/plugin-rsc/src/browser.ts @@ -0,0 +1,23 @@ +import * as clientReferences from 'virtual:vite-rsc/client-references' +import { setRequireModule } from './core/browser' + +export * from './react/browser' + +initialize() + +function initialize(): void { + setRequireModule({ + load: async (id) => { + if (!import.meta.env.__vite_rsc_build__) { + // @ts-ignore + return __vite_rsc_raw_import__(import.meta.env.BASE_URL + id.slice(1)) + } else { + const import_ = clientReferences.default[id] + if (!import_) { + throw new Error(`client reference not found '${id}'`) + } + return import_() + } + }, + }) +} diff --git a/packages/plugin-rsc/src/core/browser.ts b/packages/plugin-rsc/src/core/browser.ts new file mode 100644 index 00000000..d0f5e649 --- /dev/null +++ b/packages/plugin-rsc/src/core/browser.ts @@ -0,0 +1,19 @@ +import { memoize } from '@hiogawa/utils' +import { removeReferenceCacheTag, setInternalRequire } from './shared' + +let init = false + +export function setRequireModule(options: { + load: (id: string) => Promise +}): void { + if (init) return + init = true + + const requireModule = memoize((id: string) => { + return options.load(removeReferenceCacheTag(id)) + }) + + ;(globalThis as any).__vite_rsc_client_require__ = requireModule + + setInternalRequire() +} diff --git a/packages/plugin-rsc/src/core/plugin.ts b/packages/plugin-rsc/src/core/plugin.ts new file mode 100644 index 00000000..d22d2ab0 --- /dev/null +++ b/packages/plugin-rsc/src/core/plugin.ts @@ -0,0 +1,42 @@ +import type { Plugin } from 'vite' + +export default function vitePluginRscCore(): Plugin[] { + return [ + { + name: 'rsc:patch-react-server-dom-webpack', + transform(originalCode, _id, _options) { + let code = originalCode + if (code.includes('__webpack_require__.u')) { + // avoid accessing `__webpack_require__` on import side effect + // https://github.com/facebook/react/blob/a9bbe34622885ef5667d33236d580fe7321c0d8b/packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpackBrowser.js#L16-L17 + code = code.replaceAll('__webpack_require__.u', '({}).u') + } + + // the existance of `__webpack_require__` global can break some packages + // https://github.com/TooTallNate/node-bindings/blob/c8033dcfc04c34397384e23f7399a30e6c13830d/bindings.js#L90-L94 + if (code.includes('__webpack_require__')) { + code = code.replaceAll('__webpack_require__', '__vite_rsc_require__') + } + + if (code !== originalCode) { + return { code, map: null } + } + }, + }, + { + // commonjsOptions needs to be tweaked when this is a linked dep + // since otherwise vendored cjs doesn't work. + name: 'rsc:workaround-linked-dep', + apply: () => !import.meta.url.includes('/node_modules/'), + configEnvironment() { + return { + build: { + commonjsOptions: { + include: [/\/node_modules\//, /\/vendor\/react-server-dom\//], + }, + }, + } + }, + }, + ] +} diff --git a/packages/plugin-rsc/src/core/rsc.ts b/packages/plugin-rsc/src/core/rsc.ts new file mode 100644 index 00000000..d0045255 --- /dev/null +++ b/packages/plugin-rsc/src/core/rsc.ts @@ -0,0 +1,130 @@ +import { memoize, tinyassert } from '@hiogawa/utils' +import type { BundlerConfig, ImportManifestEntry, ModuleMap } from '../types' +import { + SERVER_DECODE_CLIENT_PREFIX, + SERVER_REFERENCE_PREFIX, + createReferenceCacheTag, + removeReferenceCacheTag, + setInternalRequire, +} from './shared' + +// @ts-ignore +import * as ReactServer from '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge' + +let init = false +let requireModule!: (id: string) => unknown + +export function setRequireModule(options: { + load: (id: string) => unknown +}): void { + if (init) return + init = true + + requireModule = (id) => { + return options.load(removeReferenceCacheTag(id)) + } + + // need memoize to return stable promise from __webpack_require__ + ;(globalThis as any).__vite_rsc_server_require__ = memoize( + async (id: string) => { + if (id.startsWith(SERVER_DECODE_CLIENT_PREFIX)) { + id = id.slice(SERVER_DECODE_CLIENT_PREFIX.length) + id = removeReferenceCacheTag(id) + // create `registerClientReference` on the fly since there's no way to + // grab the original client reference module on ther server. + // cf. https://github.com/lazarv/react-server/blob/79e7acebc6f4a8c930ad8422e2a4a9fdacfcce9b/packages/react-server/server/module-loader.mjs#L19 + // decode client reference on the server + return new Proxy({} as any, { + get(target, name, _receiver) { + if (typeof name !== 'string' || name === 'then') return + return (target[name] ??= ReactServer.registerClientReference( + () => { + throw new Error( + `Unexpectedly client reference export '${name}' is called on server`, + ) + }, + id, + name, + )) + }, + }) + } + return requireModule(id) + }, + ) + + setInternalRequire() +} + +export async function loadServerAction(id: string): Promise { + const [file, name] = id.split('#') as [string, string] + const mod: any = await requireModule(file) + return mod[name] +} + +export function createServerManifest(): BundlerConfig { + const cacheTag = import.meta.env.DEV ? createReferenceCacheTag() : '' + + return new Proxy( + {}, + { + get(_target, $$id, _receiver) { + tinyassert(typeof $$id === 'string') + let [id, name] = $$id.split('#') + tinyassert(id) + tinyassert(name) + return { + id: SERVER_REFERENCE_PREFIX + id + cacheTag, + name, + chunks: [], + async: true, + } satisfies ImportManifestEntry + }, + }, + ) +} + +export function createServerDecodeClientManifest(): ModuleMap { + return new Proxy( + {}, + { + get(_target, id: string) { + return new Proxy( + {}, + { + get(_target, name: string) { + return { + id: SERVER_REFERENCE_PREFIX + SERVER_DECODE_CLIENT_PREFIX + id, + name, + chunks: [], + async: true, + } + }, + }, + ) + }, + }, + ) +} + +export function createClientManifest(): BundlerConfig { + const cacheTag = import.meta.env.DEV ? createReferenceCacheTag() : '' + + return new Proxy( + {}, + { + get(_target, $$id, _receiver) { + tinyassert(typeof $$id === 'string') + let [id, name] = $$id.split('#') + tinyassert(id) + tinyassert(name) + return { + id: id + cacheTag, + name, + chunks: [], + async: true, + } satisfies ImportManifestEntry + }, + }, + ) +} diff --git a/packages/plugin-rsc/src/core/shared.ts b/packages/plugin-rsc/src/core/shared.ts new file mode 100644 index 00000000..bd3d18af --- /dev/null +++ b/packages/plugin-rsc/src/core/shared.ts @@ -0,0 +1,25 @@ +// use special prefix to switch client/server reference loading inside __webpack_require__ +export const SERVER_REFERENCE_PREFIX = '$$server:' + +export const SERVER_DECODE_CLIENT_PREFIX = '$$decode-client:' + +// cache bust memoized require promise during dev +export function createReferenceCacheTag(): string { + const cache = Math.random().toString(36).slice(2) + return '$$cache=' + cache +} + +export function removeReferenceCacheTag(id: string): string { + return id.split('$$cache=')[0]! +} + +export function setInternalRequire(): void { + // branch client and server require to support the case when ssr and rsc share the same global + ;(globalThis as any).__vite_rsc_require__ = (id: string) => { + if (id.startsWith(SERVER_REFERENCE_PREFIX)) { + id = id.slice(SERVER_REFERENCE_PREFIX.length) + return (globalThis as any).__vite_rsc_server_require__(id) + } + return (globalThis as any).__vite_rsc_client_require__(id) + } +} diff --git a/packages/plugin-rsc/src/core/ssr.ts b/packages/plugin-rsc/src/core/ssr.ts new file mode 100644 index 00000000..68a847a3 --- /dev/null +++ b/packages/plugin-rsc/src/core/ssr.ts @@ -0,0 +1,27 @@ +import { memoize } from '@hiogawa/utils' +import type { ServerConsumerManifest } from '../types' +import { removeReferenceCacheTag, setInternalRequire } from './shared' + +let init = false + +export function setRequireModule(options: { + load: (id: string) => unknown +}): void { + if (init) return + init = true + + const requireModule = memoize((id: string) => { + return options.load(removeReferenceCacheTag(id)) + }) + + const clientRequire = (id: string) => { + return requireModule(id) + } + ;(globalThis as any).__vite_rsc_client_require__ = clientRequire + + setInternalRequire() +} + +export function createServerConsumerManifest(): ServerConsumerManifest { + return {} +} diff --git a/packages/plugin-rsc/src/extra/browser.tsx b/packages/plugin-rsc/src/extra/browser.tsx new file mode 100644 index 00000000..509aa870 --- /dev/null +++ b/packages/plugin-rsc/src/extra/browser.tsx @@ -0,0 +1,126 @@ +import React from 'react' +import ReactDomClient from 'react-dom/client' +import { rscStream } from 'rsc-html-stream/client' +import { + type CallServerCallback, + createFromFetch, + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + setServerCallback, +} from '../browser' +import type { RscPayload } from './rsc' + +export async function hydrate(): Promise { + const callServer: CallServerCallback = async (id, args) => { + const url = new URL(window.location.href) + const temporaryReferences = createTemporaryReferenceSet() + const payload = await createFromFetch( + fetch(url, { + method: 'POST', + body: await encodeReply(args, { temporaryReferences }), + headers: { + 'x-rsc-action': id, + }, + }), + { temporaryReferences }, + ) + setPayload(payload) + return payload.returnValue + } + setServerCallback(callServer) + + async function onNavigation() { + const url = new URL(window.location.href) + const payload = await createFromFetch(fetch(url)) + setPayload(payload) + } + + const initialPayload = await createFromReadableStream(rscStream) + + let setPayload: (v: RscPayload) => void + + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload) + + React.useEffect(() => { + setPayload = (v) => React.startTransition(() => setPayload_(v)) + }, [setPayload_]) + + React.useEffect(() => { + return listenNavigation(() => onNavigation()) + }, []) + + return payload.root + } + + const browserRoot = ( + + + + ) + + ReactDomClient.hydrateRoot(document, browserRoot, { + formState: initialPayload.formState, + }) + + if (import.meta.hot) { + import.meta.hot.on('rsc:update', () => { + window.history.replaceState({}, '', window.location.href) + }) + } +} + +export async function fetchRSC( + request: string | URL | Request, +): Promise { + const payload = await createFromFetch(fetch(request)) + return payload.root +} + +function listenNavigation(onNavigation: () => void): () => void { + window.addEventListener('popstate', onNavigation) + + const oldPushState = window.history.pushState + window.history.pushState = function (...args) { + const res = oldPushState.apply(this, args) + onNavigation() + return res + } + + const oldReplaceState = window.history.replaceState + window.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args) + onNavigation() + return res + } + + function onClick(e: MouseEvent) { + let link = (e.target as Element).closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault() + history.pushState(null, '', link.href) + } + } + document.addEventListener('click', onClick) + + return () => { + document.removeEventListener('click', onClick) + window.removeEventListener('popstate', onNavigation) + window.history.pushState = oldPushState + window.history.replaceState = oldReplaceState + } +} diff --git a/packages/plugin-rsc/src/extra/rsc.tsx b/packages/plugin-rsc/src/extra/rsc.tsx new file mode 100644 index 00000000..3947ecc2 --- /dev/null +++ b/packages/plugin-rsc/src/extra/rsc.tsx @@ -0,0 +1,93 @@ +import type { ReactFormState } from 'react-dom/client' +import { + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction, + renderToReadableStream, +} from '../rsc' + +export type RscPayload = { + root: React.ReactNode + formState?: ReactFormState + returnValue?: unknown +} + +export async function renderRequest( + request: Request, + root: React.ReactNode, + options?: { nonce?: string }, +): Promise { + function RscRoot() { + // https://vite.dev/guide/features.html#content-security-policy-csp + // this isn't needed if `style-src: 'unsafe-inline'` (dev) and `script-src: 'self'` + const nonceMeta = options?.nonce && ( + + ) + return ( + <> + {nonceMeta} + {root} + + ) + } + + const url = new URL(request.url) + const isAction = request.method === 'POST' + + // use ?__rsc and ?__html for quick debugging + const isRscRequest = + (!request.headers.get('accept')?.includes('text/html') && + !url.searchParams.has('__html')) || + url.searchParams.has('__rsc') + + // TODO: error handling + // callAction + let returnValue: unknown | undefined + let formState: ReactFormState | undefined + let temporaryReferences: unknown | undefined + if (isAction) { + const actionId = request.headers.get('x-rsc-action') + if (actionId) { + // client stream request + const contentType = request.headers.get('content-type') + const body = contentType?.startsWith('multipart/form-data') + ? await request.formData() + : await request.text() + temporaryReferences = createTemporaryReferenceSet() + const args = await decodeReply(body, { temporaryReferences }) + const action = await loadServerAction(actionId) + returnValue = await action.apply(null, args) + } else { + // progressive enhancement + const formData = await request.formData() + const decodedAction = await decodeAction(formData) + const result = await decodedAction() + formState = await decodeFormState(result, formData) + } + } + + const rscPayload: RscPayload = { root: , formState, returnValue } + const rscOptions = { temporaryReferences } + const rscStream = renderToReadableStream(rscPayload, rscOptions) + + if (isRscRequest) { + return new Response(rscStream, { + headers: { + 'content-type': 'text/x-component;charset=utf-8', + vary: 'accept', + }, + }) + } + + const ssrEntry = await import.meta.viteRsc.loadModule( + 'ssr', + 'index', + ) + return ssrEntry.renderHtml(rscStream, { + formState, + nonce: options?.nonce, + debugNoJs: url.searchParams.has('__nojs'), + }) +} diff --git a/packages/plugin-rsc/src/extra/ssr.tsx b/packages/plugin-rsc/src/extra/ssr.tsx new file mode 100644 index 00000000..4d7aef66 --- /dev/null +++ b/packages/plugin-rsc/src/extra/ssr.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import type { ReactFormState } from 'react-dom/client' +import ReactDomServer from 'react-dom/server.edge' +import { injectRSCPayload } from 'rsc-html-stream/server' +import { createFromReadableStream } from '../ssr' +import type { RscPayload } from './rsc' + +export async function renderHtml( + rscStream: ReadableStream, + options?: { + formState?: ReactFormState + nonce?: string + debugNoJs?: boolean + }, +): Promise { + const [rscStream1, rscStream2] = rscStream.tee() + + // flight deserialization needs to be kicked off inside SSR context + // for ReactDomServer preinit/preloading to work + let payload: Promise + function SsrRoot() { + payload ??= createFromReadableStream(rscStream1, { + nonce: options?.nonce, + }) + const root = React.use(payload).root + return root + } + + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index') + const htmlStream = await ReactDomServer.renderToReadableStream(, { + bootstrapScriptContent: options?.debugNoJs + ? undefined + : bootstrapScriptContent, + nonce: options?.nonce, + // no types + ...{ formState: options?.formState }, + }) + + let responseStream: ReadableStream = htmlStream + if (!options?.debugNoJs) { + responseStream = responseStream.pipeThrough( + injectRSCPayload(rscStream2, { + nonce: options?.nonce, + }), + ) + } + + return new Response(responseStream, { + headers: { + 'content-type': 'text/html;charset=utf-8', + vary: 'accept', + }, + }) +} diff --git a/packages/plugin-rsc/src/index.ts b/packages/plugin-rsc/src/index.ts new file mode 100644 index 00000000..92c432ca --- /dev/null +++ b/packages/plugin-rsc/src/index.ts @@ -0,0 +1,2 @@ +export { default, type RscPluginOptions } from './plugin' +export { transformHoistInlineDirective } from './transforms' diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts new file mode 100644 index 00000000..8698f728 --- /dev/null +++ b/packages/plugin-rsc/src/plugin.ts @@ -0,0 +1,1832 @@ +import assert from 'node:assert' +import { createHash } from 'node:crypto' +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { createRequestListener } from '@mjackson/node-fetch-server' +import * as esModuleLexer from 'es-module-lexer' +import MagicString from 'magic-string' +import { + type DevEnvironment, + type EnvironmentModuleNode, + type Plugin, + type ResolvedConfig, + type Rollup, + type RunnableDevEnvironment, + type ViteDevServer, + defaultServerConditions, + isCSSRequest, + normalizePath, + parseAstAsync, +} from 'vite' +import { crawlFrameworkPkgs } from 'vitefu' +import vitePluginRscCore from './core/plugin' +import { + type TransformWrapExportFilter, + hasDirective, + transformDirectiveProxyExport, + transformServerActionServer, + transformWrapExport, +} from './transforms' +import { generateEncryptionKey, toBase64 } from './utils/encryption-utils' +import { createRpcServer } from './utils/rpc' +import { normalizeViteImportAnalysisUrl } from './vite-utils' + +// state for build orchestration +let serverReferences: Record = {} +let server: ViteDevServer +let config: ResolvedConfig +let rscBundle: Rollup.OutputBundle +let buildAssetsManifest: AssetsManifest | undefined +let isScanBuild = false +const BUILD_ASSETS_MANIFEST_NAME = '__vite_rsc_assets_manifest.js' + +type ClientReferenceMeta = { + importId: string + // same as `importId` during dev. hashed id during build. + referenceKey: string + packageSource?: string + // build only for tree-shaking unused export + exportNames: string[] + renderedExports: string[] +} +let clientReferenceMetaMap: Record = {} + +let serverResourcesMetaMap: Record = {} + +const PKG_NAME = '@vitejs/plugin-rsc' +const REACT_SERVER_DOM_NAME = `${PKG_NAME}/vendor/react-server-dom` + +// dev-only wrapper virtual module of rollupOptions.input.index +const VIRTUAL_ENTRIES = { + browser: 'virtual:vite-rsc/entry-browser', +} + +const require = createRequire(import.meta.url) + +function resolvePackage(name: string) { + return pathToFileURL(require.resolve(name)).href +} + +export type RscPluginOptions = { + /** + * shorthand for configuring `environments.(name).build.rollupOptions.input.index` + */ + entries?: Partial> + + /** @deprecated use `serverHandler: false` */ + disableServerHandler?: boolean + + /** @default { enviornmentName: "rsc", entryName: "index" } */ + serverHandler?: + | { + environmentName: string + entryName: string + } + | false + + /** @default false */ + loadModuleDevProxy?: boolean + + rscCssTransform?: false | { filter?: (id: string) => boolean } + + ignoredPackageWarnings?: (string | RegExp)[] + + /** + * This option allows customizing how client build copies assets from server build. + * By default, all assets are copied, but frameworks might want to establish some convention + * to tighten security based on this option. + */ + copyServerAssetsToClient?: (fileName: string) => boolean + + defineEncryptionKey?: string + + /** + * Allows enabling action closure encryption for debugging purpose. + * @default true + */ + enableActionEncryption?: boolean + + /** Escape hatch for Waku's `allowServer` */ + keepUseCientProxy?: boolean +} + +export default function vitePluginRsc( + rscPluginOptions: RscPluginOptions = {}, +): Plugin[] { + return [ + { + name: 'rsc', + async config(config, env) { + await esModuleLexer.init + + // crawl packages with "react" in "peerDependencies" to bundle react deps on server + // see https://github.com/svitejs/vitefu/blob/d8d82fa121e3b2215ba437107093c77bde51b63b/src/index.js#L95-L101 + const result = await crawlFrameworkPkgs({ + root: process.cwd(), + isBuild: env.command === 'build', + isFrameworkPkgByJson(pkgJson) { + if ([PKG_NAME, 'react-dom'].includes(pkgJson.name)) { + return + } + const deps = pkgJson['peerDependencies'] + return deps && 'react' in deps + }, + }) + const noExternal = [ + 'react', + 'react-dom', + 'server-only', + 'client-only', + PKG_NAME, + ...result.ssr.noExternal.sort(), + ] + + return { + appType: 'custom', + define: { + 'import.meta.env.__vite_rsc_build__': JSON.stringify( + env.command === 'build', + ), + }, + environments: { + client: { + build: { + outDir: + config.environments?.client?.build?.outDir ?? 'dist/client', + rollupOptions: { + input: rscPluginOptions.entries?.client && { + index: rscPluginOptions.entries.client, + }, + }, + }, + optimizeDeps: { + include: [ + 'react-dom/client', + `${REACT_SERVER_DOM_NAME}/client.browser`, + ], + exclude: [PKG_NAME], + }, + }, + ssr: { + build: { + outDir: config.environments?.ssr?.build?.outDir ?? 'dist/ssr', + rollupOptions: { + input: rscPluginOptions.entries?.ssr && { + index: rscPluginOptions.entries.ssr, + }, + }, + }, + resolve: { + noExternal, + }, + optimizeDeps: { + include: [ + 'react', + 'react-dom', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + 'react-dom/server.edge', + `${REACT_SERVER_DOM_NAME}/client.edge`, + ], + exclude: [PKG_NAME], + }, + }, + rsc: { + build: { + outDir: config.environments?.rsc?.build?.outDir ?? 'dist/rsc', + emitAssets: true, + rollupOptions: { + input: rscPluginOptions.entries?.rsc && { + index: rscPluginOptions.entries.rsc, + }, + }, + }, + resolve: { + conditions: ['react-server', ...defaultServerConditions], + noExternal, + }, + optimizeDeps: { + include: [ + 'react', + 'react-dom', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + `${REACT_SERVER_DOM_NAME}/server.edge`, + `${REACT_SERVER_DOM_NAME}/client.edge`, + ], + exclude: [PKG_NAME], + }, + }, + }, + builder: { + sharedPlugins: true, + sharedConfigBuild: true, + async buildApp(builder) { + isScanBuild = true + builder.environments.rsc!.config.build.write = false + builder.environments.ssr!.config.build.write = false + await builder.build(builder.environments.rsc!) + await builder.build(builder.environments.ssr!) + isScanBuild = false + builder.environments.rsc!.config.build.write = true + builder.environments.ssr!.config.build.write = true + await builder.build(builder.environments.rsc!) + // sort for stable build + clientReferenceMetaMap = sortObject(clientReferenceMetaMap) + serverResourcesMetaMap = sortObject(serverResourcesMetaMap) + await builder.build(builder.environments.client!) + await builder.build(builder.environments.ssr!) + }, + }, + } + }, + configResolved(config_) { + config = config_ + }, + configureServer(server_) { + server = server_ + ;(globalThis as any).__viteRscDevServer = server + + if (rscPluginOptions.disableServerHandler) return + if (rscPluginOptions.serverHandler === false) return + const options = rscPluginOptions.serverHandler ?? { + environmentName: 'rsc', + entryName: 'index', + } + const environment = server.environments[ + options.environmentName + ] as RunnableDevEnvironment + const source = getEntrySource(environment.config, options.entryName) + + return () => { + server.middlewares.use(async (req, res, next) => { + try { + // resolve before `runner.import` to workaround https://github.com/vitejs/vite/issues/19975 + const resolved = + await environment.pluginContainer.resolveId(source) + assert( + resolved, + `[vite-rsc] failed to resolve server handler '${source}'`, + ) + const mod = await environment.runner.import(resolved.id) + createRequestListener(mod.default)(req, res) + } catch (e) { + next(e) + } + }) + } + }, + async configurePreviewServer(server) { + if (rscPluginOptions.disableServerHandler) return + if (rscPluginOptions.serverHandler === false) return + const options = rscPluginOptions.serverHandler ?? { + environmentName: 'rsc', + entryName: 'index', + } + + const entryFile = path.join( + config.environments[options.environmentName]!.build.outDir, + `${options.entryName}.js`, + ) + const entry = pathToFileURL(entryFile).href + const mod = await import(/* @vite-ignore */ entry) + const handler = createRequestListener(mod.default) + + // disable compressions since it breaks html streaming + // https://github.com/vitejs/vite/blob/9f5c59f07aefb1756a37bcb1c0aff24d54288950/packages/vite/src/node/preview.ts#L178 + server.middlewares.use((req, _res, next) => { + delete req.headers['accept-encoding'] + next() + }) + + return () => { + server.middlewares.use(async (req, res, next) => { + try { + handler(req, res) + } catch (e) { + next(e) + } + }) + } + }, + async hotUpdate(ctx) { + if (isCSSRequest(ctx.file)) { + if (this.environment.name === 'client') { + // filter out `.css?direct` (injected by SSR) to avoid browser full reload + // when changing non-self accepting css such as `module.css`. + return ctx.modules.filter((m) => !m.id?.includes('?direct')) + } + } + + const ids = ctx.modules.map((mod) => mod.id).filter((v) => v !== null) + if (ids.length === 0) return + + // TODO: what if shared component? + function isInsideClientBoundary(mods: EnvironmentModuleNode[]) { + const visited = new Set() + function recurse(mod: EnvironmentModuleNode): boolean { + if (!mod.id) return false + if (clientReferenceMetaMap[mod.id]) return true + if (visited.has(mod.id)) return false + visited.add(mod.id) + for (const importer of mod.importers) { + if (recurse(importer)) { + return true + } + } + return false + } + return mods.some((mod) => recurse(mod)) + } + + if (!isInsideClientBoundary(ctx.modules)) { + if (this.environment.name === 'rsc') { + // server hmr + ctx.server.environments.client.hot.send({ + type: 'custom', + event: 'rsc:update', + data: { + file: ctx.file, + }, + }) + } + + if (this.environment.name === 'client') { + // Server files can be included in client module graph, for example, + // when `addWatchFile` is used to track js files as style dependency (e.g. tailwind) + // In this case, reload all importers (for css hmr), and return empty modules to avoid full-reload. + const env = ctx.server.environments.rsc! + const mod = env.moduleGraph.getModuleById(ctx.file) + if (mod) { + for (const clientMod of ctx.modules) { + for (const importer of clientMod.importers) { + if (importer.id && isCSSRequest(importer.id)) { + await this.environment.reloadModule(importer) + } + } + } + return [] + } + } + } + }, + }, + { + name: 'rsc:patch-browser-raw-import', + transform: { + order: 'post', + handler(code) { + if (code.includes('__vite_rsc_raw_import__')) { + // inject dynamic import last to avoid Vite adding `?import` query to client references + return code.replace('__vite_rsc_raw_import__', 'import') + } + }, + }, + }, + { + // backward compat: `loadSsrModule(name)` implemented as `loadModule("ssr", name)` + name: 'rsc:load-ssr-module', + transform(code) { + if (code.includes('import.meta.viteRsc.loadSsrModule(')) { + return code.replaceAll( + `import.meta.viteRsc.loadSsrModule(`, + `import.meta.viteRsc.loadModule("ssr", `, + ) + } + }, + }, + { + // allow loading entry module in other environment by + // - (dev) rewriting to `server.environments[].runner.import()` + // - (build) rewriting to external `import("..//.js")` + name: 'rsc:load-environment-module', + async transform(code) { + if (!code.includes('import.meta.viteRsc.loadModule')) return + const s = new MagicString(code) + for (const match of code.matchAll( + /import\.meta\.viteRsc\.loadModule\(([\s\S]*?)\)/dg, + )) { + const argCode = match[1]!.trim() + const [environmentName, entryName] = evalValue(`[${argCode}]`) + let replacement: string + if ( + this.environment.mode === 'dev' && + rscPluginOptions.loadModuleDevProxy + ) { + const origin = server.resolvedUrls?.local[0] + assert(origin, '[vite-rsc] no server for loadModueleDevProxy') + const endpoint = + origin + + '__vite_rsc_load_module_dev_proxy?' + + new URLSearchParams({ environmentName, entryName }) + replacement = `__vite_rsc_rpc.createRpcClient(${JSON.stringify({ + endpoint, + })})` + s.prepend( + `import * as __vite_rsc_rpc from "@vitejs/plugin-rsc/utils/rpc";`, + ) + } else if (this.environment.mode === 'dev') { + const environment = server.environments[environmentName]! + const source = getEntrySource(environment.config, entryName) + const resolved = await environment.pluginContainer.resolveId(source) + assert(resolved, `[vite-rsc] failed to resolve entry '${source}'`) + replacement = + `globalThis.__viteRscDevServer.environments[${JSON.stringify( + environmentName, + )}]` + `.runner.import(${JSON.stringify(resolved.id)})` + } else { + replacement = JSON.stringify( + `__vite_rsc_load_module:${this.environment.name}:${environmentName}:${entryName}`, + ) + } + const [start, end] = match.indices![0]! + s.overwrite(start, end, replacement) + } + if (s.hasChanged()) { + return { + code: s.toString(), + map: s.generateMap({ hires: 'boundary' }), + } + } + }, + renderChunk(code, chunk) { + if (!code.includes('__vite_rsc_load_module')) return + const s = new MagicString(code) + for (const match of code.matchAll( + /['"]__vite_rsc_load_module:(\w+):(\w+):(\w+)['"]/dg, + )) { + const [fromEnv, toEnv, entryName] = match.slice(1) + const importPath = normalizeRelativePath( + path.relative( + path.join( + config.environments[fromEnv!]!.build.outDir, + chunk.fileName, + '..', + ), + path.join( + config.environments[toEnv!]!.build.outDir, + // TODO: this breaks when custom entyFileNames + `${entryName}.js`, + ), + ), + ) + const replacement = `(import(${JSON.stringify(importPath)}))` + const [start, end] = match.indices![0]! + s.overwrite(start, end, replacement) + } + if (s.hasChanged()) { + return { + code: s.toString(), + map: s.generateMap({ hires: 'boundary' }), + } + } + }, + }, + { + name: 'vite-rsc-load-module-dev-proxy', + apply: () => !!rscPluginOptions.loadModuleDevProxy, + configureServer(server) { + async function createHandler(url: URL) { + const { environmentName, entryName } = Object.fromEntries( + url.searchParams, + ) + assert(environmentName) + assert(entryName) + const environment = server.environments[ + environmentName + ] as RunnableDevEnvironment + const source = getEntrySource(environment.config, entryName) + const resolvedEntry = + await environment.pluginContainer.resolveId(source) + assert( + resolvedEntry, + `[vite-rsc] failed to resolve entry '${source}'`, + ) + const runnerProxy = new Proxy( + {}, + { + get(_target, p, _receiver) { + if (typeof p !== 'string' || p === 'then') { + return + } + return async (...args: any[]) => { + const mod = await environment.runner.import(resolvedEntry.id) + return (mod as any)[p](...args) + } + }, + }, + ) + return createRpcServer(runnerProxy) + } + + server.middlewares.use(async (req, res, next) => { + const url = new URL(req.url ?? '/', `http://localhost`) + if (url.pathname === '/__vite_rsc_load_module_dev_proxy') { + try { + const handler = await createHandler(url) + createRequestListener(handler)(req, res) + } catch (e) { + next(e) + } + return + } + next() + }) + }, + }, + { + name: 'rsc:virtual:vite-rsc/assets-manifest', + resolveId(source) { + if (source === 'virtual:vite-rsc/assets-manifest') { + if (this.environment.mode === 'build') { + return { id: source, external: true } + } + return `\0` + source + } + }, + load(id) { + if (id === '\0virtual:vite-rsc/assets-manifest') { + assert(this.environment.name !== 'client') + assert(this.environment.mode === 'dev') + const entryUrl = assetsURL('@id/__x00__' + VIRTUAL_ENTRIES.browser) + const manifest: AssetsManifest = { + bootstrapScriptContent: `import(${JSON.stringify(entryUrl)})`, + clientReferenceDeps: {}, + } + return `export default ${JSON.stringify(manifest, null, 2)}` + } + }, + // client build + generateBundle(_options, bundle) { + // copy assets from rsc build to client build + if (this.environment.name === 'rsc') { + rscBundle = bundle + } + + if (this.environment.name === 'client') { + const filterAssets = + rscPluginOptions.copyServerAssetsToClient ?? (() => true) + const rscBuildOptions = config.environments.rsc!.build + const rscViteManifest = + typeof rscBuildOptions.manifest === 'string' + ? rscBuildOptions.manifest + : rscBuildOptions.manifest && '.vite/manifest.json' + for (const asset of Object.values(rscBundle)) { + if (asset.fileName === rscViteManifest) continue + if (asset.type === 'asset' && filterAssets(asset.fileName)) { + this.emitFile({ + type: 'asset', + fileName: asset.fileName, + source: asset.source, + }) + } + } + + const serverResources: Record = {} + const rscAssetDeps = collectAssetDeps(rscBundle) + for (const [id, meta] of Object.entries(serverResourcesMetaMap)) { + serverResources[meta.key] = assetsURLOfDeps({ + js: [], + css: rscAssetDeps[id]?.deps.css ?? [], + }) + } + + const assetDeps = collectAssetDeps(bundle) + const entry = Object.values(assetDeps).find( + (v) => v.chunk.name === 'index', + ) + assert(entry) + const entryUrl = assetsURL(entry.chunk.fileName) + const clientReferenceDeps: Record = {} + for (const [id, meta] of Object.entries(clientReferenceMetaMap)) { + const deps: AssetDeps = assetDeps[id]?.deps ?? { js: [], css: [] } + clientReferenceDeps[meta.referenceKey] = assetsURLOfDeps( + mergeAssetDeps(deps, entry.deps), + ) + } + buildAssetsManifest = { + bootstrapScriptContent: `import(${JSON.stringify(entryUrl)})`, + clientReferenceDeps, + serverResources, + } + } + }, + // non-client builds can load assets manifest as external + renderChunk(code, chunk) { + if (code.includes('virtual:vite-rsc/assets-manifest')) { + assert(this.environment.name !== 'client') + const replacement = normalizeRelativePath( + path.relative( + path.join(chunk.fileName, '..'), + BUILD_ASSETS_MANIFEST_NAME, + ), + ) + code = code.replaceAll( + 'virtual:vite-rsc/assets-manifest', + () => replacement, + ) + return { code } + } + return + }, + writeBundle() { + if (this.environment.name === 'ssr') { + // output client manifest to non-client build directly. + // this makes server build to be self-contained and deploy-able for cloudflare. + const assetsManifestCode = `export default ${JSON.stringify( + buildAssetsManifest, + null, + 2, + )}` + for (const name of ['ssr', 'rsc']) { + const manifestPath = path.join( + config.environments[name]!.build.outDir, + BUILD_ASSETS_MANIFEST_NAME, + ) + fs.writeFileSync(manifestPath, assetsManifestCode) + } + } + }, + }, + createVirtualPlugin('vite-rsc/bootstrap-script-content', function () { + assert(this.environment.name !== 'client') + return `\ +import assetsManifest from "virtual:vite-rsc/assets-manifest"; +export default assetsManifest.bootstrapScriptContent; +` + }), + { + name: 'rsc:bootstrap-script-content', + async transform(code) { + if ( + !code.includes('loadBootstrapScriptContent') || + !/import\s*\.\s*meta\s*\.\s*viteRsc\s*\.\s*loadBootstrapScriptContent/.test( + code, + ) + ) { + return + } + + assert(this.environment.name !== 'client') + const output = new MagicString(code) + + for (const match of code.matchAll( + /import\s*\.\s*meta\s*\.\s*viteRsc\s*\.\s*loadBootstrapScriptContent\(([\s\S]*?)\)/dg, + )) { + const argCode = match[1]!.trim() + const entryName = evalValue(argCode) + assert( + entryName, + `[vite-rsc] expected 'loadBootstrapScriptContent("index")' but got ${argCode}`, + ) + let replacement: string = `Promise.resolve(__vite_rsc_assets_manifest.bootstrapScriptContent)` + const [start, end] = match.indices![0]! + output.overwrite(start, end, replacement) + } + if (output.hasChanged()) { + if (!code.includes('__vite_rsc_assets_manifest')) { + output.prepend( + `import __vite_rsc_assets_manifest from "virtual:vite-rsc/assets-manifest";`, + ) + } + return { + code: output.toString(), + map: output.generateMap({ hires: 'boundary' }), + } + } + }, + }, + createVirtualPlugin( + VIRTUAL_ENTRIES.browser.slice('virtual:'.length), + async function () { + assert(this.environment.mode === 'dev') + let code = '' + // enable hmr only when react plugin is available + const resolved = await this.resolve('/@react-refresh') + if (resolved) { + code += ` +import RefreshRuntime from "/@react-refresh"; +RefreshRuntime.injectIntoGlobalHook(window); +window.$RefreshReg$ = () => {}; +window.$RefreshSig$ = () => (type) => type; +window.__vite_plugin_react_preamble_installed__ = true; +` + } + const source = getEntrySource(this.environment.config, 'index') + const resolvedEntry = await this.resolve(source) + assert(resolvedEntry, `[vite-rsc] failed to resolve entry '${source}'`) + code += `await import(${JSON.stringify(resolvedEntry.id)});` + // TODO + // should remove only the ones we injected during ssr, which are duplicated by browser imports for HMR. + // technically this doesn't have to wait for "vite:beforeUpdate" and should do it right after browser css import. + // TODO: there migth be a clever way to let Vite deduplicate itself. + // cf. https://github.com/withastro/astro/blob/acb9b302f56e38833a1ab01147f7fde0bf967889/packages/astro/src/vite-plugin-astro-server/pipeline.ts#L133-L135 + code += ` +const ssrCss = document.querySelectorAll("link[rel='stylesheet']"); +import.meta.hot.on("vite:beforeUpdate", () => ssrCss.forEach(node => node.remove())); +` + return code + }, + ), + { + // make `AsyncLocalStorage` available globally for React request context on edge build (e.g. React.cache, ssr preload) + // https://github.com/facebook/react/blob/f14d7f0d2597ea25da12bcf97772e8803f2a394c/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge.js#L16-L19 + name: 'rsc:inject-async-local-storage', + async configureServer() { + const __viteRscAyncHooks = await import('node:async_hooks') + ;(globalThis as any).AsyncLocalStorage = + __viteRscAyncHooks.AsyncLocalStorage + }, + banner(chunk) { + if ( + (this.environment.name === 'ssr' || + this.environment.name === 'rsc') && + this.environment.mode === 'build' && + chunk.isEntry + ) { + return `\ +import * as __viteRscAyncHooks from "node:async_hooks"; +globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; +` + } + return '' + }, + }, + ...vitePluginRscCore(), + ...vitePluginUseClient(rscPluginOptions), + ...vitePluginUseServer(rscPluginOptions), + ...vitePluginDefineEncryptionKey(rscPluginOptions), + ...vitePluginFindSourceMapURL(), + ...vitePluginRscCss({ rscCssTransform: rscPluginOptions.rscCssTransform }), + scanBuildStripPlugin(), + ] +} + +function scanBuildStripPlugin(): Plugin { + return { + name: 'rsc:scan-strip', + apply: 'build', + enforce: 'post', + transform(code, _id, _options) { + if (!isScanBuild) return + // During server scan, we strip all code but imports to only discover client/server references. + const [imports] = esModuleLexer.parse(code) + const output = imports + .map((e) => e.n && `import ${JSON.stringify(e.n)};\n`) + .filter(Boolean) + .join('') + return { code: output, map: { mappings: '' } } + }, + } +} + +function normalizeRelativePath(s: string) { + s = normalizePath(s) + return s[0] === '.' ? s : './' + s +} + +function getEntrySource( + config: Pick, + name: string = 'index', +) { + const input = config.build.rollupOptions.input + assert(input) + assert( + typeof input === 'object' && + !Array.isArray(input) && + name in input && + typeof input[name] === 'string', + ) + return input[name] +} + +function hashString(v: string) { + return createHash('sha256').update(v).digest().toString('hex').slice(0, 12) +} + +function normalizeReferenceId(id: string, name: 'client' | 'rsc') { + if (!server) { + return hashString(path.relative(config.root, id)) + } + + // align with how Vite import analysis would rewrite id + // to avoid double modules on browser and ssr. + const environment = server.environments[name]! + return normalizeViteImportAnalysisUrl(environment, id) +} + +function vitePluginUseClient( + useClientPluginOptions: Pick< + RscPluginOptions, + 'ignoredPackageWarnings' | 'keepUseCientProxy' + >, +): Plugin[] { + const packageSources = new Map() + + // https://github.com/vitejs/vite/blob/4bcf45863b5f46aa2b41f261283d08f12d3e8675/packages/vite/src/node/utils.ts#L175 + const bareImportRE = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/ + + return [ + { + name: 'rsc:use-client', + async transform(code, id) { + if (this.environment.name !== 'rsc') return + if (!code.includes('use client')) return + + const ast = await parseAstAsync(code) + if (!hasDirective(ast.body, 'use client')) return + + let importId: string + let referenceKey: string + const packageSource = packageSources.get(id) + if (!packageSource && id.includes('?v=')) { + assert(this.environment.mode === 'dev') + // If non package source `?v=` reached here, this is a client boundary + // created by a package imported on server environment, which breaks the + // expectation on dependency optimizer on browser. Directly copying over + // "?v=" from client optimizer in client reference can make a hashed + // module stale, so we use another virtual module wrapper to delay such process. + // TODO: suggest `optimizeDeps.exclude` and skip warning if that's already the case. + const ignored = useClientPluginOptions.ignoredPackageWarnings?.some( + (pattern) => + pattern instanceof RegExp + ? pattern.test(id) + : id.includes(`/node_modules/${pattern}/`), + ) + if (!ignored) { + this.warn( + `[vite-rsc] detected an internal client boundary created by a package imported on rsc environment`, + ) + } + importId = `/@id/__x00__virtual:vite-rsc/client-in-server-package-proxy/${encodeURIComponent( + id.split('?v=')[0]!, + )}` + referenceKey = importId + } else if (packageSource) { + if (this.environment.mode === 'dev') { + importId = `/@id/__x00__virtual:vite-rsc/client-package-proxy/${packageSource}` + referenceKey = importId + } else { + importId = packageSource + referenceKey = hashString(packageSource) + } + } else { + if (this.environment.mode === 'dev') { + importId = normalizeViteImportAnalysisUrl( + server.environments.client, + id, + ) + referenceKey = importId + } else { + importId = id + referenceKey = hashString( + normalizePath(path.relative(config.root, id)), + ) + } + } + + const transformDirectiveProxyExport_ = withRollupError( + this, + transformDirectiveProxyExport, + ) + const result = transformDirectiveProxyExport_(ast, { + directive: 'use client', + code, + keep: !!useClientPluginOptions.keepUseCientProxy, + runtime: (name, meta) => { + let proxyValue = + `() => { throw new Error("Unexpectedly client reference export '" + ` + + JSON.stringify(name) + + ` + "' is called on server") }` + if (meta?.value) { + proxyValue = `(${meta.value})` + } + return ( + `$$ReactServer.registerClientReference(` + + ` ${proxyValue},` + + ` ${JSON.stringify(referenceKey)},` + + ` ${JSON.stringify(name)})` + ) + }, + }) + if (!result) return + const { output, exportNames } = result + clientReferenceMetaMap[id] = { + importId, + referenceKey, + packageSource, + exportNames, + renderedExports: [], + } + const importSource = resolvePackage(`${PKG_NAME}/react/rsc`) + output.prepend(`import * as $$ReactServer from "${importSource}";\n`) + return { code: output.toString(), map: { mappings: '' } } + }, + }, + createVirtualPlugin('vite-rsc/client-references', function () { + if (this.environment.mode === 'dev') { + return { code: `export default {}`, map: null } + } + let code = '' + for (const meta of Object.values(clientReferenceMetaMap)) { + // vite/rollup can apply tree-shaking to dynamic import of this form + const key = JSON.stringify(meta.referenceKey) + const id = JSON.stringify(meta.importId) + const exports = meta.renderedExports + .map((name) => (name === 'default' ? 'default: _default' : name)) + .sort() + code += ` + ${key}: async () => { + const {${exports}} = await import(${id}); + return {${exports}}; + }, + ` + } + code = `export default {${code}};\n` + return { code, map: null } + }), + { + name: 'rsc:virtual-client-in-server-package', + async load(id) { + if ( + id.startsWith('\0virtual:vite-rsc/client-in-server-package-proxy/') + ) { + assert.equal(this.environment.mode, 'dev') + assert.notEqual(this.environment.name, 'rsc') + id = decodeURIComponent( + id.slice( + '\0virtual:vite-rsc/client-in-server-package-proxy/'.length, + ), + ) + // TODO: avoid `export default undefined` + return ` + export * from ${JSON.stringify(id)}; + import * as __all__ from ${JSON.stringify(id)}; + export default __all__.default; + ` + } + }, + }, + { + name: 'rsc:virtual-client-package', + resolveId: { + order: 'pre', + async handler(source, importer, options) { + if (this.environment.name === 'rsc' && bareImportRE.test(source)) { + const resolved = await this.resolve(source, importer, options) + if (resolved && resolved.id.includes('/node_modules/')) { + packageSources.set(resolved.id, source) + return resolved + } + } + }, + }, + async load(id) { + if (id.startsWith('\0virtual:vite-rsc/client-package-proxy/')) { + assert(this.environment.mode === 'dev') + const source = id.slice( + '\0virtual:vite-rsc/client-package-proxy/'.length, + ) + const meta = Object.values(clientReferenceMetaMap).find( + (v) => v.packageSource === source, + )! + const exportNames = meta.exportNames + return `export {${exportNames.join(',')}} from ${JSON.stringify( + source, + )};\n` + } + }, + generateBundle(_options, bundle) { + if (this.environment.name !== 'rsc') return + + // track used exports of client references in rsc build + // to tree shake unused exports in browser and ssr build + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk') { + for (const [id, mod] of Object.entries(chunk.modules)) { + const meta = clientReferenceMetaMap[id] + if (meta) { + meta.renderedExports = mod.renderedExports + } + } + } + } + }, + }, + ] +} + +function vitePluginDefineEncryptionKey( + useServerPluginOptions: Pick, +): Plugin[] { + let defineEncryptionKey: string + let emitEncryptionKey = false + const KEY_PLACEHOLDER = '__vite_rsc_define_encryption_key' + const KEY_FILE = '__vite_rsc_encryption_key.js' + + return [ + { + name: 'rsc:encryption-key', + async configEnvironment(name, _config, env) { + if (name === 'rsc' && !env.isPreview) { + defineEncryptionKey = + useServerPluginOptions.defineEncryptionKey ?? + JSON.stringify(toBase64(await generateEncryptionKey())) + } + }, + resolveId(source) { + if (source === 'virtual:vite-rsc/encryption-key') { + // encryption logic can be tree-shaken if action bind is not used. + return { id: '\0' + source, moduleSideEffects: false } + } + }, + load(id) { + if (id === '\0virtual:vite-rsc/encryption-key') { + if (this.environment.mode === 'build') { + // during build, load key from an external file to make chunks stable. + return `export default () => ${KEY_PLACEHOLDER}` + } + return `export default () => (${defineEncryptionKey})` + } + }, + renderChunk(code, chunk) { + if (code.includes(KEY_PLACEHOLDER)) { + assert.equal(this.environment.name, 'rsc') + emitEncryptionKey = true + const normalizedPath = normalizeRelativePath( + path.relative(path.join(chunk.fileName, '..'), KEY_FILE), + ) + const replacement = `import(${JSON.stringify( + normalizedPath, + )}).then(__m => __m.default)` + code = code.replaceAll(KEY_PLACEHOLDER, () => replacement) + return { code } + } + }, + writeBundle() { + if (this.environment.name === 'rsc' && emitEncryptionKey) { + fs.writeFileSync( + path.join(this.environment.config.build.outDir, KEY_FILE), + `export default ${defineEncryptionKey};\n`, + ) + } + }, + }, + ] +} + +function vitePluginUseServer( + useServerPluginOptions: Pick< + RscPluginOptions, + 'ignoredPackageWarnings' | 'enableActionEncryption' + >, +): Plugin[] { + return [ + { + name: 'rsc:use-server', + async transform(code, id) { + if (!code.includes('use server')) return + const ast = await parseAstAsync(code) + + let normalizedId_: string | undefined + const getNormalizedId = () => { + if (!normalizedId_) { + if (id.includes('?v=')) { + assert(this.environment.mode === 'dev') + const ignored = + useServerPluginOptions.ignoredPackageWarnings?.some( + (pattern) => + pattern instanceof RegExp + ? pattern.test(id) + : id.includes(`/node_modules/${pattern}/`), + ) + if (!ignored) { + this.warn( + `[vite-rsc] detected an internal server function created by a package imported on ${this.environment.name} environment`, + ) + } + // module runner has additional resolution step and it's not strict about + // module identity of `import(id)` like browser, so we simply strip it off. + id = id.split('?v=')[0]! + } + normalizedId_ = normalizeReferenceId(id, 'rsc') + } + return normalizedId_ + } + + if (this.environment.name === 'rsc') { + const transformServerActionServer_ = withRollupError( + this, + transformServerActionServer, + ) + const enableEncryption = + useServerPluginOptions.enableActionEncryption ?? true + const { output } = transformServerActionServer_(code, ast, { + runtime: (value, name) => + `$$ReactServer.registerServerReference(${value}, ${JSON.stringify( + getNormalizedId(), + )}, ${JSON.stringify(name)})`, + rejectNonAsyncFunction: true, + encode: enableEncryption + ? (value) => `$$ReactServer.encryptActionBoundArgs(${value})` + : undefined, + decode: enableEncryption + ? (value) => + `await $$ReactServer.decryptActionBoundArgs(${value})` + : undefined, + }) + if (!output.hasChanged()) return + serverReferences[getNormalizedId()] = id + const importSource = resolvePackage(`${PKG_NAME}/rsc`) + output.prepend(`import * as $$ReactServer from "${importSource}";\n`) + return { + code: output.toString(), + map: output.generateMap({ hires: 'boundary' }), + } + } else { + if (!hasDirective(ast.body, 'use server')) return + const transformDirectiveProxyExport_ = withRollupError( + this, + transformDirectiveProxyExport, + ) + const result = transformDirectiveProxyExport_(ast, { + code, + runtime: (name) => + `$$ReactClient.createServerReference(` + + `${JSON.stringify(getNormalizedId() + '#' + name)},` + + `$$ReactClient.callServer, ` + + `undefined, ` + + (this.environment.mode === 'dev' + ? `$$ReactClient.findSourceMapURL,` + : 'undefined,') + + `${JSON.stringify(name)})`, + directive: 'use server', + rejectNonAsyncFunction: true, + }) + const output = result?.output + if (!output?.hasChanged()) return + serverReferences[getNormalizedId()] = id + const name = this.environment.name === 'client' ? 'browser' : 'ssr' + const importSource = resolvePackage(`${PKG_NAME}/react/${name}`) + output.prepend(`import * as $$ReactClient from "${importSource}";\n`) + return { + code: output.toString(), + map: output.generateMap({ hires: 'boundary' }), + } + } + }, + }, + createVirtualPlugin('vite-rsc/server-references', function () { + if (this.environment.mode === 'dev') { + return { code: `export {}`, map: null } + } + const code = generateDynamicImportCode(serverReferences) + return { code, map: null } + }), + ] +} + +// Rethrow transform error through `this.error` with `error.pos` which is injected by `@hiogawa/transforms` +function withRollupError any>( + ctx: Rollup.TransformPluginContext, + f: F, +): F { + function processError(e: any): never { + if (e && typeof e === 'object' && typeof e.pos === 'number') { + return ctx.error(e, e.pos) + } + throw e + } + return function (this: any, ...args: any[]) { + try { + const result = f.apply(this, args) + if (result instanceof Promise) { + return result.catch((e: any) => processError(e)) + } + return result + } catch (e: any) { + processError(e) + } + } as F +} + +function createVirtualPlugin(name: string, load: Plugin['load']) { + name = 'virtual:' + name + return { + name: `rsc:virtual-${name}`, + resolveId(source, _importer, _options) { + return source === name ? '\0' + name : undefined + }, + load(id, options) { + if (id === '\0' + name) { + return (load as Function).apply(this, [id, options]) + } + }, + } satisfies Plugin +} + +function generateDynamicImportCode(map: Record) { + let code = Object.entries(map) + .map( + ([key, id]) => + `${JSON.stringify(key)}: () => import(${JSON.stringify(id)}),`, + ) + .join('\n') + return `export default {${code}};\n` +} + +// // https://github.com/vitejs/vite/blob/2a7473cfed96237711cda9f736465c84d442ddef/packages/vite/src/node/plugins/importAnalysisBuild.ts#L222-L230 +function assetsURL(url: string) { + return config.base + url +} + +function assetsURLOfDeps(deps: AssetDeps) { + return { + js: deps.js.map((href) => assetsURL(href)), + css: deps.css.map((href) => assetsURL(href)), + } +} + +// +// collect client reference dependency chunk for modulepreload +// + +export type AssetsManifest = { + bootstrapScriptContent: string + clientReferenceDeps: Record + serverResources?: Record +} + +export type AssetDeps = { + js: string[] + css: string[] +} + +function mergeAssetDeps(a: AssetDeps, b: AssetDeps): AssetDeps { + return { + js: [...new Set([...a.js, ...b.js])], + css: [...new Set([...a.css, ...b.css])], + } +} + +function collectAssetDeps(bundle: Rollup.OutputBundle) { + const chunkToDeps = new Map() + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk') { + chunkToDeps.set(chunk, collectAssetDepsInner(chunk.fileName, bundle)) + } + } + const idToDeps: Record< + string, + { chunk: Rollup.OutputChunk; deps: AssetDeps } + > = {} + for (const [chunk, deps] of chunkToDeps.entries()) { + for (const id of chunk.moduleIds) { + idToDeps[id] = { chunk, deps } + } + } + return idToDeps +} + +function collectAssetDepsInner( + fileName: string, + bundle: Rollup.OutputBundle, +): AssetDeps { + const visited = new Set() + const css: string[] = [] + + function recurse(k: string) { + if (visited.has(k)) return + visited.add(k) + const v = bundle[k] + assert(v, `Not found '${k}' in the bundle`) + if (v.type === 'chunk') { + css.push(...(v.viteMetadata?.importedCss ?? [])) + for (const k2 of v.imports) { + // server external imports is not in bundle + if (k2 in bundle) { + recurse(k2) + } + } + } + } + + recurse(fileName) + return { + js: [...visited], + css: [...new Set(css)], + } +} + +// +// support findSourceMapURL +// https://github.com/facebook/react/pull/29708 +// https://github.com/facebook/react/pull/30741 +// + +export function vitePluginFindSourceMapURL(): Plugin[] { + return [ + { + name: 'rsc:findSourceMapURL', + apply: 'serve', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + const url = new URL(req.url!, `http://localhost`) + if (url.pathname === '/__vite_rsc_findSourceMapURL') { + let filename = url.searchParams.get('filename')! + let environmentName = url.searchParams.get('environmentName')! + try { + const map = await findSourceMapURL( + server, + filename, + environmentName, + ) + res.setHeader('content-type', 'application/json') + if (!map) res.statusCode = 404 + res.end(JSON.stringify(map ?? {})) + } catch (e) { + next(e) + } + return + } + next() + }) + }, + }, + ] +} + +export async function findSourceMapURL( + server: ViteDevServer, + filename: string, + environmentName: string, +): Promise { + // this is likely server external (i.e. outside of Vite processing) + if (filename.startsWith('file://')) { + filename = fileURLToPath(filename) + if (fs.existsSync(filename)) { + // line-by-line identity source map + const content = fs.readFileSync(filename, 'utf-8') + return { + version: 3, + sources: [filename], + sourcesContent: [content], + mappings: 'AAAA' + ';AACA'.repeat(content.split('\n').length), + } + } + return + } + + // server component stack, replace log, `registerServerReference`, etc... + let mod: EnvironmentModuleNode | undefined + let map: + | NonNullable['map'] + | undefined + if (environmentName === 'Server') { + mod = server.environments.rsc!.moduleGraph.getModuleById(filename) + // React extracts stacktrace via resetting `prepareStackTrace` on the server + // and let browser devtools handle the mapping. + // https://github.com/facebook/react/blob/4a36d3eab7d9bbbfae62699989aa95e5a0297c16/packages/react-server/src/ReactFlightStackConfigV8.js#L15-L20 + // This means it has additional +2 line offset due to Vite's module runner + // function wrapper. We need to correct it just like Vite module runner. + // https://github.com/vitejs/vite/blob/d94e7b25564abb81ab7b921d4cd44d0f0d22fec4/packages/vite/src/shared/utils.ts#L58-L69 + // https://github.com/vitejs/vite/blob/d94e7b25564abb81ab7b921d4cd44d0f0d22fec4/packages/vite/src/node/ssr/fetchModule.ts#L142-L146 + map = mod?.transformResult?.map + if (map && map.mappings) { + map = { ...map, mappings: (';;' + map.mappings) as any } + } + } + + const base = server.config.base.slice(0, -1) + + // `createServerReference(... findSourceMapURL ...)` called on browser + if (environmentName === 'Client') { + try { + const url = new URL(filename).pathname.slice(base.length) + mod = server.environments.client.moduleGraph.urlToModuleMap.get(url) + map = mod?.transformResult?.map + } catch (e) {} + } + + if (mod && map) { + // fix sources to match Vite's module url on browser + return { ...map, sources: [base + mod.url] } + } +} + +// +// css support +// + +export function vitePluginRscCss( + rscCssOptions?: Pick, +): Plugin[] { + function collectCss(environment: DevEnvironment, entryId: string) { + const visited = new Set() + const cssIds = new Set() + const visitedFiles = new Set() + + function recurse(id: string) { + if (visited.has(id)) { + return + } + visited.add(id) + const mod = environment.moduleGraph.getModuleById(id) + if (mod?.file) { + visitedFiles.add(mod.file) + } + for (const next of mod?.importedModules ?? []) { + if (next.id) { + if (isCSSRequest(next.id)) { + cssIds.add(next.id) + } else { + recurse(next.id) + } + } + } + } + + recurse(entryId) + + // this doesn't include ?t= query so that RSC won't keep adding styles. + const hrefs = [...cssIds].map((id) => + normalizeViteImportAnalysisUrl(environment, id), + ) + return { ids: [...cssIds], hrefs, visitedFiles: [...visitedFiles] } + } + + function getRscCssTransformFilter({ + id, + code, + }: { + id: string + code: string + }): false | TransformWrapExportFilter { + const { query } = parseIdQuery(id) + if ('vite-rsc-css-export' in query) { + const value = query['vite-rsc-css-export'] + if (value) { + const names = value.split(',') + return (name: string) => names.includes(name) + } + return (name: string) => /^[A-Z]/.test(name) + } + + const options = rscCssOptions?.rscCssTransform + if (options === false) return false + if (options?.filter && !options.filter(id)) return false + if (id.includes('/node_modules/') || !/\.[tj]sx$/.test(id)) return false + + // skip transform if no css imports + const result = esModuleLexer.parse(code) + if (!result[0].some((i) => i.t === 1 && i.n && isCSSRequest(i.n))) { + return false + } + // transform only function exports with capital names, e.g. + // export default function Page() {} + // export function Page() {} + // export const Page = () => {} + return (_name: string, meta) => + !!( + (meta.isFunction && meta.declName && /^[A-Z]/.test(meta.declName)) || + (meta.defaultExportIdentifierName && + /^[A-Z]/.test(meta.defaultExportIdentifierName)) + ) + } + + return [ + { + name: 'rsc:rsc-css-export-transform', + async transform(code, id) { + if (this.environment.name !== 'rsc') return + const filter = getRscCssTransformFilter({ id, code }) + if (!filter) return + const ast = await parseAstAsync(code) + const result = await transformRscCssExport({ + ast, + code, + filter, + }) + if (result) { + return { + code: result.output.toString(), + map: result.output.generateMap({ hires: 'boundary' }), + } + } + }, + }, + { + name: 'rsc:css/dev-ssr-virtual', + resolveId(source) { + if (source.startsWith('virtual:vite-rsc/css/dev-ssr/')) { + return '\0' + source + } + }, + async load(id) { + if (id.startsWith('\0virtual:vite-rsc/css/dev-ssr/')) { + id = id.slice('\0virtual:vite-rsc/css/dev-ssr/'.length) + const mod = + await server.environments.ssr.moduleGraph.getModuleByUrl(id) + if (!mod?.id || !mod?.file) { + return `export default []` + } + const result = collectCss(server.environments.ssr, mod.id) + // invalidate virtual module on js file changes to reflect added/deleted css import + for (const file of [mod.file, ...result.visitedFiles]) { + this.addWatchFile(file) + } + const hrefs = result.hrefs.map((href) => assetsURL(href.slice(1))) + return `export default ${JSON.stringify(hrefs)}` + } + }, + }, + { + name: 'rsc:importer-resources', + async transform(code, id) { + if (!code.includes('import.meta.viteRsc.loadCss')) return + + assert(this.environment.name === 'rsc') + const output = new MagicString(code) + + for (const match of code.matchAll( + /import\.meta\.viteRsc\.loadCss\(([\s\S]*?)\)/dg, + )) { + const [start, end] = match.indices![0]! + const argCode = match[1]!.trim() + let importer = id + if (argCode) { + const argValue = evalValue(argCode) + const resolved = await this.resolve(argValue, id) + if (resolved) { + importer = resolved.id + } else { + this.warn( + `[vite-rsc] failed to transform 'import.meta.viteRsc.loadCss(${argCode})'`, + ) + output.update(start, end, `null`) + continue + } + } + const importId = `virtual:vite-rsc/importer-resources?importer=${encodeURIComponent( + importer, + )}` + + // use dynamic import during dev to delay crawling and discover css correctly. + let replacement: string + if (this.environment.mode === 'dev') { + replacement = `__vite_rsc_react__.createElement(async () => { + const __m = await import(${JSON.stringify(importId)}); + return __vite_rsc_react__.createElement(__m.Resources); + })` + } else { + const hash = hashString(importId) + if (!code.includes(`__vite_rsc_importer_resources_${hash}`)) { + output.prepend( + `import * as __vite_rsc_importer_resources_${hash} from ${JSON.stringify( + importId, + )};`, + ) + } + replacement = `__vite_rsc_react__.createElement(__vite_rsc_importer_resources_${hash}.Resources)` + } + output.update(start, end, replacement) + } + + if (output.hasChanged()) { + if (!code.includes('__vite_rsc_react__')) { + output.prepend(`import __vite_rsc_react__ from "react";`) + } + return { + code: output.toString(), + map: output.generateMap({ hires: 'boundary' }), + } + } + }, + resolveId(source) { + if ( + source.startsWith('virtual:vite-rsc/importer-resources?importer=') + ) { + assert(this.environment.name === 'rsc') + return '\0' + source + } + }, + load(id) { + if (id.startsWith('\0virtual:vite-rsc/importer-resources?importer=')) { + const importer = decodeURIComponent( + parseIdQuery(id).query['importer']!, + ) + if (this.environment.mode === 'dev') { + const result = collectCss(server.environments.rsc!, importer) + const cssHrefs = result.hrefs.map((href) => href.slice(1)) + const jsHrefs = [ + '@id/__x00__virtual:vite-rsc/importer-resources-browser?importer=' + + encodeURIComponent(importer), + ] + const deps = assetsURLOfDeps({ css: cssHrefs, js: jsHrefs }) + return generateResourcesCode(JSON.stringify(deps, null, 2)) + } else { + const key = normalizePath(path.relative(config.root, importer)) + serverResourcesMetaMap[importer] = { key } + return ` + import __vite_rsc_assets_manifest__ from "virtual:vite-rsc/assets-manifest"; + ${generateResourcesCode( + `__vite_rsc_assets_manifest__.serverResources[${JSON.stringify( + key, + )}]`, + )} + ` + } + } + if ( + id.startsWith( + '\0virtual:vite-rsc/importer-resources-browser?importer=', + ) + ) { + assert(this.environment.name === 'client') + assert(this.environment.mode === 'dev') + const importer = decodeURIComponent( + parseIdQuery(id).query['importer']!, + ) + const result = collectCss(server.environments.rsc!, importer) + let code = result.ids + .map((id) => id.replace(/^\0/, '')) + .map((id) => `import ${JSON.stringify(id)};\n`) + .join('') + // ensure hmr boundary at this virtual since otherwise non-self accepting css + // (e.g. css module) causes full reload + code += `if (import.meta.hot) { import.meta.hot.accept() }\n` + return code + } + }, + hotUpdate(ctx) { + if (this.environment.name === 'rsc') { + const mods = collectModuleDependents(ctx.modules) + for (const mod of mods) { + if (mod.id) { + const importer = encodeURIComponent(mod.id) + invalidteModuleById( + server.environments.rsc!, + `\0virtual:vite-rsc/importer-resources?importer=${importer}`, + ) + invalidteModuleById( + server.environments.client, + `\0virtual:vite-rsc/importer-resources-browser?importer=${importer}`, + ) + } + } + } + }, + }, + ] +} + +function invalidteModuleById(environment: DevEnvironment, id: string) { + const mod = environment.moduleGraph.getModuleById(id) + if (mod) { + environment.moduleGraph.invalidateModule(mod) + } + return mod +} + +function collectModuleDependents(mods: EnvironmentModuleNode[]) { + const visited = new Set() + function recurse(mod: EnvironmentModuleNode) { + if (visited.has(mod)) return + visited.add(mod) + for (const importer of mod.importers) { + recurse(importer) + } + } + for (const mod of mods) { + recurse(mod) + } + return [...visited] +} + +function generateResourcesCode(depsCode: string) { + const ResourcesFn = (React: typeof import('react'), deps: AssetDeps) => { + return function Resources() { + return React.createElement(React.Fragment, null, [ + ...deps.css.map((href: string) => + React.createElement('link', { + key: 'css:' + href, + rel: 'stylesheet', + precedence: 'vite-rsc/importer-resources', + href: href, + }), + ), + // js is only for dev to forward css import on browser to have hmr + ...deps.js.map((href: string) => + React.createElement('script', { + key: 'js:' + href, + type: 'module', + async: true, + src: href, + }), + ), + ]) + } + } + + return ` + import __vite_rsc_react__ from "react"; + export const Resources = (${ResourcesFn.toString()})(__vite_rsc_react__, ${depsCode}); + ` +} + +// https://github.com/vitejs/vite/blob/ea9aed7ebcb7f4be542bd2a384cbcb5a1e7b31bd/packages/vite/src/node/utils.ts#L1469-L1475 +function evalValue(rawValue: string): T { + const fn = new Function(` + var console, exports, global, module, process, require + return (\n${rawValue}\n) + `) + return fn() +} + +// https://github.com/vitejs/vite-plugin-vue/blob/06931b1ea2b9299267374cb8eb4db27c0626774a/packages/plugin-vue/src/utils/query.ts#L13 +function parseIdQuery(id: string) { + if (!id.includes('?')) return { filename: id, query: {} } + const [filename, rawQuery] = id.split(`?`, 2) + const query = Object.fromEntries(new URLSearchParams(rawQuery)) + return { filename, query } +} + +export async function transformRscCssExport(options: { + ast: Awaited> + code: string + id?: string + filter: TransformWrapExportFilter +}): Promise<{ output: MagicString } | undefined> { + if (hasDirective(options.ast.body, 'use client')) { + return + } + + const result = transformWrapExport(options.code, options.ast, { + runtime: (value, name, meta) => + `__vite_rsc_wrap_css__(${value}, ${JSON.stringify( + meta.defaultExportIdentifierName ?? name, + )})`, + filter: options.filter, + ignoreExportAllDeclaration: true, + }) + if (result.output.hasChanged()) { + if (!options.code.includes('__vite_rsc_react__')) { + result.output.prepend(`import __vite_rsc_react__ from "react";`) + } + result.output.append(` +function __vite_rsc_wrap_css__(value, name) { + if (typeof value !== 'function') return value; + + function __wrapper(props) { + return __vite_rsc_react__.createElement( + __vite_rsc_react__.Fragment, + null, + import.meta.viteRsc.loadCss(${ + options.id ? JSON.stringify(options.id) : '' + }), + __vite_rsc_react__.createElement(value, props), + ); + } + Object.defineProperty(__wrapper, "name", { value: name }); + return __wrapper; +} +`) + return { output: result.output } + } +} + +/** + * temporary workaround for + * - https://github.com/cloudflare/workers-sdk/issues/9538 (fixed in @cloudflare/vite-plugin@1.8.0) + * - https://github.com/vitejs/vite/pull/20077 (fixed in vite@7.0.0) + */ +export function __fix_cloudflare(): Plugin { + return { + name: 'rsc:workaround-cloudflare', + enforce: 'post', + config(config) { + // https://github.com/cloudflare/workers-sdk/issues/9538 + const plugin = config + .plugins!.flat() + .find((p) => p && 'name' in p && p.name === 'vite-plugin-cloudflare') + const original = (plugin as any).configResolved + ;(plugin as any).configResolved = function (this: any, ...args: any[]) { + try { + return original.apply(this, args) + } catch (e) {} + } + + // workaround (fixed in Vite 7) https://github.com/vitejs/vite/pull/20077 + ;(config.environments as any).ssr.resolve.noExternal = true + ;(config.environments as any).rsc.resolve.noExternal = true + }, + } +} + +function sortObject(o: T) { + return Object.fromEntries( + Object.entries(o).sort(([a], [b]) => a.localeCompare(b)), + ) as T +} diff --git a/packages/plugin-rsc/src/react/browser.ts b/packages/plugin-rsc/src/react/browser.ts new file mode 100644 index 00000000..2d0bded7 --- /dev/null +++ b/packages/plugin-rsc/src/react/browser.ts @@ -0,0 +1,62 @@ +// @ts-ignore +import * as ReactClient from '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser' +import type { CallServerCallback } from '../types' + +export { setRequireModule } from '../core/browser' + +export function createFromReadableStream( + stream: ReadableStream, + options: object = {}, +): Promise { + return ReactClient.createFromReadableStream(stream, { + callServer, + findSourceMapURL, + ...options, + }) +} + +export function createFromFetch( + promiseForResponse: Promise, + options: object = {}, +): Promise { + return ReactClient.createFromFetch(promiseForResponse, { + callServer, + findSourceMapURL, + ...options, + }) +} + +export const encodeReply: ( + v: unknown[], + options?: unknown, +) => Promise = ReactClient.encodeReply + +export const createServerReference: (...args: any[]) => unknown = + ReactClient.createServerReference + +// use global instead of local variable to tolerate duplicate modules +// e.g. when `setServerCallback` is pre-bundled but `createServerReference` is not + +export function callServer(...args: any[]): any { + return (globalThis as any).__viteRscCallServer(...args) +} + +export function setServerCallback(fn: CallServerCallback): void { + ;(globalThis as any).__viteRscCallServer = fn +} + +export type { CallServerCallback } + +export const createTemporaryReferenceSet: () => unknown = + ReactClient.createTemporaryReferenceSet + +export function findSourceMapURL( + filename: string, + environmentName: string, +): string | null { + // TODO: respect config.server.origin and config.base? + const url = new URL('/__vite_rsc_findSourceMapURL', window.location.origin) + url.searchParams.set('filename', filename) + url.searchParams.set('environmentName', environmentName) + return url.toString() +} diff --git a/packages/plugin-rsc/src/react/rsc.ts b/packages/plugin-rsc/src/react/rsc.ts new file mode 100644 index 00000000..260cd820 --- /dev/null +++ b/packages/plugin-rsc/src/react/rsc.ts @@ -0,0 +1,81 @@ +// @ts-ignore +import * as ReactClient from '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge' +// @ts-ignore +import * as ReactServer from '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge' +import type { ReactFormState } from 'react-dom/client' +import { + createClientManifest, + createServerDecodeClientManifest, + createServerManifest, +} from '../core/rsc' + +export { loadServerAction, setRequireModule } from '../core/rsc' + +export function renderToReadableStream( + data: T, + options?: object, +): ReadableStream { + return ReactServer.renderToReadableStream( + data, + createClientManifest(), + options, + ) +} + +export function createFromReadableStream( + stream: ReadableStream, + options: object = {}, +): Promise { + return ReactClient.createFromReadableStream(stream, { + serverConsumerManifest: { + // https://github.com/facebook/react/pull/31300 + // https://github.com/vercel/next.js/pull/71527 + serverModuleMap: createServerManifest(), + moduleMap: createServerDecodeClientManifest(), + }, + ...options, + }) +} + +export function registerClientReference( + proxy: T, + id: string, + name: string, +): T { + return ReactServer.registerClientReference(proxy, id, name) +} + +export const registerServerReference: ( + ref: T, + id: string, + name: string, +) => T = ReactServer.registerServerReference + +export function decodeReply( + body: string | FormData, + options?: unknown, +): Promise { + return ReactServer.decodeReply(body, createServerManifest(), options) +} + +export function decodeAction(body: FormData): Promise<() => Promise> { + return ReactServer.decodeAction(body, createServerManifest()) +} + +export function decodeFormState( + actionResult: unknown, + body: FormData, +): Promise { + return ReactServer.decodeFormState(actionResult, body, createServerManifest()) +} + +export const createTemporaryReferenceSet: () => unknown = + ReactServer.createTemporaryReferenceSet + +export const encodeReply: ( + v: unknown[], + options?: unknown, +) => Promise = ReactClient.encodeReply + +export const createClientTemporaryReferenceSet: () => unknown = + ReactClient.createTemporaryReferenceSet diff --git a/packages/plugin-rsc/src/react/ssr.ts b/packages/plugin-rsc/src/react/ssr.ts new file mode 100644 index 00000000..caa32575 --- /dev/null +++ b/packages/plugin-rsc/src/react/ssr.ts @@ -0,0 +1,22 @@ +// @ts-ignore +import * as ReactClient from '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge' +import { createServerConsumerManifest } from '../core/ssr' + +export { setRequireModule } from '../core/ssr' + +export function createFromReadableStream( + stream: ReadableStream, + options: object = {}, +): Promise { + return ReactClient.createFromReadableStream(stream, { + serverConsumerManifest: createServerConsumerManifest(), + ...options, + }) +} + +export function createServerReference(id: string): unknown { + return ReactClient.createServerReference(id) +} + +export const callServer = null +export const findSourceMapURL = null diff --git a/packages/plugin-rsc/src/rsc-html-stream/browser.ts b/packages/plugin-rsc/src/rsc-html-stream/browser.ts new file mode 100644 index 00000000..eb2a74e8 --- /dev/null +++ b/packages/plugin-rsc/src/rsc-html-stream/browser.ts @@ -0,0 +1,4 @@ +import * as rscHtmlStreamClient from 'rsc-html-stream/client' + +export const getRscStreamFromHtml = (): ReadableStream => + rscHtmlStreamClient.rscStream diff --git a/packages/plugin-rsc/src/rsc-html-stream/ssr.ts b/packages/plugin-rsc/src/rsc-html-stream/ssr.ts new file mode 100644 index 00000000..109d09eb --- /dev/null +++ b/packages/plugin-rsc/src/rsc-html-stream/ssr.ts @@ -0,0 +1,7 @@ +import * as rscHtmlStreamServer from 'rsc-html-stream/server' + +export const injectRscStreamToHtml = ( + stream: ReadableStream, + options?: { nonce?: string }, +): TransformStream => + rscHtmlStreamServer.injectRSCPayload(stream, options) diff --git a/packages/plugin-rsc/src/rsc.tsx b/packages/plugin-rsc/src/rsc.tsx new file mode 100644 index 00000000..801fea4c --- /dev/null +++ b/packages/plugin-rsc/src/rsc.tsx @@ -0,0 +1,33 @@ +import serverReferences from 'virtual:vite-rsc/server-references' +import { setRequireModule } from './core/rsc' + +export { + createClientManifest, + createServerManifest, + loadServerAction, +} from './core/rsc' + +export { + encryptActionBoundArgs, + decryptActionBoundArgs, +} from './utils/encryption-runtime' + +export * from './react/rsc' + +initialize() + +function initialize(): void { + setRequireModule({ + load: async (id) => { + if (!import.meta.env.__vite_rsc_build__) { + return import(/* @vite-ignore */ id) + } else { + const import_ = serverReferences[id] + if (!import_) { + throw new Error(`server reference not found '${id}'`) + } + return import_() + } + }, + }) +} diff --git a/packages/plugin-rsc/src/ssr.tsx b/packages/plugin-rsc/src/ssr.tsx new file mode 100644 index 00000000..ecd0b18f --- /dev/null +++ b/packages/plugin-rsc/src/ssr.tsx @@ -0,0 +1,65 @@ +import assetsManifest from 'virtual:vite-rsc/assets-manifest' +import * as clientReferences from 'virtual:vite-rsc/client-references' +import * as ReactDOM from 'react-dom' +import { setRequireModule } from './core/ssr' +import type { AssetDeps } from './plugin' + +export { createServerConsumerManifest } from './core/ssr' + +export * from './react/ssr' + +initialize() + +function initialize(): void { + setRequireModule({ + load: async (id) => { + if (!import.meta.env.__vite_rsc_build__) { + const mod = await import(/* @vite-ignore */ id) + const modCss = await import( + /* @vite-ignore */ '/@id/__x00__virtual:vite-rsc/css/dev-ssr/' + id + ) + return wrapResourceProxy(mod, { js: [], css: modCss.default }) + } else { + const import_ = clientReferences.default[id] + if (!import_) { + throw new Error(`client reference not found '${id}'`) + } + const deps = assetsManifest.clientReferenceDeps[id] + // kick off preload before initial async import, which is not sync-cached + if (deps) { + preloadDeps(deps) + } + const mod: any = await import_() + return wrapResourceProxy(mod, deps) + } + }, + }) +} + +// preload/preinit during getter access since `load` is cached on production +function wrapResourceProxy(mod: any, deps?: AssetDeps) { + return new Proxy(mod, { + get(target, p, receiver) { + if (p in mod) { + if (deps) { + preloadDeps(deps) + } + } + return Reflect.get(target, p, receiver) + }, + }) +} + +function preloadDeps(deps: AssetDeps) { + for (const href of deps.js) { + ReactDOM.preloadModule(href, { + as: 'script', + // vite doesn't allow configuring crossorigin at the moment, so we can hard code it as well. + // https://github.com/vitejs/vite/issues/6648 + crossOrigin: '', + }) + } + for (const href of deps.css) { + ReactDOM.preinit(href, { as: 'style' }) + } +} diff --git a/packages/plugin-rsc/src/transforms/estree.ts b/packages/plugin-rsc/src/transforms/estree.ts new file mode 100644 index 00000000..3cd7681c --- /dev/null +++ b/packages/plugin-rsc/src/transforms/estree.ts @@ -0,0 +1,9 @@ +import type {} from 'estree' + +// rollup ast has node position +declare module 'estree' { + interface BaseNode { + start: number + end: number + } +} diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts new file mode 100644 index 00000000..7f0d5393 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -0,0 +1,372 @@ +import { parseAstAsync } from 'vite' +import { describe, expect, it } from 'vitest' +import { transformHoistInlineDirective } from './hoist' +import { debugSourceMap } from './test-utils' + +describe(transformHoistInlineDirective, () => { + async function testTransform( + input: string, + options?: { encode?: boolean; noExport?: boolean; directive?: string }, + ) { + const ast = await parseAstAsync(input) + const { output } = transformHoistInlineDirective(input, ast, { + runtime: (value, name) => + `$$register(${value}, "", ${JSON.stringify(name)})`, + directive: options?.directive ?? 'use server', + encode: options?.encode ? (v) => `__enc(${v})` : undefined, + decode: options?.encode ? (v) => `__dec(${v})` : undefined, + noExport: options?.noExport, + }) + if (!output.hasChanged()) { + return + } + if (process.env['DEBUG_SOURCEMAP']) { + await debugSourceMap(output) + } + return output.toString() + } + + it('none', async () => { + const input = ` +const x = "x"; + +async function f() { + return x; +} +` + expect(await testTransform(input)).toMatchInlineSnapshot(`undefined`) + }) + + it('top level', async () => { + const input = ` +const x = "x"; + +async function f() { + "use server"; + return x; +} + +async function g() { +} + +export async function h(formData) { + "use server"; + return formData.get(x); +} + +export default function w() { + "use server"; +} +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + const x = "x"; + + const f = /* #__PURE__ */ $$register($$hoist_0_f, "", "$$hoist_0_f"); + + async function g() { + } + + export const h = /* #__PURE__ */ $$register($$hoist_1_h, "", "$$hoist_1_h"); + + const w = /* #__PURE__ */ $$register($$hoist_2_w, "", "$$hoist_2_w"); + export default w; + + ;export async function $$hoist_0_f() { + "use server"; + return x; + }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_f, "name", { value: "f" }); + + ;export async function $$hoist_1_h(formData) { + "use server"; + return formData.get(x); + }; + /* #__PURE__ */ Object.defineProperty($$hoist_1_h, "name", { value: "h" }); + + ;export function $$hoist_2_w() { + "use server"; + }; + /* #__PURE__ */ Object.defineProperty($$hoist_2_w, "name", { value: "w" }); + " + `) + + // nothing to encode + expect(await testTransform(input, { encode: true })).toBe( + await testTransform(input), + ) + }) + + it('closure', async () => { + const input = ` +let count = 0; + +function Counter() { + const name = "value"; + + async function changeCount(formData) { + "use server"; + count += Number(formData.get(name)); + } + + return "something"; +} +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + let count = 0; + + function Counter() { + const name = "value"; + + const changeCount = /* #__PURE__ */ $$register($$hoist_0_changeCount, "", "$$hoist_0_changeCount").bind(null, name); + + return "something"; + } + + ;export async function $$hoist_0_changeCount(name, formData) { + "use server"; + count += Number(formData.get(name)); + }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_changeCount, "name", { value: "changeCount" }); + " + `) + }) + + it('many', async () => { + const input = ` +let count = 0; + +function Counter() { + const name = "value"; + + async function changeCount(formData) { + "use server"; + count += Number(formData.get(name)); + } + + async function changeCount2(formData) { + "use server"; + count += Number(formData.get(name)); + } + + return "something"; +} +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + let count = 0; + + function Counter() { + const name = "value"; + + const changeCount = /* #__PURE__ */ $$register($$hoist_0_changeCount, "", "$$hoist_0_changeCount").bind(null, name); + + const changeCount2 = /* #__PURE__ */ $$register($$hoist_1_changeCount2, "", "$$hoist_1_changeCount2").bind(null, name); + + return "something"; + } + + ;export async function $$hoist_0_changeCount(name, formData) { + "use server"; + count += Number(formData.get(name)); + }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_changeCount, "name", { value: "changeCount" }); + + ;export async function $$hoist_1_changeCount2(name, formData) { + "use server"; + count += Number(formData.get(name)); + }; + /* #__PURE__ */ Object.defineProperty($$hoist_1_changeCount2, "name", { value: "changeCount2" }); + " + `) + }) + + it('arrow', async () => { + const input = ` +let count = 0; + +function Counter() { + const name = "value"; + + return { + type: "form", + action: (formData) => { + "use server"; + count += Number(formData.get(name)); + } + } +} +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + let count = 0; + + function Counter() { + const name = "value"; + + return { + type: "form", + action: /* #__PURE__ */ $$register($$hoist_0_anonymous_server_function, "", "$$hoist_0_anonymous_server_function").bind(null, name) + } + } + + ;export function $$hoist_0_anonymous_server_function(name, formData) { + "use server"; + count += Number(formData.get(name)); + }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_anonymous_server_function, "name", { value: "anonymous_server_function" }); + " + `) + + expect(await testTransform(input, { encode: true })).toMatchInlineSnapshot(` + " + let count = 0; + + function Counter() { + const name = "value"; + + return { + type: "form", + action: /* #__PURE__ */ $$register($$hoist_0_anonymous_server_function, "", "$$hoist_0_anonymous_server_function").bind(null, __enc([name])) + } + } + + ;export function $$hoist_0_anonymous_server_function($$hoist_encoded, formData) { + const [name] = __dec($$hoist_encoded); + "use server"; + count += Number(formData.get(name)); + }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_anonymous_server_function, "name", { value: "anonymous_server_function" }); + " + `) + }) + + it('higher order', async () => { + // packages/react-server/examples/next/app/actions/header/page.tsx + // packages/react-server/examples/next/app/actions/header/validator.ts + const input = ` +export default function Page() { + const x = 0; + const action = validator(async (y) => { + "use server"; + return x + y; + }) +} + +function validator(action) { + return async function (arg) { + "use server"; + return action(arg); + }; +} +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + export default function Page() { + const x = 0; + const action = validator(/* #__PURE__ */ $$register($$hoist_0_anonymous_server_function, "", "$$hoist_0_anonymous_server_function").bind(null, x)) + } + + function validator(action) { + return /* #__PURE__ */ $$register($$hoist_1_anonymous_server_function, "", "$$hoist_1_anonymous_server_function").bind(null, action); + } + + ;export async function $$hoist_0_anonymous_server_function(x, y) { + "use server"; + return x + y; + }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_anonymous_server_function, "name", { value: "anonymous_server_function" }); + + ;export async function $$hoist_1_anonymous_server_function(action, arg) { + "use server"; + return action(arg); + }; + /* #__PURE__ */ Object.defineProperty($$hoist_1_anonymous_server_function, "name", { value: "anonymous_server_function" }); + " + `) + + expect(await testTransform(input, { encode: true })).toMatchInlineSnapshot(` + " + export default function Page() { + const x = 0; + const action = validator(/* #__PURE__ */ $$register($$hoist_0_anonymous_server_function, "", "$$hoist_0_anonymous_server_function").bind(null, __enc([x]))) + } + + function validator(action) { + return /* #__PURE__ */ $$register($$hoist_1_anonymous_server_function, "", "$$hoist_1_anonymous_server_function").bind(null, __enc([action])); + } + + ;export async function $$hoist_0_anonymous_server_function($$hoist_encoded, y) { + const [x] = __dec($$hoist_encoded); + "use server"; + return x + y; + }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_anonymous_server_function, "name", { value: "anonymous_server_function" }); + + ;export async function $$hoist_1_anonymous_server_function($$hoist_encoded, arg) { + const [action] = __dec($$hoist_encoded); + "use server"; + return action(arg); + }; + /* #__PURE__ */ Object.defineProperty($$hoist_1_anonymous_server_function, "name", { value: "anonymous_server_function" }); + " + `) + }) + + // edge case found in https://github.com/remix-run/react-router/blob/98367e49900701c460cb08eb16c2441da5007efc/playground/rsc-vite/src/routes/home/home.tsx + it('export before import', async () => { + const input = ` +export {} from "edge-case"; +import { redirect } from "react-router/rsc"; + +export default () => { + const redirectOnServer = async () => { + "use server"; + throw redirect(); + }; +} +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + export {} from "edge-case"; + import { redirect } from "react-router/rsc"; + + export default () => { + const redirectOnServer = /* #__PURE__ */ $$register($$hoist_0_redirectOnServer, "", "$$hoist_0_redirectOnServer"); + } + + ;export async function $$hoist_0_redirectOnServer() { + "use server"; + throw redirect(); + }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_redirectOnServer, "name", { value: "redirectOnServer" }); + " + `) + }) + + it('noExport', async () => { + const input = ` +export async function test() { + "use cache"; + return "test"; +} +` + expect( + await testTransform(input, { + directive: 'use cache', + noExport: true, + }), + ).toMatchInlineSnapshot(` + " + export const test = /* #__PURE__ */ $$register($$hoist_0_test, "", "$$hoist_0_test"); + + ;async function $$hoist_0_test() { + "use cache"; + return "test"; + }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_test, "name", { value: "test" }); + " + `) + }) +}) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts new file mode 100644 index 00000000..3cb7e9ac --- /dev/null +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -0,0 +1,142 @@ +import { tinyassert } from '@hiogawa/utils' +import type { Program } from 'estree' +import { walk } from 'estree-walker' +import MagicString from 'magic-string' +import { analyze } from 'periscopic' +import { hasDirective } from './utils' + +export function transformHoistInlineDirective( + input: string, + ast: Program, + { + runtime, + directive, + rejectNonAsyncFunction, + ...options + }: { + runtime: (value: string, name: string) => string + directive: string + rejectNonAsyncFunction?: boolean + encode?: (value: string) => string + decode?: (value: string) => string + noExport?: boolean + }, +): { + output: MagicString + names: string[] +} { + const output = new MagicString(input) + + // re-export somehow confuses periscopic scopes so remove them before analysis + walk(ast, { + enter(node) { + if (node.type === 'ExportAllDeclaration') { + this.remove() + } + if (node.type === 'ExportNamedDeclaration' && !node.declaration) { + this.remove() + } + }, + }) + + const analyzed = analyze(ast) + const names: string[] = [] + + walk(ast, { + enter(node, parent) { + if ( + (node.type === 'FunctionExpression' || + node.type === 'FunctionDeclaration' || + node.type === 'ArrowFunctionExpression') && + node.body.type === 'BlockStatement' && + hasDirective(node.body.body, directive) + ) { + if (!node.async && rejectNonAsyncFunction) { + throw Object.assign( + new Error(`"${directive}" doesn't allow non async function`), + { + pos: node.start, + }, + ) + } + + const scope = analyzed.map.get(node) + tinyassert(scope) + const declName = node.type === 'FunctionDeclaration' && node.id.name + const originalName = + declName || + (parent?.type === 'VariableDeclarator' && + parent.id.type === 'Identifier' && + parent.id.name) || + 'anonymous_server_function' + + // bind variables which are neither global nor in own scope + const bindVars = [...scope.references].filter((ref) => { + // declared function itself is included as reference + if (ref === declName) { + return false + } + const owner = scope.find_owner(ref) + return owner && owner !== scope && owner !== analyzed.scope + }) + let newParams = [ + ...bindVars, + ...node.params.map((n) => input.slice(n.start, n.end)), + ].join(', ') + if (bindVars.length > 0 && options.decode) { + newParams = [ + '$$hoist_encoded', + ...node.params.map((n) => input.slice(n.start, n.end)), + ].join(', ') + output.appendLeft( + node.body.body[0]!.start, + `const [${bindVars.join(',')}] = ${options.decode( + '$$hoist_encoded', + )};\n`, + ) + } + + // append a new `FunctionDeclaration` at the end + const newName = + `$$hoist_${names.length}` + (originalName ? `_${originalName}` : '') + names.push(newName) + output.update( + node.start, + node.body.start, + `\n;${options.noExport ? '' : 'export '}${ + node.async ? 'async ' : '' + }function ${newName}(${newParams}) `, + ) + output.appendLeft( + node.end, + `;\n/* #__PURE__ */ Object.defineProperty(${newName}, "name", { value: ${JSON.stringify( + originalName, + )} });\n`, + ) + output.move(node.start, node.end, input.length) + + // replace original declartion with action register + bind + let newCode = `/* #__PURE__ */ ${runtime(newName, newName)}` + if (bindVars.length > 0) { + const bindArgs = options.encode + ? options.encode('[' + bindVars.join(', ') + ']') + : bindVars.join(', ') + newCode = `${newCode}.bind(null, ${bindArgs})` + } + if (declName) { + newCode = `const ${declName} = ${newCode};` + if (parent?.type === 'ExportDefaultDeclaration') { + output.remove(parent.start, node.start) + newCode = `${newCode}\nexport default ${declName};` + } + } + output.appendLeft(node.start, newCode) + } + }, + }) + + return { + output, + names, + } +} diff --git a/packages/plugin-rsc/src/transforms/index.ts b/packages/plugin-rsc/src/transforms/index.ts new file mode 100644 index 00000000..55b054c0 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/index.ts @@ -0,0 +1,5 @@ +export * from './hoist' +export * from './wrap-export' +export * from './proxy-export' +export * from './utils' +export * from './server-action' diff --git a/packages/plugin-rsc/src/transforms/proxy-export.test.ts b/packages/plugin-rsc/src/transforms/proxy-export.test.ts new file mode 100644 index 00000000..4be88ac0 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/proxy-export.test.ts @@ -0,0 +1,242 @@ +import { parseAstAsync } from 'vite' +import { describe, expect, test } from 'vitest' +import { transformProxyExport } from './proxy-export' +import { debugSourceMap } from './test-utils' +import { transformWrapExport } from './wrap-export' + +async function testTransform(input: string, options?: { keep?: boolean }) { + const ast = await parseAstAsync(input) + const result = transformProxyExport(ast, { + code: input, + runtime: (name, meta) => { + if (meta?.value) { + return `$$proxy(${meta.value}, "", ${JSON.stringify(name)})` + } + return `$$proxy("", ${JSON.stringify(name)})` + }, + ...options, + }) + if (process.env['DEBUG_SOURCEMAP']) { + await debugSourceMap(result.output) + } + return { ...result, output: result.output.toString() } +} + +describe(transformWrapExport, () => { + test('basic', async () => { + const input = ` +export const Arrow = () => { + +}; +export default "hi"; +export function Fn() { +}; + +export async function AsyncFn() { + + +}; + +export class Cls {}; +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + { + "exportNames": [ + "Arrow", + "default", + "Fn", + "AsyncFn", + "Cls", + ], + "output": " + export const Arrow = /* #__PURE__ */ $$proxy("", "Arrow"); + + export default /* #__PURE__ */ $$proxy("", "default"); + + export const Fn = /* #__PURE__ */ $$proxy("", "Fn"); + + + export const AsyncFn = /* #__PURE__ */ $$proxy("", "AsyncFn"); + + + export const Cls = /* #__PURE__ */ $$proxy("", "Cls"); + + ", + } + `) + }) + + test('export destructuring', async () => { + const input = ` +export const { x, y: [z] } = { x: 0, y: [1] }; +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + { + "exportNames": [ + "x", + "z", + ], + "output": " + export const x = /* #__PURE__ */ $$proxy("", "x"); + export const z = /* #__PURE__ */ $$proxy("", "z"); + + ", + } + `) + }) + + test('default function', async () => { + const input = `export default function Fn() {}` + expect(await testTransform(input)).toMatchInlineSnapshot( + ` + { + "exportNames": [ + "default", + ], + "output": "export default /* #__PURE__ */ $$proxy("", "default"); + ", + } + `, + ) + }) + + test('default anonymous function', async () => { + const input = `export default function () {}` + expect(await testTransform(input)).toMatchInlineSnapshot( + ` + { + "exportNames": [ + "default", + ], + "output": "export default /* #__PURE__ */ $$proxy("", "default"); + ", + } + `, + ) + }) + + test('default class', async () => { + const input = `export default class Cls {}` + expect(await testTransform(input)).toMatchInlineSnapshot( + ` + { + "exportNames": [ + "default", + ], + "output": "export default /* #__PURE__ */ $$proxy("", "default"); + ", + } + `, + ) + }) + + test('export simple', async () => { + const input = ` +const x = 0; +export { x } +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + { + "exportNames": [ + "x", + ], + "output": " + + export const x = /* #__PURE__ */ $$proxy("", "x"); + + ", + } + `) + }) + + test('export rename', async () => { + const input = ` +const x = 0; +export { x as y } +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + { + "exportNames": [ + "y", + ], + "output": " + + export const y = /* #__PURE__ */ $$proxy("", "y"); + + ", + } + `) + }) + + test('re-export simple', async () => { + const input = `export { x } from "./dep"` + expect(await testTransform(input)).toMatchInlineSnapshot(` + { + "exportNames": [ + "x", + ], + "output": "export const x = /* #__PURE__ */ $$proxy("", "x"); + ", + } + `) + }) + + test('re-export rename', async () => { + const input = `export { x as y } from "./dep"` + expect(await testTransform(input)).toMatchInlineSnapshot(` + { + "exportNames": [ + "y", + ], + "output": "export const y = /* #__PURE__ */ $$proxy("", "y"); + ", + } + `) + }) + + test('keep', async () => { + const input = `\ +"use client" +import { atom } from 'jotai/vanilla'; + +const local1 = 1; +export const countAtom = atom(local1); + +export const MyClientComp = () => { throw new Error('...') } +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + { + "exportNames": [ + "countAtom", + "MyClientComp", + ], + "output": " + + + + export const countAtom = /* #__PURE__ */ $$proxy("", "countAtom"); + + + export const MyClientComp = /* #__PURE__ */ $$proxy("", "MyClientComp"); + + ", + } + `) + expect(await testTransform(input, { keep: true })).toMatchInlineSnapshot(` + { + "exportNames": [ + "countAtom", + "MyClientComp", + ], + "output": ""use client" + import { atom } from 'jotai/vanilla'; + + const local1 = 1; + export const countAtom = /* #__PURE__ */ $$proxy(atom(local1), "", "countAtom"); + + export const MyClientComp = /* #__PURE__ */ $$proxy(() => { throw new Error('...') }, "", "MyClientComp"); + ", + } + `) + }) +}) diff --git a/packages/plugin-rsc/src/transforms/proxy-export.ts b/packages/plugin-rsc/src/transforms/proxy-export.ts new file mode 100644 index 00000000..f08a0570 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/proxy-export.ts @@ -0,0 +1,169 @@ +import { tinyassert } from '@hiogawa/utils' +import type { Node, Program } from 'estree' +import MagicString from 'magic-string' +import { extract_names } from 'periscopic' +import { hasDirective } from './utils' + +export type TransformProxyExportOptions = { + /** Required for source map and `keep` options */ + code?: string + runtime: (name: string, meta?: { value: string }) => string + ignoreExportAllDeclaration?: boolean + rejectNonAsyncFunction?: boolean + /** + * escape hatch for Waku's `allowServer` + * @default false + */ + keep?: boolean +} + +export function transformDirectiveProxyExport( + ast: Program, + options: { + directive: string + } & TransformProxyExportOptions, +): + | { + exportNames: string[] + output: MagicString + } + | undefined { + if (!hasDirective(ast.body, options.directive)) { + return + } + return transformProxyExport(ast, options) +} + +export function transformProxyExport( + ast: Program, + options: TransformProxyExportOptions, +): { + exportNames: string[] + output: MagicString +} { + if (options.keep && typeof options.code !== 'string') { + throw new Error('`keep` option requires `code`') + } + const output = new MagicString(options.code ?? ' '.repeat(ast.end)) + const exportNames: string[] = [] + + function createExport(node: Node, names: string[]) { + exportNames.push(...names) + const newCode = names + .map( + (name) => + (name === 'default' ? `export default` : `export const ${name} =`) + + ` /* #__PURE__ */ ${options.runtime(name)};\n`, + ) + .join('') + output.update(node.start, node.end, newCode) + } + + function validateNonAsyncFunction(node: Node, ok?: boolean) { + if (options.rejectNonAsyncFunction && !ok) { + throw Object.assign(new Error(`unsupported non async function`), { + pos: node.start, + }) + } + } + + for (const node of ast.body) { + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + if ( + node.declaration.type === 'FunctionDeclaration' || + node.declaration.type === 'ClassDeclaration' + ) { + /** + * export function foo() {} + */ + validateNonAsyncFunction( + node, + node.declaration.type === 'FunctionDeclaration' && + node.declaration.async, + ) + createExport(node, [node.declaration.id.name]) + } else if (node.declaration.type === 'VariableDeclaration') { + /** + * export const foo = 1, bar = 2 + */ + validateNonAsyncFunction( + node, + node.declaration.declarations.every( + (decl) => + decl.init?.type === 'ArrowFunctionExpression' && + decl.init.async, + ), + ) + if (options.keep && options.code) { + if (node.declaration.declarations.length === 1) { + const decl = node.declaration.declarations[0]! + if (decl.id.type === 'Identifier' && decl.init) { + const name = decl.id.name + const value = options.code.slice(decl.init.start, decl.init.end) + const newCode = `export const ${name} = /* #__PURE__ */ ${options.runtime( + name, + { value }, + )};` + output.update(node.start, node.end, newCode) + exportNames.push(name) + continue + } + } + } + const names = node.declaration.declarations.flatMap((decl) => + extract_names(decl.id), + ) + createExport(node, names) + } else { + node.declaration satisfies never + } + } else { + /** + * export { foo, bar as car } from './foo' + * export { foo, bar as car } + */ + const names: string[] = [] + for (const spec of node.specifiers) { + tinyassert(spec.exported.type === 'Identifier') + names.push(spec.exported.name) + } + createExport(node, names) + } + continue + } + + /** + * export * from './foo' + */ + if ( + !options.ignoreExportAllDeclaration && + node.type === 'ExportAllDeclaration' + ) { + throw new Error('unsupported ExportAllDeclaration') + } + + /** + * export default function foo() {} + * export default class Foo {} + * export default () => {} + */ + if (node.type === 'ExportDefaultDeclaration') { + validateNonAsyncFunction( + node, + node.declaration.type === 'Identifier' || + (node.declaration.type === 'FunctionDeclaration' && + node.declaration.async), + ) + createExport(node, ['default']) + continue + } + + if (options.keep) continue + + // remove all other nodes + output.remove(node.start, node.end) + } + + return { exportNames, output } +} diff --git a/packages/plugin-rsc/src/transforms/server-action.ts b/packages/plugin-rsc/src/transforms/server-action.ts new file mode 100644 index 00000000..a7e3b83a --- /dev/null +++ b/packages/plugin-rsc/src/transforms/server-action.ts @@ -0,0 +1,36 @@ +import type { Program } from 'estree' +import type MagicString from 'magic-string' +import { transformHoistInlineDirective } from './hoist' +import { hasDirective } from './utils' +import { transformWrapExport } from './wrap-export' + +// TODO +// source map for `options.runtime` (registerServerReference) call +// needs to match original position. +export function transformServerActionServer( + input: string, + ast: Program, + options: { + runtime: (value: string, name: string) => string + rejectNonAsyncFunction?: boolean + encode?: (value: string) => string + decode?: (value: string) => string + }, +): + | { + exportNames: string[] + output: MagicString + } + | { + output: MagicString + names: string[] + } { + // TODO: unify (generalize transformHoistInlineDirective to support top leve directive case) + if (hasDirective(ast.body, 'use server')) { + return transformWrapExport(input, ast, options) + } + return transformHoistInlineDirective(input, ast, { + ...options, + directive: 'use server', + }) +} diff --git a/packages/plugin-rsc/src/transforms/test-utils.ts b/packages/plugin-rsc/src/transforms/test-utils.ts new file mode 100644 index 00000000..31818b9a --- /dev/null +++ b/packages/plugin-rsc/src/transforms/test-utils.ts @@ -0,0 +1,19 @@ +import { dirname } from 'path' +import { hashString } from '@hiogawa/utils' +import { mkdir, writeFile } from 'fs/promises' +import type MagicString from 'magic-string' + +export async function debugSourceMap(output: MagicString): Promise { + // load it directly to https://evanw.github.io/source-map-visualization + const code = output.toString() + const map = output.generateMap({ includeContent: true, hires: 'boundary' }) + const filepath = `.debug/sourcemap/${hashString(code)}.js` + await mkdir(dirname(filepath), { recursive: true }) + await writeFile(filepath, inlineSourceMap(code, map)) +} + +function inlineSourceMap(code: string, map: unknown) { + const encoded = Buffer.from(JSON.stringify(map), 'utf-8').toString('base64') + const sourceMappingURL = 'sourceMappingURL'.slice() // avoid vite-node regex match + return `${code}\n\n//# ${sourceMappingURL}=data:application/json;charset=utf-8;base64,${encoded}\n` +} diff --git a/packages/plugin-rsc/src/transforms/utils.ts b/packages/plugin-rsc/src/transforms/utils.ts new file mode 100644 index 00000000..aafcecbf --- /dev/null +++ b/packages/plugin-rsc/src/transforms/utils.ts @@ -0,0 +1,82 @@ +import { tinyassert } from '@hiogawa/utils' +import type { Program } from 'estree' +import { extract_names } from 'periscopic' + +export function hasDirective( + body: Program['body'], + directive: string, +): boolean { + return !!body.find( + (stmt) => + stmt.type === 'ExpressionStatement' && + stmt.expression.type === 'Literal' && + typeof stmt.expression.value === 'string' && + stmt.expression.value === directive, + ) +} + +export function getExportNames( + ast: Program, + options: { + ignoreExportAllDeclaration?: boolean + }, +): { + exportNames: string[] +} { + const exportNames: string[] = [] + + for (const node of ast.body) { + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + if ( + node.declaration.type === 'FunctionDeclaration' || + node.declaration.type === 'ClassDeclaration' + ) { + /** + * export function foo() {} + */ + exportNames.push(node.declaration.id.name) + } else if (node.declaration.type === 'VariableDeclaration') { + /** + * export const foo = 1, bar = 2 + */ + for (const decl of node.declaration.declarations) { + exportNames.push(...extract_names(decl.id)) + } + } else { + node.declaration satisfies never + } + } else { + /** + * export { foo, bar as car } from './foo' + * export { foo, bar as car } + */ + for (const spec of node.specifiers) { + tinyassert(spec.exported.type === 'Identifier') + exportNames.push(spec.exported.name) + } + } + } + + /** + * export * from './foo' + */ + if ( + !options.ignoreExportAllDeclaration && + node.type === 'ExportAllDeclaration' + ) { + throw new Error('unsupported ExportAllDeclaration') + } + + /** + * export default function foo() {} + * export default class Foo {} + * export default () => {} + */ + if (node.type === 'ExportDefaultDeclaration') { + exportNames.push('default') + } + } + + return { exportNames } +} diff --git a/packages/plugin-rsc/src/transforms/wrap-export.test.ts b/packages/plugin-rsc/src/transforms/wrap-export.test.ts new file mode 100644 index 00000000..1382e53c --- /dev/null +++ b/packages/plugin-rsc/src/transforms/wrap-export.test.ts @@ -0,0 +1,274 @@ +import { parseAstAsync } from 'vite' +import { describe, expect, test } from 'vitest' +import { debugSourceMap } from './test-utils' +import { + type TransformWrapExportFilter, + transformWrapExport, +} from './wrap-export' + +async function testTransform( + input: string, + options?: { filter?: TransformWrapExportFilter }, +) { + const ast = await parseAstAsync(input) + const { output } = transformWrapExport(input, ast, { + runtime: (value, name) => + `$$wrap(${value}, "", ${JSON.stringify(name)})`, + ignoreExportAllDeclaration: true, + ...options, + }) + if (process.env['DEBUG_SOURCEMAP']) { + await debugSourceMap(output) + } + return output.hasChanged() && output.toString() +} + +describe(transformWrapExport, () => { + test('basic', async () => { + const input = ` +export const Arrow = () => {}; +export default "hi"; +export function Fn() {}; +export async function AsyncFn() {}; +export class Cls {}; +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + let Arrow = () => {}; + const $$default = "hi"; + function Fn() {}; + async function AsyncFn() {}; + class Cls {}; + Arrow = /* #__PURE__ */ $$wrap(Arrow, "", "Arrow"); + export { Arrow }; + Fn = /* #__PURE__ */ $$wrap(Fn, "", "Fn"); + export { Fn }; + AsyncFn = /* #__PURE__ */ $$wrap(AsyncFn, "", "AsyncFn"); + export { AsyncFn }; + Cls = /* #__PURE__ */ $$wrap(Cls, "", "Cls"); + export { Cls }; + ; + const $$wrap_$$default = /* #__PURE__ */ $$wrap($$default, "", "default"); + export { $$wrap_$$default as default }; + " + `) + }) + + test('preserve reference', async () => { + const input = ` +export let count = 0; +export function changeCount() { + count += 1; +} +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + let count = 0; + function changeCount() { + count += 1; + } + count = /* #__PURE__ */ $$wrap(count, "", "count"); + export { count }; + changeCount = /* #__PURE__ */ $$wrap(changeCount, "", "changeCount"); + export { changeCount }; + " + `) + }) + + test('export destructuring', async () => { + const input = ` +export const { x, y: [z] } = { x: 0, y: [1] }; +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + let { x, y: [z] } = { x: 0, y: [1] }; + x = /* #__PURE__ */ $$wrap(x, "", "x"); + export { x }; + z = /* #__PURE__ */ $$wrap(z, "", "z"); + export { z }; + " + `) + }) + + test('default function', async () => { + const input = `export default function Fn() {}` + expect(await testTransform(input)).toMatchInlineSnapshot( + ` + "function Fn() {}; + const $$wrap_Fn = /* #__PURE__ */ $$wrap(Fn, "", "default"); + export { $$wrap_Fn as default }; + " + `, + ) + }) + + test('default anonymous function', async () => { + const input = `export default function () {}` + expect(await testTransform(input)).toMatchInlineSnapshot( + ` + "const $$default = function () {}; + const $$wrap_$$default = /* #__PURE__ */ $$wrap($$default, "", "default"); + export { $$wrap_$$default as default }; + " + `, + ) + }) + + test('default class', async () => { + const input = `export default class Cls {}` + expect(await testTransform(input)).toMatchInlineSnapshot( + ` + "class Cls {}; + const $$wrap_Cls = /* #__PURE__ */ $$wrap(Cls, "", "default"); + export { $$wrap_Cls as default }; + " + `, + ) + }) + + test('export simple', async () => { + const input = ` +const x = 0; +export { x } +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + const x = 0; + + ; + const $$wrap_x = /* #__PURE__ */ $$wrap(x, "", "x"); + export { $$wrap_x as x }; + " + `) + }) + + test('export rename', async () => { + const input = ` +const x = 0; +export { x as y } +` + expect(await testTransform(input)).toMatchInlineSnapshot(` + " + const x = 0; + + ; + const $$wrap_x = /* #__PURE__ */ $$wrap(x, "", "y"); + export { $$wrap_x as y }; + " + `) + }) + + test('re-export simple', async () => { + const input = `export { x } from "./dep"` + expect(await testTransform(input)).toMatchInlineSnapshot(` + "; + import { x as $$import_x } from "./dep"; + const $$wrap_$$import_x = /* #__PURE__ */ $$wrap($$import_x, "", "x"); + export { $$wrap_$$import_x as x }; + " + `) + }) + + test('re-export rename', async () => { + const input = `export { x as y } from "./dep"` + expect(await testTransform(input)).toMatchInlineSnapshot(` + "; + import { x as $$import_x } from "./dep"; + const $$wrap_$$import_x = /* #__PURE__ */ $$wrap($$import_x, "", "y"); + export { $$wrap_$$import_x as y }; + " + `) + }) + + test('re-export all simple', async () => { + const input = `export * from "./dep"` + expect(await testTransform(input)).toMatchInlineSnapshot(`false`) + }) + + test('re-export all rename', async () => { + const input = `export * as all from "./dep"` + expect(await testTransform(input)).toMatchInlineSnapshot(`false`) + }) + + test('filter', async () => { + const input = ` +export const a = 0; +export const b = 0, b_no = 0; +export { c } from "./c"; +export { a as aa }; +` + const result = await testTransform(input, { + filter: (name) => !name.endsWith('no'), + }) + expect(result).toMatchInlineSnapshot(` + " + let a = 0; + let b = 0, b_no = 0; + + + a = /* #__PURE__ */ $$wrap(a, "", "a"); + export { a }; + b = /* #__PURE__ */ $$wrap(b, "", "b"); + export { b }; + export { b_no }; + ; + import { c as $$import_c } from "./c"; + const $$wrap_$$import_c = /* #__PURE__ */ $$wrap($$import_c, "", "c"); + export { $$wrap_$$import_c as c }; + const $$wrap_a = /* #__PURE__ */ $$wrap(a, "", "aa"); + export { $$wrap_a as aa }; + " + `) + }) + + test('filter meta', async () => { + const input = ` +export const a = 0; +export const b = function() {} +export const c = () => {} +export default function d() {} +export default () => {} +` + const result = await testTransform(input, { + filter: (_name, meta) => !!(meta.isFunction && meta.declName), + }) + expect(result).toMatchInlineSnapshot(` + " + let a = 0; + let b = function() {} + let c = () => {} + function d() {} + const $$default = () => {} + export { a }; + b = /* #__PURE__ */ $$wrap(b, "", "b"); + export { b }; + c = /* #__PURE__ */ $$wrap(c, "", "c"); + export { c }; + ; + const $$wrap_d = /* #__PURE__ */ $$wrap(d, "", "default"); + export { $$wrap_d as default }; + export { $$default as default }; + " + `) + }) + + test('filter defaultExportIdentifierName', async () => { + const input = ` +const Page = () => {} +export default Page; +` + expect( + await testTransform(input, { + filter: (_name, meta) => meta.defaultExportIdentifierName === 'Page', + }), + ).toMatchInlineSnapshot(` + " + const Page = () => {} + const $$default = Page; + ; + const $$wrap_$$default = /* #__PURE__ */ $$wrap($$default, "", "default"); + export { $$wrap_$$default as default }; + " + `) + }) +}) diff --git a/packages/plugin-rsc/src/transforms/wrap-export.ts b/packages/plugin-rsc/src/transforms/wrap-export.ts new file mode 100644 index 00000000..6587f966 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/wrap-export.ts @@ -0,0 +1,245 @@ +import { tinyassert } from '@hiogawa/utils' +import type { Node, Program } from 'estree' +import MagicString from 'magic-string' +import { extract_names } from 'periscopic' + +type ExportMeta = { + declName?: string + isFunction?: boolean + defaultExportIdentifierName?: string +} + +export type TransformWrapExportFilter = ( + name: string, + meta: ExportMeta, +) => boolean + +export function transformWrapExport( + input: string, + ast: Program, + options: { + runtime: (value: string, name: string, meta: ExportMeta) => string + ignoreExportAllDeclaration?: boolean + rejectNonAsyncFunction?: boolean + filter?: TransformWrapExportFilter + }, +): { + exportNames: string[] + output: MagicString +} { + const output = new MagicString(input) + const exportNames: string[] = [] + const toAppend: string[] = [] + const filter = options.filter ?? (() => true) + + function wrapSimple( + start: number, + end: number, + exports: { name: string; meta: ExportMeta }[], + ) { + // update code and move to preserve `registerServerReference` position + // e.g. + // input + // export async function f() {} + // ^^^^^^ + // output + // async function f() {} + // f = registerServerReference(f, ...) << maps to original "export" token + // export { f } << + const newCode = exports + .map((e) => [ + filter(e.name, e.meta) && + `${e.name} = /* #__PURE__ */ ${options.runtime( + e.name, + e.name, + e.meta, + )};\n`, + `export { ${e.name} };\n`, + ]) + .flat() + .filter(Boolean) + .join('') + output.update(start, end, newCode) + output.move(start, end, input.length) + } + + function wrapExport(name: string, exportName: string, meta: ExportMeta = {}) { + if (!filter(exportName, meta)) { + toAppend.push(`export { ${name} as ${exportName} }`) + return + } + + toAppend.push( + `const $$wrap_${name} = /* #__PURE__ */ ${options.runtime( + name, + exportName, + meta, + )}`, + `export { $$wrap_${name} as ${exportName} }`, + ) + } + + function validateNonAsyncFunction(node: Node, ok?: boolean) { + if (options.rejectNonAsyncFunction && !ok) { + throw Object.assign(new Error(`unsupported non async function`), { + pos: node.start, + }) + } + } + + for (const node of ast.body) { + // named exports + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + if ( + node.declaration.type === 'FunctionDeclaration' || + node.declaration.type === 'ClassDeclaration' + ) { + /** + * export function foo() {} + */ + validateNonAsyncFunction( + node, + node.declaration.type === 'FunctionDeclaration' && + node.declaration.async, + ) + const name = node.declaration.id.name + wrapSimple(node.start, node.declaration.start, [ + { name, meta: { isFunction: true, declName: name } }, + ]) + } else if (node.declaration.type === 'VariableDeclaration') { + /** + * export const foo = 1, bar = 2 + */ + validateNonAsyncFunction( + node, + node.declaration.declarations.every( + (decl) => + decl.init?.type === 'ArrowFunctionExpression' && + decl.init.async, + ), + ) + if (node.declaration.kind === 'const') { + output.update( + node.declaration.start, + node.declaration.start + 5, + 'let', + ) + } + const names = node.declaration.declarations.flatMap((decl) => + extract_names(decl.id), + ) + // treat only simple single decl as function + let isFunction = false + if (node.declaration.declarations.length === 1) { + const decl = node.declaration.declarations[0]! + isFunction = + decl.id.type === 'Identifier' && + (decl.init?.type === 'ArrowFunctionExpression' || + decl.init?.type === 'FunctionExpression') + } + wrapSimple( + node.start, + node.declaration.start, + names.map((name) => ({ + name, + meta: { isFunction, declName: name }, + })), + ) + } else { + node.declaration satisfies never + } + } else { + if (node.source) { + /** + * export { foo, bar as car } from './foo' + */ + output.remove(node.start, node.end) + for (const spec of node.specifiers) { + tinyassert(spec.local.type === 'Identifier') + tinyassert(spec.exported.type === 'Identifier') + const name = spec.local.name + toAppend.push( + `import { ${name} as $$import_${name} } from ${node.source.raw}`, + ) + wrapExport(`$$import_${name}`, spec.exported.name) + } + } else { + /** + * export { foo, bar as car } + */ + output.remove(node.start, node.end) + for (const spec of node.specifiers) { + tinyassert(spec.local.type === 'Identifier') + tinyassert(spec.exported.type === 'Identifier') + wrapExport(spec.local.name, spec.exported.name) + } + } + } + } + + /** + * export * from './foo' + */ + // vue sfc uses ExportAllDeclaration to re-export setup script. + // for now we just give an option to not throw for this case. + // https://github.com/vitejs/vite-plugin-vue/blob/30a97c1ddbdfb0e23b7dc14a1d2fb609668b9987/packages/plugin-vue/src/main.ts#L372 + if ( + !options.ignoreExportAllDeclaration && + node.type === 'ExportAllDeclaration' + ) { + throw Object.assign(new Error('unsupported ExportAllDeclaration'), { + pos: node.start, + }) + } + + /** + * export default function foo() {} + * export default class Foo {} + * export default () => {} + */ + if (node.type === 'ExportDefaultDeclaration') { + validateNonAsyncFunction( + node, + // TODO: somehow identifier is allowed in next.js? + // (see packages/react-server/examples/next/app/actions/server/actions.ts) + node.declaration.type === 'Identifier' || + (node.declaration.type === 'FunctionDeclaration' && + node.declaration.async), + ) + let localName: string + let isFunction = false + let declName: string | undefined + let defaultExportIdentifierName: string | undefined + if ( + (node.declaration.type === 'FunctionDeclaration' || + node.declaration.type === 'ClassDeclaration') && + node.declaration.id + ) { + // preserve name scope for `function foo() {}` and `class Foo {}` + localName = node.declaration.id.name + output.remove(node.start, node.declaration.start) + isFunction = node.declaration.type === 'FunctionDeclaration' + declName = node.declaration.id.name + } else { + // otherwise we can introduce new variable + localName = '$$default' + output.update(node.start, node.declaration.start, 'const $$default = ') + if (node.declaration.type === 'Identifier') { + defaultExportIdentifierName = node.declaration.name + } + } + wrapExport(localName, 'default', { + isFunction, + declName, + defaultExportIdentifierName, + }) + } + } + + if (toAppend.length > 0) { + output.append(['', ...toAppend, ''].join(';\n')) + } + + return { exportNames, output } +} diff --git a/packages/plugin-rsc/src/types/index.ts b/packages/plugin-rsc/src/types/index.ts new file mode 100644 index 00000000..aae72212 --- /dev/null +++ b/packages/plugin-rsc/src/types/index.ts @@ -0,0 +1,27 @@ +export interface ImportManifestEntry { + id: string + name: string + chunks: string[] + async?: boolean +} + +export interface BundlerConfig { + [bundlerId: string]: ImportManifestEntry +} + +export type ModuleMap = { + [id: string]: { + [exportName: string]: ImportManifestEntry + } +} + +export interface ServerConsumerManifest { + moduleMap?: ModuleMap + serverModuleMap?: BundlerConfig + moduleLoading?: { + prefix: string + crossOriign?: string + } +} + +export type CallServerCallback = (id: string, args: unknown[]) => unknown diff --git a/packages/plugin-rsc/src/types/react.ts b/packages/plugin-rsc/src/types/react.ts new file mode 100644 index 00000000..d92ea675 --- /dev/null +++ b/packages/plugin-rsc/src/types/react.ts @@ -0,0 +1,3 @@ +declare module 'react-dom/server.edge' { + export * from 'react-dom/server' +} diff --git a/packages/plugin-rsc/src/types/virtual.d.ts b/packages/plugin-rsc/src/types/virtual.d.ts new file mode 100644 index 00000000..81bbc242 --- /dev/null +++ b/packages/plugin-rsc/src/types/virtual.d.ts @@ -0,0 +1,22 @@ +declare module 'virtual:vite-rsc/assets-manifest' { + const assetsManifest: import('../plugin').AssetsManifest + export default assetsManifest +} + +declare module 'virtual:vite-rsc/client-references' { + const default_: Record Promise> + export default default_ + export const assetDeps: + | Record + | undefined +} + +declare module 'virtual:vite-rsc/server-references' { + const default_: Record Promise> + export default default_ +} + +declare module 'virtual:vite-rsc/encryption-key' { + const default_: () => string | Promise + export default default_ +} diff --git a/packages/plugin-rsc/src/utils/encryption-runtime.ts b/packages/plugin-rsc/src/utils/encryption-runtime.ts new file mode 100644 index 00000000..86a3c02e --- /dev/null +++ b/packages/plugin-rsc/src/utils/encryption-runtime.ts @@ -0,0 +1,48 @@ +import encryptionKeySource from 'virtual:vite-rsc/encryption-key' +import { once } from '@hiogawa/utils' +import { createFromReadableStream, renderToReadableStream } from '../react/rsc' +import { + arrayToStream, + concatArrayStream, + decryptBuffer, + encryptBuffer, + fromBase64, +} from './encryption-utils' + +// based on +// https://github.com/parcel-bundler/parcel/blob/9855f558a69edde843b1464f39a6010f6b421efe/packages/transformers/js/src/rsc-utils.js +// https://github.com/vercel/next.js/blob/c10c10daf9e95346c31c24dc49d6b7cda48b5bc8/packages/next/src/server/app-render/encryption.ts +// https://github.com/vercel/next.js/pull/56377 + +export async function encryptActionBoundArgs( + originalValue: unknown, +): Promise { + const serialized = renderToReadableStream(originalValue) + const serializedBuffer = await concatArrayStream(serialized) + return encryptBuffer(serializedBuffer, await getEncryptionKey()) +} + +export async function decryptActionBoundArgs( + encrypted: ReturnType, +): Promise { + const serializedBuffer = await decryptBuffer( + await encrypted, + await getEncryptionKey(), + ) + const serialized = arrayToStream(new Uint8Array(serializedBuffer)) + return createFromReadableStream(serialized) +} + +const getEncryptionKey = /* #__PURE__ */ once(async () => { + const resolved = await encryptionKeySource() + const key = await crypto.subtle.importKey( + 'raw', + fromBase64(resolved), + { + name: 'AES-GCM', + }, + true, + ['encrypt', 'decrypt'], + ) + return key +}) diff --git a/packages/plugin-rsc/src/utils/encryption-utils.ts b/packages/plugin-rsc/src/utils/encryption-utils.ts new file mode 100644 index 00000000..34ca2a37 --- /dev/null +++ b/packages/plugin-rsc/src/utils/encryption-utils.ts @@ -0,0 +1,115 @@ +// based on +// https://github.com/vercel/next.js/blob/a0993d90c280690e83a2a1bc7c292e1187429fe8/packages/next/src/server/app-render/encryption-utils.ts + +function arrayBufferToString(buffer: ArrayBuffer | Uint8Array): string { + const bytes = new Uint8Array(buffer) + const len = bytes.byteLength + if (len < 65535) { + return String.fromCharCode.apply(null, bytes as unknown as number[]) + } + let binary = '' + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]!) + } + return binary +} + +function stringToUint8Array(binary: string): Uint8Array { + const len = binary.length + const arr = new Uint8Array(len) + for (let i = 0; i < len; i++) { + arr[i] = binary.charCodeAt(i) + } + return arr +} + +function concatArray(chunks: Uint8Array[]): Uint8Array { + let total = 0 + for (const chunk of chunks) { + total += chunk.length + } + const result = new Uint8Array(total) + let offset = 0 + for (const chunk of chunks) { + result.set(chunk, offset) + offset += chunk.length + } + return result +} + +export async function concatArrayStream( + stream: ReadableStream, +): Promise { + const chunks: Uint8Array[] = [] + await stream.pipeTo( + new WritableStream({ + write(chunk) { + chunks.push(chunk) + }, + }), + ) + return concatArray(chunks) +} + +export function arrayToStream(data: Uint8Array): ReadableStream { + return new ReadableStream({ + start(controller) { + controller.enqueue(data) + controller.close() + }, + }) +} + +export function toBase64(buffer: Uint8Array): string { + return btoa(arrayBufferToString(buffer)) +} + +export function fromBase64(data: string): Uint8Array { + return stringToUint8Array(atob(data)) +} + +export async function generateEncryptionKey(): Promise { + const key = await crypto.subtle.generateKey( + { + name: 'AES-GCM', + length: 256, + }, + true, + ['encrypt', 'decrypt'], + ) + const exported = await crypto.subtle.exportKey('raw', key) + return new Uint8Array(exported) +} + +export async function encryptBuffer( + data: BufferSource, + key: CryptoKey, +): Promise { + const iv = crypto.getRandomValues(new Uint8Array(16)) + const encrypted = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + }, + key, + data, + ) + return toBase64(concatArray([iv, new Uint8Array(encrypted)])) +} + +export async function decryptBuffer( + encryptedString: string, + key: CryptoKey, +): Promise { + const concatenated = fromBase64(encryptedString) + const iv = concatenated.slice(0, 16) + const encrypted = concatenated.slice(16) + return crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv, + }, + key, + encrypted, + ) +} diff --git a/packages/plugin-rsc/src/utils/rpc.ts b/packages/plugin-rsc/src/utils/rpc.ts new file mode 100644 index 00000000..483b9a2e --- /dev/null +++ b/packages/plugin-rsc/src/utils/rpc.ts @@ -0,0 +1,76 @@ +import { decode, encode } from 'turbo-stream' + +type RequestPayload = { + method: string + args: any[] +} + +type ResponsePayload = { + ok: boolean + data: any +} + +export function createRpcServer(handlers: T) { + return async (request: Request): Promise => { + if (!request.body) { + throw new Error(`loadModuleDevProxy error: missing request body`) + } + const reqPayload = await decode( + request.body.pipeThrough(new TextDecoderStream()), + ) + const handler = (handlers as any)[reqPayload.method] + if (!handler) { + throw new Error( + `loadModuleDevProxy error: unknown method ${reqPayload.method}`, + ) + } + const resPayload: ResponsePayload = { ok: true, data: undefined } + try { + resPayload.data = await handler(...reqPayload.args) + } catch (e) { + resPayload.ok = false + resPayload.data = e + } + return new Response(encode(resPayload)) + } +} + +export function createRpcClient(options: { endpoint: string }): T { + async function callRpc(method: string, args: any[]) { + const reqPayload: RequestPayload = { + method, + args, + } + const body = encode(reqPayload).pipeThrough(new TextEncoderStream()) + const res = await fetch(options.endpoint, { + method: 'POST', + body, + // @ts-ignore undici compat + duplex: 'half', + }) + if (!res.ok || !res.body) { + throw new Error( + `loadModuleDevProxy error: ${res.status} ${res.statusText}`, + ) + } + const resPayload = await decode( + res.body.pipeThrough(new TextDecoderStream()), + ) + if (!resPayload.ok) { + throw resPayload.data + } + return resPayload.data + } + + return new Proxy( + {}, + { + get(_target, p, _receiver) { + if (typeof p !== 'string' || p === 'then') { + return + } + return (...args: any[]) => callRpc(p, args) + }, + }, + ) as any +} diff --git a/packages/plugin-rsc/src/vite-utils.ts b/packages/plugin-rsc/src/vite-utils.ts new file mode 100644 index 00000000..b23e5844 --- /dev/null +++ b/packages/plugin-rsc/src/vite-utils.ts @@ -0,0 +1,127 @@ +// import analysis logic copied from vite + +import fs from 'node:fs' +import path from 'node:path' +import type { DevEnvironment, Rollup } from 'vite' + +export const VALID_ID_PREFIX = `/@id/` + +export const NULL_BYTE_PLACEHOLDER = `__x00__` + +export const FS_PREFIX = `/@fs/` + +export function wrapId(id: string): string { + return id.startsWith(VALID_ID_PREFIX) + ? id + : VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER) +} + +export function unwrapId(id: string): string { + return id.startsWith(VALID_ID_PREFIX) + ? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0') + : id +} + +export function withTrailingSlash(path: string): string { + if (path[path.length - 1] !== '/') { + return `${path}/` + } + return path +} + +const postfixRE = /[?#].*$/ +export function cleanUrl(url: string): string { + return url.replace(postfixRE, '') +} + +export function splitFileAndPostfix(path: string): { + file: string + postfix: string +} { + const file = cleanUrl(path) + return { file, postfix: path.slice(file.length) } +} + +const windowsSlashRE = /\\/g +export function slash(p: string): string { + return p.replace(windowsSlashRE, '/') +} + +const isWindows = typeof process !== 'undefined' && process.platform === 'win32' + +export function injectQuery(url: string, queryToInject: string): string { + const { file, postfix } = splitFileAndPostfix(url) + const normalizedFile = isWindows ? slash(file) : file + return `${normalizedFile}?${queryToInject}${ + postfix[0] === '?' ? `&${postfix.slice(1)}` : /* hash only */ postfix + }` +} + +export function joinUrlSegments(a: string, b: string): string { + if (!a || !b) { + return a || b || '' + } + if (a.endsWith('/')) { + a = a.substring(0, a.length - 1) + } + if (b[0] !== '/') { + b = '/' + b + } + return a + b +} + +export function normalizeResolvedIdToUrl( + environment: DevEnvironment, + url: string, + resolved: Rollup.PartialResolvedId, +): string { + const root = environment.config.root + const depsOptimizer = environment.depsOptimizer + + // normalize all imports into resolved URLs + // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'` + if (resolved.id.startsWith(withTrailingSlash(root))) { + // in root: infer short absolute path from root + url = resolved.id.slice(root.length) + } else if ( + depsOptimizer?.isOptimizedDepFile(resolved.id) || + // vite-plugin-react isn't following the leading \0 virtual module convention. + // This is a temporary hack to avoid expensive fs checks for React apps. + // We'll remove this as soon we're able to fix the react plugins. + (resolved.id !== '/@react-refresh' && + path.isAbsolute(resolved.id) && + fs.existsSync(cleanUrl(resolved.id))) + ) { + // an optimized deps may not yet exists in the filesystem, or + // a regular file exists but is out of root: rewrite to absolute /@fs/ paths + url = path.posix.join(FS_PREFIX, resolved.id) + } else { + url = resolved.id + } + + // if the resolved id is not a valid browser import specifier, + // prefix it to make it valid. We will strip this before feeding it + // back into the transform pipeline + if (url[0] !== '.' && url[0] !== '/') { + url = wrapId(resolved.id) + } + + return url +} + +export function normalizeViteImportAnalysisUrl( + environment: DevEnvironment, + id: string, +): string { + let url = normalizeResolvedIdToUrl(environment, id, { id }) + + // https://github.com/vitejs/vite/blob/c18ce868c4d70873406e9f7d1b2d0a03264d2168/packages/vite/src/node/plugins/importAnalysis.ts#L416 + if (environment.config.consumer === 'client') { + const mod = environment.moduleGraph.getModuleById(id) + if (mod && mod.lastHMRTimestamp > 0) { + url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) + } + } + + return url +} diff --git a/packages/plugin-rsc/tsconfig.base.json b/packages/plugin-rsc/tsconfig.base.json new file mode 100644 index 00000000..e0ab8c29 --- /dev/null +++ b/packages/plugin-rsc/tsconfig.base.json @@ -0,0 +1,16 @@ +{ + "extends": "@tsconfig/strictest/tsconfig.json", + "compilerOptions": { + "paths": { + "@oxc-project/types": ["./node_modules/@types/estree"] + }, + "noImplicitReturns": false, + "exactOptionalPropertyTypes": false, + "verbatimModuleSyntax": true, + "noEmit": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"] + } +} diff --git a/packages/plugin-rsc/tsconfig.json b/packages/plugin-rsc/tsconfig.json new file mode 100644 index 00000000..e4cfe71d --- /dev/null +++ b/packages/plugin-rsc/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.base.json", + "include": ["src", "*.ts", "types"], + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "noImplicitReturns": false, + "checkJs": false, + "declaration": true, + "isolatedDeclarations": true, + "types": ["vite/client"], + "jsx": "react-jsx" + } +} diff --git a/packages/plugin-rsc/tsdown.config.ts b/packages/plugin-rsc/tsdown.config.ts new file mode 100644 index 00000000..0c39520a --- /dev/null +++ b/packages/plugin-rsc/tsdown.config.ts @@ -0,0 +1,49 @@ +import fs from 'node:fs' +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: [ + 'src/index.ts', + 'src/plugin.ts', + 'src/browser.ts', + 'src/ssr.tsx', + 'src/rsc.tsx', + 'src/vite-utils.ts', + 'src/core/browser.ts', + 'src/core/ssr.ts', + 'src/core/rsc.ts', + 'src/core/plugin.ts', + 'src/react/browser.ts', + 'src/react/ssr.ts', + 'src/react/rsc.ts', + 'src/extra/browser.tsx', + 'src/extra/ssr.tsx', + 'src/extra/rsc.tsx', + 'src/rsc-html-stream/ssr.ts', + 'src/rsc-html-stream/browser.ts', + 'src/utils/rpc.ts', + ], + format: ['esm'], + external: [/^virtual:/, /^@vitejs\/plugin-rsc\/vendor\//], + dts: { + sourcemap: process.argv.slice(2).includes('--sourcemap'), + }, + plugins: [ + { + name: 'vendor-react-server-dom', + buildStart() { + fs.rmSync('./dist/vendor/', { recursive: true, force: true }) + fs.mkdirSync('./dist/vendor', { recursive: true }) + fs.cpSync( + './node_modules/react-server-dom-webpack', + './dist/vendor/react-server-dom', + { recursive: true, dereference: true }, + ) + fs.rmSync('./dist/vendor/react-server-dom/node_modules', { + recursive: true, + force: true, + }) + }, + }, + ], +}) as any diff --git a/packages/plugin-rsc/types/index.d.ts b/packages/plugin-rsc/types/index.d.ts new file mode 100644 index 00000000..dfd4de56 --- /dev/null +++ b/packages/plugin-rsc/types/index.d.ts @@ -0,0 +1,19 @@ +import './virtual' + +declare global { + interface ImportMeta { + readonly viteRsc: { + loadCss: (importer?: string) => import('react').ReactNode + /** @deprecated use `loadModule("ssr", entry)` instead */ + loadSsrModule: (entry: string) => Promise + loadModule: (environmentName: string, entryName: string) => Promise + loadBootstrapScriptContent: (entryName: string) => Promise + } + } + + interface ImportMetaEnv { + readonly __vite_rsc_build__: boolean + } +} + +export {} diff --git a/packages/plugin-rsc/types/virtual.d.ts b/packages/plugin-rsc/types/virtual.d.ts new file mode 100644 index 00000000..cf9ba0cc --- /dev/null +++ b/packages/plugin-rsc/types/virtual.d.ts @@ -0,0 +1,5 @@ +declare module 'virtual:vite-rsc/bootstrap-script-content' { + /** @deprecated use `import.meta.viteRsc.loadBootstrapScriptContent("index")` instead */ + const bootstrapScriptContent: string + export default bootstrapScriptContent +} diff --git a/packages/plugin-rsc/vitest.config.ts b/packages/plugin-rsc/vitest.config.ts new file mode 100644 index 00000000..55fa35be --- /dev/null +++ b/packages/plugin-rsc/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + dir: './src', + }, +}) as any diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd662290..7541cbf3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,9 @@ catalogs: specifier: npm:rolldown-vite@^6.3.21 version: 6.3.21 +overrides: + '@vitejs/plugin-rsc': workspace:* + packageExtensionsChecksum: sha256-S82yCctxnlOTNFuHWCyTFRo/B6Y3jque/4DnsDO4WZA= importers: @@ -26,8 +29,8 @@ importers: specifier: ^22.16.0 version: 22.16.0 '@vitejs/release-scripts': - specifier: ^1.5.0 - version: 1.5.0 + specifier: ^1.6.0 + version: 1.6.0(conventional-commits-filter@5.0.0) eslint: specifier: ^9.30.1 version: 9.30.1(jiti@2.4.2) @@ -442,6 +445,255 @@ importers: specifier: ../../dist version: link:../../dist + packages/plugin-rsc: + dependencies: + '@mjackson/node-fetch-server': + specifier: ^0.6.1 + version: 0.6.1 + es-module-lexer: + specifier: ^1.6.0 + version: 1.7.0 + estree-walker: + specifier: ^3.0.3 + version: 3.0.3 + magic-string: + specifier: ^0.30.17 + version: 0.30.17 + periscopic: + specifier: ^4.0.2 + version: 4.0.2 + turbo-stream: + specifier: ^3.1.0 + version: 3.1.0 + vitefu: + specifier: ^1.0.5 + version: 1.1.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + devDependencies: + '@hiogawa/utils': + specifier: ^1.7.0 + version: 1.7.0 + '@playwright/test': + specifier: ^1.53.1 + version: 1.53.2 + '@tsconfig/strictest': + specifier: ^2.0.5 + version: 2.0.5 + '@types/estree': + specifier: ^1.0.8 + version: 1.0.8 + '@types/node': + specifier: ^22.14.1 + version: 22.16.0 + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: workspace:* + version: link:../plugin-react + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + react-server-dom-webpack: + specifier: ^19.1.0 + version: 19.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + rsc-html-stream: + specifier: ^0.0.6 + version: 0.0.6 + tinyexec: + specifier: ^1.0.1 + version: 1.0.1 + tsdown: + specifier: ^0.12.9 + version: 0.12.9(publint@0.3.12)(typescript@5.8.3) + + packages/plugin-rsc/examples/basic: + dependencies: + '@vitejs/plugin-rsc': + specifier: workspace:* + version: link:../.. + react: + specifier: latest + version: 19.1.0 + react-dom: + specifier: latest + version: 19.1.0(react@19.1.0) + devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.4 + version: 4.1.11(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + '@types/react': + specifier: latest + version: 19.1.8 + '@types/react-dom': + specifier: latest + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: latest + version: 4.6.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + '@vitejs/test-dep-client-in-server': + specifier: file:./test-dep/client-in-server + version: file:packages/plugin-rsc/examples/basic/test-dep/client-in-server(react@19.1.0) + '@vitejs/test-dep-client-in-server2': + specifier: file:./test-dep/client-in-server2 + version: file:packages/plugin-rsc/examples/basic/test-dep/client-in-server2(react@19.1.0) + '@vitejs/test-dep-server-in-client': + specifier: file:./test-dep/server-in-client + version: file:packages/plugin-rsc/examples/basic/test-dep/server-in-client(react@19.1.0) + '@vitejs/test-dep-server-in-server': + specifier: file:./test-dep/server-in-server + version: file:packages/plugin-rsc/examples/basic/test-dep/server-in-server(react@19.1.0) + tailwindcss: + specifier: ^4.1.4 + version: 4.1.11 + vite: + specifier: ^7.0.2 + version: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + vite-plugin-inspect: + specifier: ^11.2.0 + version: 11.3.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + + packages/plugin-rsc/examples/hono: + dependencies: + '@vitejs/plugin-rsc': + specifier: workspace:* + version: link:../.. + hono: + specifier: ^4.7.5 + version: 4.8.3 + + packages/plugin-rsc/examples/react-router: + dependencies: + '@vitejs/plugin-rsc': + specifier: workspace:* + version: link:../.. + react: + specifier: latest + version: 19.1.0 + react-dom: + specifier: latest + version: 19.1.0(react@19.1.0) + react-router: + specifier: 0.0.0-experimental-23decd7bc + version: 0.0.0-experimental-23decd7bc(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + devDependencies: + '@cloudflare/vite-plugin': + specifier: ^1.8.0 + version: 1.9.0(rollup@4.44.1)(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1))(workerd@1.20250617.0)(wrangler@4.23.0) + '@react-router/dev': + specifier: 0.0.0-experimental-23decd7bc + version: 0.0.0-experimental-23decd7bc(@types/node@22.16.0)(babel-plugin-macros@3.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@0.0.0-experimental-23decd7bc(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(tsx@4.20.3)(typescript@5.8.3)(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1))(wrangler@4.23.0)(yaml@2.7.1) + '@tailwindcss/typography': + specifier: ^0.5.16 + version: 0.5.16(tailwindcss@4.1.11) + '@tailwindcss/vite': + specifier: ^4.1.4 + version: 4.1.11(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + '@types/react': + specifier: latest + version: 19.1.8 + '@types/react-dom': + specifier: latest + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: latest + version: 4.6.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + tailwindcss: + specifier: ^4.1.4 + version: 4.1.11 + vite: + specifier: ^7.0.2 + version: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + vite-plugin-inspect: + specifier: ^11.2.0 + version: 11.3.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + wrangler: + specifier: ^4.22.0 + version: 4.23.0 + + packages/plugin-rsc/examples/ssg: + dependencies: + '@vitejs/plugin-rsc': + specifier: workspace:* + version: link:../.. + react: + specifier: latest + version: 19.1.0 + react-dom: + specifier: latest + version: 19.1.0(react@19.1.0) + devDependencies: + '@mdx-js/rollup': + specifier: ^3.1.0 + version: 3.1.0(rollup@4.44.1) + '@types/react': + specifier: latest + version: 19.1.8 + '@types/react-dom': + specifier: latest + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: latest + version: 4.6.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + vite-plugin-inspect: + specifier: latest + version: 11.3.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + + packages/plugin-rsc/examples/starter: + dependencies: + '@vitejs/plugin-rsc': + specifier: workspace:* + version: link:../.. + react: + specifier: latest + version: 19.1.0 + react-dom: + specifier: latest + version: 19.1.0(react@19.1.0) + devDependencies: + '@types/react': + specifier: latest + version: 19.1.8 + '@types/react-dom': + specifier: latest + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: latest + version: 4.6.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + vite-plugin-inspect: + specifier: latest + version: 11.3.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + + packages/plugin-rsc/examples/starter-cf-single: + dependencies: + '@vitejs/plugin-rsc': + specifier: workspace:* + version: link:../.. + react: + specifier: latest + version: 19.1.0 + react-dom: + specifier: latest + version: 19.1.0(react@19.1.0) + devDependencies: + '@cloudflare/vite-plugin': + specifier: ^1.8.0 + version: 1.9.0(rollup@4.44.1)(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1))(workerd@1.20250617.0)(wrangler@4.23.0) + '@types/react': + specifier: latest + version: 19.1.8 + '@types/react-dom': + specifier: latest + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: latest + version: 4.6.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + playground: devDependencies: kill-port: @@ -725,6 +977,16 @@ packages: resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.27.1': + resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.25.9': resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} @@ -739,10 +1001,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.27.1': resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.9': resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} @@ -793,6 +1069,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-decorators@7.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.27.1': resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} @@ -805,6 +1087,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-development@7.27.1': resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} engines: {node: '>=6.9.0'} @@ -829,6 +1123,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.27.1': + resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.23.5': resolution: {integrity: sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==} engines: {node: '>=6.9.0'} @@ -861,6 +1167,71 @@ packages: resolution: {integrity: sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==} engines: {node: '>=6.9.0'} + '@cloudflare/kv-asset-handler@0.4.0': + resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.3.3': + resolution: {integrity: sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==} + peerDependencies: + unenv: 2.0.0-rc.17 + workerd: ^1.20250508.0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/vite-plugin@1.9.0': + resolution: {integrity: sha512-YYmWZklDPF7Ay97JX51bZzKGNP7Z6Sme0+Pje1g5Jr7M6oU6L3NmmvIi8VKFLM48FRlSpXRmTF1tULJng6d6vg==} + peerDependencies: + vite: ^6.1.0 || ^7.0.0 + wrangler: ^3.101.0 || ^4.0.0 + + '@cloudflare/workerd-darwin-64@1.20250617.0': + resolution: {integrity: sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20250617.0': + resolution: {integrity: sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20250617.0': + resolution: {integrity: sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20250617.0': + resolution: {integrity: sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20250617.0': + resolution: {integrity: sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@conventional-changelog/git-client@2.5.1': + resolution: {integrity: sha512-lAw7iA5oTPWOLjiweb7DlGEMDEvzqzLLa6aWOly2FSZ64IwLE8T458rC+o+WvI31Doz6joM7X2DoNog7mX8r4A==} + engines: {node: '>=18'} + peerDependencies: + conventional-commits-filter: ^5.0.0 + conventional-commits-parser: ^6.1.0 + peerDependenciesMeta: + conventional-commits-filter: + optional: true + conventional-commits-parser: + optional: true + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} @@ -945,6 +1316,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.5': resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} engines: {node: '>=18'} @@ -963,6 +1340,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.5': resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} engines: {node: '>=18'} @@ -981,6 +1364,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.5': resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} engines: {node: '>=18'} @@ -999,6 +1388,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.5': resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} engines: {node: '>=18'} @@ -1017,6 +1412,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.5': resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} engines: {node: '>=18'} @@ -1035,6 +1436,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.5': resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} engines: {node: '>=18'} @@ -1053,6 +1460,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.5': resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} engines: {node: '>=18'} @@ -1071,6 +1484,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.5': resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} engines: {node: '>=18'} @@ -1089,6 +1508,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.5': resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} engines: {node: '>=18'} @@ -1107,6 +1532,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.5': resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} engines: {node: '>=18'} @@ -1125,6 +1556,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.5': resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} engines: {node: '>=18'} @@ -1143,6 +1580,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.5': resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} engines: {node: '>=18'} @@ -1161,6 +1604,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.5': resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} engines: {node: '>=18'} @@ -1179,6 +1628,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.5': resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} engines: {node: '>=18'} @@ -1197,6 +1652,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.5': resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} engines: {node: '>=18'} @@ -1215,6 +1676,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.5': resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} engines: {node: '>=18'} @@ -1233,6 +1700,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.5': resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} engines: {node: '>=18'} @@ -1251,6 +1724,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.25.5': resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} engines: {node: '>=18'} @@ -1269,6 +1748,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.5': resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} engines: {node: '>=18'} @@ -1287,6 +1772,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.25.5': resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} engines: {node: '>=18'} @@ -1305,6 +1796,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.5': resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} engines: {node: '>=18'} @@ -1323,7 +1820,13 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.5': + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.5': resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} engines: {node: '>=18'} cpu: [x64] @@ -1341,6 +1844,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.5': resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} engines: {node: '>=18'} @@ -1359,6 +1868,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.5': resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} engines: {node: '>=18'} @@ -1377,6 +1892,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.5': resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} engines: {node: '>=18'} @@ -1421,6 +1942,10 @@ packages: resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@generouted/react-router@1.20.0': resolution: {integrity: sha512-VAOdbH/Z2MzroTB61XYsS1uMJvX5am7CKuF/MgF3ZXS6YSvA6rz9pvQxZbg5PIK4R9h7bsA/oRQlJNLSgQKjEA==} peerDependencies: @@ -1428,6 +1953,9 @@ packages: react-router: '>=7' vite: '>=5' + '@hiogawa/utils@1.7.0': + resolution: {integrity: sha512-ghiEFWBR1NENoHn+lSuW7liicTIzVPN+8Srm5UedCTw43gus0mlse6Wp2lz6GmbOXJ/CalMPp/0Tz2X8tajkAg==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1448,6 +1976,119 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -1466,6 +2107,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@mdx-js/mdx@3.0.0': resolution: {integrity: sha512-Icm0TBKBLYqroYbNW3BPnzMGn+7mwpQOK310aZ7+fkCtiU3aqv2cdcX+nd0Ydo3wI5Rx8bX2Z2QmGb/XcAClCw==} @@ -1474,6 +2118,12 @@ packages: peerDependencies: rollup: '>=2' + '@mjackson/node-fetch-server@0.2.0': + resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==} + + '@mjackson/node-fetch-server@0.6.1': + resolution: {integrity: sha512-9ZJnk/DJjt805uv5PPv11haJIW+HHf3YEEyVXv+8iLQxLD/iXA68FH220XoiTPBC4gCg5q+IMadDw8qPqlA5wg==} + '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} @@ -1489,6 +2139,18 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@npmcli/git@4.1.0': + resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@npmcli/package-json@4.0.1': + resolution: {integrity: sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@npmcli/promise-spawn@6.0.2': + resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@oxc-project/runtime@0.73.0': resolution: {integrity: sha512-YFvBzVQK/ix0RQxOI02ebCumehSHoiJgvb7nOU4o7xFoMnnujLdjmxnEBK/qiOQrEyXlY69gXGMEsKYVe+YZ3A==} engines: {node: '>=6.9.0'} @@ -1503,15 +2165,54 @@ packages: '@oxc-project/types@0.75.0': resolution: {integrity: sha512-QMW+06WOXs7+F301Y3X0VpmWhwuQVc/X/RP2zF9OIwvSMmsif3xURS2wxbakFIABYsytgBcHpUcFepVS0Qnd3A==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@playwright/test@1.53.2': resolution: {integrity: sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==} engines: {node: '>=18'} hasBin: true + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@publint/pack@0.1.2': resolution: {integrity: sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==} engines: {node: '>=18'} + '@quansync/fs@0.1.3': + resolution: {integrity: sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg==} + engines: {node: '>=20.0.0'} + + '@react-router/dev@0.0.0-experimental-23decd7bc': + resolution: {integrity: sha512-iY4WgHNv/7mDbExXQA35u7H54ihPJTrm20Z42Ni2G+Hgz3X4A0ZmeT7CtpfuBzC3UIrqdmNZT3nQOSoKNwJlWA==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@react-router/serve': ^0.0.0-experimental-23decd7bc + react-router: ^0.0.0-experimental-23decd7bc + typescript: ^5.1.0 + vite: ^5.1.0 || ^6.0.0 || ^7.0.0 + wrangler: ^3.28.2 || ^4.0.0 + peerDependenciesMeta: + '@react-router/serve': + optional: true + typescript: + optional: true + wrangler: + optional: true + + '@react-router/node@0.0.0-experimental-23decd7bc': + resolution: {integrity: sha512-y9tOT+jEzBGXrwBjCq2obqKe+N6znsT+I02R/SDFTqTcXrdTJ7aK0iRnWGHxUw/FrOxoPeGkA0v5dWoJI0jBew==} + engines: {node: '>=20.0.0'} + peerDependencies: + react-router: 0.0.0-experimental-23decd7bc + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-beta.16': resolution: {integrity: sha512-dzlvuodUFc/QX97jYSsPHtYysqeSeM5gBxiN+DpV93tXEYyFMWm3cECxNmShz4ZM+lrgm6eG2/txzLZ/z9qWLw==} cpu: [arm64] @@ -1635,6 +2336,9 @@ packages: '@rolldown/pluginutils@1.0.0-beta.16': resolution: {integrity: sha512-w3f87JpF7lgIlK03I0R3XidspFgB4MsixE5o/VjBMJI+Ki4XW/Ffrykmj2AUCbVxhRD7Pi9W0Qu2XapJhB2mSA==} + '@rolldown/pluginutils@1.0.0-beta.19': + resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} + '@rolldown/pluginutils@1.0.0-beta.23': resolution: {integrity: sha512-lLCP4LUecUGBLq8EfkbY2esGYyvZj5ee+WZG12+mVnQH48b46SVbwp+0vJkD+6Pnsc+u9SWarBV9sQ5mVwmb5g==} @@ -1901,6 +2605,14 @@ packages: cpu: [x64] os: [win32] + '@simple-libs/child-process-utils@1.0.1': + resolution: {integrity: sha512-3nWd8irxvDI6v856wpPCHZ+08iQR0oHTZfzAZmnbsLzf+Sf1odraP6uKOHDZToXq3RPRV/LbqGVlSCogm9cJjg==} + engines: {node: '>=18'} + + '@simple-libs/stream-utils@1.1.0': + resolution: {integrity: sha512-6rsHTjodIn/t90lv5snQjRPVtOosM7Vp0AKdrObymq45ojlgVwnpAqdc+0OBBrpEiy31zZ6/TKeIVqV1HwvnuQ==} + engines: {node: '>=18'} + '@swc/core-darwin-arm64@1.12.9': resolution: {integrity: sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==} engines: {node: '>=10'} @@ -1982,10 +2694,108 @@ packages: '@swc/types@0.1.23': resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + '@tailwindcss/node@4.1.11': + resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} + + '@tailwindcss/oxide-android-arm64@4.1.11': + resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.11': + resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.11': + resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} + engines: {node: '>= 10'} + + '@tailwindcss/typography@0.5.16': + resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@tailwindcss/vite@4.1.11': + resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@tsconfig/strictest@2.0.5': + resolution: {integrity: sha512-ec4tjL2Rr0pkZ5hww65c+EEPYwxOi4Ryv+0MtjeaSQRJyq322Q27eOQiFbuNgw2hpL4hB1/W/HBGk3VKS43osg==} + '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -2019,9 +2829,6 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2052,6 +2859,9 @@ packages: '@types/node@22.16.0': resolution: {integrity: sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==} + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -2287,8 +3097,34 @@ packages: cpu: [x64] os: [win32] - '@vitejs/release-scripts@1.5.0': - resolution: {integrity: sha512-rZQdM5AneNJHzDOTUaQOOifauH6MkGTSI+GH8bKKrimBaa5BtvpnE1iz43fJ4QDO7RdGxAlxWnPQAVlFhGM1cQ==} + '@vitejs/plugin-react@4.6.0': + resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + + '@vitejs/release-scripts@1.6.0': + resolution: {integrity: sha512-XV+w22Fvn+wqDtEkz8nQIJzvmRVSh90c2xvOO7cX9fkX8+39ZJpYRiXDIRJG1JRnF8khm1rHjulid+l+khc7TQ==} + + '@vitejs/test-dep-client-in-server2@file:packages/plugin-rsc/examples/basic/test-dep/client-in-server2': + resolution: {directory: packages/plugin-rsc/examples/basic/test-dep/client-in-server2, type: directory} + peerDependencies: + react: '*' + + '@vitejs/test-dep-client-in-server@file:packages/plugin-rsc/examples/basic/test-dep/client-in-server': + resolution: {directory: packages/plugin-rsc/examples/basic/test-dep/client-in-server, type: directory} + peerDependencies: + react: '*' + + '@vitejs/test-dep-server-in-client@file:packages/plugin-rsc/examples/basic/test-dep/server-in-client': + resolution: {directory: packages/plugin-rsc/examples/basic/test-dep/server-in-client, type: directory} + peerDependencies: + react: '*' + + '@vitejs/test-dep-server-in-server@file:packages/plugin-rsc/examples/basic/test-dep/server-in-server': + resolution: {directory: packages/plugin-rsc/examples/basic/test-dep/server-in-server, type: directory} + peerDependencies: + react: '*' '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -2324,6 +3160,19 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-loose@8.5.2: + resolution: {integrity: sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==} + engines: {node: '>=0.4.0'} + + acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.14.1: resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} @@ -2341,6 +3190,10 @@ packages: resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} engines: {node: '>=18'} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-regex@6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} @@ -2357,13 +3210,26 @@ packages: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + as-table@1.0.55: + resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-kit@2.1.1: + resolution: {integrity: sha512-mfh6a7gKXE8pDlxTvqIc/syH/P3RkzbOF6LeHdcKztLEzYe6IMsRCL7N8vI7hqTGWNxpkCuuRTpT21xNWqhRtQ==} + engines: {node: '>=20.18.0'} + astring@1.8.6: resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} hasBin: true @@ -2375,6 +3241,9 @@ packages: peerDependencies: postcss: ^8.1.0 + babel-dead-code-elimination@1.0.10: + resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==} + babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -2388,6 +3257,12 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + birpc@2.4.0: + resolution: {integrity: sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg==} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -2406,6 +3281,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2454,6 +3333,14 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} @@ -2475,6 +3362,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -2499,6 +3393,9 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2512,12 +3409,43 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + conventional-changelog-conventionalcommits@9.0.0: + resolution: {integrity: sha512-5e48V0+DsWvQBEnnbBFhYQwYDzFPXVrakGPP1uSxekDkr5d7YWrmaWsgJpKFR0SkXmxK6qQr9O42uuLb9wpKxA==} + engines: {node: '>=18'} + + conventional-changelog-preset-loader@5.0.0: + resolution: {integrity: sha512-SetDSntXLk8Jh1NOAl1Gu5uLiCNSYenB5tm0YVeZKePRIgDW9lQImromTwLa3c/Gae298tsgOM+/CYT9XAl0NA==} + engines: {node: '>=18'} + + conventional-changelog-writer@8.1.0: + resolution: {integrity: sha512-dpC440QnORNCO81XYuRRFOLCsjKj4W7tMkUIn3lR6F/FAaJcWLi7iCj6IcEvSQY2zw6VUgwUKd5DEHKEWrpmEQ==} + engines: {node: '>=18'} + hasBin: true + + conventional-changelog@7.1.0: + resolution: {integrity: sha512-2hHa/MpDunPnYK3QcZdHl4MOnyLlicBmohsM5/dfvfeoPp0faIjYKHbKyb8nKth/Zd4HhxtlMWfVKrmM9OMj/Q==} + engines: {node: '>=18'} + hasBin: true + + conventional-commits-filter@5.0.0: + resolution: {integrity: sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==} + engines: {node: '>=18'} + + conventional-commits-parser@6.2.0: + resolution: {integrity: sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag==} + engines: {node: '>=18'} + hasBin: true + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} @@ -2588,6 +3516,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-uri-to-buffer@2.0.2: + resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -2613,6 +3544,14 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + dedent@1.6.0: + resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -2624,6 +3563,18 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -2635,9 +3586,17 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + engines: {node: '>=0.3.1'} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -2651,16 +3610,46 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dts-resolver@2.1.1: + resolution: {integrity: sha512-3BiGFhB6mj5Kv+W2vdJseQUYW+SKVzAFJL6YNP6ursbrwy1fXHRotfHi3xLNxe4wZl/K8qbAFeCDjZLjzqxxRw==} + engines: {node: '>=20.18.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.129: resolution: {integrity: sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==} emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + enhanced-resolve@5.17.1: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2669,9 +3658,15 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -2685,6 +3680,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.25.5: resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} @@ -2814,6 +3814,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + expect-type@1.2.1: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} @@ -2840,6 +3844,9 @@ packages: fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + fd-package-json@1.2.0: + resolution: {integrity: sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA==} + fdir@6.4.3: resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: @@ -2893,6 +3900,10 @@ packages: flatted@3.2.9: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -2939,6 +3950,13 @@ packages: resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} engines: {node: '>=18'} + get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} + + get-source@2.0.12: + resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -2957,6 +3975,13 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -2979,6 +4004,11 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2999,9 +4029,21 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.8.3: + resolution: {integrity: sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ==} + engines: {node: '>=16.9.0'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hosted-git-info@6.1.3: + resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + hosted-git-info@8.1.0: + resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==} + engines: {node: ^18.17.0 || >=20.5.0} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -3037,6 +4079,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} @@ -3044,10 +4089,19 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-fullwidth-code-point@4.0.0: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} engines: {node: '>=12'} @@ -3063,6 +4117,11 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} @@ -3070,6 +4129,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -3084,9 +4147,16 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -3120,6 +4190,10 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3236,6 +4310,12 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -3245,6 +4325,9 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -3262,9 +4345,16 @@ packages: loupe@3.1.4: resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -3305,6 +4395,10 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -3400,6 +4494,11 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -3408,6 +4507,11 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + miniflare@4.20250617.5: + resolution: {integrity: sha512-Qqn30jR6dCjXaKVizT6vH4KOb+GyLccoxLNOJEfu63yBPn8eoXa7PrdiSGTmjs2RY8/tr7eTO8Wu/Yr14k0xVA==} + engines: {node: '>=18.0.0'} + hasBin: true + minimatch@10.0.1: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} @@ -3419,6 +4523,22 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mkdist@2.2.0: resolution: {integrity: sha512-GfKwu4A2grXfhj2TZm4ydfzP515NaALqKaPq4WqaZ6NhEnD47BiIQPySoCTTvVqHxYcuqVkNdCXjYf9Bz1Y04Q==} hasBin: true @@ -3444,9 +4564,17 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3460,6 +4588,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -3472,10 +4603,34 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + normalize-package-data@5.0.0: + resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + normalize-package-data@7.0.0: + resolution: {integrity: sha512-k6U0gKRIuNCTkwHGZqblCfLfBRh+w1vI6tBo+IeJwq2M8FUiOqhX7GH+GArQGScA7azd1WfyRCvxoXDO3hQDIA==} + engines: {node: ^18.17.0 || >=20.5.0} + normalize-range@0.1.2: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + npm-install-checks@6.3.0: + resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-normalize-package-bin@3.0.1: + resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-package-arg@10.1.0: + resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-pick-manifest@8.0.2: + resolution: {integrity: sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-run-path@5.1.0: resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3487,6 +4642,9 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -3495,6 +4653,10 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + open@10.1.2: + resolution: {integrity: sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==} + engines: {node: '>=18'} + optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -3507,8 +4669,11 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - package-manager-detector@0.2.11: - resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -3536,6 +4701,13 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3550,9 +4722,15 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + periscopic@4.0.2: + resolution: {integrity: sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3746,6 +4924,10 @@ packages: peerDependencies: postcss: ^8.4.31 + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} @@ -3785,6 +4967,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + prettier@3.1.0: resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} engines: {node: '>=14'} @@ -3794,6 +4981,25 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} + printable-characters@1.0.42: + resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + + proc-log@3.0.0: + resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3804,8 +5010,8 @@ packages: property-information@6.4.0: resolution: {integrity: sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ==} - publint@0.3.9: - resolution: {integrity: sha512-irTwfRfYW38vomkxxoiZQtFtUOQKpz5m0p9Z60z4xpXrl1KmvSrX1OMARvnnolB5usOXeNfvLj6d/W3rwXKfBQ==} + publint@0.3.12: + resolution: {integrity: sha512-1w3MMtL9iotBjm1mmXtG3Nk06wnq9UhGNRpQ2j6n1Zq7YAD6gnxMMZMIxlRPAydVjVbjSm+n0lhwqsD1m4LD5w==} engines: {node: '>=18'} hasBin: true @@ -3840,6 +5046,10 @@ packages: react-is@19.1.0: resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -3851,6 +5061,16 @@ packages: react: '>=18' react-dom: '>=18' + react-router@0.0.0-experimental-23decd7bc: + resolution: {integrity: sha512-oTDa74rdP6WACxX8wihI71TiwQa+3aAXNjGGm20OAyA4hGdfe0VBEbJvuIT0vxR+LKsJisI4rpaq0boBGY3m+g==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react-router@7.6.3: resolution: {integrity: sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==} engines: {node: '>=20.0.0'} @@ -3861,6 +5081,14 @@ packages: react-dom: optional: true + react-server-dom-webpack@19.1.0: + resolution: {integrity: sha512-GUbawkNSN0oj8GnuNhMzsvyIHpXqqpAmyOY5NRqNNQ/M8wvUUN8YBoGjDUj9lbmBrmAHS65BByp6325CcWA0eg==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.1.0 + react-dom: ^19.1.0 + webpack: ^5.59.0 + react-switch@7.1.0: resolution: {integrity: sha512-4xVeyImZE8QOTDw2FmhWz0iqo2psoRiS7XzdjaZBCIP8Dzo3rT0esHUjLee5WsAPSFXWWl1eVA5arp9n2C6yQA==} peerDependencies: @@ -3875,6 +5103,10 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3910,6 +5142,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3917,6 +5153,22 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown-plugin-dts@0.13.13: + resolution: {integrity: sha512-Nchx9nQoa4IpfQ/BJzodKMvtJ3H3dT322siAJSp3uvQJ+Pi1qgEjOp7hSQwGSQRhaC5gC+9hparbWEH5oiAL9Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + '@typescript/native-preview': '>=7.0.0-dev.20250601.1' + rolldown: ^1.0.0-beta.9 + typescript: ^5.0.0 + vue-tsc: ~2.2.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + rolldown-vite@6.3.21: resolution: {integrity: sha512-mjds/3g+YPWJmT08oQic/L5sWvs/lNc4vs9vmD7uHQtGdP7qGriWtYf62Vp+6eQhd/MPeFVw71TMEEt/cH+sLQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -3982,6 +5234,13 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rsc-html-stream@0.0.6: + resolution: {integrity: sha512-oZUJ5AH0oDo9QywxD9yMY6N5Z3VwX2YfQg0FanNdCmvXmO0itTfv7BMkbMSwxg7JmBjYmefU8DTW0EcLsePPgQ==} + + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4022,6 +5281,10 @@ packages: shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -4044,6 +5307,13 @@ packages: resolution: {integrity: sha512-N+goiLxlkHJlyaYEglFypzVNMaNplPAk5syu0+OPp/Bk6dwVoXF6FfOw2vO0Dp+JHsBaI+w6cm8TnFl2Hw6tDA==} hasBin: true + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -4063,6 +5333,10 @@ packages: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} @@ -4070,6 +5344,18 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -4077,13 +5363,28 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stacktracey@2.1.8: + resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.0.0: resolution: {integrity: sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==} engines: {node: '>=18'} @@ -4091,6 +5392,10 @@ packages: stringify-entities@4.0.3: resolution: {integrity: sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-ansi@7.1.0: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} @@ -4144,16 +5449,26 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tailwindcss@4.1.11: + resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} + tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyglobby@0.2.12: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} @@ -4178,6 +5493,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -4195,6 +5514,28 @@ packages: peerDependencies: typescript: '>=4.0.0' + tsdown@0.12.9: + resolution: {integrity: sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + publint: ^0.3.0 + typescript: ^5.0.0 + unplugin-lightningcss: ^0.4.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + publint: + optional: true + typescript: + optional: true + unplugin-lightningcss: + optional: true + unplugin-unused: + optional: true + tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} @@ -4206,6 +5547,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + turbo-stream@3.1.0: + resolution: {integrity: sha512-tVI25WEXl4fckNEmrq70xU1XumxUwEx/FZD5AgEcV8ri7Wvrg2o7GEq8U7htrNx3CajciGm+kDyhRf5JB6t7/A==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4225,6 +5569,14 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + unbuild@3.5.0: resolution: {integrity: sha512-DPFttsiADnHRb/K+yJ9r9jdn6JyXlsmdT0S12VFC14DFSJD+cxBnHq+v0INmqqPVPxOoUjvJFYUVIb02rWnVeA==} hasBin: true @@ -4234,9 +5586,19 @@ packages: typescript: optional: true + unconfig@7.3.2: + resolution: {integrity: sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + + unenv@2.0.0-rc.17: + resolution: {integrity: sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==} + unified@11.0.4: resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} @@ -4265,6 +5627,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unplugin-utils@0.2.4: + resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==} + engines: {node: '>=18.12.0'} + unrs-resolver@1.9.2: resolution: {integrity: sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==} @@ -4284,17 +5650,52 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@0.41.0: + resolution: {integrity: sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} vfile@6.0.1: resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} + vite-dev-rpc@1.1.0: + resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-plugin-inspect@11.3.0: + resolution: {integrity: sha512-vmt7K1WVKQkuiwvsM6e5h3HDJ2pSWTnzoj+JP9Kvu3Sh2G+nFap1F1V7tqpyA4qFxM1GQ84ryffWFGQrwShERQ==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + vite@7.0.1: resolution: {integrity: sha512-BiKOQoW5HGR30E6JDeNsati6HnSPMVEKbkIWbCiol+xKeu3g5owrjy7kbk/QEMuzCV87dSUTvycYKmlcfGKq3Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4375,6 +5776,14 @@ packages: yaml: optional: true + vitefu@1.1.0: + resolution: {integrity: sha512-AiG/L9DVsEYHWQ9jAEnke0nKiASlPw+JYwDl6Z4l6a6/IqT1tKseEl6R5+rVnKJt/K3jCTWiQvgoIh5MuqBJJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -4403,27 +5812,81 @@ packages: jsdom: optional: true + walk-up-path@3.0.1: + resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} + web-streams-polyfill@3.2.1: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + engines: {node: '>=10.13.0'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + which@3.0.1: + resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + workerd@1.20250617.0: + resolution: {integrity: sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.23.0: + resolution: {integrity: sha512-JSeDt3IwA4TEmg/V3tRblImPjdxynBt9PUVO/acQJ83XGlMMSwswDKL1FuwvbFzgX6+JXc3GMHeu7r8AQIxw9w==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20250617.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrap-ansi@9.0.0: resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} engines: {node: '>=18'} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -4437,6 +5900,15 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + youch@3.3.4: + resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + + zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4504,6 +5976,26 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.27.7)': + dependencies: + '@babel/core': 7.27.7 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.27.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.27.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-imports@7.25.9': dependencies: '@babel/traverse': 7.27.4 @@ -4527,8 +6019,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.27.7 + '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-replace-supers@7.27.1(@babel/core@7.27.7)': + dependencies: + '@babel/core': 7.27.7 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.27.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.25.9': {} '@babel/helper-string-parser@7.27.1': {} @@ -4566,6 +6078,11 @@ snapshots: '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-pipeline-operator': 7.27.1(@babel/core@7.27.7) + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.27.7)': + dependencies: + '@babel/core': 7.27.7 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -4576,6 +6093,19 @@ snapshots: '@babel/core': 7.27.7 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.7)': + dependencies: + '@babel/core': 7.27.7 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.27.7)': + dependencies: + '@babel/core': 7.27.7 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.7) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.27.7)': dependencies: '@babel/core': 7.27.7 @@ -4604,14 +6134,36 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime@7.23.5': + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.27.7)': dependencies: - regenerator-runtime: 0.14.0 + '@babel/core': 7.27.7 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.27.7) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.7) + transitivePeerDependencies: + - supports-color - '@babel/template@7.27.2': + '@babel/preset-typescript@7.27.1(@babel/core@7.27.7)': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.7 + '@babel/core': 7.27.7 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.7) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.27.7) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.27.7) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.23.5': + dependencies: + regenerator-runtime: 0.14.0 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.27.7 '@babel/types': 7.27.7 '@babel/traverse@7.27.4': @@ -4658,6 +6210,63 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@cloudflare/kv-asset-handler@0.4.0': + dependencies: + mime: 3.0.0 + + '@cloudflare/unenv-preset@2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0)': + dependencies: + unenv: 2.0.0-rc.17 + optionalDependencies: + workerd: 1.20250617.0 + + '@cloudflare/vite-plugin@1.9.0(rollup@4.44.1)(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1))(workerd@1.20250617.0)(wrangler@4.23.0)': + dependencies: + '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0) + '@mjackson/node-fetch-server': 0.6.1 + '@rollup/plugin-replace': 6.0.2(rollup@4.44.1) + get-port: 7.1.0 + miniflare: 4.20250617.5 + picocolors: 1.1.1 + tinyglobby: 0.2.14 + unenv: 2.0.0-rc.17 + vite: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + wrangler: 4.23.0 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - rollup + - utf-8-validate + - workerd + + '@cloudflare/workerd-darwin-64@1.20250617.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20250617.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20250617.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20250617.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20250617.0': + optional: true + + '@conventional-changelog/git-client@2.5.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.0)': + dependencies: + '@simple-libs/child-process-utils': 1.0.1 + '@simple-libs/stream-utils': 1.1.0 + semver: 7.7.2 + optionalDependencies: + conventional-commits-filter: 5.0.0 + conventional-commits-parser: 6.2.0 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@emnapi/core@1.4.3': dependencies: '@emnapi/wasi-threads': 1.0.2 @@ -4771,6 +6380,9 @@ snapshots: '@esbuild/aix-ppc64@0.25.2': optional: true + '@esbuild/aix-ppc64@0.25.4': + optional: true + '@esbuild/aix-ppc64@0.25.5': optional: true @@ -4780,6 +6392,9 @@ snapshots: '@esbuild/android-arm64@0.25.2': optional: true + '@esbuild/android-arm64@0.25.4': + optional: true + '@esbuild/android-arm64@0.25.5': optional: true @@ -4789,6 +6404,9 @@ snapshots: '@esbuild/android-arm@0.25.2': optional: true + '@esbuild/android-arm@0.25.4': + optional: true + '@esbuild/android-arm@0.25.5': optional: true @@ -4798,6 +6416,9 @@ snapshots: '@esbuild/android-x64@0.25.2': optional: true + '@esbuild/android-x64@0.25.4': + optional: true + '@esbuild/android-x64@0.25.5': optional: true @@ -4807,6 +6428,9 @@ snapshots: '@esbuild/darwin-arm64@0.25.2': optional: true + '@esbuild/darwin-arm64@0.25.4': + optional: true + '@esbuild/darwin-arm64@0.25.5': optional: true @@ -4816,6 +6440,9 @@ snapshots: '@esbuild/darwin-x64@0.25.2': optional: true + '@esbuild/darwin-x64@0.25.4': + optional: true + '@esbuild/darwin-x64@0.25.5': optional: true @@ -4825,6 +6452,9 @@ snapshots: '@esbuild/freebsd-arm64@0.25.2': optional: true + '@esbuild/freebsd-arm64@0.25.4': + optional: true + '@esbuild/freebsd-arm64@0.25.5': optional: true @@ -4834,6 +6464,9 @@ snapshots: '@esbuild/freebsd-x64@0.25.2': optional: true + '@esbuild/freebsd-x64@0.25.4': + optional: true + '@esbuild/freebsd-x64@0.25.5': optional: true @@ -4843,6 +6476,9 @@ snapshots: '@esbuild/linux-arm64@0.25.2': optional: true + '@esbuild/linux-arm64@0.25.4': + optional: true + '@esbuild/linux-arm64@0.25.5': optional: true @@ -4852,6 +6488,9 @@ snapshots: '@esbuild/linux-arm@0.25.2': optional: true + '@esbuild/linux-arm@0.25.4': + optional: true + '@esbuild/linux-arm@0.25.5': optional: true @@ -4861,6 +6500,9 @@ snapshots: '@esbuild/linux-ia32@0.25.2': optional: true + '@esbuild/linux-ia32@0.25.4': + optional: true + '@esbuild/linux-ia32@0.25.5': optional: true @@ -4870,6 +6512,9 @@ snapshots: '@esbuild/linux-loong64@0.25.2': optional: true + '@esbuild/linux-loong64@0.25.4': + optional: true + '@esbuild/linux-loong64@0.25.5': optional: true @@ -4879,6 +6524,9 @@ snapshots: '@esbuild/linux-mips64el@0.25.2': optional: true + '@esbuild/linux-mips64el@0.25.4': + optional: true + '@esbuild/linux-mips64el@0.25.5': optional: true @@ -4888,6 +6536,9 @@ snapshots: '@esbuild/linux-ppc64@0.25.2': optional: true + '@esbuild/linux-ppc64@0.25.4': + optional: true + '@esbuild/linux-ppc64@0.25.5': optional: true @@ -4897,6 +6548,9 @@ snapshots: '@esbuild/linux-riscv64@0.25.2': optional: true + '@esbuild/linux-riscv64@0.25.4': + optional: true + '@esbuild/linux-riscv64@0.25.5': optional: true @@ -4906,6 +6560,9 @@ snapshots: '@esbuild/linux-s390x@0.25.2': optional: true + '@esbuild/linux-s390x@0.25.4': + optional: true + '@esbuild/linux-s390x@0.25.5': optional: true @@ -4915,6 +6572,9 @@ snapshots: '@esbuild/linux-x64@0.25.2': optional: true + '@esbuild/linux-x64@0.25.4': + optional: true + '@esbuild/linux-x64@0.25.5': optional: true @@ -4924,6 +6584,9 @@ snapshots: '@esbuild/netbsd-arm64@0.25.2': optional: true + '@esbuild/netbsd-arm64@0.25.4': + optional: true + '@esbuild/netbsd-arm64@0.25.5': optional: true @@ -4933,6 +6596,9 @@ snapshots: '@esbuild/netbsd-x64@0.25.2': optional: true + '@esbuild/netbsd-x64@0.25.4': + optional: true + '@esbuild/netbsd-x64@0.25.5': optional: true @@ -4942,6 +6608,9 @@ snapshots: '@esbuild/openbsd-arm64@0.25.2': optional: true + '@esbuild/openbsd-arm64@0.25.4': + optional: true + '@esbuild/openbsd-arm64@0.25.5': optional: true @@ -4951,6 +6620,9 @@ snapshots: '@esbuild/openbsd-x64@0.25.2': optional: true + '@esbuild/openbsd-x64@0.25.4': + optional: true + '@esbuild/openbsd-x64@0.25.5': optional: true @@ -4960,6 +6632,9 @@ snapshots: '@esbuild/sunos-x64@0.25.2': optional: true + '@esbuild/sunos-x64@0.25.4': + optional: true + '@esbuild/sunos-x64@0.25.5': optional: true @@ -4969,6 +6644,9 @@ snapshots: '@esbuild/win32-arm64@0.25.2': optional: true + '@esbuild/win32-arm64@0.25.4': + optional: true + '@esbuild/win32-arm64@0.25.5': optional: true @@ -4978,6 +6656,9 @@ snapshots: '@esbuild/win32-ia32@0.25.2': optional: true + '@esbuild/win32-ia32@0.25.4': + optional: true + '@esbuild/win32-ia32@0.25.5': optional: true @@ -4987,6 +6668,9 @@ snapshots: '@esbuild/win32-x64@0.25.2': optional: true + '@esbuild/win32-x64@0.25.4': + optional: true + '@esbuild/win32-x64@0.25.5': optional: true @@ -5034,6 +6718,8 @@ snapshots: '@eslint/core': 0.14.0 levn: 0.4.1 + '@fastify/busboy@2.1.1': {} + '@generouted/react-router@1.20.0(react-router-dom@7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1))': dependencies: fast-glob: 3.3.3 @@ -5044,6 +6730,8 @@ snapshots: transitivePeerDependencies: - react-router-dom + '@hiogawa/utils@1.7.0': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -5057,6 +6745,94 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.4.3 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -5074,9 +6850,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@mdx-js/mdx@3.0.0': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/estree-jsx': 1.0.3 '@types/hast': 3.0.3 '@types/mdx': 2.0.10 @@ -5112,6 +6893,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@mjackson/node-fetch-server@0.2.0': {} + + '@mjackson/node-fetch-server@0.6.1': {} + '@napi-rs/wasm-runtime@0.2.11': dependencies: '@emnapi/core': 1.4.3 @@ -5131,6 +6916,35 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + '@npmcli/git@4.1.0': + dependencies: + '@npmcli/promise-spawn': 6.0.2 + lru-cache: 7.18.3 + npm-pick-manifest: 8.0.2 + proc-log: 3.0.0 + promise-inflight: 1.0.1 + promise-retry: 2.0.1 + semver: 7.7.2 + which: 3.0.1 + transitivePeerDependencies: + - bluebird + + '@npmcli/package-json@4.0.1': + dependencies: + '@npmcli/git': 4.1.0 + glob: 10.4.5 + hosted-git-info: 6.1.3 + json-parse-even-better-errors: 3.0.2 + normalize-package-data: 5.0.0 + proc-log: 3.0.0 + semver: 7.7.2 + transitivePeerDependencies: + - bluebird + + '@npmcli/promise-spawn@6.0.2': + dependencies: + which: 3.0.1 + '@oxc-project/runtime@0.73.0': {} '@oxc-project/runtime@0.75.0': {} @@ -5139,12 +6953,78 @@ snapshots: '@oxc-project/types@0.75.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@playwright/test@1.53.2': dependencies: playwright: 1.53.2 + '@polka/url@1.0.0-next.29': {} + '@publint/pack@0.1.2': {} + '@quansync/fs@0.1.3': + dependencies: + quansync: 0.2.10 + + '@react-router/dev@0.0.0-experimental-23decd7bc(@types/node@22.16.0)(babel-plugin-macros@3.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@0.0.0-experimental-23decd7bc(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(tsx@4.20.3)(typescript@5.8.3)(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1))(wrangler@4.23.0)(yaml@2.7.1)': + dependencies: + '@babel/core': 7.27.7 + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.7 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.27.7) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.7) + '@babel/preset-typescript': 7.27.1(@babel/core@7.27.7) + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 + '@npmcli/package-json': 4.0.1 + '@react-router/node': 0.0.0-experimental-23decd7bc(react-router@0.0.0-experimental-23decd7bc(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.8.3) + arg: 5.0.2 + babel-dead-code-elimination: 1.0.10 + chokidar: 4.0.3 + dedent: 1.6.0(babel-plugin-macros@3.1.0) + es-module-lexer: 1.7.0 + exit-hook: 2.2.1 + jsesc: 3.0.2 + lodash: 4.17.21 + pathe: 1.1.2 + picocolors: 1.1.1 + prettier: 2.8.8 + react-refresh: 0.14.2 + react-router: 0.0.0-experimental-23decd7bc(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + semver: 7.7.2 + set-cookie-parser: 2.7.1 + tinyglobby: 0.2.14 + valibot: 0.41.0(typescript@5.8.3) + vite: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + vite-node: 3.2.4(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + optionalDependencies: + typescript: 5.8.3 + wrangler: 4.23.0 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - bluebird + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@react-router/node@0.0.0-experimental-23decd7bc(react-router@0.0.0-experimental-23decd7bc(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.8.3)': + dependencies: + '@mjackson/node-fetch-server': 0.2.0 + react-router: 0.0.0-experimental-23decd7bc(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + typescript: 5.8.3 + '@rolldown/binding-darwin-arm64@1.0.0-beta.16': optional: true @@ -5223,6 +7103,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.16': {} + '@rolldown/pluginutils@1.0.0-beta.19': {} + '@rolldown/pluginutils@1.0.0-beta.23': {} '@rollup/plugin-alias@5.1.1(rollup@4.37.0)': @@ -5264,9 +7146,16 @@ snapshots: optionalDependencies: rollup: 4.37.0 + '@rollup/plugin-replace@6.0.2(rollup@4.44.1)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.44.1) + magic-string: 0.30.17 + optionalDependencies: + rollup: 4.44.1 + '@rollup/pluginutils@5.1.0(rollup@4.44.1)': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: @@ -5274,12 +7163,20 @@ snapshots: '@rollup/pluginutils@5.1.4(rollup@4.37.0)': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: rollup: 4.37.0 + '@rollup/pluginutils@5.1.4(rollup@4.44.1)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.44.1 + '@rollup/rollup-android-arm-eabi@4.37.0': optional: true @@ -5400,6 +7297,15 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.44.1': optional: true + '@simple-libs/child-process-utils@1.0.1': + dependencies: + '@simple-libs/stream-utils': 1.1.0 + '@types/node': 22.16.0 + + '@simple-libs/stream-utils@1.1.0': + dependencies: + '@types/node': 22.16.0 + '@swc/core-darwin-arm64@1.12.9': optional: true @@ -5460,8 +7366,89 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@tailwindcss/node@4.1.11': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.2 + jiti: 2.4.2 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.11 + + '@tailwindcss/oxide-android-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide@4.1.11': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-x64': 4.1.11 + '@tailwindcss/oxide-freebsd-x64': 4.1.11 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-x64-musl': 4.1.11 + '@tailwindcss/oxide-wasm32-wasi': 4.1.11 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 + + '@tailwindcss/typography@0.5.16(tailwindcss@4.1.11)': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.11 + + '@tailwindcss/vite@4.1.11(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1))': + dependencies: + '@tailwindcss/node': 4.1.11 + '@tailwindcss/oxide': 4.1.11 + tailwindcss: 4.1.11 + vite: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + '@trysound/sax@0.2.0': {} + '@tsconfig/strictest@2.0.5': {} + '@tybys/wasm-util@0.9.0': dependencies: tslib: 2.8.1 @@ -5469,7 +7456,7 @@ snapshots: '@types/acorn@4.0.6': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/babel__core@7.20.5': dependencies: @@ -5504,12 +7491,10 @@ snapshots: '@types/estree-jsx@1.0.3': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/estree@1.0.6': {} - '@types/estree@1.0.7': {} - '@types/estree@1.0.8': {} '@types/fs-extra@11.0.4': @@ -5523,7 +7508,7 @@ snapshots: '@types/hoist-non-react-statics@3.3.6': dependencies: - '@types/react': 18.3.20 + '@types/react': 19.1.8 hoist-non-react-statics: 3.3.2 '@types/json-schema@7.0.15': {} @@ -5544,6 +7529,8 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/normalize-package-data@2.4.4': {} + '@types/parse-json@4.0.2': {} '@types/prop-types@15.7.11': {} @@ -5786,14 +7773,46 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.9.2': optional: true - '@vitejs/release-scripts@1.5.0': + '@vitejs/plugin-react@4.6.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1))': + dependencies: + '@babel/core': 7.27.7 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.7) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.7) + '@rolldown/pluginutils': 1.0.0-beta.19 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + transitivePeerDependencies: + - supports-color + + '@vitejs/release-scripts@1.6.0(conventional-commits-filter@5.0.0)': dependencies: + conventional-changelog: 7.1.0(conventional-commits-filter@5.0.0) + conventional-changelog-conventionalcommits: 9.0.0 execa: 8.0.1 mri: 1.2.0 picocolors: 1.1.1 prompts: 2.4.2 - publint: 0.3.9 - semver: 7.7.1 + publint: 0.3.12 + semver: 7.7.2 + transitivePeerDependencies: + - conventional-commits-filter + + '@vitejs/test-dep-client-in-server2@file:packages/plugin-rsc/examples/basic/test-dep/client-in-server2(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@vitejs/test-dep-client-in-server@file:packages/plugin-rsc/examples/basic/test-dep/client-in-server(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@vitejs/test-dep-server-in-client@file:packages/plugin-rsc/examples/basic/test-dep/server-in-client(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@vitejs/test-dep-server-in-server@file:packages/plugin-rsc/examples/basic/test-dep/server-in-server(react@19.1.0)': + dependencies: + react: 19.1.0 '@vitest/expect@3.2.4': dependencies: @@ -5841,6 +7860,14 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-loose@8.5.2: + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.2: {} + + acorn@8.14.0: {} + acorn@8.14.1: {} acorn@8.15.0: {} @@ -5856,6 +7883,8 @@ snapshots: dependencies: environment: 1.1.0 + ansi-regex@5.0.1: {} + ansi-regex@6.0.1: {} ansi-styles@4.3.0: @@ -5866,10 +7895,23 @@ snapshots: ansis@4.1.0: {} + arg@5.0.2: {} + argparse@2.0.1: {} + array-ify@1.0.0: {} + + as-table@1.0.55: + dependencies: + printable-characters: 1.0.42 + assertion-error@2.0.1: {} + ast-kit@2.1.1: + dependencies: + '@babel/parser': 7.27.7 + pathe: 2.0.3 + astring@1.8.6: {} autoprefixer@10.4.21(postcss@8.5.3): @@ -5882,6 +7924,15 @@ snapshots: postcss: 8.5.3 postcss-value-parser: 4.2.0 + babel-dead-code-elimination@1.0.10: + dependencies: + '@babel/core': 7.27.7 + '@babel/parser': 7.27.7 + '@babel/traverse': 7.27.7 + '@babel/types': 7.27.7 + transitivePeerDependencies: + - supports-color + babel-plugin-macros@3.1.0: dependencies: '@babel/runtime': 7.23.5 @@ -5896,6 +7947,10 @@ snapshots: balanced-match@1.0.2: {} + birpc@2.4.0: {} + + blake3-wasm@2.1.5: {} + boolbase@1.0.0: {} brace-expansion@1.1.11: @@ -5918,6 +7973,10 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.1(browserslist@4.24.4) + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + cac@6.7.14: {} callsites@3.1.0: {} @@ -5960,6 +8019,12 @@ snapshots: check-error@2.1.1: {} + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@3.0.0: {} + citty@0.1.6: dependencies: consola: 3.4.2 @@ -5981,6 +8046,16 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + colord@2.9.3: {} colorette@2.0.20: {} @@ -5995,6 +8070,11 @@ snapshots: commondir@1.0.1: {} + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -6003,10 +8083,44 @@ snapshots: consola@3.4.2: {} + conventional-changelog-conventionalcommits@9.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-preset-loader@5.0.0: {} + + conventional-changelog-writer@8.1.0: + dependencies: + conventional-commits-filter: 5.0.0 + handlebars: 4.7.8 + meow: 13.2.0 + semver: 7.7.2 + + conventional-changelog@7.1.0(conventional-commits-filter@5.0.0): + dependencies: + '@conventional-changelog/git-client': 2.5.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.0) + '@types/normalize-package-data': 2.4.4 + conventional-changelog-preset-loader: 5.0.0 + conventional-changelog-writer: 8.1.0 + conventional-commits-parser: 6.2.0 + fd-package-json: 1.2.0 + meow: 13.2.0 + normalize-package-data: 7.0.0 + transitivePeerDependencies: + - conventional-commits-filter + + conventional-commits-filter@5.0.0: {} + + conventional-commits-parser@6.2.0: + dependencies: + meow: 13.2.0 + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} + cookie@0.7.2: {} + cookie@1.0.2: {} cosmiconfig@7.1.0: @@ -6107,6 +8221,8 @@ snapshots: csstype@3.1.3: {} + data-uri-to-buffer@2.0.2: {} + data-uri-to-buffer@4.0.1: {} debug@4.4.0: @@ -6121,22 +8237,39 @@ snapshots: dependencies: character-entities: 2.0.2 + dedent@1.6.0(babel-plugin-macros@3.1.0): + optionalDependencies: + babel-plugin-macros: 3.1.0 + deep-eql@5.0.2: {} deep-is@0.1.4: {} deepmerge@4.3.1: {} + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + define-lazy-prop@3.0.0: {} + defu@6.1.4: {} dequal@2.0.3: {} detect-libc@2.0.3: {} + detect-libc@2.0.4: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 + diff@8.0.2: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -6155,23 +8288,46 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dts-resolver@2.1.1: {} + + eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.129: {} emoji-regex@10.3.0: {} + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + enhanced-resolve@5.17.1: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 + enhanced-resolve@5.18.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + entities@4.5.0: {} environment@1.1.0: {} + err-code@2.0.3: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} + es-module-lexer@1.7.0: {} esbuild@0.24.2: @@ -6230,6 +8386,34 @@ snapshots: '@esbuild/win32-ia32': 0.25.2 '@esbuild/win32-x64': 0.25.2 + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + esbuild@0.25.5: optionalDependencies: '@esbuild/aix-ppc64': 0.25.5 @@ -6348,7 +8532,7 @@ snapshots: '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 @@ -6395,7 +8579,7 @@ snapshots: estree-util-attach-comments@3.0.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-util-build-jsx@3.0.1: dependencies: @@ -6421,7 +8605,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 esutils@2.0.3: {} @@ -6439,6 +8623,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + exit-hook@2.2.1: {} + expect-type@1.2.1: {} exsolve@1.0.4: {} @@ -6463,6 +8649,10 @@ snapshots: dependencies: reusify: 1.0.4 + fd-package-json@1.2.0: + dependencies: + walk-up-path: 3.0.1 + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -6508,6 +8698,11 @@ snapshots: flatted@3.2.9: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -6544,6 +8739,13 @@ snapshots: get-east-asian-width@1.2.0: {} + get-port@7.1.0: {} + + get-source@2.0.12: + dependencies: + data-uri-to-buffer: 2.0.2 + source-map: 0.6.1 + get-stream@8.0.1: {} get-them-args@1.3.2: {} @@ -6560,6 +8762,17 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regexp@0.4.1: {} + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + globals@11.12.0: {} globals@14.0.0: {} @@ -6572,6 +8785,15 @@ snapshots: graphemer@1.4.0: {} + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-flag@4.0.0: {} hasown@2.0.2: @@ -6580,7 +8802,7 @@ snapshots: hast-util-to-estree@3.1.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/estree-jsx': 1.0.3 '@types/hast': 3.0.3 comma-separated-tokens: 2.0.3 @@ -6601,7 +8823,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/hast': 3.0.3 '@types/unist': 3.0.2 comma-separated-tokens: 2.0.3 @@ -6627,8 +8849,18 @@ snapshots: dependencies: react-is: 16.13.1 + hono@4.8.3: {} + hookable@5.5.3: {} + hosted-git-info@6.1.3: + dependencies: + lru-cache: 7.18.3 + + hosted-git-info@8.1.0: + dependencies: + lru-cache: 10.4.3 + human-signals@5.0.0: {} ignore@5.3.2: {} @@ -6655,14 +8887,20 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-core-module@2.15.1: dependencies: hasown: 2.0.2 is-decimal@2.0.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@4.0.0: {} is-fullwidth-code-point@5.0.0: @@ -6675,24 +8913,40 @@ snapshots: is-hexadecimal@2.0.1: {} + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-module@1.0.0: {} is-number@7.0.0: {} + is-obj@2.0.0: {} + is-plain-obj@4.1.0: {} is-reference@1.2.1: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 is-reference@3.0.2: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 is-stream@3.0.0: {} + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isexe@2.0.0: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@1.21.7: {} jiti@2.4.2: {} @@ -6713,6 +8967,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-parse-even-better-errors@3.0.2: {} + json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -6820,12 +9076,18 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.castarray@4.4.0: {} + + lodash.isplainobject@4.0.6: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} lodash.uniq@4.5.0: {} + lodash@4.17.21: {} + log-update@6.1.0: dependencies: ansi-escapes: 7.0.0 @@ -6844,10 +9106,14 @@ snapshots: loupe@3.1.4: {} + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lru-cache@7.18.3: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -6956,6 +9222,8 @@ snapshots: mdn-data@2.0.30: {} + meow@13.2.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6981,7 +9249,7 @@ snapshots: micromark-extension-mdx-expression@3.0.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 devlop: 1.1.0 micromark-factory-mdx-expression: 2.0.1 micromark-factory-space: 2.0.0 @@ -6993,7 +9261,7 @@ snapshots: micromark-extension-mdx-jsx@3.0.0: dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 micromark-factory-mdx-expression: 2.0.1 @@ -7009,7 +9277,7 @@ snapshots: micromark-extension-mdxjs-esm@3.0.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 micromark-util-character: 2.0.1 @@ -7045,7 +9313,7 @@ snapshots: micromark-factory-mdx-expression@2.0.1: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 devlop: 1.1.0 micromark-util-character: 2.0.1 micromark-util-events-to-acorn: 2.0.2 @@ -7109,7 +9377,7 @@ snapshots: micromark-util-events-to-acorn@2.0.2: dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/unist': 3.0.2 devlop: 1.1.0 estree-util-visit: 2.0.0 @@ -7171,10 +9439,30 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime@3.0.0: {} + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} + miniflare@4.20250617.5: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.14.0 + acorn-walk: 8.3.2 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + sharp: 0.33.5 + stoppable: 1.1.0 + undici: 5.29.0 + workerd: 1.20250617.0 + ws: 8.18.0 + youch: 3.3.4 + zod: 3.22.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.0.1: dependencies: brace-expansion: 2.0.1 @@ -7187,6 +9475,16 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + mkdist@2.2.0(typescript@5.8.3): dependencies: autoprefixer: 10.4.21(postcss@8.5.3) @@ -7214,14 +9512,20 @@ snapshots: mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: {} + mustache@4.2.0: {} + nanoid@3.3.11: {} napi-postinstall@0.2.4: {} natural-compare@1.4.0: {} + neo-async@2.6.2: {} + node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -7232,8 +9536,41 @@ snapshots: node-releases@2.0.19: {} + normalize-package-data@5.0.0: + dependencies: + hosted-git-info: 6.1.3 + is-core-module: 2.15.1 + semver: 7.7.2 + validate-npm-package-license: 3.0.4 + + normalize-package-data@7.0.0: + dependencies: + hosted-git-info: 8.1.0 + semver: 7.7.2 + validate-npm-package-license: 3.0.4 + normalize-range@0.1.2: {} + npm-install-checks@6.3.0: + dependencies: + semver: 7.7.2 + + npm-normalize-package-bin@3.0.1: {} + + npm-package-arg@10.1.0: + dependencies: + hosted-git-info: 6.1.3 + proc-log: 3.0.0 + semver: 7.7.2 + validate-npm-package-name: 5.0.1 + + npm-pick-manifest@8.0.2: + dependencies: + npm-install-checks: 6.3.0 + npm-normalize-package-bin: 3.0.1 + npm-package-arg: 10.1.0 + semver: 7.7.2 + npm-run-path@5.1.0: dependencies: path-key: 4.0.0 @@ -7244,6 +9581,8 @@ snapshots: object-assign@4.1.1: {} + ohash@2.0.11: {} + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -7252,6 +9591,13 @@ snapshots: dependencies: mimic-function: 5.0.1 + open@10.1.2: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + is-wsl: 3.1.0 + optionator@0.9.3: dependencies: '@aashutoshrathi/word-wrap': 1.2.6 @@ -7269,9 +9615,9 @@ snapshots: dependencies: p-limit: 3.1.0 - package-manager-detector@0.2.11: - dependencies: - quansync: 0.2.10 + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.3.0: {} parent-module@1.0.1: dependencies: @@ -7303,6 +9649,13 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -7311,12 +9664,20 @@ snapshots: pathval@2.0.0: {} + perfect-debounce@1.0.0: {} + periscopic@3.1.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-walker: 3.0.3 is-reference: 3.0.2 + periscopic@4.0.2: + dependencies: + '@types/estree': 1.0.8 + is-reference: 3.0.2 + zimmerframe: 1.1.2 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7492,6 +9853,11 @@ snapshots: postcss: 8.5.3 postcss-value-parser: 4.2.0 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -7535,10 +9901,23 @@ snapshots: prelude-ls@1.2.1: {} + prettier@2.8.8: {} + prettier@3.1.0: {} pretty-bytes@6.1.1: {} + printable-characters@1.0.42: {} + + proc-log@3.0.0: {} + + promise-inflight@1.0.1: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -7552,10 +9931,10 @@ snapshots: property-information@6.4.0: {} - publint@0.3.9: + publint@0.3.12: dependencies: '@publint/pack': 0.1.2 - package-manager-detector: 0.2.11 + package-manager-detector: 1.3.0 picocolors: 1.1.1 sade: 1.8.1 @@ -7584,6 +9963,8 @@ snapshots: react-is@19.1.0: {} + react-refresh@0.14.2: {} + react-refresh@0.17.0: {} react-router-dom@7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): @@ -7592,6 +9973,14 @@ snapshots: react-dom: 19.1.0(react@19.1.0) react-router: 7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-router@0.0.0-experimental-23decd7bc(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + cookie: 1.0.2 + react: 19.1.0 + set-cookie-parser: 2.7.1 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + react-router@7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: cookie: 1.0.2 @@ -7600,6 +9989,14 @@ snapshots: optionalDependencies: react-dom: 19.1.0(react@19.1.0) + react-server-dom-webpack@19.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + webpack-sources: 3.3.3 + react-switch@7.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: prop-types: 15.8.1 @@ -7612,6 +10009,8 @@ snapshots: react@19.1.0: {} + readdirp@4.1.2: {} + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.1 @@ -7662,10 +10061,29 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + reusify@1.0.4: {} rfdc@1.4.1: {} + rolldown-plugin-dts@0.13.13(rolldown@1.0.0-beta.23)(typescript@5.8.3): + dependencies: + '@babel/generator': 7.27.5 + '@babel/parser': 7.27.7 + '@babel/types': 7.27.7 + ast-kit: 2.1.1 + birpc: 2.4.0 + debug: 4.4.1 + dts-resolver: 2.1.1 + get-tsconfig: 4.10.1 + rolldown: 1.0.0-beta.23 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - oxc-resolver + - supports-color + rolldown-vite@6.3.21(@types/node@22.16.0)(esbuild@0.25.5)(jiti@2.4.2)(tsx@4.20.3)(yaml@2.7.1): dependencies: '@oxc-project/runtime': 0.73.0 @@ -7783,6 +10201,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.44.1 fsevents: 2.3.3 + rsc-html-stream@0.0.6: {} + + run-applescript@7.0.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7815,6 +10237,32 @@ snapshots: shallowequal@1.1.0: {} + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -7829,6 +10277,16 @@ snapshots: simple-git-hooks@2.13.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} slice-ansi@5.0.0: @@ -7845,18 +10303,53 @@ snapshots: source-map@0.5.7: {} + source-map@0.6.1: {} + source-map@0.7.4: {} space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + + spdx-license-ids@3.0.21: {} + stable-hash-x@0.2.0: {} stackback@0.0.2: {} + stacktracey@2.1.8: + dependencies: + as-table: 1.0.55 + get-source: 2.0.12 + std-env@3.9.0: {} + stoppable@1.1.0: {} + string-argv@0.3.2: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + string-width@7.0.0: dependencies: emoji-regex: 10.3.0 @@ -7868,6 +10361,10 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-ansi@7.1.0: dependencies: ansi-regex: 6.0.1 @@ -7928,12 +10425,25 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 + tailwindcss@4.1.11: {} + tapable@2.2.1: {} + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + tinybench@2.9.0: {} tinyexec@0.3.2: {} + tinyexec@1.0.1: {} + tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) @@ -7954,6 +10464,8 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + trim-lines@3.0.1: {} trough@2.1.0: {} @@ -7967,6 +10479,30 @@ snapshots: picomatch: 4.0.2 typescript: 5.8.3 + tsdown@0.12.9(publint@0.3.12)(typescript@5.8.3): + dependencies: + ansis: 4.1.0 + cac: 6.7.14 + chokidar: 4.0.3 + debug: 4.4.1 + diff: 8.0.2 + empathic: 2.0.0 + hookable: 5.5.3 + rolldown: 1.0.0-beta.23 + rolldown-plugin-dts: 0.13.13(rolldown@1.0.0-beta.23)(typescript@5.8.3) + semver: 7.7.2 + tinyexec: 1.0.1 + tinyglobby: 0.2.14 + unconfig: 7.3.2 + optionalDependencies: + publint: 0.3.12 + typescript: 5.8.3 + transitivePeerDependencies: + - '@typescript/native-preview' + - oxc-resolver + - supports-color + - vue-tsc + tslib@2.6.2: {} tslib@2.8.1: @@ -7979,6 +10515,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + turbo-stream@3.1.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -7997,6 +10535,11 @@ snapshots: ufo@1.5.4: {} + ufo@1.6.1: {} + + uglify-js@3.19.3: + optional: true + unbuild@3.5.0(typescript@5.8.3): dependencies: '@rollup/plugin-alias': 5.1.1(rollup@4.37.0) @@ -8030,8 +10573,27 @@ snapshots: - vue - vue-tsc + unconfig@7.3.2: + dependencies: + '@quansync/fs': 0.1.3 + defu: 6.1.4 + jiti: 2.4.2 + quansync: 0.2.10 + undici-types@6.21.0: {} + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + + unenv@2.0.0-rc.17: + dependencies: + defu: 6.1.4 + exsolve: 1.0.4 + ohash: 2.0.11 + pathe: 2.0.3 + ufo: 1.6.1 + unified@11.0.4: dependencies: '@types/unist': 3.0.2 @@ -8076,6 +10638,11 @@ snapshots: universalify@2.0.1: {} + unplugin-utils@0.2.4: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.2 + unrs-resolver@1.9.2: dependencies: napi-postinstall: 0.2.4 @@ -8120,6 +10687,17 @@ snapshots: util-deprecate@1.0.2: {} + valibot@0.41.0(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@5.0.1: {} + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.2 @@ -8131,6 +10709,16 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 + vite-dev-rpc@1.1.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)): + dependencies: + birpc: 2.4.0 + vite: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + vite-hot-client: 2.1.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + + vite-hot-client@2.1.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)): + dependencies: + vite: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + vite-node@3.2.4(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1): dependencies: cac: 6.7.14 @@ -8152,6 +10740,21 @@ snapshots: - tsx - yaml + vite-plugin-inspect@11.3.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)): + dependencies: + ansis: 4.1.0 + debug: 4.4.1 + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.1.2 + perfect-debounce: 1.0.0 + sirv: 3.0.1 + unplugin-utils: 0.2.4 + vite: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + vite-dev-rpc: 1.1.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) + transitivePeerDependencies: + - supports-color + vite@7.0.1(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1): dependencies: esbuild: 0.25.5 @@ -8184,6 +10787,10 @@ snapshots: tsx: 4.20.3 yaml: 2.7.1 + vitefu@1.1.0(vite@7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)): + optionalDependencies: + vite: 7.0.2(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.16.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1): dependencies: '@types/chai': 5.2.2 @@ -8226,29 +10833,89 @@ snapshots: - tsx - yaml + walk-up-path@3.0.1: {} + web-streams-polyfill@3.2.1: {} + webpack-sources@3.3.3: {} + which@2.0.2: dependencies: isexe: 2.0.0 + which@3.0.1: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + wordwrap@1.0.0: {} + + workerd@1.20250617.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20250617.0 + '@cloudflare/workerd-darwin-arm64': 1.20250617.0 + '@cloudflare/workerd-linux-64': 1.20250617.0 + '@cloudflare/workerd-linux-arm64': 1.20250617.0 + '@cloudflare/workerd-windows-64': 1.20250617.0 + + wrangler@4.23.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.4.0 + '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0) + blake3-wasm: 2.1.5 + esbuild: 0.25.4 + miniflare: 4.20250617.5 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.17 + workerd: 1.20250617.0 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrap-ansi@9.0.0: dependencies: ansi-styles: 6.2.1 string-width: 7.0.0 strip-ansi: 7.1.0 + ws@8.18.0: {} + yallist@3.1.1: {} + yallist@5.0.0: {} + yaml@1.10.2: {} yaml@2.7.1: {} yocto-queue@0.1.0: {} + youch@3.3.4: + dependencies: + cookie: 0.7.2 + mustache: 4.2.0 + stacktracey: 2.1.8 + + zimmerframe@1.1.2: {} + + zod@3.22.3: {} + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 94320515..7832fad0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - 'packages/*' - 'playground/**' - 'packages/plugin-react-swc/playground/**' + - 'packages/plugin-rsc/examples/*' catalogs: rolldown-vite: diff --git a/scripts/release.ts b/scripts/release.ts index 7a261555..4a71b452 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -1,12 +1,17 @@ import { readFileSync, writeFileSync } from 'node:fs' -import { release } from '@vitejs/release-scripts' +import { generateChangelog, release } from '@vitejs/release-scripts' import colors from 'picocolors' const nextH2RE = /^## /gm release({ repo: 'vite-plugin-react', - packages: ['plugin-react', 'plugin-react-swc', 'plugin-react-oxc'], + packages: [ + 'plugin-react', + 'plugin-react-swc', + 'plugin-react-oxc', + 'plugin-rsc', + ], getPkgDir(pkg) { if (pkg === 'plugin-react-swc') { return `packages/${pkg}/dist` @@ -15,6 +20,8 @@ release({ }, toTag: (pkg, version) => `${pkg}@${version}`, logChangelog: async (pkgName) => { + if (pkgName === 'plugin-rsc') return + const changelog = readFileSync(`packages/${pkgName}/CHANGELOG.md`, 'utf-8') if (!changelog.includes('## Unreleased')) { throw new Error("Can't find '## Unreleased' section in CHANGELOG.md") @@ -25,6 +32,14 @@ release({ console.log(colors.dim(changelog.slice(index, nextH2Pos).trim())) }, generateChangelog: async (pkgName, version) => { + if (pkgName === 'plugin-rsc') { + await generateChangelog({ + getPkgDir: () => `packages/${pkgName}`, + tagPrefix: `${pkgName}@`, + }) + return + } + if (pkgName === 'plugin-react-swc') { console.log(colors.cyan('\nUpdating package.json version...')) const pkgJsonPath = `packages/${pkgName}/package.json`