diff --git a/next.config.js b/next.config.js index f8ec196e131..aa5e517eb10 100644 --- a/next.config.js +++ b/next.config.js @@ -19,6 +19,23 @@ const nextConfig = { scrollRestoration: true, reactCompiler: true, }, + async headers() { + return [ + { + source: '/(.*)', + headers: [ + { + key: 'Cross-Origin-Embedder-Policy', + value: 'credentialless', + }, + { + key: 'Cross-Origin-Opener-Policy', + value: 'same-origin', + }, + ], + }, + ]; + }, async rewrites() { return { beforeFiles: [ diff --git a/package.json b/package.json index 359f30d3e21..bd936bffefc 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,7 @@ "scripts": { "analyze": "ANALYZE=true next build", "dev": "next-remote-watch ./src/content", - "prebuild:rsc": "node scripts/buildRscWorker.mjs", - "build": "node scripts/buildRscWorker.mjs && next build && node --experimental-modules ./scripts/downloadFonts.mjs", + "build": "next build && node --experimental-modules ./scripts/downloadFonts.mjs", "lint": "next lint && eslint \"src/content/**/*.md\"", "lint:fix": "next lint --fix && eslint \"src/content/**/*.md\" --fix", "format:source": "prettier --config .prettierrc --write \"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\"", @@ -27,11 +26,11 @@ "test:eslint-local-rules": "yarn --cwd eslint-local-rules test" }, "dependencies": { - "@codesandbox/sandpack-react": "2.13.5", "@docsearch/css": "^3.8.3", "@docsearch/react": "^3.8.3", "@headlessui/react": "^1.7.0", "@radix-ui/react-context-menu": "^2.1.5", + "@webcontainer/react": "^0.0.2", "body-scroll-lock": "^3.1.3", "classnames": "^2.2.6", "debounce": "^1.2.1", @@ -77,7 +76,7 @@ "eslint-plugin-local-rules": "link:eslint-local-rules", "eslint-plugin-react": "7.x", "eslint-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", - "eslint-plugin-react-hooks": "^0.0.0-experimental-fabef7a6b-20221215", + "eslint-plugin-react-hooks": "^5.2.0", "fs-extra": "^9.0.1", "globby": "^11.0.1", "gray-matter": "^4.0.2", diff --git a/scripts/buildRscWorker.mjs b/scripts/buildRscWorker.mjs deleted file mode 100644 index b02cb8f432b..00000000000 --- a/scripts/buildRscWorker.mjs +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import * as esbuild from 'esbuild'; -import fs from 'fs'; -import path from 'path'; -import {fileURLToPath} from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const root = path.resolve(__dirname, '..'); -const sandboxBase = path.resolve( - root, - 'src/components/MDX/Sandpack/sandpack-rsc/sandbox-code/src' -); - -// 1. Bundle the server Worker runtime (React server build + RSDW/server.browser + Sucrase → IIFE) -// Minified because this runs inside a Web Worker (not parsed by Sandpack's Babel). -const workerOutfile = path.resolve(sandboxBase, 'worker-bundle.dist.js'); -await esbuild.build({ - entryPoints: [path.resolve(sandboxBase, 'rsc-server.js')], - bundle: true, - format: 'iife', - platform: 'browser', - conditions: ['react-server', 'browser'], - outfile: workerOutfile, - define: {'process.env.NODE_ENV': '"production"'}, - minify: true, -}); - -// Post-process worker bundle: -// Prepend the webpack shim so __webpack_require__ (used by react-server-dom-webpack) -// is defined before the IIFE evaluates. The shim sets globalThis.__webpack_require__, -// which is accessible as a bare identifier since globalThis IS the Worker's global scope. -let workerCode = fs.readFileSync(workerOutfile, 'utf8'); - -const shimPath = path.resolve(sandboxBase, 'webpack-shim.js'); -const shimCode = fs.readFileSync(shimPath, 'utf8'); -workerCode = shimCode + '\n' + workerCode; - -fs.writeFileSync(workerOutfile, workerCode); diff --git a/src/components/MDX/Sandpack/Console.tsx b/src/components/MDX/Sandpack/Console.tsx index 625b1c365b4..f86cc1345fd 100644 --- a/src/components/MDX/Sandpack/Console.tsx +++ b/src/components/MDX/Sandpack/Console.tsx @@ -12,11 +12,8 @@ import cn from 'classnames'; import {useState, useRef, useEffect} from 'react'; import {IconChevron} from 'components/Icon/IconChevron'; -import { - SandpackCodeViewer, - useSandpack, -} from '@codesandbox/sandpack-react/unstyled'; -import type {SandpackMessageConsoleMethods} from '@codesandbox/sandpack-client'; +import {SandpackCodeViewer, useSandpack} from '@webcontainer/react'; +import type {SandpackMessageConsoleMethods} from '@webcontainer/react'; const getType = ( message: SandpackMessageConsoleMethods diff --git a/src/components/MDX/Sandpack/CustomPreset.tsx b/src/components/MDX/Sandpack/CustomPreset.tsx index 3ff1beae620..6a1bc4254f4 100644 --- a/src/components/MDX/Sandpack/CustomPreset.tsx +++ b/src/components/MDX/Sandpack/CustomPreset.tsx @@ -15,7 +15,7 @@ import { useActiveCode, SandpackCodeEditor, SandpackLayout, -} from '@codesandbox/sandpack-react/unstyled'; +} from '@webcontainer/react'; import cn from 'classnames'; import {IconChevron} from 'components/Icon/IconChevron'; diff --git a/src/components/MDX/Sandpack/DownloadButton.tsx b/src/components/MDX/Sandpack/DownloadButton.tsx index b51627d89ba..b7abd15ff92 100644 --- a/src/components/MDX/Sandpack/DownloadButton.tsx +++ b/src/components/MDX/Sandpack/DownloadButton.tsx @@ -10,7 +10,7 @@ */ import {useSyncExternalStore} from 'react'; -import {useSandpack} from '@codesandbox/sandpack-react/unstyled'; +import {useSandpack} from '@webcontainer/react'; import {IconDownload} from '../../Icon/IconDownload'; import {AppJSPath, StylesCSSPath, SUPPORTED_FILES} from './createFileMap'; export interface DownloadButtonProps {} diff --git a/src/components/MDX/Sandpack/LoadingOverlay.tsx b/src/components/MDX/Sandpack/LoadingOverlay.tsx index 1945f0c6f4f..2b6308f178b 100644 --- a/src/components/MDX/Sandpack/LoadingOverlay.tsx +++ b/src/components/MDX/Sandpack/LoadingOverlay.tsx @@ -7,26 +7,19 @@ import {useState} from 'react'; -import { - LoadingOverlayState, - OpenInCodeSandboxButton, - useSandpack, -} from '@codesandbox/sandpack-react/unstyled'; +import {LoadingOverlayState, useSandpack} from '@webcontainer/react'; import {useEffect} from 'react'; const FADE_ANIMATION_DURATION = 200; export const LoadingOverlay = ({ - clientId, dependenciesLoading, forceLoading, }: { - clientId: string; dependenciesLoading: boolean; forceLoading: boolean; } & React.HTMLAttributes): React.ReactNode | null => { const loadingOverlayState = useLoadingOverlayState( - clientId, dependenciesLoading, forceLoading ); @@ -39,18 +32,11 @@ export const LoadingOverlay = ({ return (
- Unable to establish connection with the sandpack bundler. Make sure - you are online or try again later. If the problem persists, please - report it via{' '} + Unable to start the sandbox. Make sure you are online or try again + later. If the problem persists, please submit an issue on{' '} - email - {' '} - or submit an issue on{' '} - GitHub. @@ -70,9 +56,7 @@ export const LoadingOverlay = ({ opacity: stillLoading ? 1 : 0, transition: `opacity ${FADE_ANIMATION_DURATION}ms ease-out`, }}> -
- {/* @ts-ignore: the OpenInCodeSandboxButton type from '@codesandbox/sandpack-react/unstyled' is incompatible with JSX in React 19 */} - +
@@ -89,7 +73,6 @@ export const LoadingOverlay = ({ }; const useLoadingOverlayState = ( - clientId: string, dependenciesLoading: boolean, forceLoading: boolean ): LoadingOverlayState => { @@ -100,9 +83,6 @@ const useLoadingOverlayState = ( setState('LOADING'); } - /** - * Sandpack listener - */ const sandpackIdle = sandpack.status === 'idle'; useEffect(() => { const unsubscribe = listen((message) => { @@ -111,12 +91,12 @@ const useLoadingOverlayState = ( return prev === 'LOADING' ? 'PRE_FADING' : 'HIDDEN'; }); } - }, clientId); + }); return () => { unsubscribe(); }; - }, [listen, clientId, sandpackIdle]); + }, [listen, sandpackIdle]); /** * Fading transient state diff --git a/src/components/MDX/Sandpack/NavigationBar.tsx b/src/components/MDX/Sandpack/NavigationBar.tsx index 042f4b93a0e..d06e5865cad 100644 --- a/src/components/MDX/Sandpack/NavigationBar.tsx +++ b/src/components/MDX/Sandpack/NavigationBar.tsx @@ -22,7 +22,7 @@ import { FileTabs, useSandpack, useSandpackNavigation, -} from '@codesandbox/sandpack-react/unstyled'; +} from '@webcontainer/react'; import {OpenInCodeSandboxButton} from './OpenInCodeSandboxButton'; import {ReloadButton} from './ReloadButton'; import {ClearButton} from './ClearButton'; @@ -61,9 +61,8 @@ export function NavigationBar({ // By default, show the dropdown because all tabs may not fit. // We don't know whether they'll fit or not until after hydration: const [showDropdown, setShowDropdown] = useState(true); - const {activeFile, setActiveFile, visibleFiles, clients} = sandpack; - const clientId = Object.keys(clients)[0]; - const {refresh} = useSandpackNavigation(clientId); + const {activeFile, setActiveFile, visibleFiles} = sandpack; + const {refresh} = useSandpackNavigation(); const isMultiFile = visibleFiles.length > 1; const hasJustToggledDropdown = useRef(false); @@ -146,7 +145,6 @@ export function NavigationBar({ 'w-[fit-content]', showDropdown ? 'invisible' : '' )}> - {/* @ts-ignore: the FileTabs type from '@codesandbox/sandpack-react/unstyled' is incompatible with JSX in React 19 */}
{/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */} diff --git a/src/components/MDX/Sandpack/OpenInCodeSandboxButton.tsx b/src/components/MDX/Sandpack/OpenInCodeSandboxButton.tsx index 83b968fabf7..35ae3926e7b 100644 --- a/src/components/MDX/Sandpack/OpenInCodeSandboxButton.tsx +++ b/src/components/MDX/Sandpack/OpenInCodeSandboxButton.tsx @@ -9,20 +9,20 @@ * Copyright (c) Facebook, Inc. and its affiliates. */ -import {UnstyledOpenInCodeSandboxButton} from '@codesandbox/sandpack-react/unstyled'; +import {OpenInStackBlitzButton} from '@webcontainer/react'; import {IconNewPage} from '../../Icon/IconNewPage'; export const OpenInCodeSandboxButton = () => { return ( - + title="Open in StackBlitz"> - Fork - + Open in StackBlitz + ); }; diff --git a/src/components/MDX/Sandpack/Preview.tsx b/src/components/MDX/Sandpack/Preview.tsx index ead9341b6e3..b2464fe5103 100644 --- a/src/components/MDX/Sandpack/Preview.tsx +++ b/src/components/MDX/Sandpack/Preview.tsx @@ -11,8 +11,8 @@ // eslint-disable-next-line react-compiler/react-compiler /* eslint-disable react-hooks/exhaustive-deps */ -import {useRef, useState, useEffect, useMemo, useId} from 'react'; -import {useSandpack, SandpackStack} from '@codesandbox/sandpack-react/unstyled'; +import {useRef, useState, useEffect, useMemo} from 'react'; +import {useSandpack, SandpackStack} from '@webcontainer/react'; import cn from 'classnames'; import {ErrorMessage} from './ErrorMessage'; import {SandpackConsole} from './Console'; @@ -50,15 +50,7 @@ export function Preview({ null ); - let {error: rawError, registerBundler, unregisterBundler} = sandpack; - - if ( - rawError && - rawError.message === '_csbRefreshUtils.prelude is not a function' - ) { - // Work around a noisy internal error. - rawError = null; - } + let {error: rawError, previewUrl} = sandpack; // When throwing a new Error in Sandpack - we want to disable the dev error dialog // to show the Error Boundary fallback @@ -92,20 +84,8 @@ export function Preview({ // It changes too fast, causing flicker. const error = useDebounced(rawError); - const clientId = useId(); const iframeRef = useRef(null); - const sandpackIdle = sandpack.status === 'idle'; - - useEffect(function createBundler() { - const iframeElement = iframeRef.current!; - registerBundler(iframeElement, clientId); - - return () => { - unregisterBundler(clientId); - }; - }, []); - useEffect( function bundlerListener() { let timeout: ReturnType; @@ -118,11 +98,6 @@ export function Preview({ setBundlerIsReady(false); } - /** - * The spinner component transition might be longer than - * the bundler loading, so we only show the spinner if - * it takes more than 500s to load the bundler. - */ timeout = setTimeout(() => { setShowLoading(true); }, 500); @@ -131,7 +106,7 @@ export function Preview({ setShowLoading(false); clearTimeout(timeout); } - }, clientId); + }); return () => { clearTimeout(timeout); @@ -140,7 +115,7 @@ export function Preview({ unsubscribe(); }; }, - [sandpackIdle] + [listen] ); // WARNING: @@ -158,7 +133,8 @@ export function Preview({ // - It should work on mobile. // The best way to test it is to actually go through some challenges. - const hideContent = error || !iframeComputedHeight || !bundlerIsReady; + const hideContent = + error || !iframeComputedHeight || !bundlerIsReady || !previewUrl; const iframeWrapperPosition = (): CSSProperties => { if (hideContent) { @@ -185,6 +161,7 @@ export function Preview({