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 |
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 |
44 |
45 |
46 | ```
47 | ````
48 |
49 | becomes
50 |
51 |
52 | ```vue live
53 |
54 |
55 |
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 |