├── .editorconfig ├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── apps └── playground │ ├── index.html │ ├── netlify.toml │ ├── package.json │ ├── public │ ├── apple-touch-icon.png │ ├── favicon.ico │ ├── favicon.png │ ├── favicon.svg │ ├── site.webmanifest │ ├── web-app-manifest-192x192.png │ └── web-app-manifest-512x512.png │ ├── src │ ├── App.vue │ ├── components │ │ ├── Editor │ │ │ ├── env.ts │ │ │ ├── highlight.ts │ │ │ ├── import-map.ts │ │ │ ├── index.vue │ │ │ ├── language-configs.ts │ │ │ ├── store.ts │ │ │ ├── transform.ts │ │ │ ├── utils.ts │ │ │ └── vue.worker.ts │ │ ├── Input.vue │ │ ├── Playground.vue │ │ └── Switch.vue │ ├── main.ts │ ├── pages │ │ └── index.vue │ ├── prompts │ │ └── Prompt.velin.vue │ ├── styles │ │ └── themes.css │ ├── types │ │ └── vue-repl.ts │ └── utils │ │ └── vue-repl.ts │ ├── tsconfig.json │ ├── uno.config.ts │ └── vite.config.ts ├── bump.config.ts ├── cspell.config.yaml ├── docs ├── assets │ ├── dark-playground.png │ └── light-playground.png └── public │ └── logo.svg ├── eslint.config.ts ├── examples ├── native-node │ ├── package.json │ ├── src │ │ ├── assets │ │ │ ├── MyComponent.vue │ │ │ ├── composable.md │ │ │ ├── markdown.md │ │ │ └── task.ts │ │ ├── md.ts │ │ └── sfc.ts │ └── tsconfig.json └── vite-browser │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── shim.d.ts │ ├── src │ ├── App.vue │ ├── assets │ │ ├── Markdown.velin.md │ │ ├── Prompt.velin.vue │ │ ├── Promptv2.velin.vue │ │ ├── TaskMarkdown.ts │ │ ├── task.ts │ │ └── vue.svg │ ├── main.ts │ └── types │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages ├── core │ ├── README.md │ ├── package.json │ ├── src │ │ ├── browser.ts │ │ ├── index.ts │ │ ├── render-browser │ │ │ ├── index.ts │ │ │ ├── markdown.browser.test.ts │ │ │ ├── markdown.ts │ │ │ ├── sfc.browser.test.ts │ │ │ ├── sfc.ts │ │ │ └── testdata │ │ │ │ ├── script-setup-with-props.velin.vue │ │ │ │ ├── script-setup.velin.md │ │ │ │ ├── script-setup.velin.vue │ │ │ │ ├── simple.velin.md │ │ │ │ └── simple.velin.vue │ │ ├── render-node │ │ │ ├── index.ts │ │ │ ├── markdown.test.ts │ │ │ ├── markdown.ts │ │ │ ├── sfc.test.ts │ │ │ ├── sfc.ts │ │ │ └── testdata │ │ │ │ ├── script-setup-with-props.velin.vue │ │ │ │ ├── script-setup.velin.md │ │ │ │ ├── script-setup.velin.vue │ │ │ │ ├── simple.velin.md │ │ │ │ └── simple.velin.vue │ │ ├── render-repl │ │ │ ├── index.ts │ │ │ └── sfc.ts │ │ ├── render-shared │ │ │ ├── compile.ts │ │ │ ├── component.ts │ │ │ ├── index.ts │ │ │ ├── props.ts │ │ │ └── template.ts │ │ └── types │ │ │ └── index.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── utils │ ├── README.md │ ├── package.json │ ├── src │ │ ├── from-md │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── to-md │ │ │ └── index.ts │ │ ├── transformers │ │ │ ├── typescript │ │ │ │ ├── index.ts │ │ │ │ ├── transform.test.ts │ │ │ │ └── transform.ts │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ ├── moduleCompiler.ts │ │ │ │ └── shared.ts │ │ └── vue-sfc │ │ │ └── index.ts │ └── tsconfig.json └── vue │ ├── README.md │ ├── package.json │ ├── src │ ├── composables │ │ ├── index.ts │ │ └── usePrompt │ │ │ └── index.ts │ ├── index.ts │ └── repl │ │ ├── composables │ │ ├── index.ts │ │ └── usePrompt │ │ │ └── index.ts │ │ └── index.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json ├── uno.config.ts └── vitest.workspace.ts /.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 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [luoling8192, nekomeowww] 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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@v4 19 | with: 20 | fetch-depth: 0 21 | - uses: pnpm/action-setup@v4 22 | with: 23 | run_install: false 24 | - uses: actions/setup-node@v4 25 | with: 26 | cache: pnpm 27 | node-version: latest 28 | registry-url: https://registry.npmjs.org 29 | - run: pnpm i 30 | - run: pnpm dlx changelogithub 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | - run: pnpm -r build 34 | - run: pnpm -r publish --no-git-checks --access public 35 | env: 36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | NPM_CONFIG_PROVENANCE: true 38 | -------------------------------------------------------------------------------- /.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 | 18 | dist 19 | out/ 20 | *.local 21 | *.log 22 | **/.cache/** 23 | **/temp/ 24 | .cache 25 | 26 | .coverage 27 | .coverage.* 28 | coverage/ 29 | cover/ 30 | htmlcov/ 31 | 32 | *.pcm 33 | *.wav 34 | *.ogg 35 | *.mp3 36 | 37 | # Make it easy for devenv users to override their local environment. 38 | # See: https://github.com/moeru-ai/airi/pull/110#discussion_r2024378953 39 | .direnv 40 | .pre-commit-config.yaml 41 | .envrc 42 | .devenv* 43 | devenv.* 44 | 45 | # pixi environments 46 | .pixi 47 | *.egg-info 48 | 49 | # Byte-compiled / optimized / DLL files 50 | __pycache__/ 51 | *.py[cod] 52 | *$py.class 53 | 54 | # C extensions 55 | *.so 56 | 57 | # Distribution / packaging 58 | .Python 59 | build/ 60 | develop-eggs/ 61 | dist/ 62 | downloads/ 63 | eggs/ 64 | .eggs/ 65 | lib/ 66 | lib64/ 67 | parts/ 68 | sdist/ 69 | var/ 70 | wheels/ 71 | share/python-wheels/ 72 | *.egg-info/ 73 | .installed.cfg 74 | *.egg 75 | MANIFEST 76 | 77 | # PyInstaller 78 | # Usually these files are written by a python script from a template 79 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 80 | *.manifest 81 | *.spec 82 | 83 | # Installer logs 84 | pip-log.txt 85 | pip-delete-this-directory.txt 86 | 87 | # Unit test / coverage reports 88 | .tox/ 89 | .nox/ 90 | nosetests.xml 91 | coverage.xml 92 | *.cover 93 | *.py,cover 94 | .pytest_cache/ 95 | .hypothesis/ 96 | 97 | # Translations 98 | *.mo 99 | *.pot 100 | 101 | # Django stuff: 102 | *.log 103 | local_settings.py 104 | db.sqlite3 105 | db.sqlite3-journal 106 | 107 | # Flask stuff: 108 | instance/ 109 | .webassets-cache 110 | 111 | # Scrapy stuff: 112 | .scrapy 113 | 114 | # Sphinx documentation 115 | docs/_build/ 116 | 117 | # PyBuilder 118 | .pybuilder/ 119 | target/ 120 | 121 | # Jupyter Notebook 122 | .ipynb_checkpoints 123 | 124 | # IPython 125 | profile_default/ 126 | ipython_config.py 127 | 128 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 129 | __pypackages__/ 130 | 131 | # Celery stuff 132 | celerybeat-schedule 133 | celerybeat.pid 134 | 135 | # SageMath parsed files 136 | *.sage.py 137 | 138 | # Environments 139 | .venv 140 | env/ 141 | venv/ 142 | ENV/ 143 | env.bak/ 144 | venv.bak/ 145 | 146 | # Spyder project settings 147 | .spyderproject 148 | .spyproject 149 | 150 | # Rope project settings 151 | .ropeproject 152 | 153 | # mkdocs documentation 154 | /site 155 | 156 | # mypy 157 | .mypy_cache/ 158 | .dmypy.json 159 | dmypy.json 160 | 161 | # Pyre type checker 162 | .pyre/ 163 | 164 | # pytype static type analyzer 165 | .pytype/ 166 | 167 | # Cython debug symbols 168 | cython_debug/ 169 | 170 | # PyCharm 171 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 172 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 173 | # and can be added to the global gitignore or merged into this file. For a more nuclear 174 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 175 | #.idea/ 176 | 177 | # Ruff stuff: 178 | .ruff_cache/ 179 | 180 | # PyPI configuration file 181 | .pypirc 182 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | import { renderSFCString } from '@velin-dev/core' 74 | import { ref } from 'vue' 75 | 76 | const source = await readFile('./Prompt.vue', 'utf-8') 77 | const name = ref('Velin') 78 | const result = await renderSFCString(source, { name }) 79 | 80 | console.log(result) 81 | // Hello world, this is Velin! 82 | ``` 83 | 84 | ### In Vue / Browser 85 | 86 | ```vue 87 | 101 | ``` 102 | 103 | ## Development 104 | 105 | ### Clone 106 | 107 | ```shell 108 | git clone https://github.com/luoling8192/velin.git 109 | cd airi 110 | ``` 111 | 112 | ### Install dependencies 113 | 114 | ```shell 115 | corepack enable 116 | pnpm install 117 | ``` 118 | 119 | > [!NOTE] 120 | > 121 | > We would recommend to install [@antfu/ni](https://github.com/antfu-collective/ni) to make your script simpler. 122 | > 123 | > ```shell 124 | > corepack enable 125 | > npm i -g @antfu/ni 126 | > ``` 127 | > 128 | > Once installed, you can 129 | > 130 | > - use `ni` for `pnpm install`, `npm install` and `yarn install`. 131 | > - use `nr` for `pnpm run`, `npm run` and `yarn run`. 132 | > 133 | > You don't need to care about the package manager, `ni` will help you choose the right one. 134 | 135 | ```shell 136 | pnpm dev 137 | ``` 138 | 139 | > [!NOTE] 140 | > 141 | > For [@antfu/ni](https://github.com/antfu-collective/ni) users, you can 142 | > 143 | > ```shell 144 | > nr dev 145 | > ``` 146 | 147 | ### Build 148 | 149 | ```shell 150 | pnpm build 151 | ``` 152 | 153 | > [!NOTE] 154 | > 155 | > For [@antfu/ni](https://github.com/antfu-collective/ni) users, you can 156 | > 157 | > ```shell 158 | > nr build 159 | > ``` 160 | 161 | ## License 162 | 163 | MIT 164 | 165 | [npm-version-src]: https://img.shields.io/npm/v/@velin-dev/core?style=flat&colorA=080f12&colorB=1fa669 166 | [npm-version-href]: https://npmjs.com/package/@velin-dev/core 167 | [npm-downloads-src]: https://img.shields.io/npm/dm/@velin-dev/core?style=flat&colorA=080f12&colorB=1fa669 168 | [npm-downloads-href]: https://npmjs.com/package/@velin-dev/core 169 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@velin-dev/vue?style=flat&colorA=080f12&colorB=1fa669&label=minzip 170 | [bundle-href]: https://bundlephobia.com/result?p=@velin-dev/vue 171 | [license-src]: https://img.shields.io/github/license/luoling8192/velin.svg?style=flat&colorA=080f12&colorB=1fa669 172 | [license-href]: https://github.com/luoling8192/velin/blob/main/LICENSE 173 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 174 | [jsdocs-href]: https://www.jsdocs.io/package/@velin-dev/core 175 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /apps/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@velin-dev/apps-playground", 3 | "type": "module", 4 | "private": true, 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/luoling8192/velin", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/luoling8192/velin.git", 23 | "directory": "playground" 24 | }, 25 | "bugs": "https://github.com/luoling8192/velin/issues", 26 | "scripts": { 27 | "build": "vite build", 28 | "dev": "vite", 29 | "preview": "vite preview", 30 | "typecheck": "vue-tsc --noEmit" 31 | }, 32 | "devDependencies": { 33 | "@iconify-json/carbon": "^1.2.8", 34 | "@iconify-json/line-md": "^1.2.8", 35 | "@iconify-json/simple-icons": "^1.2.35", 36 | "@iconify-json/solar": "^1.2.2", 37 | "@iconify-json/svg-spinners": "^1.2.2", 38 | "@proj-airi/lobe-icons": "^1.0.6", 39 | "@proj-airi/ui": "^0.5.0", 40 | "@shikijs/core": "^3.4.2", 41 | "@shikijs/engine-javascript": "^3.4.2", 42 | "@shikijs/langs": "^3.4.2", 43 | "@shikijs/monaco": "^3.4.2", 44 | "@shikijs/themes": "^3.4.2", 45 | "@unocss/reset": "^66.1.2", 46 | "@velin-dev/core": "workspace:^", 47 | "@velin-dev/utils": "workspace:^", 48 | "@velin-dev/vue": "workspace:^", 49 | "@vitejs/plugin-vue": "^5.2.4", 50 | "@volar/jsdelivr": "^2.4.14", 51 | "@volar/monaco": "^2.4.14", 52 | "@vue/language-service": "^2.2.10", 53 | "@vue/server-renderer": "^3.5.14", 54 | "@vueuse/core": "^13.2.0", 55 | "@vueuse/motion": "^3.0.3", 56 | "@xsai/shared": "^0.2.2", 57 | "@xsai/stream-text": "^0.2.2", 58 | "es-toolkit": "^1.38.0", 59 | "fflate": "^0.8.2", 60 | "hash-sum": "^2.0.0", 61 | "monaco-editor-core": "^0.52.2", 62 | "reka-ui": "^2.2.1", 63 | "splitpanes": "^4.0.3", 64 | "unplugin-vue-router": "^0.12.0", 65 | "vite": "^6.3.5", 66 | "vscode-uri": "^3.1.0", 67 | "vue": "^3.5.14", 68 | "vue-router": "^4.5.1", 69 | "vue-tsc": "^3.0.0-alpha.6" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/playground/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoling8192/velin/5b3c6daa3a635298e169f9f6a35575f44945c6d4/apps/playground/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoling8192/velin/5b3c6daa3a635298e169f9f6a35575f44945c6d4/apps/playground/public/favicon.ico -------------------------------------------------------------------------------- /apps/playground/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoling8192/velin/5b3c6daa3a635298e169f9f6a35575f44945c6d4/apps/playground/public/favicon.png -------------------------------------------------------------------------------- /apps/playground/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/public/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoling8192/velin/5b3c6daa3a635298e169f9f6a35575f44945c6d4/apps/playground/public/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /apps/playground/public/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoling8192/velin/5b3c6daa3a635298e169f9f6a35575f44945c6d4/apps/playground/public/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /apps/playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 48 | 49 | 69 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/env.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/monaco/env.ts 2 | 3 | import type { WorkerLanguageService } from '@volar/monaco/worker' 4 | import type { Store } from './store' 5 | import type { CreateData } from './vue.worker' 6 | 7 | import * as volar from '@volar/monaco' 8 | import { editor, languages, Uri } from 'monaco-editor-core' 9 | import EditorWorker from 'monaco-editor-core/esm/vs/editor/editor.worker?worker' 10 | import { watchEffect } from 'vue' 11 | 12 | import { debounce } from '../../utils/vue-repl' 13 | import * as languageConfigs from './language-configs' 14 | import { getOrCreateModel } from './utils' 15 | import VueWorker from './vue.worker?worker' 16 | 17 | let initted = false 18 | export function initMonaco(store: Store) { 19 | if (initted) 20 | return 21 | loadMonacoEnv(store) 22 | 23 | watchEffect(() => { 24 | // create a model for each file in the store 25 | for (const filename in store.files) { 26 | const file = store.files[filename] 27 | if (editor.getModel(Uri.parse(`file:///${filename}`))) 28 | continue 29 | getOrCreateModel( 30 | Uri.parse(`file:///${filename}`), 31 | file.language, 32 | file.code, 33 | ) 34 | } 35 | 36 | // dispose of any models that are not in the store 37 | for (const model of editor.getModels()) { 38 | const uri = model.uri.toString() 39 | if (store.files[uri.substring('file:///'.length)]) 40 | continue 41 | 42 | if (uri.startsWith('file:///node_modules')) 43 | continue 44 | if (uri.startsWith('inmemory://')) 45 | continue 46 | 47 | model.dispose() 48 | } 49 | }) 50 | 51 | initted = true 52 | } 53 | 54 | export class WorkerHost { 55 | onFetchCdnFile(uri: string, text: string) { 56 | getOrCreateModel(Uri.parse(uri), undefined, text) 57 | } 58 | } 59 | 60 | let disposeVue: undefined | (() => void) 61 | export async function reloadLanguageTools(store: Store) { 62 | disposeVue?.() 63 | 64 | let dependencies: Record = { 65 | ...store.dependencyVersion, 66 | } 67 | 68 | if (store.vueVersion) { 69 | dependencies = { 70 | ...dependencies, 71 | 'vue': store.vueVersion, 72 | '@vue/compiler-core': store.vueVersion, 73 | '@vue/compiler-dom': store.vueVersion, 74 | '@vue/compiler-sfc': store.vueVersion, 75 | '@vue/compiler-ssr': store.vueVersion, 76 | '@vue/reactivity': store.vueVersion, 77 | '@vue/runtime-core': store.vueVersion, 78 | '@vue/runtime-dom': store.vueVersion, 79 | '@vue/shared': store.vueVersion, 80 | } 81 | } 82 | 83 | if (store.typescriptVersion) { 84 | dependencies = { 85 | ...dependencies, 86 | typescript: store.typescriptVersion, 87 | } 88 | } 89 | 90 | const worker = editor.createWebWorker({ 91 | moduleId: 'vs/language/vue/vueWorker', 92 | label: 'vue', 93 | host: new WorkerHost(), 94 | createData: { 95 | tsconfig: store.getTsConfig?.() || {}, 96 | dependencies, 97 | } satisfies CreateData, 98 | }) 99 | const languageId = ['vue', 'javascript', 'typescript'] 100 | const getSyncUris = () => 101 | Object.keys(store.files).map(filename => Uri.parse(`file:///${filename}`)) 102 | 103 | const { dispose: disposeMarkers } = volar.activateMarkers( 104 | worker, 105 | languageId, 106 | 'vue', 107 | getSyncUris, 108 | editor, 109 | ) 110 | const { dispose: disposeAutoInsertion } = volar.activateAutoInsertion( 111 | worker, 112 | languageId, 113 | getSyncUris, 114 | editor, 115 | ) 116 | const { dispose: disposeProvides } = await volar.registerProviders( 117 | worker, 118 | languageId, 119 | getSyncUris, 120 | languages, 121 | ) 122 | 123 | disposeVue = () => { 124 | disposeMarkers() 125 | disposeAutoInsertion() 126 | disposeProvides() 127 | } 128 | } 129 | 130 | export interface WorkerMessage { 131 | event: 'init' 132 | tsVersion: string 133 | tsLocale?: string 134 | } 135 | 136 | export function loadMonacoEnv(store: Store) { 137 | // eslint-disable-next-line no-restricted-globals 138 | ;(self as any).MonacoEnvironment = { 139 | async getWorker(_: any, label: string) { 140 | if (label === 'vue') { 141 | const worker = new VueWorker() 142 | const init = new Promise((resolve) => { 143 | worker.addEventListener('message', (data) => { 144 | if (data.data === 'inited') { 145 | resolve() 146 | } 147 | }) 148 | worker.postMessage({ 149 | event: 'init', 150 | tsVersion: store.typescriptVersion, 151 | tsLocale: store.locale, 152 | } satisfies WorkerMessage) 153 | }) 154 | await init 155 | return worker 156 | } 157 | return new EditorWorker() 158 | }, 159 | } 160 | languages.register({ id: 'vue', extensions: ['.vue'] }) 161 | languages.register({ id: 'javascript', extensions: ['.js'] }) 162 | languages.register({ id: 'typescript', extensions: ['.ts'] }) 163 | languages.register({ id: 'css', extensions: ['.css'] }) 164 | languages.setLanguageConfiguration('vue', languageConfigs.vue) 165 | languages.setLanguageConfiguration('javascript', languageConfigs.js) 166 | languages.setLanguageConfiguration('typescript', languageConfigs.ts) 167 | languages.setLanguageConfiguration('css', languageConfigs.css) 168 | 169 | let languageToolsPromise: Promise | undefined 170 | store.reloadLanguageTools = debounce(async () => { 171 | ;(languageToolsPromise ||= reloadLanguageTools(store)).finally(() => { 172 | languageToolsPromise = undefined 173 | }) 174 | }, 250) 175 | languages.onLanguage('vue', () => store.reloadLanguageTools!()) 176 | 177 | // Support for go to definition 178 | editor.registerEditorOpener({ 179 | openCodeEditor(_, resource) { 180 | if (resource.toString().startsWith('file:///node_modules')) { 181 | return true 182 | } 183 | 184 | const path = resource.path 185 | if (/^\//.test(path)) { 186 | const fileName = path.replace('/', '') 187 | if (fileName !== store.activeFile.filename) { 188 | store.setActive(fileName) 189 | return true 190 | } 191 | } 192 | 193 | return false 194 | }, 195 | }) 196 | } 197 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/highlight.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/monaco/highlight.ts 2 | 3 | import { createHighlighterCoreSync } from '@shikijs/core' 4 | import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript' 5 | import langJsx from '@shikijs/langs/jsx' 6 | import langTsx from '@shikijs/langs/tsx' 7 | import langVue from '@shikijs/langs/vue' 8 | import { shikiToMonaco } from '@shikijs/monaco' 9 | import themeLight from '@shikijs/themes/catppuccin-latte' 10 | import themeDark from '@shikijs/themes/catppuccin-mocha' 11 | import * as monaco from 'monaco-editor-core' 12 | 13 | let registered = false 14 | export function registerHighlighter() { 15 | if (!registered) { 16 | const highlighter = createHighlighterCoreSync({ 17 | themes: [themeDark, themeLight], 18 | langs: [langVue, langTsx, langJsx], 19 | engine: createJavaScriptRegexEngine(), 20 | }) 21 | monaco.languages.register({ id: 'vue' }) 22 | shikiToMonaco(highlighter, monaco) 23 | registered = true 24 | } 25 | 26 | return { 27 | light: themeLight.name!, 28 | dark: themeDark.name!, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/import-map.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/import-map.ts 2 | 3 | import { computed, version as currentVersion, ref } from 'vue' 4 | 5 | export function useVueImportMap( 6 | defaults: { 7 | runtimeDev?: string | (() => string) 8 | runtimeProd?: string | (() => string) 9 | serverRenderer?: string | (() => string) 10 | vueVersion?: string | null 11 | } = {}, 12 | ) { 13 | function normalizeDefaults(defaults?: string | (() => string)) { 14 | if (!defaults) 15 | return 16 | return typeof defaults === 'string' ? defaults : defaults() 17 | } 18 | 19 | const productionMode = ref(false) 20 | const vueVersion = ref(defaults.vueVersion || null) 21 | const importMap = computed(() => { 22 | const vue 23 | = (!vueVersion.value 24 | && normalizeDefaults( 25 | productionMode.value ? defaults.runtimeProd : defaults.runtimeDev, 26 | )) 27 | || `https://cdn.jsdelivr.net/npm/@vue/runtime-dom@${ 28 | vueVersion.value || currentVersion 29 | }/dist/runtime-dom.esm-browser${productionMode.value ? `.prod` : ``}.js` 30 | 31 | const serverRenderer 32 | = (!vueVersion.value && normalizeDefaults(defaults.serverRenderer)) 33 | || `https://cdn.jsdelivr.net/npm/@vue/server-renderer@${ 34 | vueVersion.value || currentVersion 35 | }/dist/server-renderer.esm-browser.js` 36 | return { 37 | imports: { 38 | vue, 39 | 'vue/server-renderer': serverRenderer, 40 | }, 41 | } 42 | }) 43 | 44 | return { 45 | productionMode, 46 | importMap, 47 | vueVersion, 48 | defaultVersion: currentVersion, 49 | } 50 | } 51 | 52 | export interface ImportMap { 53 | imports?: Record 54 | scopes?: Record> 55 | } 56 | 57 | export function mergeImportMap(a: ImportMap, b: ImportMap): ImportMap { 58 | return { 59 | imports: { ...a.imports, ...b.imports }, 60 | scopes: { ...a.scopes, ...b.scopes }, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/index.vue: -------------------------------------------------------------------------------- 1 | 181 | 182 | 190 | 191 | 207 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/language-configs.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable regexp/optimal-lookaround-quantifier */ 2 | /* eslint-disable regexp/no-trivially-nested-assertion */ 3 | /* eslint-disable regexp/prefer-character-class */ 4 | /* eslint-disable regexp/no-useless-character-class */ 5 | /* eslint-disable regexp/prefer-w */ 6 | /* eslint-disable regexp/no-useless-lazy */ 7 | /* eslint-disable regexp/no-useless-flag */ 8 | /* eslint-disable regexp/no-useless-assertions */ 9 | /* eslint-disable regexp/no-dupe-characters-character-class */ 10 | /* eslint-disable regexp/no-useless-non-capturing-group */ 11 | /* eslint-disable regexp/no-useless-escape */ 12 | /* eslint-disable regexp/strict */ 13 | /* eslint-disable regexp/no-super-linear-backtracking */ 14 | /* eslint-disable prefer-regex-literals */ 15 | 16 | // https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/monaco/language-configs.ts 17 | 18 | import { languages } from 'monaco-editor-core' 19 | 20 | // export const html: languages.LanguageConfiguration = { 21 | // comments: { 22 | // blockComment: [''], 23 | // }, 24 | // brackets: [ 25 | // [''], 26 | // ['{', '}'], 27 | // ['(', ')'], 28 | // ], 29 | // autoClosingPairs: [ 30 | // { open: '{', close: '}' }, 31 | // { open: '[', close: ']' }, 32 | // { open: '(', close: ')' }, 33 | // { open: "'", close: "'" }, 34 | // { open: '"', close: '"' }, 35 | // { open: '', notIn: ['comment', 'string'] }, 36 | // ], 37 | // surroundingPairs: [ 38 | // { open: "'", close: "'" }, 39 | // { open: '"', close: '"' }, 40 | // { open: '{', close: '}' }, 41 | // { open: '[', close: ']' }, 42 | // { open: '(', close: ')' }, 43 | // { open: '<', close: '>' }, 44 | // ], 45 | // colorizedBracketPairs: [], 46 | // folding: { 47 | // markers: { 48 | // start: /^\s*/, 49 | // end: /^\s*/, 50 | // }, 51 | // }, 52 | // wordPattern: new RegExp( 53 | // '(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\$\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\\'\\"\\,\\.\\<\\>\\/\\s]+)', 54 | // ), 55 | // onEnterRules: [ 56 | // { 57 | // beforeText: new RegExp( 58 | // '<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^\'"/>]|"[^"]*"|\'[^\']*\')*?(?!\\/)>)[^<]*$', 59 | // 'i', 60 | // ), 61 | // afterText: new RegExp('^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>', 'i'), 62 | // action: { 63 | // indentAction: languages.IndentAction.IndentOutdent, 64 | // }, 65 | // }, 66 | // { 67 | // beforeText: new RegExp( 68 | // '<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^\'"/>]|"[^"]*"|\'[^\']*\')*?(?!\\/)>)[^<]*$', 69 | // 'i', 70 | // ), 71 | // action: { 72 | // indentAction: languages.IndentAction.Indent, 73 | // }, 74 | // }, 75 | // ], 76 | // indentationRules: { 77 | // increaseIndentPattern: new RegExp( 78 | // '<(?!\\?|(?:area|base|br|col|frame|hr|html|img|input|keygen|link|menuitem|meta|param|source|track|wbr)\\b|[^>]*\\/>)([-_\\.A-Za-z0-9]+)(?=\\s|>)\\b[^>]*>(?!.*<\\/\\1>)|)|\\{[^}"\']*$', 79 | // ), 80 | // decreaseIndentPattern: new RegExp( 81 | // '^\\s*(<\\/(?!html)[-_\\.A-Za-z0-9]+\\b[^>]*>|-->|\\})', 82 | // ), 83 | // }, 84 | // } 85 | 86 | export const css: languages.LanguageConfiguration = { 87 | comments: { 88 | blockComment: ['/*', '*/'], 89 | }, 90 | brackets: [ 91 | ['{', '}'], 92 | ['[', ']'], 93 | ['(', ')'], 94 | ], 95 | autoClosingPairs: [ 96 | { open: '{', close: '}', notIn: ['string', 'comment'] }, 97 | { open: '[', close: ']', notIn: ['string', 'comment'] }, 98 | { open: '(', close: ')', notIn: ['string', 'comment'] }, 99 | { open: '"', close: '"', notIn: ['string', 'comment'] }, 100 | { open: '\'', close: '\'', notIn: ['string', 'comment'] }, 101 | ], 102 | surroundingPairs: [ 103 | { 104 | open: '\'', 105 | close: '\'', 106 | }, 107 | { 108 | open: '"', 109 | close: '"', 110 | }, 111 | { 112 | open: '{', 113 | close: '}', 114 | }, 115 | { 116 | open: '[', 117 | close: ']', 118 | }, 119 | { 120 | open: '(', 121 | close: ')', 122 | }, 123 | ], 124 | folding: { 125 | markers: { 126 | start: new RegExp('^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/'), 127 | end: new RegExp('^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/'), 128 | }, 129 | }, 130 | indentationRules: { 131 | increaseIndentPattern: new RegExp('(^.*\\{[^}]*$)'), 132 | decreaseIndentPattern: new RegExp('^\\s*\\}'), 133 | }, 134 | wordPattern: new RegExp( 135 | '(#?-?\\d*\\.\\d\\w*%?)|(::?[\\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\\w-?]+%?|[@#!.])', 136 | ), 137 | } 138 | 139 | export const vue: languages.LanguageConfiguration = { 140 | comments: { 141 | blockComment: [''], 142 | }, 143 | brackets: [ 144 | [''], 145 | ['<', '>'], 146 | ['{', '}'], 147 | ['(', ')'], 148 | ], 149 | autoClosingPairs: [ 150 | { 151 | open: '{', 152 | close: '}', 153 | }, 154 | { 155 | open: '[', 156 | close: ']', 157 | }, 158 | { 159 | open: '(', 160 | close: ')', 161 | }, 162 | { 163 | open: '\'', 164 | close: '\'', 165 | }, 166 | { 167 | open: '"', 168 | close: '"', 169 | }, 170 | { 171 | open: '', 173 | notIn: ['comment', 'string'], 174 | }, 175 | { 176 | open: '`', 177 | close: '`', 178 | notIn: ['string', 'comment'], 179 | }, 180 | { 181 | open: '/**', 182 | close: ' */', 183 | notIn: ['string'], 184 | }, 185 | ], 186 | autoCloseBefore: ';:.,=}])><`\'" \n\t', 187 | surroundingPairs: [ 188 | { 189 | open: '\'', 190 | close: '\'', 191 | }, 192 | { 193 | open: '"', 194 | close: '"', 195 | }, 196 | { 197 | open: '{', 198 | close: '}', 199 | }, 200 | { 201 | open: '[', 202 | close: ']', 203 | }, 204 | { 205 | open: '(', 206 | close: ')', 207 | }, 208 | { 209 | open: '<', 210 | close: '>', 211 | }, 212 | { 213 | open: '`', 214 | close: '`', 215 | }, 216 | ], 217 | colorizedBracketPairs: [], 218 | folding: { 219 | markers: { 220 | start: /^\s*/, 221 | end: /^\s*/, 222 | }, 223 | }, 224 | wordPattern: 225 | /(-?\d*\.\d\w*)|([^\`\@\~\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/, 226 | onEnterRules: [ 227 | { 228 | beforeText: 229 | /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr|script|style))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, 230 | afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i, 231 | action: { 232 | indentAction: languages.IndentAction.IndentOutdent, 233 | }, 234 | }, 235 | { 236 | beforeText: 237 | /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr|script|style))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, 238 | action: { 239 | indentAction: languages.IndentAction.Indent, 240 | }, 241 | }, 242 | ], 243 | indentationRules: { 244 | increaseIndentPattern: 245 | /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|keygen|link|menuitem|meta|param|source|track|wbr|script|style)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!\s*\()(?!.*<\/\1>)|)|\{[^}"']*$/i, 246 | decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/, 247 | }, 248 | } 249 | 250 | export const js: languages.LanguageConfiguration = { 251 | comments: { 252 | lineComment: '//', 253 | blockComment: ['/*', '*/'], 254 | }, 255 | brackets: [ 256 | ['${', '}'], 257 | ['{', '}'], 258 | ['[', ']'], 259 | ['(', ')'], 260 | ], 261 | autoClosingPairs: [ 262 | { 263 | open: '{', 264 | close: '}', 265 | }, 266 | { 267 | open: '[', 268 | close: ']', 269 | }, 270 | { 271 | open: '(', 272 | close: ')', 273 | }, 274 | { 275 | open: '\'', 276 | close: '\'', 277 | notIn: ['string', 'comment'], 278 | }, 279 | { 280 | open: '"', 281 | close: '"', 282 | notIn: ['string'], 283 | }, 284 | { 285 | open: '`', 286 | close: '`', 287 | notIn: ['string', 'comment'], 288 | }, 289 | { 290 | open: '/**', 291 | close: ' */', 292 | notIn: ['string'], 293 | }, 294 | ], 295 | surroundingPairs: [ 296 | { 297 | open: '\'', 298 | close: '\'', 299 | }, 300 | { 301 | open: '"', 302 | close: '"', 303 | }, 304 | { 305 | open: '{', 306 | close: '}', 307 | }, 308 | { 309 | open: '[', 310 | close: ']', 311 | }, 312 | { 313 | open: '(', 314 | close: ')', 315 | }, 316 | { 317 | open: '<', 318 | close: '>', 319 | }, 320 | { 321 | open: '`', 322 | close: '`', 323 | }, 324 | ], 325 | autoCloseBefore: ';:.,=}])>` \n\t', 326 | folding: { 327 | markers: { 328 | start: /^\s*\/\/\s*#?region\b/, 329 | end: /^\s*\/\/\s*#?endregion\b/, 330 | }, 331 | }, 332 | wordPattern: 333 | /(-?\d*\.\d\w*)|([^\`\~\@\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/, 334 | indentationRules: { 335 | decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/, 336 | increaseIndentPattern: 337 | /^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/, 338 | unIndentedLinePattern: 339 | /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$|^(\t|[ ])*[ ]\*\/\s*$|^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, 340 | }, 341 | onEnterRules: [ 342 | { 343 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 344 | afterText: /^\s*\*\/$/, 345 | action: { 346 | indentAction: languages.IndentAction.IndentOutdent, 347 | appendText: ' * ', 348 | }, 349 | }, 350 | { 351 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 352 | action: { 353 | indentAction: languages.IndentAction.None, 354 | appendText: ' * ', 355 | }, 356 | }, 357 | { 358 | beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, 359 | previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, 360 | action: { 361 | indentAction: languages.IndentAction.None, 362 | appendText: '* ', 363 | }, 364 | }, 365 | { 366 | beforeText: /^(\t|[ ])*[ ]\*\/\s*$/, 367 | action: { 368 | indentAction: languages.IndentAction.None, 369 | removeText: 1, 370 | }, 371 | }, 372 | { 373 | beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/, 374 | action: { 375 | indentAction: languages.IndentAction.None, 376 | removeText: 1, 377 | }, 378 | }, 379 | { 380 | beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/, 381 | afterText: /^(?!\s*(\bcase\b|\bdefault\b))/, 382 | action: { 383 | indentAction: languages.IndentAction.Indent, 384 | }, 385 | }, 386 | { 387 | previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/, 388 | beforeText: /^\s+([^{i\s]|i(?!f\b))/, 389 | action: { 390 | indentAction: languages.IndentAction.Outdent, 391 | }, 392 | }, 393 | ], 394 | } 395 | 396 | export const ts: languages.LanguageConfiguration = { 397 | comments: { 398 | lineComment: '//', 399 | blockComment: ['/*', '*/'], 400 | }, 401 | brackets: [ 402 | ['${', '}'], 403 | ['{', '}'], 404 | ['[', ']'], 405 | ['(', ')'], 406 | ], 407 | autoClosingPairs: [ 408 | { 409 | open: '{', 410 | close: '}', 411 | }, 412 | { 413 | open: '[', 414 | close: ']', 415 | }, 416 | { 417 | open: '(', 418 | close: ')', 419 | }, 420 | { 421 | open: '\'', 422 | close: '\'', 423 | notIn: ['string', 'comment'], 424 | }, 425 | { 426 | open: '"', 427 | close: '"', 428 | notIn: ['string'], 429 | }, 430 | { 431 | open: '`', 432 | close: '`', 433 | notIn: ['string', 'comment'], 434 | }, 435 | { 436 | open: '/**', 437 | close: ' */', 438 | notIn: ['string'], 439 | }, 440 | ], 441 | surroundingPairs: [ 442 | { 443 | open: '\'', 444 | close: '\'', 445 | }, 446 | { 447 | open: '"', 448 | close: '"', 449 | }, 450 | { 451 | open: '{', 452 | close: '}', 453 | }, 454 | { 455 | open: '[', 456 | close: ']', 457 | }, 458 | { 459 | open: '(', 460 | close: ')', 461 | }, 462 | { 463 | open: '<', 464 | close: '>', 465 | }, 466 | { 467 | open: '`', 468 | close: '`', 469 | }, 470 | ], 471 | colorizedBracketPairs: [ 472 | ['(', ')'], 473 | ['[', ']'], 474 | ['{', '}'], 475 | ['<', '>'], 476 | ], 477 | autoCloseBefore: ';:.,=}])>` \n\t', 478 | folding: { 479 | markers: { 480 | start: /^\s*\/\/\s*#?region\b/, 481 | end: /^\s*\/\/\s*#?endregion\b/, 482 | }, 483 | }, 484 | wordPattern: 485 | /(-?\d*\.\d\w*)|([^\`\~\@\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/, 486 | indentationRules: { 487 | decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/, 488 | increaseIndentPattern: 489 | /^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/, 490 | unIndentedLinePattern: 491 | /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$|^(\t|[ ])*[ ]\*\/\s*$|^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, 492 | }, 493 | onEnterRules: [ 494 | { 495 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 496 | afterText: /^\s*\*\/$/, 497 | action: { 498 | indentAction: languages.IndentAction.IndentOutdent, 499 | appendText: ' * ', 500 | }, 501 | }, 502 | { 503 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 504 | action: { 505 | indentAction: languages.IndentAction.None, 506 | appendText: ' * ', 507 | }, 508 | }, 509 | { 510 | beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, 511 | previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, 512 | action: { 513 | indentAction: languages.IndentAction.None, 514 | appendText: '* ', 515 | }, 516 | }, 517 | { 518 | beforeText: /^(\t|[ ])*[ ]\*\/\s*$/, 519 | action: { 520 | indentAction: languages.IndentAction.None, 521 | removeText: 1, 522 | }, 523 | }, 524 | { 525 | beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/, 526 | action: { 527 | indentAction: languages.IndentAction.None, 528 | removeText: 1, 529 | }, 530 | }, 531 | { 532 | beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/, 533 | afterText: /^(?!\s*(\bcase\b|\bdefault\b))/, 534 | action: { 535 | indentAction: languages.IndentAction.Indent, 536 | }, 537 | }, 538 | { 539 | previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/, 540 | beforeText: /^\s+([^{i\s]|i(?!f\b))/, 541 | action: { 542 | indentAction: languages.IndentAction.Outdent, 543 | }, 544 | }, 545 | ], 546 | } 547 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/store.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/store.ts 2 | 3 | import type { editor } from 'monaco-editor-core' 4 | import type { ToRefs, UnwrapRef } from 'vue' 5 | import type { 6 | SFCAsyncStyleCompileOptions, 7 | SFCScriptCompileOptions, 8 | SFCTemplateCompileOptions, 9 | } from 'vue/compiler-sfc' 10 | import type { OutputModes } from '../../types/vue-repl' 11 | import type { ImportMap } from './import-map' 12 | 13 | import { File as VueFile } from '@velin-dev/utils/transformers/vue' 14 | import { 15 | computed, 16 | reactive, 17 | ref, 18 | shallowRef, 19 | watch, 20 | watchEffect, 21 | } from 'vue' 22 | import * as defaultCompiler from 'vue/compiler-sfc' 23 | 24 | import welcomeSFCCode from '../../prompts/Prompt.velin.vue?raw' 25 | import { atou, utoa } from '../../utils/vue-repl' 26 | import { mergeImportMap, useVueImportMap } from './import-map' 27 | import { compileFile } from './transform' 28 | 29 | export const importMapFile = 'import-map.json' 30 | export const tsconfigFile = 'tsconfig.json' 31 | 32 | export class File extends VueFile { 33 | editorViewState: editor.ICodeEditorViewState | null = null 34 | } 35 | 36 | export function useStore( 37 | { 38 | files = ref(Object.create(null)), 39 | activeFilename = undefined!, // set later 40 | mainFile = ref('src/App.vue'), 41 | template = ref({ 42 | welcomeSFC: welcomeSFCCode, 43 | }), 44 | builtinImportMap = undefined!, // set later 45 | 46 | errors = ref([]), 47 | showOutput = ref(false), 48 | outputMode = ref('preview'), 49 | sfcOptions = ref({}), 50 | compiler = shallowRef(defaultCompiler), 51 | vueVersion = ref(null), 52 | 53 | locale = ref(), 54 | typescriptVersion = ref('latest'), 55 | dependencyVersion = ref(Object.create(null)), 56 | reloadLanguageTools = ref(), 57 | }: Partial = {}, 58 | serializedState?: string, 59 | ): ReplStore { 60 | if (!builtinImportMap) { 61 | ;({ importMap: builtinImportMap, vueVersion } = useVueImportMap({ 62 | vueVersion: vueVersion.value, 63 | })) 64 | } 65 | const loading = ref(false) 66 | 67 | function applyBuiltinImportMap() { 68 | // eslint-disable-next-line ts/no-use-before-define 69 | const importMap = mergeImportMap(builtinImportMap.value, getImportMap()) 70 | setImportMap(importMap) 71 | } 72 | 73 | function init() { 74 | watchEffect(() => { 75 | // eslint-disable-next-line ts/no-use-before-define 76 | compileFile(store, activeFile.value).then(errs => (errors.value = errs)) 77 | }) 78 | 79 | watch( 80 | () => [ 81 | files.value[tsconfigFile]?.code, 82 | typescriptVersion.value, 83 | locale.value, 84 | dependencyVersion.value, 85 | vueVersion.value, 86 | ], 87 | () => reloadLanguageTools.value?.(), 88 | { deep: true }, 89 | ) 90 | 91 | watch( 92 | builtinImportMap, 93 | () => { 94 | // eslint-disable-next-line ts/no-use-before-define 95 | setImportMap(mergeImportMap(getImportMap(), builtinImportMap.value)) 96 | }, 97 | { deep: true }, 98 | ) 99 | 100 | watch( 101 | sfcOptions, 102 | () => { 103 | sfcOptions.value.script ||= {} 104 | sfcOptions.value.script.fs = { 105 | fileExists(file: string) { 106 | if (file.startsWith('/')) 107 | file = file.slice(1) 108 | // eslint-disable-next-line ts/no-use-before-define 109 | return !!store.files[file] 110 | }, 111 | readFile(file: string) { 112 | if (file.startsWith('/')) 113 | file = file.slice(1) 114 | // eslint-disable-next-line ts/no-use-before-define 115 | return store.files[file].code 116 | }, 117 | } 118 | }, 119 | { immediate: true }, 120 | ) 121 | 122 | // init tsconfig 123 | if (!files.value[tsconfigFile]) { 124 | files.value[tsconfigFile] = new File( 125 | tsconfigFile, 126 | // eslint-disable-next-line ts/no-use-before-define 127 | JSON.stringify(tsconfig, undefined, 2), 128 | ) 129 | } 130 | 131 | // compile rest of the files 132 | errors.value = [] 133 | for (const [filename, file] of Object.entries(files.value)) { 134 | if (filename !== mainFile.value) { 135 | // eslint-disable-next-line ts/no-use-before-define 136 | compileFile(store, file).then(errs => errors.value.push(...errs)) 137 | } 138 | } 139 | } 140 | 141 | function setImportMap(map: ImportMap, merge = false) { 142 | if (merge) { 143 | // eslint-disable-next-line ts/no-use-before-define 144 | map = mergeImportMap(getImportMap(), map) 145 | } 146 | 147 | if (map.imports) { 148 | for (const [key, value] of Object.entries(map.imports)) { 149 | if (value) { 150 | map.imports![key] = fixURL(value) 151 | } 152 | } 153 | } 154 | 155 | const code = JSON.stringify(map, undefined, 2) 156 | if (files.value[importMapFile]) { 157 | files.value[importMapFile].code = code 158 | } 159 | else { 160 | files.value[importMapFile] = new File(importMapFile, code) 161 | } 162 | } 163 | 164 | const setActive: Store['setActive'] = (filename) => { 165 | activeFilename.value = filename 166 | } 167 | const addFile: Store['addFile'] = (fileOrFilename) => { 168 | let file: File 169 | if (typeof fileOrFilename === 'string') { 170 | file = new File( 171 | fileOrFilename, 172 | fileOrFilename.endsWith('.vue') ? template.value.newSFC : '', 173 | ) 174 | } 175 | else { 176 | file = fileOrFilename 177 | } 178 | files.value[file.filename] = file 179 | if (!file.hidden) 180 | setActive(file.filename) 181 | } 182 | const deleteFile: Store['deleteFile'] = (filename) => { 183 | if ( 184 | // eslint-disable-next-line no-alert 185 | !confirm(`Are you sure you want to delete ${stripSrcPrefix(filename)}?`) 186 | ) { 187 | return 188 | } 189 | 190 | if (activeFilename.value === filename) { 191 | activeFilename.value = mainFile.value 192 | } 193 | delete files.value[filename] 194 | } 195 | const renameFile: Store['renameFile'] = (oldFilename, newFilename) => { 196 | const file = files.value[oldFilename] 197 | 198 | if (!file) { 199 | errors.value = [`Could not rename "${oldFilename}", file not found`] 200 | return 201 | } 202 | 203 | if (!newFilename || oldFilename === newFilename) { 204 | errors.value = [`Cannot rename "${oldFilename}" to "${newFilename}"`] 205 | return 206 | } 207 | 208 | file.filename = newFilename 209 | const newFiles: Record = {} 210 | 211 | // Preserve iteration order for files 212 | for (const [name, file] of Object.entries(files.value)) { 213 | if (name === oldFilename) { 214 | newFiles[newFilename] = file 215 | } 216 | else { 217 | newFiles[name] = file 218 | } 219 | } 220 | 221 | files.value = newFiles 222 | 223 | if (mainFile.value === oldFilename) { 224 | mainFile.value = newFilename 225 | } 226 | if (activeFilename.value === oldFilename) { 227 | activeFilename.value = newFilename 228 | } 229 | else { 230 | // eslint-disable-next-line ts/no-use-before-define 231 | compileFile(store, file).then(errs => (errors.value = errs)) 232 | } 233 | } 234 | const getImportMap: Store['getImportMap'] = () => { 235 | try { 236 | return JSON.parse(files.value[importMapFile].code) 237 | } 238 | catch (e) { 239 | errors.value = [ 240 | `Syntax error in ${importMapFile}: ${(e as Error).message}`, 241 | ] 242 | return {} 243 | } 244 | } 245 | const getTsConfig: Store['getTsConfig'] = () => { 246 | try { 247 | return JSON.parse(files.value[tsconfigFile].code) 248 | } 249 | catch { 250 | return {} 251 | } 252 | } 253 | const serialize: ReplStore['serialize'] = () => { 254 | // eslint-disable-next-line ts/no-use-before-define 255 | const files = getFiles() 256 | const importMap = files[importMapFile] 257 | if (importMap) { 258 | const parsed = JSON.parse(importMap) 259 | const builtin = builtinImportMap.value.imports || {} 260 | 261 | if (parsed.imports) { 262 | for (const [key, value] of Object.entries(parsed.imports)) { 263 | if (builtin[key] === value) { 264 | delete parsed.imports[key] 265 | } 266 | } 267 | if (parsed.imports && !Object.keys(parsed.imports).length) { 268 | delete parsed.imports 269 | } 270 | } 271 | if (parsed.scopes && !Object.keys(parsed.scopes).length) { 272 | delete parsed.scopes 273 | } 274 | if (Object.keys(parsed).length) { 275 | files[importMapFile] = JSON.stringify(parsed, null, 2) 276 | } 277 | else { 278 | delete files[importMapFile] 279 | } 280 | } 281 | if (vueVersion.value) 282 | files._version = vueVersion.value 283 | if (typescriptVersion.value !== 'latest' || files._tsVersion) { 284 | files._tsVersion = typescriptVersion.value 285 | } 286 | return `#${utoa(JSON.stringify(files))}` 287 | } 288 | const deserialize: ReplStore['deserialize'] = ( 289 | serializedState: string, 290 | checkBuiltinImportMap = true, 291 | ) => { 292 | if (serializedState.startsWith('#')) 293 | serializedState = serializedState.slice(1) 294 | let saved: any 295 | try { 296 | saved = JSON.parse(atou(serializedState)) 297 | } 298 | catch (err) { 299 | console.error(err) 300 | console.error('Failed to load code from URL.') 301 | // eslint-disable-next-line ts/no-use-before-define 302 | return setDefaultFile() 303 | } 304 | for (const filename in saved) { 305 | if (filename === '_version') { 306 | vueVersion.value = saved[filename] 307 | } 308 | else if (filename === '_tsVersion') { 309 | typescriptVersion.value = saved[filename] 310 | } 311 | else { 312 | setFile(files.value, filename, saved[filename]) 313 | } 314 | } 315 | if (checkBuiltinImportMap) { 316 | applyBuiltinImportMap() 317 | } 318 | } 319 | const getFiles: ReplStore['getFiles'] = () => { 320 | const exported: Record = {} 321 | for (const [filename, file] of Object.entries(files.value)) { 322 | const normalized = stripSrcPrefix(filename) 323 | exported[normalized] = file.code 324 | } 325 | return exported 326 | } 327 | const setFiles: ReplStore['setFiles'] = async ( 328 | newFiles, 329 | mainFile = '', 330 | ) => { 331 | const files: Record = Object.create(null) 332 | 333 | mainFile = addSrcPrefix(mainFile) 334 | if (!newFiles[mainFile]) { 335 | setFile(files, mainFile, template.value.welcomeSFC || welcomeSFCCode) 336 | } 337 | for (const [filename, file] of Object.entries(newFiles)) { 338 | setFile(files, filename, file) 339 | } 340 | 341 | const errors = [] 342 | for (const file of Object.values(files)) { 343 | // eslint-disable-next-line ts/no-use-before-define 344 | errors.push(...(await compileFile(store, file))) 345 | } 346 | 347 | // eslint-disable-next-line ts/no-use-before-define 348 | store.mainFile = mainFile 349 | // eslint-disable-next-line ts/no-use-before-define 350 | store.files = files 351 | // eslint-disable-next-line ts/no-use-before-define 352 | store.errors = errors 353 | applyBuiltinImportMap() 354 | // eslint-disable-next-line ts/no-use-before-define 355 | setActive(store.mainFile) 356 | } 357 | const setDefaultFile = (): void => { 358 | setFile( 359 | files.value, 360 | mainFile.value, 361 | template.value.welcomeSFC || welcomeSFCCode, 362 | ) 363 | } 364 | 365 | if (serializedState) { 366 | deserialize(serializedState, false) 367 | } 368 | else { 369 | setDefaultFile() 370 | } 371 | if (!files.value[mainFile.value]) { 372 | mainFile.value = Object.keys(files.value)[0] 373 | } 374 | activeFilename ||= ref(mainFile.value) 375 | const activeFile = computed(() => files.value[activeFilename.value]) 376 | 377 | applyBuiltinImportMap() 378 | 379 | const store: ReplStore = reactive({ 380 | files, 381 | activeFile, 382 | activeFilename, 383 | mainFile, 384 | template, 385 | builtinImportMap, 386 | 387 | errors, 388 | showOutput, 389 | outputMode, 390 | sfcOptions, 391 | compiler, 392 | loading, 393 | vueVersion, 394 | 395 | locale, 396 | typescriptVersion, 397 | dependencyVersion, 398 | reloadLanguageTools, 399 | 400 | init, 401 | setActive, 402 | addFile, 403 | deleteFile, 404 | renameFile, 405 | getImportMap, 406 | setImportMap, 407 | getTsConfig, 408 | serialize, 409 | deserialize, 410 | getFiles, 411 | setFiles, 412 | }) 413 | 414 | return store 415 | } 416 | 417 | const tsconfig = { 418 | compilerOptions: { 419 | allowJs: true, 420 | checkJs: true, 421 | jsx: 'Preserve', 422 | target: 'ESNext', 423 | module: 'ESNext', 424 | moduleResolution: 'Bundler', 425 | allowImportingTsExtensions: true, 426 | }, 427 | vueCompilerOptions: { 428 | target: 3.4, 429 | }, 430 | } 431 | 432 | export interface SFCOptions { 433 | script?: Partial 434 | style?: Partial 435 | template?: Partial 436 | } 437 | 438 | export type StoreState = ToRefs<{ 439 | files: Record 440 | activeFilename: string 441 | mainFile: string 442 | template: { 443 | welcomeSFC?: string 444 | newSFC?: string 445 | } 446 | builtinImportMap: ImportMap 447 | 448 | // output 449 | errors: (string | Error)[] 450 | showOutput: boolean 451 | outputMode: OutputModes 452 | sfcOptions: SFCOptions 453 | /** `@vue/compiler-sfc` */ 454 | compiler: typeof defaultCompiler 455 | /* only apply for compiler-sfc */ 456 | vueVersion: string | null 457 | 458 | // volar-related 459 | locale: string | undefined 460 | typescriptVersion: string 461 | /** \{ dependencyName: version \} */ 462 | dependencyVersion: Record 463 | reloadLanguageTools?: (() => void) | undefined 464 | }> 465 | 466 | export interface ReplStore extends UnwrapRef { 467 | activeFile: File 468 | /** Loading compiler */ 469 | loading: boolean 470 | init: () => void 471 | setActive: (filename: string) => void 472 | addFile: (filename: string | File) => void 473 | deleteFile: (filename: string) => void 474 | renameFile: (oldFilename: string, newFilename: string) => void 475 | getImportMap: () => ImportMap 476 | setImportMap: (map: ImportMap, merge?: boolean) => void 477 | getTsConfig: () => Record 478 | serialize: () => string 479 | /** 480 | * Deserializes the given string to restore the REPL store state. 481 | * @param serializedState - The serialized state string. 482 | * @param checkBuiltinImportMap - Whether to check the built-in import map. Default to true 483 | */ 484 | deserialize: (serializedState: string, checkBuiltinImportMap?: boolean) => void 485 | getFiles: () => Record 486 | setFiles: (newFiles: Record, mainFile?: string) => Promise 487 | } 488 | 489 | export type Store = Pick< 490 | ReplStore, 491 | | 'files' 492 | | 'activeFile' 493 | | 'mainFile' 494 | | 'errors' 495 | | 'showOutput' 496 | | 'outputMode' 497 | | 'sfcOptions' 498 | | 'compiler' 499 | | 'vueVersion' 500 | | 'locale' 501 | | 'typescriptVersion' 502 | | 'dependencyVersion' 503 | | 'reloadLanguageTools' 504 | | 'init' 505 | | 'setActive' 506 | | 'addFile' 507 | | 'deleteFile' 508 | | 'renameFile' 509 | | 'getImportMap' 510 | | 'getTsConfig' 511 | > 512 | 513 | function addSrcPrefix(file: string) { 514 | return file === importMapFile 515 | || file === tsconfigFile 516 | || file.startsWith('src/') 517 | ? file 518 | : `src/${file}` 519 | } 520 | 521 | export function stripSrcPrefix(file: string) { 522 | return file.replace(/^src\//, '') 523 | } 524 | 525 | function fixURL(url: string) { 526 | return url.replace('https://sfc.vuejs', 'https://play.vuejs') 527 | } 528 | 529 | function setFile( 530 | files: Record, 531 | filename: string, 532 | content: string, 533 | ) { 534 | const normalized = addSrcPrefix(filename) 535 | files[normalized] = new File(normalized, content) 536 | } 537 | -------------------------------------------------------------------------------- /apps/playground/src/components/Editor/transform.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable regexp/no-unused-capturing-group */ 2 | 3 | // https://github.com/vuejs/repl/blob/5e092b6111118f5bb5fc419f0f8f3f84cd539366/src/transform.ts 4 | 5 | import type { 6 | BindingMetadata, 7 | CompilerOptions, 8 | SFCDescriptor, 9 | } from 'vue/compiler-sfc' 10 | import type { File, Store } from './store' 11 | 12 | import { transformTS } from '@velin-dev/utils/transformers/typescript' 13 | import hashId from 'hash-sum' 14 | 15 | export const COMP_IDENTIFIER = `__sfc__` 16 | 17 | const REGEX_JS = /\.[jt]sx?$/ 18 | function testTs(filename: string | undefined | null) { 19 | return !!(filename && /(\.|\b)tsx?$/.test(filename)) 20 | } 21 | function testJsx(filename: string | undefined | null) { 22 | return !!(filename && /(\.|\b)[jt]sx$/.test(filename)) 23 | } 24 | 25 | export async function compileFile( 26 | store: Store, 27 | { filename, code, compiled }: File, 28 | ): Promise<(string | Error)[]> { 29 | if (!code.trim()) { 30 | return [] 31 | } 32 | 33 | if (filename.endsWith('.css')) { 34 | compiled.css = code 35 | return [] 36 | } 37 | 38 | if (REGEX_JS.test(filename)) { 39 | const isJSX = testJsx(filename) 40 | if (testTs(filename)) { 41 | code = transformTS(code, isJSX) 42 | } 43 | if (isJSX) { 44 | // code = await import('./jsx').then(({ transformJSX }) => 45 | // transformJSX(code), 46 | // ) 47 | console.error('JSX transform not supported in the playground') 48 | } 49 | compiled.js = compiled.ssr = code 50 | return [] 51 | } 52 | 53 | if (filename.endsWith('.json')) { 54 | let parsed 55 | try { 56 | parsed = JSON.parse(code) 57 | } 58 | catch (err: any) { 59 | console.error(`Error parsing ${filename}`, err.message) 60 | return [err.message] 61 | } 62 | compiled.js = compiled.ssr = `export default ${JSON.stringify(parsed)}` 63 | return [] 64 | } 65 | 66 | if (!filename.endsWith('.vue')) { 67 | return [] 68 | } 69 | 70 | const id = hashId(filename) 71 | const { errors, descriptor } = store.compiler.parse(code, { 72 | filename, 73 | sourceMap: true, 74 | templateParseOptions: store.sfcOptions?.template?.compilerOptions, 75 | }) 76 | if (errors.length) { 77 | return errors 78 | } 79 | 80 | const styleLangs = descriptor.styles.map(s => s.lang).filter(Boolean) 81 | const templateLang = descriptor.template?.lang 82 | if (styleLangs.length && templateLang) { 83 | return [ 84 | `lang="${styleLangs.join( 85 | ',', 86 | )}" pre-processors for 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/vite-browser/src/assets/Prompt.velin.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /examples/vite-browser/src/assets/Promptv2.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/vite-browser/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/vite-browser/vite.config.ts: -------------------------------------------------------------------------------- 1 | import Vue from '@vitejs/plugin-vue' 2 | import UnoCSS from 'unocss/vite' 3 | import Markdown from 'unplugin-vue-markdown/vite' 4 | import { defineConfig } from 'vite' 5 | import Inspector from 'vite-plugin-inspect' 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | Markdown({}), 10 | Vue({ 11 | include: ['**/*.vue', '**/*.md'], 12 | }), 13 | Inspector(), 14 | UnoCSS(), 15 | ], 16 | }) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@velin-dev/root", 3 | "type": "module", 4 | "version": "0.2.3", 5 | "packageManager": "pnpm@10.11.0", 6 | "description": "Develop prompts with Vue SFC or Markdown like pro.", 7 | "author": { 8 | "name": "RainbowBird", 9 | "email": "rbxin2003@outlook.com", 10 | "url": "https://github.com/luoling8192" 11 | }, 12 | "contributors": [ 13 | { 14 | "name": "Neko Ayaka", 15 | "email": "neko@ayaka.moe", 16 | "url": "https://github.com/nekomeowww" 17 | } 18 | ], 19 | "license": "MIT", 20 | "homepage": "https://github.com/luoling8192/velin", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/luoling8192/velin.git" 24 | }, 25 | "bugs": "https://github.com/luoling8192/velin/issues", 26 | "scripts": { 27 | "dev": "pnpm -rF=\"./packages/*\" run build && pnpm -rF=\"./apps/*\" run dev", 28 | "dev:play": "pnpm -F @velin-dev/apps-playground run dev", 29 | "dev:browser-dev": "nr -F @velin-dev/example-vite-browser dev", 30 | "build": "pnpm -rF=\"./packages/*\" -F=\"./examples/*\" -F=\"./apps/*\" run build", 31 | "build:play": "pnpm -F @velin-dev/apps-playground run build", 32 | "build:packages": "pnpm -rF=\"./packages/*\" run build", 33 | "run:node-sfc": "pnpm -F @velin-dev/example-native-node run:sfc", 34 | "run:node-md": "pnpm -F @velin-dev/example-native-node run:md", 35 | "test": "vitest --coverage", 36 | "test:run": "vitest run", 37 | "lint": "eslint --cache .", 38 | "lint:fix": "eslint --cache --fix .", 39 | "attw:packages": "pnpm -rF=\"./packages/*\" run --parallel attw", 40 | "typecheck": "pnpm -r -F=\"./apps/*\" run --parallel typecheck", 41 | "up": "taze -w -r -I -f && pnpm prune && pnpm dedupe", 42 | "nolyfill": "pnpm dlx nolyfill" 43 | }, 44 | "devDependencies": { 45 | "@antfu/eslint-config": "^4.13.2", 46 | "@antfu/ni": "^24.4.0", 47 | "@antfu/nip": "^0.1.0", 48 | "@arethetypeswrong/cli": "^0.18.1", 49 | "@catppuccin/palette": "^1.7.1", 50 | "@iconify/utils": "^2.3.0", 51 | "@pnpm/find-workspace-dir": "^1000.1.0", 52 | "@types/markdown-it": "^14.1.2", 53 | "@types/node": "^22.15.21", 54 | "@unocss/eslint-config": "^66.1.2", 55 | "@unocss/eslint-plugin": "^66.1.2", 56 | "@unocss/preset-mini": "^66.1.2", 57 | "bumpp": "^10.1.1", 58 | "changelogithub": "^13.14.0", 59 | "eslint": "^9.27.0", 60 | "pkgroll": "^2.12.2", 61 | "rollup": "^4.41.0", 62 | "taze": "^19.1.0", 63 | "tsx": "^4.19.4", 64 | "typescript": "^5.8.3", 65 | "unbuild": "^3.5.0", 66 | "unocss": "66.1.1", 67 | "unocss-preset-scrollbar": "^3.2.0", 68 | "vite": "^6.3.5", 69 | "vite-plugin-inspect": "^11.1.0", 70 | "vitest": "^3.1.4" 71 | }, 72 | "pnpm": { 73 | "overrides": { 74 | "array-flatten": "npm:@nolyfill/array-flatten@^1.0.44", 75 | "axios": "npm:feaxios@^0.0.23", 76 | "is-core-module": "npm:@nolyfill/is-core-module@^1.0.39", 77 | "isarray": "npm:@nolyfill/isarray@^1.0.44", 78 | "safe-buffer": "npm:@nolyfill/safe-buffer@^1.0.44", 79 | "safer-buffer": "npm:@nolyfill/safer-buffer@^1.0.44", 80 | "side-channel": "npm:@nolyfill/side-channel@^1.0.44", 81 | "string.prototype.matchall": "npm:@nolyfill/string.prototype.matchall@^1.0.44" 82 | }, 83 | "onlyBuiltDependencies": [ 84 | "esbuild", 85 | "unrs-resolver", 86 | "vue-demi" 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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/luoling8192/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/luoling8192/velin.svg?style=flat&colorA=080f12&colorB=1fa669 22 | [license-href]: https://github.com/luoling8192/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/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@velin-dev/core", 3 | "type": "module", 4 | "version": "0.2.3", 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/luoling8192/velin", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/luoling8192/velin.git", 23 | "directory": "packages/core" 24 | }, 25 | "bugs": "https://github.com/luoling8192/velin/issues", 26 | "exports": { 27 | ".": { 28 | "types": "./dist/index.d.ts", 29 | "import": "./dist/index.mjs" 30 | }, 31 | "./render-browser": { 32 | "types": "./dist/render-browser/index.d.ts", 33 | "import": "./dist/render-browser/index.mjs" 34 | }, 35 | "./render-repl": { 36 | "types": "./dist/render-repl/index.d.ts", 37 | "import": "./dist/render-repl/index.mjs" 38 | }, 39 | "./render-node": { 40 | "types": "./dist/render-node/index.d.ts", 41 | "import": "./dist/render-node/index.mjs" 42 | }, 43 | "./render-shared": { 44 | "types": "./dist/render-shared/index.d.ts", 45 | "import": "./dist/render-shared/index.mjs" 46 | }, 47 | "./browser": { 48 | "types": "./dist/browser.d.ts", 49 | "import": "./dist/browser.mjs" 50 | } 51 | }, 52 | "files": [ 53 | "README.md", 54 | "dist", 55 | "package.json" 56 | ], 57 | "scripts": { 58 | "dev": "unbuild", 59 | "stub": "unbuild", 60 | "build": "unbuild", 61 | "typecheck": "tsc --noEmit", 62 | "attw": "attw --pack . --profile esm-only --ignore-rules cjs-resolves-to-esm" 63 | }, 64 | "dependencies": { 65 | "@unrteljs/eval": "^0.0.6", 66 | "@velin-dev/utils": "workspace:^", 67 | "@vue/compiler-sfc": "^3.5.14", 68 | "@vue/reactivity": "^3.5.14", 69 | "@vue/runtime-core": "^3.5.14", 70 | "@vue/server-renderer": "^3.5.14", 71 | "@vue/shared": "^3.5.14", 72 | "error-stack-parser": "^2.1.4", 73 | "hast-util-from-html": "^2.0.3", 74 | "path-browserify-esm": "^1.0.6", 75 | "std-env": "^3.9.0", 76 | "vue": "^3.5.14" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/core/src/browser.ts: -------------------------------------------------------------------------------- 1 | export * from './render-browser' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { InputProps } from './types' 2 | 3 | import { isNode } from 'std-env' 4 | 5 | import { 6 | renderMarkdownString as renderMarkdownStringBrowser, 7 | renderSFCString as renderSFCStringBrowser, 8 | } from './render-browser' 9 | 10 | export async function renderMarkdownString( 11 | source: string, 12 | data?: InputProps, 13 | basePath?: string, 14 | ): Promise { 15 | if (isNode) { 16 | const { renderMarkdownString } = await import('./render-node') 17 | return renderMarkdownString(source, data, basePath) 18 | } 19 | 20 | return renderMarkdownStringBrowser(source, data, basePath) 21 | } 22 | 23 | export async function renderSFCString( 24 | source: string, 25 | data?: InputProps, 26 | basePath?: string, 27 | ): Promise { 28 | if (isNode) { 29 | const { renderSFCString } = await import('./render-node') 30 | return renderSFCString(source, data, basePath) 31 | } 32 | 33 | return renderSFCStringBrowser(source, data, basePath) 34 | } 35 | 36 | export { 37 | onlyRender, 38 | onlySetup, 39 | renderComponent, 40 | resolveProps, 41 | } from './render-shared' 42 | export * from './types' 43 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/index.ts: -------------------------------------------------------------------------------- 1 | export { renderComponent, resolveProps } from '../render-shared' 2 | export { renderMarkdownString } from './markdown' 3 | export { renderSFCString } from './sfc' 4 | -------------------------------------------------------------------------------- /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 | import { describe, expect, it } from 'vitest' 5 | 6 | import { renderMarkdownString } from './markdown' 7 | 8 | describe.skip('renderMarkdownString', async () => { 9 | it('should be able to render simple SFC', async () => { 10 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'simple.velin.md'), 'utf-8') 11 | const result = await renderMarkdownString(content) 12 | expect(result).toBeDefined() 13 | expect(result).not.toBe('') 14 | expect(result).toBe('# Hello, world!\n') 15 | }) 16 | 17 | it('should be able to render script setup SFC', async () => { 18 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.md'), 'utf-8') 19 | const result = await renderMarkdownString(content) 20 | expect(result).toBeDefined() 21 | expect(result).not.toBe('') 22 | expect(result).toBe('Count: 0\n') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/markdown.ts: -------------------------------------------------------------------------------- 1 | import type { InputProps } from '../types' 2 | 3 | import { fromMarkdown, scriptFrom } from '@velin-dev/utils/from-md' 4 | import { toMarkdown } from '@velin-dev/utils/to-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 | const html = fromMarkdown(source) 15 | 16 | const { remainingHTML, scriptContent } = scriptFrom(html) 17 | const sfcString = createSFC(remainingHTML, scriptContent) 18 | 19 | const renderedHTML = await renderSFCString(sfcString, data) 20 | const markdownResult = await toMarkdown(renderedHTML) 21 | 22 | return markdownResult 23 | } 24 | -------------------------------------------------------------------------------- /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 | import { describe, expect, it } from 'vitest' 5 | 6 | import { evaluateSFC, renderSFCString, resolvePropsFromString } from './sfc' 7 | 8 | describe.skip('renderSFCString', async () => { 9 | it('should be able to render simple SFC', async () => { 10 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'simple.velin.vue'), 'utf-8') 11 | const result = await renderSFCString(content) 12 | expect(result).toBeDefined() 13 | expect(result).not.toBe('') 14 | expect(result).toBe('# Hello, world!\n') 15 | }) 16 | 17 | it('should be able to render script setup SFC', async () => { 18 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.vue'), 'utf-8') 19 | const result = await renderSFCString(content) 20 | expect(result).toBeDefined() 21 | expect(result).not.toBe('') 22 | expect(result).toBe('# Count: 0\n') 23 | }) 24 | }) 25 | 26 | describe.skip('evaluateSFC', async () => { 27 | it('should be able to evaluate script setup SFC', async () => { 28 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.vue'), 'utf-8') 29 | const component = await evaluateSFC(content) 30 | expect(component).toBeDefined() 31 | expect(component.setup).toBeDefined() 32 | expect(typeof component.setup).toBe('function') 33 | }) 34 | }) 35 | 36 | describe.skip('resolvePropsFromString', async () => { 37 | it('should be able to render script setup SFC', async () => { 38 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup-with-props.velin.vue'), 'utf-8') 39 | const props = await resolvePropsFromString(content) 40 | expect(props).toEqual([ 41 | { key: 'date', type: 'string', title: 'date' }, 42 | ]) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/sfc.ts: -------------------------------------------------------------------------------- 1 | import type { DefineComponent } from '@vue/runtime-core' 2 | import type { InputProps } from '../types' 3 | 4 | import { evaluate } from '@unrteljs/eval/browser' 5 | import { toMarkdown } from '@velin-dev/utils/to-md' 6 | import { renderToString } from '@vue/server-renderer' 7 | import ErrorStackParser from 'error-stack-parser' 8 | import { fromHtml } from 'hast-util-from-html' 9 | import path from 'path-browserify-esm' 10 | 11 | import { compileSFC, onlyRender, resolveProps } from '../render-shared' 12 | 13 | export async function evaluateSFC( 14 | source: string, 15 | basePath?: string, 16 | ) { 17 | const { script } = await compileSFC(source) 18 | 19 | if (!basePath) { 20 | // eslint-disable-next-line unicorn/error-message 21 | const stack = ErrorStackParser.parse(new Error()) 22 | basePath = path.dirname(stack[1].fileName?.replace('async', '').trim() || '') 23 | } 24 | 25 | // TODO: evaluate setup when not ` 53 | } 54 | 55 | const html = await renderSFC(source, data, basePath) 56 | return toMarkdown(html) 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/script-setup-with-props.velin.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/script-setup.velin.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Count: {{ count }} 8 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/script-setup.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/simple.velin.md: -------------------------------------------------------------------------------- 1 |
2 |

Hello, world!

3 |
4 | -------------------------------------------------------------------------------- /packages/core/src/render-browser/testdata/simple.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/core/src/render-node/index.ts: -------------------------------------------------------------------------------- 1 | export { renderComponent, resolveProps } from '../render-shared' 2 | export { renderMarkdownString } from './markdown' 3 | export { renderSFCString } from './sfc' 4 | -------------------------------------------------------------------------------- /packages/core/src/render-node/markdown.test.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | import { dirname, join } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { describe, expect, it } from 'vitest' 5 | 6 | import { renderMarkdownString } from './markdown' 7 | 8 | describe('renderMarkdownString', async () => { 9 | it('should be able to render simple SFC', async () => { 10 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'simple.velin.md'), 'utf-8') 11 | const result = await renderMarkdownString(content) 12 | expect(result).toBeDefined() 13 | expect(result).not.toBe('') 14 | expect(result).toBe('# Hello, world!\n') 15 | }) 16 | 17 | it('should be able to render script setup SFC', async () => { 18 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.md'), 'utf-8') 19 | const result = await renderMarkdownString(content) 20 | expect(result).toBeDefined() 21 | expect(result).not.toBe('') 22 | expect(result).toBe('Count: 0\n') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/core/src/render-node/markdown.ts: -------------------------------------------------------------------------------- 1 | import type { InputProps } from '../types' 2 | 3 | import { fromMarkdown, scriptFrom } from '@velin-dev/utils/from-md' 4 | import { toMarkdown } from '@velin-dev/utils/to-md' 5 | import { createSFC } from '@velin-dev/utils/vue-sfc' 6 | 7 | import { renderSFC } from './sfc' 8 | 9 | export async function renderMarkdownString( 10 | source: string, 11 | data?: InputProps, 12 | basePath?: string, 13 | ): Promise { 14 | const html = fromMarkdown(source) 15 | 16 | const { remainingHTML, scriptContent } = scriptFrom(html) 17 | const sfcString = createSFC(remainingHTML, scriptContent) 18 | 19 | const renderedHTML = await renderSFC(sfcString, data, basePath) 20 | const markdownResult = await toMarkdown(renderedHTML) 21 | 22 | return markdownResult 23 | } 24 | -------------------------------------------------------------------------------- /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 | import { describe, expect, it } from 'vitest' 5 | 6 | import { evaluateSFC, renderSFCString, resolvePropsFromString } from './sfc' 7 | 8 | describe('renderSFCString', async () => { 9 | it('should be able to render simple SFC', async () => { 10 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'simple.velin.vue'), 'utf-8') 11 | const result = await renderSFCString(content) 12 | expect(result).toBeDefined() 13 | expect(result).not.toBe('') 14 | expect(result).toBe('# Hello, world!\n') 15 | }) 16 | 17 | it('should be able to render script setup SFC', async () => { 18 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.vue'), 'utf-8') 19 | const result = await renderSFCString(content) 20 | expect(result).toBeDefined() 21 | expect(result).not.toBe('') 22 | expect(result).toBe('# Count: 0\n') 23 | }) 24 | }) 25 | 26 | describe('evaluateSFC', async () => { 27 | it('should be able to evaluate script setup SFC', async () => { 28 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup.velin.vue'), 'utf-8') 29 | const component = await evaluateSFC(content) 30 | expect(component).toBeDefined() 31 | expect(component.setup).toBeDefined() 32 | expect(typeof component.setup).toBe('function') 33 | }) 34 | }) 35 | 36 | describe('resolvePropsFromString', async () => { 37 | it('should be able to render script setup SFC', async () => { 38 | const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), 'testdata', 'script-setup-with-props.velin.vue'), 'utf-8') 39 | const props = await resolvePropsFromString(content) 40 | expect(props).toEqual([ 41 | { key: 'date', type: 'string', title: 'date' }, 42 | ]) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/core/src/render-node/sfc.ts: -------------------------------------------------------------------------------- 1 | import type { DefineComponent } from '@vue/runtime-core' 2 | import type { InputProps } from '../types' 3 | 4 | import { evaluate } from '@unrteljs/eval/node' 5 | import { toMarkdown } from '@velin-dev/utils/to-md' 6 | import { renderToString } from '@vue/server-renderer' 7 | import ErrorStackParser from 'error-stack-parser' 8 | import { fromHtml } from 'hast-util-from-html' 9 | import path from 'path-browserify-esm' 10 | 11 | import { compileSFC, onlyRender, resolveProps } from '../render-shared' 12 | 13 | export async function evaluateSFC( 14 | source: string, 15 | basePath?: string, 16 | ) { 17 | const { script } = await compileSFC(source) 18 | 19 | if (!basePath) { 20 | // eslint-disable-next-line unicorn/error-message 21 | const stack = ErrorStackParser.parse(new Error()) 22 | basePath = path.dirname(stack[1].fileName?.replace('async', '').trim() || '') 23 | } 24 | 25 | // TODO: evaluate setup when not ` 53 | } 54 | 55 | const html = await renderSFC(source, data, basePath) 56 | return toMarkdown(html) 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup-with-props.velin.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup.velin.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Count: {{ count }} 8 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/script-setup.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/simple.velin.md: -------------------------------------------------------------------------------- 1 |
2 |

Hello, world!

3 |
4 | -------------------------------------------------------------------------------- /packages/core/src/render-node/testdata/simple.velin.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/core/src/render-repl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sfc' 2 | -------------------------------------------------------------------------------- /packages/core/src/render-repl/sfc.ts: -------------------------------------------------------------------------------- 1 | import type { DefineComponent } from 'vue' 2 | import type { ComponentProp } from '../render-shared' 3 | import type { InputProps } from '../types' 4 | 5 | import { evaluate } from '@unrteljs/eval/browser' 6 | import { toMarkdown } from '@velin-dev/utils/to-md' 7 | import { compileModulesForPreview } from '@velin-dev/utils/transformers/vue' 8 | import { renderToString } from '@vue/server-renderer' 9 | import { fromHtml } from 'hast-util-from-html' 10 | 11 | import { compileSFC, onlyRender, resolveProps } from '../render-shared' 12 | 13 | export async function evaluateSFC( 14 | source: string, 15 | _basePath?: string, 16 | ) { 17 | const { script } = await compileSFC(source) 18 | 19 | // TODO: evaluate setup when not ` 81 | } 82 | 83 | const { rendered, props } = await renderSFC(source, data, basePath) 84 | return { 85 | props, 86 | rendered: await toMarkdown(rendered), 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /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 | throw new Error(`source has no