Skip to content

Commit

Permalink
[v3] Add <Playground /> component (#3174)
Browse files Browse the repository at this point in the history
* [nextra] Playground

* move to nextra.site

* polish

* prettier

* more

* fix types

* more

* rename

* rename

* more

* more

* more

* more

* add mermaid code block

* it works

* aaa

* dada

* ad

* fix prettier

* polish

* more

* pnpm i

* fix

* fix tests

* do not increase global bundle size

* add changeset

---------

Co-authored-by: hariria <[email protected]>
  • Loading branch information
dimaMachina and hariria authored Aug 31, 2024
1 parent 177fe84 commit 452e5bd
Show file tree
Hide file tree
Showing 23 changed files with 285 additions and 257 deletions.
7 changes: 7 additions & 0 deletions .changeset/red-humans-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'nextra-theme-docs': patch
---

Add `<Playground />` component

https://nextra-v2-9x7fp6hti-shud.vercel.app/docs/guide/advanced/playground
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module.exports = {
'sonarjs/no-unused-collection': 'error',
'unicorn/catch-error-name': 'error',
'unicorn/prefer-optional-catch-binding': 'error',
'unicorn/filename-case': 'error',
// todo: enable
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ examples/swr-site/pages/en/remote/graphql-eslint/_meta.ts
examples/swr-site/pages/en/remote/graphql-yoga/_meta.ts

docs/pages/docs/guide/built-ins/cards.mdx
docs/pages/docs/guide/advanced/playground.mdx
7 changes: 6 additions & 1 deletion docs/pages/docs/guide/advanced/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@ export default {
latex: 'LaTeX',
table: 'Rendering Tables',
typescript: '',
remote: 'Remote Content'
remote: 'Remote Content',
playground: {
theme: {
layout: 'full'
}
}
}
108 changes: 108 additions & 0 deletions docs/pages/docs/guide/advanced/playground.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Mermaid, Playground, Code, Pre, Tabs } from 'nextra/components'
import { useRef, useCallback, useState, useEffect } from 'react'

export function Demo() {
const [rawMdx, setRawMdx] = useState(`Playground components allow you to write Nextra compatible MDX that renders only on the client. It's modeled after the functionality found in [MDX Playground](https://mdxjs.com/playground).
In some instances where remote loading MDX is not an option, this may work as a great alternative.
Here's an example of a codeblock.
\`\`\`ts
console.log("Hello world, this is a playground component!");
\`\`\`
## Caveats
Due to the purely client-side nature of this component, features "Table of Contents" and "Frontmatter" will not work.
## Mermaid Example
\`\`\`mermaid
graph TD
subgraph AA [Consumers]
A[Mobile App]
B[Web App]
C[Node.js Client]
end
subgraph BB [Services]
E[REST API]
F[GraphQL API]
G[SOAP API]
end
Z[GraphQL API]
A --> Z
B --> Z
C --> Z
Z --> E
Z --> F
Z --> G
\`\`\``)
const handleInput = useCallback(e => {
setRawMdx(e.currentTarget.textContent ?? '')
}, [])

const spanRef = useRef(null)
const initialRender = useRef(false)

useEffect(() => {
if (!initialRender.current && spanRef.current) {
initialRender.current = true
spanRef.current.textContent = rawMdx
}
}, []);

return (
<div className="grid grid-cols-2 gap-2 mt-6">
<Pre data-filename="MDX" icon={MdxIcon}>
<Code>
<span
ref={spanRef}
contentEditable
suppressContentEditableWarning
className="outline-none"
onInput={handleInput}
/>
</Code>
</Pre>
<div>
<Playground
fallback={
<div className="flex h-full items-center justify-center text-4xl">
Loading playground...
</div>
}
source={rawMdx}
components={{ Mermaid, $Tabs: Tabs }}
/>
</div>
</div>
)
}

# Playground

<Demo />

## Usage

```mdx filename="Basic Usage"
import { Playground } from 'nextra/components'

# Playground

Below is a playground component. It mixes into the rest of your MDX perfectly.

<Playground source="## Hello world" />
```

You may also specify a fallback component like so:

```mdx filename="Usage with Fallback"
import { Playground } from 'nextra/components'

<Playground
source="## Hello world"
fallback={<div>Loading playground...</div>}
/>
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"prettier-plugin-pkg": "0.18.1",
"prettier-plugin-tailwindcss": "0.6.5",
"rimraf": "6.0.1",
"tsup": "8.1.0",
"tsup": "8.2.4",
"tsx": "^4.7.0",
"turbo": "2.1.1",
"typescript": "5.5.4"
Expand All @@ -54,7 +54,7 @@
},
"patchedDependencies": {
"@changesets/[email protected]": "patches/@[email protected]",
"tsup@8.1.0": "patches/tsup@8.0.1.patch"
"tsup@8.2.4": "patches/tsup.patch"
}
}
}
2 changes: 1 addition & 1 deletion packages/nextra-theme-blog/__test__/collect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
config,
indexOpts,
postsOpts
} from './__fixture__/pageMap'
} from './__fixture__/page-map'

vi.mock('next/router', () => ({
useRouter: vi.fn()
Expand Down
2 changes: 1 addition & 1 deletion packages/nextra-theme-blog/__test__/parent.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRouter } from 'next/router'
import type { Mock } from 'vitest'
import { getParent } from '../src/utils/parent'
import { articleOpts, config } from './__fixture__/pageMap'
import { articleOpts, config } from './__fixture__/page-map'

vi.mock('next/router', () => ({
useRouter: vi.fn()
Expand Down
2 changes: 1 addition & 1 deletion packages/nextra-theme-blog/__test__/tag.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { getStaticTags } from '../src/utils/get-tags'
import { articleOpts, indexOpts, postsOpts } from './__fixture__/pageMap'
import { articleOpts, indexOpts, postsOpts } from './__fixture__/page-map'

describe('parent', () => {
it('string', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/nextra/__test__/normalize-page.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { normalizePages } from '../src/client/normalize-pages.js'
import { cnPageMap, usPageMap } from './fixture/page-maps/pageMap.js'
import { cnPageMap, usPageMap } from './fixture/page-maps/page-map.js'

describe('normalize-page', () => {
it('zh-CN home', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/nextra/src/client/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export { Th } from './th.js'
export { Tr } from './tr.js'
export { Mermaid } from '@theguild/remark-mermaid/mermaid'
export { MathJax, MathJaxContext } from 'better-react-mathjax'
export { Playground } from './playground.js'
82 changes: 82 additions & 0 deletions packages/nextra/src/client/components/playground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useEffect, useState } from 'react'
import type { ReactElement } from 'react'
import { CrossCircledIcon } from '../icons/index.js'
import type { MDXComponents } from '../mdx.js'
import { Code } from './code.js'
import { Pre } from './pre.js'
import { evaluate } from './remote-content.js'

export function Playground({
source,
scope,
components,
fallback = null
}: {
/**
* String with source MDX
*
* @example '# hello world <br /> nice to see you'
*/
source: string
/**
* An object mapping names to React components.
* The key used will be the name accessible to MDX.
*
* @example `{ ComponentName: Component }` will be accessible in the MDX as `<ComponentName/>`.
*/
components?: MDXComponents
/**
* Pass-through variables for use in the MDX content
*/
scope?: Record<string, unknown>
/**
* Fallback component for loading
*/
fallback?: ReactElement | null
}) {
const [compiledSource, setCompiledSource] = useState('')
const [error, setError] = useState<unknown>()

useEffect(() => {
async function doCompile() {
// Importing in useEffect to not increase global bundle size
const { compileMdx } = await import('../../server/compile.js')
try {
const mdx = await compileMdx(source)
setCompiledSource(mdx.result)
setError(null)
} catch (error) {
setError(error)
}
}

doCompile()
}, [source])

if (error) {
return (
<div className="[&_svg]:_text-red-500">
<Pre
data-filename="Could not compile code"
icon={CrossCircledIcon}
className="_whitespace-pre-wrap"
>
<Code>
<span>
{error instanceof Error
? `${error.name}: ${error.message}`
: String(error)}
</span>
</Code>
</Pre>
</div>
)
}

if (compiledSource) {
const MDXContent = evaluate(compiledSource, scope).default
return <MDXContent components={components} />
}

return fallback
}
4 changes: 2 additions & 2 deletions packages/nextra/src/client/components/remote-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function RemoteContent({
* An object mapping names to React components.
* The key used will be the name accessible to MDX.
*
* For example: `{ ComponentName: Component }` will be accessible in the MDX as `<ComponentName/>`.
* @example `{ ComponentName: Component }` will be accessible in the MDX as `<ComponentName/>`.
*/
components?: MDXComponents
/**
Expand All @@ -45,7 +45,7 @@ export function RemoteContent({
)
}

const { default: MDXContent } = evaluate(compiledSource, scope)
const MDXContent = evaluate(compiledSource, scope).default

return <MDXContent components={components} />
}
Expand Down
5 changes: 5 additions & 0 deletions packages/nextra/src/client/icons/cross-circled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/nextra/src/client/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export { ReactComponent as PythonIcon } from './python.svg'
export { ReactComponent as RustIcon } from './rust.svg'
export { ReactComponent as TerraformIcon } from './terraform.svg'
export { ReactComponent as MoveIcon } from './move.svg'
export { ReactComponent as CrossCircledIcon } from './cross-circled.svg'
6 changes: 2 additions & 4 deletions packages/nextra/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ declare module 'next/dist/compiled/webpack/webpack.js' {
}

declare module '*.svg' {
import type { ComponentPropsWithRef, ReactElement } from 'react'
export const ReactComponent: (
_props: ComponentPropsWithRef<'svg'>
) => ReactElement
import type { FC, SVGProps } from 'react'
export const ReactComponent: FC<SVGProps<SVGSVGElement>>
}
4 changes: 2 additions & 2 deletions packages/nextra/src/icon.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ComponentProps, ReactElement } from 'react'
import type { FC, SVGProps } from 'react'

declare const ReactComponent: (props: ComponentProps<'svg'>) => ReactElement
declare const ReactComponent: FC<SVGProps<SVGElement>>

export { ReactComponent }
2 changes: 1 addition & 1 deletion packages/nextra/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2016",
"target": "es2018",
"module": "ESNext",
"declaration": true,
"noEmit": true,
Expand Down
16 changes: 15 additions & 1 deletion packages/nextra/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,21 @@ export default defineConfig([
const jsxRuntimeTo = path.join(CWD, 'dist', 'client', 'jsx-runtime.cjs')

await fs.copyFile(jsxRuntimeFrom, jsxRuntimeTo)
}
},
plugins: [
{
// Strip `node:` prefix from imports because
// Next.js only polyfills `path` and not `node:path` for browser
name: 'strip-node-colon',
renderChunk(code) {
const replaced = code.replaceAll(
/ from "node:(?<moduleName>.*?)";/g,
matched => matched.replace('node:', '')
)
return { code: replaced }
}
}
]
},
{
name: 'nextra/icons',
Expand Down
4 changes: 2 additions & 2 deletions patches/[email protected] → patches/tsup.patch
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
diff --git a/dist/rollup.js b/dist/rollup.js
index daba939dbbfab21c360e4021e5723abf8fe997ea..9d9be8a782a6fed1f3ce891c19a40c7543dc1910 100644
index 8c514b6e014a4cda52f4b2538659418861049229..dddccb73c38a2a44b68d18097f6437e433152e6e 100644
--- a/dist/rollup.js
+++ b/dist/rollup.js
@@ -8160,6 +8160,10 @@ var getRollupConfig = async (options) => {
@@ -8392,6 +8392,10 @@ var getRollupConfig = async (options) => {
tsResolveOptions && tsResolvePlugin(tsResolveOptions),
json(),
ignoreFiles,
Expand Down
Loading

0 comments on commit 452e5bd

Please sign in to comment.