Skip to content

Commit 795303e

Browse files
authored
test(rsc): test browser only component (#528)
1 parent feb762f commit 795303e

File tree

5 files changed

+102
-1
lines changed

5 files changed

+102
-1
lines changed

packages/plugin-rsc/e2e/basic.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,4 +952,17 @@ function defineTest(f: Fixture) {
952952
await waitForHydration(page)
953953
expect(errors).toEqual([])
954954
})
955+
956+
test('browser only', async ({ page, browser }) => {
957+
await page.goto(f.url())
958+
await expect(page.getByTestId('test-browser-only')).toHaveText(
959+
'test-browser-only: true',
960+
)
961+
962+
const pageNoJs = await browser.newPage({ javaScriptEnabled: false })
963+
await pageNoJs.goto(f.url())
964+
await expect(pageNoJs.getByTestId('test-browser-only')).toHaveText(
965+
'test-browser-only: loading...',
966+
)
967+
})
955968
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function BrowserDep() {
2+
return <>{String(!!window)}</>
3+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use client'
2+
3+
import * as React from 'react'
4+
5+
const BrowserDep = (
6+
import.meta.env.SSR ? undefined : React.lazy(() => import('./browser-dep'))
7+
)!
8+
9+
export function TestBrowserOnly() {
10+
return (
11+
<div data-testid="test-browser-only">
12+
test-browser-only:{' '}
13+
<BrowserOnly fallback={<>loading...</>}>
14+
<BrowserDep />
15+
</BrowserOnly>
16+
</div>
17+
)
18+
}
19+
20+
function BrowserOnly(props: React.SuspenseProps) {
21+
const hydrated = useHydrated()
22+
if (!hydrated) {
23+
return props.fallback
24+
}
25+
return <React.Suspense {...props} />
26+
}
27+
28+
const noopStore = () => () => {}
29+
30+
const useHydrated = () =>
31+
React.useSyncExternalStore(
32+
noopStore,
33+
() => true,
34+
() => false,
35+
)
36+
37+
/*
38+
If we were to implement this whole logic via hypothetical `browserOnly` helper with transform:
39+
40+
======= input ======
41+
42+
const SomeDep = browserOnly(() => import('./some-dep'))
43+
44+
======= output ======
45+
46+
const __TmpLazy = import.meta.env.SSR ? undefined : React.lazy(() => import('./some-dep'}));
47+
48+
const SomeDep = ({ browserOnlyFallback, ...props }) => {
49+
const hydrated = useHydrated()
50+
if (!hydrated) {
51+
return browserOnlyFallback
52+
}
53+
return (
54+
<React.Suspense fallback={browserOnlyFallback}>
55+
<__TmpLazy {...props} />
56+
</React.Suspense>
57+
)
58+
}
59+
60+
=== helper types ===
61+
62+
declare function browserOnly<T>(fn: () => Promise<{ default: React.ComponentType<T> }>):
63+
React.ComponentType<T & { browserOnlyFallback?: React.ReactNode }>
64+
65+
*/

packages/plugin-rsc/examples/basic/src/routes/root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { TestTailwindServer } from './tailwind/server'
2828
import { TestTemporaryReference } from './temporary-reference/client'
2929
import { TestUseCache } from './use-cache/server'
3030
import { TestHydrationMismatch } from './hydration-mismatch/server'
31+
import { TestBrowserOnly } from './browser-only/client'
3132

3233
export function Root(props: { url: URL }) {
3334
return (
@@ -67,6 +68,7 @@ export function Root(props: { url: URL }) {
6768
<TestServerInClient />
6869
<TestActionStateServer />
6970
<TestModuleInvalidationServer />
71+
<TestBrowserOnly />
7072
<TestUseCache />
7173
</body>
7274
</html>

packages/plugin-rsc/examples/basic/vite.config.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import assert from 'node:assert'
22
import rsc, { transformHoistInlineDirective } from '@vitejs/plugin-rsc'
33
import tailwindcss from '@tailwindcss/vite'
44
import react from '@vitejs/plugin-react'
5-
import { type Plugin, defineConfig, parseAstAsync } from 'vite'
5+
import { type Plugin, defineConfig, normalizePath, parseAstAsync } from 'vite'
66
import inspect from 'vite-plugin-inspect'
7+
import path from 'node:path'
78

89
// log unhandled rejection to debug e2e failures
910
if (!(globalThis as any).__debugHandlerRegisterd) {
@@ -96,6 +97,23 @@ export default defineConfig({
9697
}
9798
},
9899
},
100+
{
101+
name: 'test-browser-only',
102+
writeBundle(_options, bundle) {
103+
const moduleIds = Object.values(bundle).flatMap((c) =>
104+
c.type === 'chunk' ? [...c.moduleIds] : [],
105+
)
106+
const browserId = normalizePath(
107+
path.resolve('src/routes/browser-only/browser-dep.tsx'),
108+
)
109+
if (this.environment.name === 'client') {
110+
assert(moduleIds.includes(browserId))
111+
}
112+
if (this.environment.name === 'ssr') {
113+
assert(!moduleIds.includes(browserId))
114+
}
115+
},
116+
},
99117
{
100118
name: 'cf-build',
101119
enforce: 'post',

0 commit comments

Comments
 (0)