Skip to content

Commit aae201d

Browse files
feat: migrate sandboxes to WebContainer
1 parent 40ea071 commit aae201d

File tree

97 files changed

+5025
-38606
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+5025
-38606
lines changed

next.config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ const nextConfig = {
1919
scrollRestoration: true,
2020
reactCompiler: true,
2121
},
22+
async headers() {
23+
return [
24+
{
25+
source: '/(.*)',
26+
headers: [
27+
{
28+
key: 'Cross-Origin-Embedder-Policy',
29+
value: 'credentialless',
30+
},
31+
{
32+
key: 'Cross-Origin-Opener-Policy',
33+
value: 'same-origin',
34+
},
35+
],
36+
},
37+
];
38+
},
2239
async rewrites() {
2340
return {
2441
beforeFiles: [

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727
"test:eslint-local-rules": "yarn --cwd eslint-local-rules test"
2828
},
2929
"dependencies": {
30-
"@codesandbox/sandpack-react": "2.13.5",
3130
"@docsearch/css": "^3.8.3",
3231
"@docsearch/react": "^3.8.3",
3332
"@headlessui/react": "^1.7.0",
3433
"@radix-ui/react-context-menu": "^2.1.5",
34+
"@webcontainer/react": "0.0.1",
3535
"body-scroll-lock": "^3.1.3",
3636
"classnames": "^2.2.6",
3737
"debounce": "^1.2.1",
@@ -77,7 +77,7 @@
7777
"eslint-plugin-local-rules": "link:eslint-local-rules",
7878
"eslint-plugin-react": "7.x",
7979
"eslint-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",
80-
"eslint-plugin-react-hooks": "^0.0.0-experimental-fabef7a6b-20221215",
80+
"eslint-plugin-react-hooks": "^5.2.0",
8181
"fs-extra": "^9.0.1",
8282
"globby": "^11.0.1",
8383
"gray-matter": "^4.0.2",

scripts/rename-js-to-jsx.mjs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
import fs from 'fs';
5+
import path from 'path';
6+
import {parse} from '@babel/parser';
7+
import {fileURLToPath} from 'url';
8+
9+
const __filename = fileURLToPath(import.meta.url);
10+
const __dirname = path.dirname(__filename);
11+
const ROOT = path.resolve(__dirname, '..');
12+
const CONTENT_DIR = path.join(ROOT, 'src', 'content');
13+
const DRY_RUN = process.argv.includes('--dry-run');
14+
15+
// ---------------------------------------------------------------------------
16+
// JSX detection via Babel AST
17+
// ---------------------------------------------------------------------------
18+
19+
function containsJSX(code) {
20+
try {
21+
const ast = parse(code, {
22+
sourceType: 'module',
23+
plugins: ['jsx'],
24+
errorRecovery: true,
25+
});
26+
return walkForJSX(ast);
27+
} catch {
28+
return /<[A-Z]|<>|<\/>/.test(code);
29+
}
30+
}
31+
32+
function walkForJSX(node, seen = new WeakSet()) {
33+
if (!node || typeof node !== 'object' || seen.has(node)) return false;
34+
seen.add(node);
35+
if (node.type === 'JSXElement' || node.type === 'JSXFragment') return true;
36+
for (const val of Object.values(node)) {
37+
if (Array.isArray(val)) {
38+
for (const item of val) {
39+
if (item && typeof item === 'object' && walkForJSX(item, seen))
40+
return true;
41+
}
42+
} else if (val && typeof val === 'object') {
43+
if (walkForJSX(val, seen)) return true;
44+
}
45+
}
46+
return false;
47+
}
48+
49+
// ---------------------------------------------------------------------------
50+
// File discovery
51+
// ---------------------------------------------------------------------------
52+
53+
function collectMDX(dir) {
54+
const out = [];
55+
for (const ent of fs.readdirSync(dir, {withFileTypes: true})) {
56+
const full = path.join(dir, ent.name);
57+
if (ent.isDirectory()) out.push(...collectMDX(full));
58+
else if (/\.mdx?$/.test(ent.name)) out.push(full);
59+
}
60+
return out;
61+
}
62+
63+
// ---------------------------------------------------------------------------
64+
// Helpers
65+
// ---------------------------------------------------------------------------
66+
67+
function escapeRe(s) {
68+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69+
}
70+
71+
// ---------------------------------------------------------------------------
72+
// Process one MDX file
73+
// ---------------------------------------------------------------------------
74+
75+
function processFile(filePath) {
76+
const src = fs.readFileSync(filePath, 'utf8');
77+
if (!/<Sandpack|<SandpackRSC/.test(src)) return null;
78+
79+
const lines = src.split('\n');
80+
let changed = false;
81+
const renames = [];
82+
83+
// 1. Find Sandpack / SandpackRSC block ranges
84+
const blocks = [];
85+
let bStart = -1;
86+
let bTag = null;
87+
for (let i = 0; i < lines.length; i++) {
88+
const t = lines[i].trim();
89+
if (bStart === -1) {
90+
const m = t.match(/^<(Sandpack|SandpackRSC)([\s>]|$)/);
91+
if (m) {
92+
bStart = i;
93+
bTag = m[1];
94+
}
95+
} else if (t === `</${bTag}>`) {
96+
blocks.push({start: bStart, end: i});
97+
bStart = -1;
98+
bTag = null;
99+
}
100+
}
101+
102+
// 2. Process each block
103+
for (const block of blocks) {
104+
// 2a. Parse code fences
105+
const fences = [];
106+
let inFence = false;
107+
let fMeta = '',
108+
fLang = '',
109+
fMetaLine = -1,
110+
fCodeStart = -1;
111+
112+
for (let i = block.start + 1; i < block.end; i++) {
113+
if (!inFence) {
114+
const m = lines[i].match(/^```(\w+)\s*(.*)$/);
115+
if (m) {
116+
inFence = true;
117+
fMetaLine = i;
118+
fLang = m[1];
119+
fMeta = m[2].trim();
120+
fCodeStart = i + 1;
121+
}
122+
} else if (lines[i].trim() === '```') {
123+
fences.push({
124+
metaLine: fMetaLine,
125+
codeStart: fCodeStart,
126+
codeEnd: i - 1,
127+
lang: fLang,
128+
meta: fMeta,
129+
});
130+
inFence = false;
131+
}
132+
}
133+
134+
// 2b. Identify .js files containing JSX → build rename map
135+
const renameMap = new Map(); // basename.js → basename.jsx
136+
137+
for (const f of fences) {
138+
if (f.lang !== 'js' && f.lang !== 'jsx') continue;
139+
140+
const tokens = f.meta
141+
.split(/\s+/)
142+
.filter((tok) => tok && !tok.startsWith('{'));
143+
const jsName = tokens.find((tok) => tok.endsWith('.js'));
144+
if (!jsName) continue;
145+
146+
const code =
147+
f.codeStart <= f.codeEnd
148+
? lines.slice(f.codeStart, f.codeEnd + 1).join('\n')
149+
: '';
150+
if (!code.trim()) continue;
151+
152+
if (containsJSX(code)) {
153+
const base = path.basename(jsName);
154+
renameMap.set(base, base.replace(/\.js$/, '.jsx'));
155+
156+
const newName = jsName.replace(/\.js$/, '.jsx');
157+
lines[f.metaLine] = lines[f.metaLine].replace(jsName, newName);
158+
changed = true;
159+
renames.push(`${jsName}${newName}`);
160+
}
161+
}
162+
163+
if (renameMap.size === 0) continue;
164+
165+
// 2c. Update imports referencing renamed files
166+
for (const f of fences) {
167+
if (f.lang !== 'js' && f.lang !== 'jsx') continue;
168+
169+
for (let i = f.codeStart; i <= f.codeEnd; i++) {
170+
let line = lines[i];
171+
for (const [oldBase, newBase] of renameMap) {
172+
const re = new RegExp(
173+
`(?<=/)${escapeRe(oldBase)}(?=['"])`,
174+
'g'
175+
);
176+
const updated = line.replace(re, newBase);
177+
if (updated !== line) {
178+
line = updated;
179+
changed = true;
180+
}
181+
}
182+
lines[i] = line;
183+
}
184+
}
185+
}
186+
187+
if (!changed) return null;
188+
189+
if (!DRY_RUN) {
190+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
191+
}
192+
return renames;
193+
}
194+
195+
// ---------------------------------------------------------------------------
196+
// Main
197+
// ---------------------------------------------------------------------------
198+
199+
console.log(
200+
`${DRY_RUN ? '[DRY RUN] ' : ''}Scanning ${path.relative(ROOT, CONTENT_DIR)} …\n`
201+
);
202+
203+
const files = collectMDX(CONTENT_DIR);
204+
let modCount = 0;
205+
let renameCount = 0;
206+
207+
for (const f of files) {
208+
const result = processFile(f);
209+
if (result) {
210+
modCount++;
211+
renameCount += result.length;
212+
console.log(path.relative(ROOT, f));
213+
for (const r of result) console.log(` ${r}`);
214+
}
215+
}
216+
217+
console.log(
218+
`\n${DRY_RUN ? 'Would modify' : 'Modified'} ${modCount} file(s), ${renameCount} rename(s).`
219+
);

src/components/MDX/Sandpack/Console.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@ import cn from 'classnames';
1212
import {useState, useRef, useEffect} from 'react';
1313
import {IconChevron} from 'components/Icon/IconChevron';
1414

15-
import {
16-
SandpackCodeViewer,
17-
useSandpack,
18-
} from '@codesandbox/sandpack-react/unstyled';
19-
import type {SandpackMessageConsoleMethods} from '@codesandbox/sandpack-client';
15+
import {SandpackCodeViewer, useSandpack} from '@webcontainer/react';
16+
import type {SandpackMessageConsoleMethods} from '@webcontainer/react';
2017

2118
const getType = (
2219
message: SandpackMessageConsoleMethods

src/components/MDX/Sandpack/CustomPreset.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
useActiveCode,
1616
SandpackCodeEditor,
1717
SandpackLayout,
18-
} from '@codesandbox/sandpack-react/unstyled';
18+
} from '@webcontainer/react';
1919
import cn from 'classnames';
2020

2121
import {IconChevron} from 'components/Icon/IconChevron';

src/components/MDX/Sandpack/DownloadButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*/
1111

1212
import {useSyncExternalStore} from 'react';
13-
import {useSandpack} from '@codesandbox/sandpack-react/unstyled';
13+
import {useSandpack} from '@webcontainer/react';
1414
import {IconDownload} from '../../Icon/IconDownload';
1515
import {AppJSPath, StylesCSSPath, SUPPORTED_FILES} from './createFileMap';
1616
export interface DownloadButtonProps {}

src/components/MDX/Sandpack/LoadingOverlay.tsx

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,21 @@ import {useState} from 'react';
99

1010
import {
1111
LoadingOverlayState,
12-
OpenInCodeSandboxButton,
1312
useSandpack,
14-
} from '@codesandbox/sandpack-react/unstyled';
13+
OpenInStackBlitzButton,
14+
} from '@webcontainer/react';
1515
import {useEffect} from 'react';
1616

1717
const FADE_ANIMATION_DURATION = 200;
1818

1919
export const LoadingOverlay = ({
20-
clientId,
2120
dependenciesLoading,
2221
forceLoading,
2322
}: {
24-
clientId: string;
2523
dependenciesLoading: boolean;
2624
forceLoading: boolean;
2725
} & React.HTMLAttributes<HTMLDivElement>): React.ReactNode | null => {
2826
const loadingOverlayState = useLoadingOverlayState(
29-
clientId,
3027
dependenciesLoading,
3128
forceLoading
3229
);
@@ -39,18 +36,11 @@ export const LoadingOverlay = ({
3936
return (
4037
<div className="sp-overlay sp-error">
4138
<div className="sp-error-message">
42-
Unable to establish connection with the sandpack bundler. Make sure
43-
you are online or try again later. If the problem persists, please
44-
report it via{' '}
39+
Unable to start the sandbox. Make sure you are online or try again
40+
later. If the problem persists, please submit an issue on{' '}
4541
<a
4642
className="sp-error-message"
47-
href="mailto:hello@codesandbox.io?subject=Sandpack Timeout Error">
48-
email
49-
</a>{' '}
50-
or submit an issue on{' '}
51-
<a
52-
className="sp-error-message"
53-
href="https://github.com/codesandbox/sandpack/issues"
43+
href="https://github.com/reactjs/react.dev/issues"
5444
rel="noreferrer noopener"
5545
target="_blank">
5646
GitHub.
@@ -70,9 +60,8 @@ export const LoadingOverlay = ({
7060
opacity: stillLoading ? 1 : 0,
7161
transition: `opacity ${FADE_ANIMATION_DURATION}ms ease-out`,
7262
}}>
73-
<div className="sp-cube-wrapper" title="Open in CodeSandbox">
74-
{/* @ts-ignore: the OpenInCodeSandboxButton type from '@codesandbox/sandpack-react/unstyled' is incompatible with JSX in React 19 */}
75-
<OpenInCodeSandboxButton />
63+
<div className="sp-cube-wrapper" title="Open in StackBlitz">
64+
{/* <OpenInStackBlitzButton /> */}
7665
<div className="sp-cube">
7766
<div className="sp-sides">
7867
<div className="top" />
@@ -89,7 +78,6 @@ export const LoadingOverlay = ({
8978
};
9079

9180
const useLoadingOverlayState = (
92-
clientId: string,
9381
dependenciesLoading: boolean,
9482
forceLoading: boolean
9583
): LoadingOverlayState => {
@@ -100,9 +88,6 @@ const useLoadingOverlayState = (
10088
setState('LOADING');
10189
}
10290

103-
/**
104-
* Sandpack listener
105-
*/
10691
const sandpackIdle = sandpack.status === 'idle';
10792
useEffect(() => {
10893
const unsubscribe = listen((message) => {
@@ -111,12 +96,12 @@ const useLoadingOverlayState = (
11196
return prev === 'LOADING' ? 'PRE_FADING' : 'HIDDEN';
11297
});
11398
}
114-
}, clientId);
99+
});
115100

116101
return () => {
117102
unsubscribe();
118103
};
119-
}, [listen, clientId, sandpackIdle]);
104+
}, [listen, sandpackIdle]);
120105

121106
/**
122107
* Fading transient state

0 commit comments

Comments
 (0)