Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/content/6.integrations/.navigation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
title: Integrations
icon: i-lucide-puzzle
126 changes: 126 additions & 0 deletions docs/content/6.integrations/1.vite-devtools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
title: Vite DevTools
description: Embed the Comark playground in Vite DevTools for any Vite-based project — Vue, React, Svelte, or plain Vite.
navigation:
icon: i-simple-icons-vite
links:
- label: Vite DevTools
icon: i-simple-icons-vite
to: https://devtools.vite.dev
target: _blank
color: neutral
variant: soft
- label: Playground
icon: i-lucide-play
to: /play
color: neutral
variant: soft
---

Comark ships a lightweight Vite plugin that registers the [Comark Playground](https://comark.dev/play) as a panel inside [Vite DevTools](https://devtools.vite.dev/). It works with **any** Vite-based project — Vue, React, Svelte, or a plain Vite app.

::callout{icon="i-lucide-info" color="info"}
Vite DevTools requires **Vite 8+** and the `@vitejs/devtools` package.
::

## Installation

Install `@vitejs/devtools` as a dev dependency:

::code-group
```bash [pnpm]
pnpm add -D @vitejs/devtools
```
```bash [npm]
npm install -D @vitejs/devtools
```
```bash [yarn]
yarn add -D @vitejs/devtools
```
```bash [bun]
bun add -D @vitejs/devtools
```
::

## Usage

### Standalone (any framework)

Import `comarkDevtools` from `comark/vite` and add it alongside `DevTools()`:

```ts [vite.config.ts]
import { DevTools } from '@vitejs/devtools'
import { comarkDevtools } from 'comark/vite'
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
DevTools(),
comarkDevtools(),
// ... your framework plugin (vue(), react(), etc.)
],
})
```

### With `@comark/vue`

The `@comark/vue/vite` plugin **already includes** devtools support — no extra plugin needed. Just add `DevTools()`:

```ts [vite.config.ts]
import { DevTools } from '@vitejs/devtools'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import comark from '@comark/vue/vite'

export default defineConfig({
plugins: [
DevTools(),
vue(),
comark(), // devtools panel included automatically
],
})
```

### React

```ts [vite.config.ts]
import { DevTools } from '@vitejs/devtools'
import { comarkDevtools } from 'comark/vite'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
plugins: [
DevTools(),
react(),
comarkDevtools(),
],
})
```

### Svelte

```ts [vite.config.ts]
import { DevTools } from '@vitejs/devtools'
import { comarkDevtools } from 'comark/vite'
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'

export default defineConfig({
plugins: [
DevTools(),
svelte(),
comarkDevtools(),
],
})
```

## What You Get

Once enabled, a **Comark** icon appears in the Vite DevTools dock. Clicking it opens the full [Comark Playground](https://comark.dev/play) in an embedded panel where you can:

- Write markdown with component syntax
- See the rendered preview in real-time
- Inspect the parsed Comark AST
- View the formatted/stringified output
- Test roundtrip fidelity (parse → stringify → compare)
1 change: 1 addition & 0 deletions examples/2.vite/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"vue-router": "catalog:"
},
"devDependencies": {
"@vitejs/devtools": "catalog:",
"@vitejs/plugin-vue": "catalog:",
"autoprefixer": "catalog:",
"postcss": "catalog:",
Expand Down
2 changes: 2 additions & 0 deletions examples/2.vite/vue/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DevTools } from '@vitejs/devtools'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
Expand All @@ -6,6 +7,7 @@ import { fileURLToPath } from 'node:url'

export default defineConfig({
plugins: [
DevTools(),
vue(),
comark(),
ui({
Expand Down
138 changes: 71 additions & 67 deletions packages/comark-vue/src/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join, basename } from 'node:path'
import type { Plugin, ResolvedConfig } from 'vite'
import type { ElementNode, DirectiveNode, TransformContext } from '@vue/compiler-core'
import { existsSync } from 'node:fs'
import { comarkDevtools } from 'comark/vite'

const runtimeDir = fileURLToPath(new URL('./utils', import.meta.url))

Expand Down Expand Up @@ -112,7 +113,7 @@ function generateComponentsModule(files: string[]): string {
* })
* ```
*/
export default function comark(opts: { prose?: boolean } = {}): Plugin {
export default function comark(opts: { prose?: boolean } = {}): Plugin[] {
let proseDir: string
let proseFilesCache: string[] | null = null

Expand All @@ -131,71 +132,74 @@ export default function comark(opts: { prose?: boolean } = {}): Plugin {
}
}

return {
name: 'comark-vue',
enforce: 'pre',

configResolved(config: ResolvedConfig) {
if (existsSync(join(config.root, 'src', 'components', 'prose'))) {
proseDir = join(config.root, 'src', 'components', 'prose')
}
if (existsSync(join(config.root, 'components', 'prose'))) {
proseDir = join(config.root, 'components', 'prose')
}

const vuePlugin = config.plugins.find(p => p.name === 'vite:vue')
if (!vuePlugin) {
console.warn('[comark-vue] @vitejs/plugin-vue not found. Make sure vue() is in your plugins.')
return
}

const vueOptions = (vuePlugin as any).api?.options
if (!vueOptions) return

vueOptions.template ??= {}
vueOptions.template.compilerOptions ??= {}
vueOptions.template.compilerOptions.nodeTransforms ??= []
vueOptions.template.compilerOptions.nodeTransforms.unshift(viteComarkSlot)
},

resolveId(id) {
if (id === VIRTUAL_COMPONENTS_ID) return RESOLVED_COMPONENTS_ID
return [
{
name: 'comark-vue',
enforce: 'pre',

configResolved(config: ResolvedConfig) {
if (existsSync(join(config.root, 'src', 'components', 'prose'))) {
proseDir = join(config.root, 'src', 'components', 'prose')
}
if (existsSync(join(config.root, 'components', 'prose'))) {
proseDir = join(config.root, 'components', 'prose')
}

const vuePlugin = config.plugins.find(p => p.name === 'vite:vue')
if (!vuePlugin) {
console.warn('[comark-vue] @vitejs/plugin-vue not found. Make sure vue() is in your plugins.')
return
}

const vueOptions = (vuePlugin as any).api?.options
if (!vueOptions) return

vueOptions.template ??= {}
vueOptions.template.compilerOptions ??= {}
vueOptions.template.compilerOptions.nodeTransforms ??= []
vueOptions.template.compilerOptions.nodeTransforms.unshift(viteComarkSlot)
},

resolveId(id) {
if (id === VIRTUAL_COMPONENTS_ID) return RESOLVED_COMPONENTS_ID
},

async load(id) {
if (id !== RESOLVED_COMPONENTS_ID) return
const files = await resolveProseFiles()
return generateComponentsModule(files)
},

async transform(code) {
if (!code.includes('createApp') || !code.includes('.mount(')) return null
// Skip if already injected
if (code.includes(VIRTUAL_COMPONENTS_ID)) return null

const files = await resolveProseFiles()
if (!files.length) return null

const injected = `import __comarkProse from ${JSON.stringify(VIRTUAL_COMPONENTS_ID)};\n`
+ code.replace(/\.mount\s*\(/, '.use(__comarkProse).mount(')

return { code: injected, map: null }
},

configureServer(server) {
if (opts.prose === false) return null
if (!proseDir) return

server.watcher.add(proseDir)

const invalidate = (file: string) => {
invalidateCache(file)
const mod = server.moduleGraph.getModuleById(RESOLVED_COMPONENTS_ID)
if (mod) server.moduleGraph.invalidateModule(mod)
}

server.watcher.on('add', invalidate)
server.watcher.on('unlink', invalidate)
},
},

async load(id) {
if (id !== RESOLVED_COMPONENTS_ID) return
const files = await resolveProseFiles()
return generateComponentsModule(files)
},

async transform(code) {
if (opts.prose === false) return null
if (!code.includes('createApp') || !code.includes('.mount(')) return null
// Skip if already injected
if (code.includes(VIRTUAL_COMPONENTS_ID)) return null

const files = await resolveProseFiles()
if (!files.length) return null

const injected = `import __comarkProse from ${JSON.stringify(VIRTUAL_COMPONENTS_ID)};\n`
+ code.replace(/\.mount\s*\(/, '.use(__comarkProse).mount(')

return { code: injected, map: null }
},

configureServer(server) {
if (!proseDir) return

server.watcher.add(proseDir)

const invalidate = (file: string) => {
invalidateCache(file)
const mod = server.moduleGraph.getModuleById(RESOLVED_COMPONENTS_ID)
if (mod) server.moduleGraph.invalidateModule(mod)
}

server.watcher.on('add', invalidate)
server.watcher.on('unlink', invalidate)
},
}
comarkDevtools(),
]
}
8 changes: 7 additions & 1 deletion packages/comark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
],
"exports": {
".": "./dist/index.js",
"./vite": "./dist/vite.js",
"./plugins/*": "./dist/plugins/*.js",
"./utils": "./dist/utils/index.js",
"./parse": "./dist/parse.js",
Expand Down Expand Up @@ -50,7 +51,8 @@
"peerDependencies": {
"beautiful-mermaid": "catalog:",
"katex": "catalog:",
"shiki": "catalog:"
"shiki": "catalog:",
"vite": "catalog:"
},
"peerDependenciesMeta": {
"shiki": {
Expand All @@ -61,6 +63,9 @@
},
"katex": {
"optional": true
},
"vite": {
"optional": true
}
},
"devDependencies": {
Expand All @@ -76,6 +81,7 @@
"mitata": "catalog:",
"tsx": "catalog:",
"twoslash": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
},
"dependencies": {
Expand Down
40 changes: 40 additions & 0 deletions packages/comark/src/vite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Plugin } from 'vite'

const COMARK_LIGHT_ICON = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 212 212"><path stroke="#000" stroke-width="8" fill="none" d="M200.4 52.5v110H10.4v-110h190z"/><path fill="#000" d="M129.4 94.8V75.5h19.9v19.3h-19.9zm0 44.7v-19.2h19.9v19.2h-19.9zm30.1-44.7V75.5h19.9v19.3h-19.9zm0 44.7v-19.2h19.9v19.2h-19.9zM31.4 141.5v-68h20l20 25 20-25h20v68h-20v-39l-20 25-20-25v39h-20z"/></svg>')}`
const COMARK_DARK_ICON = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 212 212"><path stroke="#fff" stroke-width="8" fill="none" d="M200.4 52.5v110H10.4v-110h190z"/><path fill="#fff" d="M129.4 94.8V75.5h19.9v19.3h-19.9zm0 44.7v-19.2h19.9v19.2h-19.9zm30.1-44.7V75.5h19.9v19.3h-19.9zm0 44.7v-19.2h19.9v19.2h-19.9zM31.4 141.5v-68h20l20 25 20-25h20v68h-20v-39l-20 25-20-25v39h-20z"/></svg>')}`

/**
* Vite plugin that registers the Comark playground in Vite DevTools.
*
* Works with any framework (Vue, React, Svelte, etc.) — just add it to
* your Vite config alongside `@vitejs/devtools`.
*
* @example
* ```ts
* import { DevTools } from '@vitejs/devtools'
* import { comarkDevtools } from 'comark/vite'
*
* export default defineConfig({
* plugins: [DevTools(), comarkDevtools()],
* })
* ```
*/
export function comarkDevtools(): Plugin {
return {
name: 'comark:devtools',
devtools: {
setup(ctx) {
ctx.docks.register({
id: 'comark',
title: 'Comark',
icon: {
light: COMARK_LIGHT_ICON,
dark: COMARK_DARK_ICON,
},
type: 'iframe',
url: 'https://comark.dev/play',
})
},
},
}
}
Loading
Loading