Browse Source

[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>
main
Danilo Woznica 2 years ago
committed by GitHub
parent
commit
8a22e6e6ca
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      beta/src/components/MDX/Sandpack/ErrorMessage.tsx
  2. 142
      beta/src/components/MDX/Sandpack/LoadingOverlay.tsx
  3. 14
      beta/src/components/MDX/Sandpack/NavigationBar.tsx
  4. 122
      beta/src/components/MDX/Sandpack/Preview.tsx
  5. 3
      beta/src/components/MDX/Sandpack/SandpackRoot.tsx
  6. 6
      beta/src/styles/sandpack.css

6
beta/src/components/MDX/Sandpack/Error.tsx → beta/src/components/MDX/Sandpack/ErrorMessage.tsx

@ -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

@ -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;
};

14
beta/src/components/MDX/Sandpack/NavigationBar.tsx

@ -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();
};

122
beta/src/components/MDX/Sandpack/Preview.tsx

@ -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} />

3
beta/src/components/MDX/Sandpack/SandpackRoot.tsx

@ -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

6
beta/src/styles/sandpack.css

@ -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;

Loading…
Cancel
Save