Skip to content

Commit de62f82

Browse files
committed
Recover from errors, parse directives in worker with AST
1 parent f12068a commit de62f82

File tree

3 files changed

+16382
-9366
lines changed

3 files changed

+16382
-9366
lines changed

src/components/MDX/Sandpack/sandpack-rsc/sandbox-code/src/rsc-client.js

Lines changed: 78 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Runs inside the Sandpack iframe. Orchestrates the RSC pipeline:
33
// 1. Creates a Web Worker from pre-bundled server runtime
44
// 2. Receives file updates from parent (RscFileBridge) via postMessage
5-
// 3. Classifies files by directive, sends raw source to Worker
5+
// 3. Sends all user files to Worker for directive detection + compilation
66
// 4. Worker compiles with Sucrase + executes, sends back Flight chunks
77
// 5. Renders the Flight stream result with React
88

@@ -57,6 +57,36 @@ export function initClient() {
5757
root.render(jsx(Root, {initialPromise: initialPromise}));
5858
});
5959

60+
// Error overlay — rendered inside the iframe via DOM so it doesn't
61+
// interfere with Sandpack's parent-frame message protocol.
62+
function showError(message) {
63+
var overlay = document.getElementById('rsc-error-overlay');
64+
if (!overlay) {
65+
overlay = document.createElement('div');
66+
overlay.id = 'rsc-error-overlay';
67+
overlay.style.cssText =
68+
'background:#fff;border:2px solid #c00;border-radius:8px;padding:16px;margin:20px;font-family:sans-serif;';
69+
var heading = document.createElement('h2');
70+
heading.style.cssText = 'color:#c00;margin:0 0 8px;font-size:16px;';
71+
heading.textContent = 'Server Error';
72+
overlay.appendChild(heading);
73+
var pre = document.createElement('pre');
74+
pre.style.cssText =
75+
'margin:0;white-space:pre-wrap;word-break:break-word;font-size:14px;color:#222;';
76+
overlay.appendChild(pre);
77+
rootEl.parentNode.insertBefore(overlay, rootEl);
78+
}
79+
overlay.lastChild.textContent = message;
80+
overlay.style.display = '';
81+
rootEl.style.display = 'none';
82+
}
83+
84+
function clearError() {
85+
var overlay = document.getElementById('rsc-error-overlay');
86+
if (overlay) overlay.style.display = 'none';
87+
rootEl.style.display = '';
88+
}
89+
6090
// Worker message handler
6191
worker.onmessage = function (e) {
6292
var msg = e.data;
@@ -67,15 +97,19 @@ export function initClient() {
6797
pendingFiles = null;
6898
}
6999
} else if (msg.type === 'deploy-result') {
100+
clearError();
70101
// Register compiled client modules in the webpack cache before rendering
71102
if (msg.result && msg.result.compiledClients) {
72-
registerClientModules(msg.result.compiledClients);
103+
registerClientModules(
104+
msg.result.compiledClients,
105+
msg.result.clientEntries || {}
106+
);
73107
}
74108
triggerRender();
75109
} else if (msg.type === 'rsc-chunk') {
76110
handleChunk(msg);
77111
} else if (msg.type === 'rsc-error') {
78-
console.error('RSC Worker error:', msg.error);
112+
showError(msg.error);
79113
}
80114
};
81115

@@ -184,37 +218,20 @@ export function initClient() {
184218

185219
function processFiles(files) {
186220
console.clear();
187-
var serverFiles = {};
188-
var clientManifest = {};
189-
var clientFiles = {};
190-
221+
var userFiles = {};
191222
Object.keys(files).forEach(function (filePath) {
192-
var code = files[filePath];
193-
194-
// Skip non-JS files and infrastructure files
195223
if (!filePath.match(/\.(js|jsx|ts|tsx)$/)) return;
196224
if (filePath.indexOf('node_modules') !== -1) return;
197225
if (filePath === '/src/index.js') return;
198226
if (filePath === '/src/rsc-client.js') return;
199227
if (filePath === '/src/rsc-server.js') return;
200-
201-
// Check for 'use client' directive
202-
if (hasDirective(code, 'use client')) {
203-
clientManifest[filePath] = true;
204-
clientFiles[filePath] = code;
205-
} else {
206-
// Server file — send raw source to Worker for compilation
207-
serverFiles[filePath] = code;
208-
}
228+
if (filePath === '/src/__webpack_shim__.js') return;
229+
userFiles[filePath] = files[filePath];
209230
});
210-
211-
// Send raw server + client files to Worker (Worker compiles with Sucrase)
212231
worker.postMessage({
213232
type: 'deploy',
214233
requestId: ++renderRequestId,
215-
serverFiles: serverFiles,
216-
clientManifest: clientManifest,
217-
clientFiles: clientFiles,
234+
files: userFiles,
218235
});
219236
}
220237

@@ -237,66 +254,64 @@ export function initClient() {
237254

238255
// Evaluate compiled client modules and register them in the webpack cache
239256
// so RSDW client can resolve them via __webpack_require__.
240-
function registerClientModules(compiledClients) {
241-
var moduleIds = Object.keys(compiledClients);
242-
moduleIds.forEach(function (moduleId) {
243-
var code = compiledClients[moduleId];
257+
function registerClientModules(compiledClients, clientEntries) {
258+
// Store all compiled code for lazy evaluation
259+
var allCompiled = compiledClients;
260+
261+
function evaluateModule(moduleId) {
262+
if (globalThis.__webpack_module_cache__[moduleId]) {
263+
var cached = globalThis.__webpack_module_cache__[moduleId];
264+
return cached.exports !== undefined ? cached.exports : cached;
265+
}
266+
var code = allCompiled[moduleId];
267+
if (!code)
268+
throw new Error('Client require: module "' + moduleId + '" not found');
269+
244270
var mod = {exports: {}};
271+
// Register before evaluating to handle circular deps
272+
globalThis.__webpack_module_cache__[moduleId] = {exports: mod.exports};
273+
245274
var clientRequire = function (id) {
246275
if (id === 'react') return React;
247276
if (id === 'react/jsx-runtime') return ReactJSXRuntime;
248277
if (id === 'react/jsx-dev-runtime') return ReactJSXDevRuntime;
249278
if (id.endsWith('.css')) return {};
250279
var resolvedId = id.startsWith('.') ? resolvePath(moduleId, id) : id;
251-
// Try exact match, then with extensions
252280
var candidates = [resolvedId];
253281
var exts = ['.js', '.jsx', '.ts', '.tsx'];
254282
for (var i = 0; i < exts.length; i++) {
255283
candidates.push(resolvedId + exts[i]);
256284
}
257285
for (var j = 0; j < candidates.length; j++) {
258-
var cached = globalThis.__webpack_module_cache__[candidates[j]];
259-
if (cached)
260-
return cached.exports !== undefined ? cached.exports : cached;
286+
if (
287+
allCompiled[candidates[j]] ||
288+
globalThis.__webpack_module_cache__[candidates[j]]
289+
) {
290+
return evaluateModule(candidates[j]);
291+
}
261292
}
262293
throw new Error('Client require: module "' + id + '" not found');
263294
};
295+
264296
try {
265-
new Function(
266-
'module',
267-
'exports',
268-
'require',
269-
'React',
270-
code
271-
)(mod, mod.exports, clientRequire, React);
297+
new Function('module', 'exports', 'require', 'React', code)(
298+
mod,
299+
mod.exports,
300+
clientRequire,
301+
React
302+
);
272303
} catch (err) {
273304
console.error('Error executing client module ' + moduleId + ':', err);
274-
return;
305+
return mod.exports;
275306
}
307+
// Update cache with actual exports (handles non-circular case)
276308
globalThis.__webpack_module_cache__[moduleId] = {exports: mod.exports};
277-
});
278-
}
279-
280-
function hasDirective(code, directive) {
281-
var lines = code.split('\n');
282-
for (var i = 0; i < Math.min(lines.length, 10); i++) {
283-
var line = lines[i].trim();
284-
if (line === '') continue;
285-
if (line.startsWith('//')) continue;
286-
if (line.startsWith('/*')) {
287-
while (i < lines.length && !lines[i].includes('*/')) i++;
288-
continue;
289-
}
290-
if (
291-
line === "'" + directive + "';" ||
292-
line === '"' + directive + '";' ||
293-
line === "'" + directive + "'" ||
294-
line === '"' + directive + '"'
295-
) {
296-
return true;
297-
}
298-
return false;
309+
return mod.exports;
299310
}
300-
return false;
311+
312+
// Eagerly evaluate 'use client' entry points; their imports resolve lazily
313+
Object.keys(clientEntries).forEach(function (moduleId) {
314+
evaluateModule(moduleId);
315+
});
301316
}
302317
}

0 commit comments

Comments
 (0)