Skip to content

MVP: Support Sandpack snippets in TypeScript #5426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
MVP: Support snippets in TypeScript
  • Loading branch information
eps1lon committed Jan 1, 2023
commit 13880e90e5c6adbe9c29ae7ce14c3982aa6ba9ee
41 changes: 41 additions & 0 deletions beta/src/components/MDX/Sandpack/NavigationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
useState,
useEffect,
Fragment,
useContext,
} from 'react';
import cn from 'classnames';
import {
Expand All @@ -19,8 +20,10 @@ import {
import {OpenInCodeSandboxButton} from './OpenInCodeSandboxButton';
import {ResetButton} from './ResetButton';
import {DownloadButton} from './DownloadButton';
import {SnippetTargetLanguageContext} from './SnippetLanguage';
import {IconChevron} from '../../Icon/IconChevron';
import {Listbox} from '@headlessui/react';
import classNames from 'classnames';

export function useEvent(fn: any): any {
const ref = useRef(null);
Expand All @@ -39,6 +42,43 @@ const getFileName = (filePath: string): string => {
return filePath.slice(lastIndexOfSlash + 1);
};

function SnippetTargetLanguageButton(props: any) {
const {children, snippetTargetLanguage} = props;
const {
setSnippetTargetLanguage,
snippetTargetLanguage: currentSnippetTargetLanguage,
} = useContext(SnippetTargetLanguageContext);

const isCurrent = currentSnippetTargetLanguage === snippetTargetLanguage;

return (
<button
aria-pressed={isCurrent ? true : undefined}
className={classNames(
'text-sm text-primary dark:text-primary-dark inline-flex mx-1 border-black',
isCurrent && 'border-b-4'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried aria-pressed:border-b-4 but this didn't work. Either I misunderstood that API or it would require a Tailwind bump which seems excessive.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea this is available with 3.2.0 https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.2.0 may be we can bump the package? its a minor bump only?

)}
onClick={() => setSnippetTargetLanguage(snippetTargetLanguage)}
type="button">
{children}
</button>
);
}

function SnippetTargetLanguageToggle() {
return (
<div role="group" aria-label="Snippet target language">
<SnippetTargetLanguageButton key="js" snippetTargetLanguage="js">
JS
</SnippetTargetLanguageButton>

<SnippetTargetLanguageButton key="ts" snippetTargetLanguage="ts">
TS
</SnippetTargetLanguageButton>
</div>
);
}

export function NavigationBar({providedFiles}: {providedFiles: Array<string>}) {
const {sandpack} = useSandpack();
const containerRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -179,6 +219,7 @@ export function NavigationBar({providedFiles}: {providedFiles: Array<string>}) {
<div
className="px-3 flex items-center justify-end text-right"
translate="yes">
<SnippetTargetLanguageToggle />
<DownloadButton providedFiles={providedFiles} />
<ResetButton onReset={handleReset} />
<OpenInCodeSandboxButton />
Expand Down
17 changes: 14 additions & 3 deletions beta/src/components/MDX/Sandpack/SandpackRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {SandpackLogLevel} from '@codesandbox/sandpack-client';
import {CustomPreset} from './CustomPreset';
import {createFileMap} from './createFileMap';
import {CustomTheme} from './Themes';
import {SnippetTargetLanguageContext} from './SnippetLanguage';

type SandpackProps = {
children: React.ReactNode;
autorun?: boolean;
defaultActiveFile: string;
showDevTools?: boolean;
};

Expand Down Expand Up @@ -67,10 +69,18 @@ ul {
`.trim();

function SandpackRoot(props: SandpackProps) {
let {children, autorun = true, showDevTools = false} = props;
let {
children,
autorun = true,
defaultActiveFile,
showDevTools = false,
} = props;
const [devToolsLoaded, setDevToolsLoaded] = useState(false);
const codeSnippets = Children.toArray(children) as React.ReactElement[];
const files = createFileMap(codeSnippets);
const {snippetTargetLanguage} = React.useContext(
SnippetTargetLanguageContext
);
const files = createFileMap(codeSnippets, snippetTargetLanguage);

files['/styles.css'] = {
code: [sandboxStyle, files['/styles.css']?.code ?? ''].join('\n\n'),
Expand All @@ -80,10 +90,11 @@ function SandpackRoot(props: SandpackProps) {
return (
<div className="sandpack sandpack--playground my-8">
<SandpackProvider
template="react"
template={snippetTargetLanguage === 'ts' ? 'react-ts' : 'react'}
files={files}
theme={CustomTheme}
options={{
activeFile: defaultActiveFile,
autorun,
initMode: 'user-visible',
initModeObserverOptions: {rootMargin: '1400px 0px'},
Expand Down
24 changes: 24 additions & 0 deletions beta/src/components/MDX/Sandpack/SnippetLanguage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {createContext} from 'react';

export type SnippetTargetLanguage = 'js' | 'ts';

export interface SnippetTargetLanguageContextValue {
snippetTargetLanguage: SnippetTargetLanguage;
setSnippetTargetLanguage: (
nextSnippetTargetLanguage: SnippetTargetLanguage
) => void;
}

export const SnippetTargetLanguageContext =
createContext<SnippetTargetLanguageContextValue>({
snippetTargetLanguage: 'ts',
setSnippetTargetLanguage: () => {
throw new TypeError(
`Could not change snippet language since no <SnippetLanguageProvider> was used in this React tree. This is a bug.`
);
},
});

if (process.env.NODE_ENV !== 'production') {
SnippetTargetLanguageContext.displayName = 'SnippetTargetLanguageContext';
}
12 changes: 12 additions & 0 deletions beta/src/components/MDX/Sandpack/createFileMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const createFileMap = (codeSnippets: React.ReactElement[]) => {
} else {
if (props.className === 'language-js') {
filePath = '/App.js';
} else if (props.className === 'language-tsx') {
filePath = '/App.tsx';
} else if (props.className === 'language-css') {
filePath = '/styles.css';
} else {
Expand All @@ -40,6 +42,16 @@ export const createFileMap = (codeSnippets: React.ReactElement[]) => {
`File ${filePath} was defined multiple times. Each file snippet should have a unique path name`
);
}

if (snippetTargetLanguage === 'js' && /\.(mts|ts|tsx)$/.test(filePath)) {
fileHidden = true;
} else if (
snippetTargetLanguage === 'ts' &&
/\.(mjs|js|jsx)$/.test(filePath)
) {
fileHidden = true;
}

result[filePath] = {
code: (props.children || '') as string,
hidden: fileHidden,
Expand Down
34 changes: 28 additions & 6 deletions beta/src/components/MDX/Sandpack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
* Copyright (c) Facebook, Inc. and its affiliates.
*/

import {lazy, memo, Children, Suspense} from 'react';
import {lazy, memo, Children, Suspense, useState, useMemo} from 'react';
import {createFileMap} from './createFileMap';
import {
SnippetTargetLanguage,
SnippetTargetLanguageContext,
SnippetTargetLanguageContextValue,
} from './SnippetLanguage';

const SandpackRoot = lazy(() => import('./SandpackRoot'));

Expand Down Expand Up @@ -46,6 +51,16 @@ const SandpackGlimmer = ({code}: {code: string}) => (
);

export default memo(function SandpackWrapper(props: any): any {
const defaultSnippetTargetLanguage = 'ts';
const [snippetTargetLanguage, setSnippetTargetLanguage] =
useState<SnippetTargetLanguage>(defaultSnippetTargetLanguage);
const contextValue = useMemo((): SnippetTargetLanguageContextValue => {
return {
snippetTargetLanguage,
setSnippetTargetLanguage,
};
}, [snippetTargetLanguage]);

const codeSnippets = Children.toArray(props.children) as React.ReactElement[];
const files = createFileMap(codeSnippets);

Expand All @@ -55,16 +70,23 @@ export default memo(function SandpackWrapper(props: any): any {
(fileName) =>
files[fileName]?.active === true && files[fileName]?.hidden === false
);
let activeCode;
let defaultActiveCodeSnippet;
if (!activeCodeSnippet.length) {
activeCode = files['/App.js'].code;
if (snippetTargetLanguage === 'ts' && '/App.tsx' in files) {
defaultActiveCodeSnippet = files['/App.tsx'];
} else {
defaultActiveCodeSnippet = files['/App.js'];
}
} else {
activeCode = files[activeCodeSnippet[0]].code;
defaultActiveCodeSnippet = files[activeCodeSnippet[0]];
}

return (
<Suspense fallback={<SandpackGlimmer code={activeCode} />}>
<SandpackRoot {...props} />
<Suspense
fallback={<SandpackGlimmer code={defaultActiveCodeSnippet.code} />}>
<SnippetTargetLanguageContext.Provider value={contextValue}>
<SandpackRoot {...props} />
</SnippetTargetLanguageContext.Provider>
</Suspense>
);
});
24 changes: 24 additions & 0 deletions beta/src/content/learn/keeping-components-pure.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,30 @@ export default function App() {
}
```

```ts App.tsx
function Recipe({ drinkers }: { drinkers: number }) {
return (
<ol>
<li>Boil {drinkers} cups of water.</li>
<li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li>
<li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li>
</ol>
);
}

export default function App() {
return (
<section>
<h1>Spiced Chai Recipe</h1>
<h2>For two</h2>
<Recipe drinkers={2} />
<h2>For a gathering</h2>
<Recipe drinkers={4} />
</section>
);
}
```

</Sandpack>

When you pass `drinkers={2}` to `Recipe`, it will return JSX containing `2 cups of water`. Always.
Expand Down