Skip to content

Commit f773e31

Browse files
authored
fix: Fixes and improvements for HMR and module graph (#597)
## Fix module graph for SSR modules ### Context To support SSR having its own module resolution behaviour (e.g. not using `react-server` import condition or RSC react runtimes) while still existing in the same runtime env as the rest of the worker (where `react-sever` import condition and RSC react runtimes should apply), we have separate `worker` and `ssr` environments, then have an "ssr bridge" concept: in the `worker` environment, for all modules to be evaluated in SSR context, we fetch code from the `ssr` environment for that module. We parse the code we fetch from the SSR environment to find all imports vite does (`__vite_ssr_import__`, `__vite_ssr_dynamic_import__`), and add `import`s for these fetches so that vite will build the module graph correctly. ### The fix There was a bug in the way we parse the vite SSR imports - we weren't matching on cases where there were extra arguments being passed to these functions. As a result, vite wasn't able to construct the module graph correctly. Ultimately, this meant that when some modules were invalidated, their importers should have been invalidated too, but werent (since the link between them in the graph wasn't there). ## Invalidate explicitly for SSR On changes to files in SSR graph, we now invalidate the relevant module and as well as its importers (recursively), then invalidate the same way for the corresponding virtual modules in the worker module graph, as well as its importers (also recursively). ## Better client module invalidation We now: * look at all client updates - rather than only ones that were also worker updates as we did previously; then * invalid all the modules vite tells us where relevant when a file changes - previously we'd pass control on to vite's own HMR invalidation logic for use client modules and css files, and otherwise short circuit, but this turns out to be necessary ## Improvement: Avoid redundant logic for client/server lookups We maintain "lookup" modules for modules with `use client` and `use server` directives, in order to find and import them on demand when RSC needs them. We were handling different variations on how these modules might be resolved (with/without `/@id/`, with/without `.js). We now instead avoid the possibility of there being multiple ways for these to be resolved, so that there's only one request id every specified for each - `virtual:use-client-lookup.js` and `virtual:use-server-lookup.js`) => we were able to remove complexity by only needing to resolve these modules 1 way instead of multiple different ways.
1 parent d8860de commit f773e31

File tree

12 files changed

+81
-51
lines changed

12 files changed

+81
-51
lines changed

pnpm-lock.yaml

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rwsdk",
3-
"version": "0.1.15",
3+
"version": "0.1.15-test.20250714213423",
44
"description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
55
"type": "module",
66
"bin": {

sdk/src/runtime/imports/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const loadModule = memoize(async (id: string) => {
66
return await import(/* @vite-ignore */ id);
77
} else {
88
const { useClientLookup } = await import(
9-
"virtual:use-client-lookup" as string
9+
"virtual:use-client-lookup.js" as string
1010
);
1111

1212
const moduleFn = useClientLookup[id];

sdk/src/runtime/imports/ssr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import memoize from "lodash/memoize";
22

33
export const ssrLoadModule = memoize(async (id: string) => {
44
const { useClientLookup } = await import(
5-
"virtual:use-client-lookup" as string
5+
"virtual:use-client-lookup.js" as string
66
);
77

88
const moduleFn = useClientLookup[id];

sdk/src/runtime/imports/worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ssrWebpackRequire as baseSsrWebpackRequire } from "rwsdk/__ssr_bridge";
44

55
export const loadServerModule = memoize(async (id: string) => {
66
const { useServerLookup } = await import(
7-
"virtual:use-server-lookup" as string
7+
"virtual:use-server-lookup.js" as string
88
);
99

1010
const moduleFn = useServerLookup[id];

sdk/src/runtime/register/ssr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createServerReference as baseCreateServerReference } from "react-server
33

44
export const loadServerModule = memoize(async (id: string) => {
55
const { useServerLookup } = await import(
6-
"virtual:use-server-lookup" as string
6+
"virtual:use-server-lookup.js" as string
77
);
88

99
const moduleFn = useServerLookup[id];

sdk/src/vite/createDirectiveLookupPlugin.mts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ export const createDirectiveLookupPlugin = async ({
271271
build.onResolve(
272272
{
273273
filter: new RegExp(
274-
`^(${escapedVirtualModuleName}|${escapedPrefixedModuleName})$`,
274+
`^(${escapedVirtualModuleName}|${escapedPrefixedModuleName})\.js$`,
275275
),
276276
},
277277
() => {
@@ -281,7 +281,7 @@ export const createDirectiveLookupPlugin = async ({
281281
config.virtualModuleName,
282282
);
283283
return {
284-
path: config.virtualModuleName,
284+
path: `${config.virtualModuleName}.js`,
285285
external: true,
286286
};
287287
},
@@ -318,15 +318,11 @@ export const createDirectiveLookupPlugin = async ({
318318
resolveId(source) {
319319
process.env.VERBOSE && log("Resolving id=%s", source);
320320

321-
if (
322-
source === config.virtualModuleName ||
323-
source === `/@id/${config.virtualModuleName}` ||
324-
source === `/@id/${config.virtualModuleName}.js`
325-
) {
321+
if (source === `${config.virtualModuleName}.js`) {
326322
log("Resolving %s module", config.virtualModuleName);
327323
// context(justinvdm, 16 Jun 2025): Include .js extension
328324
// so it goes through vite processing chain
329-
return `${config.virtualModuleName}.js`;
325+
return source;
330326
}
331327

332328
process.env.VERBOSE && log("No resolution for id=%s", source);

sdk/src/vite/findSsrSpecifiers.mts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ export function findSsrImportSpecifiers(
3838
pattern: `__vite_ssr_dynamic_import__('$SPECIFIER')`,
3939
list: dynamicImports,
4040
},
41+
{
42+
pattern: `__vite_ssr_import__("$SPECIFIER", $$$REST)`,
43+
list: imports,
44+
},
45+
{
46+
pattern: `__vite_ssr_import__('$SPECIFIER', $$$REST)`,
47+
list: imports,
48+
},
49+
{
50+
pattern: `__vite_ssr_dynamic_import__("$SPECIFIER", $$$REST)`,
51+
list: dynamicImports,
52+
},
53+
{
54+
pattern: `__vite_ssr_dynamic_import__('$SPECIFIER', $$$REST)`,
55+
list: dynamicImports,
56+
},
4157
];
4258

4359
for (const { pattern, list } of patterns) {

sdk/src/vite/miniflareHMRPlugin.mts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const miniflareHMRPlugin = (givenOptions: {
6363
} = givenOptions;
6464

6565
if (process.env.VERBOSE) {
66-
this.environment.logger.info(
66+
log(
6767
`Hot update: (env=${
6868
this.environment.name
6969
}) ${ctx.file}\nModule graph:\n\n${dumpFullModuleGraph(
@@ -73,6 +73,21 @@ export const miniflareHMRPlugin = (givenOptions: {
7373
);
7474
}
7575

76+
if (this.environment.name === "ssr") {
77+
log("SSR update, invalidating recursively", ctx.file);
78+
invalidateModule(ctx.server, "ssr", ctx.file, {
79+
invalidateImportersRecursively: true,
80+
});
81+
invalidateModule(
82+
ctx.server,
83+
environment,
84+
VIRTUAL_SSR_PREFIX +
85+
normalizeModulePath(givenOptions.rootDir, ctx.file),
86+
{ invalidateImportersRecursively: true },
87+
);
88+
return [];
89+
}
90+
7691
if (!["client", environment].includes(this.environment.name)) {
7792
return [];
7893
}
@@ -141,37 +156,30 @@ export const miniflareHMRPlugin = (givenOptions: {
141156
ctx.file === entry ||
142157
modules.some((module) => hasEntryAsAncestor(module, entry));
143158

144-
// The worker doesnt need an update
145-
// => Short circuit HMR
146-
if (!isWorkerUpdate) {
147-
return [];
148-
}
149-
150159
// The worker needs an update, but this is the client environment
151160
// => Notify for HMR update of any css files imported by in worker, that are also in the client module graph
152161
// Why: There may have been changes to css classes referenced, which might css modules to change
153162
if (this.environment.name === "client") {
154-
for (const [_, module] of ctx.server.environments[environment]
155-
.moduleGraph.idToModuleMap) {
156-
// todo(justinvdm, 13 Dec 2024): We check+update _all_ css files in worker module graph,
157-
// but it could just be a subset of css files that are actually affected, depending
158-
// on the importers and imports of the changed file. We should be smarter about this.
159-
if (module.file && module.file.endsWith(".css")) {
160-
const clientModules =
161-
ctx.server.environments.client.moduleGraph.getModulesByFile(
162-
module.file,
163-
);
164-
165-
for (const clientModule of clientModules ?? []) {
166-
invalidateModule(ctx.server, "client", clientModule);
163+
if (isWorkerUpdate) {
164+
for (const [_, module] of ctx.server.environments[environment]
165+
.moduleGraph.idToModuleMap) {
166+
// todo(justinvdm, 13 Dec 2024): We check+update _all_ css files in worker module graph,
167+
// but it could just be a subset of css files that are actually affected, depending
168+
// on the importers and imports of the changed file. We should be smarter about this.
169+
if (module.file && module.file.endsWith(".css")) {
170+
const clientModules =
171+
ctx.server.environments.client.moduleGraph.getModulesByFile(
172+
module.file,
173+
);
174+
175+
for (const clientModule of clientModules ?? []) {
176+
invalidateModule(ctx.server, "client", clientModule);
177+
}
167178
}
168179
}
169180
}
170181

171-
// context(justinvdm, 10 Jul 2025): If this isn't a file with a client
172-
// directive or a css file, we shouldn't invalidate anything else to
173-
// avoid full page reload
174-
return hasClientDirective || ctx.file.endsWith(".css") ? undefined : [];
182+
return ctx.modules;
175183
}
176184

177185
// The worker needs an update, and the hot check is for the worker environment

sdk/src/vite/ssrBridgePlugin.mts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,25 +160,35 @@ export const ssrBridgePlugin = ({
160160
log,
161161
);
162162

163-
const allSpecifiers = [...new Set([...imports, ...dynamicImports])];
163+
const allSpecifiers = [
164+
...new Set([...imports, ...dynamicImports]),
165+
].map((id) =>
166+
id.startsWith("/@id/") ? id.slice("/@id/".length) : id,
167+
);
164168

165169
const switchCases = allSpecifiers
166170
.map(
167171
(specifier) =>
168-
` case "${specifier}": return import("${VIRTUAL_SSR_PREFIX}${specifier}");`,
172+
` case "${specifier}": import("${VIRTUAL_SSR_PREFIX}${specifier}");`,
169173
)
170174
.join("\n");
171175

172176
const transformedCode = `
173177
await (async function(__vite_ssr_import__, __vite_ssr_dynamic_import__) {${code}})(
174-
(id, ...args) => {ssrImport(id); return __vite_ssr_import__('/@id/${VIRTUAL_SSR_PREFIX}' + id, ...args);},
175-
(id, ...args) => {ssrImport(id); return __vite_ssr_dynamic_import__('/@id/${VIRTUAL_SSR_PREFIX}' + id, ...args);}
178+
__ssrImport.bind(null, false),
179+
__ssrImport.bind(null, true)
176180
);
177181
178-
function ssrImport(id) {
182+
function __ssrImport(isDynamic, id, ...args) {
183+
id = id.startsWith('/@id/') ? id.slice('/@id/'.length) : id;
184+
179185
switch (id) {
180186
${switchCases}
181187
}
188+
189+
return isDynamic
190+
? __vite_ssr_dynamic_import__("/@id/${VIRTUAL_SSR_PREFIX}" + id, ...args)
191+
: __vite_ssr_import__("/@id/${VIRTUAL_SSR_PREFIX}" + id, ...args);
182192
}
183193
`;
184194

0 commit comments

Comments
 (0)