Some notes on React Server Components (specifically the streaming wire format), Next.js v13+ (specifically __next_f
), and Webpack.
- Figure a better way of extracting the main script URLs from the
index.html
/etc:-
const scriptTags = document.querySelectorAll('html script[src*="_next"]'); const scriptUrls = Array.from(scriptTags).map(tag => tag.src).sort() // console.log(scriptUrls); const joinedUrls = urls.join('\n') console.log(joinedUrls); copy(joinedUrls);
-
const scriptTags = document.querySelectorAll('html script[src*="_next"]'); const scriptUrls = Array.from(scriptTags).map(tag => tag.src).sort() const joinedUrls = urls.join('\n') // const missingIds = window.webpackChunk_N_E.map(([[id]]) => id).filter(id => !joinedUrls.includes(id)).sort() // Note: See below code for how to calculate chunkMappings const missingIds = window.webpackChunk_N_E .map(([[id]]) => id) .filter(id => !Object.hasOwn(chunkMappings, id)) .filter(id => !scriptUrls.some(url => url.includes(`chunks/${id}-`))) .sort() const missingIdModules = window.webpackChunk_N_E.filter(([[id]]) => missingIds.includes(id)) console.log({ missingIds, chunkMappings, scriptUrls }) // If there are only 2 missingIds left, then one of them is probably yhe 'main-app-' chunk, and one is probably a chunk with a hashed name, which might be react or similar.
- This seems to give us ~13 webpack script files at the moment; but
window.webpackChunk_N_E.map(([[id]])=> id).sort()
is giving us ~44 entries.. so need to figure out how to find and load the others statically..
-
- Figure out how Udio handles the files, as it doesn't seem to use a buildManifest, instead seeming to use
self.__next_f
in the main index page- vercel/next.js#42170
- vercel/next.js#42170 (comment)
-
Here's how it works in the App Router. First, the stream returns critical metadata for the page (e.g. generateMetadata). Then, parts of your UI can be incrementally streamed in, helping achieve that fast initial paint. This UI streamed can be progressively enhanced, so if you have forms that call server actions, those will still work even though hydration hasn't completed. At the same time, the JS is streaming in to hydrate the page, as well as React Server Components payload that’s being added to the end of the stream. That's
self.__next_f
. -
self.__next_f
contains the RSC payload, which you can see when you're viewing source.- https://react.dev/reference/rsc/server-components
- https://vercel.com/blog/understanding-react-server-components
- https://vercel.com/templates/next.js/app-directory
-
Next.js App Router Playground
-
- https://vercel.com/templates/next.js/app-directory
- https://github.com/alvarlagerlof/rsc-parser
-
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 requiered 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.
- https://github.com/alvarlagerlof/rsc-parser#extension
-
There is a Chrome Extension than you can add
- https://chromewebstore.google.com/detail/rsc-devtools/jcejahepddjnppkhomnidalpnnnemomn
-
RSC Devtools
-
React Server Components network visualizer
This is a tool that lets you record streaming data from React Server Components to then visualize and explore it. You can see how your components and data are loading and in what order and it lets you travel back in time by dragging the timeline slider.
-
- https://chromewebstore.google.com/detail/rsc-devtools/jcejahepddjnppkhomnidalpnnnemomn
-
- alvarlagerlof/rsc-parser#229
-
Explore turning on recording mode as soon as dev tools panel is shown
-
- alvarlagerlof/rsc-parser#923
-
Improve error message in web version
-
- alvarlagerlof/rsc-parser#924
-
Publish 'raw' package for use in other apps + CLI
- alvarlagerlof/rsc-parser#924 (comment)
-
I think I could expose
createFlightResponse
from@rsc-parser/core
. I'll make a PR.- alvarlagerlof/rsc-parser#946
-
Export
unstable_createFlightResponse
from@rsc-parser/core
-
- alvarlagerlof/rsc-parser#946
-
- alvarlagerlof/rsc-parser#924 (comment)
-
I think that this is the best reference for how to use it:
- Format for the
messages
to pass tocreateFlightResponse
: https://github.com/alvarlagerlof/rsc-parser/blob/0ceab13e635f741fd8765a31ccef26ef2d6c6f43/packages/core/src/components/ViewerPayload.tsx#L51-L66createFlightResponse
: https://github.com/alvarlagerlof/rsc-parser/blob/main/packages/core/src/createFlightResponse.ts#L9processBinaryChunk
: https://github.com/alvarlagerlof/rsc-parser/blob/main/packages/core/src/react/ReactFlightClient.ts#L882parseModelString
: https://github.com/alvarlagerlof/rsc-parser/blob/main/packages/core/src/react/ReactFlightClient.ts#L177
- Format for the
-
-
- https://rsc-parser.vercel.app/
- https://rsc-parser-storybook.vercel.app/
- https://www.alvar.dev/blog/creating-devtools-for-react-server-components
-
- https://github.com/horita-yuya/rscq
-
CLI parser for React Server Component
- https://hrtyy.dev/web/rsc_parser/
-
- https://github.com/matt-kruse/demystifying-rsc
- https://demystifying-rsc.vercel.app/
-
Demystifying React Server Components with NextJS 13 App Router
-
This purpose of this application is to demonstrate the concepts and code of React Server Components in NextJS13 in a way that exposes what is really happening.
-
- https://demystifying-rsc.vercel.app/
-
- vercel/next.js#42170 (comment)
- https://www.reddit.com/r/nextjs/comments/173nx13/what_are_all_the_self_next_fpush_function_calls/
- vercel/next.js#14779
- vercel/next.js#56180 (comment)
-
those scripts are the initialization of the router state on the client. When next navigates from one page to another it does a client side transition (with a fetch to the server to get whatever new markup is required). Unlike in pages router App Router has shared layouts and the client is aware of these layouts and only fetches the part of the page that is not shared. The scripts you see here are the initialization of the client router state so that it can do these navigations. effectively. This includes preserving scroll position when hitting the back button and other aspects of the Router behavior.
-
- There seems to be some code related to
__next_f
here:- https://github.com/vercel/next.js/blob/50f3823d7eec2a9c6b652d5272d824d441c8cf69/packages/next/src/server/send-payload.ts#L81-L114
- This just seems to be sorting the script tags to ensure e-tag generation is deterministic
- https://github.com/vercel/next.js/blob/50f3823d7eec2a9c6b652d5272d824d441c8cf69/packages/next/src/server/app-render/use-flight-response.tsx#L138-L166
- This seems to be the main part of the code that generates/inserts the script tags (including defining what the 0, 1, 2 means)
- https://github.com/vercel/next.js/blob/50f3823d7eec2a9c6b652d5272d824d441c8cf69/packages/next/src/client/app-index.tsx#L115-L118
- This seems to be related to parsing the
__next_f
chunks..
- This seems to be related to parsing the
- https://github.com/vercel/next.js/blob/50f3823d7eec2a9c6b652d5272d824d441c8cf69/packages/next/src/client/app-index.tsx#L54-L74
- This is the callback from the above, that handles the different
__next_f
script chunks
- This is the callback from the above, that handles the different
- https://github.com/vercel/next.js/blob/50f3823d7eec2a9c6b652d5272d824d441c8cf69/packages/next/src/server/send-payload.ts#L81-L114
- The webpack file seems to just refer to a single
.js
file like this (and i'm not even sure if that gets used...):-
(d.u = function (e) { return "static/chunks/" + e + ".302361b2546aebf4.js"; }),
-
-
self.__next_f.map(f => f?.[1]).filter(f => f?.includes('static/'))
-
// const parseJSONFromEntry = entry => { // const jsonPart = entry.substring(entry.indexOf('[') + 1, entry.lastIndexOf(']')); // try { // return JSON.parse(`[${jsonPart}]`); // } catch (e) { // console.error("Failed to parse JSON for entry: ", entry); // return []; // Return an empty array or null as per error handling choice // } // }; // // Get build ID/etc // const buildId = self.__next_f // .map(f => f?.[1]) // .filter(f => f?.includes('buildId')) // .flatMap(f => f.trim().split('\n')) // .flatMap(parseJSONFromEntry) // .map(f => Array.isArray(f) ? f.flat() : f) // .map(f => f?.[3]?.buildId) // .filter(Boolean)?.[0]
-
// Source: https://gist.github.com/0xdevalias/ac465fb2f7e6fded183c2a4273d21e61#react-server-components-nextjs-v13-and-webpack-notes-on-streaming-wire-format-__next_f-etc const parseJSONFromEntry = entry => { const jsonPart = entry.substring(entry.indexOf('[') + 1, entry.lastIndexOf(']')); try { return JSON.parse(`[${jsonPart}]`); } catch (e) { console.error("Failed to parse JSON for entry: ", entry); return []; // Return an empty array or null as per error handling choice } }; // Function to transform dependencies into a simpler, directly accessible format function transformDependencies(dependencies) { return Object.values(dependencies).reduce((acc, currentDeps) => { Object.entries(currentDeps).forEach(([moduleId, path]) => { // If the paths match, skip to the next entry if (acc?.[moduleId] === path) return if (!acc[moduleId]) { // If this module ID has not been encountered yet, initialize it with the current path acc[moduleId] = path; } else if (typeof acc[moduleId] === 'string' && acc[moduleId] !== path) { // If the current path for this module ID is different from the existing one, // and the existing one is a string, transform it into an array containing both paths. const oldPath = acc[moduleId]; acc[moduleId] = [oldPath, path]; } else if (Array.isArray(acc[moduleId]) && !acc[moduleId].includes(path)) { // If the existing entry for this module ID is an array and does not already include the current path, // add the current path to the array. acc[moduleId].push(path); } else { // Log any unhandled cases for further investigation. This could be used to catch any unexpected data structures or duplicates. console.error('Unhandled case', { acc, currentDeps, moduleId, path }); } }); return acc; }, {}); } // Get _next script urls const scriptTags = document.querySelectorAll('html script[src*="_next"]'); const scriptUrls = Array.from(scriptTags).map(tag => tag.src).sort() // console.log(scriptUrls); // Get imports/etc (v3) const moduleDependencies = self.__next_f .map(f => f?.[1]) .filter(f => f?.includes('static/')) .flatMap(f => f.split('\n')) .map(parseJSONFromEntry) .filter(f => Array.isArray(f) ? f.length > 0 : !!f) .map(f => { if (!Array.isArray(f?.[1])) { return f } else { // Convert alternating key/value array to an object const keyValueArray = f[1]; const keyValuePairs = []; for (let i = 0; i < keyValueArray.length; i += 2) { keyValuePairs.push([keyValueArray[i], keyValueArray[i + 1]]); } f[1] = Object.fromEntries(keyValuePairs); return f; } }) .filter(f => Array.isArray(f) && f.length === 3 && typeof f?.[1] === 'object') .reduce((acc, [moduleId, dependencies, _]) => { acc[moduleId] = dependencies; return acc; }, {}); const chunkMappings = transformDependencies(moduleDependencies) const uniqueChunkPaths = Array.from(new Set(Object.values(chunkMappings))).sort() const dynamicChunkUrls = uniqueChunkPaths .map(path => `https://www.udio.com/_next/${path}`) .sort() const chunkUrls = Array.from(new Set([...scriptUrls, ...dynamicChunkUrls])).sort() const chunkUrlsJoined = chunkUrls.join('\n') const buildId = self.__next_f .map(f => f?.[1]) .filter(f => f?.includes('buildId')) .flatMap(f => f.trim().split('\n')) .flatMap(parseJSONFromEntry) .map(f => Array.isArray(f) ? f.flat() : f) .map(f => f?.[3]?.buildId) .filter(Boolean)?.[0] console.log({ scriptUrls, moduleDependencies, chunkMappings, uniqueChunkPaths, dynamicChunkUrls, chunkUrls, buildId, }) console.log(chunkUrlsJoined) copy(chunkUrlsJoined) console.log('Chunk URLs (joined) copied to clipboard')
-
// Get imports/etc (v2) // const parseJSONFromEntry = entry => { // const jsonPart = entry.substring(entry.indexOf('[') + 1, entry.lastIndexOf(']')); // try { // return JSON.parse(`[${jsonPart}]`); // } catch (e) { // console.error("Failed to parse JSON for entry: ", entry); // return []; // Return an empty array or null as per error handling choice // } // }; // // Get imports/etc (v2) // self.__next_f // .map(f => f?.[1]) // .filter(f => f?.includes('static/')) // .flatMap(f => f.split('\n')) // .map(parseJSONFromEntry) // .filter(f => Array.isArray(f) ? f.length > 0 : !!f) // .map(f => { // if (!Array.isArray(f?.[1])) { return f } else { // // Convert alternating key/value array to an object // const keyValueArray = f[1]; // const keyValuePairs = []; // for (let i = 0; i < keyValueArray.length; i += 2) { // keyValuePairs.push([keyValueArray[i], keyValueArray[i + 1]]); // } // f[1] = Object.fromEntries(keyValuePairs); // return f; // } // })
-
// Get imports/etc (v1) // const parseJSONFromEntry = entry => { // const jsonPart = entry.substring(entry.indexOf('[') + 1, entry.lastIndexOf(']')); // try { // return JSON.parse(`[${jsonPart}]`); // } catch (e) { // console.error("Failed to parse JSON for entry: ", entry); // return []; // Return an empty array or null as per error handling choice // } // }; // // Get imports/etc (v1) // self.__next_f // .map(f => f?.[1]) // .filter(f => f?.includes('static/')) // .flatMap(f => f.split('\n')) // .map(parseJSONFromEntry) // .filter(f => Array.isArray(f) ? f.length > 0 : !!f)
- vercel/next.js#42170
- https://github.com/0xdevalias/chatgpt-source-watch : Analyzing the evolution of ChatGPT's codebase through time with curated archives and scripts.
- Deobfuscating / Unminifying Obfuscated Web App Code (0xdevalias gist)
- Fingerprinting Minified JavaScript Libraries / AST Fingerprinting / Source Code Similarity / Etc (0xdevalias gist)
- Reverse Engineering Webpack Apps (0xdevalias gist)
- Reverse Engineered Webpack Tailwind-Styled-Component (0xdevalias gist)
- Bypassing Cloudflare, Akamai, etc (0xdevalias gist)
- Debugging Electron Apps (and related memory issues) (0xdevalias gist)
- devalias' Beeper CSS Hacks (0xdevalias gist)
- Reverse Engineering Golang (0xdevalias' gist)
- Reverse Engineering on macOS (0xdevalias' gist)