Repository: alvarlagerlof/rsc-parser Branch: main Commit: a23e9607b114 Files: 183 Total size: 5.2 MB Directory structure: gitextract_pei3ls9p/ ├── .changeset/ │ ├── README.md │ ├── config.json │ └── lucky-numbers-roll.md ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── apply-version.sh ├── examples/ │ └── embedded-example/ │ ├── .gitignore │ ├── .prettierignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── app/ │ │ ├── action/ │ │ │ └── page.tsx │ │ ├── error/ │ │ │ └── [name]/ │ │ │ └── page.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── other/ │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── suspense/ │ │ └── page.tsx │ ├── eslint.config.js │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.cjs │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── turbo.json │ └── vercel.json ├── package.json ├── packages/ │ ├── chrome-extension/ │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── devtoolsPanel.html │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── public/ │ │ │ ├── devtoolsPage.html │ │ │ └── manifest.json │ │ ├── src/ │ │ │ ├── RscDevtoolsExtension.tsx │ │ │ ├── assets/ │ │ │ │ ├── contentScript.ts │ │ │ │ ├── devtoolsPage.ts │ │ │ │ ├── fetchPatcherInjector.ts │ │ │ │ └── readNextJsScriptTagsInjector.ts │ │ │ ├── dev/ │ │ │ │ └── reactPreamble.ts │ │ │ ├── main.tsx │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── turbo.json │ │ └── vite.config.ts │ ├── core/ │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── jest.config.cjs │ │ ├── jest.setup.js │ │ ├── package.json │ │ ├── postcss.config.cjs │ │ ├── src/ │ │ │ ├── color.ts │ │ │ ├── components/ │ │ │ │ ├── BottomPanel.stories.tsx │ │ │ │ ├── BottomPanel.tsx │ │ │ │ ├── EndTimeContext.tsx │ │ │ │ ├── FlightResponseChunkConsole.stories.tsx │ │ │ │ ├── FlightResponseChunkConsole.tsx │ │ │ │ ├── FlightResponseChunkDebugInfo.stories.tsx │ │ │ │ ├── FlightResponseChunkDebugInfo.tsx │ │ │ │ ├── FlightResponseChunkHint.stories.tsx │ │ │ │ ├── FlightResponseChunkHint.tsx │ │ │ │ ├── FlightResponseChunkModel.stories.tsx │ │ │ │ ├── FlightResponseChunkModel.tsx │ │ │ │ ├── FlightResponseChunkModule.stories.tsx │ │ │ │ ├── FlightResponseChunkModule.tsx │ │ │ │ ├── FlightResponseChunkText.tsx │ │ │ │ ├── FlightResponseChunkUnknown.tsx │ │ │ │ ├── FlightResponseIcons.tsx │ │ │ │ ├── GenericErrorBoundaryFallback.tsx │ │ │ │ ├── IconButton.tsx │ │ │ │ ├── Logo.stories.tsx │ │ │ │ ├── Logo.tsx │ │ │ │ ├── OverflowButton.tsx │ │ │ │ ├── PanelLayout.stories.tsx │ │ │ │ ├── PanelLayout.tsx │ │ │ │ ├── RecordButton.stories.tsx │ │ │ │ ├── RecordButton.tsx │ │ │ │ ├── RequestDetail.tsx │ │ │ │ ├── RequestDetailTabEmptyState.tsx │ │ │ │ ├── RequestDetailTabHeaders.stories.tsx │ │ │ │ ├── RequestDetailTabHeaders.tsx │ │ │ │ ├── RequestDetailTabNetwork.stories.tsx │ │ │ │ ├── RequestDetailTabNetwork.tsx │ │ │ │ ├── RequestDetailTabParsedPayload.stories.tsx │ │ │ │ ├── RequestDetailTabParsedPayload.tsx │ │ │ │ ├── RequestDetailTabRawPayload.stories.tsx │ │ │ │ ├── RequestDetailTabRawPayload.tsx │ │ │ │ ├── RequestDetailTabTimings.stories.tsx │ │ │ │ ├── RequestDetailTabTimings.tsx │ │ │ │ ├── TimeScrubber.stories.tsx │ │ │ │ ├── TimeScrubber.tsx │ │ │ │ ├── ViewerPayload.stories.tsx │ │ │ │ ├── ViewerPayload.tsx │ │ │ │ ├── ViewerStreams.stories.tsx │ │ │ │ ├── ViewerStreams.tsx │ │ │ │ ├── ViewerStreamsEmptyState.stories.tsx │ │ │ │ ├── ViewerStreamsEmptyState.tsx │ │ │ │ ├── isDev.ts │ │ │ │ └── useTabStoreWithTransitions.ts │ │ │ ├── copyEventsToClipboard.ts │ │ │ ├── eventArrayHelpers.ts │ │ │ ├── events.ts │ │ │ ├── example-data/ │ │ │ │ ├── alvar-dev.ts │ │ │ │ ├── gh-fredkiss-dev.ts │ │ │ │ └── nextjs-org.ts │ │ │ ├── fetchPatcher.ts │ │ │ ├── main.ts │ │ │ ├── readNextJsScriptTags.ts │ │ │ └── tailwind.css │ │ ├── tailwind.config.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ ├── turbo.json │ │ └── vite.config.ts │ ├── embedded/ │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── RscDevtoolsPanel.tsx │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── turbo.json │ │ └── vite.config.ts │ ├── react-client/ │ │ ├── .gitignore │ │ ├── .prettierignore │ │ ├── .prettierrc │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── flight.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── ReactDOMTypes.ts │ │ │ ├── ReactFlightClient.ts │ │ │ ├── ReactFlightClientConfigBrowser.ts │ │ │ ├── ReactFlightClientConfigBundlerWebpack.ts │ │ │ ├── ReactFlightImportMetadata.ts │ │ │ ├── ReactFlightServerConfigDOM.ts │ │ │ ├── ReactSymbols.ts │ │ │ ├── ReactTypes.ts │ │ │ └── crossOriginStrings.ts │ │ ├── tsconfig.json │ │ ├── turbo.json │ │ └── vite.config.ts │ ├── storybook/ │ │ ├── .gitignore │ │ ├── .storybook/ │ │ │ ├── main.ts │ │ │ ├── preview-body.html │ │ │ └── preview.ts │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── turbo.json │ │ └── vercel.json │ └── website/ │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── app/ │ │ ├── ViewerPayloadClientWrapper.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── eslint.config.js │ ├── jest.config.js │ ├── jest.setup.js │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.cjs │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── turbo.json │ └── vercel.json ├── renovate.json └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [ [ "@rsc-parser/chrome-extension", "@rsc-parser/core", "@rsc-parser/website", "@rsc-parser/embedded", "@rsc-parser/react-client" ] ], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [] } ================================================ FILE: .changeset/lucky-numbers-roll.md ================================================ --- '@rsc-parser/chrome-extension': patch '@rsc-parser/embedded-example': patch '@rsc-parser/core': patch '@rsc-parser/embedded': patch '@rsc-parser/react-client': patch '@rsc-parser/storybook': patch '@rsc-parser/website': patch --- Uninstall unused web-vitals package ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request_target: # Types must be filtered to prevent getting stuck when a # bot with GITHUB_TOKEN pushes commits against the PR branch types: - opened - reopened pull_request: branches: - main jobs: build_and_test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - uses: oven-sh/setup-bun@v2 - name: bun install run: bun install - name: Turborepo run: bun run ci - name: Archive production artifacts uses: actions/upload-artifact@v6 with: name: dist.zip path: | packages/chrome-extension/dist ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - main concurrency: ${{ github.workflow }}-${{ github.ref }} jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v6 with: ref: ${{github.event.pull_request.head.sha}} fetch-depth: 0 - name: Setup Node.js 18 uses: actions/setup-node@v6 with: node-version: 24.14.1 - uses: oven-sh/setup-bun@v2 - name: Install Dependencies run: bun install - name: Run CI run: bun run ci - name: Create Release Pull Request uses: changesets/action@v1 id: changesets with: version: bun run version publish: bun run release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Zip dist if: steps.changesets.outputs.published == 'true' run: | cd packages/chrome-extension zip dist.zip -r dist - name: Edit release if: steps.changesets.outputs.published == 'true' uses: xresloader/upload-to-github-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: file: 'packages/chrome-extension/dist.zip' update_latest_release: true ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo # turborepo .turbo ================================================ FILE: .prettierignore ================================================ .yarn ================================================ FILE: .prettierrc ================================================ { "trailingComma": "all", "singleQuote": true } ================================================ FILE: .vscode/settings.json ================================================ { "css.lint.unknownAtRules": "ignore", "typescript.tsdk": "node_modules/typescript/lib" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Alvar Lagerlöf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # RSC Parser This is a parser for React Server Components (RSC) when sent over the network. React uses a format to represent a tree of components/html or metadata such as required imports, suspense boundaries, and css/fonts that needs to be loaded. I made this tool to more easily let you understand the data and explore it visually. ## Comparison
image image
## How do I use this? ### Extension There is a Chrome Extension that you can add [here](https://chromewebstore.google.com/detail/rsc-devtools/jcejahepddjnppkhomnidalpnnnemomn). ### npm package You can also add the parser as a package to your project. You'll get an embedded panel component that you can render in a layout.tsx for example. First, install [@rsc-parser/embedded](https://www.npmjs.com/package/@rsc-parser/embedded) from npm like: `bun add @rsc-parser/embedded` Then you can load it in a `layout.tsx` for example. ```tsx import { Suspense, lazy } from "react"; const RscDevtoolsPanel = lazy(() => import("@rsc-parser/embedded").then(module => ({ default: module.RscDevtoolsPanel, })), ); export default function RootLayout({ children }) { return ( {children} {/* Use any condition or flag you want here */ } {process.env.NODE_ENV === "development" ? ( ) : null} ); ``` ### Website There is also a website that you can use by manually copy pasting RSC payloads. 1. Go to a site that uses the NextJS App router or generally is based on React Server components. 2. Open the network tab in your dev tools 3. Reload. 4. Look for fetch responses the payload roughly looks like json, but each like starts with something like `O:`, `1:I`, `b:` or similar. 5. Copy the text and paste it into the form on https://rsc-parser.vercel.app/ 6. Explore! ## It crashed! Please make an issue on https://github.com/alvarlagerlof/rsc-parser/issues/new and include the text content that the parser was unable to handle. ================================================ FILE: apply-version.sh ================================================ #!/bin/bash set -e # Step 1: Extract newVersion from ./packages/core/package.json new_version=$(jq -r '.version' ./packages/core/package.json) echo "Found new version: $new_version" # Step 2: Update the version in manifest.json jq --arg version "$new_version" '.version = $version' packages/chrome-extension/public/manifest.json > tmp.json && mv tmp.json packages/chrome-extension/public/manifest.json echo "Applied new version: $_new_version" # Step 3: Clean up bun run prettier -w packages/chrome-extension/public/manifest.json ================================================ FILE: examples/embedded-example/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: examples/embedded-example/.prettierignore ================================================ .next .turbo ================================================ FILE: examples/embedded-example/CHANGELOG.md ================================================ # @rsc-parser/embedded-example ## 1.1.2 ### Patch Changes - c965537: Add "repository" to /embedded package.json - Updated dependencies [c965537] - @rsc-parser/embedded@1.1.2 ## 1.1.1 ### Patch Changes - e0b935a: Improve console chunk list rendering - Updated dependencies [e0b935a] - @rsc-parser/embedded@1.1.1 ## 1.1.0 ### Minor Changes - 6ce957a: Add support for rendering console chunks - 83cbaaf: Link to debug info references ### Patch Changes - 6d9815c: Improve readability when a tab has multiple panels - Updated dependencies [6ce957a] - Updated dependencies [83cbaaf] - Updated dependencies [6d9815c] - @rsc-parser/embedded@1.1.0 ## 1.0.0 ### Major Changes - 65231fc: Fix dev check in the Chrome extension BREAKING: `createFlightResponse` from @rsc-parser/react-client now takes a `__DEV__` parameter. ### Patch Changes - 44cc2a8: Fix meter styling - 1860787: Fixed a few react runtime errors - Updated dependencies [44cc2a8] - Updated dependencies [1860787] - Updated dependencies [65231fc] - @rsc-parser/embedded@1.0.0 ## 0.9.2 ### Patch Changes - 5fb463b: Specify "files" for @rsc-parser/react-client - Updated dependencies [5fb463b] - @rsc-parser/embedded@0.9.2 ## 0.9.1 ### Patch Changes - 71a3eac: Properly externalize react in the core and embedded packages - Updated dependencies [71a3eac] - @rsc-parser/embedded@0.9.1 ## 0.9.0 ### Minor Changes - 0f2a24a: Fix network graph max call stack error ### Patch Changes - Updated dependencies [0f2a24a] - @rsc-parser/embedded@0.9.0 ## 0.8.0 ### Minor Changes - 3db1a5f: Don't flip recording state when running triggerReadNextJsScriptTags - 1478826: Add one error boundary per tab panel in RequestDetail ### Patch Changes - Updated dependencies [3db1a5f] - Updated dependencies [1478826] - @rsc-parser/embedded@0.8.0 ## 0.7.0 ### Minor Changes - 531f06e: Update fork of ReactFlightClient.js - 4bf3887: Make it possible to read Next.js script tags from the Chrome extension ### Patch Changes - Updated dependencies [531f06e] - Updated dependencies [4bf3887] - @rsc-parser/embedded@0.7.0 ## 0.6.0 ### Minor Changes - 3f5a1c5: Make the "Headers" tab table column 1 min-size smaller, and make the `BottomPanel` default size larger - 22cbbbf: Redesign `RequestTab` again - f8836e5: Make request tab multiline by default ### Patch Changes - 0002b9a: Add flex wrapping to the tabs in `RequestDetail` - Updated dependencies [3f5a1c5] - Updated dependencies [0002b9a] - Updated dependencies [22cbbbf] - Updated dependencies [f8836e5] - @rsc-parser/embedded@0.6.0 ## 0.5.1 ### Patch Changes - 1c0b883: Fix `contentScript.ts` bundling to avoid ESM imports - d3228c2: Remove debug console.log statements - Updated dependencies [1c0b883] - Updated dependencies [d3228c2] - @rsc-parser/embedded@0.5.1 ## 0.5.0 ### Minor Changes - 44a478e: Create a Timings tab - b07521b: Capture request and response headers - 2aa4133: Refactor fetch patching and events - c937a64: Rename Chrome extension root component from `App` to `RscDevtoolsExtension` - 8ae76c5: Make the `BottomPanel` easier to resize - 56627ae: Improve Headers tab design and add general information - 4096674: Improve the `Raw` tab - 2883dd1: Restructure @rsc-parser/chrome-extension files - e155780: Create an `OverflowButton` for less important actions in `PanelLayout` - a4eca6d: Add text on top of links in `RequestDetailTabNetwork` - 65d0acd: Create `@rsc-parser/react-client` ### Patch Changes - fe3446e: Make it possible to position the `BottomPanel` on the right - 57c8a9b: Refactor `ViewerStreams` internals to allow a single list of tabs - 095e40e: Ensure that `react` and `react-dom` are externalized - a3a202c: Sync the version @rsc-parser/embedded-example with the other packages - 800fced: Decrease the minimum panel sizes in `FlightResponseSelector` - Updated dependencies [44a478e] - Updated dependencies [b07521b] - Updated dependencies [2aa4133] - Updated dependencies [fe3446e] - Updated dependencies [c937a64] - Updated dependencies [8ae76c5] - Updated dependencies [56627ae] - Updated dependencies [4096674] - Updated dependencies [57c8a9b] - Updated dependencies [2883dd1] - Updated dependencies [095e40e] - Updated dependencies [a3a202c] - Updated dependencies [800fced] - Updated dependencies [e155780] - Updated dependencies [a4eca6d] - Updated dependencies [65d0acd] - @rsc-parser/embedded@0.5.0 ## 0.18.2 ### Patch Changes - 5356856: Only show links for which there are nodes in FlightResponseTabNetwork - Updated dependencies [5356856] - @rsc-parser/embedded@0.4.2 ## 0.18.1 ### Patch Changes - e129d14: Specify files in @rsc-parser/core - Updated dependencies [e129d14] - @rsc-parser/embedded@0.4.1 ## 0.18.0 ### Minor Changes - 61a3d5a: Expose `unstable_Viewer` ### Patch Changes - Updated dependencies [61a3d5a] - @rsc-parser/embedded@0.4.0 ## 0.17.1 ### Patch Changes - 4542777: Fix Chrome Extension bundling - e09465a: Add typecheck command - 4f48e76: Add format command everywher - ed20b59: Remove console logs - Updated dependencies [4542777] - Updated dependencies [e09465a] - Updated dependencies [4f48e76] - Updated dependencies [ed20b59] - @rsc-parser/embedded@0.3.1 ## 0.17.0 ### Minor Changes - f313d13: Compile Chrome extension scripts using Vite - ee03219: Move fetch patching code to @rsc-parser/core - 4d0f42e: Added support for server actions ### Patch Changes - 2cd0dd4: Export `unstable_createFlightResponse` - 2836e57: Add a button to read Next.js payload script tags - faa158f: Export `unstable_createFlightResponse` - Updated dependencies [f313d13] - Updated dependencies [2cd0dd4] - Updated dependencies [2836e57] - Updated dependencies [ee03219] - Updated dependencies [faa158f] - Updated dependencies [4d0f42e] - @rsc-parser/embedded@0.3.0 ## 0.16.4 ### Patch Changes - 71e069c: Set a higher z-index for the BottomPanel when when open - Updated dependencies [71e069c] - @rsc-parser/embedded@0.2.4 ## 0.16.3 ### Patch Changes - f181ce8: Scope styles for RscDevtoolsPanel - 39e168a: Stop wrapping
================================================ FILE: packages/chrome-extension/eslint.config.js ================================================ // @ts-check import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; export default [ { ignores: ['dist/**', '.turbo/**', 'vite.config.ts.timestamp*'], }, ...[eslint.configs.recommended, ...tseslint.configs.recommended].map( (conf) => ({ ...conf, files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], }), ), { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], rules: { curly: ['error', 'all'], }, }, ]; ================================================ FILE: packages/chrome-extension/package.json ================================================ { "name": "@rsc-parser/chrome-extension", "version": "1.1.2", "license": "MIT", "packageManager": "bun@1.3.3", "private": true, "dependencies": { "@rsc-parser/core": "workspace:*", "react": "19.2.4", "react-dom": "19.2.4", "react-error-boundary": "6.0.3" }, "devDependencies": { "@ariakit/react": "0.4.24", "@eslint/js": "9.39.4", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "@vitejs/plugin-react": "4.7.0", "autoprefixer": "10.4.27", "chrome-types": "0.1.425", "concurrently": "9.2.1", "esbuild": "0.27.4", "eslint": "9.39.4", "postcss": "8.5.8", "prettier": "3.8.1", "type-fest": "5.5.0", "typescript": "5.9.3", "typescript-eslint": "8.57.2", "vite": "7.3.1", "vite-plugin-html-config": "2.0.2" }, "scripts": { "lint": "eslint", "format": "prettier . --check --config ../../.prettierrc", "typecheck": "tsc --noEmit", "dev": "vite build --mode development && vite dev", "build": "vite build && sed -i'.backup' 's| http://localhost:6020||g' dist/manifest.json && rm dist/manifest.json.backup", "clean": "rm -rf dist" }, "type": "module" } ================================================ FILE: packages/chrome-extension/public/devtoolsPage.html ================================================ ================================================ FILE: packages/chrome-extension/public/manifest.json ================================================ { "name": "RSC Devtools", "version": "1.1.2", "description": "React Server Components network visualizer", "content_security_policy": { "extension_pages": "script-src 'self' http://localhost:6020; object-src 'self'" }, "manifest_version": 3, "devtools_page": "devtoolsPage.html", "content_scripts": [ { "matches": ["http://*/*", "https://*/*"], "js": ["assets/contentScript.js"], "all_frames": true } ], "web_accessible_resources": [ { "resources": ["assets/fetchPatcherInjector.js"], "matches": ["http://*/*", "https://*/*"] }, { "resources": ["assets/readNextJsScriptTagsInjector.js"], "matches": ["http://*/*", "https://*/*"] } ] } ================================================ FILE: packages/chrome-extension/src/RscDevtoolsExtension.tsx ================================================ import React, { startTransition, useCallback, useEffect, useMemo, useState, } from 'react'; import { ViewerStreams, ViewerStreamsEmptyState, PanelLayout, Logo, RecordButton, OverflowButton, copyEventsToClipboard, } from '@rsc-parser/core'; import '@rsc-parser/core/style.css'; import { RscEvent, StartRecordingEvent, isEvent, isRscChunkEvent, isRscEvent, isStopRecordingEvent, ReadNextJsScriptTagsEvent, } from '@rsc-parser/core/events'; export function RscDevtoolsExtension() { const { isRecording, startRecording, events, clearEvents, triggerReadNextJsScriptTags, } = useRscEvents(); return ( } buttons={ <> {process.env.NODE_ENV === 'development' ? ( ) : null} } /> } > {events.length === 0 ? ( ) : ( )} ); } function useRscEvents() { const [events, setEvents] = useState([]); const [isRecording, setIsRecording] = useState(false); const tabId = useMemo(() => Date.now(), []); useEffect(() => { function handleEvent( request: unknown, // eslint-disable-next-line @typescript-eslint/no-unused-vars _sender: unknown, // eslint-disable-next-line @typescript-eslint/no-unused-vars _sendResponse: unknown, ) { if (!isEvent(request)) { return true; } // If the event is from a different tab, ignore it if (request.data.tabId !== tabId) { return true; } if (isStopRecordingEvent(request)) { setIsRecording(false); setEvents([]); return true; } if (!isRscEvent(request)) { return true; } // It's possible that this lookup will miss a duplicated event if another // one is being added at the same time. I haven't seen this happen in practice. if ( isRscChunkEvent(request) && events .filter(isRscChunkEvent) .some((item) => arraysEqual(item.data.chunkValue, request.data.chunkValue), ) ) { return true; } startTransition(() => { setEvents((previous) => [...previous, request]); }); return true; } chrome.runtime.onMessage.addListener(handleEvent); return () => { chrome.runtime.onMessage.removeListener(handleEvent); }; }, []); const startRecording = useCallback(() => { setIsRecording(true); chrome.tabs.sendMessage(chrome.devtools.inspectedWindow.tabId, { type: 'START_RECORDING', data: { tabId: tabId, }, } satisfies StartRecordingEvent); }, [tabId]); const clearEvents = useCallback(() => { setEvents([]); }, []); const triggerReadNextJsScriptTags = useCallback(() => { chrome.tabs.sendMessage(chrome.devtools.inspectedWindow.tabId, { type: 'READ_NEXT_JS_SCRIPT_TAGS', data: { tabId: tabId, }, } satisfies ReadNextJsScriptTagsEvent); }, []); return { isRecording, startRecording, events, clearEvents, triggerReadNextJsScriptTags, }; } function arraysEqual(a: unknown[], b: unknown[]) { if (a === b) { return true; } if (a == null || b == null) { return false; } if (a.length !== b.length) { return false; } // If you don't care about the order of the elements inside // the array, you should sort both arrays here. // Please note that calling sort on an array will modify that array. // you might want to clone your array first. for (let i = 0; i < a.length; ++i) { if (a[i] !== b[i]) { return false; } } return true; } ================================================ FILE: packages/chrome-extension/src/assets/contentScript.ts ================================================ import { type RscEvent, type StopRecordingEvent, } from '@rsc-parser/core/events'; // import { RscEvent, isRscEvent } from "@rsc-parser/core/events"; // import { // StopRecordingEvent, // isStartRecordingEvent, // } from "@rsc-parser/core/events"; // TODO: Find a way that these can be imported that makes them bundled // into the output file without using ESM imports function isStartRecordingEvent(event: unknown) { return ( typeof event === 'object' && event !== null && 'type' in event && event.type === 'START_RECORDING' ); } function isRscRequestEvent(event: unknown) { return ( typeof event === 'object' && event !== null && 'type' in event && event.type === 'RSC_REQUEST' ); } function isRscResponseEvent(event: unknown) { return ( typeof event === 'object' && event !== null && 'type' in event && event.type === 'RSC_RESPONSE' ); } function isRscChunkEvent(event: unknown) { return ( typeof event === 'object' && event !== null && 'type' in event && event.type === 'RSC_CHUNK' ); } export function isReadNextJsScriptTagsEvent(event: unknown) { return ( typeof event === 'object' && event !== null && 'type' in event && event.type === 'READ_NEXT_JS_SCRIPT_TAGS' ); } function isRscEvent(event: unknown) { return ( isRscRequestEvent(event) || isRscResponseEvent(event) || isRscChunkEvent(event) ); } /** * injectScript - Inject internal script to available access to the `window` * * @param {type} file_path Local path of the internal script. * @param {type} tag The tag as string, where the script will be append (default: 'body'). * @see {@link http://stackoverflow.com/questions/20499994/access-window-variable-from-content-script} */ function injectScript(file_path: string, tag: string) { const node = document.getElementsByTagName(tag)[0]; const script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.setAttribute('src', file_path); node.appendChild(script); } // This is used in the devtools panel to only accept messages from the current tab let tabId: number | undefined = undefined; // Only inject the fetch patch script when the START_RECORDING evebt // is received from the devtools panel chrome.runtime.onMessage.addListener(function (request) { if (isStartRecordingEvent(request)) { // Store the tabId so that the devtools panel can filter messages to // only show the ones from the current tab tabId = request.data.tabId; injectScript( chrome.runtime.getURL('assets/fetchPatcherInjector.js'), 'body', ); } if (isReadNextJsScriptTagsEvent(request)) { // Store the tabId so that the devtools panel can filter messages to // only show the ones from the current tab tabId = request.data.tabId; injectScript( chrome.runtime.getURL('assets/readNextJsScriptTagsInjector.js'), 'body', ); } return true; }); // This code passes along events from fetchPatcherInjector // and readNextJsScriptTagsInjector to the devtools panel window.addEventListener( 'message', function (event) { // We only accept events from this window to itself [i.e. not from any iframes] if (event.source != window) { return; } if (typeof tabId !== 'number') { return; } if (isRscEvent(event.data)) { const baseEvent = event.data; baseEvent.data.tabId = tabId; chrome.runtime.sendMessage(baseEvent satisfies RscEvent); } }, false, ); // When the content script is unloaded (like for a refresh), send a message to the devtools panel to stop recording window.addEventListener('beforeunload', () => { if (!tabId) { return; } chrome.runtime.sendMessage({ type: 'STOP_RECORDING', data: { tabId }, } satisfies StopRecordingEvent); }); ================================================ FILE: packages/chrome-extension/src/assets/devtoolsPage.ts ================================================ function handleShown() { //console.log("panel is being shown"); } function handleHidden() { //console.log("panel is being hidden"); } chrome.devtools.panels.create( 'RSC Devtools', '', './devtoolsPanel.html', function (panel) { // console.log("Hello! from panel create"); panel.onShown.addListener(handleShown); panel.onHidden.addListener(handleHidden); }, ); ================================================ FILE: packages/chrome-extension/src/assets/fetchPatcherInjector.ts ================================================ import { fetchPatcher } from '@rsc-parser/core/fetchPatcher'; fetchPatcher({ onRscEvent: (event) => { // Forward the message so that the content script can pick it up window.postMessage(event, '*'); }, }); ================================================ FILE: packages/chrome-extension/src/assets/readNextJsScriptTagsInjector.ts ================================================ import { readNextJsScriptTags } from '@rsc-parser/core/readNextJsScriptTags'; (() => { const events = readNextJsScriptTags(); if (events) { for (const event of events) { window.postMessage(event, '*'); } } })(); ================================================ FILE: packages/chrome-extension/src/dev/reactPreamble.ts ================================================ // @ts-expect-error Vite special sauce import RefreshRuntime from 'http://localhost:6020/@react-refresh'; RefreshRuntime.injectIntoGlobalHook(window); // @ts-expect-error Vite special sauce window.$RefreshReg$ = () => {}; // @ts-expect-error Vite special sauce window.$RefreshSig$ = () => (type) => type; // @ts-expect-error Vite special sauce window.__vite_plugin_react_preamble_installed__ = true; ================================================ FILE: packages/chrome-extension/src/main.tsx ================================================ import 'vite/modulepreload-polyfill'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { RscDevtoolsExtension } from './RscDevtoolsExtension.jsx'; try { if (typeof process === 'undefined') { process = { // @ts-expect-error Overriding process is meant to be done env: 'development', }; } } catch (error) { console.log('error', error); } ReactDOM.createRoot(document.getElementById('root')!).render( , ); ================================================ FILE: packages/chrome-extension/src/vite-env.d.ts ================================================ /// ================================================ FILE: packages/chrome-extension/tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "NodeNext", "moduleResolution": "NodeNext", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "types": ["chrome-types"] }, "include": ["**/*.ts", "**/*.tsx", "jest.config.js"], "exclude": ["node_modules", "vite.config.ts.timestamp*"] } ================================================ FILE: packages/chrome-extension/turbo.json ================================================ { "$schema": "https://turbo.build/schema.json", "extends": ["//"], "tasks": { "build": { "outputs": ["dist/**"], "inputs": [ "src/**", "vite.config.ts", "tsconfig.json", "postcss.config.cjs", "tailwind.config.cjs" ] } } } ================================================ FILE: packages/chrome-extension/vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import htmlPlugin, { Options, ScriptTag } from 'vite-plugin-html-config'; import { resolve } from 'path'; const dev = { headScripts: [ { src: 'http://localhost:6020/src/dev/reactPreamble.ts', type: 'module', } as ScriptTag, { src: 'http://localhost:6020/@vite/client', type: 'module', } as ScriptTag, { src: 'http://localhost:6020/src/main.tsx', type: 'module', } as ScriptTag, ], } satisfies Options; const build = { headScripts: [ { async: false, src: './src/main.tsx', type: 'module', } as ScriptTag, ], } satisfies Options; export default defineConfig(({ mode }) => { if (mode === 'development') { return { // @ts-expect-error TODO: Fix type plugins: [react(), htmlPlugin(dev)], base: '', build: { outDir: './dist', minify: false, rollupOptions: { input: { main: resolve(__dirname, 'devtoolsPanel.html'), fetchPatcherInjector: resolve( __dirname, 'src/assets/fetchPatcherInjector.ts', ), readNextJsScriptTagsInjector: resolve( __dirname, 'src/assets/readNextJsScriptTagsInjector.ts', ), contentScript: resolve(__dirname, 'src/assets/contentScript.ts'), devtoolsPage: resolve(__dirname, 'src/assets/devtoolsPage.ts'), }, output: { entryFileNames: `assets/[name].js`, chunkFileNames: `assets/[name].js`, assetFileNames: `assets/[name].[ext]`, }, }, sourcemap: 'inline', }, server: { port: 6020, }, }; } return { // @ts-expect-error TODO: Fix type plugins: [react(), htmlPlugin(build)], base: '', build: { outDir: './dist', minify: false, rollupOptions: { input: { main: resolve(__dirname, 'devtoolsPanel.html'), fetchPatcherInjector: resolve( __dirname, 'src/assets/fetchPatcherInjector.ts', ), readNextJsScriptTagsInjector: resolve( __dirname, 'src/assets/readNextJsScriptTagsInjector.ts', ), contentScript: resolve(__dirname, 'src/assets/contentScript.ts'), devtoolsPage: resolve(__dirname, 'src/assets/devtoolsPage.ts'), }, output: { entryFileNames: `assets/[name].js`, chunkFileNames: `assets/[name].js`, assetFileNames: `assets/[name].[ext]`, }, }, sourcemap: false, }, server: { port: 6020, }, }; }); ================================================ FILE: packages/core/.gitignore ================================================ /node_modules dist vite.config.ts.timestamp* ================================================ FILE: packages/core/.prettierignore ================================================ vite.config.ts.timestamp* src/example-data dist ================================================ FILE: packages/core/CHANGELOG.md ================================================ # @rsc-parser/core ## 1.1.2 ### Patch Changes - c965537: Add "repository" to /embedded package.json - Updated dependencies [c965537] - @rsc-parser/react-client@1.1.2 ## 1.1.1 ### Patch Changes - e0b935a: Improve console chunk list rendering - Updated dependencies [e0b935a] - @rsc-parser/react-client@1.1.1 ## 1.1.0 ### Minor Changes - 6ce957a: Add support for rendering console chunks - 83cbaaf: Link to debug info references ### Patch Changes - 6d9815c: Improve readability when a tab has multiple panels - Updated dependencies [6ce957a] - Updated dependencies [83cbaaf] - Updated dependencies [6d9815c] - @rsc-parser/react-client@1.1.0 ## 1.0.0 ### Major Changes - 65231fc: Fix dev check in the Chrome extension BREAKING: `createFlightResponse` from @rsc-parser/react-client now takes a `__DEV__` parameter. ### Patch Changes - 44cc2a8: Fix meter styling - 1860787: Fixed a few react runtime errors - Updated dependencies [44cc2a8] - Updated dependencies [1860787] - Updated dependencies [65231fc] - @rsc-parser/react-client@1.0.0 ## 0.9.2 ### Patch Changes - 5fb463b: Specify "files" for @rsc-parser/react-client - Updated dependencies [5fb463b] - @rsc-parser/react-client@0.9.2 ## 0.9.1 ### Patch Changes - 71a3eac: Properly externalize react in the core and embedded packages - Updated dependencies [71a3eac] - @rsc-parser/react-client@0.9.1 ## 0.9.0 ### Minor Changes - 0f2a24a: Fix network graph max call stack error ### Patch Changes - Updated dependencies [0f2a24a] - @rsc-parser/react-client@0.9.0 ## 0.8.0 ### Minor Changes - 3db1a5f: Don't flip recording state when running triggerReadNextJsScriptTags - 1478826: Add one error boundary per tab panel in RequestDetail ### Patch Changes - Updated dependencies [3db1a5f] - Updated dependencies [1478826] - @rsc-parser/react-client@0.8.0 ## 0.7.0 ### Minor Changes - 531f06e: Update fork of ReactFlightClient.js - 4bf3887: Make it possible to read Next.js script tags from the Chrome extension ### Patch Changes - Updated dependencies [531f06e] - Updated dependencies [4bf3887] - @rsc-parser/react-client@0.7.0 ## 0.6.0 ### Minor Changes - 3f5a1c5: Make the "Headers" tab table column 1 min-size smaller, and make the `BottomPanel` default size larger - 22cbbbf: Redesign `RequestTab` again - f8836e5: Make request tab multiline by default ### Patch Changes - 0002b9a: Add flex wrapping to the tabs in `RequestDetail` - Updated dependencies [3f5a1c5] - Updated dependencies [0002b9a] - Updated dependencies [22cbbbf] - Updated dependencies [f8836e5] - @rsc-parser/react-client@0.6.0 ## 0.5.1 ### Patch Changes - 1c0b883: Fix `contentScript.ts` bundling to avoid ESM imports - d3228c2: Remove debug console.log statements - Updated dependencies [1c0b883] - Updated dependencies [d3228c2] - @rsc-parser/react-client@0.5.1 ## 0.5.0 ### Minor Changes - 44a478e: Create a Timings tab - b07521b: Capture request and response headers - 2aa4133: Refactor fetch patching and events - c937a64: Rename Chrome extension root component from `App` to `RscDevtoolsExtension` - 8ae76c5: Make the `BottomPanel` easier to resize - 56627ae: Improve Headers tab design and add general information - 4096674: Improve the `Raw` tab - 2883dd1: Restructure @rsc-parser/chrome-extension files - e155780: Create an `OverflowButton` for less important actions in `PanelLayout` - a4eca6d: Add text on top of links in `RequestDetailTabNetwork` - 65d0acd: Create `@rsc-parser/react-client` ### Patch Changes - fe3446e: Make it possible to position the `BottomPanel` on the right - 57c8a9b: Refactor `ViewerStreams` internals to allow a single list of tabs - 095e40e: Ensure that `react` and `react-dom` are externalized - a3a202c: Sync the version @rsc-parser/embedded-example with the other packages - 800fced: Decrease the minimum panel sizes in `FlightResponseSelector` - Updated dependencies [44a478e] - Updated dependencies [56627ae] - Updated dependencies [4096674] - Updated dependencies [57c8a9b] - Updated dependencies [a4eca6d] - Updated dependencies [65d0acd] - @rsc-parser/react-client@0.5.0 ## 0.4.2 ### Patch Changes - 5356856: Only show links for which there are nodes in FlightResponseTabNetwork ## 0.4.1 ### Patch Changes - e129d14: Specify files in @rsc-parser/core ## 0.4.0 ### Minor Changes - 61a3d5a: Expose `unstable_Viewer` ## 0.3.1 ### Patch Changes - 4542777: Fix Chrome Extension bundling - e09465a: Add typecheck command - 4f48e76: Add format command everywher - ed20b59: Remove console logs ## 0.3.0 ### Minor Changes - f313d13: Compile Chrome extension scripts using Vite - ee03219: Move fetch patching code to @rsc-parser/core - 4d0f42e: Added support for server actions ### Patch Changes - 2cd0dd4: Export `unstable_createFlightResponse` - 2836e57: Add a button to read Next.js payload script tags - faa158f: Export `unstable_createFlightResponse` ## 0.2.4 ### Patch Changes - 71e069c: Set a higher z-index for the BottomPanel when when open ## 0.2.3 ### Patch Changes - f181ce8: Scope styles for RscDevtoolsPanel - 39e168a: Stop wrapping {children} ); } function useRscEvents() { const [events, setEvents] = useState([]); const [isRecording, setIsRecording] = useState(false); useEffect(() => { if (!isRecording) { return; } fetchPatcher({ onRscEvent: (event) => { if ( isRscChunkEvent(event) && events .filter(isRscChunkEvent) .some((item) => arraysEqual( item.data.chunkValue, Array.from(event.data.chunkValue), ), ) ) { return true; } startTransition(() => { setEvents((previous) => [...previous, event]); }); }, }); }, [isRecording]); const startRecording = useCallback(() => { setIsRecording(true); }, []); const clearEvents = useCallback(() => { setEvents([]); }, []); const triggerReadNextJsScriptTags = useCallback(() => { const events = readNextJsScriptTags(); if (!events) { return; } startTransition(() => { setEvents((previousEvents) => [...previousEvents, ...events]); }); }, []); return { isRecording, startRecording, events, clearEvents, triggerReadNextJsScriptTags, }; } function arraysEqual(a: unknown[], b: unknown[]) { if (a === b) { return true; } if (a == null || b == null) { return false; } if (a.length !== b.length) { return false; } // If you don't care about the order of the elements inside // the array, you should sort both arrays here. // Please note that calling sort on an array will modify that array. // you might want to clone your array first. for (let i = 0; i < a.length; ++i) { if (a[i] !== b[i]) { return false; } } return true; } ================================================ FILE: packages/embedded/eslint.config.js ================================================ // @ts-check import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; export default [ { ignores: [ 'dist/**', 'storybook-static/**', '.turbo/**', 'vite.config.ts.timestamp*', ], }, ...[eslint.configs.recommended, ...tseslint.configs.recommended].map( (conf) => ({ ...conf, files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], }), ), { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], rules: { curly: ['error', 'all'], }, }, ]; ================================================ FILE: packages/embedded/package.json ================================================ { "name": "@rsc-parser/embedded", "packageManager": "bun@1.3.3", "version": "1.1.2", "license": "MIT", "repository": "https://github.com/alvarlagerlof/rsc-parser", "type": "module", "scripts": { "build": "vite build", "dev": "vite build --watch --mode development", "lint": "eslint", "format": "prettier . --check --config ../../.prettierrc", "typecheck": "tsc --noEmit", "clean": "rm -rf dist" }, "files": [ "package.json", "dist" ], "types": "./dist/js/RscDevtoolsPanel.d.ts", "exports": { ".": { "import": "./dist/js/embedded.js" }, "./style.css": "./dist/style.css" }, "devDependencies": { "@eslint/js": "9.39.4", "@rsc-parser/core": "workspace:^", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "4.7.0", "eslint": "9.39.4", "rollup-plugin-preserve-directives": "0.4.0", "typescript": "5.9.3", "typescript-eslint": "8.57.2", "vite": "7.3.1", "vite-plugin-dts": "4.5.4" }, "peerDependencies": { "react": "19.2.4", "react-dom": "19.2.4" } } ================================================ FILE: packages/embedded/tsconfig.json ================================================ { "compilerOptions": { "strict": true, "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "NodeNext", "moduleResolution": "NodeNext", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true }, "exclude": ["node_modules", "vite.config.ts.timestamp*"] } ================================================ FILE: packages/embedded/turbo.json ================================================ { "$schema": "https://turbo.build/schema.json", "extends": ["//"], "tasks": { "build": { "outputs": ["dist/**"] } } } ================================================ FILE: packages/embedded/vite.config.ts ================================================ // vite.config.js import { resolve } from 'path'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import dts from 'vite-plugin-dts'; import preserveDirectives from 'rollup-plugin-preserve-directives'; export default defineConfig(({ mode }) => ({ plugins: [react(), dts()], build: { outDir: './dist/js', emptyOutDir: false, lib: { // Could also be a dictionary or array of multiple entry points entry: resolve(__dirname, 'RscDevtoolsPanel.tsx'), name: 'embedded', // the proper extensions will be added fileName: 'embedded', formats: ['es'], }, rollupOptions: { external: ['react', 'react/jsx-runtime', 'react-dom'], output: { preserveModules: true, }, plugins: [preserveDirectives()], }, minify: false, sourcemap: mode === 'development' ? 'inline' : false, }, })); ================================================ FILE: packages/react-client/.gitignore ================================================ /node_modules dist vite.config.ts.timestamp* ================================================ FILE: packages/react-client/.prettierignore ================================================ dist ================================================ FILE: packages/react-client/.prettierrc ================================================ { "singleQuote": true } ================================================ FILE: packages/react-client/CHANGELOG.md ================================================ # @rsc-parser/react-client ## 1.1.2 ### Patch Changes - c965537: Add "repository" to /embedded package.json ## 1.1.1 ### Patch Changes - e0b935a: Improve console chunk list rendering ## 1.1.0 ### Minor Changes - 6ce957a: Add support for rendering console chunks - 83cbaaf: Link to debug info references ### Patch Changes - 6d9815c: Improve readability when a tab has multiple panels ## 1.0.0 ### Major Changes - 65231fc: Fix dev check in the Chrome extension BREAKING: `createFlightResponse` from @rsc-parser/react-client now takes a `__DEV__` parameter. ### Patch Changes - 44cc2a8: Fix meter styling - 1860787: Fixed a few react runtime errors ## 0.9.2 ### Patch Changes - 5fb463b: Specify "files" for @rsc-parser/react-client ## 0.9.1 ### Patch Changes - 71a3eac: Properly externalize react in the core and embedded packages ## 0.9.0 ### Minor Changes - 0f2a24a: Fix network graph max call stack error ## 0.8.0 ### Minor Changes - 3db1a5f: Don't flip recording state when running triggerReadNextJsScriptTags - 1478826: Add one error boundary per tab panel in RequestDetail ## 0.7.0 ### Minor Changes - 531f06e: Update fork of ReactFlightClient.js - 4bf3887: Make it possible to read Next.js script tags from the Chrome extension ## 0.6.0 ### Minor Changes - 3f5a1c5: Make the "Headers" tab table column 1 min-size smaller, and make the `BottomPanel` default size larger - 22cbbbf: Redesign `RequestTab` again - f8836e5: Make request tab multiline by default ### Patch Changes - 0002b9a: Add flex wrapping to the tabs in `RequestDetail` ## 0.5.1 ### Patch Changes - 1c0b883: Fix `contentScript.ts` bundling to avoid ESM imports - d3228c2: Remove debug console.log statements ## 0.5.0 ### Minor Changes - 44a478e: Create a Timings tab - 56627ae: Improve Headers tab design and add general information - 4096674: Improve the `Raw` tab - a4eca6d: Add text on top of links in `RequestDetailTabNetwork` - 65d0acd: Create `@rsc-parser/react-client` ### Patch Changes - 57c8a9b: Refactor `ViewerStreams` internals to allow a single list of tabs ================================================ FILE: packages/react-client/eslint.config.js ================================================ // @ts-check import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; export default [ { ignores: ['dist/**', '.turbo/**', 'vite.config.ts.timestamp*'], }, ...[eslint.configs.recommended, ...tseslint.configs.recommended].map( (conf) => ({ ...conf, files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], }), ), ]; ================================================ FILE: packages/react-client/flight.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ export * from './src/ReactFlightClient'; // ReactSymbols technically comes from another package in the React source. export * from './src/ReactSymbols'; ================================================ FILE: packages/react-client/package.json ================================================ { "name": "@rsc-parser/react-client", "version": "1.1.2", "license": "MIT", "packageManager": "bun@1.3.3", "type": "module", "files": [ "package.json", "dist" ], "scripts": { "build": "vite build", "dev": "vite build --watch --mode development", "lint": "eslint", "format": "prettier . --check --config ../../.prettierrc", "typecheck": "tsc --noEmit", "clean": "rm -rf dist" }, "exports": { ".": { "import": "./dist/flight.js", "types": "./dist/flight.d.ts" } }, "devDependencies": { "@eslint/eslintrc": "3.3.5", "@eslint/js": "9.39.4", "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "eslint": "9.39.4", "prettier": "3.8.1", "typescript": "5.9.3", "typescript-eslint": "8.57.2", "vite": "7.3.1", "vite-plugin-dts": "4.5.4", "@vitejs/plugin-react": "4.7.0" } } ================================================ FILE: packages/react-client/src/ReactDOMTypes.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import type { CrossOriginString } from './crossOriginStrings'; export type PrefetchDNSOptions = Record; export type PreconnectOptions = { crossOrigin?: string }; export type PreloadOptions = { as: string; crossOrigin?: string; integrity?: string; type?: string; nonce?: string; fetchPriority?: FetchPriorityEnum; imageSrcSet?: string; imageSizes?: string; referrerPolicy?: string; }; export type PreloadModuleOptions = { as?: string; crossOrigin?: string; integrity?: string; nonce?: string; }; export type PreinitOptions = { as: string; precedence?: string; crossOrigin?: string; integrity?: string; nonce?: string; fetchPriority?: FetchPriorityEnum; }; export type PreinitModuleOptions = { as?: string; crossOrigin?: string; integrity?: string; nonce?: string; }; export type CrossOriginEnum = '' | 'use-credentials' | CrossOriginString; export type FetchPriorityEnum = 'high' | 'low' | 'auto'; export type PreloadImplOptions = { crossOrigin?: CrossOriginEnum; integrity?: string | null | undefined; nonce?: string | null | undefined; type?: string | null | undefined; fetchPriority?: string | null | undefined; referrerPolicy?: string | null | undefined; imageSrcSet?: string | null | undefined; imageSizes?: string | null | undefined; media?: string | null | undefined; }; export type PreloadModuleImplOptions = { as?: string | null | undefined; crossOrigin?: CrossOriginEnum | null | undefined; integrity?: string | null | undefined; nonce?: string | null | undefined; }; export type PreinitStyleOptions = { crossOrigin?: CrossOriginEnum | null | undefined; integrity?: string | null | undefined; fetchPriority?: string | null | undefined; }; export type PreinitScriptOptions = { crossOrigin?: CrossOriginEnum | null | undefined; integrity?: string | null | undefined; fetchPriority?: string | null | undefined; nonce?: string | null | undefined; }; export type PreinitModuleScriptOptions = { crossOrigin?: CrossOriginEnum | null | undefined; integrity?: string | null | undefined; nonce?: string | null | undefined; }; export type HostDispatcher = { prefetchDNS: (href: string) => void; preconnect: ( href: string, crossOrigin?: CrossOriginEnum | null | undefined, ) => void; preload: ( href: string, as: string, options?: PreloadImplOptions | null | undefined, ) => void; preloadModule: ( href: string, options?: PreloadModuleImplOptions | null | undefined, ) => void; preinitStyle: ( href: string, precedence: string | null | undefined, options?: PreinitStyleOptions | null | undefined, ) => void; preinitScript: (src: string, options?: PreinitScriptOptions) => void; preinitModuleScript: ( src: string, options?: PreinitModuleScriptOptions | null | undefined, ) => void; }; export type ImportMap = { imports?: { [specifier: string]: string; }; scopes?: { [scope: string]: { [specifier: string]: string; }; }; }; ================================================ FILE: packages/react-client/src/ReactFlightClient.ts ================================================ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import { StringDecoder, createStringDecoder, readFinalStringChunk, readPartialStringChunk, } from './ReactFlightClientConfigBrowser'; import { ClientReferenceMetadata } from './ReactFlightClientConfigBundlerWebpack'; import { ImportMetadata } from './ReactFlightImportMetadata'; import { HintCode, HintModel } from './ReactFlightServerConfigDOM'; import { REACT_ELEMENT_TYPE } from './ReactSymbols'; import { ReactComponentInfo, ReactErrorInfoDev, ReactStackTrace, } from './ReactTypes'; const enablePostpone = true; const enableFlightReadableStream = true; const enableBinaryFlight = true; // const enableOwnerStacks = false; export type TextChunk = { type: 'text'; id: string; value: string; originalValue: string; timestamp: number; _response: FlightResponse; }; export type ModuleChunk = { type: 'module'; id: string; value: ImportMetadata; originalValue: string; timestamp: number; _response: FlightResponse; }; export type ModelChunk = { type: 'model'; id: string; value: unknown; originalValue: string; timestamp: number; _response: FlightResponse; }; export type HintChunk = { type: 'hint'; id: string; code: string; value: HintModel; originalValue: { code: string; model: string }; timestamp: number; _response: FlightResponse; }; export type ErrorDevChunk = { type: 'errorDev'; id: string; error: ErrorWithDigest; originalValue: ReactErrorInfoDev; timestamp: number; _response: FlightResponse; }; export type ErrorProdChunk = { type: 'errorProd'; id: string; error: ErrorWithDigest; originalValue: null; timestamp: number; _response: FlightResponse; }; export type PostponeDevChunk = { type: 'postponeDev'; id: string; error: Postpone; originalValue: { reason: string; stack: string }; timestamp: number; _response: FlightResponse; }; export type PostponeProdChunk = { type: 'postponeProd'; id: string; error: Postpone; originalValue: undefined; timestamp: number; _response: FlightResponse; }; export type BufferChunk = { type: 'buffer'; id: string; value: ArrayBufferView | ArrayBuffer; originalValue: string; timestamp: number; _response: FlightResponse; }; export type DebugInfoChunk = { type: 'debugInfo'; id: string; value: unknown; originalValue: string; timestamp: number; _response: FlightResponse; }; export type ConsoleChunk = { type: 'console'; id: string; value: { methodName: string; stackTrace: ReactStackTrace; owner: Reference; env: string; args: Array; }; originalValue: undefined; timestamp: number; _response: FlightResponse; }; export type StartAsyncIterableChunk = { type: 'startAsyncIterable'; id: string; value: { iterator: boolean; }; originalValue: undefined; timestamp: number; _response: FlightResponse; }; export type StartReadableStreamChunk = { type: 'startReadableStream'; id: string; value: { type: void | 'bytes'; }; originalValue: undefined; timestamp: number; _response: FlightResponse; }; export type StopStreamChunk = { type: 'stopStream'; id: string; value: UninitializedModel; originalValue: undefined; timestamp: number; _response: FlightResponse; }; export type Chunk = | TextChunk | ModuleChunk | HintChunk | ModelChunk | ErrorDevChunk | ErrorProdChunk | PostponeDevChunk | PostponeProdChunk | BufferChunk | DebugInfoChunk | ConsoleChunk | StartReadableStreamChunk | StartAsyncIterableChunk | StopStreamChunk; type RowParserState = 0 | 1 | 2 | 3 | 4; type UninitializedModel = string; type JSONValue = | string | number | boolean | null | Array | { [key: string]: JSONValue }; export type FlightResponse = { _rowState: RowParserState; _rowID: number; // parts of a row ID parsed so far _rowTag: number; // 0 indicates that we're currently parsing the row ID _rowLength: number; // remaining bytes in the row. 0 indicates that we're looking for a newline. _buffer: Array; // chunks received so far as part of this row _stringDecoder: StringDecoder; _replayConsole: boolean; _chunks: Chunk[]; _currentTimestamp: number; __DEV__: boolean; _fromJSON: (key: string, value: JSONValue) => any; }; export type Reference = { $$type: 'reference'; id: string; identifier: string; type: string; }; export function isReference(x: unknown): x is Reference { return ( typeof x === 'object' && x !== null && '$$type' in x && x.$$type === 'reference' ); } function createServerReferenceProxy /*, T>*/( response: FlightResponse, metaData: { id: any; bound: null /*| Thenable>*/ }, ): Pick /*:(...A) => Promise*/ { // const callServer = response._callServer; // const proxy = function (): Promise { // // $FlowFixMe[method-unbinding] // const args = Array.prototype.slice.call(arguments); // const p = metaData.bound; // if (!p) { // return callServer(metaData.id, args); // } // if (p.status === INITIALIZED) { // const bound = p.value; // return callServer(metaData.id, bound.concat(args)); // } // // Since this is a fake Promise whose .then doesn't chain, we have to wrap it. // // TODO: Remove the wrapper once that's fixed. // return ((Promise.resolve(p): any): Promise>).then( // function (bound) { // return callServer(metaData.id, bound.concat(args)); // }, // ); // }; // registerServerReference(proxy, metaData, response._encodeFormAction); // return proxy; return { identifier: 'F', type: 'Server Reference', }; } function getOutlinedModel( response: FlightResponse, reference: string, parentObject: object, key: string, map: ( response: FlightResponse, model: any, ) => Pick /*T*/, ): Reference /*: T*/ { const path = reference.split(':'); const id = parseInt(path[0], 16); const metadata = map(response, undefined); return { $$type: 'reference', id: new Number(id).toString(16), identifier: '', type: metadata.type, }; // const chunk = getChunk(response, id); // switch (chunk.status) { // case RESOLVED_MODEL: // initializeModelChunk(chunk); // break; // case RESOLVED_MODULE: // initializeModuleChunk(chunk); // break; // } // The status might have changed after initialization. // switch (chunk.status) { // case INITIALIZED: // let value = chunk.value; // for (let i = 1; i < path.length; i++) { // value = value[path[i]]; // } // const chunkValue = map(response, value); // if (response.__DEV__ && chunk._debugInfo) { // // If we have a direct reference to an object that was rendered by a synchronous // // server component, it might have some debug info about how it was rendered. // // We forward this to the underlying object. This might be a React Element or // // an Array fragment. // // If this was a string / number return value we lose the debug info. We choose // // that tradeoff to allow sync server components to return plain values and not // // use them as React Nodes necessarily. We could otherwise wrap them in a Lazy. // if ( // typeof chunkValue === 'object' && // chunkValue !== null && // (isArray(chunkValue) || // typeof chunkValue[ASYNC_ITERATOR] === 'function' || // chunkValue.$$typeof === REACT_ELEMENT_TYPE) && // !chunkValue._debugInfo // ) { // // We should maybe use a unique symbol for arrays but this is a React owned array. // // $FlowFixMe[prop-missing]: This should be added to elements. // Object.defineProperty((chunkValue: any), '_debugInfo', { // configurable: false, // enumerable: false, // writable: true, // value: chunk._debugInfo, // }); // } // } // return chunkValue; // case PENDING: // case BLOCKED: // return waitForReference(chunk, parentObject, key, response, map, path); // default: // // This is an error. Instead of erroring directly, we're going to encode this on // // an initialization handler so that we can catch it at the nearest Element. // if (initializingHandler) { // initializingHandler.errored = true; // initializingHandler.value = chunk.reason; // } else { // initializingHandler = { // parent: null, // chunk: null, // value: chunk.reason, // deps: 0, // errored: true, // }; // } // // Placeholder // return (null: any); // } } function createMap( response: FlightResponse, model: Array<[any, any]>, ): Pick /*: Map*/ { //return new Map(model); return { identifier: 'Q', type: 'Map', } as const; } function createSet( response: FlightResponse, model: Array, ): Pick /*: Set*/ { //return new Set(model); return { identifier: 'W', type: 'Set', }; } function createBlob( response: FlightResponse, model: Array, ): Pick /*: Blob*/ { // return new Blob(model.slice(1), { type: model[0] }); return { identifier: 'B', type: 'Blob', }; } function createFormData( response: FlightResponse, model: Array<[any, any]>, ): Pick /*: FormData*/ { // const formData = new FormData(); // for (let i = 0; i < model.length; i++) { // formData.append(model[i][0], model[i][1]); // } // return formData; return { identifier: 'K', type: 'FormData', }; } function extractIterator( response: FlightResponse, model: Array, ): Pick /*: Iterator*/ { // // $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array. // return model[Symbol.iterator](); return { identifier: 'i', type: 'Iterator', }; } function createModel( response: FlightResponse, model: any, ): Pick /*: any*/ { // return model; return { identifier: '', type: 'Reference', }; } function parseModelString( response: FlightResponse, parentObject: object, key: string, value: string, ) { if (value[0] === '$') { if (value === '$') { // A very common symbol. return REACT_ELEMENT_TYPE; } switch (value[1]) { case '$': { // This was an escaped string value. return value.slice(1); } case 'L': { // Lazy node const id = parseInt(value.slice(2), 16); // const chunk = getChunk(response, id); // // We create a React.lazy wrapper around any lazy values. // // When passed into React, we'll know how to suspend on this. // return createLazyChunkWrapper(chunk); return { $$type: 'reference', id: new Number(id).toString(16), identifier: 'L', type: 'Lazy node', } satisfies Reference; } case '@': { // Promise const id = parseInt(value.slice(2), 16); // const chunk = getChunk(response, id); // return chunk; return { $$type: 'reference', id: new Number(id).toString(16), identifier: '@', type: 'Promise', } satisfies Reference; } case 'S': { // Symbol return Symbol.for(value.slice(2)); } case 'F': { // Server Reference const ref = value.slice(2); return getOutlinedModel( response, ref, parentObject, key, createServerReferenceProxy, ); } case 'T': { // Temporary Reference // const reference = '$' + value.slice(2); // const temporaryReferences = response._tempRefs; // if (temporaryReferences == null) { // throw new Error( // 'Missing a temporary reference set but the RSC response returned a temporary reference. ' + // 'Pass a temporaryReference option with the set that was used with the reply.', // ); // } // return readTemporaryReference(temporaryReferences, reference); return 'Missing a temporary reference set but the RSC response returned a temporary reference.'; } case 'Q': { // Map const ref = value.slice(2); return getOutlinedModel(response, ref, parentObject, key, createMap); } case 'W': { // Set const ref = value.slice(2); return getOutlinedModel(response, ref, parentObject, key, createSet); } case 'B': { // Blob if (enableBinaryFlight) { const ref = value.slice(2); return getOutlinedModel(response, ref, parentObject, key, createBlob); } return undefined; } case 'K': { // FormData const ref = value.slice(2); return getOutlinedModel( response, ref, parentObject, key, createFormData, ); } case 'Z': { // Error if (response.__DEV__) { const ref = value.slice(2); return getOutlinedModel(response, ref, parentObject, key, () => { return { identifier: 'Z', type: 'Error', }; }); } else { return resolveErrorProd(response, 1000); } } case 'i': { // Iterator const ref = value.slice(2); return getOutlinedModel( response, ref, parentObject, key, extractIterator, ); } case 'I': { // $Infinity return Infinity; } case '-': { // $-0 or $-Infinity if (value === '$-0') { return -0; } else { return -Infinity; } } case 'N': { // $NaN return NaN; } case 'u': { // matches "$undefined" // Special encoding for `undefined` which can't be serialized as JSON otherwise. return undefined; } case 'D': { // Date return new Date(Date.parse(value.slice(2))); } case 'n': { // BigInt return BigInt(value.slice(2)); } case 'E': { if (response.__DEV__) { // // In DEV mode we allow indirect eval to produce functions for logging. // // This should not compile to eval() because then it has local scope access. // try { // return (0, eval)(value.slice(2)); // } catch (x) { // // We currently use this to express functions so we fail parsing it, // // let's just return a blank function as a place holder. // return function () {}; // } } // Fallthrough } case 'Y': { if (response.__DEV__) { // // In DEV mode we encode omitted objects in logs as a getter that throws // // so that when you try to access it on the client, you know why that // // happened. // Object.defineProperty(parentObject, key, { // get: function () { // // We intentionally don't throw an error object here because it looks better // // without the stack in the console which isn't useful anyway. // throw ( // 'This object has been omitted by React in the console log ' + // 'to avoid sending too much data from the server. Try logging smaller ' + // 'or more specific objects.' // ); // }, // enumerable: true, // configurable: false, // }); // return null; } // Fallthrough } default: { // We assume that anything else is a reference ID. const ref = value.slice(1); return getOutlinedModel(response, ref, parentObject, key, createModel); } } } return value; } function parseModelTuple( response: FlightResponse, value: { [key: string]: JSONValue } | ReadonlyArray, ) { if (!Array.isArray(value)) { return value; } if (value.length < 4) { return value; } if (value[0] === REACT_ELEMENT_TYPE) { // TODO: Consider having React just directly accept these arrays as elements. // Or even change the ReactElement type to be an array. return createElement(value[1], value[2], value[3]); } return value; } function ResponseInstance(__DEV__: boolean) { // @ts-expect-error TODO: fix this const response: FlightResponse = { _buffer: [], _rowID: 0, _rowTag: 0, _rowLength: 0, _rowState: 0, _replayConsole: true, _currentTimestamp: 0, _stringDecoder: createStringDecoder(), _chunks: [] as FlightResponse['_chunks'], __DEV__: __DEV__, }; response._fromJSON = createFromJSONCallback(response); return response; } export function createFlightResponse(__DEV__: boolean): FlightResponse { return ResponseInstance(__DEV__); } export function createElement(type: unknown, key: unknown, props: unknown) { const element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type: type as string, key: key as string | number | bigint, // ref: null, props: props as { [key: string]: unknown }, // // Record the component responsible for creating this element. // _owner: null, }; // if (response.__DEV__) { // // We don't really need to add any of these but keeping them for good measure. // // Unfortunately, _store is enumerable in jest matchers so for equality to // // work, I need to keep it or make _store non-enumerable in the other file. // element._store = ({}: { // validated?: boolean, // }); // Object.defineProperty(element._store, 'validated', { // configurable: false, // enumerable: false, // writable: true, // value: true, // This element has already been validated on the server. // }); // Object.defineProperty(element, '_self', { // configurable: false, // enumerable: false, // writable: false, // value: null, // }); // Object.defineProperty(element, '_source', { // configurable: false, // enumerable: false, // writable: false, // value: null, // }); // } return element; } export function isElement(x: unknown): x is ReturnType { return ( typeof x === 'object' && x !== null && '$$typeof' in x && x.$$typeof === REACT_ELEMENT_TYPE ); } function resolveModel( response: FlightResponse, id: number, model: UninitializedModel, ): void { const chunks = response._chunks; const value = parseModel(response, model); chunks.push({ type: 'model', id: new Number(id).toString(16), value: value, originalValue: model, timestamp: response._currentTimestamp, _response: response, }); } function resolveText(response: FlightResponse, id: number, text: string): void { const chunks = response._chunks; // We assume that we always reference large strings after they've been // emitted. chunks.push({ type: 'text', id: new Number(id).toString(16), value: text, originalValue: text, timestamp: response._currentTimestamp, _response: response, }); } function resolveBuffer( response: FlightResponse, id: number, buffer: ArrayBufferView | ArrayBuffer, ): void { const chunks = response._chunks; // We assume that we always reference buffers after they've been emitted. chunks.push({ type: 'buffer', id: new Number(id).toString(16), value: buffer, originalValue: buffer.toString(), timestamp: response._currentTimestamp, _response: response, }); } function resolveModule( response: FlightResponse, id: number, model: UninitializedModel, ): void { const chunks = response._chunks; const clientReferenceMetadata: ClientReferenceMetadata = parseModel( response, model, ); chunks.push({ type: 'module', id: new Number(id).toString(16), value: clientReferenceMetadata, originalValue: model, timestamp: response._currentTimestamp, _response: response, }); } // function resolveStream>( // response: Response, // id: number, // stream: T, // controller: FlightStreamController, // ): void { // const chunks = response._chunks; // const chunk = chunks.get(id); // if (!chunk) { // chunks.set(id, createInitializedStreamChunk(response, stream, controller)); // return; // } // if (chunk.status !== PENDING) { // // We already resolved. We didn't expect to see this. // return; // } // const resolveListeners = chunk.value; // const resolvedChunk: InitializedStreamChunk = (chunk: any); // resolvedChunk.status = INITIALIZED; // resolvedChunk.value = stream; // resolvedChunk.reason = controller; // if (resolveListeners !== null) { // wakeChunk(resolveListeners, chunk.value); // } // } function startReadableStream( response: FlightResponse, id: number, type: void | 'bytes', ): void { response._chunks.push({ type: 'startReadableStream', id: new Number(id).toString(16), value: { type: type, }, originalValue: undefined, timestamp: response._currentTimestamp, _response: response, }); // let controller: ReadableStreamController = (null: any); // const stream = new ReadableStream({ // type: type, // start(c) { // controller = c; // }, // }); // let previousBlockedChunk: SomeChunk | null = null; // const flightController = { // enqueueValue(value: T): void { // if (previousBlockedChunk === null) { // controller.enqueue(value); // } else { // // We're still waiting on a previous chunk so we can't enqueue quite yet. // previousBlockedChunk.then(function () { // controller.enqueue(value); // }); // } // }, // enqueueModel(json: UninitializedModel): void { // if (previousBlockedChunk === null) { // // If we're not blocked on any other chunks, we can try to eagerly initialize // // this as a fast-path to avoid awaiting them. // const chunk: ResolvedModelChunk = createResolvedModelChunk( // response, // json, // ); // initializeModelChunk(chunk); // const initializedChunk: SomeChunk = chunk; // if (initializedChunk.status === INITIALIZED) { // controller.enqueue(initializedChunk.value); // } else { // chunk.then( // (v) => controller.enqueue(v), // (e) => controller.error((e: any)), // ); // previousBlockedChunk = chunk; // } // } else { // // We're still waiting on a previous chunk so we can't enqueue quite yet. // const blockedChunk = previousBlockedChunk; // const chunk: SomeChunk = createPendingChunk(response); // chunk.then( // (v) => controller.enqueue(v), // (e) => controller.error((e: any)), // ); // previousBlockedChunk = chunk; // blockedChunk.then(function () { // if (previousBlockedChunk === chunk) { // // We were still the last chunk so we can now clear the queue and return // // to synchronous emitting. // previousBlockedChunk = null; // } // resolveModelChunk(chunk, json); // }); // } // }, // close(json: UninitializedModel): void { // if (previousBlockedChunk === null) { // controller.close(); // } else { // const blockedChunk = previousBlockedChunk; // // We shouldn't get any more enqueues after this so we can set it back to null. // previousBlockedChunk = null; // blockedChunk.then(() => controller.close()); // } // }, // error(error: mixed): void { // if (previousBlockedChunk === null) { // // $FlowFixMe[incompatible-call] // controller.error(error); // } else { // const blockedChunk = previousBlockedChunk; // // We shouldn't get any more enqueues after this so we can set it back to null. // previousBlockedChunk = null; // blockedChunk.then(() => controller.error((error: any))); // } // }, // }; // resolveStream(response, id, stream, flightController); } // function asyncIterator(this: $AsyncIterator) { // // Self referencing iterator. // return this; // } // function createIterator( // next: (arg: void) => SomeChunk>, // ): $AsyncIterator { // const iterator: any = { // next: next, // // TODO: Add return/throw as options for aborting. // }; // // TODO: The iterator could inherit the AsyncIterator prototype which is not exposed as // // a global but exists as a prototype of an AsyncGenerator. However, it's not needed // // to satisfy the iterable protocol. // (iterator: any)[ASYNC_ITERATOR] = asyncIterator; // return iterator; // } function startAsyncIterable( response: FlightResponse, id: number, iterator: boolean, ): void { response._chunks.push({ type: 'startAsyncIterable', id: new Number(id).toString(16), value: { iterator: iterator, }, originalValue: undefined, timestamp: response._currentTimestamp, _response: response, }); // const buffer: Array>> = []; // let closed = false; // let nextWriteIndex = 0; // const flightController = { // enqueueValue(value: T): void { // if (nextWriteIndex === buffer.length) { // buffer[nextWriteIndex] = createInitializedIteratorResultChunk( // response, // value, // false, // ); // } else { // const chunk: PendingChunk> = (buffer[ // nextWriteIndex // ]: any); // const resolveListeners = chunk.value; // const rejectListeners = chunk.reason; // const initializedChunk: InitializedChunk> = // (chunk: any); // initializedChunk.status = INITIALIZED; // initializedChunk.value = { done: false, value: value }; // if (resolveListeners !== null) { // wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); // } // } // nextWriteIndex++; // }, // enqueueModel(value: UninitializedModel): void { // if (nextWriteIndex === buffer.length) { // buffer[nextWriteIndex] = createResolvedIteratorResultChunk( // response, // value, // false, // ); // } else { // resolveIteratorResultChunk(buffer[nextWriteIndex], value, false); // } // nextWriteIndex++; // }, // close(value: UninitializedModel): void { // closed = true; // if (nextWriteIndex === buffer.length) { // buffer[nextWriteIndex] = createResolvedIteratorResultChunk( // response, // value, // true, // ); // } else { // resolveIteratorResultChunk(buffer[nextWriteIndex], value, true); // } // nextWriteIndex++; // while (nextWriteIndex < buffer.length) { // // In generators, any extra reads from the iterator have the value undefined. // resolveIteratorResultChunk( // buffer[nextWriteIndex++], // '"$undefined"', // true, // ); // } // }, // error(error: Error): void { // closed = true; // if (nextWriteIndex === buffer.length) { // buffer[nextWriteIndex] = // createPendingChunk>(response); // } // while (nextWriteIndex < buffer.length) { // triggerErrorOnChunk(buffer[nextWriteIndex++], error); // } // }, // }; // const iterable: $AsyncIterable = { // [ASYNC_ITERATOR](): $AsyncIterator { // let nextReadIndex = 0; // return createIterator((arg) => { // if (arg !== undefined) { // throw new Error( // 'Values cannot be passed to next() of AsyncIterables passed to Client Components.', // ); // } // if (nextReadIndex === buffer.length) { // if (closed) { // // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors // return new Chunk( // INITIALIZED, // { done: true, value: undefined }, // null, // response, // ); // } // buffer[nextReadIndex] = // createPendingChunk>(response); // } // return buffer[nextReadIndex++]; // }); // }, // }; // TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after // reading through the end, but currently we favor code size over this optimization. // resolveStream( // response, // id, // iterator ? iterable[ASYNC_ITERATOR]() : iterable, // flightController, // ); } // function stopStream( // response: FlightResponse, // id: number, // row: UninitializedModel, // ): void { // const chunks = response._chunks; // const chunk = chunks.get(id); // if (!chunk || chunk.status !== INITIALIZED) { // // We didn't expect not to have an existing stream; // return; // } // const streamChunk: InitializedStreamChunk = (chunk: any); // const controller = streamChunk.reason; // controller.close(row === '' ? '"$undefined"' : row); // } function stopStream( response: FlightResponse, id: number, row: UninitializedModel, ): void { const chunks = response._chunks; chunks.push({ type: 'stopStream', id: new Number(id).toString(16), value: row, originalValue: undefined, timestamp: response._currentTimestamp, _response: response, }); // const chunk = chunks.get(id); // if (!chunk || chunk.status !== INITIALIZED) { // // We didn't expect not to have an existing stream; // return; // } // const streamChunk: InitializedStreamChunk = (chunk: any); // const controller = streamChunk.reason; // controller.close(row === '' ? '"$undefined"' : row); } type ErrorWithDigest = Error & { digest?: string }; function resolveErrorProd(response: FlightResponse, id: number): void { // if (response.__DEV__) { // // These errors should never make it into a build so we don't need to encode them in codes.json // // eslint-disable-next-line react-internal/prod-error-codes // throw new Error( // "resolveErrorProd should never be called in development mode. Use resolveErrorDev instead. This is a bug in React.", // ); // } const error = new Error( 'An error occurred in the Server Components render. The specific message is omitted in production' + ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' + ' may provide additional details about the nature of the error.', ); error.stack = 'Error: ' + error.message; const chunks = response._chunks; chunks.push({ type: 'errorProd', id: new Number(id).toString(16), error: error, originalValue: null, timestamp: response._currentTimestamp, _response: response, }); } function resolveErrorDev( response: FlightResponse, id: number, errorInfo: ReactErrorInfoDev, ): void { // if (!response.__DEV__) { // // These errors should never make it into a build so we don't need to encode them in codes.json // // eslint-disable-next-line react-internal/prod-error-codes // throw new Error( // "resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.", // ); // } // // eslint-disable-next-line react-internal/prod-error-codes const error = new Error( errorInfo.message || 'An error occurred in the Server Components render but no message was provided', ); // @ts-ex error.stack = errorInfo.stack.join(', '); (error as any).digest = errorInfo.digest; const errorWithDigest = error as ErrorWithDigest; const chunks = response._chunks; chunks.push({ type: 'errorDev', id: new Number(id).toString(16), error: errorWithDigest, originalValue: errorInfo, timestamp: response._currentTimestamp, _response: response, }); } declare class Postpone extends Error { $$typeof: symbol; } const REACT_POSTPONE_TYPE = Symbol.for('react.postpone'); function resolvePostponeProd(response: FlightResponse, id: number): void { // if (response.__DEV__) { // // These errors should never make it into a build so we don't need to encode them in codes.json // // eslint-disable-next-line react-internal/prod-error-codes // throw new Error( // "resolvePostponeProd should never be called in development mode. Use resolvePostponeDev instead. This is a bug in React.", // ); // } const error = new Error( 'A Server Component was postponed. The reason is omitted in production' + ' builds to avoid leaking sensitive details.', ); const postponeInstance = error as Postpone; postponeInstance.$$typeof = REACT_POSTPONE_TYPE; postponeInstance.stack = 'Error: ' + error.message; const chunks = response._chunks; chunks.push({ type: 'postponeProd', id: new Number(id).toString(16), error: postponeInstance, originalValue: undefined, timestamp: response._currentTimestamp, _response: response, }); } function resolvePostponeDev( response: FlightResponse, id: number, reason: string, stack: string, ): void { // if (!response.__DEV__) { // // These errors should never make it into a build so we don't need to encode them in codes.json // // eslint-disable-next-line react-internal/prod-error-codes // throw new Error( // "resolvePostponeDev should never be called in production mode. Use resolvePostponeProd instead. This is a bug in React.", // ); // } // // eslint-disable-next-line react-internal/prod-error-codes const error = new Error(reason || ''); const postponeInstance = error as Postpone; postponeInstance.$$typeof = REACT_POSTPONE_TYPE; postponeInstance.stack = stack; const chunks = response._chunks; chunks.push({ type: 'postponeDev', id: new Number(id).toString(16), error: postponeInstance, originalValue: { reason, stack }, timestamp: response._currentTimestamp, _response: response, }); } function resolveHint( response: FlightResponse, id: number, code: Code, model: UninitializedModel, ): void { const hintModel: HintModel = parseModel(response, model); const chunks = response._chunks; chunks.push({ type: 'hint', id: new Number(id).toString(16), code: code, value: hintModel, originalValue: { code, model }, timestamp: response._currentTimestamp, _response: response, }); } function resolveDebugInfo( response: FlightResponse, id: number, model: UninitializedModel, ): void { // if (!response.__DEV__) { // // These errors should never make it into a build so we don't need to encode them in codes.json // // eslint-disable-next-line react-internal/prod-error-codes // throw new Error( // 'resolveDebugInfo should never be called in production mode. This is a bug in React.', // ); // } const chunks = response._chunks; const value = getOutlinedModel(response, model, {}, '', () => { return { identifier: '', type: 'Debug info', }; }); chunks.push({ type: 'debugInfo', id: new Number(id).toString(16), value: value, originalValue: model, timestamp: response._currentTimestamp, _response: response, }); } function resolveConsoleEntry( response: FlightResponse, id: number, value: UninitializedModel, ): void { // if (!response.__DEV__) { // // These errors should never make it into a build so we don't need to encode them in codes.json // // eslint-disable-next-line react-internal/prod-error-codes // throw new Error( // 'resolveConsoleEntry should never be called in production mode. This is a bug in React.', // ); // } if (!response._replayConsole) { return; } const payload: [string, ReactStackTrace, Reference, string, any] = parseModel( response, value, ); const methodName = payload[0]; const stackTrace = payload[1]; const owner = payload[2]; const env = payload[3]; const args = payload.slice(4); response._chunks.push({ type: 'console', id: new Number(id).toString(16), value: { methodName, stackTrace, owner, env, args, }, originalValue: undefined, timestamp: response._currentTimestamp, _response: response, }); // if (!enableOwnerStacks) { // // Printing with stack isn't really limited to owner stacks but // // we gate it behind the same flag for now while iterating. // printToConsole(methodName, args, env); // return; // } // const callStack = buildFakeCallStack( // response, // stackTrace, // printToConsole.bind(null, methodName, args, env), // ); // if (owner != null) { // const task = initializeFakeTask(response, owner); // if (task !== null) { // task.run(callStack); // return; // } // // TODO: Set the current owner so that consoleWithStackDev adds the component // // stack during the replay - if needed. // } // const rootTask = response._debugRootTask; // if (rootTask != null) { // rootTask.run(callStack); // return; // } // callStack(); } function mergeBuffer( buffer: Array, lastChunk: Uint8Array, ): Uint8Array { const l = buffer.length; // Count the bytes we'll need let byteLength = lastChunk.length; for (let i = 0; i < l; i++) { byteLength += buffer[i].byteLength; } // Allocate enough contiguous space const result = new Uint8Array(byteLength); let offset = 0; // Copy all the buffers into it. for (let i = 0; i < l; i++) { const chunk = buffer[i]; result.set(chunk, offset); offset += chunk.byteLength; } result.set(lastChunk, offset); return result; } function resolveTypedArray( response: FlightResponse, id: number, buffer: Array, lastChunk: Uint8Array, constructor: | Int8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor | BigInt64ArrayConstructor | BigUint64ArrayConstructor | DataViewConstructor, bytesPerElement: number, ): void { // If the view fits into one original buffer, we just reuse that buffer instead of // copying it out to a separate copy. This means that it's not always possible to // transfer these values to other threads without copying first since they may // share array buffer. For this to work, it must also have bytes aligned to a // multiple of a size of the type. const chunk = buffer.length === 0 && lastChunk.byteOffset % bytesPerElement === 0 ? lastChunk : mergeBuffer(buffer, lastChunk); // TODO: The transfer protocol of RSC is little-endian. If the client isn't little-endian // we should convert it instead. In practice big endian isn't really Web compatible so it's // somewhat safe to assume that browsers aren't going to run it, but maybe there's some SSR // server that's affected. // @ts-expect-error TODO: fix this const view: ArrayBufferView = new constructor( chunk.buffer, chunk.byteOffset, chunk.byteLength / bytesPerElement, ); resolveBuffer(response, id, view); } function processFullBinaryRow( response: FlightResponse, id: number, tag: number, buffer: Array, chunk: Uint8Array, ): void { if (enableBinaryFlight) { switch (tag) { case 65 /* "A" */: // We must always clone to extract it into a separate buffer instead of just a view. // @ts-expect-error TODO: fix this resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer); return; case 79 /* "O" */: resolveTypedArray(response, id, buffer, chunk, Int8Array, 1); return; case 111 /* "o" */: resolveBuffer( response, id, buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk), ); return; case 85 /* "U" */: resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1); return; case 83 /* "S" */: resolveTypedArray(response, id, buffer, chunk, Int16Array, 2); return; case 115 /* "s" */: resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2); return; case 76 /* "L" */: resolveTypedArray(response, id, buffer, chunk, Int32Array, 4); return; case 108 /* "l" */: resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4); return; case 71 /* "G" */: resolveTypedArray(response, id, buffer, chunk, Float32Array, 4); return; case 103 /* "g" */: resolveTypedArray(response, id, buffer, chunk, Float64Array, 8); return; case 77 /* "M" */: resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8); return; case 109 /* "m" */: resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8); return; case 86 /* "V" */: resolveTypedArray(response, id, buffer, chunk, DataView, 1); return; } } const stringDecoder = response._stringDecoder; let row = ''; for (let i = 0; i < buffer.length; i++) { row += readPartialStringChunk(stringDecoder, buffer[i]); } row += readFinalStringChunk(stringDecoder, chunk); processFullStringRow(response, id, tag, row); } function processFullStringRow( response: FlightResponse, id: number, tag: number, row: string, ): void { switch (tag) { case 73 /* "I" */: { resolveModule(response, id, row); return; } case 72 /* "H" */: { const code = row[0] as HintCode; resolveHint(response, id, code, row.slice(1)); return; } case 69 /* "E" */: { const errorInfo = JSON.parse(row); if (response.__DEV__) { resolveErrorDev(response, id, errorInfo); } else { resolveErrorProd(response, id); } return; } case 84 /* "T" */: { resolveText(response, id, row); return; } case 68 /* "D" */: { // if (!response.__DEV__) { // // These errors should never make it into a build so we don't need to encode them in codes.json // // eslint-disable-next-line react-internal/prod-error-codes // throw new Error( // 'resolveDebugInfo should never be called in production mode. This is a bug in React.', // ); // } const cleaned = row.replaceAll(`"`, ''); resolveDebugInfo(response, id, cleaned.slice(1)); return; // // We eagerly initialize the fake task because this resolving happens outside any // // render phase so we're not inside a user space stack at this point. If we waited // // to initialize it when we need it, we might be inside user code. // initializeFakeTask(response, debugInfo); // const chunk = getChunk(response, id); // const chunkDebugInfo: ReactDebugInfo = // chunk._debugInfo || (chunk._debugInfo = []); // chunkDebugInfo.push(debugInfo); } case 87 /* "W" */: { if (response.__DEV__) { resolveConsoleEntry(response, id, row); return; } throw new Error( 'Failed to read a RSC payload created by a development version of React ' + 'on the server while using a production version on the client. Always use ' + 'matching versions on the server and the client.', ); } case 82 /* "R" */: { if (enableFlightReadableStream) { startReadableStream(response, id, undefined); return; } } // Fallthrough case 114 /* "r" */: { if (enableFlightReadableStream) { startReadableStream(response, id, 'bytes'); return; } } // Fallthrough case 88 /* "X" */: { if (enableFlightReadableStream) { startAsyncIterable(response, id, false); return; } } // Fallthrough case 120 /* "x" */: { if (enableFlightReadableStream) { startAsyncIterable(response, id, true); return; } } // Fallthrough case 67 /* "C" */: { if (enableFlightReadableStream) { stopStream(response, id, row); return; } } // Fallthrough case 80 /* "P" */: { if (enablePostpone) { if (response.__DEV__) { const postponeInfo = JSON.parse(row); resolvePostponeDev( response, id, postponeInfo.reason, postponeInfo.stack, ); } else { resolvePostponeProd(response, id); } return; } } // Fallthrough default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ { // We assume anything else is JSON. resolveModel(response, id, row); return; } } } const ROW_ID = 0; const ROW_TAG = 1; const ROW_LENGTH = 2; const ROW_CHUNK_BY_NEWLINE = 3; const ROW_CHUNK_BY_LENGTH = 4; export function processBinaryChunk( response: FlightResponse, chunk: Uint8Array, ): void { let i = 0; let rowState = response._rowState; let rowID = response._rowID; let rowTag = response._rowTag; let rowLength = response._rowLength; const buffer = response._buffer; const chunkLength = chunk.length; while (i < chunkLength) { let lastIdx = -1; switch (rowState) { case ROW_ID: { const byte = chunk[i++]; if (byte === 58 /* ":" */) { // Finished the rowID, next we'll parse the tag. rowState = ROW_TAG; } else { rowID = (rowID << 4) | (byte > 96 ? byte - 87 : byte - 48); } continue; } case ROW_TAG: { const resolvedRowTag = chunk[i]; if ( resolvedRowTag === 84 /* "T" */ || (enableBinaryFlight && (resolvedRowTag === 65 /* "A" */ || resolvedRowTag === 79 /* "O" */ || resolvedRowTag === 111 /* "o" */ || resolvedRowTag === 85 /* "U" */ || resolvedRowTag === 83 /* "S" */ || resolvedRowTag === 115 /* "s" */ || resolvedRowTag === 76 /* "L" */ || resolvedRowTag === 108 /* "l" */ || resolvedRowTag === 71 /* "G" */ || resolvedRowTag === 103 /* "g" */ || resolvedRowTag === 77 /* "M" */ || resolvedRowTag === 109 /* "m" */ || resolvedRowTag === 86)) /* "V" */ ) { rowTag = resolvedRowTag; rowState = ROW_LENGTH; i++; } else if ( (resolvedRowTag > 64 && resolvedRowTag < 91) /* "A"-"Z" */ || resolvedRowTag === 114 /* "r" */ || resolvedRowTag === 120 /* "x" */ ) { rowTag = resolvedRowTag; rowState = ROW_CHUNK_BY_NEWLINE; i++; } else { rowTag = 0; rowState = ROW_CHUNK_BY_NEWLINE; // This was an unknown tag so it was probably part of the data. } continue; } case ROW_LENGTH: { const byte = chunk[i++]; if (byte === 44 /* "," */) { // Finished the rowLength, next we'll buffer up to that length. rowState = ROW_CHUNK_BY_LENGTH; } else { rowLength = (rowLength << 4) | (byte > 96 ? byte - 87 : byte - 48); } continue; } case ROW_CHUNK_BY_NEWLINE: { // We're looking for a newline lastIdx = chunk.indexOf(10 /* "\n" */, i); break; } case ROW_CHUNK_BY_LENGTH: { // We're looking for the remaining byte length lastIdx = i + rowLength; if (lastIdx > chunk.length) { lastIdx = -1; } break; } } const offset = chunk.byteOffset + i; if (lastIdx > -1) { // We found the last chunk of the row const length = lastIdx - i; const lastChunk = new Uint8Array(chunk.buffer, offset, length); processFullBinaryRow(response, rowID, rowTag, buffer, lastChunk); // Reset state machine for a new row i = lastIdx; if (rowState === ROW_CHUNK_BY_NEWLINE) { // If we're trailing by a newline we need to skip it. i++; } rowState = ROW_ID; rowTag = 0; rowID = 0; rowLength = 0; buffer.length = 0; } else { // The rest of this row is in a future chunk. We stash the rest of the // current chunk until we can process the full row. const length = chunk.byteLength - i; const remainingSlice = new Uint8Array(chunk.buffer, offset, length); buffer.push(remainingSlice); // Update how many bytes we're still waiting for. If we're looking for // a newline, this doesn't hurt since we'll just ignore it. rowLength -= remainingSlice.byteLength; break; } } response._rowState = rowState; response._rowID = rowID; response._rowTag = rowTag; response._rowLength = rowLength; } // prettier-ignore export function processStringChunk(response: FlightResponse, chunk: string): void { // This is a fork of processBinaryChunk that takes a string as input. // This can't be just any binary chunk coverted to a string. It needs to be // in the same offsets given from the Flight Server. E.g. if it's shifted by // one byte then it won't line up to the UCS-2 encoding. It also needs to // be valid Unicode. Also binary chunks cannot use this even if they're // value Unicode. Large strings are encoded as binary and cannot be passed // here. Basically, only if Flight Server gave you this string as a chunk, // you can use it here. let i = 0; let rowState = response._rowState; let rowID = response._rowID; let rowTag = response._rowTag; let rowLength = response._rowLength; const buffer = response._buffer; const chunkLength = chunk.length; while (i < chunkLength) { let lastIdx = -1; switch (rowState) { case ROW_ID: { const byte = chunk.charCodeAt(i++); if (byte === 58 /* ":" */) { // Finished the rowID, next we'll parse the tag. rowState = ROW_TAG; } else { rowID = (rowID << 4) | (byte > 96 ? byte - 87 : byte - 48); } continue; } case ROW_TAG: { const resolvedRowTag = chunk.charCodeAt(i); if ( resolvedRowTag === 84 /* "T" */ || (enableBinaryFlight && (resolvedRowTag === 65 /* "A" */ || resolvedRowTag === 79 /* "O" */ || resolvedRowTag === 111 /* "o" */ || resolvedRowTag === 85 /* "U" */ || resolvedRowTag === 83 /* "S" */ || resolvedRowTag === 115 /* "s" */ || resolvedRowTag === 76 /* "L" */ || resolvedRowTag === 108 /* "l" */ || resolvedRowTag === 71 /* "G" */ || resolvedRowTag === 103 /* "g" */ || resolvedRowTag === 77 /* "M" */ || resolvedRowTag === 109 /* "m" */ || resolvedRowTag === 86)) /* "V" */ ) { rowTag = resolvedRowTag; rowState = ROW_LENGTH; i++; } else if ( (resolvedRowTag > 64 && resolvedRowTag < 91) /* "A"-"Z" */ || resolvedRowTag === 114 /* "r" */ || resolvedRowTag === 120 /* "x" */ ) { rowTag = resolvedRowTag; rowState = ROW_CHUNK_BY_NEWLINE; i++; } else { rowTag = 0; rowState = ROW_CHUNK_BY_NEWLINE; // This was an unknown tag so it was probably part of the data. } continue; } case ROW_LENGTH: { const byte = chunk.charCodeAt(i++); if (byte === 44 /* "," */) { // Finished the rowLength, next we'll buffer up to that length. rowState = ROW_CHUNK_BY_LENGTH; } else { rowLength = (rowLength << 4) | (byte > 96 ? byte - 87 : byte - 48); } continue; } case ROW_CHUNK_BY_NEWLINE: { // We're looking for a newline lastIdx = chunk.indexOf('\n', i); break; } case ROW_CHUNK_BY_LENGTH: { if (rowTag !== 84) { throw new Error( 'Binary RSC chunks cannot be encoded as strings. ' + 'This is a bug in the wiring of the React streams.', ); } // For a large string by length, we don't know how many unicode characters // we are looking for but we can assume that the raw string will be its own // chunk. We add extra validation that the length is at least within the // possible byte range it could possibly be to catch mistakes. if (rowLength < chunk.length || chunk.length > rowLength * 3) { throw new Error( 'String chunks need to be passed in their original shape. ' + 'Not split into smaller string chunks. ' + 'This is a bug in the wiring of the React streams.', ); } lastIdx = chunk.length; break; } } if (lastIdx > -1) { // We found the last chunk of the row if (buffer.length > 0) { // If we had a buffer already, it means that this chunk was split up into // binary chunks preceeding it. throw new Error( 'String chunks need to be passed in their original shape. ' + 'Not split into smaller string chunks. ' + 'This is a bug in the wiring of the React streams.', ); } const lastChunk = chunk.slice(i, lastIdx); processFullStringRow(response, rowID, rowTag, lastChunk); // Reset state machine for a new row i = lastIdx; if (rowState === ROW_CHUNK_BY_NEWLINE) { // If we're trailing by a newline we need to skip it. i++; } rowState = ROW_ID; rowTag = 0; rowID = 0; rowLength = 0; buffer.length = 0; } else if (chunk.length !== i) { // The rest of this row is in a future chunk. We only support passing the // string from chunks in their entirety. Not split up into smaller string chunks. // We could support this by buffering them but we shouldn't need to for // this use case. throw new Error( 'String chunks need to be passed in their original shape. ' + 'Not split into smaller string chunks. ' + 'This is a bug in the wiring of the React streams.', ); } } response._rowState = rowState; response._rowID = rowID; response._rowTag = rowTag; response._rowLength = rowLength; } function parseModel(response: FlightResponse, json: UninitializedModel): T { return JSON.parse(json, response._fromJSON); } export function createFromJSONCallback(response: FlightResponse) { return function (key: string, value: JSONValue) { if (typeof value === 'string') { // We can't use .bind here because we need the "this" value. // @ts-expect-error `this` doesn't work return parseModelString(response, this, key, value); } if (typeof value === 'object' && value !== null) { return parseModelTuple(response, value); } return value; }; } ================================================ FILE: packages/react-client/src/ReactFlightClientConfigBrowser.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ export type StringDecoder = TextDecoder; export function createStringDecoder(): StringDecoder { return new TextDecoder(); } const decoderOptions = { stream: true }; export function readPartialStringChunk( decoder: StringDecoder, buffer: Uint8Array, ): string { return decoder.decode(buffer, decoderOptions); } export function readFinalStringChunk( decoder: StringDecoder, buffer: Uint8Array, ): string { return decoder.decode(buffer); } ================================================ FILE: packages/react-client/src/ReactFlightClientConfigBundlerWebpack.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import { ImportMetadata } from './ReactFlightImportMetadata'; export type ClientReferenceMetadata = ImportMetadata; ================================================ FILE: packages/react-client/src/ReactFlightImportMetadata.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ // This is the parsed shape of the wire format which is why it is // condensed to only the essentialy information export type ImportMetadata = | [ /* id */ string, /* chunks id/filename pairs, double indexed */ Array, /* name */ string, /* async */ 1, ] | [ /* id */ string, /* chunks id/filename pairs, double indexed */ Array, /* name */ string, ]; export const ID = 0; export const CHUNKS = 1; export const NAME = 2; // export const ASYNC = 3; // This logic is correct because currently only include the 4th tuple member // when the module is async. If that changes we will need to actually assert // the value is true. We don't index into the 4th slot because flow does not // like the potential out of bounds access export function isAsyncImport(metadata: ImportMetadata): boolean { return metadata.length === 4; } ================================================ FILE: packages/react-client/src/ReactFlightServerConfigDOM.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import type { CrossOriginEnum, PreloadImplOptions, PreloadModuleImplOptions, PreinitStyleOptions, PreinitScriptOptions, PreinitModuleScriptOptions, } from './ReactDOMTypes'; // We use zero to represent the absence of an explicit precedence because it is // small, smaller than how we encode undefined, and is unambiguous. We could use // a different tuple structure to encode this instead but this makes the runtime // cost cheaper by eliminating a type checks in more positions. type UnspecifiedPrecedence = 0; // prettier-ignore type TypeMap = { // prefetchDNS(href) 'D': /* href */ string, // preconnect(href, options) 'C': | /* href */ string | [/* href */ string, CrossOriginEnum], // preconnect(href, options) 'L': | [/* href */ string, /* as */ string] | [/* href */ string, /* as */ string, PreloadImplOptions], 'm': | /* href */ string | [/* href */ string, PreloadModuleImplOptions], 'S': | /* href */ string | [/* href */ string, /* precedence */ string] | [/* href */ string, /* precedence */ string | UnspecifiedPrecedence, PreinitStyleOptions], 'X': | /* href */ string | [/* href */ string, PreinitScriptOptions], 'M': | /* href */ string | [/* href */ string, PreinitModuleScriptOptions], } export type HintCode = keyof TypeMap; export type HintModel = TypeMap[T]; export type Hints = Set; ================================================ FILE: packages/react-client/src/ReactSymbols.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ export const REACT_ELEMENT_TYPE: symbol = Symbol.for('react.element'); export const REACT_LAZY_TYPE: symbol = Symbol.for('react.lazy'); export const REACT_POSTPONE_TYPE: symbol = Symbol.for('react.postpone'); ================================================ FILE: packages/react-client/src/ReactTypes.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ // eslint-disable-next-line @typescript-eslint/no-explicit-any type ConsoleTask = any; export type ReactCallSite = [ string, // function name string, // file name TODO: model nested eval locations as nested arrays number, // line number number, // column number ]; export type ReactStackTrace = Array; export type ReactComponentInfo = { name?: string; env?: string; owner?: null | ReactComponentInfo; stack?: null | string; task?: null | ConsoleTask; }; export type ReactEnvironmentInfo = { env: string; }; export type ReactErrorInfoProd = { digest: string; }; export type ReactErrorInfoDev = { digest?: string; name: string; message: string; stack: ReactStackTrace; env: string; }; export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev; export type ReactAsyncInfo = { started?: number; completed?: number; stack?: string; }; export type ReactDebugInfo = Array; ================================================ FILE: packages/react-client/src/crossOriginStrings.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ export type CrossOriginString = string; ================================================ FILE: packages/react-client/tsconfig.json ================================================ { "compilerOptions": { "strict": true, "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true }, "exclude": ["node_modules", "dist", "vite.config.ts.timestamp*"] } ================================================ FILE: packages/react-client/turbo.json ================================================ { "$schema": "https://turbo.build/schema.json", "extends": ["//"], "tasks": { "build": { "outputs": ["dist/**"], "inputs": ["src/**", "flight.ts", "vite.config.ts", "tsconfig.json"] } } } ================================================ FILE: packages/react-client/vite.config.ts ================================================ // vite.config.js import { resolve } from 'path'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import dts from 'vite-plugin-dts'; export default defineConfig(({ mode }) => ({ plugins: [ react(), dts({ rollupTypes: true, }), ], build: { outDir: './dist', emptyOutDir: false, lib: { entry: resolve(__dirname, 'flight.ts'), fileName: 'flight', formats: ['es'], }, minify: false, sourcemap: mode === 'development' ? 'inline' : false, }, })); ================================================ FILE: packages/storybook/.gitignore ================================================ /node_modules dist storybook-static ================================================ FILE: packages/storybook/.storybook/main.ts ================================================ import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import type { StorybookConfig } from '@storybook/react-vite'; const require = createRequire(import.meta.url); const config: StorybookConfig = { stories: ['../../core/src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], framework: { name: getAbsolutePath('@storybook/react-vite'), options: {}, }, previewHead: (head) => ` ${head} `, }; export default config; function getAbsolutePath(value: string): any { const result = dirname(require.resolve(join(value, 'package.json'))); console.log(result); return result; } ================================================ FILE: packages/storybook/.storybook/preview-body.html ================================================ ================================================ FILE: packages/storybook/.storybook/preview.ts ================================================ import type { Preview } from '@storybook/react-vite'; import '../../core/dist/style.css'; const preview: Preview = { parameters: { actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, }, }; export default preview; ================================================ FILE: packages/storybook/CHANGELOG.md ================================================ # @rsc-parser/storybook ## 1.1.2 ### Patch Changes - c965537: Add "repository" to /embedded package.json ## 1.1.1 ### Patch Changes - e0b935a: Improve console chunk list rendering ## 1.1.0 ### Minor Changes - 6ce957a: Add support for rendering console chunks - 83cbaaf: Link to debug info references ### Patch Changes - 6d9815c: Improve readability when a tab has multiple panels ## 1.0.0 ### Major Changes - 65231fc: Fix dev check in the Chrome extension BREAKING: `createFlightResponse` from @rsc-parser/react-client now takes a `__DEV__` parameter. ### Patch Changes - 44cc2a8: Fix meter styling - 1860787: Fixed a few react runtime errors ## 0.9.2 ### Patch Changes - 5fb463b: Specify "files" for @rsc-parser/react-client ## 0.9.1 ### Patch Changes - 71a3eac: Properly externalize react in the core and embedded packages ## 0.9.0 ### Minor Changes - 0f2a24a: Fix network graph max call stack error ## 0.8.0 ### Minor Changes - 3db1a5f: Don't flip recording state when running triggerReadNextJsScriptTags - 1478826: Add one error boundary per tab panel in RequestDetail ## 0.7.0 ### Minor Changes - 531f06e: Update fork of ReactFlightClient.js - 4bf3887: Make it possible to read Next.js script tags from the Chrome extension ## 0.6.0 ### Minor Changes - 3f5a1c5: Make the "Headers" tab table column 1 min-size smaller, and make the `BottomPanel` default size larger - 22cbbbf: Redesign `RequestTab` again - f8836e5: Make request tab multiline by default ### Patch Changes - 0002b9a: Add flex wrapping to the tabs in `RequestDetail` ## 0.5.1 ### Patch Changes - 1c0b883: Fix `contentScript.ts` bundling to avoid ESM imports - d3228c2: Remove debug console.log statements ## 0.5.0 ### Minor Changes - 44a478e: Create a Timings tab - b07521b: Capture request and response headers - 2aa4133: Refactor fetch patching and events - c937a64: Rename Chrome extension root component from `App` to `RscDevtoolsExtension` - 8ae76c5: Make the `BottomPanel` easier to resize - 56627ae: Improve Headers tab design and add general information - 4096674: Improve the `Raw` tab - 2883dd1: Restructure @rsc-parser/chrome-extension files - e155780: Create an `OverflowButton` for less important actions in `PanelLayout` - a4eca6d: Add text on top of links in `RequestDetailTabNetwork` - 65d0acd: Create `@rsc-parser/react-client` ### Patch Changes - fe3446e: Make it possible to position the `BottomPanel` on the right - 57c8a9b: Refactor `ViewerStreams` internals to allow a single list of tabs - 095e40e: Ensure that `react` and `react-dom` are externalized - a3a202c: Sync the version @rsc-parser/embedded-example with the other packages - 800fced: Decrease the minimum panel sizes in `FlightResponseSelector` ## 0.4.2 ### Patch Changes - 5356856: Only show links for which there are nodes in FlightResponseTabNetwork ## 0.4.1 ### Patch Changes - e129d14: Specify files in @rsc-parser/core ## 0.4.0 ### Minor Changes - 61a3d5a: Expose `unstable_Viewer` ## 0.3.1 ### Patch Changes - 4542777: Fix Chrome Extension bundling - e09465a: Add typecheck command - 4f48e76: Add format command everywher - ed20b59: Remove console logs ## 0.3.0 ### Minor Changes - f313d13: Compile Chrome extension scripts using Vite - ee03219: Move fetch patching code to @rsc-parser/core - 4d0f42e: Added support for server actions ### Patch Changes - 2cd0dd4: Export `unstable_createFlightResponse` - 2836e57: Add a button to read Next.js payload script tags - faa158f: Export `unstable_createFlightResponse` ## 0.2.4 ### Patch Changes - 71e069c: Set a higher z-index for the BottomPanel when when open ## 0.2.3 ### Patch Changes - f181ce8: Scope styles for RscDevtoolsPanel - 39e168a: Stop wrapping