Skip to content

Commit

Permalink
Merge branch 'main' into rsc-hot-reload
Browse files Browse the repository at this point in the history
  • Loading branch information
dai-shi committed Feb 9, 2024
2 parents 6e663cd + 87f08a2 commit db41db9
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 25 deletions.
4 changes: 2 additions & 2 deletions packages/waku/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import type { ReactNode } from 'react';
import RSDWClient from 'react-server-dom-webpack/client';

import { encodeInput } from './lib/renderers/utils.js';
import { encodeInput, encodeActionId } from './lib/renderers/utils.js';

const { createFromFetch, encodeReply } = RSDWClient;

Expand Down Expand Up @@ -81,7 +81,7 @@ export const fetchRSC = (
const options = {
async callServer(actionId: string, args: unknown[]) {
const response = fetch(
BASE_PATH + encodeInput(encodeURIComponent(actionId)),
BASE_PATH + encodeInput(encodeActionId(actionId)),
{
method: 'POST',
body: await encodeReply(args),
Expand Down
5 changes: 5 additions & 0 deletions packages/waku/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export interface Config {
* Defaults to "assets".
*/
assetsDir?: string;
/**
* The SSR directory relative to distDir.
* Defaults to "ssr".
*/
ssrDir?: string;
/**
* The index.html file for any directories.
* Defaults to "index.html".
Expand Down
111 changes: 90 additions & 21 deletions packages/waku/src/lib/builder/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createReadStream,
createWriteStream,
existsSync,
copyFile,
rename,
mkdir,
readFile,
Expand Down Expand Up @@ -135,6 +136,7 @@ const analyzeEntries = async (entriesFile: string) => {
};
};

// For RSC
const buildServerBundle = async (
rootDir: string,
config: ResolvedConfig,
Expand All @@ -152,13 +154,11 @@ const buildServerBundle = async (
rscTransformPlugin({
isBuild: true,
assetsDir: config.assetsDir,
clientEntryFiles: {
// FIXME this seems very ad-hoc
[WAKU_CLIENT]: decodeFilePathFromAbsolute(
joinPath(fileURLToFilePath(import.meta.url), '../../../client.js'),
),
...clientEntryFiles,
},
wakuClientId: WAKU_CLIENT,
wakuClientPath: decodeFilePathFromAbsolute(
joinPath(fileURLToFilePath(import.meta.url), '../../../client.js'),
),
clientEntryFiles,
serverEntryFiles,
}),
rscEnvPlugin({ config }),
Expand Down Expand Up @@ -224,7 +224,8 @@ const buildServerBundle = async (
if (!('output' in serverBuildOutput)) {
throw new Error('Unexpected vite server build output');
}
const psDir = joinPath(config.publicDir, config.assetsDir);
// TODO If ssr === false, we don't need to write ssr entries.
const ssrAssetsDir = joinPath(config.ssrDir, config.assetsDir);
const code = `
export function loadModule(id) {
switch (id) {
Expand All @@ -234,24 +235,24 @@ ${Object.keys(CLIENT_MODULE_MAP)
.map(
(key) => `
case '${CLIENT_PREFIX}${key}':
return import('./${psDir}/${key}.js');
return import('./${ssrAssetsDir}/${key}.js');
`,
)
.join('')}
case '${psDir}/${WAKU_CLIENT}.js':
return import('./${psDir}/${WAKU_CLIENT}.js');
${Object.entries(serverEntryFiles || {})
case '${ssrAssetsDir}/${WAKU_CLIENT}.js':
return import('./${ssrAssetsDir}/${WAKU_CLIENT}.js');
${Object.entries(clientEntryFiles || {})
.map(
([k]) => `
case '${config.assetsDir}/${k}.js':
return import('./${config.assetsDir}/${k}.js');`,
case '${ssrAssetsDir}/${k}.js':
return import('./${ssrAssetsDir}/${k}.js');`,
)
.join('')}
${Object.entries(clientEntryFiles || {})
${Object.entries(serverEntryFiles || {})
.map(
([k]) => `
case '${psDir}/${k}.js':
return import('./${psDir}/${k}.js');`,
case '${config.assetsDir}/${k}.js':
return import('./${config.assetsDir}/${k}.js');`,
)
.join('')}
default:
Expand All @@ -263,6 +264,67 @@ ${Object.entries(clientEntryFiles || {})
return serverBuildOutput;
};

// For SSR (render client components on server to generate HTML)
const buildSsrBundle = async (
rootDir: string,
config: ResolvedConfig,
commonEntryFiles: Record<string, string>,
clientEntryFiles: Record<string, string>,
serverBuildOutput: Awaited<ReturnType<typeof buildServerBundle>>,
) => {
const mainJsFile = joinPath(rootDir, config.srcDir, config.mainJs);
const cssAssets = serverBuildOutput.output.flatMap(({ type, fileName }) =>
type === 'asset' && fileName.endsWith('.css') ? [fileName] : [],
);
await buildVite({
base: config.basePath,
plugins: [
rscIndexPlugin({ ...config, cssAssets }),
rscEnvPlugin({ config, hydrate: true }),
],
ssr: {
noExternal: /^(?!node:)/,
},
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
publicDir: false,
build: {
ssr: true,
outDir: joinPath(rootDir, config.distDir, config.ssrDir),
rollupOptions: {
onwarn,
input: {
main: mainJsFile,
...CLIENT_MODULE_MAP,
...commonEntryFiles,
...clientEntryFiles,
},
output: {
entryFileNames: (chunkInfo) => {
if (
CLIENT_MODULE_MAP[
chunkInfo.name as keyof typeof CLIENT_MODULE_MAP
] ||
commonEntryFiles[chunkInfo.name] ||
clientEntryFiles[chunkInfo.name]
) {
return config.assetsDir + '/[name].js';
}
return config.assetsDir + '/[name]-[hash].js';
},
},
},
},
});
for (const cssAsset of cssAssets) {
const from = joinPath(rootDir, config.distDir, cssAsset);
const to = joinPath(rootDir, config.distDir, config.ssrDir, cssAsset);
await copyFile(from, to);
}
};

// For Browsers
const buildClientBundle = async (
rootDir: string,
config: ResolvedConfig,
Expand All @@ -288,17 +350,15 @@ const buildClientBundle = async (
onwarn,
input: {
main: mainJsFile,
...CLIENT_MODULE_MAP,
[WAKU_CLIENT]: CLIENT_MODULE_MAP[WAKU_CLIENT],
...commonEntryFiles,
...clientEntryFiles,
},
preserveEntrySignatures: 'exports-only',
output: {
entryFileNames: (chunkInfo) => {
if (
CLIENT_MODULE_MAP[
chunkInfo.name as keyof typeof CLIENT_MODULE_MAP
] ||
[WAKU_CLIENT].includes(chunkInfo.name) ||
commonEntryFiles[chunkInfo.name] ||
clientEntryFiles[chunkInfo.name]
) {
Expand Down Expand Up @@ -567,6 +627,15 @@ export async function build(options: {
(options.deploy === 'netlify-functions' ? 'netlify' : false) ||
(options.deploy === 'aws-lambda' ? 'aws-lambda' : false),
);
if (options.ssr) {
await buildSsrBundle(
rootDir,
config,
commonEntryFiles,
clientEntryFiles,
serverBuildOutput,
);
}
await buildClientBundle(
rootDir,
config,
Expand Down
1 change: 1 addition & 0 deletions packages/waku/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function resolveConfig(config: Config) {
distDir: 'dist',
publicDir: 'public',
assetsDir: 'assets',
ssrDir: 'ssr',
indexHtml: 'index.html',
mainJs: 'main.tsx',
entriesJs: 'entries.js',
Expand Down
5 changes: 5 additions & 0 deletions packages/waku/src/lib/plugins/vite-plugin-rsc-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export function rscTransformPlugin(
| {
isBuild: true;
assetsDir: string;
wakuClientId: string;
wakuClientPath: string;
clientEntryFiles: Record<string, string>;
serverEntryFiles: Record<string, string>;
},
Expand All @@ -17,6 +19,9 @@ export function rscTransformPlugin(
if (!opts.isBuild) {
throw new Error('not buiding');
}
if (id === opts.wakuClientPath) {
return `@id/${opts.assetsDir}/${opts.wakuClientId}.js`;
}
for (const [k, v] of Object.entries(opts.clientEntryFiles)) {
if (v === id) {
return `@id/${opts.assetsDir}/${k}.js`;
Expand Down
2 changes: 1 addition & 1 deletion packages/waku/src/lib/renderers/html-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export const renderHtml = async (
moduleLoading.set(
id,
opts
.loadModule(joinPath(config.publicDir, id))
.loadModule(joinPath(config.ssrDir, id))
.then((m: any) => {
moduleCache.set(id, m);
}),
Expand Down
3 changes: 2 additions & 1 deletion packages/waku/src/lib/renderers/rsc-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../utils/path.js';
import { parseFormData } from '../utils/form.js';
import { streamToString } from '../utils/stream.js';
import { decodeActionId } from '../renderers/utils.js';

export const RSDW_SERVER_MODULE = 'rsdw-server';
export const RSDW_SERVER_MODULE_VALUE = 'react-server-dom-webpack/server.edge';
Expand Down Expand Up @@ -110,7 +111,7 @@ export async function renderRsc(
);

if (method === 'POST') {
const rsfId = decodeURIComponent(input);
const rsfId = decodeActionId(input);
let args: unknown[] = [];
let bodyStr = '';
if (body) {
Expand Down
13 changes: 13 additions & 0 deletions packages/waku/src/lib/renderers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ export const decodeInput = (encodedInput: string) => {
throw err;
};

export const encodeActionId = (actionId: string) => {
const [file, name] = actionId.split('#') as [string, string];
if (name.includes('/')) {
throw new Error('Unsupported action name');
}
return '_' + file + '/' + name;
};

export const decodeActionId = (encoded: string) => {
const index = encoded.lastIndexOf('/');
return encoded.slice(1, index) + '#' + encoded.slice(index + 1);
};

export const hasStatusCode = (x: unknown): x is { statusCode: number } =>
typeof (x as any)?.statusCode === 'number';

Expand Down
3 changes: 3 additions & 0 deletions packages/waku/src/lib/utils/node-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const createWriteStream = (filePath: string) =>
export const existsSync = (filePath: string) =>
fs.existsSync(filePathToOsPath(filePath));

export const copyFile = (filePath1: string, filePath2: string) =>
fsPromises.copyFile(filePathToOsPath(filePath1), filePathToOsPath(filePath2));

export const rename = (filePath1: string, filePath2: string) =>
fsPromises.rename(filePathToOsPath(filePath1), filePathToOsPath(filePath2));

Expand Down

0 comments on commit db41db9

Please sign in to comment.