Repository navigation
fix(rsc): crawl linked packages for React libraries to keep on the server - #1521
james-elicx wants to merge 4 commits into
Conversation
Also run the linked package e2e test with hoisted dependencies, where builds externalize the client libraries without the linked crawl.
Skip the dependency names of framework packages and the package names of other packages, so a framework package installed under an npm alias is still checked under its own name. Merge names after each crawl so the app's crawl is unchanged.
|
I tested this PR against the repro from cloudflare/vinext#4006: a shadcn monorepo (pnpm) whose Each cell is dev / build, on the Cloudflare target unless noted:
Next.js serves all of these routes in dev and build, with both webpack and Turbopack. The dev |
Description
rsccrawls the app'spackage.jsonwith vitefu'scrawlFrameworkPkgsto find React libraries (packages withreactinpeerDependencies). It adds them toresolve.noExternaland tooptimizeDeps.excludein therscandssrenvironments. vitefu only descends into packages that are themselves framework packages. As a result, React libraries imported only by a linked workspace package that listsreactunderdependenciesare never found. The shadcn monorepo template's@workspace/uipackage has this layout.Linked packages resolve outside
node_modules, so Vite treats them as source. The React libraries they import therefore get the default handling for server dependencies:rscenvironment externalizes the library, so its"use client"module runs as a Server Component:TypeError: Cannot read properties of null (reading 'useState').@cloudflare/vite-plugin, which enables dependency discovery in therscenvironment, the library is pre-bundled. That drops the"use client"directive from its internal modules:TypeError: createContext is not a function.Builds only externalize dependencies that resolve from the app, so they fail the same way when the libraries are hoisted to the app's
node_modules(npm and yarn workspaces). With pnpm's isolated layout, builds bundle them and work. The same packages work when the app depends on them directly.This PR crawls each linked package the app depends on (recursively, through
dependenciesanddevDependencies) as if it were the app, then merges the framework packages it finds. A package counts as linked when its real path is outsidenode_modules, which is the rule Vite uses to treat a dependency as source. Workspace packages,link:andfile:dependencies all qualify, whether or not they'reprivate.Downstream report: cloudflare/vinext#4006 (a shadcn monorepo with Base UI, on Cloudflare).
Alternatives considered
workspaceRoot. vitefu'sisPrivateWorkspacePackageuses this rule for crawlingdevDependencies. It would misslink:/file:dependencies and non-private workspace packages, even though Vite treats them as source too.crawlFrameworkPkgs,findDepPkgJsonPath,findClosestPkgJsonPath).Performance
Each linked package costs one extra
crawlFrameworkPkgscall at config time. Packages checked by earlier crawls, framework packages or not, are skipped throughisFrameworkPkgByName. That cuts the cost by about two thirds when linked packages share dependencies (+21 / +101 / +462 ms without it in the synthetic projects below). vitefu only passes the dependency name toisFrameworkPkgByName. So if linked packages install different versions of a package, only the first version found is checked. The app's own crawl doesn't skip anything, so it finds exactly what it found before.Medians of 41 runs on an Apple Silicon Mac, alternating between the existing crawl and the full one:
In the shadcn monorepo repro on Cloudflare, I compared dev with the newly excluded libraries against the existing pre-bundled behaviour. A page rendering 60
lucide-reacticons, reachable only through the workspace package, showed no measurable difference in cold start, warm start or steady-state requests. Its build output was identical, since the repro is a pnpm workspace.Tests
src/plugins/crawl-framework-packages.test.tscovers linked packages, linked packages of linked packages,devDependencies, dependencies of framework packages reached through a linked package, linked packages that depend on each other, framework packages installed under an npm alias elsewhere, linked packages with an unreadablepackage.json(skipped instead of failing startup), and the existing behaviour for non-linked packages. Seven of the nine cases fail without the change.e2e/linked-package.test.tsruns an app with a linked package and a nested linked package, each importing a client library whose"use client"module is internal. It runs in dev and build, with and without@cloudflare/vite-plugin, for both an isolated (pnpm) and a hoisted (npm) dependency layout. Five of the eight cases fail without the change: both dev variants in each layout, and the hoisted build. The Cloudflare builds pass either way, so those cases guard against regressions.Notes for reviewers
The pre-commit hook runs
oxlint --fixon stagedpackages/*/srcfiles, butoxlint.config.tsignorespackages/plugin-rsc/**. A plugin-rsc-only commit therefore fails with "No files found to lint", so I committed with--no-verifyafteroxfmt --checkpassed.