Skip to content

Commit b5fa16c

Browse files
authored
feat: Sandpack[folder] (#361)
1 parent 5e99474 commit b5fa16c

File tree

10 files changed

+181
-21
lines changed

10 files changed

+181
-21
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { CameraControls, Cloud } from '@react-three/drei'
2+
import { Canvas } from '@react-three/fiber'
3+
4+
export default function App() {
5+
return (
6+
<Canvas camera={{ position: [0, -13, 0] }}>
7+
<Cloud speed={0.4} growth={6} />
8+
<ambientLight intensity={Math.PI} />
9+
<CameraControls />
10+
</Canvas>
11+
)
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "simple-box",
3+
"dependencies": {
4+
"three": "latest",
5+
"@react-three/fiber": "latest",
6+
"@react-three/drei": "latest"
7+
}
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
html,
2+
body,
3+
#root {
4+
height: 100%;
5+
margin: unset;
6+
}

docs/getting-started/authoring.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,34 @@ export default function App() {
342342

343343
</details>
344344

345+
#### `Sandpack[folder]`
345346

347+
Instead of `files`, a `folder` prop allow you to pass a folder containing all the files:
348+
349+
```tsx
350+
<Sandpack
351+
template="react-ts"
352+
folder="authoring-sandpack-cloud"
353+
/>
354+
```
355+
356+
NB: `folder` path is relative to the mdx file.
357+
358+
> [!TIP]
359+
> It will simply:
360+
> - build the `files` prop for you (including all `.{js|ts|jsx|tsx|css}` it finds)
361+
> - build `customSetup.dependencies` from `package.json` if it exists
362+
363+
<details>
364+
<summary>Result</summary>
365+
366+
<Sandpack
367+
template="react-ts"
368+
folder="authoring-sandpack-cloud"
369+
/>
370+
371+
372+
</details>
346373

347374
### `Codesandbox`
348375

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import cn from '@/lib/cn'
2+
import { crawl } from '@/utils/docs'
3+
import { Sandpack as SP } from '@codesandbox/sandpack-react'
4+
import fs from 'node:fs'
5+
import path from 'node:path'
6+
import { ComponentProps } from 'react'
7+
8+
function getSandpackDependencies(folder: string) {
9+
const pkgPath = `${folder}/package.json`
10+
if (!fs.existsSync(pkgPath)) return null
11+
12+
const str = fs.readFileSync(pkgPath, 'utf-8')
13+
return JSON.parse(str).dependencies as Record<string, string>
14+
}
15+
16+
type File = { code: string }
17+
18+
async function getSandpackFiles(folder: string, extensions = ['js', 'ts', 'jsx', 'tsx', 'css']) {
19+
const filepaths = await crawl(
20+
folder,
21+
(dir) =>
22+
!dir.includes('node_modules') && extensions.map((ext) => dir.endsWith(ext)).some(Boolean),
23+
)
24+
// console.log('filepaths', filepaths)
25+
26+
const files = filepaths.reduce(
27+
(acc, filepath) => {
28+
const relativeFilepath = path.relative(folder, filepath)
29+
30+
return {
31+
...acc,
32+
[`/${relativeFilepath}`]: {
33+
code: fs.readFileSync(filepath, 'utf-8'),
34+
},
35+
}
36+
},
37+
{} as Record<string, File>,
38+
)
39+
40+
return files
41+
}
42+
43+
// https://sandpack.codesandbox.io/docs/getting-started/usage
44+
export const Sandpack = async ({
45+
className,
46+
folder,
47+
...props
48+
}: { className: string; folder?: string } & ComponentProps<typeof SP>) => {
49+
// console.log('folder', folder)
50+
51+
const files = folder ? await getSandpackFiles(folder) : props.files
52+
53+
const pkgDeps = folder ? getSandpackDependencies(folder) : null
54+
const dependencies = pkgDeps ?? props.customSetup?.dependencies
55+
const customSetup = {
56+
...props.customSetup,
57+
dependencies,
58+
}
59+
60+
const options = {
61+
...props.options,
62+
// editorHeight: 350
63+
}
64+
65+
return (
66+
<div className={cn(className, 'sandpack')}>
67+
<SP {...props} files={files} customSetup={customSetup} options={options} />
68+
</div>
69+
)
70+
}

src/components/mdx/Sandpack/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Sandpack'
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Root } from 'hast'
2+
import { resolve } from 'path'
3+
import { visit } from 'unist-util-visit'
4+
5+
//
6+
// <Sandpack folder="authoring-sandpack-cloud" />
7+
//
8+
// {
9+
// type: 'mdxJsxFlowElement',
10+
// name: 'Sandpack',
11+
// attributes: [
12+
// {
13+
// type: 'mdxJsxAttribute',
14+
// name: 'folder',
15+
// value: 'authoring-sandpack-cloud',
16+
// position: [Object]
17+
// },
18+
// ...
19+
// ],
20+
// position: {
21+
// start: { line: 3, column: 1, offset: 2 },
22+
// end: { line: 3, column: 32, offset: 33 }
23+
// },
24+
// data: { _mdxExplicitJsx: true },
25+
// children: []
26+
// }
27+
28+
// https://unifiedjs.com/learn/guide/create-a-rehype-plugin/
29+
export function rehypeSandpack(dir: string) {
30+
return () => (tree: Root) => {
31+
visit(tree, null, function (node) {
32+
if ('name' in node && node.name === 'Sandpack') {
33+
//
34+
// Resolve folder path
35+
//
36+
37+
const folderAttr = node.attributes
38+
.filter((node) => 'name' in node)
39+
.find((attr) => attr.name === 'folder')
40+
41+
if (folderAttr) {
42+
const oldFolder = folderAttr?.value
43+
44+
if (typeof oldFolder === 'string') folderAttr.value = `${resolve(dir, oldFolder)}`
45+
}
46+
}
47+
})
48+
}
49+
}

src/components/mdx/index.tsx

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ export * from './Img'
88
export * from './Intro'
99
export * from './Keypoints'
1010
export * from './People'
11+
export * from './Sandpack'
1112
export * from './Summary'
1213
export * from './Toc'
1314

1415
import cn from '@/lib/cn'
1516
import { MARKDOWN_REGEX } from '@/utils/docs'
16-
import { Sandpack as SP } from '@codesandbox/sandpack-react'
1717
import { ComponentProps } from 'react'
1818
import { Img } from './Img'
1919

@@ -111,19 +111,3 @@ export const code = (props: ComponentProps<'code'>) => (
111111
{...props}
112112
/>
113113
)
114-
115-
// https://sandpack.codesandbox.io/docs/getting-started/usage
116-
export const Sandpack = ({
117-
className,
118-
...props
119-
}: { className: string } & ComponentProps<typeof SP>) => (
120-
<div className={cn(className, 'sandpack')}>
121-
<SP
122-
{...props}
123-
options={{
124-
...props.options,
125-
// editorHeight: 350
126-
}}
127-
/>
128-
</div>
129-
)

src/utils/docs.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { rehypeCodesandbox } from '@/components/mdx/Codesandbox/rehypeCodesandbo
77
import { rehypeDetails } from '@/components/mdx/Details/rehypeDetails'
88
import { rehypeGha } from '@/components/mdx/Gha/rehypeGha'
99
import { rehypeImg } from '@/components/mdx/Img/rehypeImg'
10+
import { rehypeSandpack } from '@/components/mdx/Sandpack/rehypeSandpack'
1011
import { rehypeSummary } from '@/components/mdx/Summary/rehypeSummary'
1112
import { rehypeToc } from '@/components/mdx/Toc/rehypeToc'
1213
import resolveMdxUrl from '@/utils/resolveMdxUrl'
1314
import matter from 'gray-matter'
1415
import { compileMDX } from 'next-mdx-remote/rsc'
1516
import fs from 'node:fs'
17+
import { dirname } from 'node:path'
1618
import React, { cache } from 'react'
1719
import rehypePrismPlus from 'rehype-prism-plus'
1820
import remarkGFM from 'remark-gfm'
@@ -40,11 +42,11 @@ const INLINE_LINK_REGEX = /<(http[^>]+)>/g
4042
/**
4143
* Recursively crawls a directory, returning an array of file paths.
4244
*/
43-
async function crawl(dir: string, filter?: RegExp, files: string[] = []) {
45+
export async function crawl(dir: string, filter?: (dir: string) => boolean, files: string[] = []) {
4446
if (fs.lstatSync(dir).isDirectory()) {
4547
const filenames = fs.readdirSync(dir) as string[]
4648
await Promise.all(filenames.map(async (filename) => crawl(`${dir}/${filename}`, filter, files)))
47-
} else if (!filter || filter.test(dir)) {
49+
} else if (!filter || filter(dir)) {
4850
files.push(dir)
4951
}
5052

@@ -65,7 +67,7 @@ async function _getDocs(
6567
slugOfInterest: string[] | null,
6668
slugOnly = false,
6769
): Promise<Doc[]> {
68-
const files = await crawl(root, MARKDOWN_REGEX)
70+
const files = await crawl(root, (dir) => MARKDOWN_REGEX.test(dir))
6971
// console.log('files', files)
7072

7173
const docs = await Promise.all(
@@ -167,6 +169,7 @@ async function _getDocs(
167169
rehypeCode(),
168170
rehypeCodesandbox(boxes), // 1. put all Codesandbox[id] into `doc.boxes`
169171
rehypeToc(tableOfContents, url, title), // 2. will populate `doc.tableOfContents`
172+
rehypeSandpack(dirname(file)),
170173
],
171174
},
172175
},

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@
2525
}
2626
},
2727
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28-
"exclude": ["node_modules"]
28+
"exclude": ["node_modules", "docs"]
2929
}

0 commit comments

Comments
 (0)