├── .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 | 
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 |
63 |
64 | Hello world, this is {{ name }}!
65 |
66 |
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 | This website requires JavaScript to function properly. Please enable JavaScript to continue.
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 |
10 |
11 |
12 |
13 | Velin Playground
14 |
15 |
21 |
28 |
29 |
30 |
toggleDark()"
37 | >
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
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 |
183 |
189 |
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 |
14 |
15 |
{{ language }}
16 |
{{ count }}
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/vite-browser/src/assets/Promptv2.velin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 你好,世界!
4 |
5 |
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 |
12 |
13 |
Count: {{ count }}
14 |
15 |
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 |
8 |
9 |
Count: {{ count }}
10 |
11 |
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 |
2 |
3 |
Hello, world!
4 |
5 |
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 |
12 |
13 |
Count: {{ count }}
14 |
15 |
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 |
8 |
9 |
Count: {{ count }}
10 |
11 |
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 |
2 |
3 |
Hello, world!
4 |
5 |
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 tag.`)
20 | }
21 |
22 | const templateOptions = {
23 | source: descriptor.template.content,
24 | filename: 'temp.vue',
25 | id: `vue-component-${Date.now()}`,
26 | compilerOptions: { runtimeModuleName: 'vue' },
27 | }
28 |
29 | const templateResult = compileTemplate(templateOptions)
30 |
31 | const scriptLang = descriptor.script?.lang || descriptor.scriptSetup?.lang
32 | const isTS = testTs(scriptLang)
33 |
34 | const expressionPlugins: CompilerOptions['expressionPlugins'] = []
35 | if (isTS) {
36 | expressionPlugins.push('typescript')
37 | }
38 |
39 | const scriptResult = compileScript(descriptor, {
40 | id: `vue-component-${Date.now()}`,
41 | inlineTemplate: isUseInlineTemplate(descriptor),
42 | ...{
43 | ...templateOptions,
44 | expressionPlugins,
45 | },
46 | })
47 |
48 | if (isTS) {
49 | scriptResult.content = transformTS(scriptResult.content)
50 | }
51 |
52 | return {
53 | template: templateResult,
54 | script: scriptResult,
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/packages/core/src/render-shared/component.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentPropsOptions } from '@vue/runtime-core'
2 | import type {
3 | InputProps,
4 | LooseRequiredRenderComponentInputProps,
5 | RenderComponentInputComponent,
6 | ResolveRenderComponentInputProps,
7 | } from '../types'
8 |
9 | import { toMarkdown } from '@velin-dev/utils/to-md'
10 | import { toValue } from '@vue/reactivity'
11 | import { renderToString } from '@vue/server-renderer'
12 | import { createSSRApp } from 'vue'
13 |
14 | export function onlySetup<
15 | RawProps = any,
16 | ComponentProps = ComponentPropsOptions,
17 | ResolvedProps = ResolveRenderComponentInputProps,
18 | >(
19 | promptComponent: RenderComponentInputComponent,
20 | props: InputProps,
21 | ) {
22 | return promptComponent.setup?.(
23 | toValue(props) as unknown as LooseRequiredRenderComponentInputProps,
24 | { attrs: {}, slots: {}, emit: () => { }, expose: () => { } },
25 | )
26 | }
27 |
28 | export function onlyRender<
29 | RawProps = any,
30 | ComponentProps = ComponentPropsOptions,
31 | ResolvedProps = ResolveRenderComponentInputProps,
32 | >(
33 | promptComponent: RenderComponentInputComponent,
34 | props: InputProps,
35 | ) {
36 | return createSSRApp(promptComponent, toValue(props) as Record)
37 | }
38 |
39 | export function renderComponent<
40 | RawProps = any,
41 | ComponentProps = ComponentPropsOptions,
42 | ResolvedProps = ResolveRenderComponentInputProps,
43 | >(
44 | promptComponent: RenderComponentInputComponent,
45 | props: InputProps,
46 | ) {
47 | return new Promise((resolve, reject) => {
48 | renderToString(onlyRender(promptComponent, props))
49 | .then(toMarkdown)
50 | .then(resolve)
51 | .catch(reject)
52 | })
53 | }
54 |
--------------------------------------------------------------------------------
/packages/core/src/render-shared/index.ts:
--------------------------------------------------------------------------------
1 | export * from './compile'
2 | export * from './component'
3 | export * from './props'
4 | export * from './template'
5 |
--------------------------------------------------------------------------------
/packages/core/src/render-shared/props.ts:
--------------------------------------------------------------------------------
1 | import type { App, ComponentInternalInstance, DefineComponent } from 'vue'
2 |
3 | export interface ComponentPropText {
4 | type: 'string'
5 | value?: string
6 | }
7 |
8 | export interface ComponentPropBool {
9 | type: 'boolean'
10 | value?: boolean
11 | }
12 |
13 | export interface ComponentPropNumber {
14 | type: 'number'
15 | value?: number
16 | }
17 |
18 | export interface ComponentPropUnknown {
19 | type: 'unknown'
20 | value?: unknown
21 | }
22 |
23 | export type ComponentProp = (ComponentPropText | ComponentPropBool | ComponentPropNumber | ComponentPropUnknown) & {
24 | title: string
25 | key: string
26 | }
27 |
28 | function willTurnIntoNumber(value: unknown): boolean {
29 | if (value === Number) {
30 | return true
31 | }
32 |
33 | // it is possible value is { type: Number() }
34 | if (typeof value === 'object' && value !== null && 'type' in value) {
35 | // check if value.type is Number()
36 | if (typeof (value as { type: unknown }).type === 'function' && (value as { type: unknown }).type === Number) {
37 | return true
38 | }
39 | }
40 |
41 | return false
42 | }
43 |
44 | function willTurnIntoBoolean(value: unknown): boolean {
45 | if (value === Boolean) {
46 | return true
47 | }
48 |
49 | // it is possible value is { type: Boolean() }
50 | if (typeof value === 'object' && value !== null && 'type' in value) {
51 | // check if value.type is Boolean()
52 | if (typeof (value as { type: unknown }).type === 'function' && (value as { type: unknown }).type === Boolean) {
53 | return true
54 | }
55 | }
56 |
57 | return false
58 | }
59 |
60 | function willTurnIntoString(value: unknown): boolean {
61 | if (value === String) {
62 | return true
63 | }
64 |
65 | // it is possible value is { type: String() }
66 | if (typeof value === 'object' && value !== null && 'type' in value) {
67 | // check if value.type is String()
68 | if (typeof (value as { type: unknown }).type === 'function' && (value as { type: unknown }).type === String) {
69 | return true
70 | }
71 | }
72 |
73 | return false
74 | }
75 |
76 | function inferType(propDef:
77 | | {
78 | // eslint-disable-next-line ts/no-unsafe-function-type
79 | type: Function
80 | }
81 | | typeof String
82 | | typeof Number
83 | | typeof Boolean
84 | | unknown) {
85 | let type: 'unknown' | 'string' | 'number' | 'boolean' = 'unknown'
86 | if (willTurnIntoString(propDef)) {
87 | type = 'string'
88 | }
89 | else if (willTurnIntoNumber(propDef)) {
90 | type = 'number'
91 | }
92 | else if (willTurnIntoBoolean(propDef)) {
93 | type = 'boolean'
94 | }
95 | return type
96 | }
97 |
98 | /**
99 | * @see https://github.com/vuejs/devtools/blob/e7dffa24fe98b212404a1451818b6c66739f88ee/packages/devtools-kit/src/core/component/state/process.ts#L62
100 | * @see https://github.com/vuejs/devtools/blob/e7dffa24fe98b212404a1451818b6c66739f88ee/packages/devtools-kit/src/core/app/index.ts#L14
101 | *
102 | * @param component
103 | */
104 | export function resolveProps(component: DefineComponent | App): ComponentProp[] {
105 | if (component._component && component._component.props && typeof component._component.props === 'object') {
106 | return Object.entries(component._component.props).map(([key, propDef]) => {
107 | return {
108 | key,
109 | title: key,
110 | type: inferType(propDef),
111 | }
112 | })
113 | }
114 | else if ((component as unknown as ComponentInternalInstance).props && typeof (component as unknown as ComponentInternalInstance).props === 'object') {
115 | return Object.entries((component as unknown as ComponentInternalInstance).props).map(([key, propDef]) => {
116 | return {
117 | key,
118 | title: key,
119 | type: inferType(propDef),
120 | }
121 | })
122 | }
123 | else {
124 | return []
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/packages/core/src/render-shared/template.ts:
--------------------------------------------------------------------------------
1 | import type { SFCDescriptor } from '@vue/compiler-sfc'
2 |
3 | // Check if we can use compile template as inlined render function
4 | // inside `
4 | }
5 |
--------------------------------------------------------------------------------
/packages/utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": [
5 | "ESNext"
6 | ],
7 | "module": "ESNext",
8 | "moduleResolution": "bundler",
9 | "esModuleInterop": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "isolatedModules": true,
12 | "verbatimModuleSyntax": true,
13 | "skipLibCheck": true
14 | },
15 | "include": [
16 | "src/**/*.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/vue/README.md:
--------------------------------------------------------------------------------
1 | # `@velin-dev/vue`
2 |
3 | [![npm version][npm-version-src]][npm-version-href]
4 | [![npm downloads][npm-downloads-src]][npm-downloads-href]
5 | [![bundle][bundle-src]][bundle-href]
6 | [![JSDocs][jsdocs-src]][jsdocs-href]
7 | [![License][license-src]][license-href]
8 |
9 | Refer to [README.md](https://github.com/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/vue?style=flat&colorA=080f12&colorB=1fa669
16 | [npm-version-href]: https://npmjs.com/package/@velin-dev/vue
17 | [npm-downloads-src]: https://img.shields.io/npm/dm/@velin-dev/vue?style=flat&colorA=080f12&colorB=1fa669
18 | [npm-downloads-href]: https://npmjs.com/package/@velin-dev/vue
19 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@velin-dev/vue?style=flat&colorA=080f12&colorB=1fa669&label=minzip
20 | [bundle-href]: https://bundlephobia.com/result?p=@velin-dev/vue
21 | [license-src]: https://img.shields.io/github/license/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/vue
25 |
--------------------------------------------------------------------------------
/packages/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@velin-dev/vue",
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/vue"
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 | "./repl": {
32 | "types": "./dist/repl/index.d.ts",
33 | "import": "./dist/repl/index.mjs"
34 | }
35 | },
36 | "main": "./dist/index.mjs",
37 | "module": "./dist/index.mjs",
38 | "types": "./dist/index.d.ts",
39 | "files": [
40 | "README.md",
41 | "dist",
42 | "package.json"
43 | ],
44 | "scripts": {
45 | "dev": "unbuild",
46 | "stub": "unbuild",
47 | "build": "unbuild",
48 | "typecheck": "tsc --noEmit",
49 | "attw": "attw --pack . --profile esm-only --ignore-rules cjs-resolves-to-esm"
50 | },
51 | "dependencies": {
52 | "@velin-dev/core": "workspace:^",
53 | "@velin-dev/utils": "workspace:^",
54 | "vue": "^3.5.14"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/packages/vue/src/composables/index.ts:
--------------------------------------------------------------------------------
1 | export * from './usePrompt'
2 |
--------------------------------------------------------------------------------
/packages/vue/src/composables/usePrompt/index.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | InputProps,
3 | RenderComponentInputComponent,
4 | ResolveRenderComponentInputProps,
5 | } from '@velin-dev/core'
6 | import type { ComponentPropsOptions, Reactive, Ref } from 'vue'
7 |
8 | import { renderComponent } from '@velin-dev/core/browser'
9 | import { isReactive, isRef, ref, toRef, watch, watchEffect } from 'vue'
10 |
11 | export function usePrompt<
12 | RawProps = any,
13 | ComponentProps = ComponentPropsOptions,
14 | ResolvedProps = ResolveRenderComponentInputProps,
15 | >(
16 | promptComponent: RenderComponentInputComponent,
17 | props: InputProps,
18 | ) {
19 | const prompt = ref('')
20 | const rendering = ref(false)
21 |
22 | const onPromptedCallbacks = ref<(() => Promise | void)[]>([])
23 | const onUnPromptedCallbacks = ref<(() => Promise | void)[]>([])
24 |
25 | function onPrompted(cb: () => Promise | void) {
26 | onPromptedCallbacks.value.push(cb)
27 | }
28 |
29 | function onUnprompted(cb: () => Promise | void) {
30 | onUnPromptedCallbacks.value.push(cb)
31 | }
32 |
33 | function renderEffect() {
34 | rendering.value = true
35 |
36 | renderComponent(promptComponent, props).then((md) => {
37 | prompt.value = md
38 | onPromptedCallbacks.value.forEach(cb => cb())
39 | }).finally(() => {
40 | rendering.value = false
41 | })
42 | }
43 |
44 | function dispose() {
45 | onUnPromptedCallbacks.value.forEach(cb => cb())
46 | }
47 |
48 | if (isReactive(props)) {
49 | watch(props as unknown as Reactive, renderEffect)
50 | }
51 | else if (isRef(props)) {
52 | watch(props as unknown as Ref, renderEffect)
53 | }
54 | else if (typeof props === 'object' && props !== null) {
55 | watch(Object.values(props).map((val) => {
56 | if (isReactive(val)) {
57 | return val
58 | }
59 | else if (isRef(val)) {
60 | return val
61 | }
62 | else {
63 | return toRef(val)
64 | }
65 | }), renderEffect)
66 | }
67 | else {
68 | watchEffect(renderEffect)
69 | }
70 |
71 | // immediate: true
72 | renderEffect()
73 |
74 | return {
75 | prompt,
76 | rendering,
77 | dispose,
78 | onPrompted,
79 | onUnprompted,
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/packages/vue/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './composables'
2 |
--------------------------------------------------------------------------------
/packages/vue/src/repl/composables/index.ts:
--------------------------------------------------------------------------------
1 | export * from './usePrompt'
2 |
--------------------------------------------------------------------------------
/packages/vue/src/repl/composables/usePrompt/index.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | InputProps,
3 | ResolveRenderComponentInputProps,
4 | } from '@velin-dev/core'
5 | import type { ComponentProp } from '@velin-dev/core/render-shared'
6 | import type { ComponentPropsOptions, MaybeRefOrGetter, Reactive, Ref } from 'vue'
7 |
8 | import { renderSFCString } from '@velin-dev/core/render-repl'
9 | import { isReactive, isRef, ref, toRef, toValue, watch, watchEffect } from 'vue'
10 |
11 | export function usePrompt<
12 | RawProps = any,
13 | _ComponentProps = ComponentPropsOptions,
14 | _ResolvedProps = ResolveRenderComponentInputProps,
15 | >(
16 | promptComponent: MaybeRefOrGetter,
17 | props: InputProps>,
18 | ) {
19 | const prompt = ref('')
20 | const promptProps = ref([])
21 | const rendering = ref(false)
22 |
23 | const onPromptedCallbacks = ref<(() => Promise | void)[]>([])
24 | const onUnPromptedCallbacks = ref<(() => Promise | void)[]>([])
25 |
26 | function onPrompted(cb: () => Promise | void) {
27 | onPromptedCallbacks.value.push(cb)
28 | }
29 |
30 | function onUnprompted(cb: () => Promise | void) {
31 | onUnPromptedCallbacks.value.push(cb)
32 | }
33 |
34 | function renderEffect() {
35 | rendering.value = true
36 |
37 | renderSFCString(toValue(promptComponent), props).then((renderedResults) => {
38 | prompt.value = renderedResults.rendered
39 | promptProps.value = renderedResults.props
40 | onPromptedCallbacks.value.forEach(cb => cb())
41 | }).finally(() => {
42 | rendering.value = false
43 | })
44 | }
45 |
46 | function dispose() {
47 | onUnPromptedCallbacks.value.forEach(cb => cb())
48 | }
49 |
50 | if (isRef(promptComponent)) {
51 | watch(promptComponent as unknown as Ref, renderEffect)
52 | }
53 | else {
54 | watchEffect(renderEffect)
55 | }
56 |
57 | if (isReactive(props)) {
58 | watch(props as unknown as Reactive>>, renderEffect)
59 | }
60 | else if (isRef(props)) {
61 | watch(props as unknown as Ref>>, renderEffect)
62 | }
63 | else if (typeof props === 'object' && props !== null) {
64 | watch(Object.values(props).map((val) => {
65 | if (isReactive(val)) {
66 | return val
67 | }
68 | else if (isRef(val)) {
69 | return val
70 | }
71 | else {
72 | return toRef(val)
73 | }
74 | }), renderEffect)
75 | }
76 | else {
77 | watchEffect(renderEffect)
78 | }
79 |
80 | // immediate: true
81 | renderEffect()
82 |
83 | return {
84 | prompt,
85 | promptProps,
86 | rendering,
87 | dispose,
88 | onPrompted,
89 | onUnprompted,
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/packages/vue/src/repl/index.ts:
--------------------------------------------------------------------------------
1 | export * from './composables'
2 |
--------------------------------------------------------------------------------
/packages/vue/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": [
5 | "ESNext"
6 | ],
7 | "module": "ESNext",
8 | "moduleResolution": "bundler",
9 | "esModuleInterop": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "isolatedModules": true,
12 | "verbatimModuleSyntax": true,
13 | "skipLibCheck": true
14 | },
15 | "include": [
16 | "src/**/*.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - examples/**
3 | - packages/**
4 | - apps/**
5 |
6 | onlyBuiltDependencies:
7 | - esbuild
8 | - unrs-resolver
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "resolveJsonModule": true,
7 | "strictNullChecks": true,
8 | "noUnusedLocals": true,
9 | "noUnusedParameters": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "isolatedModules": true,
14 | "verbatimModuleSyntax": true,
15 | "skipLibCheck": true
16 | },
17 | "exclude": [
18 | "**/dist/**",
19 | "node_modules"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/uno.config.ts:
--------------------------------------------------------------------------------
1 | import type { PresetOrFactoryAwaitable } from 'unocss'
2 |
3 | import { flavors } from '@catppuccin/palette'
4 | import { createExternalPackageIconLoader } from '@iconify/utils/lib/loader/external-pkg'
5 | import { colorToString } from '@unocss/preset-mini/utils'
6 | import { defineConfig, mergeConfigs, presetAttributify, presetIcons, presetTypography, presetWebFonts, presetWind3, transformerDirectives, transformerVariantGroup } from 'unocss'
7 | import { presetScrollbar } from 'unocss-preset-scrollbar'
8 | import { parseColor } from 'unocss/preset-mini'
9 |
10 | function createColorSchemeConfig(hueOffset = 0) {
11 | return {
12 | DEFAULT: `oklch(62% var(--theme-colors-chroma) calc(var(--theme-colors-hue) + ${hueOffset}))`,
13 | 50: `color-mix(in srgb, oklch(95% var(--theme-colors-chroma-50) calc(var(--theme-colors-hue) + ${hueOffset})) 30%, oklch(100% 0 360))`,
14 | 100: `color-mix(in srgb, oklch(95% var(--theme-colors-chroma-100) calc(var(--theme-colors-hue) + ${hueOffset})) 80%, oklch(100% 0 360))`,
15 | 200: `oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + ${hueOffset}))`,
16 | 300: `oklch(85% var(--theme-colors-chroma-300) calc(var(--theme-colors-hue) + ${hueOffset}))`,
17 | 400: `oklch(74% var(--theme-colors-chroma-400) calc(var(--theme-colors-hue) + ${hueOffset}))`,
18 | 500: `oklch(62% var(--theme-colors-chroma) calc(var(--theme-colors-hue) + ${hueOffset}))`,
19 | 600: `oklch(54% var(--theme-colors-chroma-600) calc(var(--theme-colors-hue) + ${hueOffset}))`,
20 | 700: `oklch(49% var(--theme-colors-chroma-700) calc(var(--theme-colors-hue) + ${hueOffset}))`,
21 | 800: `oklch(42% var(--theme-colors-chroma-800) calc(var(--theme-colors-hue) + ${hueOffset}))`,
22 | 900: `oklch(37% var(--theme-colors-chroma-900) calc(var(--theme-colors-hue) + ${hueOffset}))`,
23 | 950: `oklch(29% var(--theme-colors-chroma-950) calc(var(--theme-colors-hue) + ${hueOffset}))`,
24 | }
25 | }
26 |
27 | export function presetStoryMockHover(): PresetOrFactoryAwaitable {
28 | return {
29 | name: 'story-mock-hover',
30 | variants: [
31 | (matcher) => {
32 | if (!matcher.includes('hover')) {
33 | return matcher
34 | }
35 |
36 | return {
37 | matcher,
38 | selector: (s) => {
39 | return `${s}, ${s.replace(/:hover$/, '')}._hover`
40 | },
41 | }
42 | },
43 | ],
44 | }
45 | }
46 |
47 | export function safelistAllPrimaryBackgrounds(): string[] {
48 | return [
49 | ...[undefined, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950].map((shade) => {
50 | const prefix = shade ? `bg-primary-${shade}` : `bg-primary`
51 | return [
52 | prefix,
53 | ...[5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map(opacity => `${prefix}/${opacity}`),
54 | ]
55 | }).flat(),
56 | ]
57 | }
58 |
59 | export function sharedUnoConfig() {
60 | return defineConfig({
61 | presets: [
62 | presetWind3(),
63 | presetAttributify(),
64 | presetTypography(),
65 | presetWebFonts({
66 | fonts: {
67 | sans: 'DM Sans',
68 | serif: 'DM Serif Display',
69 | mono: 'DM Mono',
70 | cute: 'Kiwi Maru',
71 | cuteen: 'Sniglet',
72 | jura: 'Jura',
73 | gugi: 'Gugi',
74 | quicksand: 'Quicksand',
75 | },
76 | timeouts: {
77 | warning: 5000,
78 | failure: 10000,
79 | },
80 | }),
81 | presetIcons({
82 | scale: 1.2,
83 | collections: {
84 | ...createExternalPackageIconLoader('@proj-airi/lobe-icons'),
85 | },
86 | }),
87 | presetScrollbar(),
88 | ],
89 | transformers: [
90 | transformerDirectives({
91 | applyVariable: ['--at-apply'],
92 | }),
93 | transformerVariantGroup(),
94 | ],
95 | safelist: [
96 | ...'prose prose-sm m-auto text-left'.split(' '),
97 | ],
98 | // hyoban/unocss-preset-shadcn: Use shadcn ui with UnoCSS
99 | // https://github.com/hyoban/unocss-preset-shadcn
100 | //
101 | // Thanks to
102 | // https://github.com/unovue/shadcn-vue/issues/34#issuecomment-2467318118
103 | // https://github.com/hyoban-template/shadcn-vue-unocss-starter
104 | //
105 | // By default, `.ts` and `.js` files are NOT extracted.
106 | // If you want to extract them, use the following configuration.
107 | // It's necessary to add the following configuration if you use shadcn-vue or shadcn-svelte.
108 | content: {
109 | pipeline: {
110 | include: [
111 | // the default
112 | /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
113 | // include js/ts files
114 | '(components|src)/**/*.{js,ts,vue}',
115 | ],
116 | },
117 | },
118 | rules: [
119 | [/^mask-\[(.*)\]$/, ([, suffix]) => ({ '-webkit-mask-image': suffix.replace(/_/g, ' ') })],
120 | [/^bg-dotted-\[(.*)\]$/, ([, color], { theme }) => {
121 | const parsedColor = parseColor(color, theme)
122 | // Util usage: https://github.com/unocss/unocss/blob/f57ef6ae50006a92f444738e50f3601c0d1121f2/packages-presets/preset-mini/src/_utils/utilities.ts#L186
123 | return {
124 | 'background-image': `radial-gradient(circle at 1px 1px, ${colorToString(parsedColor?.cssColor ?? parsedColor?.color ?? color, 'var(--un-background-opacity)')} 1px, transparent 0)`,
125 | '--un-background-opacity': parsedColor?.cssColor?.alpha ?? parsedColor?.alpha ?? 1,
126 | }
127 | }],
128 | ],
129 | theme: {
130 | colors: {
131 | 'primary': createColorSchemeConfig(),
132 | 'complementary': createColorSchemeConfig(180),
133 |
134 | // Palette • Catppuccin
135 | // https://catppuccin.com/palette/
136 | 'latte-rosewater': flavors.latte.colors.rosewater.hex,
137 | 'latte-flamingo': flavors.latte.colors.flamingo.hex,
138 | 'latte-pink': flavors.latte.colors.pink.hex,
139 | 'latte-mauve': flavors.latte.colors.mauve.hex,
140 | 'latte-red': flavors.latte.colors.red.hex,
141 | 'latte-maroon': flavors.latte.colors.maroon.hex,
142 | 'latte-peach': flavors.latte.colors.peach.hex,
143 | 'latte-yellow': flavors.latte.colors.yellow.hex,
144 | 'latte-green': flavors.latte.colors.green.hex,
145 | 'latte-teal': flavors.latte.colors.teal.hex,
146 | 'latte-sky': flavors.latte.colors.sky.hex,
147 | 'latte-sapphire': flavors.latte.colors.sapphire.hex,
148 | 'latte-blue': flavors.latte.colors.blue.hex,
149 | 'latte-lavender': flavors.latte.colors.lavender.hex,
150 | 'latte-text': flavors.latte.colors.text.hex,
151 | 'latte-subtext-0': flavors.latte.colors.subtext0.hex,
152 | 'latte-subtext-1': flavors.latte.colors.subtext1.hex,
153 | 'latte-overlay-0': flavors.latte.colors.overlay0.hex,
154 | 'latte-overlay-1': flavors.latte.colors.overlay1.hex,
155 | 'latte-overlay-2': flavors.latte.colors.overlay2.hex,
156 | 'latte-surface-0': flavors.latte.colors.surface0.hex,
157 | 'latte-surface-1': flavors.latte.colors.surface1.hex,
158 | 'latte-surface-2': flavors.latte.colors.surface2.hex,
159 | 'latte-base': flavors.latte.colors.base.hex,
160 | 'latte-mantle': flavors.latte.colors.mantle.hex,
161 | 'latte-crust': flavors.latte.colors.crust.hex,
162 |
163 | 'frappe-rosewater': flavors.frappe.colors.rosewater.hex,
164 | 'frappe-flamingo': flavors.frappe.colors.flamingo.hex,
165 | 'frappe-pink': flavors.frappe.colors.pink.hex,
166 | 'frappe-mauve': flavors.frappe.colors.mauve.hex,
167 | 'frappe-red': flavors.frappe.colors.red.hex,
168 | 'frappe-maroon': flavors.frappe.colors.maroon.hex,
169 | 'frappe-peach': flavors.frappe.colors.peach.hex,
170 | 'frappe-yellow': flavors.frappe.colors.yellow.hex,
171 | 'frappe-green': flavors.frappe.colors.green.hex,
172 | 'frappe-teal': flavors.frappe.colors.teal.hex,
173 | 'frappe-sky': flavors.frappe.colors.sky.hex,
174 | 'frappe-sapphire': flavors.frappe.colors.sapphire.hex,
175 | 'frappe-blue': flavors.frappe.colors.blue.hex,
176 | 'frappe-lavender': flavors.frappe.colors.lavender.hex,
177 | 'frappe-text': flavors.frappe.colors.text.hex,
178 | 'frappe-subtext-0': flavors.frappe.colors.subtext0.hex,
179 | 'frappe-subtext-1': flavors.frappe.colors.subtext1.hex,
180 | 'frappe-overlay-0': flavors.frappe.colors.overlay0.hex,
181 | 'frappe-overlay-1': flavors.frappe.colors.overlay1.hex,
182 | 'frappe-overlay-2': flavors.frappe.colors.overlay2.hex,
183 | 'frappe-surface-0': flavors.frappe.colors.surface0.hex,
184 | 'frappe-surface-1': flavors.frappe.colors.surface1.hex,
185 | 'frappe-surface-2': flavors.frappe.colors.surface2.hex,
186 | 'frappe-base': flavors.frappe.colors.base.hex,
187 | 'frappe-mantle': flavors.frappe.colors.mantle.hex,
188 | 'frappe-crust': flavors.frappe.colors.crust.hex,
189 |
190 | 'macchiato-rosewater': flavors.macchiato.colors.rosewater.hex,
191 | 'macchiato-flamingo': flavors.macchiato.colors.flamingo.hex,
192 | 'macchiato-pink': flavors.macchiato.colors.pink.hex,
193 | 'macchiato-mauve': flavors.macchiato.colors.mauve.hex,
194 | 'macchiato-red': flavors.macchiato.colors.red.hex,
195 | 'macchiato-maroon': flavors.macchiato.colors.maroon.hex,
196 | 'macchiato-peach': flavors.macchiato.colors.peach.hex,
197 | 'macchiato-yellow': flavors.macchiato.colors.yellow.hex,
198 | 'macchiato-green': flavors.macchiato.colors.green.hex,
199 | 'macchiato-teal': flavors.macchiato.colors.teal.hex,
200 | 'macchiato-sky': flavors.macchiato.colors.sky.hex,
201 | 'macchiato-sapphire': flavors.macchiato.colors.sapphire.hex,
202 | 'macchiato-blue': flavors.macchiato.colors.blue.hex,
203 | 'macchiato-lavender': flavors.macchiato.colors.lavender.hex,
204 | 'macchiato-text': flavors.macchiato.colors.text.hex,
205 | 'macchiato-subtext-0': flavors.macchiato.colors.subtext0.hex,
206 | 'macchiato-subtext-1': flavors.macchiato.colors.subtext1.hex,
207 | 'macchiato-overlay-0': flavors.macchiato.colors.overlay0.hex,
208 | 'macchiato-overlay-1': flavors.macchiato.colors.overlay1.hex,
209 | 'macchiato-overlay-2': flavors.macchiato.colors.overlay2.hex,
210 | 'macchiato-surface-0': flavors.macchiato.colors.surface0.hex,
211 | 'macchiato-surface-1': flavors.macchiato.colors.surface1.hex,
212 | 'macchiato-surface-2': flavors.macchiato.colors.surface2.hex,
213 |
214 | 'mocha-rosewater': flavors.mocha.colors.rosewater.hex,
215 | 'mocha-flamingo': flavors.mocha.colors.flamingo.hex,
216 | 'mocha-pink': flavors.mocha.colors.pink.hex,
217 | 'mocha-mauve': flavors.mocha.colors.mauve.hex,
218 | 'mocha-red': flavors.mocha.colors.red.hex,
219 | 'mocha-maroon': flavors.mocha.colors.maroon.hex,
220 | 'mocha-peach': flavors.mocha.colors.peach.hex,
221 | 'mocha-yellow': flavors.mocha.colors.yellow.hex,
222 | 'mocha-green': flavors.mocha.colors.green.hex,
223 | 'mocha-teal': flavors.mocha.colors.teal.hex,
224 | 'mocha-sky': flavors.mocha.colors.sky.hex,
225 | 'mocha-sapphire': flavors.mocha.colors.sapphire.hex,
226 | 'mocha-blue': flavors.mocha.colors.blue.hex,
227 | 'mocha-lavender': flavors.mocha.colors.lavender.hex,
228 | 'mocha-text': flavors.mocha.colors.text.hex,
229 | 'mocha-subtext-0': flavors.mocha.colors.subtext0.hex,
230 | 'mocha-subtext-1': flavors.mocha.colors.subtext1.hex,
231 | 'mocha-overlay-0': flavors.mocha.colors.overlay0.hex,
232 | 'mocha-overlay-1': flavors.mocha.colors.overlay1.hex,
233 | 'mocha-overlay-2': flavors.mocha.colors.overlay2.hex,
234 | 'mocha-surface-0': flavors.mocha.colors.surface0.hex,
235 | 'mocha-surface-1': flavors.mocha.colors.surface1.hex,
236 | 'mocha-surface-2': flavors.mocha.colors.surface2.hex,
237 | },
238 | },
239 | })
240 | }
241 |
242 | export default mergeConfigs([
243 | sharedUnoConfig(),
244 | ])
245 |
--------------------------------------------------------------------------------
/vitest.workspace.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | 'examples/*',
3 | 'packages/*',
4 | ]
5 |
--------------------------------------------------------------------------------