From b5fa16c65ade4f2ccd8e36a8bbd7c4e19daf5667 Mon Sep 17 00:00:00 2001 From: Antoine BERNIER Date: Thu, 31 Oct 2024 13:23:27 +0100 Subject: [PATCH] feat: Sandpack[folder] (#361) --- .../authoring-sandpack-cloud/App.tsx | 12 ++++ .../authoring-sandpack-cloud/package.json | 8 +++ .../authoring-sandpack-cloud/styles.css | 6 ++ docs/getting-started/authoring.mdx | 27 +++++++ src/components/mdx/Sandpack/Sandpack.tsx | 70 +++++++++++++++++++ src/components/mdx/Sandpack/index.ts | 1 + src/components/mdx/Sandpack/rehypeSandpack.ts | 49 +++++++++++++ src/components/mdx/index.tsx | 18 +---- src/utils/docs.tsx | 9 ++- tsconfig.json | 2 +- 10 files changed, 181 insertions(+), 21 deletions(-) create mode 100644 docs/getting-started/authoring-sandpack-cloud/App.tsx create mode 100644 docs/getting-started/authoring-sandpack-cloud/package.json create mode 100644 docs/getting-started/authoring-sandpack-cloud/styles.css create mode 100644 src/components/mdx/Sandpack/Sandpack.tsx create mode 100644 src/components/mdx/Sandpack/index.ts create mode 100644 src/components/mdx/Sandpack/rehypeSandpack.ts diff --git a/docs/getting-started/authoring-sandpack-cloud/App.tsx b/docs/getting-started/authoring-sandpack-cloud/App.tsx new file mode 100644 index 00000000..ded40b89 --- /dev/null +++ b/docs/getting-started/authoring-sandpack-cloud/App.tsx @@ -0,0 +1,12 @@ +import { CameraControls, Cloud } from '@react-three/drei' +import { Canvas } from '@react-three/fiber' + +export default function App() { + return ( + + + + + + ) +} diff --git a/docs/getting-started/authoring-sandpack-cloud/package.json b/docs/getting-started/authoring-sandpack-cloud/package.json new file mode 100644 index 00000000..8bd4e880 --- /dev/null +++ b/docs/getting-started/authoring-sandpack-cloud/package.json @@ -0,0 +1,8 @@ +{ + "name": "simple-box", + "dependencies": { + "three": "latest", + "@react-three/fiber": "latest", + "@react-three/drei": "latest" + } +} diff --git a/docs/getting-started/authoring-sandpack-cloud/styles.css b/docs/getting-started/authoring-sandpack-cloud/styles.css new file mode 100644 index 00000000..cc4a8790 --- /dev/null +++ b/docs/getting-started/authoring-sandpack-cloud/styles.css @@ -0,0 +1,6 @@ +html, +body, +#root { + height: 100%; + margin: unset; +} diff --git a/docs/getting-started/authoring.mdx b/docs/getting-started/authoring.mdx index f04598e0..b5412613 100644 --- a/docs/getting-started/authoring.mdx +++ b/docs/getting-started/authoring.mdx @@ -342,7 +342,34 @@ export default function App() { +#### `Sandpack[folder]` +Instead of `files`, a `folder` prop allow you to pass a folder containing all the files: + +```tsx + +``` + +NB: `folder` path is relative to the mdx file. + +> [!TIP] +> It will simply: +> - build the `files` prop for you (including all `.{js|ts|jsx|tsx|css}` it finds) +> - build `customSetup.dependencies` from `package.json` if it exists + +
+ Result + + + + +
### `Codesandbox` diff --git a/src/components/mdx/Sandpack/Sandpack.tsx b/src/components/mdx/Sandpack/Sandpack.tsx new file mode 100644 index 00000000..69e0184a --- /dev/null +++ b/src/components/mdx/Sandpack/Sandpack.tsx @@ -0,0 +1,70 @@ +import cn from '@/lib/cn' +import { crawl } from '@/utils/docs' +import { Sandpack as SP } from '@codesandbox/sandpack-react' +import fs from 'node:fs' +import path from 'node:path' +import { ComponentProps } from 'react' + +function getSandpackDependencies(folder: string) { + const pkgPath = `${folder}/package.json` + if (!fs.existsSync(pkgPath)) return null + + const str = fs.readFileSync(pkgPath, 'utf-8') + return JSON.parse(str).dependencies as Record +} + +type File = { code: string } + +async function getSandpackFiles(folder: string, extensions = ['js', 'ts', 'jsx', 'tsx', 'css']) { + const filepaths = await crawl( + folder, + (dir) => + !dir.includes('node_modules') && extensions.map((ext) => dir.endsWith(ext)).some(Boolean), + ) + // console.log('filepaths', filepaths) + + const files = filepaths.reduce( + (acc, filepath) => { + const relativeFilepath = path.relative(folder, filepath) + + return { + ...acc, + [`/${relativeFilepath}`]: { + code: fs.readFileSync(filepath, 'utf-8'), + }, + } + }, + {} as Record, + ) + + return files +} + +// https://sandpack.codesandbox.io/docs/getting-started/usage +export const Sandpack = async ({ + className, + folder, + ...props +}: { className: string; folder?: string } & ComponentProps) => { + // console.log('folder', folder) + + const files = folder ? await getSandpackFiles(folder) : props.files + + const pkgDeps = folder ? getSandpackDependencies(folder) : null + const dependencies = pkgDeps ?? props.customSetup?.dependencies + const customSetup = { + ...props.customSetup, + dependencies, + } + + const options = { + ...props.options, + // editorHeight: 350 + } + + return ( +
+ +
+ ) +} diff --git a/src/components/mdx/Sandpack/index.ts b/src/components/mdx/Sandpack/index.ts new file mode 100644 index 00000000..83aba001 --- /dev/null +++ b/src/components/mdx/Sandpack/index.ts @@ -0,0 +1 @@ +export * from './Sandpack' diff --git a/src/components/mdx/Sandpack/rehypeSandpack.ts b/src/components/mdx/Sandpack/rehypeSandpack.ts new file mode 100644 index 00000000..2187f127 --- /dev/null +++ b/src/components/mdx/Sandpack/rehypeSandpack.ts @@ -0,0 +1,49 @@ +import type { Root } from 'hast' +import { resolve } from 'path' +import { visit } from 'unist-util-visit' + +// +// +// +// { +// type: 'mdxJsxFlowElement', +// name: 'Sandpack', +// attributes: [ +// { +// type: 'mdxJsxAttribute', +// name: 'folder', +// value: 'authoring-sandpack-cloud', +// position: [Object] +// }, +// ... +// ], +// position: { +// start: { line: 3, column: 1, offset: 2 }, +// end: { line: 3, column: 32, offset: 33 } +// }, +// data: { _mdxExplicitJsx: true }, +// children: [] +// } + +// https://unifiedjs.com/learn/guide/create-a-rehype-plugin/ +export function rehypeSandpack(dir: string) { + return () => (tree: Root) => { + visit(tree, null, function (node) { + if ('name' in node && node.name === 'Sandpack') { + // + // Resolve folder path + // + + const folderAttr = node.attributes + .filter((node) => 'name' in node) + .find((attr) => attr.name === 'folder') + + if (folderAttr) { + const oldFolder = folderAttr?.value + + if (typeof oldFolder === 'string') folderAttr.value = `${resolve(dir, oldFolder)}` + } + } + }) + } +} diff --git a/src/components/mdx/index.tsx b/src/components/mdx/index.tsx index 4942fc28..7587aa38 100644 --- a/src/components/mdx/index.tsx +++ b/src/components/mdx/index.tsx @@ -8,12 +8,12 @@ export * from './Img' export * from './Intro' export * from './Keypoints' export * from './People' +export * from './Sandpack' export * from './Summary' export * from './Toc' import cn from '@/lib/cn' import { MARKDOWN_REGEX } from '@/utils/docs' -import { Sandpack as SP } from '@codesandbox/sandpack-react' import { ComponentProps } from 'react' import { Img } from './Img' @@ -111,19 +111,3 @@ export const code = (props: ComponentProps<'code'>) => ( {...props} /> ) - -// https://sandpack.codesandbox.io/docs/getting-started/usage -export const Sandpack = ({ - className, - ...props -}: { className: string } & ComponentProps) => ( -
- -
-) diff --git a/src/utils/docs.tsx b/src/utils/docs.tsx index 88d9bfe2..7f5274b1 100644 --- a/src/utils/docs.tsx +++ b/src/utils/docs.tsx @@ -7,12 +7,14 @@ import { rehypeCodesandbox } from '@/components/mdx/Codesandbox/rehypeCodesandbo import { rehypeDetails } from '@/components/mdx/Details/rehypeDetails' import { rehypeGha } from '@/components/mdx/Gha/rehypeGha' import { rehypeImg } from '@/components/mdx/Img/rehypeImg' +import { rehypeSandpack } from '@/components/mdx/Sandpack/rehypeSandpack' import { rehypeSummary } from '@/components/mdx/Summary/rehypeSummary' import { rehypeToc } from '@/components/mdx/Toc/rehypeToc' import resolveMdxUrl from '@/utils/resolveMdxUrl' import matter from 'gray-matter' import { compileMDX } from 'next-mdx-remote/rsc' import fs from 'node:fs' +import { dirname } from 'node:path' import React, { cache } from 'react' import rehypePrismPlus from 'rehype-prism-plus' import remarkGFM from 'remark-gfm' @@ -40,11 +42,11 @@ const INLINE_LINK_REGEX = /<(http[^>]+)>/g /** * Recursively crawls a directory, returning an array of file paths. */ -async function crawl(dir: string, filter?: RegExp, files: string[] = []) { +export async function crawl(dir: string, filter?: (dir: string) => boolean, files: string[] = []) { if (fs.lstatSync(dir).isDirectory()) { const filenames = fs.readdirSync(dir) as string[] await Promise.all(filenames.map(async (filename) => crawl(`${dir}/${filename}`, filter, files))) - } else if (!filter || filter.test(dir)) { + } else if (!filter || filter(dir)) { files.push(dir) } @@ -65,7 +67,7 @@ async function _getDocs( slugOfInterest: string[] | null, slugOnly = false, ): Promise { - const files = await crawl(root, MARKDOWN_REGEX) + const files = await crawl(root, (dir) => MARKDOWN_REGEX.test(dir)) // console.log('files', files) const docs = await Promise.all( @@ -167,6 +169,7 @@ async function _getDocs( rehypeCode(), rehypeCodesandbox(boxes), // 1. put all Codesandbox[id] into `doc.boxes` rehypeToc(tableOfContents, url, title), // 2. will populate `doc.tableOfContents` + rehypeSandpack(dirname(file)), ], }, }, diff --git a/tsconfig.json b/tsconfig.json index cc116982..3686cd15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "docs"] }