Skip to content

Commit

Permalink
feat(react-server): obfuscate reference id on production (#201)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Mar 17, 2024
1 parent 872c044 commit 99896cf
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 47 deletions.
2 changes: 1 addition & 1 deletion packages/react-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hiogawa/react-server",
"version": "0.1.0-pre.2",
"version": "0.1.0-pre.4",
"license": "MIT",
"type": "module",
"exports": {
Expand Down
76 changes: 50 additions & 26 deletions packages/react-server/src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import nodeCrypto from "node:crypto";
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
Expand All @@ -20,9 +21,9 @@ import { USE_CLIENT_RE, USE_SERVER_RE, getExportNames } from "./ast-utils";

const require = createRequire(import.meta.url);

// convenient singleton to track file ids to decide RSC hot reload
class RscManager {
parentServer: ViteDevServer | undefined;
// convenient singleton to share states
class ReactServerManager {
buildType?: "rsc" | "client" | "ssr";

// expose "use client" node modules to client via virtual modules
// to avoid dual package due to deps optimization hash during dev
Expand Down Expand Up @@ -54,10 +55,10 @@ export function vitePluginReactServer(options?: {
plugins?: PluginOption[];
}): Plugin[] {
const rscEntry = options?.entry ?? "@hiogawa/react-server/entry-react-server";
const manager = new ReactServerManager();
let parentServer: ViteDevServer | undefined;
let parentEnv: ConfigEnv;
let rscDevServer: ViteDevServer | undefined;
let manager = new RscManager();

const rscConfig: InlineConfig = {
// TODO: custom logger to distinct two server logs easily?
Expand Down Expand Up @@ -90,7 +91,7 @@ export function vitePluginReactServer(options?: {
},
plugins: [
// expose server reference for RSC itself
vitePluginServerUseServer(),
vitePluginServerUseServer({ manager }),

// transform "use client" into client referecnes
vitePluginServerUseClient({ manager }),
Expand Down Expand Up @@ -128,7 +129,8 @@ export function vitePluginReactServer(options?: {
if (id === "\0virtual:rsc-use-server") {
let result = `export default {\n`;
for (const id of manager.rscUseServerIds) {
result += `"${id}": () => import("${id}"),\n`;
let key = manager.buildType ? hashString(id) : id;
result += `"${key}": () => import("${id}"),\n`;
}
result += "};\n";
return result;
Expand Down Expand Up @@ -178,7 +180,6 @@ export function vitePluginReactServer(options?: {
},
async configureServer(server) {
parentServer = server;
manager.parentServer = server;
},
async buildStart(_options) {
if (parentEnv.command === "serve") {
Expand All @@ -190,8 +191,14 @@ export function vitePluginReactServer(options?: {
__rscEntry: rscEntry,
});
}
if (parentEnv.command === "build" && !parentEnv.isSsrBuild) {
await build(rscConfig);
if (parentEnv.command === "build") {
if (parentEnv.isSsrBuild) {
manager.buildType = "ssr";
} else {
manager.buildType = "rsc";
await build(rscConfig);
manager.buildType = "client";
}
}
},
async buildEnd(_options) {
Expand Down Expand Up @@ -274,20 +281,20 @@ export function vitePluginReactServer(options?: {
}

/*
transform "use client" directive
transform "use client" directive on react server code
[input]
"use client"
export function Counter() {}
[output (rsc)]
[output]
import { createClientReference } from "/src/runtime/rsc"
export const Counter = createClientReference("<id>::Counter");
*/
function vitePluginServerUseClient({
manager,
}: {
manager: RscManager;
manager: ReactServerManager;
}): PluginOption {
// intercept Vite's node resolve to virtualize "use client" in node_modules
const pluginUseClientNodeModules: Plugin = {
Expand Down Expand Up @@ -373,8 +380,11 @@ function vitePluginServerUseClient({
manager.rscUseClientIds.add(id);
// normalize client reference during dev
// to align with Vite's import analysis
if (manager.parentServer) {
if (!manager.buildType) {
id = noramlizeClientReferenceId(id);
} else {
// obfuscate reference
id = hashString(id);
}
// TODO:
// "@hiogawa/react-server/client" needs to self-reference
Expand Down Expand Up @@ -403,7 +413,6 @@ function vitePluginServerUseClient({
/**
* emit client-references as dynamic import map
* TODO: re-export only used exports via virtual modules?
* TODO: obfuscate "id" for production?
*
* export default {
* "some-file1": () => import("some-file1"),
Expand All @@ -415,6 +424,9 @@ function vitePluginServerUseClient({
for (let id of manager.rscUseClientIds) {
// virtual module needs to be mapped back to the original form
const to = id.startsWith("\0") ? id.slice(1) : id;
if (manager.buildType) {
id = hashString(id);
}
result += `"${id}": () => import("${to}"),\n`;
}
result += "};\n";
Expand Down Expand Up @@ -448,7 +460,6 @@ function noramlizeClientReferenceId(id: string) {

/*
transform "use server" directive
TODO: include all "use server" files for rsc build
[input]
"use server"
Expand All @@ -461,15 +472,10 @@ export const hello = createServerReference("<id>::hello");
function vitePluginClientUseServer({
manager,
}: {
manager: RscManager;
manager: ReactServerManager;
}): Plugin {
let configEnv: ConfigEnv;

return {
name: vitePluginClientUseServer.name,
config(_config, env) {
configEnv = env;
},
async transform(code, id, _options) {
if (!code.match(USE_SERVER_RE)) {
return;
Expand All @@ -480,15 +486,17 @@ function vitePluginClientUseServer({
id,
exportNames,
});
// TODO
// only rsc build and client build are built in the same process
// so we cannot validate in ssr build
if (configEnv.command === "build" && !configEnv.isSsrBuild) {
// validate server reference used by client is properly generated in rsc build
if (manager.buildType === "client") {
tinyassert(
manager.rscUseServerIds.has(id),
`missing server references in RSC build: ${id}`,
);
}
// obfuscate reference
if (manager.buildType) {
id = hashString(id);
}
let result = `import { createServerReference } from "${require.resolve(
"@hiogawa/react-server/client-internal",
)}";\n`;
Expand All @@ -505,7 +513,11 @@ function vitePluginClientUseServer({
};
}

function vitePluginServerUseServer(): Plugin {
function vitePluginServerUseServer({
manager,
}: {
manager: ReactServerManager;
}): Plugin {
return {
name: vitePluginServerUseClient.name,
async transform(code, id, _options) {
Expand All @@ -525,6 +537,10 @@ function vitePluginServerUseServer(): Plugin {
"@hiogawa/react-server/server-internal",
)}";\n`,
);
// obfuscate reference
if (manager.buildType) {
id = hashString(id);
}
for (const name of exportNames) {
mcode.append(
`${name} = createServerReference("${id}::${name}", ${name});\n`,
Expand All @@ -537,3 +553,11 @@ function vitePluginServerUseServer(): Plugin {
},
};
}

function hashString(v: string) {
return nodeCrypto
.createHash("sha256")
.update(v)
.digest()
.toString("base64url");
}
22 changes: 2 additions & 20 deletions pnpm-lock.yaml

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

0 comments on commit 99896cf

Please sign in to comment.