├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ ├── publish.yml │ └── ci.yml ├── packages ├── vue │ ├── src │ │ ├── index.ts │ │ ├── repl │ │ │ ├── index.ts │ │ │ └── composables │ │ │ │ ├── index.ts │ │ │ │ └── usePrompt │ │ │ │ └── index.ts │ │ └── composables │ │ │ ├── index.ts │ │ │ └── usePrompt │ │ │ └── index.ts │ ├── tsdown.config.ts │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── utils │ ├── src │ │ ├── transformers │ │ │ ├── typescript │ │ │ │ ├── index.ts │ │ │ │ ├── transform.ts │ │ │ │ └── transform.test.ts │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ ├── shared.ts │ │ │ │ └── moduleCompiler.ts │ │ ├── index.ts │ │ ├── vue-sfc │ │ │ └── index.ts │ │ ├── to-md │ │ │ └── index.ts │ │ └── from-md │ │ │ ├── index.test.ts │ │ │ └── index.ts │ ├── tsconfig.json │ ├── tsdown.config.ts │ ├── README.md │ └── package.json └── core │ ├── src │ ├── browser.ts │ ├── render-browser │ │ ├── testdata │ │ │ ├── simple.velin.md │ │ │ ├── simple.velin.vue │ │ │ ├── script-setup.velin.md │ │ │ ├── script-setup.velin.vue │ │ │ └── script-setup-with-props.velin.vue │ │ ├── markdown.ts │ │ ├── markdown.browser.test.ts │ │ ├── index.ts │ │ ├── sfc.browser.test.ts │ │ └── sfc.ts │ ├── render-node │ │ ├── testdata │ │ │ ├── simple.velin.md │ │ │ ├── simple.velin.vue │ │ │ ├── script-setup.velin.md │ │ │ ├── script-setup.ts.velin.md │ │ │ ├── script-setup.velin.vue │ │ │ ├── script-setup.ts.velin.vue │ │ │ ├── script-setup-with-props.velin.md │ │ │ ├── script-setup-with-props.ts.velin.md │ │ │ ├── script-setup-with-props.velin.vue │ │ │ └── script-setup-with-props.ts.velin.vue │ │ ├── markdown.ts │ │ ├── index.ts │ │ ├── sfc.ts │ │ ├── markdown.test.ts │ │ └── sfc.test.ts │ ├── render-shared │ │ ├── index.ts │ │ ├── template.ts │ │ ├── sfc.ts │ │ ├── compile.ts │ │ ├── component.ts │ │ ├── props.ts │ │ └── props.test.ts │ ├── render-repl │ │ ├── markdown.ts │ │ ├── index.ts │ │ └── sfc.ts │ ├── types │ │ └── index.ts │ └── index.ts │ ├── tsconfig.json │ ├── tsdown.config.ts │ ├── vitest.config.ts │ ├── README.md │ └── package.json ├── docs ├── assets │ ├── dark-playground.png │ └── light-playground.png └── public │ └── logo.svg ├── apps └── playground │ ├── public │ ├── favicon.ico │ ├── favicon.png │ ├── apple-touch-icon.png │ ├── web-app-manifest-192x192.png │ ├── web-app-manifest-512x512.png │ ├── site.webmanifest │ └── favicon.svg │ ├── netlify.toml │ ├── uno.config.ts │ ├── src │ ├── prompts │ │ ├── Prompt.velin.vue │ │ └── PromptWithArray.velin.vue │ ├── components │ │ ├── Editor │ │ │ ├── monaco │ │ │ │ └── utils.ts │ │ │ ├── highlight.ts │ │ │ ├── utils.ts │ │ │ ├── import-map.ts │ │ │ ├── sourcemap.ts │ │ │ ├── index.vue │ │ │ ├── env.ts │ │ │ ├── vue.worker.ts │ │ │ └── transform.ts │ │ ├── Input.vue │ │ ├── Switch.vue │ │ ├── ArrayInput.vue │ │ └── Playground.vue │ ├── main.ts │ ├── styles │ │ └── themes.css │ ├── utils │ │ └── vue-repl.ts │ ├── pages │ │ └── index.vue │ ├── types │ │ └── vue-repl.ts │ └── App.vue │ ├── tsconfig.json │ ├── vite.config.ts │ ├── index.html │ └── package.json ├── examples ├── vite-browser │ ├── src │ │ ├── assets │ │ │ ├── Promptv2.velin.vue │ │ │ ├── task.ts │ │ │ ├── Markdown.velin.md │ │ │ ├── TaskMarkdown.ts │ │ │ ├── Prompt.velin.vue │ │ │ └── vue.svg │ │ ├── main.ts │ │ ├── types │ │ │ └── index.ts │ │ └── App.vue │ ├── shim.d.ts │ ├── index.html │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ └── public │ │ └── vite.svg └── native-node │ ├── src │ ├── assets │ │ ├── task.ts │ │ ├── composable.md │ │ ├── markdown.md │ │ └── MyComponent.vue │ ├── sfc.ts │ └── md.ts │ ├── package.json │ └── tsconfig.json ├── bump.config.ts ├── .editorconfig ├── vitest.config.ts ├── cspell.config.yaml ├── .vscode ├── extensions.json └── settings.json ├── tsconfig.json ├── pnpm-workspace.yaml ├── LICENSE ├── eslint.config.ts ├── package.json ├── .gitignore ├── README.md └── uno.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [luoling8192, nekomeowww] 2 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './composables' 2 | -------------------------------------------------------------------------------- /packages/vue/src/repl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './composables' 2 | -------------------------------------------------------------------------------- /packages/vue/src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './usePrompt' 2 | -------------------------------------------------------------------------------- /packages/vue/src/repl/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './usePrompt' 2 | -------------------------------------------------------------------------------- /packages/utils/src/transformers/typescript/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transform' 2 | -------------------------------------------------------------------------------- /packages/core/src/browser.ts: -------------------------------------------------------------------------------- 1 | export * from './render-browser' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/simple.velin.md: -------------------------------------------------------------------------------- 1 |
2 |

Hello, world!

3 |
4 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/simple.velin.md: -------------------------------------------------------------------------------- 1 |
2 |

Hello, world!

3 |
4 | -------------------------------------------------------------------------------- /docs/assets/dark-playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeru-ai/velin/HEAD/docs/assets/dark-playground.png -------------------------------------------------------------------------------- /packages/utils/src/transformers/vue/index.ts: -------------------------------------------------------------------------------- 1 | export * from './moduleCompiler' 2 | export * from './shared' 3 | -------------------------------------------------------------------------------- /apps/playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeru-ai/velin/HEAD/apps/playground/public/favicon.ico -------------------------------------------------------------------------------- /apps/playground/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeru-ai/velin/HEAD/apps/playground/public/favicon.png -------------------------------------------------------------------------------- /docs/assets/light-playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeru-ai/velin/HEAD/docs/assets/light-playground.png -------------------------------------------------------------------------------- /examples/vite-browser/src/assets/Promptv2.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /apps/playground/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeru-ai/velin/HEAD/apps/playground/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/simple.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/simple.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /apps/playground/public/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeru-ai/velin/HEAD/apps/playground/public/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /apps/playground/public/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeru-ai/velin/HEAD/apps/playground/public/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /bump.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'bumpp' 2 | 3 | export default defineConfig({ 4 | all: true, 5 | push: false, 6 | recursive: true, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/core/src/render-shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compile' 2 | export * from './component' 3 | export * from './props' 4 | export * from './template' 5 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | console.warn('import either @velin-dev/utils/vue-sfc, @velin-dev/utils/to-md, @velin-dev/utils/from-md, @velin-dev/utils/import') 2 | -------------------------------------------------------------------------------- /examples/vite-browser/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import App from './App.vue' 4 | 5 | import 'virtual:uno.css' 6 | 7 | createApp(App).mount('#app') 8 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup.velin.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Count: {{ count }} 8 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/script-setup.velin.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Count: {{ count }} 8 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup.ts.velin.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Count: {{ count }} 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /examples/vite-browser/shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md' { 2 | import type { DefineComponent } from 'vue' 3 | 4 | const component: DefineComponent 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | projects: [ 6 | 'examples/*', 7 | 'packages/*', 8 | ], 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /examples/native-node/src/assets/task.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export function useTask() { 4 | const task = ref('say hello') 5 | const result = ref('') 6 | 7 | return { 8 | task, 9 | result, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/vite-browser/src/assets/task.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export function useTask() { 4 | const task = ref('say hello') 5 | const result = ref('') 6 | 7 | return { 8 | task, 9 | result, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /packages/utils/src/vue-sfc/index.ts: -------------------------------------------------------------------------------- 1 | export function createSFC(html: string, scriptContent: string, lang: string): string { 2 | return `\n 3 | ` 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/script-setup.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup.ts.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /apps/playground/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "/" 3 | command = "pnpm run build" 4 | publish = "/apps/playground/dist" 5 | 6 | [build.environment] 7 | NODE_VERSION = "23" 8 | 9 | [[redirects]] 10 | from = "/*" 11 | to = "/index.html" 12 | status = 200 13 | force = false 14 | -------------------------------------------------------------------------------- /examples/vite-browser/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface CompoText { 2 | type: 'text' 3 | value?: string 4 | } 5 | 6 | export interface CompoBool { 7 | type: 'switch' 8 | value?: boolean 9 | } 10 | 11 | export type Component = (CompoText | CompoBool) & { 12 | title: string 13 | } 14 | -------------------------------------------------------------------------------- /packages/vue/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: { 5 | 'index': './src/index.ts', 6 | 'repl/index': './src/repl/index.ts', 7 | }, 8 | sourcemap: true, 9 | unused: true, 10 | fixedExtension: true, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/vite-browser/src/assets/Markdown.velin.md: -------------------------------------------------------------------------------- 1 | # Prompt template 2 | 3 | 8 | 9 | ## System Prompt 10 | 11 | You are a professional code assistant, please answer the question using {{ props?.language }} language. 12 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/script-setup-with-props.velin.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup-with-props.velin.md: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |

Count: {{ count }}

13 |

{{ props.date }}

14 |
15 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup-with-props.ts.velin.md: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |

Count: {{ count }}

13 |

{{ props.date }}

14 |
15 | -------------------------------------------------------------------------------- /examples/native-node/src/assets/composable.md: -------------------------------------------------------------------------------- 1 | ## Prompt Composable 2 | 3 | 10 | 11 | ## User Prompt 12 | 13 | {{ markdown }} 14 | 15 | ## Task 16 | 17 | {{ task }} 18 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup-with-props.velin.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup-with-props.ts.velin.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /examples/vite-browser/src/assets/TaskMarkdown.ts: -------------------------------------------------------------------------------- 1 | export const taskMarkdown = ` 2 | ## Prompt Composable 3 | 4 | 11 | 12 | ## User Prompt 13 | 14 | {{ markdown }} 15 | 16 | ## Task 17 | 18 | {{ task }} 19 | ` 20 | -------------------------------------------------------------------------------- /apps/playground/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfigs } from 'unocss' 2 | 3 | import { sharedUnoConfig } from '../../uno.config' 4 | 5 | export default mergeConfigs([ 6 | sharedUnoConfig(), 7 | { 8 | rules: [ 9 | ['transition-colors-none', { 10 | 'transition-property': 'color, background-color, border-color, text-color', 11 | 'transition-duration': '0s', 12 | }], 13 | ], 14 | }, 15 | ]) 16 | -------------------------------------------------------------------------------- /examples/vite-browser/src/assets/Prompt.velin.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | words: 2 | - Attributify 3 | - attw 4 | - Ayaka 5 | - bumpp 6 | - catppuccin 7 | - changelogithub 8 | - defu 9 | - frommd 10 | - iconify 11 | - Neko 12 | - nolyfill 13 | - ofetch 14 | - pkgroll 15 | - shiki 16 | - shikijs 17 | - sizecheck 18 | - splitpanes 19 | - taze 20 | - testdata 21 | - tomd 22 | - tsdown 23 | - unocss 24 | - unplugin 25 | - unrteljs 26 | - velin 27 | - vueuse 28 | -------------------------------------------------------------------------------- /packages/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [ 5 | "ESNext" 6 | ], 7 | "module": "ESNext", 8 | "moduleResolution": "bundler", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | "skipLibCheck": true 14 | }, 15 | "include": [ 16 | "src/**/*.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /examples/native-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@velin-dev/example-native-node", 3 | "type": "module", 4 | "private": true, 5 | "description": "Velin Example - Native Node", 6 | "scripts": { 7 | "run:sfc": "tsx src/sfc.ts", 8 | "run:md": "tsx src/md.ts" 9 | }, 10 | "dependencies": { 11 | "@velin-dev/core": "workspace:^", 12 | "vue": "^3.5.25" 13 | }, 14 | "devDependencies": { 15 | "tsx": "^4.21.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/native-node/src/sfc.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | 3 | import { renderSFCString } from '@velin-dev/core' 4 | import { ref } from 'vue' 5 | 6 | async function main() { 7 | const source = await readFile('./src/assets/MyComponent.vue', 'utf-8') 8 | const html = await renderSFCString(source, { language: ref('TypeScript') }) 9 | 10 | // eslint-disable-next-line no-console 11 | console.log(html) 12 | } 13 | 14 | main() 15 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [ 5 | "ESNext" 6 | ], 7 | "module": "ESNext", 8 | "moduleResolution": "bundler", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | "skipLibCheck": true 14 | }, 15 | "include": [ 16 | "src/**/*.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /examples/native-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [ 5 | "ESNext" 6 | ], 7 | "module": "ESNext", 8 | "moduleResolution": "bundler", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | "skipLibCheck": true 14 | }, 15 | "include": [ 16 | "src/**/*.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /examples/vite-browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Velin Example - Vite + Browser 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/playground/src/prompts/Prompt.velin.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "streetsidesoftware.code-spell-checker", 4 | "mikestead.dotenv", 5 | "EditorConfig.EditorConfig", 6 | "usernamehw.errorlens", 7 | "dbaeumer.vscode-eslint", 8 | "antfu.goto-alias", 9 | "lokalise.i18n-ally", 10 | "antfu.iconify", 11 | "yzhang.markdown-all-in-one", 12 | "antfu.unocss", 13 | "Vue.volar", 14 | "vitest.explorer", 15 | "redhat.vscode-yaml" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /examples/native-node/src/md.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | 3 | import { renderMarkdownString } from '@velin-dev/core' 4 | import { ref } from 'vue' 5 | 6 | async function main() { 7 | const markdownString = await readFile('./src/assets/markdown.md', 'utf-8') 8 | const result1 = await renderMarkdownString(markdownString, { language: ref('TypeScript') }) 9 | 10 | // eslint-disable-next-line no-console 11 | console.log(result1) 12 | } 13 | 14 | main() 15 | -------------------------------------------------------------------------------- /packages/core/src/render-shared/template.ts: -------------------------------------------------------------------------------- 1 | import type { SFCDescriptor } from '@vue/compiler-sfc' 2 | 3 | // Check if we can use compile template as inlined render function 4 | // inside 9 | 10 |
11 | 12 | ## System Prompt 13 | 14 | You are a professional code assistant, please answer the question using {{language}} language. 15 | 16 |
17 | 18 | ## User Prompt 19 | 20 | {{userQuestion}} 21 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":dependencyDashboard", 6 | ":semanticPrefixFixDepsChoreOthers", 7 | ":prHourlyLimitNone", 8 | ":prConcurrentLimitNone", 9 | ":ignoreModulesAndTests", 10 | "group:monorepos", 11 | "group:recommended", 12 | "group:allNonMajor", 13 | "replacements:all", 14 | "workarounds:all" 15 | ], 16 | "rangeStrategy": "bump", 17 | "labels": ["dependencies"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/native-node/src/assets/MyComponent.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [ 5 | "ESNext" 6 | ], 7 | "module": "ESNext", 8 | "moduleResolution": "bundler", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | "skipLibCheck": true 14 | }, 15 | "include": [ 16 | "src/**/*.ts" 17 | ], 18 | "vueCompilerOptions": { 19 | "globalTypesPath": "./vue-global-types.d.ts" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: { 5 | 'index': './src/index.ts', 6 | 'render-browser/index': './src/render-browser/index.ts', 7 | 'render-repl/index': './src/render-repl/index.ts', 8 | 'render-node/index': './src/render-node/index.ts', 9 | 'render-shared/index': './src/render-shared/index.ts', 10 | 'browser': './src/browser.ts', 11 | }, 12 | sourcemap: true, 13 | unused: true, 14 | fixedExtension: true, 15 | dts: true, 16 | }) 17 | -------------------------------------------------------------------------------- /apps/playground/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Velin", 3 | "short_name": "Velin", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/monaco/utils.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/69c2ed1dca84132708c3b9a1d0a008e11be2be74/src/monaco/utils.ts 2 | 3 | import type { Uri } from 'monaco-editor-core' 4 | 5 | import { editor } from 'monaco-editor-core' 6 | 7 | export function getOrCreateModel( 8 | uri: Uri, 9 | lang: string | undefined, 10 | value: string, 11 | ) { 12 | const model = editor.getModel(uri) 13 | if (model) { 14 | model.setValue(value) 15 | return model 16 | } 17 | return editor.createModel(value, lang, uri) 18 | } 19 | -------------------------------------------------------------------------------- /packages/utils/src/to-md/index.ts: -------------------------------------------------------------------------------- 1 | import rehypeParse from 'rehype-parse' 2 | import rehypeRemark from 'rehype-remark' 3 | import remarkStringify from 'remark-stringify' 4 | 5 | import { unified } from 'unified' 6 | 7 | export async function toMarkdown(html: string): Promise { 8 | const htmlToMarkdownProcessor = unified() 9 | .use(rehypeParse, { fragment: true }) 10 | .use(rehypeRemark) 11 | .use(remarkStringify, { bullet: '-' }) 12 | 13 | const result = await htmlToMarkdownProcessor.process(html) 14 | return result.toString() 15 | } 16 | -------------------------------------------------------------------------------- /packages/utils/src/transformers/typescript/transform.ts: -------------------------------------------------------------------------------- 1 | import type { Transform } from 'sucrase' 2 | 3 | import { transform } from 'sucrase' 4 | 5 | export function testTs(filename: string | undefined | null) { 6 | // eslint-disable-next-line regexp/no-unused-capturing-group 7 | return !!(filename && /(\.|\b)tsx?$/.test(filename)) 8 | } 9 | 10 | export function transformTS(src: string, isJSX?: boolean) { 11 | return transform(src, { 12 | transforms: ['typescript', ...(isJSX ? (['jsx'] as Transform[]) : [])], 13 | jsxRuntime: 'preserve', 14 | }).code 15 | } 16 | -------------------------------------------------------------------------------- /apps/playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { MotionPlugin } from '@vueuse/motion' 2 | import { createApp } from 'vue' 3 | import { createRouter, createWebHashHistory } from 'vue-router' 4 | import { routes } from 'vue-router/auto-routes' 5 | 6 | import App from './App.vue' 7 | 8 | import '@unocss/reset/tailwind.css' 9 | import 'uno.css' 10 | import './styles/themes.css' 11 | import 'splitpanes/dist/splitpanes.css' 12 | 13 | const router = createRouter({ history: createWebHashHistory(), routes }) 14 | 15 | createApp(App) 16 | .use(router) 17 | .use(MotionPlugin) 18 | .mount('#app') 19 | -------------------------------------------------------------------------------- /packages/utils/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | entry: { 5 | 'index': './src/index.ts', 6 | 'from-md/index': './src/from-md/index.ts', 7 | 'to-md/index': './src/to-md/index.ts', 8 | 'vue-sfc/index': './src/vue-sfc/index.ts', 9 | 'transformers/vue/index': './src/transformers/vue/index.ts', 10 | 'transformers/typescript/index': './src/transformers/typescript/index.ts', 11 | }, 12 | noExternal: [ 13 | 'sucrase', 14 | ], 15 | sourcemap: true, 16 | unused: true, 17 | fixedExtension: true, 18 | dts: true, 19 | }) 20 | -------------------------------------------------------------------------------- /apps/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [ 5 | "ESNext", 6 | "DOM", 7 | "DOM.Iterable" 8 | ], 9 | "module": "ESNext", 10 | "moduleResolution": "bundler", 11 | "types": [ 12 | "vite/client", 13 | "unplugin-vue-router/client" 14 | ], 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "isolatedModules": true, 18 | "verbatimModuleSyntax": true, 19 | "skipLibCheck": true 20 | }, 21 | "include": [ 22 | "src/**/*.ts", 23 | "src/**/*.d.ts", 24 | "src/**/*.mts", 25 | "src/**/*.vue" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/vite-browser/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "resolveJsonModule": true, 7 | "strictNullChecks": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "isolatedModules": true, 14 | "verbatimModuleSyntax": true, 15 | "skipLibCheck": true 16 | }, 17 | "exclude": [ 18 | "**/dist/**", 19 | "node_modules" 20 | ], 21 | "vueCompilerOptions": { 22 | "globalTypesPath": "./vue-global-types.d.ts" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | shellEmulator: true 2 | trustPolicy: no-downgrade 3 | 4 | packages: 5 | - examples/** 6 | - packages/** 7 | - apps/** 8 | 9 | overrides: 10 | array-flatten: npm:@nolyfill/array-flatten@^1.0.44 11 | axios: npm:feaxios@^0.0.23 12 | is-core-module: npm:@nolyfill/is-core-module@^1.0.39 13 | isarray: npm:@nolyfill/isarray@^1.0.44 14 | safe-buffer: npm:@nolyfill/safe-buffer@^1.0.44 15 | safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 16 | side-channel: npm:@nolyfill/side-channel@^1.0.44 17 | string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44 18 | 19 | onlyBuiltDependencies: 20 | - esbuild 21 | - unrs-resolver 22 | -------------------------------------------------------------------------------- /packages/core/src/render-shared/sfc.ts: -------------------------------------------------------------------------------- 1 | import { fromHtml } from 'hast-util-from-html' 2 | 3 | export function normalizeSFCSource(source: string): string { 4 | const hastRoot = fromHtml(source, { fragment: true }) 5 | 6 | const hasTemplate = hastRoot.children.some(node => node.type === 'element' && node.tagName === 'template') 7 | const hasScript = hastRoot.children.some(node => node.type === 'element' && node.tagName === 'script') 8 | 9 | if (!hasTemplate && !source) { 10 | source = `${source}\n` 11 | } 12 | if (!hasScript) { 13 | source = `${source}\n` 14 | } 15 | 16 | return source 17 | } 18 | -------------------------------------------------------------------------------- /apps/playground/src/prompts/PromptWithArray.velin.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | -------------------------------------------------------------------------------- /examples/vite-browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "lib": [ 6 | "DOM", 7 | "ESNext", 8 | "DOM.Iterable", 9 | "DOM.AsyncIterable" 10 | ], 11 | "resolveJsonModule": true, 12 | "types": [ 13 | "vitest", 14 | "vite/client", 15 | "vite-plugin-vue-layouts/client", 16 | "unplugin-vue-router/client" 17 | ], 18 | "allowJs": true, 19 | "strict": true, 20 | "skipLibCheck": true 21 | }, 22 | "vueCompilerOptions": { 23 | "plugins": [ 24 | "@vue-macros/volar/define-models", 25 | "@vue-macros/volar/define-slots" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/utils/src/transformers/vue/shared.ts: -------------------------------------------------------------------------------- 1 | export class File { 2 | compiled = { 3 | js: '', 4 | css: '', 5 | ssr: '', 6 | clientMap: '', 7 | ssrMap: '', 8 | } 9 | 10 | constructor( 11 | public filename: string, 12 | public code = '', 13 | public hidden = false, 14 | ) {} 15 | 16 | get language() { 17 | if (this.filename.endsWith('.vue')) { 18 | return 'vue' 19 | } 20 | if (this.filename.endsWith('.html')) { 21 | return 'html' 22 | } 23 | if (this.filename.endsWith('.css')) { 24 | return 'css' 25 | } 26 | if (this.filename.endsWith('.ts')) { 27 | return 'typescript' 28 | } 29 | return 'javascript' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/markdown.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProp } from '../render-shared/props' 2 | import type { InputProps } from '../types' 3 | 4 | import { fromMarkdown, scriptFrom } from '@velin-dev/utils/from-md' 5 | import { createSFC } from '@velin-dev/utils/vue-sfc' 6 | 7 | import { renderSFCString } from './sfc' 8 | 9 | export async function renderMarkdownString( 10 | source: string, 11 | data?: InputProps, 12 | _basePath?: string, 13 | ): Promise<{ 14 | props: ComponentProp[] 15 | rendered: string 16 | }> { 17 | const html = fromMarkdown(source) 18 | 19 | const { script, template, lang } = scriptFrom(html) 20 | const sfcString = createSFC(template, script, lang) 21 | 22 | return await renderSFCString(sfcString, data) 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | workspace: [ 6 | { 7 | extends: true, 8 | test: { 9 | name: 'node', 10 | environment: 'node', 11 | include: ['**/*.{spec,test}.ts'], 12 | exclude: ['**/*.browser.{spec,test}.ts', '**/node_modules/**'], 13 | }, 14 | }, 15 | { 16 | extends: true, 17 | test: { 18 | name: 'browser', 19 | include: ['**/*.browser.{spec,test}.ts'], 20 | exclude: ['**/node_modules/**'], 21 | browser: { 22 | enabled: true, 23 | provider: 'playwright', 24 | instances: [ 25 | { browser: 'chromium' }, 26 | ], 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /apps/playground/src/styles/themes.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --theme-colors-hue: 270; 3 | --theme-colors-chroma: calc(0.18 + (cos(var(--theme-colors-hue) * 3.14159265 / 180) * 0.04)); 4 | --theme-colors-chroma-50: calc(var(--theme-colors-chroma) * 0.3); 5 | --theme-colors-chroma-100: calc(var(--theme-colors-chroma) * 0.5); 6 | --theme-colors-chroma-200: calc(var(--theme-colors-chroma) * 0.6); 7 | --theme-colors-chroma-300: calc(var(--theme-colors-chroma) * 0.75); 8 | --theme-colors-chroma-400: var(--theme-colors-chroma); 9 | --theme-colors-chroma-600: calc(var(--theme-colors-chroma) * 1.15); 10 | --theme-colors-chroma-700: calc(var(--theme-colors-chroma) * 1.1); 11 | --theme-colors-chroma-800: calc(var(--theme-colors-chroma) * 0.85); 12 | --theme-colors-chroma-900: calc(var(--theme-colors-chroma) * 0.7); 13 | --theme-colors-chroma-950: calc(var(--theme-colors-chroma) * 0.5); 14 | } 15 | -------------------------------------------------------------------------------- /apps/playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | 3 | import { resolve } from 'node:path' 4 | 5 | import Vue from '@vitejs/plugin-vue' 6 | import Unocss from 'unocss/vite' 7 | import VueRouter from 'unplugin-vue-router/vite' 8 | 9 | import { defineConfig } from 'vite' 10 | 11 | export default defineConfig({ 12 | optimizeDeps: { 13 | exclude: ['@vue/repl'], 14 | }, 15 | plugins: [ 16 | // https://github.com/posva/unplugin-vue-router 17 | VueRouter({ 18 | dts: resolve(import.meta.dirname, 'src', 'typed-router.d.ts'), 19 | extensions: ['.vue', '.md'], 20 | }), 21 | Vue({ 22 | script: { 23 | fs: { 24 | fileExists: fs.existsSync, 25 | readFile: file => fs.readFileSync(file, 'utf-8'), 26 | }, 27 | }, 28 | }), 29 | // https://github.com/antfu/unocss 30 | // see uno.config.ts for config 31 | Unocss(), 32 | ], 33 | }) 34 | -------------------------------------------------------------------------------- /packages/core/src/render-node/markdown.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProp } from '../render-shared/props' 2 | import type { InputProps } from '../types' 3 | 4 | import { fromMarkdown, scriptFrom } from '@velin-dev/utils/from-md' 5 | import { toMarkdown } from '@velin-dev/utils/to-md' 6 | import { createSFC } from '@velin-dev/utils/vue-sfc' 7 | 8 | import { renderSFC } from './sfc' 9 | 10 | export async function renderMarkdownString( 11 | source: string, 12 | data?: InputProps, 13 | basePath?: string, 14 | ): Promise<{ 15 | props: ComponentProp[] 16 | rendered: string 17 | }> { 18 | const html = fromMarkdown(source) 19 | 20 | const { script, template, lang } = scriptFrom(html) 21 | const sfcString = createSFC(template, script, lang) 22 | 23 | const { props, rendered } = await renderSFC(sfcString, data, basePath) 24 | const markdownResult = await toMarkdown(rendered) 25 | 26 | return { 27 | props, 28 | rendered: markdownResult, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/render-repl/markdown.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProp } from '../render-shared/props' 2 | import type { InputProps } from '../types' 3 | 4 | import { fromMarkdown, scriptFrom } from '@velin-dev/utils/from-md' 5 | import { toMarkdown } from '@velin-dev/utils/to-md' 6 | import { createSFC } from '@velin-dev/utils/vue-sfc' 7 | 8 | import { renderSFC } from './sfc' 9 | 10 | export async function renderMarkdownString( 11 | source: string, 12 | data?: InputProps, 13 | basePath?: string, 14 | ): Promise<{ 15 | props: ComponentProp[] 16 | rendered: string 17 | }> { 18 | const html = fromMarkdown(source) 19 | 20 | const { script, template, lang } = scriptFrom(html) 21 | const sfcString = createSFC(template, script, lang) 22 | 23 | const { props, rendered } = await renderSFC(sfcString, data, basePath) 24 | const markdownResult = await toMarkdown(rendered) 25 | 26 | return { 27 | props, 28 | rendered: markdownResult, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/vite-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@velin-dev/example-vite-browser", 3 | "type": "module", 4 | "private": true, 5 | "description": "Velin Example - Vite + Browser", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc -b && vite build", 9 | "preview": "vite preview", 10 | "typecheck": "vue-tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "@velin-dev/core": "workspace:^", 14 | "vue": "^3.5.25" 15 | }, 16 | "devDependencies": { 17 | "@unocss/reset": "^66.5.10", 18 | "@velin-dev/vue": "workspace:^", 19 | "@vitejs/plugin-vue": "^6.0.3", 20 | "@vue-macros/volar": "^3.1.1", 21 | "@vue/shared": "^3.5.25", 22 | "@vueuse/core": "^14.1.0", 23 | "@vueuse/motion": "^3.0.3", 24 | "unplugin-vue-macros": "^2.14.5", 25 | "unplugin-vue-markdown": "^29.2.0", 26 | "unplugin-vue-router": "^0.19.0", 27 | "vite-plugin-vue-devtools": "^8.0.5", 28 | "vite-plugin-vue-layouts": "^0.11.0", 29 | "vue-tsc": "^3.1.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter, Reactive } from '@vue/reactivity' 2 | import type { ComponentPropsOptions, DefineComponent, ExtractPropTypes } from '@vue/runtime-core' 3 | import type { LooseRequired } from '@vue/shared' 4 | 5 | export type RenderComponentInputComponent 6 | // eslint-disable-next-line ts/no-empty-object-type 7 | = | DefineComponent 8 | | DefineComponent 9 | | DefineComponent 10 | 11 | export type InputProps 12 | = | T 13 | | MaybeRefOrGetter 14 | | Record> 15 | | Record> 16 | 17 | export type ResolveRenderComponentInputProps> 18 | = P extends ComponentPropsOptions 19 | ? ExtractPropTypes

20 | : P 21 | 22 | export type LooseRequiredRenderComponentInputProps 23 | = LooseRequired> 25 | ? ExtractPropTypes 26 | : T 27 | > & {}> 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/markdown.browser.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | import { dirname, join } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { describe, expect, it } from 'vitest' 6 | 7 | import { renderMarkdownString } from './markdown' 8 | 9 | // TODO: Browser testing 10 | describe.todo('renderMarkdownString', async () => { 11 | it('should be able to render simple SFC', async () => { 12 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'simple.velin.md'), 'utf-8') 13 | const result = await renderMarkdownString(content) 14 | expect(result).toBeDefined() 15 | expect(result).not.toBe('') 16 | expect(result).toBe('# Hello, world!\n') 17 | }) 18 | 19 | it('should be able to render script setup SFC', async () => { 20 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.md'), 'utf-8') 21 | const result = await renderMarkdownString(content) 22 | expect(result).toBeDefined() 23 | expect(result).not.toBe('') 24 | expect(result).toBe('Count: 0\n') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/utils/src/from-md/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { scriptFrom } from '.' 4 | 5 | describe('scriptFrom', () => { 6 | it('should be able to parse', () => { 7 | const result = scriptFrom(` 8 | 13 | `) 14 | 15 | expect(result).toMatchObject({ 16 | template: ` 17 | 22 | `, 23 | }) 24 | }) 25 | 26 | it('should be able to parse with script', () => { 27 | const result = scriptFrom(` 28 | 32 | 33 | 39 | `) 40 | 41 | expect(result).toMatchObject({ 42 | script: ` 43 | import { ref } from 'vue' 44 | const count = ref(0) 45 | `, 46 | template: ` 47 | 48 | 49 | 55 | `, 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/highlight.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/monaco/highlight.ts 2 | 3 | import langJsx from '@shikijs/langs/jsx' 4 | import langTsx from '@shikijs/langs/tsx' 5 | import langVue from '@shikijs/langs/vue' 6 | import themeLight from '@shikijs/themes/catppuccin-latte' 7 | import themeDark from '@shikijs/themes/catppuccin-mocha' 8 | 9 | import { createHighlighterCoreSync } from '@shikijs/core' 10 | import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript' 11 | import { shikiToMonaco } from '@shikijs/monaco' 12 | 13 | import * as monaco from 'monaco-editor-core' 14 | 15 | let registered = false 16 | export function registerHighlighter() { 17 | if (!registered) { 18 | const highlighter = createHighlighterCoreSync({ 19 | themes: [themeDark, themeLight], 20 | langs: [langVue, langTsx, langJsx], 21 | engine: createJavaScriptRegexEngine(), 22 | }) 23 | monaco.languages.register({ id: 'vue' }) 24 | shikiToMonaco(highlighter, monaco) 25 | registered = true 26 | } 27 | 28 | return { 29 | light: themeLight.name!, 30 | dark: themeDark.name!, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/playground/src/utils/vue-repl.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/utils.ts 2 | 3 | import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate' 4 | 5 | export function debounce(fn: (...args: any[]) => any, n = 100) { 6 | let handle: any 7 | return (...args: any[]) => { 8 | if (handle) 9 | clearTimeout(handle) 10 | handle = setTimeout(() => { 11 | fn(...args) 12 | }, n) 13 | } 14 | } 15 | 16 | export function utoa(data: string): string { 17 | const buffer = strToU8(data) 18 | const zipped = zlibSync(buffer, { level: 9 }) 19 | const binary = strFromU8(zipped, true) 20 | return btoa(binary) 21 | } 22 | 23 | export function atou(base64: string): string { 24 | const binary = atob(base64) 25 | 26 | // zlib header (x78), level 9 (xDA) 27 | if (binary.startsWith('\x78\xDA')) { 28 | const buffer = strToU8(binary, true) 29 | const unzipped = unzlibSync(buffer) 30 | return strFromU8(unzipped) 31 | } 32 | 33 | // old unicode hacks for backward compatibility 34 | // https://base64.guru/developers/javascript/examples/unicode-strings 35 | return decodeURIComponent(escape(binary)) 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | concurrency: ${{ github.workflow }}-${{ github.ref }} 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | id-token: write 17 | steps: 18 | - uses: actions/checkout@v6 19 | with: 20 | fetch-depth: 0 21 | - uses: pnpm/action-setup@v4 22 | with: 23 | run_install: false 24 | - uses: actions/setup-node@v6 25 | with: 26 | cache: pnpm 27 | node-version: latest 28 | registry-url: https://registry.npmjs.org 29 | 30 | # npm 11.5.1 or later is required so update to latest to use OIDC trusted publisher 31 | # https://github.com/eslint/config-inspector/pull/174/files 32 | # https://github.com/e18e/ecosystem-issues/issues/201 33 | - run: npm install -g npm@latest 34 | - run: pnpm i 35 | - run: pnpm dlx changelogithub 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | - run: pnpm -r build 39 | - run: pnpm -r publish --no-git-checks --access public 40 | -------------------------------------------------------------------------------- /apps/playground/src/components/Input.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | -------------------------------------------------------------------------------- /packages/utils/src/from-md/index.ts: -------------------------------------------------------------------------------- 1 | import type { Element, Text } from 'hast' 2 | 3 | import markdownIt from 'markdown-it' 4 | 5 | import { fromHtml } from 'hast-util-from-html' 6 | import { select } from 'hast-util-select' 7 | import { toHtml } from 'hast-util-to-html' 8 | import { remove } from 'unist-util-remove' 9 | 10 | export function fromMarkdown(markdownString: string): string { 11 | const md = markdownIt({ html: true }) 12 | return md.render(markdownString) 13 | } 14 | 15 | function asHTMLElement(input?: any): Element | undefined { 16 | return input 17 | } 18 | 19 | function asHTMLText(input?: any): Text | undefined { 20 | return input 21 | } 22 | 23 | export function scriptFrom(html: string): { script: string, template: string, lang: string } { 24 | const hastTree = fromHtml(html, { fragment: true }) 25 | const scriptNode = asHTMLElement(select('script[setup]', hastTree)) 26 | const lang = String(scriptNode?.properties?.lang || 'js') 27 | const script = scriptNode ? asHTMLText(scriptNode.children[0]).value : '' 28 | if (scriptNode) { 29 | remove(hastTree, scriptNode) 30 | } 31 | 32 | const template = toHtml(hastTree) 33 | return { script, template, lang } 34 | } 35 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/utils.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/69c2ed1dca84132708c3b9a1d0a008e11be2be74/src/utils.ts 2 | 3 | import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate' 4 | 5 | // eslint-disable-next-line ts/no-unsafe-function-type 6 | export function debounce(fn: Function, n = 100) { 7 | let handle: any 8 | return (...args: any[]) => { 9 | if (handle) 10 | clearTimeout(handle) 11 | handle = setTimeout(() => { 12 | fn(...args) 13 | }, n) 14 | } 15 | } 16 | 17 | export function utoa(data: string): string { 18 | const buffer = strToU8(data) 19 | const zipped = zlibSync(buffer, { level: 9 }) 20 | const binary = strFromU8(zipped, true) 21 | return btoa(binary) 22 | } 23 | 24 | export function atou(base64: string): string { 25 | const binary = atob(base64) 26 | 27 | // zlib header (x78), level 9 (xDA) 28 | if (binary.startsWith('\x78\xDA')) { 29 | const buffer = strToU8(binary, true) 30 | const unzipped = unzlibSync(buffer) 31 | return strFromU8(unzipped) 32 | } 33 | 34 | // old unicode hacks for backward compatibility 35 | // https://base64.guru/developers/javascript/examples/unicode-strings 36 | return decodeURIComponent(escape(binary)) 37 | } 38 | -------------------------------------------------------------------------------- /apps/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Velin Playground 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 |

23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/core/src/render-node/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProp } from '../render-shared/props' 2 | import type { InputProps } from '../types' 3 | 4 | import { fromHtml } from 'hast-util-from-html' 5 | 6 | import { renderMarkdownString } from './markdown' 7 | import { renderSFCString } from './sfc' 8 | 9 | export { renderComponent, resolveProps } from '../render-shared' 10 | export { renderMarkdownString } from './markdown' 11 | export { renderSFCString } from './sfc' 12 | 13 | export async function render( 14 | source: string, 15 | data?: InputProps, 16 | basePath?: string, 17 | ): Promise<{ 18 | props: ComponentProp[] 19 | rendered: string 20 | }> { 21 | const hastRoot = fromHtml(source, { fragment: true }) 22 | 23 | const hasTemplate = hastRoot.children.some(node => node.type === 'element' && node.tagName === 'template') 24 | const hasScript = hastRoot.children.some(node => node.type === 'element' && node.tagName === 'script') 25 | 26 | if (!hasTemplate && !hasScript && source) { 27 | return await renderMarkdownString(source, data, basePath) 28 | } 29 | if (hasScript && !hasTemplate && source) { 30 | return await renderMarkdownString(source, data, basePath) 31 | } 32 | 33 | return await renderSFCString(source, data, basePath) 34 | } 35 | -------------------------------------------------------------------------------- /packages/vue/README.md: -------------------------------------------------------------------------------- 1 | # `@velin-dev/vue` 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | Refer to [README.md](https://github.com/moeru-ai/velin/blob/main/README.md) for more information. 10 | 11 | ## License 12 | 13 | MIT 14 | 15 | [npm-version-src]: https://img.shields.io/npm/v/@velin-dev/vue?style=flat&colorA=080f12&colorB=1fa669 16 | [npm-version-href]: https://npmjs.com/package/@velin-dev/vue 17 | [npm-downloads-src]: https://img.shields.io/npm/dm/@velin-dev/vue?style=flat&colorA=080f12&colorB=1fa669 18 | [npm-downloads-href]: https://npmjs.com/package/@velin-dev/vue 19 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@velin-dev/vue?style=flat&colorA=080f12&colorB=1fa669&label=minzip 20 | [bundle-href]: https://bundlephobia.com/result?p=@velin-dev/vue 21 | [license-src]: https://img.shields.io/github/license/moeru-ai/velin.svg?style=flat&colorA=080f12&colorB=1fa669 22 | [license-href]: https://github.com/moeru-ai/velin/blob/main/LICENSE 23 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 24 | [jsdocs-href]: https://www.jsdocs.io/package/@velin-dev/vue 25 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # `@velin-dev/core` 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | Refer to [README.md](https://github.com/moeru-ai/velin/blob/main/README.md) for more information. 10 | 11 | ## License 12 | 13 | MIT 14 | 15 | [npm-version-src]: https://img.shields.io/npm/v/@velin-dev/core?style=flat&colorA=080f12&colorB=1fa669 16 | [npm-version-href]: https://npmjs.com/package/@velin-dev/core 17 | [npm-downloads-src]: https://img.shields.io/npm/dm/@velin-dev/core?style=flat&colorA=080f12&colorB=1fa669 18 | [npm-downloads-href]: https://npmjs.com/package/@velin-dev/core 19 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@velin-dev/vue?style=flat&colorA=080f12&colorB=1fa669&label=minzip 20 | [bundle-href]: https://bundlephobia.com/result?p=@velin-dev/vue 21 | [license-src]: https://img.shields.io/github/license/moeru-ai/velin.svg?style=flat&colorA=080f12&colorB=1fa669 22 | [license-href]: https://github.com/moeru-ai/velin/blob/main/LICENSE 23 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 24 | [jsdocs-href]: https://www.jsdocs.io/package/@velin-dev/core 25 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # `@velin-dev/utils` 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | Refer to [README.md](https://github.com/moeru-ai/velin/blob/main/README.md) for more information. 10 | 11 | ## License 12 | 13 | MIT 14 | 15 | [npm-version-src]: https://img.shields.io/npm/v/@velin-dev/utils?style=flat&colorA=080f12&colorB=1fa669 16 | [npm-version-href]: https://npmjs.com/package/@velin-dev/utils 17 | [npm-downloads-src]: https://img.shields.io/npm/dm/@velin-dev/utils?style=flat&colorA=080f12&colorB=1fa669 18 | [npm-downloads-href]: https://npmjs.com/package/@velin-dev/utils 19 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@velin-dev/utils?style=flat&colorA=080f12&colorB=1fa669&label=minzip 20 | [bundle-href]: https://bundlephobia.com/result?p=@velin-dev/utils 21 | [license-src]: https://img.shields.io/github/license/moeru-ai/velin.svg?style=flat&colorA=080f12&colorB=1fa669 22 | [license-href]: https://github.com/moeru-ai/velin/blob/main/LICENSE 23 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 24 | [jsdocs-href]: https://www.jsdocs.io/package/@velin-dev/utils 25 | -------------------------------------------------------------------------------- /packages/core/src/render-repl/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProp } from '../render-shared/props' 2 | import type { InputProps } from '../types' 3 | 4 | import { fromHtml } from 'hast-util-from-html' 5 | 6 | import { renderMarkdownString } from './markdown' 7 | import { renderSFCString } from './sfc' 8 | 9 | export * from './sfc' 10 | 11 | export async function render( 12 | source: string, 13 | data?: InputProps, 14 | basePath?: string, 15 | ): Promise<{ 16 | props: ComponentProp[] 17 | rendered: string 18 | }> { 19 | const hastRoot = fromHtml(source, { fragment: true }) 20 | 21 | const hasTemplate = hastRoot.children.some(node => node.type === 'element' && node.tagName === 'template') 22 | const hasScript = hastRoot.children.some(node => node.type === 'element' && node.tagName === 'script') 23 | 24 | if (!hasTemplate && !hasScript && source) { 25 | return await renderMarkdownString(source, data, basePath) 26 | } 27 | if (hasScript && !hasTemplate && source) { 28 | return await renderMarkdownString(source, data, basePath) 29 | } 30 | if (!hasTemplate && !source) { 31 | source = `${source}\n` 32 | } 33 | if (!hasScript) { 34 | source = `${source}\n` 35 | } 36 | 37 | return await renderSFCString(source, data, basePath) 38 | } 39 | -------------------------------------------------------------------------------- /apps/playground/src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 46 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProp } from './render-shared' 2 | import type { InputProps } from './types' 3 | 4 | import { isNode } from 'std-env' 5 | 6 | import { 7 | renderMarkdownString as renderMarkdownStringBrowser, 8 | renderSFCString as renderSFCStringBrowser, 9 | } from './render-browser' 10 | 11 | export async function renderMarkdownString( 12 | source: string, 13 | data?: InputProps, 14 | basePath?: string, 15 | ): Promise<{ 16 | props: ComponentProp[] 17 | rendered: string 18 | }> { 19 | if (isNode) { 20 | const { renderMarkdownString } = await import('./render-node') 21 | return renderMarkdownString(source, data, basePath) 22 | } 23 | 24 | return renderMarkdownStringBrowser(source, data, basePath) 25 | } 26 | 27 | export async function renderSFCString( 28 | source: string, 29 | data?: InputProps, 30 | basePath?: string, 31 | ): Promise<{ 32 | props: ComponentProp[] 33 | rendered: string 34 | }> { 35 | if (isNode) { 36 | const { renderSFCString } = await import('./render-node') 37 | return renderSFCString(source, data, basePath) 38 | } 39 | 40 | return renderSFCStringBrowser(source, data, basePath) 41 | } 42 | 43 | export { 44 | onlyRender, 45 | onlySetup, 46 | renderComponent, 47 | resolveProps, 48 | } from './render-shared' 49 | export * from './types' 50 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | vue: true, 5 | pnpm: true, 6 | ignores: [ 7 | '**/vue-global-types.d.ts', 8 | ], 9 | rules: { 10 | 'vue/prefer-separate-static-class': 'off', 11 | 'yaml/plain-scalar': 'off', 12 | 'import/order': 'off', 13 | 'antfu/import-dedupe': 'error', 14 | 'style/padding-line-between-statements': 'error', 15 | 'perfectionist/sort-imports': [ 16 | 'error', 17 | { 18 | groups: [ 19 | 'type-builtin', 20 | 'type-import', 21 | 'type-internal', 22 | ['type-parent', 'type-sibling', 'type-index'], 23 | 'default-value-builtin', 24 | 'named-value-builtin', 25 | 'value-builtin', 26 | 'default-value-external', 27 | 'named-value-external', 28 | 'value-external', 29 | 'default-value-internal', 30 | 'named-value-internal', 31 | 'value-internal', 32 | ['default-value-parent', 'default-value-sibling', 'default-value-index'], 33 | ['named-value-parent', 'named-value-sibling', 'named-value-index'], 34 | ['wildcard-value-parent', 'wildcard-value-sibling', 'wildcard-value-index'], 35 | ['value-parent', 'value-sibling', 'value-index'], 36 | 'side-effect', 37 | 'style', 38 | ], 39 | newlinesBetween: 'always', 40 | }, 41 | ], 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /apps/playground/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-browser/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/index.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProp } from '../render-shared/props' 2 | import type { InputProps } from '../types' 3 | 4 | import { fromHtml } from 'hast-util-from-html' 5 | 6 | import { renderMarkdownString } from './markdown' 7 | import { renderSFCString } from './sfc' 8 | 9 | export { renderComponent, resolveProps } from '../render-shared' 10 | export { renderMarkdownString } from './markdown' 11 | export { renderSFCString } from './sfc' 12 | 13 | export async function render( 14 | source: string, 15 | data?: InputProps, 16 | basePath?: string, 17 | ): Promise<{ 18 | props: ComponentProp[] 19 | rendered: string 20 | }> { 21 | const hastRoot = fromHtml(source, { fragment: true }) 22 | 23 | const hasTemplate = hastRoot.children.some(node => node.type === 'element' && node.tagName === 'template') 24 | const hasScript = hastRoot.children.some(node => node.type === 'element' && node.tagName === 'script') 25 | 26 | if (!hasTemplate && !hasScript && source) { 27 | return await renderMarkdownString(source, data, basePath) 28 | } 29 | if (hasScript && !hasTemplate && source) { 30 | return await renderMarkdownString(source, data, basePath) 31 | } 32 | if (!hasTemplate && !source) { 33 | source = `${source}\n` 34 | } 35 | if (!hasScript) { 36 | source = `${source}\n` 37 | } 38 | 39 | return await renderSFCString(source, data, basePath) 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off", "fixable": true }, 15 | { "rule": "format/*", "severity": "off", "fixable": true }, 16 | { "rule": "*-indent", "severity": "off", "fixable": true }, 17 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 18 | { "rule": "*-spaces", "severity": "off", "fixable": true }, 19 | { "rule": "*-order", "severity": "off", "fixable": true }, 20 | { "rule": "*-dangle", "severity": "off", "fixable": true }, 21 | { "rule": "*-newline", "severity": "off", "fixable": true }, 22 | { "rule": "*quotes", "severity": "off", "fixable": true }, 23 | { "rule": "*semi", "severity": "off", "fixable": true } 24 | ], 25 | 26 | // Enable eslint for all supported languages 27 | "eslint.validate": [ 28 | "javascript", 29 | "javascriptreact", 30 | "typescript", 31 | "typescriptreact", 32 | "vue", 33 | "html", 34 | "markdown", 35 | "json", 36 | "json5", 37 | "jsonc", 38 | "yaml", 39 | "toml", 40 | "xml", 41 | "gql", 42 | "graphql", 43 | "astro", 44 | "svelte", 45 | "css", 46 | "less", 47 | "scss", 48 | "pcss", 49 | "postcss" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@velin-dev/vue", 3 | "type": "module", 4 | "version": "0.3.4", 5 | "description": "Develop prompts with Vue SFC or Markdown like pro.", 6 | "author": { 7 | "name": "RainbowBird", 8 | "email": "rbxin2003@outlook.com", 9 | "url": "https://github.com/luoling8192" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Neko Ayaka", 14 | "email": "neko@ayaka.moe", 15 | "url": "https://github.com/nekomeowww" 16 | } 17 | ], 18 | "license": "MIT", 19 | "homepage": "https://github.com/moeru-ai/velin", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/moeru-ai/velin.git", 23 | "directory": "packages/vue" 24 | }, 25 | "bugs": "https://github.com/moeru-ai/velin/issues", 26 | "exports": { 27 | ".": { 28 | "types": "./dist/index.d.mts", 29 | "import": "./dist/index.mjs" 30 | }, 31 | "./repl": { 32 | "types": "./dist/repl/index.d.mts", 33 | "import": "./dist/repl/index.mjs" 34 | } 35 | }, 36 | "main": "./dist/index.mjs", 37 | "module": "./dist/index.mjs", 38 | "types": "./dist/index.d.mts", 39 | "files": [ 40 | "README.md", 41 | "dist", 42 | "package.json" 43 | ], 44 | "scripts": { 45 | "dev": "tsdown", 46 | "stub": "tsdown", 47 | "build": "tsdown", 48 | "typecheck": "tsc --noEmit", 49 | "attw": "attw --pack . --profile esm-only --ignore-rules cjs-resolves-to-esm" 50 | }, 51 | "dependencies": { 52 | "@velin-dev/core": "workspace:^", 53 | "@velin-dev/utils": "workspace:^", 54 | "vue": "^3.5.25" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /apps/playground/src/components/Switch.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 37 | -------------------------------------------------------------------------------- /apps/playground/src/types/vue-repl.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/types.ts 2 | 3 | import type { editor } from 'monaco-editor-core' 4 | import type { Component, InjectionKey, ToRefs } from 'vue' 5 | 6 | import type { Store } from '../components/Editor/store' 7 | 8 | export type EditorMode = 'js' | 'css' | 'ssr' 9 | export interface EditorProps { 10 | value: string 11 | filename: string 12 | readonly?: boolean 13 | mode?: EditorMode 14 | } 15 | export interface EditorEmits { 16 | (e: 'change', code: string): void 17 | } 18 | export type EditorComponentType = Component 19 | 20 | export type OutputModes = 'preview' | EditorMode 21 | 22 | export const injectKeyProps: InjectionKey>> = Symbol('props') 56 | -------------------------------------------------------------------------------- /packages/core/src/render-shared/compile.ts: -------------------------------------------------------------------------------- 1 | /// @vue/repl: https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/transform.ts 2 | 3 | import type { CompilerOptions, SFCScriptBlock, SFCTemplateCompileResults } from '@vue/compiler-sfc' 4 | 5 | import { testTs, transformTS } from '@velin-dev/utils/transformers/typescript' 6 | import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc' 7 | 8 | import { isUseInlineTemplate } from './template' 9 | 10 | export interface CompiledResult { 11 | template: SFCTemplateCompileResults 12 | script: SFCScriptBlock 13 | } 14 | 15 | export async function compileSFC(source: string): Promise { 16 | const { descriptor } = parse(source) 17 | 18 | if (!descriptor.template) { 19 | return await compileSFC(`${source}\n`) 20 | } 21 | 22 | const templateOptions = { 23 | source: descriptor.template.content, 24 | filename: 'temp.vue', 25 | id: `vue-component-${Date.now()}`, 26 | compilerOptions: { runtimeModuleName: 'vue' }, 27 | } 28 | 29 | const templateResult = compileTemplate(templateOptions) 30 | 31 | const scriptLang = descriptor.script?.lang || descriptor.scriptSetup?.lang 32 | const isTS = testTs(scriptLang) 33 | 34 | const expressionPlugins: CompilerOptions['expressionPlugins'] = [] 35 | if (isTS) { 36 | expressionPlugins.push('typescript') 37 | } 38 | 39 | const scriptResult = compileScript(descriptor, { 40 | id: `vue-component-${Date.now()}`, 41 | inlineTemplate: isUseInlineTemplate(descriptor), 42 | ...{ 43 | ...templateOptions, 44 | expressionPlugins, 45 | }, 46 | }) 47 | 48 | if (isTS) { 49 | scriptResult.content = transformTS(scriptResult.content) 50 | } 51 | 52 | return { 53 | template: templateResult, 54 | script: scriptResult, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/src/render-shared/component.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsOptions } from '@vue/runtime-core' 2 | 3 | import type { 4 | InputProps, 5 | LooseRequiredRenderComponentInputProps, 6 | RenderComponentInputComponent, 7 | ResolveRenderComponentInputProps, 8 | } from '../types' 9 | 10 | import { toMarkdown } from '@velin-dev/utils/to-md' 11 | import { toValue } from '@vue/reactivity' 12 | import { renderToString } from '@vue/server-renderer' 13 | import { createSSRApp } from 'vue' 14 | 15 | export function onlySetup< 16 | RawProps = any, 17 | ComponentProps = ComponentPropsOptions, 18 | ResolvedProps = ResolveRenderComponentInputProps, 19 | >( 20 | promptComponent: RenderComponentInputComponent, 21 | props: InputProps, 22 | ) { 23 | return promptComponent.setup?.( 24 | toValue(props) as unknown as LooseRequiredRenderComponentInputProps, 25 | { attrs: {}, slots: {}, emit: () => { }, expose: () => { } }, 26 | ) 27 | } 28 | 29 | export function onlyRender< 30 | RawProps = any, 31 | ComponentProps = ComponentPropsOptions, 32 | ResolvedProps = ResolveRenderComponentInputProps, 33 | >( 34 | promptComponent: RenderComponentInputComponent, 35 | props: InputProps, 36 | ) { 37 | return createSSRApp(promptComponent, toValue(props) as Record) 38 | } 39 | 40 | export function renderComponent< 41 | RawProps = any, 42 | ComponentProps = ComponentPropsOptions, 43 | ResolvedProps = ResolveRenderComponentInputProps, 44 | >( 45 | promptComponent: RenderComponentInputComponent, 46 | props: InputProps, 47 | ) { 48 | return new Promise((resolve, reject) => { 49 | renderToString(onlyRender(promptComponent, props)) 50 | .then(toMarkdown) 51 | .then(resolve) 52 | .catch(reject) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /apps/playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 48 | 49 | 69 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: pnpm/action-setup@v3 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: lts/* 22 | cache: pnpm 23 | 24 | - name: Install 25 | run: pnpm install 26 | 27 | - name: Lint 28 | run: pnpm run lint 29 | 30 | build-test: 31 | name: Build Test 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: pnpm/action-setup@v3 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: lts/* 39 | cache: pnpm 40 | 41 | - name: Install 42 | run: pnpm install 43 | 44 | - name: Build 45 | run: | 46 | pnpm run build 47 | pnpm run attw:packages 48 | 49 | unit-test: 50 | name: Unit Test 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v4 54 | - uses: pnpm/action-setup@v3 55 | - uses: actions/setup-node@v4 56 | with: 57 | node-version: lts/* 58 | cache: pnpm 59 | 60 | - name: Install 61 | run: pnpm install 62 | 63 | - name: Build 64 | run: | 65 | pnpm run build 66 | 67 | - name: Unit Test 68 | run: pnpm run test:run 69 | 70 | typecheck: 71 | name: Type Check 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: pnpm/action-setup@v3 76 | - uses: actions/setup-node@v4 77 | with: 78 | node-version: lts/* 79 | cache: pnpm 80 | 81 | - name: Install 82 | run: pnpm install 83 | 84 | - name: Build 85 | run: pnpm run build:packages 86 | 87 | - name: Typecheck 88 | run: pnpm run typecheck 89 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/sfc.browser.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | import { dirname, join } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { describe, expect, it } from 'vitest' 6 | 7 | import { evaluateSFC, renderSFCString, resolvePropsFromString } from './sfc' 8 | 9 | // TODO: Browser testing 10 | describe.todo('renderSFCString', async () => { 11 | it('should be able to render simple SFC', async () => { 12 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'simple.velin.vue'), 'utf-8') 13 | const result = await renderSFCString(content) 14 | expect(result).toBeDefined() 15 | expect(result).not.toBe('') 16 | expect(result).toBe('# Hello, world!\n') 17 | }) 18 | 19 | it('should be able to render script setup SFC', async () => { 20 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.vue'), 'utf-8') 21 | const result = await renderSFCString(content) 22 | expect(result).toBeDefined() 23 | expect(result).not.toBe('') 24 | expect(result).toBe('# Count: 0\n') 25 | }) 26 | }) 27 | 28 | // TODO: Browser testing 29 | describe.todo('evaluateSFC', async () => { 30 | it('should be able to evaluate script setup SFC', async () => { 31 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.vue'), 'utf-8') 32 | const component = await evaluateSFC(content) 33 | expect(component).toBeDefined() 34 | expect(component.setup).toBeDefined() 35 | expect(typeof component.setup).toBe('function') 36 | }) 37 | }) 38 | 39 | describe.todo('resolvePropsFromString', async () => { 40 | it('should be able to render script setup SFC', async () => { 41 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup-with-props.velin.vue'), 'utf-8') 42 | const props = await resolvePropsFromString(content) 43 | expect(props).toEqual([ 44 | { key: 'date', type: 'string', title: 'date' }, 45 | ]) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/utils/src/transformers/typescript/transform.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { transformTS } from './transform' 4 | 5 | describe('transformTS', async () => { 6 | it('should transform TS', () => { 7 | const result = transformTS(`import { defineComponent as _defineComponent } from 'vue'\nimport { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, unref as _unref, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"\n\nimport { useCounter } from '@vueuse/core'\n\n\nexport default /*@__PURE__*/_defineComponent({\n props: {\n text: String,\n number: Number,\n check: Boolean,\n},\n setup(__props) {\n\n\n\nconst { count } = useCounter()\n\nreturn (_ctx: any,_cache: any) => {\n return (_openBlock(), _createElementBlock("div", null, [\n _createElementVNode("div", null, _toDisplayString(__props.text), 1 /* TEXT */),\n _createElementVNode("div", null, _toDisplayString(__props.number), 1 /* TEXT */),\n _createElementVNode("div", null, _toDisplayString(__props.check), 1 /* TEXT */),\n _createElementVNode("div", null, "Internal count: " + _toDisplayString(_unref(count)), 1 /* TEXT */)\n ]))\n}\n}\n\n})`) 8 | expect(result).toBeDefined() 9 | expect(result).toEqual(`import { defineComponent as _defineComponent } from 'vue'\nimport { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, unref as _unref, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"\n\nimport { useCounter } from '@vueuse/core'\n\n\nexport default /*@__PURE__*/_defineComponent({\n props: {\n text: String,\n number: Number,\n check: Boolean,\n},\n setup(__props) {\n\n\n\nconst { count } = useCounter()\n\nreturn (_ctx,_cache) => {\n return (_openBlock(), _createElementBlock("div", null, [\n _createElementVNode("div", null, _toDisplayString(__props.text), 1 /* TEXT */),\n _createElementVNode("div", null, _toDisplayString(__props.number), 1 /* TEXT */),\n _createElementVNode("div", null, _toDisplayString(__props.check), 1 /* TEXT */),\n _createElementVNode("div", null, "Internal count: " + _toDisplayString(_unref(count)), 1 /* TEXT */)\n ]))\n}\n}\n\n})`) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/sfc.ts: -------------------------------------------------------------------------------- 1 | import type { DefineComponent } from '@vue/runtime-core' 2 | 3 | import type { ComponentProp } from '../render-shared' 4 | import type { InputProps } from '../types' 5 | 6 | import ErrorStackParser from 'error-stack-parser' 7 | import path from 'path-browserify-esm' 8 | 9 | import { evaluate } from '@unrteljs/eval/browser' 10 | import { toMarkdown } from '@velin-dev/utils/to-md' 11 | import { renderToString } from '@vue/server-renderer' 12 | 13 | import { compileSFC, onlyRender, resolveProps } from '../render-shared' 14 | import { normalizeSFCSource } from '../render-shared/sfc' 15 | 16 | export async function evaluateSFC( 17 | source: string, 18 | basePath?: string, 19 | ) { 20 | const { script } = await compileSFC(source) 21 | 22 | if (!basePath) { 23 | // eslint-disable-next-line unicorn/error-message 24 | const stack = ErrorStackParser.parse(new Error()) 25 | basePath = path.dirname(stack[1].fileName?.replace('async', '').trim() || '') 26 | } 27 | 28 | // TODO: evaluate setup when not 50 | 51 | 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Trash/ 2 | .trash/ 3 | .DS_Store 4 | **/.obsidian/ 5 | .postgres 6 | 7 | # Node.js 8 | .idea 9 | .nuxt 10 | .temp 11 | .vite-inspect 12 | components.d.ts 13 | **/typed-router.d.ts 14 | node_modules 15 | .eslintcache 16 | **/tsconfig.tsbuildinfo 17 | **/vue-global-types.d.ts 18 | 19 | dist 20 | out/ 21 | *.local 22 | *.local.* 23 | *.log 24 | **/.cache/** 25 | **/temp/ 26 | .cache 27 | 28 | .coverage 29 | .coverage.* 30 | coverage/ 31 | cover/ 32 | htmlcov/ 33 | 34 | *.pcm 35 | *.wav 36 | *.ogg 37 | *.mp3 38 | 39 | # Make it easy for devenv users to override their local environment. 40 | # See: https://github.com/moeru-ai/airi/pull/110#discussion_r2024378953 41 | .direnv 42 | .pre-commit-config.yaml 43 | .envrc 44 | .devenv* 45 | devenv.* 46 | 47 | # pixi environments 48 | .pixi 49 | *.egg-info 50 | 51 | # Byte-compiled / optimized / DLL files 52 | __pycache__/ 53 | *.py[cod] 54 | *$py.class 55 | 56 | # C extensions 57 | *.so 58 | 59 | # Distribution / packaging 60 | .Python 61 | build/ 62 | develop-eggs/ 63 | dist/ 64 | downloads/ 65 | eggs/ 66 | .eggs/ 67 | lib/ 68 | lib64/ 69 | parts/ 70 | sdist/ 71 | var/ 72 | wheels/ 73 | share/python-wheels/ 74 | *.egg-info/ 75 | .installed.cfg 76 | *.egg 77 | MANIFEST 78 | 79 | # PyInstaller 80 | # Usually these files are written by a python script from a template 81 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 82 | *.manifest 83 | *.spec 84 | 85 | # Installer logs 86 | pip-log.txt 87 | pip-delete-this-directory.txt 88 | 89 | # Unit test / coverage reports 90 | .tox/ 91 | .nox/ 92 | nosetests.xml 93 | coverage.xml 94 | *.cover 95 | *.py,cover 96 | .pytest_cache/ 97 | .hypothesis/ 98 | 99 | # Translations 100 | *.mo 101 | *.pot 102 | 103 | # Django stuff: 104 | *.log 105 | local_settings.py 106 | db.sqlite3 107 | db.sqlite3-journal 108 | 109 | # Flask stuff: 110 | instance/ 111 | .webassets-cache 112 | 113 | # Scrapy stuff: 114 | .scrapy 115 | 116 | # Sphinx documentation 117 | docs/_build/ 118 | 119 | # PyBuilder 120 | .pybuilder/ 121 | target/ 122 | 123 | # Jupyter Notebook 124 | .ipynb_checkpoints 125 | 126 | # IPython 127 | profile_default/ 128 | ipython_config.py 129 | 130 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 131 | __pypackages__/ 132 | 133 | # Celery stuff 134 | celerybeat-schedule 135 | celerybeat.pid 136 | 137 | # SageMath parsed files 138 | *.sage.py 139 | 140 | # Environments 141 | .venv 142 | env/ 143 | venv/ 144 | ENV/ 145 | env.bak/ 146 | venv.bak/ 147 | 148 | # Spyder project settings 149 | .spyderproject 150 | .spyproject 151 | 152 | # Rope project settings 153 | .ropeproject 154 | 155 | # mkdocs documentation 156 | /site 157 | 158 | # mypy 159 | .mypy_cache/ 160 | .dmypy.json 161 | dmypy.json 162 | 163 | # Pyre type checker 164 | .pyre/ 165 | 166 | # pytype static type analyzer 167 | .pytype/ 168 | 169 | # Cython debug symbols 170 | cython_debug/ 171 | 172 | # PyCharm 173 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 174 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 175 | # and can be added to the global gitignore or merged into this file. For a more nuclear 176 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 177 | #.idea/ 178 | 179 | # Ruff stuff: 180 | .ruff_cache/ 181 | 182 | # PyPI configuration file 183 | .pypirc 184 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/sourcemap.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/69c2ed1dca84132708c3b9a1d0a008e11be2be74/src/sourcemap.ts 2 | 3 | import type { EncodedSourceMap as GenEncodedSourceMap } from '@jridgewell/gen-mapping' 4 | import type { EncodedSourceMap as TraceEncodedSourceMap } from '@jridgewell/trace-mapping' 5 | import type { RawSourceMap } from 'source-map-js' 6 | 7 | import { addMapping, fromMap, toEncodedMap } from '@jridgewell/gen-mapping' 8 | import { eachMapping, TraceMap } from '@jridgewell/trace-mapping' 9 | 10 | // trim analyzed bindings comment 11 | export function trimAnalyzedBindings(scriptCode: string) { 12 | return scriptCode.replace(/\/\*[\s\S]*?\*\/\n/, '').trim() 13 | } 14 | /** 15 | * The merge logic of sourcemap is consistent with the logic in vite-plugin-vue 16 | */ 17 | export function getSourceMap( 18 | filename: string, 19 | scriptCode: string, 20 | scriptMap: any, 21 | templateMap: any, 22 | ): RawSourceMap { 23 | let resolvedMap: RawSourceMap | undefined 24 | if (templateMap) { 25 | // if the template is inlined into the main module (indicated by the presence 26 | // of templateMap), we need to concatenate the two source maps. 27 | const from = scriptMap ?? { 28 | file: filename, 29 | sourceRoot: '', 30 | version: 3, 31 | sources: [], 32 | sourcesContent: [], 33 | names: [], 34 | mappings: '', 35 | } 36 | const gen = fromMap( 37 | // version property of result.map is declared as string 38 | // but actually it is `3` 39 | from as Omit as TraceEncodedSourceMap, 40 | ) 41 | const tracer = new TraceMap( 42 | // same above 43 | templateMap as Omit as TraceEncodedSourceMap, 44 | ) 45 | const offset 46 | = (trimAnalyzedBindings(scriptCode).match(/\r?\n/g)?.length ?? 0) 47 | eachMapping(tracer, (m) => { 48 | if (m.source == null) 49 | return 50 | addMapping(gen, { 51 | source: m.source, 52 | original: { line: m.originalLine, column: m.originalColumn }, 53 | generated: { 54 | line: m.generatedLine + offset, 55 | column: m.generatedColumn, 56 | }, 57 | }) 58 | }) 59 | 60 | // same above 61 | resolvedMap = toEncodedMap(gen) as Omit< 62 | GenEncodedSourceMap, 63 | 'version' 64 | > as RawSourceMap 65 | // if this is a template only update, we will be reusing a cached version 66 | // of the main module compile result, which has outdated sourcesContent. 67 | resolvedMap.sourcesContent = templateMap.sourcesContent 68 | } 69 | else { 70 | resolvedMap = scriptMap 71 | } 72 | 73 | return resolvedMap! 74 | } 75 | 76 | /* 77 | * Slightly modified version of https://github.com/AriPerkkio/vite-plugin-source-map-visualizer/blob/main/src/generate-link.ts 78 | */ 79 | export function toVisualizer(code: string, sourceMap: RawSourceMap) { 80 | const map = JSON.stringify(sourceMap) 81 | const encoder = new TextEncoder() 82 | 83 | // Convert the strings to Uint8Array 84 | const codeArray = encoder.encode(code) 85 | const mapArray = encoder.encode(map) 86 | 87 | // Create Uint8Array for the lengths 88 | const codeLengthArray = encoder.encode(codeArray.length.toString()) 89 | const mapLengthArray = encoder.encode(mapArray.length.toString()) 90 | 91 | // Combine the lengths and the data 92 | const combinedArray = new Uint8Array( 93 | codeLengthArray.length 94 | + 1 95 | + codeArray.length 96 | + mapLengthArray.length 97 | + 1 98 | + mapArray.length, 99 | ) 100 | 101 | combinedArray.set(codeLengthArray) 102 | combinedArray.set([0], codeLengthArray.length) 103 | combinedArray.set(codeArray, codeLengthArray.length + 1) 104 | combinedArray.set( 105 | mapLengthArray, 106 | codeLengthArray.length + 1 + codeArray.length, 107 | ) 108 | combinedArray.set( 109 | [0], 110 | codeLengthArray.length + 1 + codeArray.length + mapLengthArray.length, 111 | ) 112 | combinedArray.set( 113 | mapArray, 114 | codeLengthArray.length + 1 + codeArray.length + mapLengthArray.length + 1, 115 | ) 116 | 117 | // Convert the Uint8Array to a binary string 118 | let binary = '' 119 | const len = combinedArray.byteLength 120 | for (let i = 0; i < len; i++) binary += String.fromCharCode(combinedArray[i]) 121 | 122 | // Convert the binary string to a base64 string and return it 123 | return `https://evanw.github.io/source-map-visualization#${btoa(binary)}` 124 | } 125 | -------------------------------------------------------------------------------- /packages/core/src/render-shared/props.ts: -------------------------------------------------------------------------------- 1 | import type { App, ComponentInternalInstance, DefineComponent } from 'vue' 2 | 3 | export interface ComponentPropText { 4 | type: 'string' 5 | value?: string 6 | } 7 | 8 | export interface ComponentPropBool { 9 | type: 'boolean' 10 | value?: boolean 11 | } 12 | 13 | export interface ComponentPropNumber { 14 | type: 'number' 15 | value?: number 16 | } 17 | 18 | export interface ComponentPropUnknown { 19 | type: 'unknown' 20 | value?: unknown 21 | } 22 | 23 | export interface ComponentPropArray { 24 | type: 'array' 25 | value?: unknown[] 26 | } 27 | 28 | export type ComponentProp = (ComponentPropText | ComponentPropBool | ComponentPropNumber | ComponentPropArray | ComponentPropUnknown) & { 29 | title: string 30 | key: string 31 | } 32 | 33 | function willTurnIntoNumber(value: unknown): boolean { 34 | if (value === Number) { 35 | return true 36 | } 37 | 38 | // it is possible value is { type: Number() } 39 | if (typeof value === 'object' && value !== null && 'type' in value) { 40 | // check if value.type is Number() 41 | if (typeof (value as { type: unknown }).type === 'function' && (value as { type: unknown }).type === Number) { 42 | return true 43 | } 44 | } 45 | 46 | return false 47 | } 48 | 49 | function willTurnIntoBoolean(value: unknown): boolean { 50 | if (value === Boolean) { 51 | return true 52 | } 53 | 54 | // it is possible value is { type: Boolean() } 55 | if (typeof value === 'object' && value !== null && 'type' in value) { 56 | // check if value.type is Boolean() 57 | if (typeof (value as { type: unknown }).type === 'function' && (value as { type: unknown }).type === Boolean) { 58 | return true 59 | } 60 | } 61 | 62 | return false 63 | } 64 | 65 | function willTurnIntoString(value: unknown): boolean { 66 | if (value === String) { 67 | return true 68 | } 69 | 70 | // it is possible value is { type: String() } 71 | if (typeof value === 'object' && value !== null && 'type' in value) { 72 | // check if value.type is String() 73 | if (typeof (value as { type: unknown }).type === 'function' && (value as { type: unknown }).type === String) { 74 | return true 75 | } 76 | } 77 | 78 | return false 79 | } 80 | 81 | function willTurnIntoArray(value: unknown): boolean { 82 | if (value === Array) { 83 | return true 84 | } 85 | 86 | // it is possible value is { type: Array() } 87 | if (typeof value === 'object' && value !== null && 'type' in value) { 88 | // check if value.type is Array() 89 | if (typeof (value as { type: unknown }).type === 'function' && (value as { type: unknown }).type === Array) { 90 | return true 91 | } 92 | } 93 | 94 | return false 95 | } 96 | 97 | function inferType( 98 | propDef: 99 | | { 100 | // eslint-disable-next-line ts/no-unsafe-function-type 101 | type: Function 102 | } 103 | | typeof String 104 | | typeof Number 105 | | typeof Boolean 106 | | typeof Array 107 | | unknown, 108 | ) { 109 | let type: 'unknown' | 'string' | 'number' | 'boolean' | 'array' = 'unknown' 110 | if (willTurnIntoString(propDef)) { 111 | type = 'string' 112 | } 113 | else if (willTurnIntoNumber(propDef)) { 114 | type = 'number' 115 | } 116 | else if (willTurnIntoBoolean(propDef)) { 117 | type = 'boolean' 118 | } 119 | else if (willTurnIntoArray(propDef)) { 120 | type = 'array' 121 | } 122 | return type 123 | } 124 | 125 | /** 126 | * @see https://github.com/vuejs/devtools/blob/e7dffa24fe98b212404a1451818b6c66739f88ee/packages/devtools-kit/src/core/component/state/process.ts#L62 127 | * @see https://github.com/vuejs/devtools/blob/e7dffa24fe98b212404a1451818b6c66739f88ee/packages/devtools-kit/src/core/app/index.ts#L14 128 | * 129 | * @param component 130 | */ 131 | export function resolveProps(component: DefineComponent | App): ComponentProp[] { 132 | if (component._component && component._component.props && typeof component._component.props === 'object') { 133 | return Object.entries(component._component.props).map(([key, propDef]) => { 134 | return { 135 | key, 136 | title: key, 137 | type: inferType(propDef), 138 | } 139 | }) 140 | } 141 | else if ((component as unknown as ComponentInternalInstance).props && typeof (component as unknown as ComponentInternalInstance).props === 'object') { 142 | return Object.entries((component as unknown as ComponentInternalInstance).props).map(([key, propDef]) => { 143 | return { 144 | key, 145 | title: key, 146 | type: inferType(propDef), 147 | } 148 | }) 149 | } 150 | else { 151 | return [] 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./docs/public/logo.svg) 2 | 3 | # Velin 4 | 5 | [![npm version][npm-version-src]][npm-version-href] 6 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 7 | [![bundle][bundle-src]][bundle-href] 8 | [![JSDocs][jsdocs-src]][jsdocs-href] 9 | [![License][license-src]][license-href] 10 | 11 | > Have you wondered how it feels if you can develop the prompts of agents and MCP servers with the power of Vue? 12 | 13 | Develop prompts with Vue SFC or Markdown like pro. 14 | 15 | We got a playground too, check it out: 16 | 17 |

18 | 19 | 23 | 27 | 28 | 29 |

30 | 31 | ### Quick Start 32 | 33 | Try it by running following command under your `pnpm`/`npm` project. 34 | 35 | ```bash 36 | # For browser users 37 | npm i @velin-dev/vue 38 | 39 | # For Node.js, CI, server rendering and backend users 40 | npm i @velin-dev/core 41 | ``` 42 | 43 | ## Features 44 | 45 | - No longer need to fight and format with the non-supported DSL of templating language! 46 | - Use HTML elements like `
` for block elements, `` for inline elements. 47 | - Directives with native Vue template syntax, `v-if`, `v-else` all works. 48 | - Compositing other open sourced prompt component or composables over memory system. 49 | 50 | All included... 51 | 52 | ## How it feels 53 | 54 | ```html 55 | 56 | 61 | 62 | 67 | ``` 68 | 69 | ### In Node.js 70 | 71 | ```ts 72 | import { readFile } from 'node:fs/promises' 73 | 74 | import { renderSFCString } from '@velin-dev/core' 75 | import { ref } from 'vue' 76 | 77 | const source = await readFile('./Prompt.vue', 'utf-8') 78 | const name = ref('Velin') 79 | const result = await renderSFCString(source, { name }) 80 | 81 | console.log(result) 82 | // Hello world, this is Velin! 83 | ``` 84 | 85 | ### In Vue / Browser 86 | 87 | ```vue 88 | 102 | ``` 103 | 104 | ## Similar projects 105 | 106 | - [poml](https://github.com/microsoft/poml) / [pomljs](https://github.com/microsoft/poml) 107 | 108 | ## Development 109 | 110 | ### Clone 111 | 112 | ```shell 113 | git clone https://github.com/moeru-ai/velin.git 114 | cd airi 115 | ``` 116 | 117 | ### Install dependencies 118 | 119 | ```shell 120 | corepack enable 121 | pnpm install 122 | ``` 123 | 124 | > [!NOTE] 125 | > 126 | > We would recommend to install [@antfu/ni](https://github.com/antfu-collective/ni) to make your script simpler. 127 | > 128 | > ```shell 129 | > corepack enable 130 | > npm i -g @antfu/ni 131 | > ``` 132 | > 133 | > Once installed, you can 134 | > 135 | > - use `ni` for `pnpm install`, `npm install` and `yarn install`. 136 | > - use `nr` for `pnpm run`, `npm run` and `yarn run`. 137 | > 138 | > You don't need to care about the package manager, `ni` will help you choose the right one. 139 | 140 | ```shell 141 | pnpm dev 142 | ``` 143 | 144 | > [!NOTE] 145 | > 146 | > For [@antfu/ni](https://github.com/antfu-collective/ni) users, you can 147 | > 148 | > ```shell 149 | > nr dev 150 | > ``` 151 | 152 | ### Build 153 | 154 | ```shell 155 | pnpm build 156 | ``` 157 | 158 | > [!NOTE] 159 | > 160 | > For [@antfu/ni](https://github.com/antfu-collective/ni) users, you can 161 | > 162 | > ```shell 163 | > nr build 164 | > ``` 165 | 166 | ## License 167 | 168 | MIT 169 | 170 | [npm-version-src]: https://img.shields.io/npm/v/@velin-dev/core?style=flat&colorA=080f12&colorB=1fa669 171 | [npm-version-href]: https://npmjs.com/package/@velin-dev/core 172 | [npm-downloads-src]: https://img.shields.io/npm/dm/@velin-dev/core?style=flat&colorA=080f12&colorB=1fa669 173 | [npm-downloads-href]: https://npmjs.com/package/@velin-dev/core 174 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@velin-dev/vue?style=flat&colorA=080f12&colorB=1fa669&label=minzip 175 | [bundle-href]: https://bundlephobia.com/result?p=@velin-dev/vue 176 | [license-src]: https://img.shields.io/github/license/moeru-ai/velin.svg?style=flat&colorA=080f12&colorB=1fa669 177 | [license-href]: https://github.com/moeru-ai/velin/blob/main/LICENSE 178 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 179 | [jsdocs-href]: https://www.jsdocs.io/package/@velin-dev/core 180 | -------------------------------------------------------------------------------- /packages/core/src/render-node/sfc.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | import { dirname, join } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { describe, expect, it } from 'vitest' 6 | 7 | import { evaluateSFC, renderSFCString, resolvePropsFromString } from './sfc' 8 | 9 | describe('renderSFCString', async () => { 10 | it('should be able to render simple SFC', async () => { 11 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'simple.velin.vue'), 'utf-8') 12 | const { props, rendered } = await renderSFCString(content) 13 | expect(props).toBeDefined() 14 | expect(props.length).toBe(0) 15 | expect(rendered).toBeDefined() 16 | expect(rendered).not.toBe('') 17 | expect(rendered).toBe('# Hello, world!\n') 18 | }) 19 | 20 | it('should be able to render script setup SFC with', async () => { 21 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.vue'), 'utf-8') 22 | const { props, rendered } = await renderSFCString(content) 23 | expect(props).toBeDefined() 24 | expect(props.length).toBe(0) 25 | expect(rendered).toBeDefined() 26 | expect(rendered).not.toBe('') 27 | expect(rendered).toBe('# Count: 0\n') 28 | }) 29 | 30 | it('should be able to render script setup SFC lang="ts"', async () => { 31 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.ts.velin.vue'), 'utf-8') 32 | const { props, rendered } = await renderSFCString(content) 33 | expect(props).toBeDefined() 34 | expect(props.length).toBe(0) 35 | expect(rendered).toBeDefined() 36 | expect(rendered).not.toBe('') 37 | expect(rendered).toBe('# Count: 0\n') 38 | }) 39 | 40 | it('should be able to render script setup SFC with props', async () => { 41 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup-with-props.velin.vue'), 'utf-8') 42 | const { props, rendered } = await renderSFCString(content, { date: '2025-07-01' }) 43 | expect(props).toBeDefined() 44 | expect(props.length).toBe(1) 45 | expect(rendered).toBeDefined() 46 | expect(rendered).not.toBe('') 47 | expect(rendered).toBe('# Count: 0\n\n2025-07-01\n') 48 | }) 49 | 50 | it('should be able to render script setup SFC with props with lang="ts"', async () => { 51 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup-with-props.ts.velin.vue'), 'utf-8') 52 | const { props, rendered } = await renderSFCString(content, { date: '2025-07-01' }) 53 | expect(props).toBeDefined() 54 | expect(props.length).toBe(1) 55 | expect(rendered).toBeDefined() 56 | expect(rendered).not.toBe('') 57 | expect(rendered).toBe('# Count: 0\n\n2025-07-01\n') 58 | }) 59 | }) 60 | 61 | describe('evaluateSFC', async () => { 62 | it('should be able to evaluate script setup SFC', async () => { 63 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.vue'), 'utf-8') 64 | const component = await evaluateSFC(content) 65 | expect(component).toBeDefined() 66 | expect(component.setup).toBeDefined() 67 | expect(typeof component.setup).toBe('function') 68 | }) 69 | 70 | it('should be able to evaluate script setup SFC with lang="ts"', async () => { 71 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.ts.velin.vue'), 'utf-8') 72 | const component = await evaluateSFC(content) 73 | expect(component).toBeDefined() 74 | expect(component.setup).toBeDefined() 75 | expect(typeof component.setup).toBe('function') 76 | }) 77 | 78 | it('should be able to evaluate script setup SFC with props', async () => { 79 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup-with-props.velin.vue'), 'utf-8') 80 | const component = await evaluateSFC(content) 81 | expect(component).toBeDefined() 82 | expect(component.setup).toBeDefined() 83 | expect(typeof component.setup).toBe('function') 84 | }) 85 | 86 | it('should be able to evaluate script setup SFC with props with lang="ts"', async () => { 87 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup-with-props.ts.velin.vue'), 'utf-8') 88 | const component = await evaluateSFC(content) 89 | expect(component).toBeDefined() 90 | expect(component.setup).toBeDefined() 91 | expect(typeof component.setup).toBe('function') 92 | }) 93 | }) 94 | 95 | describe('resolvePropsFromString', async () => { 96 | it('should be able to render script setup SFC', async () => { 97 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup-with-props.velin.vue'), 'utf-8') 98 | const props = await resolvePropsFromString(content) 99 | expect(props).toEqual([ 100 | { key: 'date', type: 'string', title: 'date' }, 101 | ]) 102 | }) 103 | 104 | it('should be able to render script setup SFC with lang="ts"', async () => { 105 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup-with-props.ts.velin.vue'), 'utf-8') 106 | const props = await resolvePropsFromString(content) 107 | expect(props).toEqual([ 108 | { key: 'date', type: 'string', title: 'date' }, 109 | ]) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/index.vue: -------------------------------------------------------------------------------- 1 | 182 | 183 | 191 | 192 | 208 | -------------------------------------------------------------------------------- /packages/core/src/render-shared/props.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { resolveProps } from './props' 4 | 5 | describe('resolveProps', () => { 6 | describe('string type', () => { 7 | it('should resolve String constructor as string type', () => { 8 | const component = { 9 | _component: { 10 | props: { 11 | name: String, 12 | }, 13 | }, 14 | } 15 | const props = resolveProps(component as any) 16 | expect(props).toEqual([ 17 | { key: 'name', title: 'name', type: 'string' }, 18 | ]) 19 | }) 20 | 21 | it('should resolve { type: String } as string type', () => { 22 | const component = { 23 | _component: { 24 | props: { 25 | name: { type: String }, 26 | }, 27 | }, 28 | } 29 | const props = resolveProps(component as any) 30 | expect(props).toEqual([ 31 | { key: 'name', title: 'name', type: 'string' }, 32 | ]) 33 | }) 34 | }) 35 | 36 | describe('number type', () => { 37 | it('should resolve Number constructor as number type', () => { 38 | const component = { 39 | _component: { 40 | props: { 41 | age: Number, 42 | }, 43 | }, 44 | } 45 | const props = resolveProps(component as any) 46 | expect(props).toEqual([ 47 | { key: 'age', title: 'age', type: 'number' }, 48 | ]) 49 | }) 50 | 51 | it('should resolve { type: Number } as number type', () => { 52 | const component = { 53 | _component: { 54 | props: { 55 | age: { type: Number }, 56 | }, 57 | }, 58 | } 59 | const props = resolveProps(component as any) 60 | expect(props).toEqual([ 61 | { key: 'age', title: 'age', type: 'number' }, 62 | ]) 63 | }) 64 | }) 65 | 66 | describe('boolean type', () => { 67 | it('should resolve Boolean constructor as boolean type', () => { 68 | const component = { 69 | _component: { 70 | props: { 71 | active: Boolean, 72 | }, 73 | }, 74 | } 75 | const props = resolveProps(component as any) 76 | expect(props).toEqual([ 77 | { key: 'active', title: 'active', type: 'boolean' }, 78 | ]) 79 | }) 80 | 81 | it('should resolve { type: Boolean } as boolean type', () => { 82 | const component = { 83 | _component: { 84 | props: { 85 | active: { type: Boolean }, 86 | }, 87 | }, 88 | } 89 | const props = resolveProps(component as any) 90 | expect(props).toEqual([ 91 | { key: 'active', title: 'active', type: 'boolean' }, 92 | ]) 93 | }) 94 | }) 95 | 96 | describe('array type', () => { 97 | it('should resolve Array constructor as array type', () => { 98 | const component = { 99 | _component: { 100 | props: { 101 | items: Array, 102 | }, 103 | }, 104 | } 105 | const props = resolveProps(component as any) 106 | expect(props).toEqual([ 107 | { key: 'items', title: 'items', type: 'array' }, 108 | ]) 109 | }) 110 | 111 | it('should resolve { type: Array } as array type', () => { 112 | const component = { 113 | _component: { 114 | props: { 115 | items: { type: Array }, 116 | }, 117 | }, 118 | } 119 | const props = resolveProps(component as any) 120 | expect(props).toEqual([ 121 | { key: 'items', title: 'items', type: 'array' }, 122 | ]) 123 | }) 124 | 125 | it('should resolve { type: Array, default: () => [] } as array type', () => { 126 | const component = { 127 | _component: { 128 | props: { 129 | items: { type: Array, default: () => [] }, 130 | }, 131 | }, 132 | } 133 | const props = resolveProps(component as any) 134 | expect(props).toEqual([ 135 | { key: 'items', title: 'items', type: 'array' }, 136 | ]) 137 | }) 138 | }) 139 | 140 | describe('mixed types', () => { 141 | it('should resolve multiple props with different types', () => { 142 | const component = { 143 | _component: { 144 | props: { 145 | name: String, 146 | age: Number, 147 | active: Boolean, 148 | tags: Array, 149 | }, 150 | }, 151 | } 152 | const props = resolveProps(component as any) 153 | expect(props).toEqual([ 154 | { key: 'name', title: 'name', type: 'string' }, 155 | { key: 'age', title: 'age', type: 'number' }, 156 | { key: 'active', title: 'active', type: 'boolean' }, 157 | { key: 'tags', title: 'tags', type: 'array' }, 158 | ]) 159 | }) 160 | 161 | it('should resolve props from ComponentInternalInstance', () => { 162 | const component = { 163 | props: { 164 | name: String, 165 | items: Array, 166 | }, 167 | } 168 | const props = resolveProps(component as any) 169 | expect(props).toEqual([ 170 | { key: 'name', title: 'name', type: 'string' }, 171 | { key: 'items', title: 'items', type: 'array' }, 172 | ]) 173 | }) 174 | }) 175 | 176 | describe('unknown type', () => { 177 | it('should resolve unknown constructor as unknown type', () => { 178 | const component = { 179 | _component: { 180 | props: { 181 | custom: Object, 182 | }, 183 | }, 184 | } 185 | const props = resolveProps(component as any) 186 | expect(props).toEqual([ 187 | { key: 'custom', title: 'custom', type: 'unknown' }, 188 | ]) 189 | }) 190 | }) 191 | 192 | describe('empty props', () => { 193 | it('should return empty array when no props defined', () => { 194 | const component = {} 195 | const props = resolveProps(component as any) 196 | expect(props).toEqual([]) 197 | }) 198 | 199 | it('should return empty array when props is null', () => { 200 | const component = { 201 | _component: { 202 | props: null, 203 | }, 204 | } 205 | const props = resolveProps(component as any) 206 | expect(props).toEqual([]) 207 | }) 208 | }) 209 | }) 210 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/env.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/f2b38cf978abb9c21c6c788589b4599b4ff85a7d/src/monaco/env.ts 2 | 3 | import type { WorkerLanguageService } from '@volar/monaco/worker' 4 | 5 | import type { Store } from './store' 6 | import type { CreateData } from './vue.worker' 7 | 8 | import EditorWorker from 'monaco-editor-core/esm/vs/editor/editor.worker?worker' 9 | 10 | import { editor, languages, Uri } from 'monaco-editor-core' 11 | import { watchEffect } from 'vue' 12 | 13 | import * as volar from '@volar/monaco' 14 | 15 | import VueWorker from './vue.worker?worker' 16 | 17 | import { debounce } from '../../utils/vue-repl' 18 | import { getOrCreateModel } from './monaco/utils' 19 | 20 | import * as languageConfigs from './language-configs' 21 | 22 | let initted = false 23 | export function initMonaco(store: Store) { 24 | if (initted) 25 | return 26 | loadMonacoEnv(store) 27 | 28 | watchEffect(() => { 29 | // create a model for each file in the store 30 | for (const filename in store.files) { 31 | const file = store.files[filename] 32 | if (editor.getModel(Uri.parse(`file:///${filename}`))) 33 | continue 34 | getOrCreateModel( 35 | Uri.parse(`file:///${filename}`), 36 | file.language, 37 | file.code, 38 | ) 39 | } 40 | 41 | // dispose of any models that are not in the store 42 | for (const model of editor.getModels()) { 43 | const uri = model.uri.toString() 44 | if (store.files[uri.substring('file:///'.length)]) 45 | continue 46 | 47 | if (uri.startsWith('file:///node_modules')) 48 | continue 49 | if (uri.startsWith('inmemory://')) 50 | continue 51 | 52 | model.dispose() 53 | } 54 | }) 55 | 56 | initted = true 57 | } 58 | 59 | export class WorkerHost { 60 | onFetchCdnFile(uri: string, text: string) { 61 | getOrCreateModel(Uri.parse(uri), undefined, text) 62 | } 63 | } 64 | 65 | let disposeVue: undefined | (() => void) 66 | export async function reloadLanguageTools(store: Store) { 67 | disposeVue?.() 68 | 69 | let dependencies: Record = { 70 | ...store.dependencyVersion, 71 | } 72 | 73 | if (store.vueVersion) { 74 | dependencies = { 75 | ...dependencies, 76 | 'vue': store.vueVersion, 77 | '@vue/compiler-core': store.vueVersion, 78 | '@vue/compiler-dom': store.vueVersion, 79 | '@vue/compiler-sfc': store.vueVersion, 80 | '@vue/compiler-ssr': store.vueVersion, 81 | '@vue/reactivity': store.vueVersion, 82 | '@vue/runtime-core': store.vueVersion, 83 | '@vue/runtime-dom': store.vueVersion, 84 | '@vue/shared': store.vueVersion, 85 | } 86 | } 87 | 88 | if (store.typescriptVersion) { 89 | dependencies = { 90 | ...dependencies, 91 | typescript: store.typescriptVersion, 92 | } 93 | } 94 | 95 | const worker = editor.createWebWorker({ 96 | moduleId: 'vs/language/vue/vueWorker', 97 | label: 'vue', 98 | host: new WorkerHost(), 99 | createData: { 100 | tsconfig: store.getTsConfig?.() || {}, 101 | dependencies, 102 | } satisfies CreateData, 103 | }) 104 | const languageId = ['vue', 'javascript', 'typescript'] 105 | const getSyncUris = () => 106 | Object.keys(store.files).map(filename => Uri.parse(`file:///${filename}`)) 107 | 108 | const { dispose: disposeMarkers } = volar.activateMarkers( 109 | worker, 110 | languageId, 111 | 'vue', 112 | getSyncUris, 113 | editor, 114 | ) 115 | const { dispose: disposeAutoInsertion } = volar.activateAutoInsertion( 116 | worker, 117 | languageId, 118 | getSyncUris, 119 | editor, 120 | ) 121 | const { dispose: disposeProvides } = await volar.registerProviders( 122 | worker, 123 | languageId, 124 | getSyncUris, 125 | languages, 126 | ) 127 | 128 | disposeVue = () => { 129 | disposeMarkers() 130 | disposeAutoInsertion() 131 | disposeProvides() 132 | } 133 | } 134 | 135 | export interface WorkerMessage { 136 | event: 'init' 137 | tsVersion: string 138 | tsLocale?: string 139 | } 140 | 141 | export function loadMonacoEnv(store: Store) { 142 | // eslint-disable-next-line no-restricted-globals 143 | ;(self as any).MonacoEnvironment = { 144 | async getWorker(_: any, label: string) { 145 | if (label === 'vue') { 146 | const worker = new VueWorker() 147 | const init = new Promise((resolve) => { 148 | worker.addEventListener('message', (data) => { 149 | if (data.data === 'inited') { 150 | resolve() 151 | } 152 | }) 153 | worker.postMessage({ 154 | event: 'init', 155 | tsVersion: store.typescriptVersion, 156 | tsLocale: store.locale, 157 | } satisfies WorkerMessage) 158 | }) 159 | await init 160 | return worker 161 | } 162 | return new EditorWorker() 163 | }, 164 | } 165 | languages.register({ id: 'vue', extensions: ['.vue'] }) 166 | languages.register({ id: 'javascript', extensions: ['.js'] }) 167 | languages.register({ id: 'typescript', extensions: ['.ts'] }) 168 | languages.register({ id: 'css', extensions: ['.css'] }) 169 | languages.setLanguageConfiguration('vue', languageConfigs.vue) 170 | languages.setLanguageConfiguration('javascript', languageConfigs.js) 171 | languages.setLanguageConfiguration('typescript', languageConfigs.ts) 172 | languages.setLanguageConfiguration('css', languageConfigs.css) 173 | 174 | let languageToolsPromise: Promise | undefined 175 | store.reloadLanguageTools = debounce(async () => { 176 | ;(languageToolsPromise ||= reloadLanguageTools(store)).finally(() => { 177 | languageToolsPromise = undefined 178 | }) 179 | }, 250) 180 | languages.onLanguage('vue', () => store.reloadLanguageTools!()) 181 | 182 | // Support for go to definition 183 | editor.registerEditorOpener({ 184 | openCodeEditor(_, resource) { 185 | if (resource.toString().startsWith('file:///node_modules')) { 186 | return true 187 | } 188 | 189 | const path = resource.path 190 | if (/^\//.test(path)) { 191 | const fileName = path.replace('/', '') 192 | if (fileName !== store.activeFile.filename) { 193 | store.setActive(fileName) 194 | return true 195 | } 196 | } 197 | 198 | return false 199 | }, 200 | }) 201 | } 202 | -------------------------------------------------------------------------------- /apps/playground/src/components/Playground.vue: -------------------------------------------------------------------------------- 1 | 133 | 134 | 191 | -------------------------------------------------------------------------------- /packages/utils/src/transformers/vue/moduleCompiler.ts: -------------------------------------------------------------------------------- 1 | import type { ExportSpecifier, Identifier, Node } from '@babel/types' 2 | 3 | import type { File } from './shared' 4 | 5 | import { 6 | babelParse, 7 | extractIdentifiers, 8 | isInDestructureAssignment, 9 | isStaticProperty, 10 | MagicString, 11 | walk, 12 | walkIdentifiers, 13 | } from '@vue/compiler-sfc' 14 | 15 | export function compileModulesForPreview(options: { 16 | files: Record 17 | mainFile: string 18 | }, isSSR = false) { 19 | const seen = new Set() 20 | const processed: string[] = [] 21 | processFile(options, options.files[options.mainFile], processed, seen, isSSR) 22 | 23 | if (!isSSR) { 24 | // also add css files that are not imported 25 | for (const filename in options.files) { 26 | if (filename.endsWith('.css')) { 27 | const file = options.files[filename] 28 | if (!seen.has(file)) { 29 | processed.push( 30 | `\nwindow.__css__.push(${JSON.stringify(file.compiled.css)})`, 31 | ) 32 | } 33 | } 34 | } 35 | } 36 | 37 | // return the default export of the main module 38 | processed.push(`\nreturn __modules__["${options.mainFile}"].default`) 39 | 40 | return processed 41 | } 42 | 43 | const modulesKey = `__modules__` 44 | const exportKey = `__export__` 45 | const dynamicImportKey = `__dynamic_import__` 46 | const moduleKey = `__module__` 47 | 48 | // similar logic with Vite's SSR transform, except this is targeting the browser 49 | function processFile( 50 | options: { 51 | files: Record 52 | mainFile: string 53 | }, 54 | file: File, 55 | processed: string[], 56 | seen: Set, 57 | isSSR: boolean, 58 | ) { 59 | if (seen.has(file)) { 60 | return [] 61 | } 62 | seen.add(file) 63 | 64 | if (!isSSR && file.filename.endsWith('.html')) { 65 | return processHtmlFile(options, file.code, file.filename, processed, seen) 66 | } 67 | 68 | let { 69 | code: js, 70 | importedFiles, 71 | hasDynamicImport, 72 | } = processModule( 73 | options, 74 | isSSR ? file.compiled.ssr : file.compiled.js, 75 | file.filename, 76 | ) 77 | processChildFiles( 78 | options, 79 | importedFiles, 80 | hasDynamicImport, 81 | processed, 82 | seen, 83 | isSSR, 84 | ) 85 | // append css 86 | if (file.compiled.css && !isSSR) { 87 | js += `\nwindow.__css__.push(${JSON.stringify(file.compiled.css)})` 88 | } 89 | 90 | // push self 91 | processed.push(js) 92 | } 93 | 94 | function processChildFiles( 95 | options: { 96 | files: Record 97 | mainFile: string 98 | }, 99 | importedFiles: Set, 100 | hasDynamicImport: boolean, 101 | processed: string[], 102 | seen: Set, 103 | isSSR: boolean, 104 | ) { 105 | if (hasDynamicImport) { 106 | // process all files 107 | for (const file of Object.values(options.files)) { 108 | if (seen.has(file)) 109 | continue 110 | processFile(options, file, processed, seen, isSSR) 111 | } 112 | } 113 | else if (importedFiles.size > 0) { 114 | // crawl child imports 115 | for (const imported of importedFiles) { 116 | processFile(options, options.files[imported], processed, seen, isSSR) 117 | } 118 | } 119 | } 120 | 121 | function processModule(options: { 122 | files: Record 123 | mainFile: string 124 | }, src: string, filename: string) { 125 | const s = new MagicString(src) 126 | 127 | const ast = babelParse(src, { 128 | sourceFilename: filename, 129 | sourceType: 'module', 130 | }).program.body 131 | 132 | const idToImportMap = new Map() 133 | const declaredConst = new Set() 134 | const importedFiles = new Set() 135 | const importToIdMap = new Map() 136 | 137 | function resolveImport(raw: string): string | undefined { 138 | const files = options.files 139 | let resolved = raw 140 | const file 141 | = files[resolved] 142 | || files[(resolved = `${raw}.ts`)] 143 | || files[(resolved = `${raw}.js`)] 144 | return file ? resolved : undefined 145 | } 146 | 147 | function defineImport(node: Node, source: string) { 148 | const filename = resolveImport(source.replace(/^\.\/+/, 'src/')) 149 | if (!filename) { 150 | throw new Error(`File "${source}" does not exist.`) 151 | } 152 | if (importedFiles.has(filename)) { 153 | return importToIdMap.get(filename)! 154 | } 155 | importedFiles.add(filename) 156 | const id = `__import_${importedFiles.size}__` 157 | importToIdMap.set(filename, id) 158 | s.appendLeft( 159 | node.start!, 160 | `const ${id} = ${modulesKey}[${JSON.stringify(filename)}]\n`, 161 | ) 162 | return id 163 | } 164 | 165 | function defineExport(name: string, local = name) { 166 | s.append(`\n${exportKey}(${moduleKey}, "${name}", () => ${local})`) 167 | } 168 | 169 | // 0. instantiate module 170 | s.prepend( 171 | `const ${moduleKey} = ${modulesKey}[${JSON.stringify( 172 | filename, 173 | )}] = { [Symbol.toStringTag]: "Module" }\n\n`, 174 | ) 175 | 176 | // 1. check all import statements and record id -> importName map 177 | for (const node of ast) { 178 | // import foo from 'foo' --> foo -> __import_foo__.default 179 | // import { baz } from 'foo' --> baz -> __import_foo__.baz 180 | // import * as ok from 'foo' --> ok -> __import_foo__ 181 | if (node.type === 'ImportDeclaration') { 182 | const source = node.source.value 183 | if (source.startsWith('./')) { 184 | const importId = defineImport(node, node.source.value) 185 | for (const spec of node.specifiers) { 186 | if (spec.type === 'ImportSpecifier') { 187 | idToImportMap.set( 188 | spec.local.name, 189 | `${importId}.${(spec.imported as Identifier).name}`, 190 | ) 191 | } 192 | else if (spec.type === 'ImportDefaultSpecifier') { 193 | idToImportMap.set(spec.local.name, `${importId}.default`) 194 | } 195 | else { 196 | // namespace specifier 197 | idToImportMap.set(spec.local.name, importId) 198 | } 199 | } 200 | s.remove(node.start!, node.end!) 201 | } 202 | } 203 | } 204 | 205 | // 2. check all export statements and define exports 206 | for (const node of ast) { 207 | // named exports 208 | if (node.type === 'ExportNamedDeclaration') { 209 | if (node.declaration) { 210 | if ( 211 | node.declaration.type === 'FunctionDeclaration' 212 | || node.declaration.type === 'ClassDeclaration' 213 | ) { 214 | // export function foo() {} 215 | defineExport(node.declaration.id!.name) 216 | } 217 | else if (node.declaration.type === 'VariableDeclaration') { 218 | // export const foo = 1, bar = 2 219 | for (const decl of node.declaration.declarations) { 220 | for (const id of extractIdentifiers(decl.id)) { 221 | defineExport(id.name) 222 | } 223 | } 224 | } 225 | s.remove(node.start!, node.declaration.start!) 226 | } 227 | else if (node.source) { 228 | // export { foo, bar } from './foo' 229 | const importId = defineImport(node, node.source.value) 230 | for (const spec of node.specifiers) { 231 | defineExport( 232 | (spec.exported as Identifier).name, 233 | `${importId}.${(spec as ExportSpecifier).local.name}`, 234 | ) 235 | } 236 | s.remove(node.start!, node.end!) 237 | } 238 | else { 239 | // export { foo, bar } 240 | for (const spec of node.specifiers) { 241 | const local = (spec as ExportSpecifier).local.name 242 | const binding = idToImportMap.get(local) 243 | defineExport((spec.exported as Identifier).name, binding || local) 244 | } 245 | s.remove(node.start!, node.end!) 246 | } 247 | } 248 | 249 | // default export 250 | if (node.type === 'ExportDefaultDeclaration') { 251 | if ('id' in node.declaration && node.declaration.id) { 252 | // named hoistable/class exports 253 | // export default function foo() {} 254 | // export default class A {} 255 | const { name } = node.declaration.id 256 | s.remove(node.start!, node.start! + 15) 257 | s.append(`\n${exportKey}(${moduleKey}, "default", () => ${name})`) 258 | } 259 | else { 260 | // anonymous default exports 261 | s.overwrite(node.start!, node.start! + 14, `${moduleKey}.default =`) 262 | } 263 | } 264 | 265 | // export * from './foo' 266 | if (node.type === 'ExportAllDeclaration') { 267 | const importId = defineImport(node, node.source.value) 268 | s.remove(node.start!, node.end!) 269 | s.append(`\nfor (const key in ${importId}) { 270 | if (key !== 'default') { 271 | ${exportKey}(${moduleKey}, key, () => ${importId}[key]) 272 | } 273 | }`) 274 | } 275 | } 276 | 277 | // 3. convert references to import bindings 278 | for (const node of ast) { 279 | if (node.type === 'ImportDeclaration') 280 | continue 281 | walkIdentifiers(node, (id, parent, parentStack) => { 282 | const binding = idToImportMap.get(id.name) 283 | if (!binding) { 284 | return 285 | } 286 | if (parent && isStaticProperty(parent) && parent.shorthand) { 287 | // let binding used in a property shorthand 288 | // { foo } -> { foo: __import_x__.foo } 289 | // skip for destructure patterns 290 | if ( 291 | !(parent as any).inPattern 292 | || isInDestructureAssignment(parent, parentStack) 293 | ) { 294 | s.appendLeft(id.end!, `: ${binding}`) 295 | } 296 | } 297 | else if ( 298 | parent 299 | && parent.type === 'ClassDeclaration' 300 | && id === parent.superClass 301 | ) { 302 | if (!declaredConst.has(id.name)) { 303 | declaredConst.add(id.name) 304 | // locate the top-most node containing the class declaration 305 | const topNode = parentStack[1] 306 | s.prependRight(topNode.start!, `const ${id.name} = ${binding};\n`) 307 | } 308 | } 309 | else { 310 | s.overwrite(id.start!, id.end!, binding) 311 | } 312 | }) 313 | } 314 | 315 | // 4. convert dynamic imports 316 | let hasDynamicImport = false 317 | walk(ast, { 318 | enter(node: Node, parent: Node) { 319 | if (node.type === 'Import' && parent.type === 'CallExpression') { 320 | const arg = parent.arguments[0] 321 | if (arg.type === 'StringLiteral' && arg.value.startsWith('./')) { 322 | hasDynamicImport = true 323 | s.overwrite(node.start!, node.start! + 6, dynamicImportKey) 324 | s.overwrite( 325 | arg.start!, 326 | arg.end!, 327 | JSON.stringify(arg.value.replace(/^\.\/+/, 'src/')), 328 | ) 329 | } 330 | } 331 | }, 332 | }) 333 | 334 | return { 335 | code: s.toString(), 336 | importedFiles, 337 | hasDynamicImport, 338 | } 339 | } 340 | 341 | // eslint-disable-next-line regexp/no-useless-assertions, regexp/match-any 342 | const scriptRE = /]*>|>)([^]*?)<\/script>/gi 343 | const scriptModuleRE 344 | // eslint-disable-next-line regexp/no-contradiction-with-assertion, regexp/match-any 345 | = /]*type\s*=\s*(?:"module"|'module')[^>]*>([^]*?)<\/script>/gi 346 | 347 | function processHtmlFile( 348 | options: { 349 | files: Record 350 | mainFile: string 351 | }, 352 | src: string, 353 | filename: string, 354 | processed: string[], 355 | seen: Set, 356 | ) { 357 | const deps: string[] = [] 358 | let jsCode = '' 359 | const html = src 360 | .replace(scriptModuleRE, (_, content) => { 361 | const { code, importedFiles, hasDynamicImport } = processModule( 362 | options, 363 | content, 364 | filename, 365 | ) 366 | processChildFiles( 367 | options, 368 | importedFiles, 369 | hasDynamicImport, 370 | deps, 371 | seen, 372 | false, 373 | ) 374 | jsCode += `\n${code}` 375 | return '' 376 | }) 377 | .replace(scriptRE, (_, content) => { 378 | jsCode += `\n${content}` 379 | return '' 380 | }) 381 | processed.push(`document.body.innerHTML = ${JSON.stringify(html)}`) 382 | processed.push(...deps) 383 | processed.push(jsCode) 384 | } 385 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/vue.worker.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/f2b38cf978abb9c21c6c788589b4599b4ff85a7d/src/monaco/vue.worker.ts 2 | 3 | import type { 4 | Language, 5 | LanguageServiceEnvironment, 6 | } from '@volar/monaco/worker' 7 | import type { VueCompilerOptions } from '@vue/language-core' 8 | import type { 9 | LanguageService, 10 | } from '@vue/language-service' 11 | import type * as monaco from 'monaco-editor-core' 12 | 13 | import type { WorkerHost, WorkerMessage } from './env' 14 | 15 | import { createNpmFileSystem } from '@volar/jsdelivr' 16 | import { 17 | createTypeScriptWorkerLanguageService, 18 | } from '@volar/monaco/worker' 19 | import { 20 | createVueLanguagePlugin, 21 | generateGlobalTypes, 22 | getDefaultCompilerOptions, 23 | getGlobalTypesFileName, 24 | 25 | VueVirtualCode, 26 | } from '@vue/language-core' 27 | import { 28 | createVueLanguageServicePlugins, 29 | } from '@vue/language-service' 30 | import { createVueLanguageServiceProxy } from '@vue/typescript-plugin/lib/common' 31 | import { getComponentDirectives } from '@vue/typescript-plugin/lib/requests/getComponentDirectives' 32 | import { getComponentEvents } from '@vue/typescript-plugin/lib/requests/getComponentEvents' 33 | import { getComponentNames } from '@vue/typescript-plugin/lib/requests/getComponentNames' 34 | import { getComponentProps } from '@vue/typescript-plugin/lib/requests/getComponentProps' 35 | import { getComponentSlots } from '@vue/typescript-plugin/lib/requests/getComponentSlots' 36 | import { getElementAttrs } from '@vue/typescript-plugin/lib/requests/getElementAttrs' 37 | import { getElementNames } from '@vue/typescript-plugin/lib/requests/getElementNames' 38 | import { getPropertiesAtLocation } from '@vue/typescript-plugin/lib/requests/getPropertiesAtLocation' 39 | import { create as createTypeScriptDirectiveCommentPlugin } from 'volar-service-typescript/lib/plugins/directiveComment' 40 | import { create as createTypeScriptSemanticPlugin } from 'volar-service-typescript/lib/plugins/semantic' 41 | import { URI } from 'vscode-uri' 42 | 43 | import * as worker from 'monaco-editor-core/esm/vs/editor/editor.worker' 44 | 45 | export interface CreateData { 46 | tsconfig: { 47 | compilerOptions?: import('typescript').CompilerOptions 48 | vueCompilerOptions?: Partial 49 | } 50 | dependencies: Record 51 | } 52 | 53 | const asFileName = (uri: URI) => uri.path 54 | const asUri = (fileName: string): URI => URI.file(fileName) 55 | 56 | let ts: typeof import('typescript') 57 | let locale: string | undefined 58 | 59 | // eslint-disable-next-line no-restricted-globals 60 | self.onmessage = async (msg: MessageEvent) => { 61 | if (msg.data?.event === 'init') { 62 | locale = msg.data.tsLocale 63 | ts = await importTsFromCdn(msg.data.tsVersion) 64 | // eslint-disable-next-line no-restricted-globals 65 | self.postMessage('inited') 66 | return 67 | } 68 | 69 | worker.initialize( 70 | ( 71 | ctx: monaco.worker.IWorkerContext, 72 | { tsconfig, dependencies }: CreateData, 73 | ) => { 74 | const env: LanguageServiceEnvironment = { 75 | workspaceFolders: [URI.file('/')], 76 | locale, 77 | fs: createNpmFileSystem( 78 | (uri) => { 79 | if (uri.scheme === 'file') { 80 | if (uri.path === '/node_modules') { 81 | return '' 82 | } 83 | else if (uri.path.startsWith('/node_modules/')) { 84 | return uri.path.slice('/node_modules/'.length) 85 | } 86 | } 87 | }, 88 | pkgName => dependencies[pkgName], 89 | (path, content) => { 90 | ctx.host.onFetchCdnFile( 91 | asUri(`/node_modules/${path}`).toString(), 92 | content, 93 | ) 94 | }, 95 | ), 96 | } 97 | 98 | const { options: compilerOptions } = ts.convertCompilerOptionsFromJson( 99 | tsconfig?.compilerOptions || {}, 100 | '', 101 | ) 102 | const vueCompilerOptions: VueCompilerOptions = { 103 | ...getDefaultCompilerOptions(), 104 | ...tsconfig.vueCompilerOptions, 105 | } 106 | setupGlobalTypes(vueCompilerOptions, env) 107 | 108 | const workerService = createTypeScriptWorkerLanguageService({ 109 | typescript: ts, 110 | compilerOptions, 111 | workerContext: ctx, 112 | env, 113 | uriConverter: { 114 | asFileName, 115 | asUri, 116 | }, 117 | languagePlugins: [ 118 | createVueLanguagePlugin( 119 | ts, 120 | compilerOptions, 121 | vueCompilerOptions, 122 | asFileName, 123 | ), 124 | ], 125 | languageServicePlugins: [ 126 | ...getTsLanguageServicePlugins(), 127 | ...getVueLanguageServicePlugins(), 128 | ], 129 | }) 130 | 131 | return workerService 132 | 133 | function setupGlobalTypes( 134 | options: VueCompilerOptions, 135 | env: LanguageServiceEnvironment, 136 | ) { 137 | const globalTypes = generateGlobalTypes(options) 138 | const globalTypesPath 139 | = `/node_modules/${getGlobalTypesFileName(options)}` 140 | options.globalTypesPath = () => globalTypesPath 141 | const { stat, readFile } = env.fs! 142 | const ctime = Date.now() 143 | env.fs!.stat = async (uri) => { 144 | if (uri.path === globalTypesPath) { 145 | return { 146 | type: 1, 147 | ctime, 148 | mtime: ctime, 149 | size: globalTypes.length, 150 | } 151 | } 152 | return stat(uri) 153 | } 154 | env.fs!.readFile = async (uri) => { 155 | if (uri.path === globalTypesPath) { 156 | return globalTypes 157 | } 158 | return readFile(uri) 159 | } 160 | } 161 | 162 | function getTsLanguageServicePlugins() { 163 | const semanticPlugin = createTypeScriptSemanticPlugin(ts) 164 | const { create } = semanticPlugin 165 | semanticPlugin.create = (context) => { 166 | const created = create(context) 167 | const ls = created.provide[ 168 | 'typescript/languageService' 169 | ]() as import('typescript').LanguageService 170 | const proxy = createVueLanguageServiceProxy( 171 | ts, 172 | new Proxy( 173 | {}, 174 | { 175 | get(_target, prop, receiver) { 176 | return Reflect.get(context.language, prop, receiver) 177 | }, 178 | }, 179 | ) as unknown as Language, 180 | ls, 181 | vueCompilerOptions, 182 | asUri, 183 | ) 184 | ls.getCompletionsAtPosition = proxy.getCompletionsAtPosition 185 | ls.getCompletionEntryDetails = proxy.getCompletionEntryDetails 186 | ls.getCodeFixesAtPosition = proxy.getCodeFixesAtPosition 187 | ls.getDefinitionAndBoundSpan = proxy.getDefinitionAndBoundSpan 188 | ls.getQuickInfoAtPosition = proxy.getQuickInfoAtPosition 189 | return created 190 | } 191 | return [semanticPlugin, createTypeScriptDirectiveCommentPlugin()] 192 | } 193 | 194 | function getVueLanguageServicePlugins() { 195 | const plugins = createVueLanguageServicePlugins(ts, { 196 | getComponentDirectives(fileName) { 197 | return getComponentDirectives(ts, getProgram(), fileName) 198 | }, 199 | getComponentEvents(fileName, tag) { 200 | return getComponentEvents(ts, getProgram(), fileName, tag) 201 | }, 202 | getComponentNames(fileName) { 203 | return getComponentNames(ts, getProgram(), fileName) 204 | }, 205 | getComponentProps(fileName, tag) { 206 | return getComponentProps(ts, getProgram(), fileName, tag) 207 | }, 208 | getComponentSlots(fileName) { 209 | const { virtualCode } = getVirtualCode(fileName) 210 | return getComponentSlots(ts, getProgram(), virtualCode) 211 | }, 212 | getElementAttrs(fileName, tag) { 213 | return getElementAttrs(ts, getProgram(), fileName, tag) 214 | }, 215 | getElementNames(fileName) { 216 | return getElementNames(ts, getProgram(), fileName) 217 | }, 218 | getPropertiesAtLocation(fileName, position) { 219 | const { sourceScript, virtualCode } = getVirtualCode(fileName) 220 | return getPropertiesAtLocation( 221 | ts, 222 | getLanguageService().context.language, 223 | getProgram(), 224 | sourceScript, 225 | virtualCode, 226 | position, 227 | false, 228 | ) 229 | }, 230 | async getQuickInfoAtPosition(fileName, position) { 231 | const uri = asUri(fileName) 232 | const sourceScript 233 | = getLanguageService().context.language.scripts.get(uri) 234 | if (!sourceScript) { 235 | return 236 | } 237 | const hover = await getLanguageService().getHover(uri, position) 238 | let text = '' 239 | if (typeof hover?.contents === 'string') { 240 | text = hover.contents 241 | } 242 | else if (Array.isArray(hover?.contents)) { 243 | text = hover.contents 244 | .map(c => (typeof c === 'string' ? c : c.value)) 245 | .join('\n') 246 | } 247 | else if (hover) { 248 | text = hover.contents.value 249 | } 250 | text = text.replace(/```typescript/g, '') 251 | text = text.replace(/```/g, '') 252 | text = text.replace(/---/g, '') 253 | text = text.trim() 254 | while (true) { 255 | const newText = text.replace(/\n\n/g, '\n') 256 | if (newText === text) { 257 | break 258 | } 259 | text = newText 260 | } 261 | text = text.replace(/\n/g, ' | ') 262 | return text 263 | }, 264 | collectExtractProps() { 265 | throw new Error('Not implemented') 266 | }, 267 | getImportPathForFile() { 268 | throw new Error('Not implemented') 269 | }, 270 | getDocumentHighlights() { 271 | throw new Error('Not implemented') 272 | }, 273 | getEncodedSemanticClassifications() { 274 | throw new Error('Not implemented') 275 | }, 276 | }) 277 | const ignoreVueServicePlugins = new Set([ 278 | 'vue-extract-file', 279 | 'vue-document-drop', 280 | 'vue-document-highlights', 281 | 'typescript-semantic-tokens', 282 | ]) 283 | return plugins.filter( 284 | plugin => !ignoreVueServicePlugins.has(plugin.name!), 285 | ) 286 | 287 | function getVirtualCode(fileName: string) { 288 | const uri = asUri(fileName) 289 | const sourceScript 290 | = getLanguageService().context.language.scripts.get(uri) 291 | if (!sourceScript) { 292 | throw new Error(`No source script found for file: ${fileName}`) 293 | } 294 | const virtualCode = sourceScript.generated?.root 295 | if (!(virtualCode instanceof VueVirtualCode)) { 296 | throw new TypeError(`No virtual code found for file: ${fileName}`) 297 | } 298 | return { 299 | sourceScript, 300 | virtualCode, 301 | } 302 | } 303 | 304 | function getProgram() { 305 | const tsService: import('typescript').LanguageService 306 | = getLanguageService().context.inject('typescript/languageService') 307 | return tsService.getProgram()! 308 | } 309 | 310 | function getLanguageService() { 311 | return (workerService as any).languageService as LanguageService 312 | } 313 | } 314 | }, 315 | ) 316 | } 317 | 318 | async function importTsFromCdn(tsVersion: string) { 319 | const _module = globalThis.module 320 | ;(globalThis as any).module = { exports: {} } 321 | const tsUrl = `https://cdn.jsdelivr.net/npm/typescript@${tsVersion}/lib/typescript.js` 322 | await import(/* @vite-ignore */ tsUrl) 323 | const ts = globalThis.module.exports 324 | globalThis.module = _module 325 | return ts as typeof import('typescript') 326 | } 327 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/transform.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable regexp/no-unused-capturing-group */ 2 | 3 | // https://github.com/vuejs/repl/blob/69c2ed1dca84132708c3b9a1d0a008e11be2be74/src/transform.ts 4 | 5 | import type { 6 | BindingMetadata, 7 | CompilerOptions, 8 | SFCDescriptor, 9 | } from 'vue/compiler-sfc' 10 | 11 | import type { File, Store } from './store' 12 | 13 | import hashId from 'hash-sum' 14 | 15 | import { transformTS } from '@velin-dev/utils/transformers/typescript' 16 | 17 | import { getSourceMap, toVisualizer, trimAnalyzedBindings } from './sourcemap' 18 | 19 | export const COMP_IDENTIFIER = `__sfc__` 20 | 21 | const REGEX_JS = /\.[jt]sx?$/ 22 | function testTs(filename: string | undefined | null) { 23 | return !!(filename && /(\.|\b)tsx?$/.test(filename)) 24 | } 25 | function testJsx(filename: string | undefined | null) { 26 | return !!(filename && /(\.|\b)[jt]sx$/.test(filename)) 27 | } 28 | 29 | export async function compileFile( 30 | store: Store, 31 | { filename, code, compiled }: File, 32 | ): Promise<(string | Error)[]> { 33 | if (!code.trim()) { 34 | return [] 35 | } 36 | 37 | if (filename.endsWith('.css')) { 38 | compiled.css = code 39 | return [] 40 | } 41 | 42 | if (REGEX_JS.test(filename)) { 43 | const isJSX = testJsx(filename) 44 | if (testTs(filename)) { 45 | code = transformTS(code, isJSX) 46 | } 47 | if (isJSX) { 48 | // code = await import('./jsx').then(({ transformJSX }) => 49 | // transformJSX(code), 50 | // ) 51 | console.error('JSX transform not supported in the playground') 52 | } 53 | compiled.js = compiled.ssr = code 54 | return [] 55 | } 56 | 57 | if (filename.endsWith('.json')) { 58 | let parsed 59 | try { 60 | parsed = JSON.parse(code) 61 | } 62 | catch (err: any) { 63 | console.error(`Error parsing ${filename}`, err.message) 64 | return [err.message] 65 | } 66 | compiled.js = compiled.ssr = `export default ${JSON.stringify(parsed)}` 67 | return [] 68 | } 69 | 70 | if (!filename.endsWith('.vue')) { 71 | return [] 72 | } 73 | 74 | const id = hashId(filename) 75 | const { errors, descriptor } = store.compiler.parse(code, { 76 | filename, 77 | sourceMap: true, 78 | templateParseOptions: store.sfcOptions?.template?.compilerOptions, 79 | }) 80 | if (errors.length) { 81 | return errors 82 | } 83 | 84 | const styleLangs = descriptor.styles.map(s => s.lang).filter(Boolean) 85 | const templateLang = descriptor.template?.lang 86 | if (styleLangs.length && templateLang) { 87 | return [ 88 | `lang="${styleLangs.join( 89 | ',', 90 | )}" pre-processors for