Skip to content

Commit

Permalink
build/analyze: avoid walking after the client boundary (#532)
Browse files Browse the repository at this point in the history
* build/analyze: avoid walking after the client boundary

* fix: no analyzing after client boundary (#533)

* some removals and type gymnastics

* some removals and type gymnastics

* refactor

* add a test

---------

Co-authored-by: Mohammad Bagher Abiyat <zorofight94@gmail.com>
  • Loading branch information
dai-shi and Aslemammad authored Feb 25, 2024
1 parent c71158f commit ff20bfe
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 19 deletions.
3 changes: 3 additions & 0 deletions e2e/fixtures/ssr-swr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SSR SWR

RSC features with SSR with SWR.
23 changes: 23 additions & 0 deletions e2e/fixtures/ssr-swr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "ssr-swr",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "waku dev --with-ssr",
"build": "waku build --with-ssr",
"start": "waku start --with-ssr"
},
"dependencies": {
"react": "18.3.0-canary-14fd9630e-20240213",
"react-dom": "18.3.0-canary-14fd9630e-20240213",
"react-server-dom-webpack": "18.3.0-canary-14fd9630e-20240213",
"swr": "2.2.5",
"waku": "workspace:*"
},
"devDependencies": {
"@types/react": "18.2.55",
"@types/react-dom": "18.2.19",
"typescript": "5.3.3"
}
}
13 changes: 13 additions & 0 deletions e2e/fixtures/ssr-swr/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Counter } from './Counter.js';

const App = ({ name }: { name: string }) => {
return (
<div style={{ border: '3px red dashed', margin: '1em', padding: '1em' }}>
<title>Waku example</title>
<h1 data-testid="app-name">{name}</h1>
<Counter />
</div>
);
};

export default App;
23 changes: 23 additions & 0 deletions e2e/fixtures/ssr-swr/src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { useState } from 'react';
import useSWR from 'swr';

const fetcher = () => ({ name: 'Waku' });

export const Counter = () => {
const [count, setCount] = useState(0);
const { data } = useSWR('/api/user', fetcher);
return (
<div
data-testid="counter"
style={{ border: '3px blue dashed', margin: '1em', padding: '1em' }}
>
<p data-testid="count">{count}</p>
<button data-testid="increment" onClick={() => setCount((c) => c + 1)}>
Increment
</button>
<p data-testid="swr-data">{JSON.stringify(data)}</p>
</div>
);
};
28 changes: 28 additions & 0 deletions e2e/fixtures/ssr-swr/src/entries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { lazy } from 'react';
import { defineEntries } from 'waku/server';
import { Slot } from 'waku/client';

const App = lazy(() => import('./components/App.js'));

export default defineEntries(
// renderEntries
async (input) => {
return {
App: <App name={input || 'Waku'} />,
};
},
// getBuildConfig
async () => [{ pathname: '/', entries: [{ input: '' }] }],
// getSsrConfig
async (pathname) => {
switch (pathname) {
case '/':
return {
input: '',
body: <Slot id="App" />,
};
default:
return null;
}
},
);
17 changes: 17 additions & 0 deletions e2e/fixtures/ssr-swr/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { StrictMode } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import { Root, Slot } from 'waku/client';

const rootElement = (
<StrictMode>
<Root>
<Slot id="App" />
</Root>
</StrictMode>
);

if (import.meta.env.WAKU_HYDRATE) {
hydrateRoot(document.body, rootElement);
} else {
createRoot(document.body).render(rootElement);
}
17 changes: 17 additions & 0 deletions e2e/fixtures/ssr-swr/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"composite": true,
"strict": true,
"target": "esnext",
"downlevelIteration": true,
"esModuleInterop": true,
"module": "nodenext",
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"types": ["react/experimental"],
"jsx": "react-jsx",
"rootDir": "./src",
"outDir": "./dist"
}
}
2 changes: 1 addition & 1 deletion e2e/fixtures/ssr-target-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"react": "18.3.0-canary-14fd9630e-20240213",
"react-dom": "18.3.0-canary-14fd9630e-20240213",
"react-server-dom-webpack": "18.3.0-canary-14fd9630e-20240213",
"react-textarea-autosize": "^8.5.3",
"react-textarea-autosize": "8.5.3",
"waku": "workspace:*"
},
"devDependencies": {
Expand Down
83 changes: 83 additions & 0 deletions e2e/ssr-swr.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { expect } from '@playwright/test';
import { execSync, exec, ChildProcess } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import waitPort from 'wait-port';
import { debugChildProcess, getFreePort, terminate, test } from './utils.js';
import { rm } from 'node:fs/promises';

const waku = fileURLToPath(
new URL('../packages/waku/dist/cli.js', import.meta.url),
);

const commands = [
{
command: 'dev --with-ssr',
},
{
build: 'build --with-ssr',
command: 'start --with-ssr',
},
];

const cwd = fileURLToPath(new URL('./fixtures/ssr-swr', import.meta.url));

for (const { build, command } of commands) {
test.describe(`ssr-swr: ${command}`, () => {
let cp: ChildProcess;
let port: number;
test.beforeAll('remove cache', async () => {
await rm(`${cwd}/dist`, {
recursive: true,
force: true,
});
});

test.beforeAll(async () => {
if (build) {
execSync(`node ${waku} ${build}`, {
cwd,
});
}
port = await getFreePort();
cp = exec(`node ${waku} ${command}`, {
cwd,
env: {
...process.env,
PORT: `${port}`,
},
});
debugChildProcess(cp, fileURLToPath(import.meta.url));
await waitPort({
port,
});
});

test.afterAll(async () => {
await terminate(cp.pid!);
});

test('increase counter', async ({ page }) => {
await page.goto(`http://localhost:${port}/`);
await expect(page.getByTestId('app-name')).toHaveText('Waku');
await expect(page.getByTestId('count')).toHaveText('0');
await page.getByTestId('increment').click();
await page.getByTestId('increment').click();
await page.getByTestId('increment').click();
await expect(page.getByTestId('count')).toHaveText('3');
});

test('no js environment should have first screen', async ({ browser }) => {
const context = await browser.newContext({
javaScriptEnabled: false,
});
const page = await context.newPage();
await page.goto(`http://localhost:${port}/`);
await expect(page.getByTestId('app-name')).toHaveText('Waku');
await expect(page.getByTestId('count')).toHaveText('0');
await page.getByTestId('increment').click();
await expect(page.getByTestId('count')).toHaveText('0');
await page.close();
await context.close();
});
});
}
29 changes: 12 additions & 17 deletions packages/waku/src/lib/plugins/vite-plugin-rsc-analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,19 @@ import path from 'node:path';
import type { Plugin } from 'vite';
import * as swc from '@swc/core';

// HACK: Is it common to depend on another plugin like this?
import { rscTransformPlugin } from './vite-plugin-rsc-transform.js';

export function rscAnalyzePlugin(
clientFileSet: Set<string>,
serverFileSet: Set<string>,
): Plugin {
const dependencyMap = new Map<string, Set<string>>();
const rscTransform = rscTransformPlugin({ isBuild: false }).transform;
const clientEntryCallback = (id: string) => clientFileSet.add(id);
const serverEntryCallback = (id: string) => serverFileSet.add(id);
const dependencyCallback = (id: string, depId: string) => {
let depSet = dependencyMap.get(id);
if (!depSet) {
depSet = new Set();
dependencyMap.set(id, depSet);
}
depSet.add(depId);
};
return {
name: 'rsc-analyze-plugin',
async transform(code, id) {
async transform(code, id, options) {
const ext = path.extname(id);
if (['.ts', '.tsx', '.js', '.jsx', '.mjs'].includes(ext)) {
const mod = swc.parseSync(code, {
Expand All @@ -37,15 +32,15 @@ export function rscAnalyzePlugin(
serverEntryCallback(id);
}
}
if (item.type === 'ImportDeclaration') {
const resolvedId = await this.resolve(item.source.value, id);
if (resolvedId) {
dependencyCallback(id, resolvedId.id);
}
}
}
}
return code;
// Avoid walking after the client boundary
if (clientFileSet.has(id)) {
// TODO this isn't efficient. let's refactor it in the future.
return (
rscTransform as typeof rscTransform & { handler: undefined }
).call(this, code, id, options);
}
},
};
}
52 changes: 51 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tsconfig.e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
},
{
"path": "./e2e/fixtures/ssr-target-bundle/tsconfig.json"
},
{
"path": "./e2e/fixtures/ssr-swr/tsconfig.json"
}
]
}

0 comments on commit ff20bfe

Please sign in to comment.