Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions packages/plugin-rsc/e2e/linked-package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import fs from 'node:fs'
import path from 'node:path'
import { expect, test } from '@playwright/test'
import { setupInlineFixture, useFixture } from './fixture'
import { waitForHydration } from './helper'

// "isolated" installs each package's dependencies next to it, like pnpm.
// "hoisted" installs every dependency in the app's node_modules, like npm.
for (const layout of ['isolated', 'hoisted'] as const) {
test.describe(layout, () => {
defineLinkedPackageTests(layout)
})
}

function defineLinkedPackageTests(layout: 'isolated' | 'hoisted') {
const root = `examples/e2e/temp/linked-package-${layout}`
// the node_modules that the dependencies of the package at `dir` go in
const nodeModules = (dir: string) =>
layout === 'hoisted' ? 'node_modules' : path.posix.join(dir, 'node_modules')

test.beforeAll(async () => {
// Dependency and import graph:
//
// app
// `-- @vitejs/test-workspace-ui (linked, "react" in "dependencies")
// |-- @vitejs/test-client-lib ("react" in "peerDependencies")
// `-- @vitejs/test-workspace-nested (linked, "react" in "dependencies")
// `-- @vitejs/test-nested-client-lib ("react" in "peerDependencies")
//
// Linked packages resolve outside node_modules, so Vite treats them as
// source. The client libraries are only reachable through the linked
// packages, and their "use client" directive sits on an internal module.
// Unless they're in "noExternal" and excluded from the server optimizers,
// dev externalizes them, or pre-bundles them and drops the directive when
// dependency discovery is enabled. Builds only externalize dependencies
// that resolve from the app, so they fail in the hoisted layout.
const json = (value: unknown) => JSON.stringify(value, null, 2)
// a React library whose "use client" directive is on an internal module
const clientLibrary = (parent: string, name: string, label: string) => {
const dir = `${nodeModules(parent)}/${name}`
return {
[`${dir}/package.json`]: json({
name,
version: '1.0.0',
type: 'module',
exports: './index.js',
peerDependencies: {
react: '*',
},
}),
[`${dir}/index.js`]: /* js */ `
export { TestClient } from './client.js'
`,
[`${dir}/client.js`]: /* js */ `
'use client'

import React from 'react'

const CountContext = React.createContext(0)

function Count() {
return ${JSON.stringify(label + ': ')} + React.useContext(CountContext)
}

export function TestClient() {
const [count, setCount] = React.useState(0)
return React.createElement(
CountContext.Provider,
{ value: count },
React.createElement(
'button',
{ onClick: () => setCount((value) => value + 1) },
React.createElement(Count),
),
)
}
`,
}
}
await setupInlineFixture({
src: 'examples/starter-extra',
dest: root,
files: {
'package.json': {
edit: (source) => {
const packageJson = JSON.parse(source)
packageJson.dependencies = {
...packageJson.dependencies,
'@vitejs/test-workspace-ui': '0.0.0',
}
return JSON.stringify(packageJson, null, 2) + '\n'
},
},
'src/root.tsx': /* tsx */ `
import { TestWorkspace } from '@vitejs/test-workspace-ui/server'

export function Root() {
return (
<html lang="en">
<body>
<TestWorkspace />
</body>
</html>
)
}
`,
'packages/ui/package.json': json({
name: '@vitejs/test-workspace-ui',
version: '0.0.0',
private: true,
type: 'module',
exports: {
'./server': './server.js',
},
dependencies: {
'@vitejs/test-client-lib': '1.0.0',
'@vitejs/test-workspace-nested': '0.0.0',
react: '*',
},
}),
'packages/ui/server.js': /* js */ `
import { TestClient } from '@vitejs/test-client-lib'
import { TestNested } from '@vitejs/test-workspace-nested/server'
import React from 'react'

export function TestWorkspace() {
return React.createElement(
'div',
null,
React.createElement(TestClient),
React.createElement(TestNested),
)
}
`,
...clientLibrary(
'packages/ui',
'@vitejs/test-client-lib',
'Workspace client',
),
'packages/nested/package.json': json({
name: '@vitejs/test-workspace-nested',
version: '0.0.0',
private: true,
type: 'module',
exports: {
'./server': './server.js',
},
dependencies: {
'@vitejs/test-nested-client-lib': '1.0.0',
react: '*',
},
}),
'packages/nested/server.js': /* js */ `
import { TestClient } from '@vitejs/test-nested-client-lib'
import React from 'react'

export function TestNested() {
return React.createElement(TestClient)
}
`,
...clientLibrary(
'packages/nested',
'@vitejs/test-nested-client-lib',
'Nested workspace client',
),
},
})
// link workspace packages like a package manager would
const link = (target: string, linkPath: string) => {
const absoluteLinkPath = path.resolve(root, linkPath)
fs.mkdirSync(path.dirname(absoluteLinkPath), { recursive: true })
// a junction doesn't need extra privileges on Windows (ignored elsewhere)
fs.symlinkSync(path.resolve(root, target), absoluteLinkPath, 'junction')
}
link('packages/ui', `${nodeModules('.')}/@vitejs/test-workspace-ui`)
link(
'packages/nested',
`${nodeModules('packages/ui')}/@vitejs/test-workspace-nested`,
)
})

function defineTest(f: ReturnType<typeof useFixture>) {
test('client libraries imported by linked packages', async ({ page }) => {
await page.goto(f.url())
await waitForHydration(page)
for (const label of ['Workspace client', 'Nested workspace client']) {
const button = (count: number) =>
page.getByRole('button', { name: `${label}: ${count}`, exact: true })
await button(0).click()
await expect(button(1)).toBeVisible()
}
})
}

for (const mode of ['dev', 'build'] as const) {
test.describe(mode, () => {
defineTest(useFixture({ root, mode }))
})
}

// `@cloudflare/vite-plugin` enables dependency discovery in the `rsc` environment
test.describe('dev-cloudflare', () => {
defineTest(useFixture({ root, mode: 'dev', command: 'pnpm cf:dev' }))
})

test.describe('build-cloudflare', () => {
defineTest(
useFixture({
root,
mode: 'build',
buildCommand: 'pnpm cf:build',
command: 'pnpm cf:preview',
}),
)
})
}
9 changes: 4 additions & 5 deletions packages/plugin-rsc/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import {
normalizePath,
parseAstAsync,
} from 'vite'
import { crawlFrameworkPkgs } from 'vitefu'
import vitePluginRscCore from './core/plugin'
import { cjsModuleRunnerPlugin } from './plugins/cjs'
import { crawlFrameworkPackages } from './plugins/crawl-framework-packages'
import { vitePluginFindSourceMapURL } from './plugins/find-source-map-url'
import {
ensureEnvironmentImportsEntryFallback,
Expand Down Expand Up @@ -511,8 +511,7 @@ export default function vitePluginRsc(
}
// crawl packages with "react" in "peerDependencies" to bundle react deps on server
// see https://github.com/svitejs/vitefu/blob/d8d82fa121e3b2215ba437107093c77bde51b63b/src/index.js#L95-L101
const result = await crawlFrameworkPkgs({
root: process.cwd(),
const frameworkPackages = await crawlFrameworkPackages(process.cwd(), {
isBuild: env.command === 'build',
isFrameworkPkgByJson(pkgJson) {
if ([PKG_NAME, 'react-dom'].includes(pkgJson.name)) {
Expand All @@ -528,13 +527,13 @@ export default function vitePluginRsc(
'server-only',
'client-only',
PKG_NAME,
...result.ssr.noExternal.sort(),
...frameworkPackages.sort(),
]
const optimizeDepsExclude = noExternal.filter(
(pkg) =>
!['react', 'react-dom', 'react-server-dom-webpack'].includes(pkg),
)
hasReactServerDomWebpack = result.ssr.noExternal.includes(
hasReactServerDomWebpack = frameworkPackages.includes(
'react-server-dom-webpack',
)
const reactServerDomPackageName = hasReactServerDomWebpack
Expand Down
Loading
Loading