├── .prettierrc ├── src ├── lib │ ├── virtual-files.js │ ├── components │ │ ├── index.js │ │ └── LiveCodeLayout.astro │ ├── index.d.ts │ ├── index.js │ ├── vite.js │ └── remark.js ├── env.d.ts ├── assets │ └── houston.webp └── content │ ├── docs │ ├── guides │ │ ├── JSXWrapper.jsx │ │ ├── ThemeProvider.jsx │ │ ├── client-directives.mdx │ │ ├── diff-syntax.mdx │ │ ├── getting-started.md │ │ ├── passing-props.mdx │ │ ├── basic-usage.mdx │ │ ├── multiple-jsx-frameworks.mdx │ │ └── customization.mdx │ └── index.mdx │ └── config.ts ├── .vscode ├── extensions.json └── launch.json ├── svelte.config.js ├── README.md ├── tsconfig.json ├── .gitignore ├── public └── favicon.svg ├── LICENSE ├── package.json └── astro.config.mjs /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /src/lib/virtual-files.js: -------------------------------------------------------------------------------- 1 | export const virtualFiles = new Map() 2 | -------------------------------------------------------------------------------- /src/lib/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as LiveCodeLayout } from './LiveCodeLayout.astro' -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/assets/houston.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattjennings/astro-live-code/HEAD/src/assets/houston.webp -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@astrojs/svelte'; 2 | 3 | export default { 4 | preprocess: vitePreprocess(), 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astro Live Code 2 | 3 | Render your MDX code blocks in Astro. 4 | 5 | [See documentation](https://astro-live-code.mattjennin.gs) 6 | -------------------------------------------------------------------------------- /src/content/docs/guides/JSXWrapper.jsx: -------------------------------------------------------------------------------- 1 | import ThemeProvider from './ThemeProvider' 2 | 3 | export default function JSXWrapper({ children }) { 4 | return {children} 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "jsxImportSource": "preact" 6 | }, 7 | "include": ["src/**/*", "lib/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface LiveCodeOptions { 2 | layout?: string 3 | wrapper?: string 4 | defaultProps?: Record 5 | } 6 | 7 | declare function liveCode( 8 | options: LiveCodeOptions, 9 | ): import('astro').AstroIntegration 10 | 11 | export default liveCode 12 | -------------------------------------------------------------------------------- /src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | i18n: defineCollection({ type: 'data', schema: i18nSchema() }), 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | .vercel 23 | -------------------------------------------------------------------------------- /src/content/docs/guides/ThemeProvider.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'preact' 2 | import { useContext } from 'preact/hooks' 3 | 4 | export const ThemeContext = createContext('light') 5 | 6 | export default function ThemeProvider({ children }) { 7 | return {children} 8 | } 9 | 10 | export const useTheme = () => { 11 | const theme = useContext(ThemeContext) 12 | return theme 13 | } 14 | -------------------------------------------------------------------------------- /src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Astro Live Code 3 | description: Render your MDX code blocks in Astro 4 | template: splash 5 | hero: 6 | tagline: Render your MDX code blocks in Astro 7 | image: 8 | file: ../../assets/houston.webp 9 | actions: 10 | - text: Get Started 11 | link: /guides/getting-started/ 12 | icon: right-arrow 13 | variant: primary 14 | --- 15 | 16 | ```jsx live 17 | export default () => { 18 | return 19 | } 20 | ``` -------------------------------------------------------------------------------- /src/content/docs/guides/client-directives.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client Directives 3 | sidebar: 4 | order: 3 5 | --- 6 | 7 | You are able to pass `client:*` props. 8 | 9 | ````md 10 | ```jsx live props={{ "client:load": true }} 11 | // code 12 | ``` 13 | ```` 14 | 15 | ```jsx live props={{ "client:load": true }} 16 | import { useState } from 'preact/hooks' 17 | 18 | export default () => { 19 | const [count, setCount] = useState(0) 20 | return ( 21 | 24 | ) 25 | } 26 | ``` -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/components/LiveCodeLayout.astro: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 | 10 | 28 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | import remark from './remark.js' 2 | import vite from './vite.js' 3 | 4 | /** 5 | * @type {import('./index.js').default} 6 | */ 7 | export default function liveCode(options = {}) { 8 | Object.assign( 9 | options, 10 | { 11 | layout: 'astro-live-code/components/LiveCodeLayout.astro', 12 | }, 13 | { ...options }, 14 | ) 15 | 16 | return { 17 | name: 'astro-live-code', 18 | hooks: { 19 | 'astro:config:setup': ({ updateConfig }) => { 20 | updateConfig({ 21 | markdown: { 22 | remarkPlugins: [[remark, options]], 23 | }, 24 | vite: { 25 | plugins: [vite(options)], 26 | }, 27 | }) 28 | }, 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/content/docs/guides/diff-syntax.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Diff Syntax 3 | sidebar: 4 | order: 4 5 | --- 6 | 7 | Diff syntax is supported when used with Expressive Code. [See their docs on using diff syntax](https://expressive-code.com/key-features/text-markers/#combining-syntax-highlighting-with-diff-like-syntax). 8 | (If you are using Starlight, Expressive Code is already included.) 9 | 10 | Lines that begin with `-` are removed from the rendered code. 11 | 12 | ```` 13 | ```diff live lang="html" 14 |
15 | - 16 | + 17 |
18 | ``` 19 | ```` 20 | 21 | becomes 22 | 23 | ```diff live lang="html" 24 |
25 | - 26 | + 27 |
28 | ``` 29 | -------------------------------------------------------------------------------- /src/content/docs/guides/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | sidebar: 4 | order: 0 5 | --- 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install astro-live-code 11 | ``` 12 | 13 | ## Config 14 | 15 | Add the following to your `astro.config.mjs` file: 16 | 17 | ```js {2,7} title="astro.config.mjs" 18 | import { defineConfig } from 'astro/config' 19 | import liveCode from 'astro-live-code' 20 | 21 | export default defineConfig({ 22 | integrations: [ 23 | // ... other integrations 24 | liveCode(), 25 | ], 26 | }) 27 | ``` 28 | 29 | ## Usage 30 | 31 | Add `live` to the end of your code block. If the language is renderable by your 32 | Astro project (e.g. `jsx`, `svelte`, `vue`, etc.) it will be rendered as an 33 | imported component. 34 | 35 | ````md 36 | ```html live 37 | 38 | ``` 39 | ```` 40 | 41 | See [the next page](/guides/basic-usage) for more examples. 42 | -------------------------------------------------------------------------------- /src/content/docs/guides/passing-props.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Passing Props 3 | sidebar: 4 | order: 2 5 | --- 6 | 7 | Props can be passed to the live code block using the `props` attribute. This attribute accepts a JSON object of props to pass to the live code block. 8 | 9 | ````md 10 | ```jsx live props={{ children: 'hello world' }} 11 | // code 12 | ``` 13 | ```` 14 | 15 | ```jsx live props={{ children: 'hello world' }} 16 | export default ({ children }) => { 17 | return 18 | } 19 | ``` 20 | 21 | 22 | ## Default Props 23 | 24 | If you have a common set of props you'd like applied to all examples, you can configure 25 | it using `defaultProps` in the integration config: 26 | 27 | ```js title="astro.config.mjs" 28 | export default defineConfig({ 29 | integrations: [ 30 | liveCode({ 31 | defaultProps: { 32 | theme: 'dark', 33 | 34 | // apply client directives to all components (see next page) 35 | 'client:load': true 36 | }, 37 | }), 38 | ], 39 | }) 40 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2024 Matt Jennings 2 | 3 | Permission is hereby granted, free 4 | of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/content/docs/guides/basic-usage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Usage 3 | sidebar: 4 | order: 1 5 | --- 6 | 7 | ## React / Preact / Solid 8 | 9 | ````md 10 | ```jsx live 11 | export default () => { 12 | return 13 | } 14 | ``` 15 | ```` 16 | 17 | becomes 18 | 19 | ```jsx live 20 | export default () => { 21 | return 22 | } 23 | ``` 24 | 25 | ## Svelte 26 | 27 | ````md 28 | ```svelte live 29 | 30 | ``` 31 | ```` 32 | 33 | becomes 34 | 35 | ```svelte live 36 | 37 | ``` 38 | 39 | ## Vue 40 | 41 | ````md 42 | ```vue live 43 | 46 | ``` 47 | ```` 48 | 49 | becomes 50 | 51 | 52 | ```vue live 53 | 56 | ``` 57 | 58 | 59 | ## Astro 60 | 61 | ````md 62 | ```astro live 63 | 64 | ``` 65 | ```` 66 | 67 | becomes 68 | 69 | ```astro live 70 | 71 | ``` 72 | 73 | ## HTML 74 | 75 | ````md 76 | ```html live 77 | 78 | ``` 79 | ```` 80 | 81 | becomes 82 | 83 | ```html live 84 | 85 | ``` 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-live-code", 3 | "type": "module", 4 | "version": "0.0.5", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/mattjennings/astro-live-code" 9 | }, 10 | "files": [ 11 | "src/lib" 12 | ], 13 | "exports": { 14 | ".": { 15 | "types": "./src/lib/index.d.ts", 16 | "import": "./src/lib/index.js", 17 | "default": "./src/lib/index.js" 18 | }, 19 | "./components": "./src/lib/components/index.js", 20 | "./components/LiveCodeLayout.astro": "./src/lib/components/LiveCodeLayout.astro" 21 | }, 22 | "scripts": { 23 | "dev": "astro dev", 24 | "start": "astro dev", 25 | "build": "astro build", 26 | "preview": "astro preview", 27 | "astro": "astro" 28 | }, 29 | "dependencies": { 30 | "estree-util-visit": "^2.0.0", 31 | "magic-string": "^0.30.5", 32 | "unist-util-visit-parents": "^6.0.1" 33 | }, 34 | "devDependencies": { 35 | "@astrojs/preact": "^4.0.1", 36 | "@astrojs/solid-js": "^5.0.5", 37 | "@astrojs/starlight": "^0.32.2", 38 | "@astrojs/svelte": "^7.0.0", 39 | "@astrojs/vue": "^5.0.0", 40 | "astro": "^5.4.2", 41 | "preact": "^10.26.0", 42 | "prettier": "^3.1.0", 43 | "sharp": "^0.32.5", 44 | "solid-js": "^1.9.5", 45 | "svelte": "^5.0.0", 46 | "vue": "^3.2.30" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config' 2 | import starlight from '@astrojs/starlight' 3 | import liveCode from './src/lib/index.js' 4 | import path from 'path' 5 | import { fileURLToPath } from 'url' 6 | import svelte from '@astrojs/svelte' 7 | import preact from '@astrojs/preact' 8 | import solidJs from '@astrojs/solid-js' 9 | import vue from '@astrojs/vue' 10 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 11 | 12 | // https://astro.build/config 13 | export default defineConfig({ 14 | vite: { 15 | resolve: { 16 | alias: [ 17 | { 18 | find: 'astro-live-code', 19 | replacement: '/src/lib', 20 | }, 21 | ], 22 | }, 23 | }, 24 | integrations: [ 25 | starlight({ 26 | title: 'Astro Live Code', 27 | social: { 28 | github: 'https://github.com/mattjennings/astro-live-code', 29 | }, 30 | 31 | tableOfContents: { 32 | minHeadingLevel: 1, 33 | }, 34 | sidebar: [ 35 | { 36 | label: 'Guides', 37 | autogenerate: { 38 | directory: 'guides', 39 | }, 40 | }, 41 | ], 42 | }), 43 | liveCode({}), 44 | svelte(), 45 | solidJs({ 46 | include: ['**/solid/*', '**/*.solid.*'], 47 | }), 48 | preact({ exclude: ['**/solid/*', '**/*.solid.*'] }), 49 | vue(), 50 | ], 51 | }) 52 | -------------------------------------------------------------------------------- /src/content/docs/guides/multiple-jsx-frameworks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Multiple JSX Frameworks 3 | sidebar: 4 | order: 7 5 | --- 6 | Astro Live Code works by generating files out of the code blocks and naming them something like `AstroExample_XYZ.jsx`, where the extension is determined by the code block language. 7 | This will be an issue if you are using multiple JSX frameworks as Astro can't know which framework to use without changes to the filename. 8 | 9 | To solve this, you can manually specify the file extension with the `ext` meta attribute and use that to match against your 10 | [Astro integrations](https://docs.astro.build/en/guides/integrations-guide/preact/#combining-multiple-jsx-frameworks). 11 | 12 | First, edit your `astro.config.mjs` to look something like this: 13 | 14 | ```js title="astro.config.mjs" 15 | import { defineConfig } from 'astro/config'; 16 | import preact from '@astrojs/preact'; 17 | import react from '@astrojs/react'; 18 | import solid from '@astrojs/solid-js'; 19 | 20 | export default defineConfig({ 21 | integrations: [ 22 | preact({ 23 | include: ['**/*.preact.*'], 24 | }), 25 | react({ 26 | include: ['**/*.react.*'], 27 | }), 28 | solid({ 29 | include: ['**/*.solid.*'], 30 | }), 31 | ], 32 | }); 33 | ``` 34 | 35 | Then specify the file extension on the codeblock 36 | 37 | ```` 38 | ```jsx live ext="solid.jsx" 39 | ... 40 | ``` 41 | ```` 42 | 43 | ```jsx live ext="solid.jsx" 44 | import { For } from 'solid-js' 45 | 46 | export default () => { 47 | const items = ['a', 'b', 'c'] 48 | return ( 49 | {(item) => {item}} 50 | ) 51 | } 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /src/lib/vite.js: -------------------------------------------------------------------------------- 1 | import MagicString from 'magic-string' 2 | import { virtualFiles } from './virtual-files.js' 3 | 4 | /** 5 | * @returns {import('vite').Plugin} 6 | */ 7 | export default function liveCodeVitePlugin() { 8 | return { 9 | name: 'vite-live-code', 10 | resolveId(id) { 11 | if (virtualFiles.has(id)) { 12 | return id 13 | } 14 | }, 15 | async load(id) { 16 | if (virtualFiles.has(id)) { 17 | const s = new MagicString(virtualFiles.get(id).src) 18 | 19 | return { 20 | code: s.toString(), 21 | map: s.generateMap({ 22 | source: id, 23 | file: `${id}.map`, 24 | includeContent: true, 25 | }), 26 | } 27 | } 28 | }, 29 | handleHotUpdate(ctx) { 30 | const { server } = ctx 31 | const modules = [] 32 | const extensions = ['.md', '.mdx'] 33 | 34 | // return virtual file modules for parent file 35 | if (extensions.some((ext) => ctx.file.endsWith(ext))) { 36 | const files = [...virtualFiles.entries()] 37 | 38 | files 39 | .map(([id, file]) => ({ 40 | id, 41 | parent: file.parent, 42 | updated: file.updated, 43 | })) 44 | .filter((file) => { 45 | return ctx.file.endsWith(file.parent) 46 | }) 47 | .forEach((file) => { 48 | const mod = server.moduleGraph.getModuleById(file.id) 49 | if (mod) { 50 | modules.push( 51 | mod, 52 | ...mod.clientImportedModules, 53 | ...mod.ssrImportedModules, 54 | ) 55 | } 56 | }) 57 | } 58 | return [...modules, ...ctx.modules] 59 | }, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/content/docs/guides/customization.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Customization 3 | sidebar: 4 | order: 5 5 | --- 6 | import LiveCodeLayoutSrc from 'astro-live-code/components/LiveCodeLayout.astro?raw' 7 | import {Code} from 'astro:components' 8 | 9 | ## Layouts 10 | 11 | Live code is rendered in an Astro component. By default it uses `astro-live-code/components/LiveCodeLayout.astro`: 12 | 13 | 14 | 15 | You can customize this by creating your own Astro layout and providing it in the integration config: 16 | 17 | ```js title="astro.config.mjs" 18 | export default defineConfig({ 19 | integrations: [ 20 | liveCode({ 21 | layout: '/src/MyLayout.astro', 22 | }), 23 | ], 24 | }) 25 | ``` 26 | 27 | ### Layouts per code block 28 | 29 | Layouts can be specified for a specific code block by adding a `layout` attribute to the code block: 30 | 31 | ````md 32 | ```jsx layout="./path/to/Layout.astro" 33 | ``` 34 | ```` 35 | 36 | ### Props 37 | 38 | Layouts will receive the following props: 39 | 40 | ```ts 41 | interface Props { 42 | // The language of the code block 43 | lang: string 44 | // The filename of the mdx file 45 | filename: string 46 | // The props being passed to the component from props={{ ... }} 47 | componentProps: Record 48 | } 49 | ``` 50 | 51 | It will also receive `example` and `code` slots, where the rendered component and code block will be passed respectively. 52 | 53 | ## Wrappers 54 | 55 | Wrappers are similar to layouts, but they are components of any language that will wrap the component being rendered. 56 | This is useful if you need to wrap the code component in something like a `ThemeProvider`: 57 | 58 | ```jsx title="ThemeProvider.jsx" 59 | import { createContext } from 'preact' 60 | import { useContext } from 'preact/hooks' 61 | 62 | export const ThemeContext = createContext('light') 63 | 64 | export default function ThemeProvider({ children }) { 65 | return {children} 66 | } 67 | 68 | export const useTheme = () => useContext(ThemeContext) 69 | ``` 70 | 71 | ```jsx title="MyWrapper.jsx" 72 | import ThemeProvider from './ThemeProvider' 73 | 74 | export default function MyWrapper({ children }) { 75 | return {children} 76 | } 77 | ``` 78 | 79 | ```js title="astro.config.mjs" 80 | export default defineConfig({ 81 | integrations: [ 82 | liveCode({ 83 | wrapper: './src/MyWrapper.jsx', 84 | }), 85 | ], 86 | }) 87 | ``` 88 | 89 | Now code blocks will be rendered as children of `MyWrapper` 90 | 91 | ```jsx live wrapper="./JSXWrapper.jsx" 92 | import { useTheme } from './ThemeProvider' 93 | 94 | export default () => { 95 | const theme = useTheme() 96 | return ( 97 |

The theme is "{theme}"

98 | ) 99 | } 100 | ``` 101 | 102 | ### Wrappers per code block 103 | 104 | Like layouts, these can also be specified per code block: 105 | 106 | ````md 107 | ```jsx wrapper="./path/to/Wrapper.jsx" 108 | ``` 109 | ```` 110 | 111 | ### Props 112 | 113 | Wrappers will receive all props that the code component receives from `props={{ ... }}`. 114 | -------------------------------------------------------------------------------- /src/lib/remark.js: -------------------------------------------------------------------------------- 1 | import { visitParents as unistVisit } from 'unist-util-visit-parents' 2 | import { visit as estreeVisit } from 'estree-util-visit' 3 | import { virtualFiles } from './virtual-files.js' 4 | import path from 'path' 5 | 6 | export const EXAMPLE_COMPONENT_PREFIX = 'AstroExample_' 7 | 8 | /** 9 | * @param {import('./index.js').LiveCodeOptions} options 10 | * @returns 11 | */ 12 | export default function examples(options) { 13 | return function transformer(tree, file) { 14 | let examples = [] 15 | 16 | unistVisit(tree, 'code', (node, parents) => { 17 | const parent = parents[parents.length - 1] 18 | const childIndex = parent.children.indexOf(node) 19 | 20 | if (node.meta && node.meta.split(' ').includes('live')) { 21 | const isDiff = node.lang === 'diff' 22 | const lang = getLang(node) 23 | const meta = node.meta 24 | const parsedMeta = parseMeta(meta) 25 | 26 | let src = isDiff 27 | ? node.value 28 | ?.split('\n') 29 | // filter out lines with leading - 30 | .filter((line) => !line.match(/^[-]/)) 31 | // remove leading + from lines 32 | .map((line) => line.replace(/^[+]/, '')) 33 | .join('\n') 34 | : node.value 35 | 36 | const i = examples.length 37 | 38 | const parentId = toPOSIX( 39 | file.history[0].split(toPOSIX(process.cwd()))[1].slice(1), 40 | ) 41 | const mdFilename = toPOSIX( 42 | path.basename(parentId, path.extname(parentId)), 43 | ) 44 | 45 | const exampleComponentName = EXAMPLE_COMPONENT_PREFIX + i 46 | 47 | const fileExt = (parsedMeta.ext || lang).replace(/^\.+/, '') 48 | 49 | const filename = toPOSIX( 50 | [ 51 | parentId.replace(path.extname(parentId), ''), 52 | `${exampleComponentName}.${fileExt}`, 53 | ].join('-'), 54 | ) 55 | 56 | const layout = toPOSIX(parsedMeta.layout || options.layout) 57 | const layoutName = layout === options.layout ? 'Example' : `Example${i}` 58 | 59 | const wrapper = toPOSIX(parsedMeta.wrapper || options.wrapper) 60 | const wrapperFilename = wrapper 61 | ? filename.replace(`.${fileExt}`, `.w.${fileExt}`) 62 | : null 63 | 64 | examples.push({ filename, src }) 65 | 66 | virtualFiles.set(filename, { 67 | src, 68 | parent: parentId, 69 | updated: Date.now(), 70 | }) 71 | 72 | if (wrapper) { 73 | virtualFiles.set(wrapperFilename, { 74 | src: createWrapperSrc({ 75 | lang, 76 | inner: filename, 77 | outer: wrapper, 78 | }), 79 | parent: parentId, 80 | updated: Date.now(), 81 | }) 82 | } 83 | 84 | ensureImport(tree, { 85 | default: true, 86 | name: exampleComponentName, 87 | from: wrapper ? wrapperFilename : filename, 88 | }) 89 | 90 | ensureImport(tree, { 91 | from: layout, 92 | name: layoutName, 93 | default: true, 94 | }) 95 | 96 | const codeNode = { ...node } 97 | 98 | const props = { 99 | ...(options?.defaultProps ?? {}), 100 | ...parsePropsFromString(meta), 101 | } 102 | 103 | const commonPropAttributes = [ 104 | { 105 | type: 'mdxJsxAttribute', 106 | name: 'code', 107 | value: node.value, 108 | }, 109 | { 110 | type: 'mdxJsxAttribute', 111 | name: 'lang', 112 | value: lang, 113 | }, 114 | // filename of the markdown file 115 | { 116 | type: 'mdxJsxAttribute', 117 | name: 'filename', 118 | value: mdFilename, 119 | }, 120 | ] 121 | 122 | const componentPropAttributes = [ 123 | ...commonPropAttributes, 124 | ...propsToJSXAttributes(props), 125 | ] 126 | 127 | const layoutPropAttributes = [ 128 | ...commonPropAttributes, 129 | { 130 | type: 'mdxJsxAttribute', 131 | name: 'componentProps', 132 | value: { 133 | type: 'mdxJsxAttributeValueExpression', 134 | data: { 135 | estree: { 136 | type: 'Program', 137 | body: [ 138 | { 139 | type: 'ExpressionStatement', 140 | expression: valueToEstreeExpression( 141 | // filter out client:xyz props 142 | Object.entries(props).reduce((acc, [key, value]) => { 143 | if (key.startsWith('client:')) { 144 | return acc 145 | } 146 | 147 | acc[key] = value 148 | 149 | return acc 150 | }, {}), 151 | ), 152 | }, 153 | ], 154 | sourceType: 'module', 155 | }, 156 | }, 157 | }, 158 | }, 159 | ] 160 | 161 | node = { 162 | type: 'mdxJsxFlowElement', 163 | name: layoutName, 164 | data: { _mdxExplicitJsx: true, _example: true }, 165 | attributes: layoutPropAttributes, 166 | children: [ 167 | { 168 | type: 'mdxJsxFlowElement', 169 | name: 'slot', 170 | data: { _mdxExplicitJsx: true }, 171 | attributes: [ 172 | { 173 | type: 'mdxJsxAttribute', 174 | name: 'slot', 175 | value: 'example', 176 | }, 177 | ], 178 | children: [ 179 | { 180 | type: 'mdxJsxFlowElement', 181 | name: exampleComponentName, 182 | attributes: componentPropAttributes, 183 | children: [], 184 | }, 185 | ], 186 | }, 187 | { 188 | type: 'mdxJsxFlowElement', 189 | name: 'slot', 190 | data: { _mdxExplicitJsx: true }, 191 | attributes: [ 192 | { 193 | type: 'mdxJsxAttribute', 194 | name: 'slot', 195 | value: 'code', 196 | }, 197 | ], 198 | children: [codeNode], 199 | }, 200 | ], 201 | } 202 | 203 | parent.children.splice(childIndex, 1, node) 204 | } 205 | }) 206 | } 207 | } 208 | 209 | function ensureImport(tree, imp) { 210 | let importExists = false 211 | 212 | unistVisit(tree, 'mdxjsEsm', (node) => { 213 | estreeVisit(node.data.estree, (node) => { 214 | if (node.type === 'ImportDeclaration') { 215 | if (node.source.value === imp.from) { 216 | if ( 217 | imp.default && 218 | node.specifiers.find((s) => s.local.name === imp.default) 219 | ) { 220 | importExists = true 221 | } 222 | if ( 223 | imp.name && 224 | node.specifiers.find((s) => s.imported.name === imp.name) 225 | ) { 226 | importExists = true 227 | } 228 | } 229 | } 230 | }) 231 | }) 232 | 233 | if (!importExists) { 234 | tree.children.push({ 235 | type: 'mdxjsEsm', 236 | data: { 237 | estree: { 238 | type: 'Program', 239 | sourceType: 'module', 240 | body: [ 241 | { 242 | type: 'ImportDeclaration', 243 | specifiers: [ 244 | { 245 | type: imp.default 246 | ? 'ImportDefaultSpecifier' 247 | : 'ImportSpecifier', 248 | imported: { 249 | type: 'Identifier', 250 | name: imp.name, 251 | }, 252 | local: { 253 | type: 'Identifier', 254 | name: imp.name, 255 | }, 256 | }, 257 | ], 258 | source: { 259 | type: 'Literal', 260 | value: imp.from, 261 | }, 262 | }, 263 | ], 264 | }, 265 | }, 266 | }) 267 | } 268 | } 269 | 270 | function propsToJSXAttributes(props) { 271 | return Object.entries(props ?? {}).map(([key, value]) => { 272 | const type = typeof value 273 | const isArray = Array.isArray(value) 274 | 275 | switch (type) { 276 | case 'string': 277 | case 'number': 278 | case 'boolean': 279 | return { 280 | type: 'mdxJsxAttribute', 281 | name: key, 282 | value, 283 | } 284 | case 'object': 285 | if (isArray) { 286 | return { 287 | type: 'mdxJsxAttribute', 288 | name: key, 289 | value: { 290 | type: 'mdxJsxAttributeValueExpression', 291 | data: { 292 | estree: { 293 | type: 'Program', 294 | body: [ 295 | { 296 | type: 'ExpressionStatement', 297 | expression: valueToEstreeExpression(value), 298 | }, 299 | ], 300 | sourceType: 'module', 301 | }, 302 | }, 303 | }, 304 | } 305 | } else { 306 | return { 307 | type: 'mdxJsxAttribute', 308 | name: key, 309 | value: { 310 | type: 'mdxJsxAttributeValueExpression', 311 | data: { 312 | estree: { 313 | type: 'Program', 314 | body: [ 315 | { 316 | type: 'ExpressionStatement', 317 | expression: valueToEstreeExpression(value), 318 | }, 319 | ], 320 | sourceType: 'module', 321 | }, 322 | }, 323 | }, 324 | } 325 | } 326 | default: 327 | throw new Error(`Unsupported prop type: ${type}`) 328 | } 329 | }, []) 330 | } 331 | 332 | function valueToEstreeExpression(value) { 333 | const type = typeof value 334 | const isArray = Array.isArray(value) 335 | 336 | switch (type) { 337 | case 'string': 338 | case 'number': 339 | case 'boolean': 340 | return { 341 | type: 'Literal', 342 | value, 343 | } 344 | case 'object': 345 | if (isArray) { 346 | return { 347 | type: 'ArrayExpression', 348 | elements: value.map(valueToEstreeExpression), 349 | } 350 | } else { 351 | return { 352 | type: 'ObjectExpression', 353 | properties: Object.entries(value).map(([key, value]) => { 354 | return { 355 | type: 'Property', 356 | kind: 'init', 357 | key: { 358 | type: 'Identifier', 359 | name: key, 360 | }, 361 | value: valueToEstreeExpression(value), 362 | } 363 | }), 364 | } 365 | } 366 | } 367 | } 368 | 369 | function parseMeta(meta) { 370 | return meta.split(' ').reduce((acc, part) => { 371 | let [key, value] = part.split('=') 372 | 373 | if (value) { 374 | if ( 375 | (value.startsWith('"') && value.endsWith('"')) || 376 | (value.startsWith("'") && value.endsWith("'")) 377 | ) { 378 | value = value.slice(1, -1) 379 | } else if (value.startsWith('{') && value.endsWith('}')) { 380 | value = JSON.parse(value) 381 | } else if (value === 'true' || value === 'false') { 382 | value = value === 'true' 383 | } 384 | } else { 385 | value = true 386 | } 387 | 388 | acc[key] = value 389 | return acc 390 | }, {}) 391 | } 392 | 393 | function getLang(node) { 394 | if (node.lang === 'diff') { 395 | const { lang } = parseMeta(node.meta) 396 | 397 | if (lang) { 398 | return lang 399 | } 400 | } 401 | 402 | return node.lang 403 | } 404 | 405 | function parsePropsFromString(string) { 406 | const regex = /props={{(.*?)}}/g 407 | const matches = [] 408 | let match 409 | 410 | while ((match = regex.exec(string)) !== null) { 411 | matches.push(match[1]) 412 | } 413 | 414 | // Check if a match is found 415 | if (matches.length) { 416 | // Use the Function constructor to create a function that returns the "props" object 417 | const getProps = new Function(`return {${matches.join(' ')}}`) 418 | 419 | // Call the function to get the parsed "props" object 420 | const props = getProps() 421 | 422 | return props 423 | } else { 424 | // Return null if no match is found 425 | return null 426 | } 427 | } 428 | 429 | function toPOSIX(path) { 430 | if (!path) return path 431 | 432 | return path.replace(/\\/g, '/') 433 | } 434 | 435 | function createWrapperSrc({ lang, inner, outer }) { 436 | switch (lang) { 437 | case 'svelte': 438 | return `\ 439 | 443 | 444 | 445 | 446 | ` 447 | case 'jsx': 448 | case 'tsx': 449 | return `\ 450 | import Inner from ${JSON.stringify(inner)} 451 | import Outer from ${JSON.stringify(outer)} 452 | 453 | export default (props) => ` 454 | case 'vue': 455 | // TODO: verify, i just used copilot for this 456 | return `\ 457 |