mirror of
https://github.com/reactjs/react.dev.git
synced 2026-02-23 20:23:08 +00:00
[Beta] Sandpack bundler improvements (#5164)
* cache babel assets * Update SandpackRoot.tsx * Update NavigationBar.tsx * Update SandpackRoot.tsx * Update 7 files * Update 6 files * Update 6 files * Update LoadingOverlay.tsx * Update Preview.tsx * Update LoadingOverlay.tsx * Update 4 files * Update beta/src/components/MDX/Sandpack/LoadingOverlay.tsx Co-authored-by: Shanmughapriyan S <priyanshan03@gmail.com> * Update sandpack.css * Update Preview.tsx and SandpackRoot.tsx * Update 3 files * Update ErrorMessage.tsx and Preview.tsx * Update Preview.tsx * Update SandpackRoot.tsx * Update Preview.tsx Co-authored-by: Shanmughapriyan S <priyanshan03@gmail.com>
This commit is contained in:
@@ -10,13 +10,13 @@ interface ErrorType {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export function Error({error}: {error: ErrorType}) {
|
||||
export function ErrorMessage({error, ...props}: {error: ErrorType}) {
|
||||
const {message, title} = error;
|
||||
|
||||
return (
|
||||
<div className={'bg-white border-2 border-red-40 rounded-lg p-6'}>
|
||||
<div className="bg-white border-2 border-red-40 rounded-lg p-6" {...props}>
|
||||
<h2 className="text-red-40 text-xl mb-4">{title || 'Error'}</h2>
|
||||
<pre className="text-secondary whitespace-pre-wrap break-words">
|
||||
<pre className="text-secondary whitespace-pre-wrap break-words leading-tight">
|
||||
{message}
|
||||
</pre>
|
||||
</div>
|
||||
142
beta/src/components/MDX/Sandpack/LoadingOverlay.tsx
Normal file
142
beta/src/components/MDX/Sandpack/LoadingOverlay.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import {useState} from 'react';
|
||||
|
||||
import {
|
||||
LoadingOverlayState,
|
||||
OpenInCodeSandboxButton,
|
||||
useSandpack,
|
||||
} from '@codesandbox/sandpack-react';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
const FADE_ANIMATION_DURATION = 200;
|
||||
|
||||
export const LoadingOverlay = ({
|
||||
clientId,
|
||||
dependenciesLoading,
|
||||
forceLoading,
|
||||
}: {
|
||||
clientId: string;
|
||||
dependenciesLoading: boolean;
|
||||
forceLoading: boolean;
|
||||
} & React.HTMLAttributes<HTMLDivElement>): JSX.Element | null => {
|
||||
const loadingOverlayState = useLoadingOverlayState(
|
||||
clientId,
|
||||
dependenciesLoading,
|
||||
forceLoading
|
||||
);
|
||||
|
||||
if (loadingOverlayState === 'HIDDEN') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loadingOverlayState === 'TIMEOUT') {
|
||||
return (
|
||||
<div className="sp-overlay sp-error">
|
||||
<div className="sp-error-message">
|
||||
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{' '}
|
||||
<a
|
||||
className="sp-error-message"
|
||||
href="mailto:hello@codesandbox.io?subject=Sandpack Timeout Error">
|
||||
email
|
||||
</a>{' '}
|
||||
or submit an issue on{' '}
|
||||
<a
|
||||
className="sp-error-message"
|
||||
href="https://github.com/codesandbox/sandpack/issues"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank">
|
||||
GitHub.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const stillLoading =
|
||||
loadingOverlayState === 'LOADING' || loadingOverlayState === 'PRE_FADING';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sp-overlay sp-loading"
|
||||
style={{
|
||||
opacity: stillLoading ? 1 : 0,
|
||||
transition: `opacity ${FADE_ANIMATION_DURATION}ms ease-out`,
|
||||
}}>
|
||||
<div className="sp-cube-wrapper" title="Open in CodeSandbox">
|
||||
<OpenInCodeSandboxButton />
|
||||
<div className="sp-cube">
|
||||
<div className="sp-sides">
|
||||
<div className="top" />
|
||||
<div className="right" />
|
||||
<div className="bottom" />
|
||||
<div className="left" />
|
||||
<div className="front" />
|
||||
<div className="back" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useLoadingOverlayState = (
|
||||
clientId: string,
|
||||
dependenciesLoading: boolean,
|
||||
forceLoading: boolean
|
||||
): LoadingOverlayState => {
|
||||
const {sandpack, listen} = useSandpack();
|
||||
const [state, setState] = useState<LoadingOverlayState>('HIDDEN');
|
||||
|
||||
if (state !== 'LOADING' && forceLoading) {
|
||||
setState('LOADING');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sandpack listener
|
||||
*/
|
||||
const sandpackIdle = sandpack.status === 'idle';
|
||||
useEffect(() => {
|
||||
const unsubscribe = listen((message) => {
|
||||
if (message.type === 'done') {
|
||||
setState((prev) => {
|
||||
return prev === 'LOADING' ? 'PRE_FADING' : 'HIDDEN';
|
||||
});
|
||||
}
|
||||
}, clientId);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [listen, clientId, sandpackIdle]);
|
||||
|
||||
/**
|
||||
* Fading transient state
|
||||
*/
|
||||
useEffect(() => {
|
||||
let fadeTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
if (state === 'PRE_FADING' && !dependenciesLoading) {
|
||||
setState('FADING');
|
||||
} else if (state === 'FADING') {
|
||||
fadeTimeout = setTimeout(
|
||||
() => setState('HIDDEN'),
|
||||
FADE_ANIMATION_DURATION
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(fadeTimeout);
|
||||
};
|
||||
}, [state, dependenciesLoading]);
|
||||
|
||||
if (sandpack.status === 'timeout') {
|
||||
return 'TIMEOUT';
|
||||
}
|
||||
|
||||
if (sandpack.status !== 'running') {
|
||||
return 'HIDDEN';
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
@@ -22,7 +22,6 @@ import {DownloadButton} from './DownloadButton';
|
||||
import {IconChevron} from '../../Icon/IconChevron';
|
||||
import {Listbox} from '@headlessui/react';
|
||||
|
||||
// TODO: Replace with real useEvent.
|
||||
export function useEvent(fn: any): any {
|
||||
const ref = useRef(null);
|
||||
useInsertionEffect(() => {
|
||||
@@ -94,9 +93,20 @@ export function NavigationBar({providedFiles}: {providedFiles: Array<string>}) {
|
||||
}, [isMultiFile]);
|
||||
|
||||
const handleReset = () => {
|
||||
if (confirm('Reset all your edits too?')) {
|
||||
/**
|
||||
* resetAllFiles must come first, otherwise
|
||||
* the previous content will appears for a second
|
||||
* when the iframe loads.
|
||||
*
|
||||
* Plus, it should only prompts if there's any file changes
|
||||
*/
|
||||
if (
|
||||
sandpack.editorState === 'dirty' &&
|
||||
confirm('Reset all your edits too?')
|
||||
) {
|
||||
sandpack.resetAllFiles();
|
||||
}
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
|
||||
@@ -3,26 +3,17 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {useRef, useState, useEffect, useMemo} from 'react';
|
||||
import {
|
||||
useSandpack,
|
||||
LoadingOverlay,
|
||||
SandpackStack,
|
||||
} from '@codesandbox/sandpack-react';
|
||||
import {useRef, useState, useEffect, useMemo, useId} from 'react';
|
||||
import {useSandpack, SandpackStack} from '@codesandbox/sandpack-react';
|
||||
import cn from 'classnames';
|
||||
import {Error} from './Error';
|
||||
import {ErrorMessage} from './ErrorMessage';
|
||||
import {SandpackConsole} from './Console';
|
||||
import type {LintDiagnostic} from './useSandpackLint';
|
||||
|
||||
/**
|
||||
* TODO: can we use React.useId?
|
||||
*/
|
||||
const generateRandomId = (): string =>
|
||||
Math.floor(Math.random() * 10000).toString();
|
||||
import {CSSProperties} from 'react';
|
||||
import {LoadingOverlay} from './LoadingOverlay';
|
||||
|
||||
type CustomPreviewProps = {
|
||||
className?: string;
|
||||
customStyle?: Record<string, unknown>;
|
||||
isExpanded: boolean;
|
||||
lintErrors: LintDiagnostic;
|
||||
};
|
||||
@@ -40,13 +31,13 @@ function useDebounced(value: any): any {
|
||||
}
|
||||
|
||||
export function Preview({
|
||||
customStyle,
|
||||
isExpanded,
|
||||
className,
|
||||
lintErrors,
|
||||
}: CustomPreviewProps) {
|
||||
const {sandpack, listen} = useSandpack();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [bundlerIsReady, setBundlerIsReady] = useState(false);
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
const [iframeComputedHeight, setComputedAutoHeight] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
@@ -95,7 +86,7 @@ export function Preview({
|
||||
// It changes too fast, causing flicker.
|
||||
const error = useDebounced(rawError);
|
||||
|
||||
const clientId = useRef<string>(generateRandomId());
|
||||
const clientId = useId();
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
|
||||
// SandpackPreview immediately registers the custom screens/components so the bundler does not render any of them
|
||||
@@ -104,46 +95,54 @@ export function Preview({
|
||||
errorScreenRegisteredRef.current = true;
|
||||
loadingScreenRegisteredRef.current = true;
|
||||
|
||||
const sandpackIdle = sandpack.status === 'idle';
|
||||
|
||||
useEffect(function createBundler() {
|
||||
const iframeElement = iframeRef.current!;
|
||||
registerBundler(iframeElement, clientId.current);
|
||||
registerBundler(iframeElement, clientId);
|
||||
|
||||
return () => {
|
||||
unregisterBundler(clientId.current);
|
||||
unregisterBundler(clientId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
function bundlerListener() {
|
||||
const unsubscribe = listen((message: any) => {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const unsubscribe = listen((message) => {
|
||||
if (message.type === 'resize') {
|
||||
setComputedAutoHeight(message.height);
|
||||
} else if (message.type === 'start') {
|
||||
if (message.firstLoad) {
|
||||
setIsReady(false);
|
||||
setBundlerIsReady(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* The spinner component transition might be longer than
|
||||
* the bundler loading, so we only show the spinner if
|
||||
* it takes more than 1s to load the bundler.
|
||||
*/
|
||||
timeout = setTimeout(() => {
|
||||
setShowLoading(true);
|
||||
}, 500);
|
||||
} else if (message.type === 'done') {
|
||||
setIsReady(true);
|
||||
setBundlerIsReady(true);
|
||||
setShowLoading(false);
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}, clientId.current);
|
||||
}, clientId);
|
||||
|
||||
return () => {
|
||||
setIsReady(false);
|
||||
clearTimeout(timeout);
|
||||
setBundlerIsReady(false);
|
||||
setComputedAutoHeight(null);
|
||||
unsubscribe();
|
||||
};
|
||||
},
|
||||
[status === 'idle']
|
||||
[sandpackIdle]
|
||||
);
|
||||
|
||||
const overrideStyle = error
|
||||
? {
|
||||
// Don't collapse errors
|
||||
maxHeight: undefined,
|
||||
}
|
||||
: null;
|
||||
const hideContent = !isReady || error;
|
||||
|
||||
// WARNING:
|
||||
// The layout and styling here is convoluted and really easy to break.
|
||||
// If you make changes to it, you need to test different cases:
|
||||
@@ -159,67 +158,68 @@ 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 iframeWrapperPosition = (): CSSProperties => {
|
||||
if (hideContent) {
|
||||
return {position: 'relative'};
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
return {position: 'sticky', top: '2em'};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
return (
|
||||
<SandpackStack
|
||||
className={className}
|
||||
style={{
|
||||
// TODO: clean up this mess.
|
||||
...customStyle,
|
||||
...overrideStyle,
|
||||
}}>
|
||||
<SandpackStack className={className}>
|
||||
<div
|
||||
className={cn(
|
||||
'p-0 sm:p-2 md:p-4 lg:p-8 bg-card dark:bg-wash-dark h-full relative md:rounded-b-lg lg:rounded-b-none',
|
||||
// Allow content to be scrolled if it's too high to fit.
|
||||
// Note we don't want this in the expanded state
|
||||
// because it breaks position: sticky (and isn't needed anyway).
|
||||
!isExpanded && (error || isReady) ? 'overflow-auto' : null
|
||||
!isExpanded && (error || bundlerIsReady) ? 'overflow-auto' : null
|
||||
)}>
|
||||
<div
|
||||
style={{
|
||||
padding: 'initial',
|
||||
position: hideContent
|
||||
? 'relative'
|
||||
: isExpanded
|
||||
? 'sticky'
|
||||
: undefined,
|
||||
top: isExpanded ? '2rem' : undefined,
|
||||
}}>
|
||||
<div style={iframeWrapperPosition()}>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
className={cn(
|
||||
'rounded-t-none bg-white md:shadow-md sm:rounded-lg w-full max-w-full',
|
||||
'rounded-t-none bg-white md:shadow-md sm:rounded-lg w-full max-w-full transition-opacity',
|
||||
// We can't *actually* hide content because that would
|
||||
// break calculating the computed height in the iframe
|
||||
// (which we're using for autosizing). This is noticeable
|
||||
// if you make a compiler error and then fix it with code
|
||||
// that expands the content. You want to measure that.
|
||||
hideContent
|
||||
? 'absolute opacity-0 pointer-events-none'
|
||||
: 'opacity-100'
|
||||
? 'absolute opacity-0 pointer-events-none duration-75'
|
||||
: 'opacity-100 duration-150'
|
||||
)}
|
||||
title="Sandbox Preview"
|
||||
style={{
|
||||
height: iframeComputedHeight || '100%',
|
||||
height: iframeComputedHeight || '15px',
|
||||
zIndex: isExpanded ? 'initial' : -1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className={cn(
|
||||
'p-2',
|
||||
'z-50',
|
||||
// This isn't absolutely positioned so that
|
||||
// the errors can also expand the parent height.
|
||||
isExpanded ? 'sticky top-8' : null
|
||||
isExpanded ? 'sticky top-8 ' : null
|
||||
)}>
|
||||
<Error error={error} />
|
||||
<ErrorMessage error={error} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LoadingOverlay
|
||||
showOpenInCodeSandbox
|
||||
clientId={clientId.current}
|
||||
loading={!isReady && iframeComputedHeight === null}
|
||||
clientId={clientId}
|
||||
dependenciesLoading={!bundlerIsReady && iframeComputedHeight === null}
|
||||
forceLoading={showLoading}
|
||||
/>
|
||||
</div>
|
||||
<SandpackConsole visible={!error} />
|
||||
|
||||
@@ -87,7 +87,8 @@ function SandpackRoot(props: SandpackProps) {
|
||||
autorun,
|
||||
initMode: 'user-visible',
|
||||
initModeObserverOptions: {rootMargin: '1400px 0px'},
|
||||
bundlerURL: 'https://ac83f2d6.sandpack-bundler.pages.dev',
|
||||
bundlerURL:
|
||||
'https://71d9edc6.sandpack-bundler.pages.dev/?babel=minimal',
|
||||
logLevel: SandpackLogLevel.None,
|
||||
}}>
|
||||
<CustomPreset
|
||||
|
||||
@@ -255,6 +255,12 @@ html.dark .sp-wrapper {
|
||||
@apply border-red-40;
|
||||
}
|
||||
|
||||
.sandpack .sp-cm {
|
||||
margin: 0px;
|
||||
outline: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sp-code-editor .sp-cm .cm-scroller {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
Reference in New Issue
Block a user