Skip to content

Commit

Permalink
feat: Sandpack[folder] (#361)
Browse files Browse the repository at this point in the history
  • Loading branch information
abernier authored Oct 31, 2024
1 parent 5e99474 commit b5fa16c
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 21 deletions.
12 changes: 12 additions & 0 deletions docs/getting-started/authoring-sandpack-cloud/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CameraControls, Cloud } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'

export default function App() {
return (
<Canvas camera={{ position: [0, -13, 0] }}>
<Cloud speed={0.4} growth={6} />
<ambientLight intensity={Math.PI} />
<CameraControls />
</Canvas>
)
}
8 changes: 8 additions & 0 deletions docs/getting-started/authoring-sandpack-cloud/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "simple-box",
"dependencies": {
"three": "latest",
"@react-three/fiber": "latest",
"@react-three/drei": "latest"
}
}
6 changes: 6 additions & 0 deletions docs/getting-started/authoring-sandpack-cloud/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
html,
body,
#root {
height: 100%;
margin: unset;
}
27 changes: 27 additions & 0 deletions docs/getting-started/authoring.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,34 @@ export default function App() {

</details>

#### `Sandpack[folder]`

Instead of `files`, a `folder` prop allow you to pass a folder containing all the files:

```tsx
<Sandpack
template="react-ts"
folder="authoring-sandpack-cloud"
/>
```

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
<details>
<summary>Result</summary>

<Sandpack
template="react-ts"
folder="authoring-sandpack-cloud"
/>


</details>

### `Codesandbox`

Expand Down
70 changes: 70 additions & 0 deletions src/components/mdx/Sandpack/Sandpack.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string>
}

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<string, File>,
)

return files
}

// https://sandpack.codesandbox.io/docs/getting-started/usage
export const Sandpack = async ({
className,
folder,
...props
}: { className: string; folder?: string } & ComponentProps<typeof SP>) => {
// 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 (
<div className={cn(className, 'sandpack')}>
<SP {...props} files={files} customSetup={customSetup} options={options} />
</div>
)
}
1 change: 1 addition & 0 deletions src/components/mdx/Sandpack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Sandpack'
49 changes: 49 additions & 0 deletions src/components/mdx/Sandpack/rehypeSandpack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Root } from 'hast'
import { resolve } from 'path'
import { visit } from 'unist-util-visit'

//
// <Sandpack folder="authoring-sandpack-cloud" />
//
// {
// 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)}`
}
}
})
}
}
18 changes: 1 addition & 17 deletions src/components/mdx/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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<typeof SP>) => (
<div className={cn(className, 'sandpack')}>
<SP
{...props}
options={{
...props.options,
// editorHeight: 350
}}
/>
</div>
)
9 changes: 6 additions & 3 deletions src/utils/docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}

Expand All @@ -65,7 +67,7 @@ async function _getDocs(
slugOfInterest: string[] | null,
slugOnly = false,
): Promise<Doc[]> {
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(
Expand Down Expand Up @@ -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)),
],
},
},
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "docs"]
}

0 comments on commit b5fa16c

Please sign in to comment.