├── .git-blame-ignore-revs ├── .github ├── renovate.json5 └── workflows │ ├── release-continuous.yml │ └── release.yml ├── .gitignore ├── .node-version ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.js ├── index-dist.html ├── index.html ├── package.json ├── pnpm-lock.yaml ├── src ├── Message.vue ├── Repl.vue ├── SplitPane.vue ├── codemirror │ ├── CodeMirror.vue │ ├── codemirror.css │ └── codemirror.ts ├── core.ts ├── editor │ ├── CodeMirrorEditor.vue │ ├── EditorContainer.vue │ ├── FileSelector.vue │ ├── MonacoEditor.vue │ └── ToggleButton.vue ├── env.d.ts ├── import-map.ts ├── index.ts ├── jsx.ts ├── monaco │ ├── Monaco.vue │ ├── env.ts │ ├── highlight.ts │ ├── language-configs.ts │ ├── utils.ts │ └── vue.worker.ts ├── output │ ├── Output.vue │ ├── Preview.vue │ ├── PreviewProxy.ts │ ├── Sandbox.vue │ ├── SsrOutput.vue │ ├── moduleCompiler.ts │ └── srcdoc.html ├── sourcemap.ts ├── store.ts ├── template │ ├── new-sfc.vue │ └── welcome.vue ├── transform.ts ├── types.ts ├── utils.ts ├── vue-dev-proxy.ts └── vue-server-renderer-dev-proxy.ts ├── ssr-stub.js ├── test └── main.ts ├── tsconfig.json ├── vite.config.ts └── vite.preview.config.ts /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # style: add trailing comma 2 | 497f07527b162f42123ead110031f265981f4d4d 3 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'config:base', 5 | 'schedule:weekly', 6 | 'group:allNonMajor', 7 | ':semanticCommitTypeAll(chore)', 8 | ], 9 | labels: ['dependencies'], 10 | rangeStrategy: 'bump', 11 | packageRules: [ 12 | { 13 | depTypeList: ['peerDependencies'], 14 | enabled: false, 15 | }, 16 | { 17 | matchPackageNames: ['codemirror'], 18 | matchUpdateTypes: ['major'], 19 | enabled: false, 20 | }, 21 | ], 22 | postUpdateOptions: ['pnpmDedupe'], 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/release-continuous.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - name: Install pnpm 13 | uses: pnpm/action-setup@v4 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version-file: '.node-version' 19 | cache: 'pnpm' 20 | 21 | - name: Install dependencies 22 | run: pnpm install 23 | 24 | - name: Build 25 | run: pnpm build 26 | 27 | - run: pnpx pkg-pr-new publish --compact 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | id-token: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v4 22 | 23 | - name: Install Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version-file: '.node-version' 27 | cache: pnpm 28 | registry-url: 'https://registry.npmjs.org' 29 | 30 | - run: npx changelogithub 31 | continue-on-error: true 32 | env: 33 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 34 | 35 | - name: Install Dependencies 36 | run: pnpm i 37 | 38 | - name: Publish to NPM 39 | run: pnpm -r publish --access public --no-git-checks 40 | env: 41 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 42 | NPM_CONFIG_PROVENANCE: true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | TODOs.md 5 | 6 | # jetbrains files 7 | .idea 8 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | CHANGELOG.md 3 | pnpm-lock.yaml 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.6.1](https://github.com/vuejs/repl/compare/v4.6.0...v4.6.1) (2025-06-13) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * the line number offset in the DEV ([#344](https://github.com/vuejs/repl/issues/344)) ([cc292d3](https://github.com/vuejs/repl/commit/cc292d3dba25ac872edf147e14b6c9bbf6a0c577)) 7 | 8 | 9 | 10 | # [4.6.0](https://github.com/vuejs/repl/compare/v4.5.1...v4.6.0) (2025-06-13) 11 | 12 | 13 | ### Features 14 | 15 | * add support for viewing sourcemap ([#341](https://github.com/vuejs/repl/issues/341)) ([5714d5b](https://github.com/vuejs/repl/commit/5714d5b706d7c945ee5393bef545dcf70c189db3)) 16 | * show SSR output ([#343](https://github.com/vuejs/repl/issues/343)) ([52a193a](https://github.com/vuejs/repl/commit/52a193a8a658d0059ee8c30345ca10ff52af7c04)) 17 | 18 | 19 | 20 | ## [4.5.1](https://github.com/vuejs/repl/compare/v4.5.0...v4.5.1) (2025-02-19) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * apply builtin import map after deserialize ([#315](https://github.com/vuejs/repl/issues/315)) ([e62ddda](https://github.com/vuejs/repl/commit/e62ddda06fe3339a467f88b13b0271c2a5c7e96d)) 26 | * **split-pane:** rendering order comes from the store.show-output ([0bd4c17](https://github.com/vuejs/repl/commit/0bd4c17b6dd26d4e17387f50e089dd3ffefaf054)) 27 | * ts error ([a927083](https://github.com/vuejs/repl/commit/a927083734f1d4dae5be0d200aafd26e49ce82aa)) 28 | 29 | 30 | ### Features 31 | 32 | * add core entry for node usage ([#310](https://github.com/vuejs/repl/issues/310)) ([da105a4](https://github.com/vuejs/repl/commit/da105a42618899d214701a5fb6549719f73331bb)) 33 | * **editor:** scrollbar style for firefox ([#320](https://github.com/vuejs/repl/issues/320)) ([bbc740b](https://github.com/vuejs/repl/commit/bbc740bfa840dfb6c77824470f1ffdc4a6261e85)) 34 | * export `Sandbox` as standalone output component ([#309](https://github.com/vuejs/repl/issues/309)) ([b549715](https://github.com/vuejs/repl/commit/b5497152fefe8f190eca59755bedb27b2f3178f2)) 35 | * **store:** return `setImportMap` and add `merge` parameter ([9f53bd1](https://github.com/vuejs/repl/commit/9f53bd11aee1d75984e5597878e53bec4ae168e5)) 36 | * support cache selected typescript version ([#305](https://github.com/vuejs/repl/issues/305)) ([33ca3c0](https://github.com/vuejs/repl/commit/33ca3c0317aa0418c094ec8f9e6712d81fa11465)) 37 | * support vapor template-only component ([#322](https://github.com/vuejs/repl/issues/322)) ([9ae056b](https://github.com/vuejs/repl/commit/9ae056b701ff54446c5c1ec9f29444d5239e0931)) 38 | 39 | 40 | 41 | # [4.5.0](https://github.com/vuejs/repl/compare/v4.4.3...v4.5.0) (2025-02-03) 42 | 43 | 44 | ### Features 45 | 46 | * pass on descriptor vapor flag when compiling template ([adaaceb](https://github.com/vuejs/repl/commit/adaaceb24984435ae02ab3eda071f10dba9e0362)) 47 | 48 | 49 | 50 | ## [4.4.3](https://github.com/vuejs/repl/compare/v4.4.2...v4.4.3) (2025-01-02) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * transform jsx for entire file ([48325f9](https://github.com/vuejs/repl/commit/48325f99e010c3065c99efd4fb3e95950cda9596)), closes [#301](https://github.com/vuejs/repl/issues/301) [#300](https://github.com/vuejs/repl/issues/300) 56 | 57 | 58 | 59 | ## [4.4.2](https://github.com/vuejs/repl/compare/v4.4.1...v4.4.2) (2024-09-16) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * output toggle button ([#279](https://github.com/vuejs/repl/issues/279)) ([93051f3](https://github.com/vuejs/repl/commit/93051f35a232b53d01dd0a40623cab5b11baa3ee)) 65 | * upgrade vue language tools ([ec393cf](https://github.com/vuejs/repl/commit/ec393cfe4b8b8008976e4fd2017bd112d98fa599)) 66 | 67 | 68 | 69 | ## [4.4.1](https://github.com/vuejs/repl/compare/v4.4.0...v4.4.1) (2024-09-08) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * cancel creating new file ([#281](https://github.com/vuejs/repl/issues/281)) ([7467f38](https://github.com/vuejs/repl/commit/7467f38f65e4b05dacc389644a8001b24f86fcdb)) 75 | * type error ([6653d0e](https://github.com/vuejs/repl/commit/6653d0e4b0b30eeee4bfe0c0c92bf00a84c0c753)) 76 | 77 | 78 | ### Features 79 | 80 | * add `autoSave` toggle button ([#283](https://github.com/vuejs/repl/issues/283)) ([83d8e48](https://github.com/vuejs/repl/commit/83d8e487de724261cf709c5648cc2512b4c33732)) 81 | * export `languageToolsVersion` ([5a92a92](https://github.com/vuejs/repl/commit/5a92a9259da01da4aa30b09ed6dcedfb4503c71d)) 82 | 83 | 84 | 85 | # [4.4.0](https://github.com/vuejs/repl/compare/v4.3.1...v4.4.0) (2024-09-07) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * debounce reloadLanguageTools in monaco ([f9f650a](https://github.com/vuejs/repl/commit/f9f650ada945f7ea597b7e7b51c132c4594bd5cb)), closes [#275](https://github.com/vuejs/repl/issues/275) [#263](https://github.com/vuejs/repl/issues/263) 91 | * no default value for object props ([d786626](https://github.com/vuejs/repl/commit/d78662652ec18f4610facc8f3e2a50f38c01f93f)) 92 | * rollback & pin volar version ([c6f58c7](https://github.com/vuejs/repl/commit/c6f58c7f4d04799a64f788251362f8349728bea4)) 93 | 94 | 95 | ### Features 96 | 97 | * add more customization options ([#274](https://github.com/vuejs/repl/issues/274)) ([c73b786](https://github.com/vuejs/repl/commit/c73b7868d73d3dad792c80a36507ce92234443d4)) 98 | * expose `editorOptions.monacoOptions` ([00176d0](https://github.com/vuejs/repl/commit/00176d007ff5eb74e216abff3de87e625cde543b)), closes [#277](https://github.com/vuejs/repl/issues/277) [#232](https://github.com/vuejs/repl/issues/232) 99 | * reactivity `autoSave` option ([#266](https://github.com/vuejs/repl/issues/266)) ([d90082a](https://github.com/vuejs/repl/commit/d90082a2e44956e08fc64d296661b30ea4590506)) 100 | 101 | 102 | 103 | ## [4.3.1](https://github.com/vuejs/repl/compare/v4.3.0...v4.3.1) (2024-07-03) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * match source file ([4cf06b6](https://github.com/vuejs/repl/commit/4cf06b63fa1807ccfbf14c0ef1f1fa9b1f268717)) 109 | 110 | 111 | ### Reverts 112 | 113 | * refactor: replace assert with assert-plus ([e55baa4](https://github.com/vuejs/repl/commit/e55baa481f70c387f24cebfdfa7f143814fb0ce2)) 114 | 115 | 116 | 117 | # [4.3.0](https://github.com/vuejs/repl/compare/v4.2.1...v4.3.0) (2024-07-02) 118 | 119 | 120 | ### Bug Fixes 121 | 122 | * alert if deserialization fails ([071b1d1](https://github.com/vuejs/repl/commit/071b1d1216fa3df2e14c9c6e5453cfe85eed4b79)), closes [#256](https://github.com/vuejs/repl/issues/256) 123 | * move assert-plus to devDep ([dd9f1bb](https://github.com/vuejs/repl/commit/dd9f1bb74c17f19c25aa9b0e485366781094e818)) 124 | 125 | 126 | ### Features 127 | 128 | * show view size while dragging split pane ([#253](https://github.com/vuejs/repl/issues/253)) ([a6bbeea](https://github.com/vuejs/repl/commit/a6bbeea7b8ec7c1302ba08afa0c789ad198cc8e2)) 129 | * volar 2.x ([#225](https://github.com/vuejs/repl/issues/225)) ([47030b6](https://github.com/vuejs/repl/commit/47030b66a6f1811a24d8292f9f3aa5185f7e8e23)) 130 | 131 | 132 | 133 | ## [4.2.1](https://github.com/vuejs/repl/compare/v4.2.0...v4.2.1) (2024-05-31) 134 | 135 | 136 | ### Bug Fixes 137 | 138 | * don't overwrite import map from initial state ([1410b8c](https://github.com/vuejs/repl/commit/1410b8cac4dd993c5ba6a94e299b261ed84c3f12)), closes [#252](https://github.com/vuejs/repl/issues/252) 139 | 140 | 141 | 142 | # [4.2.0](https://github.com/vuejs/repl/compare/v4.1.2...v4.2.0) (2024-05-26) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * refine dragging view area ([#246](https://github.com/vuejs/repl/issues/246)) ([df14639](https://github.com/vuejs/repl/commit/df14639b85edc0e153ba2fd6f29656785a0af3aa)) 148 | * specify unspported pre-processors lang ([#212](https://github.com/vuejs/repl/issues/212)) ([5cea974](https://github.com/vuejs/repl/commit/5cea974451ae23b82bea0c6270ad7ac726d831a4)) 149 | 150 | 151 | ### Features 152 | 153 | * `CodeMirror` support `autoSave` option ([#249](https://github.com/vuejs/repl/issues/249)) ([ae80c5b](https://github.com/vuejs/repl/commit/ae80c5b995ffa375013b665fd9a212c5607a1236)) 154 | * add `autoSave` option ([#247](https://github.com/vuejs/repl/issues/247)) ([d47eca5](https://github.com/vuejs/repl/commit/d47eca5926dcac798171fc216fcee2e21f275dd4)) 155 | * jsx for vue ([#248](https://github.com/vuejs/repl/issues/248)) ([d5b0d40](https://github.com/vuejs/repl/commit/d5b0d40ecc7f630b89e45ebe8472bc4e7563b3e2)) 156 | 157 | 158 | ### Performance Improvements 159 | 160 | * avoid parse repeatedly ([c6b7352](https://github.com/vuejs/repl/commit/c6b735298d5ab630cdc130aad7b8acaf7c9c41bb)) 161 | 162 | 163 | 164 | ## [4.1.2](https://github.com/vuejs/repl/compare/v4.1.1...v4.1.2) (2024-04-26) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * dynamic import ([#213](https://github.com/vuejs/repl/issues/213)) ([bb6f1fe](https://github.com/vuejs/repl/commit/bb6f1fe8599f1a11cd1b8aa40a630bea09d2b577)) 170 | * fix file rename breaking ([caace63](https://github.com/vuejs/repl/commit/caace639bde964323a253bdcd252a18869973a1d)) 171 | 172 | 173 | 174 | ## [4.1.1](https://github.com/vuejs/repl/compare/v4.1.0...v4.1.1) (2024-02-14) 175 | 176 | 177 | ### Bug Fixes 178 | 179 | * add vue import maps for default import map ([c74673f](https://github.com/vuejs/repl/commit/c74673fb55d2232de55562e62a818142681bdc8b)) 180 | * reload preview style error after switching theme ([#214](https://github.com/vuejs/repl/issues/214)) ([bc4c76c](https://github.com/vuejs/repl/commit/bc4c76c3f143b5edc3546d80002cb813704b8351)) 181 | 182 | 183 | 184 | # [4.1.0](https://github.com/vuejs/repl/compare/v4.0.2...v4.1.0) (2024-02-11) 185 | 186 | 187 | ### Features 188 | 189 | * add `previewTheme` prop ([c830fc4](https://github.com/vuejs/repl/commit/c830fc434a1781523af332d289957bc485f51a0b)) 190 | 191 | 192 | 193 | ## [4.0.2](https://github.com/vuejs/repl/compare/v4.0.1...v4.0.2) (2024-02-10) 194 | 195 | 196 | ### Bug Fixes 197 | 198 | * respect vue version at initialization ([ef22052](https://github.com/vuejs/repl/commit/ef22052055590dbbe6e85e26ce368938b0c93266)) 199 | 200 | 201 | 202 | ## [4.0.1](https://github.com/vuejs/repl/compare/v4.0.0...v4.0.1) (2024-02-10) 203 | 204 | 205 | ### Bug Fixes 206 | 207 | * save version only when serialize ([d3ee13d](https://github.com/vuejs/repl/commit/d3ee13ded3c5a162bf990ef83cb9a38991792170)) 208 | 209 | 210 | ### Features 211 | 212 | * register language configuration ([3ad7035](https://github.com/vuejs/repl/commit/3ad7035e26cb02626b06e58f53e12ffb5443a5fc)) 213 | 214 | 215 | 216 | # [4.0.0](https://github.com/vuejs/repl/compare/v4.0.0-beta.0...v4.0.0) (2024-02-10) 217 | 218 | 219 | ### Features 220 | 221 | * expose loading status ([eee6bb3](https://github.com/vuejs/repl/commit/eee6bb38ddecbe8bf7ba3ab77d6a549e654b6313)) 222 | * save vue version ([08b4492](https://github.com/vuejs/repl/commit/08b4492fe883bdd4bbe7fd972cd3fbbd8f6416cf)) 223 | 224 | 225 | 226 | # [4.0.0-beta.0](https://github.com/vuejs/repl/compare/v4.0.0-alpha.1...v4.0.0-beta.0) (2024-02-10) 227 | 228 | 229 | ### Bug Fixes 230 | 231 | * pass readonly in code mirror editor ([1100158](https://github.com/vuejs/repl/commit/1100158aec97dae9cf47ac04ff2bb9ec00d05e58)) 232 | 233 | 234 | 235 | # [4.0.0-alpha.1](https://github.com/vuejs/repl/compare/v4.0.0-alpha.0...v4.0.0-alpha.1) (2024-01-24) 236 | 237 | 238 | ### Bug Fixes 239 | 240 | * don't re-create import map file ([9e6f078](https://github.com/vuejs/repl/commit/9e6f078206883821d9bc618a194cf50333f38d3d)) 241 | 242 | 243 | 244 | # [4.0.0-alpha.0](https://github.com/vuejs/repl/compare/v3.3.0...v4.0.0-alpha.0) (2024-01-21) 245 | 246 | 247 | ### Bug Fixes 248 | 249 | * add corresponding black theme background ([#206](https://github.com/vuejs/repl/issues/206)) ([3921c85](https://github.com/vuejs/repl/commit/3921c85f90a40a871838f9740fa3588e2cfa4758)) 250 | * don't overwrite `a` tag without href ([#209](https://github.com/vuejs/repl/issues/209)) ([c7fcf38](https://github.com/vuejs/repl/commit/c7fcf381f195ffce6284cc92c26f8a90e09b484a)) 251 | * don't show tsconfig if not present ([ca548b2](https://github.com/vuejs/repl/commit/ca548b240addd4f58851a545622684a00eb09a0a)) 252 | 253 | 254 | ### Features 255 | 256 | * export `package.json` ([79a22de](https://github.com/vuejs/repl/commit/79a22deb84aa50e31cf6506a67e93f69291fb82f)) 257 | 258 | 259 | 260 | # [3.3.0](https://github.com/vuejs/repl/compare/v3.2.0...v3.3.0) (2024-01-11) 261 | 262 | 263 | ### Bug Fixes 264 | 265 | * default to white color on dark theme ([#202](https://github.com/vuejs/repl/issues/202)) ([481035a](https://github.com/vuejs/repl/commit/481035a443031e50de26b75c8e5b86fbb8ca96f2)) 266 | * serialize import maps ([e085e30](https://github.com/vuejs/repl/commit/e085e3041a228fe0ec076056e23e8f55258120ab)), closes [#204](https://github.com/vuejs/repl/issues/204) 267 | 268 | 269 | ### Features 270 | 271 | * add theme as classname to sandbox ([#203](https://github.com/vuejs/repl/issues/203)) ([7e9dc0f](https://github.com/vuejs/repl/commit/7e9dc0f3b1f2c488664ccfa22cdf21ba19926158)) 272 | * apply theme to preview ([#200](https://github.com/vuejs/repl/issues/200)) ([7ae1061](https://github.com/vuejs/repl/commit/7ae106129274f13393808000fd25995d919ae0bd)) 273 | * mutable sfc options ([9e83b09](https://github.com/vuejs/repl/commit/9e83b09344ecc90ad0e28024a9b260fff97ffccd)) 274 | * support custom template ([#196](https://github.com/vuejs/repl/issues/196)) ([8038b49](https://github.com/vuejs/repl/commit/8038b49cc5fb76a7dc34acffcda5b3f55ff8aa11)) 275 | 276 | 277 | 278 | # [3.2.0](https://github.com/vuejs/repl/compare/v3.1.1...v3.2.0) (2024-01-03) 279 | 280 | 281 | ### Bug Fixes 282 | 283 | * **codemirror:** fix codemirror editor showing nothing on start on small layouts when starting in ouput mode ([#181](https://github.com/vuejs/repl/issues/181)) ([6d7598d](https://github.com/vuejs/repl/commit/6d7598d763c79d777efae4e17ef61132930ae9a0)) 284 | * **messages:** place error messages in editor in front of bottom toggles ([#183](https://github.com/vuejs/repl/issues/183)) ([b1594d0](https://github.com/vuejs/repl/commit/b1594d07dbb29d7d3c15afa4110a4005d4245297)) 285 | * **Preview:** fix style loading delay ([#191](https://github.com/vuejs/repl/issues/191)) ([ece4414](https://github.com/vuejs/repl/commit/ece4414186fab8bb19290ed047e2a4ab665ae3ef)) 286 | 287 | 288 | ### Features 289 | 290 | * **playground:** use a height of 100dvh with fallback to original 100vh ([#182](https://github.com/vuejs/repl/issues/182)) ([9e99990](https://github.com/vuejs/repl/commit/9e99990f7aa2bd8792510fcc03fda931691e8353)) 291 | 292 | 293 | 294 | ## [3.1.1](https://github.com/vuejs/repl/compare/v3.1.0...v3.1.1) (2024-01-02) 295 | 296 | 297 | ### Bug Fixes 298 | 299 | * pass sfc template options to sfc parse ([d72dfdf](https://github.com/vuejs/repl/commit/d72dfdfd2e2670592c957616fcf4e694609912a0)) 300 | 301 | 302 | 303 | # [3.1.0](https://github.com/vuejs/repl/compare/v3.0.1...v3.1.0) (2023-12-21) 304 | 305 | 306 | ### Bug Fixes 307 | 308 | * remove onigasm dep ([e7a73ac](https://github.com/vuejs/repl/commit/e7a73ac249ce44a6f4b661f6e6ff4842f3225d6b)) 309 | 310 | 311 | ### Features 312 | 313 | * use shikiji for more accurate highlight ([#190](https://github.com/vuejs/repl/issues/190)) ([e79aa1a](https://github.com/vuejs/repl/commit/e79aa1af8dc898d9170c5f33ee031ead61f32320)) 314 | 315 | 316 | 317 | ## [3.0.1](https://github.com/vuejs/repl/compare/v3.0.0...v3.0.1) (2023-12-19) 318 | 319 | 320 | ### Bug Fixes 321 | 322 | * ensure reuse AST in non-inline mode ([5e4c710](https://github.com/vuejs/repl/commit/5e4c7101e4b6cc27fb0810390b0ca0287a101149)) 323 | 324 | 325 | 326 | # [3.0.0](https://github.com/vuejs/repl/compare/v2.9.0...v3.0.0) (2023-11-30) 327 | 328 | 329 | ### Bug Fixes 330 | 331 | * handle main file src prefix when setting files + avoid infinite loop due to state.error push ([743b731](https://github.com/vuejs/repl/commit/743b73121dbd63f164a013c8ba722d0a8bfe5ebd)) 332 | * make main repl styles lower specificity for easier override ([fbfaa44](https://github.com/vuejs/repl/commit/fbfaa4495c9bbf3ab936bec27445c52c9521b67e)) 333 | 334 | 335 | 336 | # [2.9.0](https://github.com/vuejs/repl/compare/v2.8.1...v2.9.0) (2023-11-30) 337 | 338 | 339 | ### Bug Fixes 340 | 341 | * **types:** fix editor prop types ([828f202](https://github.com/vuejs/repl/commit/828f2027ff3986a029de3833f521525c7ac3e1d7)) 342 | 343 | 344 | ### Features 345 | 346 | * support custom element styles ([#173](https://github.com/vuejs/repl/issues/173)) ([812730d](https://github.com/vuejs/repl/commit/812730db62b6f1865cee90b67f9f593412a0dce6)) 347 | 348 | 349 | 350 | ## [2.8.1](https://github.com/vuejs/repl/compare/v2.8.0...v2.8.1) (2023-11-28) 351 | 352 | 353 | ### Bug Fixes 354 | 355 | * new sfc file template ([79643d7](https://github.com/vuejs/repl/commit/79643d71a5eabd7e7c9c092e8501cc07f9ee5136)) 356 | * worker plugins for vite 5 ([6e66250](https://github.com/vuejs/repl/commit/6e6625084d0c9ba8c24915ebd1060b7421e0de5b)) 357 | 358 | 359 | 360 | # [2.8.0](https://github.com/vuejs/repl/compare/v2.7.0...v2.8.0) (2023-11-19) 361 | 362 | 363 | ### Features 364 | 365 | * add template for new file ([9a0be1d](https://github.com/vuejs/repl/commit/9a0be1df8c06ffdeab2985f9e9cd5f2cde1437fe)) 366 | 367 | 368 | 369 | # [2.7.0](https://github.com/vuejs/repl/compare/v2.6.3...v2.7.0) (2023-11-12) 370 | 371 | 372 | ### Features 373 | 374 | * support toggling between dev/prod for Vue runtime ([8d3a2e6](https://github.com/vuejs/repl/commit/8d3a2e62358104663af48531467ac8eda4bafffa)) 375 | 376 | 377 | 378 | ## [2.6.3](https://github.com/vuejs/repl/compare/v2.6.2...v2.6.3) (2023-11-03) 379 | 380 | 381 | 382 | ## [2.6.2](https://github.com/vuejs/repl/compare/v2.6.1...v2.6.2) (2023-11-01) 383 | 384 | 385 | 386 | ## [2.6.1](https://github.com/vuejs/repl/compare/v2.6.0...v2.6.1) (2023-10-26) 387 | 388 | 389 | 390 | # [2.6.0](https://github.com/vuejs/repl/compare/v2.5.8...v2.6.0) (2023-10-26) 391 | 392 | 393 | ### Features 394 | 395 | * add layout reverse api [#162](https://github.com/vuejs/repl/issues/162) ([#163](https://github.com/vuejs/repl/issues/163)) ([c1cd77a](https://github.com/vuejs/repl/commit/c1cd77a913b050e2fb3d921d4dcd86a1db74b8b1)) 396 | * support custom display placeholder content ([#160](https://github.com/vuejs/repl/issues/160)) ([9ca27a1](https://github.com/vuejs/repl/commit/9ca27a12cf92b6ac6b7132a5c2ae667a13af4faa)) 397 | 398 | 399 | 400 | ## [2.5.8](https://github.com/vuejs/repl/compare/v2.5.7...v2.5.8) (2023-08-10) 401 | 402 | 403 | ### Bug Fixes 404 | 405 | * don't set editor value if not changed ([bd59eef](https://github.com/vuejs/repl/commit/bd59eefb1d2731179f772ab118ee642f453fa5d2)), closes [#147](https://github.com/vuejs/repl/issues/147) 406 | 407 | 408 | 409 | ## [2.5.7](https://github.com/vuejs/repl/compare/v2.5.6...v2.5.7) (2023-08-08) 410 | 411 | 412 | ### Bug Fixes 413 | 414 | * respect value from monaco editor props ([49fdc71](https://github.com/vuejs/repl/commit/49fdc7161ec91fed617043aca0b751858a10289e)), closes [#145](https://github.com/vuejs/repl/issues/145) 415 | 416 | 417 | 418 | ## [2.5.6](https://github.com/vuejs/repl/compare/v2.5.5...v2.5.6) (2023-07-31) 419 | 420 | 421 | ### Bug Fixes 422 | 423 | * remove preinstall ([8e41043](https://github.com/vuejs/repl/commit/8e410433eb46b45845c39aca8ad2895c3fabae12)), closes [/github.com/vuejs/repl/commit/569fe6275db45a420850cac9419b4614a51a360e#r123111912](https://github.com//github.com/vuejs/repl/commit/569fe6275db45a420850cac9419b4614a51a360e/issues/r123111912) 424 | 425 | 426 | 427 | ## [2.5.5](https://github.com/vuejs/repl/compare/v2.5.4...v2.5.5) (2023-07-09) 428 | 429 | 430 | ### Features 431 | 432 | * expose dependency version ([aecfd8a](https://github.com/vuejs/repl/commit/aecfd8a92e6e7814dd6dbd5d5e94f71ef9fe5b1a)) 433 | 434 | 435 | 436 | ## [2.5.4](https://github.com/vuejs/repl/compare/v2.5.3...v2.5.4) (2023-07-09) 437 | 438 | 439 | ### Bug Fixes 440 | 441 | * replace NODE_ENV ([863f8f3](https://github.com/vuejs/repl/commit/863f8f39d36d25240388a9c5bc68eff0ea7e7856)) 442 | 443 | 444 | 445 | ## [2.5.3](https://github.com/vuejs/repl/compare/v2.5.2...v2.5.3) (2023-07-08) 446 | 447 | 448 | ### Bug Fixes 449 | 450 | * filename index auto-increment ([#133](https://github.com/vuejs/repl/issues/133)) ([4f55810](https://github.com/vuejs/repl/commit/4f55810f729fc61e22eafa7ea69afe79bcfe1cb6)) 451 | * make reloadLanguageTools optional ([5ab1a2d](https://github.com/vuejs/repl/commit/5ab1a2d149820ecb737c3bc97581a87f3adc83d7)) 452 | * use dev version of compiler ([#132](https://github.com/vuejs/repl/issues/132)) ([bfc3522](https://github.com/vuejs/repl/commit/bfc3522422926b0e3f18c1368111066cf268e206)) 453 | 454 | 455 | 456 | ## [2.5.2](https://github.com/vuejs/repl/compare/v2.5.1...v2.5.2) (2023-07-06) 457 | 458 | 459 | ### Bug Fixes 460 | 461 | * remove postinstall ([8167272](https://github.com/vuejs/repl/commit/816727232d0adac0c0955c1d6bee9d7be7f70d61)) 462 | 463 | 464 | 465 | ## [2.5.1](https://github.com/vuejs/repl/compare/v2.5.0...v2.5.1) (2023-07-05) 466 | 467 | 468 | ### Bug Fixes 469 | 470 | * cannot get ts module in prod env ([0cc220d](https://github.com/vuejs/repl/commit/0cc220d7efaafaaa3b8af07c34fd27c7825caa8b)) 471 | * cdn file models were accidentally disposed ([4301d86](https://github.com/vuejs/repl/commit/4301d8659ad84d9c6b66d63f1567cf31119eb9f4)) 472 | 473 | 474 | ### Features 475 | 476 | * use ts version option for ts lib dts acquire ([376fe3b](https://github.com/vuejs/repl/commit/376fe3ba2582fc128ccc4bbb2cd4b19666a5f1ec)) 477 | 478 | 479 | 480 | # [2.5.0](https://github.com/vuejs/repl/compare/v2.4.0...v2.5.0) (2023-07-05) 481 | 482 | 483 | ### Bug Fixes 484 | 485 | * change message toggle position ([#120](https://github.com/vuejs/repl/issues/120)) ([3f7e090](https://github.com/vuejs/repl/commit/3f7e090c143ca0a40b0bff1d13f2db3e6964b17a)) 486 | 487 | 488 | ### Features 489 | 490 | * download TS dynamically ([#125](https://github.com/vuejs/repl/issues/125)) ([97f698f](https://github.com/vuejs/repl/commit/97f698f1f88690ab371e156ac2113955fdaa5fa8)) 491 | * expose TS localized languages ([a52dd14](https://github.com/vuejs/repl/commit/a52dd1468a6d6fb8ce4927a0a6771529f6a0f1ab)) 492 | * respect browser language ([f9fedcd](https://github.com/vuejs/repl/commit/f9fedcd1ca56a6965c9617a16d2fba834f8a44a1)), closes [#123](https://github.com/vuejs/repl/issues/123) 493 | * upgrade volar ([d925ba3](https://github.com/vuejs/repl/commit/d925ba3c8a08966eab06eaf2720a7476415e7760)) 494 | 495 | 496 | 497 | # [2.4.0](https://github.com/vuejs/repl/compare/v2.3.0...v2.4.0) (2023-06-28) 498 | 499 | 500 | ### Bug Fixes 501 | 502 | * multiple style tags ([#116](https://github.com/vuejs/repl/issues/116)) ([f0f5512](https://github.com/vuejs/repl/commit/f0f5512f49832321a6c96631025927635a834d9a)) 503 | * strip src prefix on dialog ([d29d1de](https://github.com/vuejs/repl/commit/d29d1de3f31930005dfc0b29f7d8a0435c0f94a4)) 504 | 505 | 506 | ### Features 507 | 508 | * add `reload` function ([#103](https://github.com/vuejs/repl/issues/103)) ([12ebcea](https://github.com/vuejs/repl/commit/12ebceab49c2a56702fffdfc01bb8b0bc3a708ca)) 509 | * add monaco light theme ([#121](https://github.com/vuejs/repl/issues/121)) ([ead9667](https://github.com/vuejs/repl/commit/ead9667a85c1f217dab5955ebd9a11992b3fbe65)) 510 | * memorize show error state ([#117](https://github.com/vuejs/repl/issues/117)) ([ab4b7cd](https://github.com/vuejs/repl/commit/ab4b7cd4d2c99b2750e29feaa0b3487f4bb8ed85)) 511 | 512 | 513 | 514 | # [2.3.0](https://github.com/vuejs/repl/compare/v2.2.0...v2.3.0) (2023-06-24) 515 | 516 | 517 | ### Features 518 | 519 | * add tsconfig file ([#114](https://github.com/vuejs/repl/issues/114)) ([29f6af5](https://github.com/vuejs/repl/commit/29f6af5037826a6d37f77ba4cae748e7297152e3)) 520 | 521 | 522 | 523 | # [2.2.0](https://github.com/vuejs/repl/compare/v2.1.4...v2.2.0) (2023-06-24) 524 | 525 | 526 | ### Bug Fixes 527 | 528 | * don't dispose in-memory files ([5f543da](https://github.com/vuejs/repl/commit/5f543da6815e30c76dc3a595b993a380043af54b)) 529 | * set page height ([ee814e7](https://github.com/vuejs/repl/commit/ee814e7313162f19d45dcff7a3ecabedfdf081d5)), closes [#112](https://github.com/vuejs/repl/issues/112) 530 | 531 | 532 | ### Features 533 | 534 | * add default height for Repl component ([#109](https://github.com/vuejs/repl/issues/109)) ([d9673eb](https://github.com/vuejs/repl/commit/d9673eb4c7a3e20ca7d0f1e152d177c6c1f8956d)) 535 | * add error toggle ([#98](https://github.com/vuejs/repl/issues/98)) ([51819cc](https://github.com/vuejs/repl/commit/51819ccd3adcd40c189bd216f635ca6f62c4bc56)) 536 | 537 | 538 | 539 | ## [2.1.4](https://github.com/vuejs/repl/compare/v2.1.3...v2.1.4) (2023-06-23) 540 | 541 | 542 | ### Bug Fixes 543 | 544 | * default main file path ([c0184da](https://github.com/vuejs/repl/commit/c0184da073456706c44cc5e78e2d3283f4d3fe0f)) 545 | * **monaco:** enable `fixedOverflowWidgets` option ([#110](https://github.com/vuejs/repl/issues/110)) ([c7ddf12](https://github.com/vuejs/repl/commit/c7ddf12f25b23675c12c2760297c7d7d37668943)) 546 | 547 | 548 | 549 | ## [2.1.3](https://github.com/vuejs/repl/compare/v2.1.2...v2.1.3) (2023-06-22) 550 | 551 | 552 | ### Bug Fixes 553 | 554 | * actually fix editor type lol ([95ab2ab](https://github.com/vuejs/repl/commit/95ab2abc29b01a565ba7bc25ef293f1434db5ef6)) 555 | 556 | 557 | 558 | ## [2.1.2](https://github.com/vuejs/repl/compare/v2.1.1...v2.1.2) (2023-06-22) 559 | 560 | 561 | ### Bug Fixes 562 | 563 | * ensure imported editor can be passed as prop without type error ([414b0e6](https://github.com/vuejs/repl/commit/414b0e6cb729234ccb188332b22c184e44f162e0)) 564 | 565 | 566 | 567 | ## [2.1.1](https://github.com/vuejs/repl/compare/v2.1.0...v2.1.1) (2023-06-22) 568 | 569 | 570 | ### Bug Fixes 571 | 572 | * **types:** fix editor generated dts ([e5705df](https://github.com/vuejs/repl/commit/e5705df7d1ea4a44d9f6eba4443e28712631053a)) 573 | 574 | 575 | 576 | # [2.1.0](https://github.com/vuejs/repl/compare/v2.0.0...v2.1.0) (2023-06-22) 577 | 578 | 579 | ### Features 580 | 581 | * support custom file go to difinition ([#102](https://github.com/vuejs/repl/issues/102)) ([519b0cc](https://github.com/vuejs/repl/commit/519b0cc079dccdb08ed00f1b5d2fb0c965fbab03)) 582 | 583 | 584 | 585 | # [2.0.0](https://github.com/vuejs/repl/compare/v1.5.0...v2.0.0) (2023-06-22) 586 | 587 | 588 | ### Bug Fixes 589 | 590 | * disable pug and script setup codeLens ([1c6e646](https://github.com/vuejs/repl/commit/1c6e6464bea009b279fe43ed401e722230bf95bd)) 591 | * don't delete dts models ([d04a1ed](https://github.com/vuejs/repl/commit/d04a1ed01a9f5aaeafc6845d6165dcc9b45b7a04)) 592 | * f@ck ([0aa0a24](https://github.com/vuejs/repl/commit/0aa0a24e9c196d846827623b48b5eace0b8498a0)) 593 | * larger font size ([#100](https://github.com/vuejs/repl/issues/100)) ([a1a3fe1](https://github.com/vuejs/repl/commit/a1a3fe1294cf0daa89e30902d607addbdd525b6e)) 594 | * make monaco editor works normally ([f538199](https://github.com/vuejs/repl/commit/f538199e5cd99c380b3b5468cc660d47a6910783)) 595 | * minor fix ([aacba06](https://github.com/vuejs/repl/commit/aacba0673414b759377e3cbfa764bff82d64f857)) 596 | * split monaco out ([fd0b06a](https://github.com/vuejs/repl/commit/fd0b06af78e25632ee4fd6525ae5c90ef2c51f0b)) 597 | * styles ([6aee9cf](https://github.com/vuejs/repl/commit/6aee9cfa7df19de0f60f0d9fe2ccbd2291998cbb)) 598 | * update exports and types ([e4988b0](https://github.com/vuejs/repl/commit/e4988b0a9ddbc9888e481fd8f1c000b1756a5ba5)) 599 | * use monaco-volar ([9846c8e](https://github.com/vuejs/repl/commit/9846c8e67720c2d7402d52bc1c2106a9a1b28c08)) 600 | * use worker ([e1e80b9](https://github.com/vuejs/repl/commit/e1e80b9e40805bd541ccd80d48b01228378bf2c7)) 601 | 602 | 603 | ### Features 604 | 605 | * add ls ([a7bffd6](https://github.com/vuejs/repl/commit/a7bffd64c43e8ff375f1e59b62c78ca13969c723)) 606 | * complete provideDefinition ([2035fc4](https://github.com/vuejs/repl/commit/2035fc49977e5c2f5e8c57fe80c0bb53bb85df70)) 607 | * completed provideCompletionItems ([81ef510](https://github.com/vuejs/repl/commit/81ef51028f9188fcf598e78e45836f09b9f191ae)) 608 | * completed provideHover ([3922239](https://github.com/vuejs/repl/commit/39222398316adfda107f2b00d2cc27523f6cef52)) 609 | * completed provideSignatureHelp ([c599f2b](https://github.com/vuejs/repl/commit/c599f2bdf136d0d02e0c2f50198d8f71f0e858f2)) 610 | * completed setModelMarkers ([86079ad](https://github.com/vuejs/repl/commit/86079ad2030559052cef8b32e0d4a301a62b5f65)) 611 | * implemented provideCodeActions, resolveCodeAction ([1d37f55](https://github.com/vuejs/repl/commit/1d37f55eacb4124171813d1db777496aa1bddeb3)) 612 | * implemented provideCodeLenses, resolveCodeLens ([bddb65b](https://github.com/vuejs/repl/commit/bddb65b19053f069de3a3366666e3db1fd6bd6b1)) 613 | * implemented provideDeclaration ([8e437f0](https://github.com/vuejs/repl/commit/8e437f0e8796ba3b8d7a19478fc1b9bcde6f2e43)) 614 | * implemented provideDocumentColors, provideColorPresentations ([ddde114](https://github.com/vuejs/repl/commit/ddde1140945ac819765d93c7e33b07046fca55a6)) 615 | * implemented provideDocumentFormattingEdits ([73f046d](https://github.com/vuejs/repl/commit/73f046d80cd17c9dea7051f6d488e3b757366ad9)) 616 | * implemented provideDocumentHighlights ([f782496](https://github.com/vuejs/repl/commit/f7824960cb34f4ee2b58ad37a2af1a9364367b7a)) 617 | * implemented provideDocumentRangeFormattingEdits ([0e64a81](https://github.com/vuejs/repl/commit/0e64a81a57e2e973d95bcf3bba03c43f48abc508)) 618 | * implemented provideDocumentSymbols ([1cda6c6](https://github.com/vuejs/repl/commit/1cda6c638223c3d26f1b07deb24c6ba7b699014b)) 619 | * implemented provideFoldingRanges ([dd083d3](https://github.com/vuejs/repl/commit/dd083d3b246b5db8e7a158585e0fc912c9dd03ce)) 620 | * implemented provideImplementation ([49d8d7e](https://github.com/vuejs/repl/commit/49d8d7e52b34e94956a1e93bd7c98452d96831dd)) 621 | * implemented provideInlayHints ([61d18c9](https://github.com/vuejs/repl/commit/61d18c97d9ea309a48ff50188661bcbfe97312a4)) 622 | * implemented provideLinkedEditingRanges ([1046ac8](https://github.com/vuejs/repl/commit/1046ac8a29045b7a326002cb3fec9387675d9b33)) 623 | * implemented provideLinks ([3bc4063](https://github.com/vuejs/repl/commit/3bc406352ac7ce697e7d6c4b83f7ff737adede08)) 624 | * implemented provideOnTypeFormattingEdits ([f50e1c9](https://github.com/vuejs/repl/commit/f50e1c9c1ef7ed0ea02d51cdaaa5802f69911c7f)) 625 | * implemented provideReferences ([6b05d17](https://github.com/vuejs/repl/commit/6b05d1726d3e06c36008b847a55978e1e7b99843)) 626 | * implemented provideRenameEdits ([5ca6318](https://github.com/vuejs/repl/commit/5ca63181a2beebcc180282cd70f6732e572fd51d)) 627 | * implemented provideSelectionRanges ([2037d73](https://github.com/vuejs/repl/commit/2037d73ded9944b10a4379d369aeec06a304df97)) 628 | * implemented provideTypeDefinition ([b80bb0d](https://github.com/vuejs/repl/commit/b80bb0da90481cbebb214abf4a333daa7c4a42e9)) 629 | * implemented resolveCompletionItem ([3ef6ffd](https://github.com/vuejs/repl/commit/3ef6ffdf58a66f74a8903b7479b43195b13aeb69)) 630 | * **monaco:** support to keep selection and cursor position ([#99](https://github.com/vuejs/repl/issues/99)) ([db8c1bd](https://github.com/vuejs/repl/commit/db8c1bdd13ac44c15336795387aa8e7a449dfd74)) 631 | * pass vue dts module version from store ([2a0dfc0](https://github.com/vuejs/repl/commit/2a0dfc011a547d61523e5f64d882e5ed940bbb30)) 632 | * support auto close tag / auto .value ([f765d54](https://github.com/vuejs/repl/commit/f765d54a69ef6aca5586873d19cf3f666adeb0c5)) 633 | * support omitting ts/js extensions ([130a137](https://github.com/vuejs/repl/commit/130a137dd9640ea912e68d27e390dd03664e2699)) 634 | * upgrade volar ([0783d93](https://github.com/vuejs/repl/commit/0783d93fdd310e92ae3b2e22b0dc6ea78f93beaf)) 635 | * upgrade volar ([0aac8d1](https://github.com/vuejs/repl/commit/0aac8d11632e0963e0ae3de4e02cf47a16deec60)) 636 | * use monaco ([e833cf1](https://github.com/vuejs/repl/commit/e833cf14d447063654a02a83ba12fd23c8619c77)) 637 | 638 | 639 | ### Performance Improvements 640 | 641 | * cache `ts.ScriptSnapshot.fromString` ([6f34b78](https://github.com/vuejs/repl/commit/6f34b78d7d637d8fc238ca68c8beb87884f110b0)) 642 | * preset failed node_modules paths to speed up dts acquisition ([e93f049](https://github.com/vuejs/repl/commit/e93f0499719595aad61473b0b7819ece1b46818e)) 643 | 644 | 645 | 646 | # [1.5.0](https://github.com/vuejs/repl/compare/v1.4.1...v1.5.0) (2023-06-14) 647 | 648 | 649 | ### Bug Fixes 650 | 651 | * add ts and json mode ([#37](https://github.com/vuejs/repl/issues/37)) ([0e467af](https://github.com/vuejs/repl/commit/0e467afbb52c759fdad0a2bfc263812b0df285c5)) 652 | * console logging for component instance proxies ([#62](https://github.com/vuejs/repl/issues/62)) ([bb0e143](https://github.com/vuejs/repl/commit/bb0e1430bff586b5505c3e9d11e8331359ee23d2)) 653 | * css update in ssr mode ([3b7e511](https://github.com/vuejs/repl/commit/3b7e51126dd32e4ebf36b9bd492f1c117ac9de69)), closes [#91](https://github.com/vuejs/repl/issues/91) [#92](https://github.com/vuejs/repl/issues/92) 654 | * Fix reason.message not existing case ([#54](https://github.com/vuejs/repl/issues/54)) ([2508030](https://github.com/vuejs/repl/commit/2508030241504d750a3226eb9a70fddd45d3299d)) 655 | * improve code gen when using cssVars in SSR ([#85](https://github.com/vuejs/repl/issues/85)) ([7e2bcc8](https://github.com/vuejs/repl/commit/7e2bcc864360e302d8b2a48e6904b7ec6c099f3f)) 656 | * improve code with optional chain ([#72](https://github.com/vuejs/repl/issues/72)) ([b8caeae](https://github.com/vuejs/repl/commit/b8caeaef0368609fa3c41e992304d21d526de08c)) 657 | * prevent opening new tab for a tags with javascript href ([#94](https://github.com/vuejs/repl/issues/94)) ([64906a5](https://github.com/vuejs/repl/commit/64906a529cc48869791e663ba6d203baed236f6f)) 658 | * process all files when dynamic import ([#60](https://github.com/vuejs/repl/issues/60)) ([7049ae0](https://github.com/vuejs/repl/commit/7049ae006f8687d2dafce38b7f54d7281410062a)) 659 | 660 | 661 | ### Features 662 | 663 | * add `sublime` keymap ([#45](https://github.com/vuejs/repl/issues/45)) ([29263d8](https://github.com/vuejs/repl/commit/29263d83d2d28e2ea3fc85c59de6d6d7ef92cca6)) 664 | * add file renaming ([#63](https://github.com/vuejs/repl/issues/63)) ([eb41c3a](https://github.com/vuejs/repl/commit/eb41c3a180eb720ba0959ba2da8064442f1b25e6)) 665 | * add search and replace ([#67](https://github.com/vuejs/repl/issues/67)) ([4ca3d94](https://github.com/vuejs/repl/commit/4ca3d94c98ed2029ccd61197780d45f348b2fcde)) 666 | * local JSON files ([#82](https://github.com/vuejs/repl/issues/82)) ([db076eb](https://github.com/vuejs/repl/commit/db076eb2b07e104ef460d7e2bd99769b5653e1a5)) 667 | * support for sandbox page customization ([#42](https://github.com/vuejs/repl/issues/42)) ([a22b969](https://github.com/vuejs/repl/commit/a22b96968894dcaf4fa096edf8a1dd7d7f903e5e)) 668 | 669 | 670 | 671 | ## [1.4.1](https://github.com/vuejs/repl/compare/v1.4.0...v1.4.1) (2023-04-21) 672 | 673 | 674 | 675 | # [1.4.0](https://github.com/vuejs/repl/compare/v1.3.6...v1.4.0) (2023-04-13) 676 | 677 | 678 | ### Features 679 | 680 | * provide fs option to support 3.3 external type resolving ([f0e826a](https://github.com/vuejs/repl/commit/f0e826a1ff9eae7c008f2b92b4af35a518dd0c7f)) 681 | 682 | 683 | 684 | ## [1.3.6](https://github.com/vuejs/repl/compare/v1.3.5...v1.3.6) (2023-04-13) 685 | 686 | 687 | ### Bug Fixes 688 | 689 | * **types:** make sfc options partial ([9916f28](https://github.com/vuejs/repl/commit/9916f2862b327891604f3282fedf626759694e2c)) 690 | 691 | 692 | 693 | ## [1.3.5](https://github.com/vuejs/repl/compare/v1.3.4...v1.3.5) (2023-04-06) 694 | 695 | 696 | ### Bug Fixes 697 | 698 | * avoid including vue in import map if using default URLs ([37ce32b](https://github.com/vuejs/repl/commit/37ce32b107864332eeebbc406a817d78ae8d982a)) 699 | 700 | 701 | 702 | ## [1.3.4](https://github.com/vuejs/repl/compare/v1.3.3...v1.3.4) (2023-04-06) 703 | 704 | 705 | ### Bug Fixes 706 | 707 | * fix legacy domain in import maps ([7e7c7f9](https://github.com/vuejs/repl/commit/7e7c7f9dd62995f2f27448e72effb4c8fe879d72)) 708 | 709 | 710 | 711 | ## [1.3.3](https://github.com/vuejs/repl/compare/v1.3.2...v1.3.3) (2023-03-17) 712 | 713 | 714 | ### Bug Fixes 715 | 716 | * ignore polyfill error in Safari ([39f4ab1](https://github.com/vuejs/repl/commit/39f4ab1956af85383e6616eafec3efc616313d28)) 717 | 718 | 719 | 720 | ## [1.3.2](https://github.com/vuejs/repl/compare/v1.3.1...v1.3.2) (2022-09-27) 721 | 722 | 723 | ### Bug Fixes 724 | 725 | * reset sandbox when changing files for safari compat ([68a6197](https://github.com/vuejs/repl/commit/68a6197bbfb88dc74ec317ae50e3f686cbfeb081)), closes [vuejs/docs#1973](https://github.com/vuejs/docs/issues/1973) 726 | 727 | 728 | 729 | ## [1.3.1](https://github.com/vuejs/repl/compare/v1.3.0...v1.3.1) (2022-09-27) 730 | 731 | 732 | 733 | # [1.3.0](https://github.com/vuejs/repl/compare/v1.2.4...v1.3.0) (2022-06-26) 734 | 735 | 736 | 737 | ## [1.2.4](https://github.com/vuejs/repl/compare/v1.2.3...v1.2.4) (2022-06-26) 738 | 739 | 740 | ### Bug Fixes 741 | 742 | * compile error when no script ([#38](https://github.com/vuejs/repl/issues/38)) ([6b9b7bc](https://github.com/vuejs/repl/commit/6b9b7bc9ea3f89772eaf1807e3b7478d39f3ef9c)) 743 | 744 | 745 | ### Features 746 | 747 | * export Preview component ([#39](https://github.com/vuejs/repl/issues/39)) ([0b93cd6](https://github.com/vuejs/repl/commit/0b93cd66f5dc0beb2e44f271efa3868a155bff21)) 748 | * gzip serialized state ([#43](https://github.com/vuejs/repl/issues/43)) ([b12eb88](https://github.com/vuejs/repl/commit/b12eb885deb080246d372495f443fe543de1eb6d)) 749 | 750 | 751 | 752 | ## [1.2.3](https://github.com/vuejs/repl/compare/v1.2.2...v1.2.3) (2022-05-25) 753 | 754 | 755 | ### Bug Fixes 756 | 757 | * also reset import map when resetting to defauilt vue version ([5e89f07](https://github.com/vuejs/repl/commit/5e89f074ea5d33b301e079c5f4fe7860e1e5ca82)) 758 | * improve new filename logic ([9647666](https://github.com/vuejs/repl/commit/9647666554407b32f16b8b5581333542769a5ea0)) 759 | * warn versions that do not support in browser SSR ([01cb5b2](https://github.com/vuejs/repl/commit/01cb5b20cd15c3dcbe9f1b6d3dbc8797702924e9)) 760 | 761 | 762 | 763 | ## [1.2.2](https://github.com/vuejs/repl/compare/v1.2.1...v1.2.2) (2022-05-25) 764 | 765 | 766 | ### Bug Fixes 767 | 768 | * do not start compiling until sfc options are set ([b6f86d9](https://github.com/vuejs/repl/commit/b6f86d920d22d83fde3bb77b11e8f44fff1a244d)) 769 | 770 | 771 | 772 | ## [1.2.1](https://github.com/vuejs/repl/compare/v1.2.0...v1.2.1) (2022-05-25) 773 | 774 | 775 | ### Bug Fixes 776 | 777 | * fix html initialization in ssr mode ([152f2fa](https://github.com/vuejs/repl/commit/152f2fad88fa87fb617a8a69ff8f9f2c1b1eba33)) 778 | 779 | 780 | 781 | # [1.2.0](https://github.com/vuejs/repl/compare/v1.1.2...v1.2.0) (2022-05-25) 782 | 783 | 784 | ### Bug Fixes 785 | 786 | * avoid using native crypto ([c22e216](https://github.com/vuejs/repl/commit/c22e216b1c6d8bbce3cbb4376d82ce15ce149433)), closes [#25](https://github.com/vuejs/repl/issues/25) 787 | 788 | 789 | ### Features 790 | 791 | * **FileSelector:** add an increment counter for new files ([#36](https://github.com/vuejs/repl/issues/36)) ([63b8f22](https://github.com/vuejs/repl/commit/63b8f22a991984ce1ce6c56d14ae4f35f8b4a436)) 792 | * support ssr + hydration ([098aa89](https://github.com/vuejs/repl/commit/098aa8992ad860c8529fb285552c6c26e7518e9e)) 793 | 794 | 795 | 796 | ## [1.1.2](https://github.com/vuejs/repl/compare/v1.1.1...v1.1.2) (2022-05-20) 797 | 798 | 799 | ### Bug Fixes 800 | 801 | * apply TS transform to template when inine is disabled ([ec2ae81](https://github.com/vuejs/repl/commit/ec2ae811bd25da4be74b9df3bb8fcf9ba5d34cfb)) 802 | 803 | 804 | 805 | ## [1.1.1](https://github.com/vuejs/repl/compare/v1.1.0...v1.1.1) (2022-05-17) 806 | 807 | 808 | ### Bug Fixes 809 | 810 | * adding file using enter emits error ([#23](https://github.com/vuejs/repl/issues/23)) ([918de7f](https://github.com/vuejs/repl/commit/918de7f3646a24db083e54301d6ac5c3a970c0df)) 811 | 812 | 813 | 814 | # [1.1.0](https://github.com/vuejs/repl/compare/v1.0.1...v1.1.0) (2022-05-17) 815 | 816 | 817 | 818 | ## [1.0.1](https://github.com/vuejs/repl/compare/f8bb46f969860539e3105ff56d092f0184a70eba...v1.0.1) (2022-05-17) 819 | 820 | 821 | ### Bug Fixes 822 | 823 | * also generate render function if inline mode is disabled ([9a325bb](https://github.com/vuejs/repl/commit/9a325bbf66b61403cd4df5ace31d0e7e1532fddf)) 824 | * avoid reloading the iframe when switching output tabs ([20bde55](https://github.com/vuejs/repl/commit/20bde550e481c0a9c9218f8a583eae7b27ca42d2)) 825 | * css double # ([#14](https://github.com/vuejs/repl/issues/14)) ([8bcf7f0](https://github.com/vuejs/repl/commit/8bcf7f0f22553214f7936863de3d9780272781b0)) 826 | * fix module instantiation order ([879f084](https://github.com/vuejs/repl/commit/879f08495c061afa11e058a3e059365fe09277c6)) 827 | * fix rewriteDefault TS edge case ([d277d7f](https://github.com/vuejs/repl/commit/d277d7f50113c45b8ae71afcda9aa369c64fba32)) 828 | * fix setFiles with multi files cross imports ([424e00d](https://github.com/vuejs/repl/commit/424e00d2ac50636b3a2a9739620435b156f1a94a)) 829 | * force app name ([18863af](https://github.com/vuejs/repl/commit/18863af803922f3966a80922db7c8a45a0cdd78d)) 830 | * small screen error msg covered code button ([#18](https://github.com/vuejs/repl/issues/18)) ([02da79d](https://github.com/vuejs/repl/commit/02da79d0a238b8777fcd95675c8c5dbd1b626fd4)) 831 | * toggler should be absolute ([f8bb46f](https://github.com/vuejs/repl/commit/f8bb46f969860539e3105ff56d092f0184a70eba)) 832 | * update import map when setting vue versions ([15cc696](https://github.com/vuejs/repl/commit/15cc696054b49fe5ea6879b9492b96cca611c945)) 833 | 834 | 835 | ### Features 836 | 837 | * add hidden file ([#17](https://github.com/vuejs/repl/issues/17)) ([35b6f1a](https://github.com/vuejs/repl/commit/35b6f1a38611e31b9adbe7540d789be144e33bdc)) 838 | * allow starting on a specific view ([#15](https://github.com/vuejs/repl/issues/15)) ([7e63511](https://github.com/vuejs/repl/commit/7e635110bb5e11e8103b66c5d347cf959be8bd55)) 839 | * export compileFile ([#13](https://github.com/vuejs/repl/issues/13)) ([60db549](https://github.com/vuejs/repl/commit/60db54905699e005d3117a693410c0cd50f154fe)) 840 | * file-selector add horizontal scroll ([#10](https://github.com/vuejs/repl/issues/10)) ([d0c961e](https://github.com/vuejs/repl/commit/d0c961e7b20939f0e028fd0cb89ce75123f32aa7)) 841 | * support passing in compiler-sfc options ([f6c7049](https://github.com/vuejs/repl/commit/f6c7049f9bc4a5e1dd3e1c1948ba2ecb43fad3c3)) 842 | * support ts in template expressions ([a1e9881](https://github.com/vuejs/repl/commit/a1e98814699c020a2d82c8c5aad664e99bd6ef52)) 843 | * vertical mode ([d59bb6c](https://github.com/vuejs/repl/commit/d59bb6cd0eb0e03fa548595f5c64b990cecd133e)) 844 | 845 | 846 | 847 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-present, Yuxi (Evan) You 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @vue/repl 2 | 3 | Vue SFC REPL as a Vue 3 component. 4 | 5 | ## Basic Usage 6 | 7 | **Note: `@vue/repl` >= 2 now supports Monaco Editor, but also requires explicitly passing in the editor to be used for tree-shaking.** 8 | 9 | ```ts 10 | // vite.config.ts 11 | import { defineConfig } from 'vite' 12 | export default defineConfig({ 13 | optimizeDeps: { 14 | exclude: ['@vue/repl'], 15 | }, 16 | // ... 17 | }) 18 | ``` 19 | 20 | ### With CodeMirror Editor 21 | 22 | Basic editing experience with no intellisense. Lighter weight, fewer network requests, better for embedding use cases. 23 | 24 | ```vue 25 | 31 | 32 | 35 | ``` 36 | 37 | ### With Monaco Editor 38 | 39 | With Volar support, autocomplete, type inference, and semantic highlighting. Heavier bundle, loads dts files from CDN, better for standalone use cases. 40 | 41 | ```vue 42 | 48 | 49 | 52 | ``` 53 | 54 | ## Advanced Usage 55 | 56 | Customize the behavior of the REPL by manually initializing the store. 57 | 58 | See [v4 Migration Guide](https://github.com/vuejs/repl/releases/tag/v4.0.0) 59 | 60 | ```vue 61 | 105 | 106 | 109 | ``` 110 | 111 | Use only the Preview without the editor 112 | 113 | ```vue 114 | 130 | 131 | 134 | ``` 135 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js' 2 | import tseslint from 'typescript-eslint' 3 | import pluginVue from 'eslint-plugin-vue' 4 | 5 | export default tseslint.config( 6 | { ignores: ['**/node_modules', '**/dist'] }, 7 | eslint.configs.recommended, 8 | tseslint.configs.base, 9 | ...pluginVue.configs['flat/recommended'], 10 | { 11 | files: ['**/*.vue'], 12 | languageOptions: { 13 | parserOptions: { 14 | parser: '@typescript-eslint/parser', 15 | }, 16 | }, 17 | }, 18 | { 19 | rules: { 20 | 'no-debugger': 'error', 21 | 'no-console': ['error', { allow: ['warn', 'error', 'info', 'clear'] }], 22 | 'no-unused-vars': 'off', 23 | 'no-undef': 'off', 24 | 'prefer-const': 'error', 25 | 'sort-imports': ['error', { ignoreDeclarationSort: true }], 26 | 'no-duplicate-imports': 'error', 27 | // This rule enforces the preference for using '@ts-expect-error' comments in TypeScript 28 | // code to indicate intentional type errors, improving code clarity and maintainability. 29 | '@typescript-eslint/prefer-ts-expect-error': 'error', 30 | // Enforce the use of 'import type' for importing types 31 | '@typescript-eslint/consistent-type-imports': [ 32 | 'error', 33 | { 34 | fixStyle: 'inline-type-imports', 35 | disallowTypeAnnotations: false, 36 | }, 37 | ], 38 | // Enforce the use of top-level import type qualifier when an import only has specifiers with inline type qualifiers 39 | '@typescript-eslint/no-import-type-side-effects': 'error', 40 | 'vue/max-attributes-per-line': 'off', 41 | 'vue/singleline-html-element-content-newline': 'off', 42 | 'vue/multi-word-component-names': 'off', 43 | 'vue/html-self-closing': [ 44 | 'error', 45 | { 46 | html: { component: 'always', normal: 'always', void: 'any' }, 47 | math: 'always', 48 | svg: 'always', 49 | }, 50 | ], 51 | }, 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /index-dist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue SFC Playground 8 | 17 | 18 | 26 | 27 | 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue SFC Playground 8 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/repl", 3 | "version": "4.6.1", 4 | "description": "Vue component for editing Vue components", 5 | "packageManager": "pnpm@9.15.5", 6 | "type": "module", 7 | "main": "dist/ssr-stub.js", 8 | "module": "dist/vue-repl.js", 9 | "files": [ 10 | "dist" 11 | ], 12 | "types": "dist/vue-repl.d.ts", 13 | "exports": { 14 | ".": { 15 | "types": "./dist/vue-repl.d.ts", 16 | "import": "./dist/vue-repl.js", 17 | "require": "./dist/ssr-stub.js" 18 | }, 19 | "./monaco-editor": { 20 | "types": "./dist/monaco-editor.d.ts", 21 | "import": "./dist/monaco-editor.js", 22 | "require": null 23 | }, 24 | "./codemirror-editor": { 25 | "types": "./dist/codemirror-editor.d.ts", 26 | "import": "./dist/codemirror-editor.js", 27 | "require": null 28 | }, 29 | "./core": { 30 | "types": "./dist/core.d.ts", 31 | "import": "./dist/core.js", 32 | "require": null 33 | }, 34 | "./package.json": "./package.json", 35 | "./style.css": "./dist/vue-repl.css", 36 | "./dist/style.css": "./dist/vue-repl.css" 37 | }, 38 | "typesVersions": { 39 | "*": { 40 | "*": [ 41 | "./dist/*", 42 | "./*" 43 | ] 44 | } 45 | }, 46 | "publishConfig": { 47 | "tag": "latest" 48 | }, 49 | "scripts": { 50 | "dev": "vite", 51 | "build": "vite build", 52 | "build-preview": "vite build -c vite.preview.config.ts", 53 | "format": "prettier --write .", 54 | "lint": "eslint .", 55 | "typecheck": "vue-tsc --noEmit", 56 | "release": "bumpp --all", 57 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 58 | "prepublishOnly": "npm run build" 59 | }, 60 | "simple-git-hooks": { 61 | "pre-commit": "pnpm exec lint-staged --concurrent false" 62 | }, 63 | "lint-staged": { 64 | "*": [ 65 | "prettier --write --cache --ignore-unknown" 66 | ] 67 | }, 68 | "repository": { 69 | "type": "git", 70 | "url": "git+https://github.com/vuejs/repl.git" 71 | }, 72 | "author": "Evan You", 73 | "license": "MIT", 74 | "bugs": { 75 | "url": "https://github.com/vuejs/repl/issues" 76 | }, 77 | "homepage": "https://github.com/vuejs/repl#readme", 78 | "devDependencies": { 79 | "@babel/standalone": "^7.26.9", 80 | "@babel/types": "^7.26.9", 81 | "@eslint/js": "^9.20.0", 82 | "@jridgewell/gen-mapping": "^0.3.8", 83 | "@jridgewell/trace-mapping": "^0.3.25", 84 | "@rollup/plugin-replace": "^6.0.2", 85 | "@shikijs/monaco": "^1.29.2", 86 | "@types/babel__standalone": "^7.1.9", 87 | "@types/codemirror": "^5.60.15", 88 | "@types/hash-sum": "^1.0.2", 89 | "@types/node": "^22.13.4", 90 | "@vitejs/plugin-vue": "^5.2.1", 91 | "@volar/jsdelivr": "~2.4.11", 92 | "@volar/monaco": "~2.4.11", 93 | "@vue/babel-plugin-jsx": "^1.2.5", 94 | "@vue/language-service": "~2.2.2", 95 | "assert": "^2.1.0", 96 | "bumpp": "^9.11.1", 97 | "codemirror": "^5.65.18", 98 | "conventional-changelog-cli": "^5.0.0", 99 | "eslint": "^9.20.1", 100 | "eslint-plugin-vue": "^9.32.0", 101 | "fflate": "^0.8.2", 102 | "hash-sum": "^2.0.0", 103 | "lint-staged": "^15.4.3", 104 | "monaco-editor-core": "^0.52.2", 105 | "prettier": "^3.5.1", 106 | "shiki": "^1.29.2", 107 | "simple-git-hooks": "^2.11.1", 108 | "source-map-js": "^1.2.1", 109 | "sucrase": "^3.35.0", 110 | "typescript": "^5.7.3", 111 | "typescript-eslint": "^8.24.1", 112 | "vite": "^6.1.0", 113 | "vite-plugin-dts": "^4.5.0", 114 | "vscode-uri": "^3.1.0", 115 | "vue": "^3.5.15", 116 | "vue-tsc": "~2.2.2" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Message.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 45 | 46 | 129 | -------------------------------------------------------------------------------- /src/Repl.vue: -------------------------------------------------------------------------------- 1 | 100 | 101 | 120 | 121 | 158 | -------------------------------------------------------------------------------- /src/SplitPane.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 102 | 103 | 221 | -------------------------------------------------------------------------------- /src/codemirror/CodeMirror.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 103 | 104 | 118 | -------------------------------------------------------------------------------- /src/codemirror/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | color: var(--symbols); 5 | --symbols: #777; 6 | --base: #545281; 7 | --comment: hsl(210, 25%, 60%); 8 | --keyword: #af4ab1; 9 | --variable: var(--base); 10 | --function: #c25205; 11 | --string: #2ba46d; 12 | --number: #c25205; 13 | --tags: #dd0000; 14 | --brackets: var(--comment); 15 | --qualifier: #ff6032; 16 | --important: var(--string); 17 | --attribute: #9c3eda; 18 | --property: #6182b8; 19 | 20 | --selected-bg: #d7d4f0; 21 | --selected-bg-non-focus: #d9d9d9; 22 | --cursor: #000; 23 | 24 | direction: ltr; 25 | font-family: var(--font-code); 26 | height: auto; 27 | } 28 | 29 | .dark .CodeMirror { 30 | color: var(--symbols); 31 | --symbols: #89ddff; 32 | --base: #a6accd; 33 | --comment: #6d6d6d; 34 | --keyword: #89ddff; 35 | --string: #c3e88d; 36 | --variable: #82aaff; 37 | --number: #f78c6c; 38 | --tags: #f07178; 39 | --brackets: var(--symbols); 40 | --property: #f07178; 41 | --attribute: #c792ea; 42 | --cursor: #fff; 43 | 44 | --selected-bg: rgba(255, 255, 255, 0.1); 45 | --selected-bg-non-focus: rgba(255, 255, 255, 0.15); 46 | } 47 | 48 | /* PADDING */ 49 | 50 | .CodeMirror-lines { 51 | padding: 4px 0; /* Vertical padding around content */ 52 | } 53 | .CodeMirror pre { 54 | padding: 0 4px; /* Horizontal padding of content */ 55 | } 56 | 57 | .CodeMirror-scrollbar-filler, 58 | .CodeMirror-gutter-filler { 59 | background-color: white; /* The little square between H and V scrollbars */ 60 | } 61 | 62 | /* GUTTER */ 63 | 64 | .CodeMirror-gutters { 65 | border-right: 1px solid var(--border); 66 | background-color: transparent; 67 | white-space: nowrap; 68 | } 69 | .CodeMirror-linenumber { 70 | padding: 0 3px 0 5px; 71 | min-width: 20px; 72 | text-align: right; 73 | color: var(--comment); 74 | white-space: nowrap; 75 | opacity: 0.6; 76 | } 77 | 78 | .CodeMirror-guttermarker { 79 | color: black; 80 | } 81 | .CodeMirror-guttermarker-subtle { 82 | color: #999; 83 | } 84 | 85 | /* FOLD GUTTER */ 86 | 87 | .CodeMirror-foldmarker { 88 | color: #414141; 89 | text-shadow: 90 | #ff9966 1px 1px 2px, 91 | #ff9966 -1px -1px 2px, 92 | #ff9966 1px -1px 2px, 93 | #ff9966 -1px 1px 2px; 94 | font-family: arial; 95 | line-height: 0.3; 96 | cursor: pointer; 97 | } 98 | .CodeMirror-foldgutter { 99 | width: 0.7em; 100 | } 101 | .CodeMirror-foldgutter-open, 102 | .CodeMirror-foldgutter-folded { 103 | cursor: pointer; 104 | } 105 | .CodeMirror-foldgutter-open:after, 106 | .CodeMirror-foldgutter-folded:after { 107 | content: '>'; 108 | font-size: 0.8em; 109 | opacity: 0.8; 110 | transition: transform 0.2s; 111 | display: inline-block; 112 | top: -0.1em; 113 | position: relative; 114 | transform: rotate(90deg); 115 | } 116 | .CodeMirror-foldgutter-folded:after { 117 | transform: none; 118 | } 119 | 120 | /* CURSOR */ 121 | 122 | .CodeMirror-cursor { 123 | border-left: 1px solid var(--cursor); 124 | border-right: none; 125 | width: 0; 126 | } 127 | /* Shown when moving in bi-directional text */ 128 | .CodeMirror div.CodeMirror-secondarycursor { 129 | border-left: 1px solid silver; 130 | } 131 | .cm-fat-cursor .CodeMirror-cursor { 132 | width: auto; 133 | border: 0 !important; 134 | background: #7e7; 135 | } 136 | .cm-fat-cursor div.CodeMirror-cursors { 137 | z-index: 1; 138 | } 139 | .cm-fat-cursor-mark { 140 | background-color: rgba(20, 255, 20, 0.5); 141 | -webkit-animation: blink 1.06s steps(1) infinite; 142 | -moz-animation: blink 1.06s steps(1) infinite; 143 | animation: blink 1.06s steps(1) infinite; 144 | } 145 | .cm-animate-fat-cursor { 146 | width: auto; 147 | border: 0; 148 | -webkit-animation: blink 1.06s steps(1) infinite; 149 | -moz-animation: blink 1.06s steps(1) infinite; 150 | animation: blink 1.06s steps(1) infinite; 151 | background-color: #7e7; 152 | } 153 | @-moz-keyframes blink { 154 | 0% { 155 | } 156 | 50% { 157 | background-color: transparent; 158 | } 159 | 100% { 160 | } 161 | } 162 | @-webkit-keyframes blink { 163 | 0% { 164 | } 165 | 50% { 166 | background-color: transparent; 167 | } 168 | 100% { 169 | } 170 | } 171 | @keyframes blink { 172 | 0% { 173 | } 174 | 50% { 175 | background-color: transparent; 176 | } 177 | 100% { 178 | } 179 | } 180 | 181 | .cm-tab { 182 | display: inline-block; 183 | text-decoration: inherit; 184 | } 185 | 186 | .CodeMirror-rulers { 187 | position: absolute; 188 | left: 0; 189 | right: 0; 190 | top: -50px; 191 | bottom: -20px; 192 | overflow: hidden; 193 | } 194 | .CodeMirror-ruler { 195 | border-left: 1px solid #ccc; 196 | top: 0; 197 | bottom: 0; 198 | position: absolute; 199 | } 200 | 201 | /* DEFAULT THEME */ 202 | .cm-s-default.CodeMirror { 203 | background-color: transparent; 204 | } 205 | .cm-s-default .cm-header { 206 | color: blue; 207 | } 208 | .cm-s-default .cm-quote { 209 | color: #090; 210 | } 211 | .cm-negative { 212 | color: #d44; 213 | } 214 | .cm-positive { 215 | color: #292; 216 | } 217 | .cm-header, 218 | .cm-strong { 219 | font-weight: bold; 220 | } 221 | .cm-em { 222 | font-style: italic; 223 | } 224 | .cm-link { 225 | text-decoration: underline; 226 | } 227 | .cm-strikethrough { 228 | text-decoration: line-through; 229 | } 230 | 231 | .cm-s-default .cm-atom, 232 | .cm-s-default .cm-def, 233 | .cm-s-default .cm-variable-2, 234 | .cm-s-default .cm-variable-3, 235 | .cm-s-default .cm-punctuation { 236 | color: var(--base); 237 | } 238 | .cm-s-default .cm-property { 239 | color: var(--property); 240 | } 241 | .cm-s-default .cm-hr, 242 | .cm-s-default .cm-comment { 243 | color: var(--comment); 244 | } 245 | .cm-s-default .cm-attribute { 246 | color: var(--attribute); 247 | } 248 | .cm-s-default .cm-keyword { 249 | color: var(--keyword); 250 | } 251 | .cm-s-default .cm-variable { 252 | color: var(--variable); 253 | } 254 | .cm-s-default .cm-tag { 255 | color: var(--tags); 256 | } 257 | .cm-s-default .cm-bracket { 258 | color: var(--brackets); 259 | } 260 | .cm-s-default .cm-number { 261 | color: var(--number); 262 | } 263 | .cm-s-default .cm-string, 264 | .cm-s-default .cm-string-2 { 265 | color: var(--string); 266 | } 267 | .cm-s-default .cm-type { 268 | color: rgb(255, 208, 0); 269 | } 270 | .cm-s-default .cm-meta { 271 | color: #555; 272 | } 273 | .cm-s-default .cm-qualifier { 274 | color: var(--qualifier); 275 | } 276 | .cm-s-default .cm-builtin { 277 | color: #7539ff; 278 | } 279 | .cm-s-default .cm-link { 280 | color: var(--flash); 281 | } 282 | .cm-s-default .cm-error { 283 | color: #ff008c; 284 | } 285 | .cm-invalidchar { 286 | color: #ff008c; 287 | } 288 | 289 | .CodeMirror-composing { 290 | border-bottom: 2px solid; 291 | } 292 | 293 | /* Default styles for common addons */ 294 | 295 | div.CodeMirror span.CodeMirror-matchingbracket { 296 | color: #0b0; 297 | } 298 | div.CodeMirror span.CodeMirror-nonmatchingbracket { 299 | color: #a22; 300 | } 301 | .CodeMirror-matchingtag { 302 | background: rgba(255, 150, 0, 0.3); 303 | } 304 | .CodeMirror-activeline-background { 305 | background: #e8f2ff; 306 | } 307 | 308 | /* STOP */ 309 | 310 | /* The rest of this file contains styles related to the mechanics of 311 | the editor. You probably shouldn't touch them. */ 312 | 313 | .CodeMirror { 314 | position: relative; 315 | overflow: hidden; 316 | background: white; 317 | } 318 | 319 | .CodeMirror-scroll { 320 | overflow: scroll !important; /* Things will break if this is overridden */ 321 | /* 30px is the magic margin used to hide the element's real scrollbars */ 322 | /* See overflow: hidden in .CodeMirror */ 323 | margin-bottom: -30px; 324 | margin-right: -30px; 325 | padding-bottom: 30px; 326 | height: 100%; 327 | outline: none; /* Prevent dragging from highlighting the element */ 328 | position: relative; 329 | } 330 | .CodeMirror-sizer { 331 | position: relative; 332 | border-right: 30px solid transparent; 333 | } 334 | 335 | /* The fake, visible scrollbars. Used to force redraw during scrolling 336 | before actual scrolling happens, thus preventing shaking and 337 | flickering artifacts. */ 338 | .CodeMirror-vscrollbar, 339 | .CodeMirror-hscrollbar, 340 | .CodeMirror-scrollbar-filler, 341 | .CodeMirror-gutter-filler { 342 | position: absolute; 343 | z-index: 6; 344 | display: none; 345 | } 346 | .CodeMirror-vscrollbar { 347 | right: 0; 348 | top: 0; 349 | overflow-x: hidden; 350 | overflow-y: scroll; 351 | } 352 | .CodeMirror-hscrollbar { 353 | bottom: 0; 354 | left: 0; 355 | overflow-y: hidden; 356 | overflow-x: scroll; 357 | } 358 | .CodeMirror-scrollbar-filler { 359 | right: 0; 360 | bottom: 0; 361 | } 362 | .CodeMirror-gutter-filler { 363 | left: 0; 364 | bottom: 0; 365 | } 366 | 367 | .CodeMirror-gutters { 368 | position: absolute; 369 | left: 0; 370 | top: 0; 371 | min-height: 100%; 372 | z-index: 3; 373 | } 374 | .CodeMirror-gutter { 375 | white-space: normal; 376 | height: 100%; 377 | display: inline-block; 378 | vertical-align: top; 379 | margin-bottom: -30px; 380 | } 381 | .CodeMirror-gutter-wrapper { 382 | position: absolute; 383 | z-index: 4; 384 | background: none !important; 385 | border: none !important; 386 | } 387 | .CodeMirror-gutter-background { 388 | position: absolute; 389 | top: 0; 390 | bottom: 0; 391 | z-index: 4; 392 | } 393 | .CodeMirror-gutter-elt { 394 | position: absolute; 395 | cursor: default; 396 | z-index: 4; 397 | } 398 | .CodeMirror-gutter-wrapper ::selection { 399 | background-color: transparent; 400 | } 401 | .CodeMirror-gutter-wrapper ::-moz-selection { 402 | background-color: transparent; 403 | } 404 | 405 | .CodeMirror-lines { 406 | cursor: text; 407 | min-height: 1px; /* prevents collapsing before first draw */ 408 | } 409 | .CodeMirror pre { 410 | /* Reset some styles that the rest of the page might have set */ 411 | -moz-border-radius: 0; 412 | -webkit-border-radius: 0; 413 | border-radius: 0; 414 | border-width: 0; 415 | background: transparent; 416 | font-family: inherit; 417 | font-size: inherit; 418 | margin: 0; 419 | white-space: pre; 420 | word-wrap: normal; 421 | line-height: inherit; 422 | color: inherit; 423 | z-index: 2; 424 | position: relative; 425 | overflow: visible; 426 | -webkit-tap-highlight-color: transparent; 427 | -webkit-font-variant-ligatures: contextual; 428 | font-variant-ligatures: contextual; 429 | } 430 | .CodeMirror-wrap pre { 431 | word-wrap: break-word; 432 | white-space: pre-wrap; 433 | word-break: normal; 434 | } 435 | 436 | .CodeMirror-linebackground { 437 | position: absolute; 438 | left: 0; 439 | right: 0; 440 | top: 0; 441 | bottom: 0; 442 | z-index: 0; 443 | } 444 | 445 | .CodeMirror-linewidget { 446 | position: relative; 447 | z-index: 2; 448 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 449 | } 450 | 451 | .CodeMirror-rtl pre { 452 | direction: rtl; 453 | } 454 | 455 | .CodeMirror-code { 456 | outline: none; 457 | } 458 | 459 | /* Force content-box sizing for the elements where we expect it */ 460 | .CodeMirror-scroll, 461 | .CodeMirror-sizer, 462 | .CodeMirror-gutter, 463 | .CodeMirror-gutters, 464 | .CodeMirror-linenumber { 465 | -moz-box-sizing: content-box; 466 | box-sizing: content-box; 467 | } 468 | 469 | .CodeMirror-measure { 470 | position: absolute; 471 | width: 100%; 472 | height: 0; 473 | overflow: hidden; 474 | visibility: hidden; 475 | } 476 | 477 | .CodeMirror-cursor { 478 | position: absolute; 479 | pointer-events: none; 480 | } 481 | .CodeMirror-measure pre { 482 | position: static; 483 | } 484 | 485 | div.CodeMirror-cursors { 486 | visibility: hidden; 487 | position: relative; 488 | z-index: 3; 489 | } 490 | div.CodeMirror-dragcursors { 491 | visibility: visible; 492 | } 493 | 494 | .CodeMirror-focused div.CodeMirror-cursors { 495 | visibility: visible; 496 | } 497 | 498 | .CodeMirror-selected { 499 | background: var(--selected-bg-non-focus); 500 | } 501 | .CodeMirror-focused .CodeMirror-selected { 502 | background: var(--selected-bg); 503 | } 504 | .CodeMirror-crosshair { 505 | cursor: crosshair; 506 | } 507 | .CodeMirror-line::selection, 508 | .CodeMirror-line > span::selection, 509 | .CodeMirror-line > span > span::selection { 510 | background: var(--selected-bg); 511 | } 512 | .CodeMirror-line::-moz-selection, 513 | .CodeMirror-line > span::-moz-selection, 514 | .CodeMirror-line > span > span::-moz-selection { 515 | background: var(--selected-bg); 516 | } 517 | 518 | .cm-searching { 519 | background-color: #ffa; 520 | background-color: rgba(255, 255, 0, 0.4); 521 | } 522 | 523 | /* Used to force a border model for a node */ 524 | .cm-force-border { 525 | padding-right: 0.1px; 526 | } 527 | 528 | @media print { 529 | /* Hide the cursor when printing */ 530 | .CodeMirror div.CodeMirror-cursors { 531 | visibility: hidden; 532 | } 533 | } 534 | 535 | /* See issue #2901 */ 536 | .cm-tab-wrap-hack:after { 537 | content: ''; 538 | } 539 | 540 | /* Help users use markselection to safely style text background */ 541 | span.CodeMirror-selectedtext { 542 | background: none; 543 | } 544 | 545 | .CodeMirror-dialog { 546 | background-color: var(--bg); 547 | } 548 | -------------------------------------------------------------------------------- /src/codemirror/codemirror.ts: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror' 2 | import 'codemirror/addon/dialog/dialog.css' 3 | import './codemirror.css' 4 | 5 | // modes 6 | import 'codemirror/mode/javascript/javascript.js' 7 | import 'codemirror/mode/css/css.js' 8 | import 'codemirror/mode/htmlmixed/htmlmixed.js' 9 | 10 | // addons 11 | import 'codemirror/addon/edit/closebrackets.js' 12 | import 'codemirror/addon/edit/closetag.js' 13 | import 'codemirror/addon/comment/comment.js' 14 | import 'codemirror/addon/fold/foldcode.js' 15 | import 'codemirror/addon/fold/foldgutter.js' 16 | import 'codemirror/addon/fold/brace-fold.js' 17 | import 'codemirror/addon/fold/indent-fold.js' 18 | import 'codemirror/addon/fold/comment-fold.js' 19 | import 'codemirror/addon/search/search.js' 20 | import 'codemirror/addon/search/searchcursor.js' 21 | import 'codemirror/addon/dialog/dialog.js' 22 | 23 | // keymap 24 | import 'codemirror/keymap/sublime.js' 25 | 26 | export default CodeMirror 27 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | export { 2 | useStore, 3 | File, 4 | type SFCOptions, 5 | type StoreState, 6 | type Store, 7 | type ReplStore, 8 | } from './store' 9 | export { useVueImportMap, mergeImportMap, type ImportMap } from './import-map' 10 | export { compileFile } from './transform' 11 | export { version as languageToolsVersion } from '@vue/language-service/package.json' 12 | -------------------------------------------------------------------------------- /src/editor/CodeMirrorEditor.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 49 | -------------------------------------------------------------------------------- /src/editor/EditorContainer.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 60 | 61 | 82 | -------------------------------------------------------------------------------- /src/editor/FileSelector.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 175 | 176 | 294 | -------------------------------------------------------------------------------- /src/editor/MonacoEditor.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /src/editor/ToggleButton.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | 53 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { ComponentOptions } from 'vue' 5 | const comp: ComponentOptions 6 | export default comp 7 | } 8 | -------------------------------------------------------------------------------- /src/import-map.ts: -------------------------------------------------------------------------------- 1 | import { computed, version as currentVersion, ref } from 'vue' 2 | 3 | export function useVueImportMap( 4 | defaults: { 5 | runtimeDev?: string | (() => string) 6 | runtimeProd?: string | (() => string) 7 | serverRenderer?: string | (() => string) 8 | vueVersion?: string | null 9 | } = {}, 10 | ) { 11 | function normalizeDefaults(defaults?: string | (() => string)) { 12 | if (!defaults) return 13 | return typeof defaults === 'string' ? defaults : defaults() 14 | } 15 | 16 | const productionMode = ref(false) 17 | const vueVersion = ref(defaults.vueVersion || null) 18 | const importMap = computed(() => { 19 | const vue = 20 | (!vueVersion.value && 21 | normalizeDefaults( 22 | productionMode.value ? defaults.runtimeProd : defaults.runtimeDev, 23 | )) || 24 | `https://cdn.jsdelivr.net/npm/@vue/runtime-dom@${ 25 | vueVersion.value || currentVersion 26 | }/dist/runtime-dom.esm-browser${productionMode.value ? `.prod` : ``}.js` 27 | 28 | const serverRenderer = 29 | (!vueVersion.value && normalizeDefaults(defaults.serverRenderer)) || 30 | `https://cdn.jsdelivr.net/npm/@vue/server-renderer@${ 31 | vueVersion.value || currentVersion 32 | }/dist/server-renderer.esm-browser.js` 33 | return { 34 | imports: { 35 | vue, 36 | 'vue/server-renderer': serverRenderer, 37 | }, 38 | } 39 | }) 40 | 41 | return { 42 | productionMode, 43 | importMap, 44 | vueVersion, 45 | defaultVersion: currentVersion, 46 | } 47 | } 48 | 49 | export interface ImportMap { 50 | imports?: Record 51 | scopes?: Record> 52 | } 53 | 54 | export function mergeImportMap(a: ImportMap, b: ImportMap): ImportMap { 55 | return { 56 | imports: { ...a.imports, ...b.imports }, 57 | scopes: { ...a.scopes, ...b.scopes }, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Repl, type Props as ReplProps } from './Repl.vue' 2 | export { default as Preview } from './output/Preview.vue' 3 | export { default as Sandbox, type SandboxProps } from './output/Sandbox.vue' 4 | export type { OutputModes } from './types' 5 | export * from './core' 6 | -------------------------------------------------------------------------------- /src/jsx.ts: -------------------------------------------------------------------------------- 1 | import { transform } from '@babel/standalone' 2 | import jsx from '@vue/babel-plugin-jsx' 3 | 4 | export function transformJSX(src: string) { 5 | return transform(src, { 6 | plugins: [jsx], 7 | }).code! 8 | } 9 | -------------------------------------------------------------------------------- /src/monaco/Monaco.vue: -------------------------------------------------------------------------------- 1 | 173 | 174 | 182 | 183 | 191 | -------------------------------------------------------------------------------- /src/monaco/env.ts: -------------------------------------------------------------------------------- 1 | import * as volar from '@volar/monaco' 2 | import { Uri, editor, languages } from 'monaco-editor-core' 3 | import editorWorker from 'monaco-editor-core/esm/vs/editor/editor.worker?worker' 4 | import { watchEffect } from 'vue' 5 | import type { Store } from '../store' 6 | import { getOrCreateModel } from './utils' 7 | import type { CreateData } from './vue.worker' 8 | import vueWorker from './vue.worker?worker' 9 | import * as languageConfigs from './language-configs' 10 | import type { WorkerLanguageService } from '@volar/monaco/worker' 11 | import { debounce } from '../utils' 12 | 13 | let initted = false 14 | export function initMonaco(store: Store) { 15 | if (initted) return 16 | loadMonacoEnv(store) 17 | 18 | watchEffect(() => { 19 | // create a model for each file in the store 20 | for (const filename in store.files) { 21 | const file = store.files[filename] 22 | if (editor.getModel(Uri.parse(`file:///${filename}`))) continue 23 | getOrCreateModel( 24 | Uri.parse(`file:///${filename}`), 25 | file.language, 26 | file.code, 27 | ) 28 | } 29 | 30 | // dispose of any models that are not in the store 31 | for (const model of editor.getModels()) { 32 | const uri = model.uri.toString() 33 | if (store.files[uri.substring('file:///'.length)]) continue 34 | 35 | if (uri.startsWith('file:///node_modules')) continue 36 | if (uri.startsWith('inmemory://')) continue 37 | 38 | model.dispose() 39 | } 40 | }) 41 | 42 | initted = true 43 | } 44 | 45 | export class WorkerHost { 46 | onFetchCdnFile(uri: string, text: string) { 47 | getOrCreateModel(Uri.parse(uri), undefined, text) 48 | } 49 | } 50 | 51 | let disposeVue: undefined | (() => void) 52 | export async function reloadLanguageTools(store: Store) { 53 | disposeVue?.() 54 | 55 | let dependencies: Record = { 56 | ...store.dependencyVersion, 57 | } 58 | 59 | if (store.vueVersion) { 60 | dependencies = { 61 | ...dependencies, 62 | vue: store.vueVersion, 63 | '@vue/compiler-core': store.vueVersion, 64 | '@vue/compiler-dom': store.vueVersion, 65 | '@vue/compiler-sfc': store.vueVersion, 66 | '@vue/compiler-ssr': store.vueVersion, 67 | '@vue/reactivity': store.vueVersion, 68 | '@vue/runtime-core': store.vueVersion, 69 | '@vue/runtime-dom': store.vueVersion, 70 | '@vue/shared': store.vueVersion, 71 | } 72 | } 73 | 74 | if (store.typescriptVersion) { 75 | dependencies = { 76 | ...dependencies, 77 | typescript: store.typescriptVersion, 78 | } 79 | } 80 | 81 | const worker = editor.createWebWorker({ 82 | moduleId: 'vs/language/vue/vueWorker', 83 | label: 'vue', 84 | host: new WorkerHost(), 85 | createData: { 86 | tsconfig: store.getTsConfig?.() || {}, 87 | dependencies, 88 | } satisfies CreateData, 89 | }) 90 | const languageId = ['vue', 'javascript', 'typescript'] 91 | const getSyncUris = () => 92 | Object.keys(store.files).map((filename) => Uri.parse(`file:///${filename}`)) 93 | 94 | const { dispose: disposeMarkers } = volar.activateMarkers( 95 | worker, 96 | languageId, 97 | 'vue', 98 | getSyncUris, 99 | editor, 100 | ) 101 | const { dispose: disposeAutoInsertion } = volar.activateAutoInsertion( 102 | worker, 103 | languageId, 104 | getSyncUris, 105 | editor, 106 | ) 107 | const { dispose: disposeProvides } = await volar.registerProviders( 108 | worker, 109 | languageId, 110 | getSyncUris, 111 | languages, 112 | ) 113 | 114 | disposeVue = () => { 115 | disposeMarkers() 116 | disposeAutoInsertion() 117 | disposeProvides() 118 | } 119 | } 120 | 121 | export interface WorkerMessage { 122 | event: 'init' 123 | tsVersion: string 124 | tsLocale?: string 125 | } 126 | 127 | export function loadMonacoEnv(store: Store) { 128 | ;(self as any).MonacoEnvironment = { 129 | async getWorker(_: any, label: string) { 130 | if (label === 'vue') { 131 | const worker = new vueWorker() 132 | const init = new Promise((resolve) => { 133 | worker.addEventListener('message', (data) => { 134 | if (data.data === 'inited') { 135 | resolve() 136 | } 137 | }) 138 | worker.postMessage({ 139 | event: 'init', 140 | tsVersion: store.typescriptVersion, 141 | tsLocale: store.locale, 142 | } satisfies WorkerMessage) 143 | }) 144 | await init 145 | return worker 146 | } 147 | return new editorWorker() 148 | }, 149 | } 150 | languages.register({ id: 'vue', extensions: ['.vue'] }) 151 | languages.register({ id: 'javascript', extensions: ['.js'] }) 152 | languages.register({ id: 'typescript', extensions: ['.ts'] }) 153 | languages.register({ id: 'css', extensions: ['.css'] }) 154 | languages.setLanguageConfiguration('vue', languageConfigs.vue) 155 | languages.setLanguageConfiguration('javascript', languageConfigs.js) 156 | languages.setLanguageConfiguration('typescript', languageConfigs.ts) 157 | languages.setLanguageConfiguration('css', languageConfigs.css) 158 | 159 | let languageToolsPromise: Promise | undefined 160 | store.reloadLanguageTools = debounce(async () => { 161 | ;(languageToolsPromise ||= reloadLanguageTools(store)).finally(() => { 162 | languageToolsPromise = undefined 163 | }) 164 | }, 250) 165 | languages.onLanguage('vue', () => store.reloadLanguageTools!()) 166 | 167 | // Support for go to definition 168 | editor.registerEditorOpener({ 169 | openCodeEditor(_, resource) { 170 | if (resource.toString().startsWith('file:///node_modules')) { 171 | return true 172 | } 173 | 174 | const path = resource.path 175 | if (/^\//.test(path)) { 176 | const fileName = path.replace('/', '') 177 | if (fileName !== store.activeFile.filename) { 178 | store.setActive(fileName) 179 | return true 180 | } 181 | } 182 | 183 | return false 184 | }, 185 | }) 186 | } 187 | -------------------------------------------------------------------------------- /src/monaco/highlight.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor-core' 2 | import { createHighlighterCoreSync } from 'shiki/core' 3 | import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs' 4 | import { shikiToMonaco } from '@shikijs/monaco' 5 | 6 | import langVue from 'shiki/langs/vue.mjs' 7 | import langTsx from 'shiki/langs/tsx.mjs' 8 | import langJsx from 'shiki/langs/jsx.mjs' 9 | import themeDark from 'shiki/themes/dark-plus.mjs' 10 | import themeLight from 'shiki/themes/light-plus.mjs' 11 | 12 | let registered = false 13 | export function registerHighlighter() { 14 | if (!registered) { 15 | const highlighter = createHighlighterCoreSync({ 16 | themes: [themeDark, themeLight], 17 | langs: [langVue, langTsx, langJsx], 18 | engine: createJavaScriptRegexEngine(), 19 | }) 20 | monaco.languages.register({ id: 'vue' }) 21 | shikiToMonaco(highlighter, monaco) 22 | registered = true 23 | } 24 | 25 | return { 26 | light: themeLight.name!, 27 | dark: themeDark.name!, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/monaco/language-configs.ts: -------------------------------------------------------------------------------- 1 | import { languages } from 'monaco-editor-core' 2 | 3 | // export const html: languages.LanguageConfiguration = { 4 | // comments: { 5 | // blockComment: [''], 6 | // }, 7 | // brackets: [ 8 | // [''], 9 | // ['{', '}'], 10 | // ['(', ')'], 11 | // ], 12 | // autoClosingPairs: [ 13 | // { open: '{', close: '}' }, 14 | // { open: '[', close: ']' }, 15 | // { open: '(', close: ')' }, 16 | // { open: "'", close: "'" }, 17 | // { open: '"', close: '"' }, 18 | // { open: '', notIn: ['comment', 'string'] }, 19 | // ], 20 | // surroundingPairs: [ 21 | // { open: "'", close: "'" }, 22 | // { open: '"', close: '"' }, 23 | // { open: '{', close: '}' }, 24 | // { open: '[', close: ']' }, 25 | // { open: '(', close: ')' }, 26 | // { open: '<', close: '>' }, 27 | // ], 28 | // colorizedBracketPairs: [], 29 | // folding: { 30 | // markers: { 31 | // start: /^\s*/, 32 | // end: /^\s*/, 33 | // }, 34 | // }, 35 | // wordPattern: new RegExp( 36 | // '(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\$\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\\'\\"\\,\\.\\<\\>\\/\\s]+)', 37 | // ), 38 | // onEnterRules: [ 39 | // { 40 | // beforeText: new RegExp( 41 | // '<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^\'"/>]|"[^"]*"|\'[^\']*\')*?(?!\\/)>)[^<]*$', 42 | // 'i', 43 | // ), 44 | // afterText: new RegExp('^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>', 'i'), 45 | // action: { 46 | // indentAction: languages.IndentAction.IndentOutdent, 47 | // }, 48 | // }, 49 | // { 50 | // beforeText: new RegExp( 51 | // '<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^\'"/>]|"[^"]*"|\'[^\']*\')*?(?!\\/)>)[^<]*$', 52 | // 'i', 53 | // ), 54 | // action: { 55 | // indentAction: languages.IndentAction.Indent, 56 | // }, 57 | // }, 58 | // ], 59 | // indentationRules: { 60 | // increaseIndentPattern: new RegExp( 61 | // '<(?!\\?|(?: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>)|)|\\{[^}"\']*$', 62 | // ), 63 | // decreaseIndentPattern: new RegExp( 64 | // '^\\s*(<\\/(?!html)[-_\\.A-Za-z0-9]+\\b[^>]*>|-->|\\})', 65 | // ), 66 | // }, 67 | // } 68 | 69 | export const css: languages.LanguageConfiguration = { 70 | comments: { 71 | blockComment: ['/*', '*/'], 72 | }, 73 | brackets: [ 74 | ['{', '}'], 75 | ['[', ']'], 76 | ['(', ')'], 77 | ], 78 | autoClosingPairs: [ 79 | { open: '{', close: '}', notIn: ['string', 'comment'] }, 80 | { open: '[', close: ']', notIn: ['string', 'comment'] }, 81 | { open: '(', close: ')', notIn: ['string', 'comment'] }, 82 | { open: '"', close: '"', notIn: ['string', 'comment'] }, 83 | { open: "'", close: "'", notIn: ['string', 'comment'] }, 84 | ], 85 | surroundingPairs: [ 86 | { 87 | open: "'", 88 | close: "'", 89 | }, 90 | { 91 | open: '"', 92 | close: '"', 93 | }, 94 | { 95 | open: '{', 96 | close: '}', 97 | }, 98 | { 99 | open: '[', 100 | close: ']', 101 | }, 102 | { 103 | open: '(', 104 | close: ')', 105 | }, 106 | ], 107 | folding: { 108 | markers: { 109 | start: new RegExp('^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/'), 110 | end: new RegExp('^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/'), 111 | }, 112 | }, 113 | indentationRules: { 114 | increaseIndentPattern: new RegExp('(^.*\\{[^}]*$)'), 115 | decreaseIndentPattern: new RegExp('^\\s*\\}'), 116 | }, 117 | wordPattern: new RegExp( 118 | '(#?-?\\d*\\.\\d\\w*%?)|(::?[\\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\\w-?]+%?|[@#!.])', 119 | ), 120 | } 121 | 122 | export const vue: languages.LanguageConfiguration = { 123 | comments: { 124 | blockComment: [''], 125 | }, 126 | brackets: [ 127 | [''], 128 | ['<', '>'], 129 | ['{', '}'], 130 | ['(', ')'], 131 | ], 132 | autoClosingPairs: [ 133 | { 134 | open: '{', 135 | close: '}', 136 | }, 137 | { 138 | open: '[', 139 | close: ']', 140 | }, 141 | { 142 | open: '(', 143 | close: ')', 144 | }, 145 | { 146 | open: "'", 147 | close: "'", 148 | }, 149 | { 150 | open: '"', 151 | close: '"', 152 | }, 153 | { 154 | open: '', 156 | notIn: ['comment', 'string'], 157 | }, 158 | { 159 | open: '`', 160 | close: '`', 161 | notIn: ['string', 'comment'], 162 | }, 163 | { 164 | open: '/**', 165 | close: ' */', 166 | notIn: ['string'], 167 | }, 168 | ], 169 | autoCloseBefore: ';:.,=}])><`\'" \n\t', 170 | surroundingPairs: [ 171 | { 172 | open: "'", 173 | close: "'", 174 | }, 175 | { 176 | open: '"', 177 | close: '"', 178 | }, 179 | { 180 | open: '{', 181 | close: '}', 182 | }, 183 | { 184 | open: '[', 185 | close: ']', 186 | }, 187 | { 188 | open: '(', 189 | close: ')', 190 | }, 191 | { 192 | open: '<', 193 | close: '>', 194 | }, 195 | { 196 | open: '`', 197 | close: '`', 198 | }, 199 | ], 200 | colorizedBracketPairs: [], 201 | folding: { 202 | markers: { 203 | start: /^\s*/, 204 | end: /^\s*/, 205 | }, 206 | }, 207 | wordPattern: 208 | /(-?\d*\.\d\w*)|([^\`\@\~\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/, 209 | onEnterRules: [ 210 | { 211 | beforeText: 212 | /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr|script|style))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, 213 | afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i, 214 | action: { 215 | indentAction: languages.IndentAction.IndentOutdent, 216 | }, 217 | }, 218 | { 219 | beforeText: 220 | /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr|script|style))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, 221 | action: { 222 | indentAction: languages.IndentAction.Indent, 223 | }, 224 | }, 225 | ], 226 | indentationRules: { 227 | increaseIndentPattern: 228 | /<(?!\?|(?: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, 229 | decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/, 230 | }, 231 | } 232 | 233 | export const js: languages.LanguageConfiguration = { 234 | comments: { 235 | lineComment: '//', 236 | blockComment: ['/*', '*/'], 237 | }, 238 | brackets: [ 239 | ['${', '}'], 240 | ['{', '}'], 241 | ['[', ']'], 242 | ['(', ')'], 243 | ], 244 | autoClosingPairs: [ 245 | { 246 | open: '{', 247 | close: '}', 248 | }, 249 | { 250 | open: '[', 251 | close: ']', 252 | }, 253 | { 254 | open: '(', 255 | close: ')', 256 | }, 257 | { 258 | open: "'", 259 | close: "'", 260 | notIn: ['string', 'comment'], 261 | }, 262 | { 263 | open: '"', 264 | close: '"', 265 | notIn: ['string'], 266 | }, 267 | { 268 | open: '`', 269 | close: '`', 270 | notIn: ['string', 'comment'], 271 | }, 272 | { 273 | open: '/**', 274 | close: ' */', 275 | notIn: ['string'], 276 | }, 277 | ], 278 | surroundingPairs: [ 279 | { 280 | open: "'", 281 | close: "'", 282 | }, 283 | { 284 | open: '"', 285 | close: '"', 286 | }, 287 | { 288 | open: '{', 289 | close: '}', 290 | }, 291 | { 292 | open: '[', 293 | close: ']', 294 | }, 295 | { 296 | open: '(', 297 | close: ')', 298 | }, 299 | { 300 | open: '<', 301 | close: '>', 302 | }, 303 | { 304 | open: '`', 305 | close: '`', 306 | }, 307 | ], 308 | autoCloseBefore: ';:.,=}])>` \n\t', 309 | folding: { 310 | markers: { 311 | start: /^\s*\/\/\s*#?region\b/, 312 | end: /^\s*\/\/\s*#?endregion\b/, 313 | }, 314 | }, 315 | wordPattern: 316 | /(-?\d*\.\d\w*)|([^\`\~\@\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/, 317 | indentationRules: { 318 | decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/, 319 | increaseIndentPattern: 320 | /^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/, 321 | unIndentedLinePattern: 322 | /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$|^(\t|[ ])*[ ]\*\/\s*$|^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, 323 | }, 324 | onEnterRules: [ 325 | { 326 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 327 | afterText: /^\s*\*\/$/, 328 | action: { 329 | indentAction: languages.IndentAction.IndentOutdent, 330 | appendText: ' * ', 331 | }, 332 | }, 333 | { 334 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 335 | action: { 336 | indentAction: languages.IndentAction.None, 337 | appendText: ' * ', 338 | }, 339 | }, 340 | { 341 | beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, 342 | previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, 343 | action: { 344 | indentAction: languages.IndentAction.None, 345 | appendText: '* ', 346 | }, 347 | }, 348 | { 349 | beforeText: /^(\t|[ ])*[ ]\*\/\s*$/, 350 | action: { 351 | indentAction: languages.IndentAction.None, 352 | removeText: 1, 353 | }, 354 | }, 355 | { 356 | beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/, 357 | action: { 358 | indentAction: languages.IndentAction.None, 359 | removeText: 1, 360 | }, 361 | }, 362 | { 363 | beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/, 364 | afterText: /^(?!\s*(\bcase\b|\bdefault\b))/, 365 | action: { 366 | indentAction: languages.IndentAction.Indent, 367 | }, 368 | }, 369 | { 370 | previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/, 371 | beforeText: /^\s+([^{i\s]|i(?!f\b))/, 372 | action: { 373 | indentAction: languages.IndentAction.Outdent, 374 | }, 375 | }, 376 | ], 377 | } 378 | 379 | export const ts: languages.LanguageConfiguration = { 380 | comments: { 381 | lineComment: '//', 382 | blockComment: ['/*', '*/'], 383 | }, 384 | brackets: [ 385 | ['${', '}'], 386 | ['{', '}'], 387 | ['[', ']'], 388 | ['(', ')'], 389 | ], 390 | autoClosingPairs: [ 391 | { 392 | open: '{', 393 | close: '}', 394 | }, 395 | { 396 | open: '[', 397 | close: ']', 398 | }, 399 | { 400 | open: '(', 401 | close: ')', 402 | }, 403 | { 404 | open: "'", 405 | close: "'", 406 | notIn: ['string', 'comment'], 407 | }, 408 | { 409 | open: '"', 410 | close: '"', 411 | notIn: ['string'], 412 | }, 413 | { 414 | open: '`', 415 | close: '`', 416 | notIn: ['string', 'comment'], 417 | }, 418 | { 419 | open: '/**', 420 | close: ' */', 421 | notIn: ['string'], 422 | }, 423 | ], 424 | surroundingPairs: [ 425 | { 426 | open: "'", 427 | close: "'", 428 | }, 429 | { 430 | open: '"', 431 | close: '"', 432 | }, 433 | { 434 | open: '{', 435 | close: '}', 436 | }, 437 | { 438 | open: '[', 439 | close: ']', 440 | }, 441 | { 442 | open: '(', 443 | close: ')', 444 | }, 445 | { 446 | open: '<', 447 | close: '>', 448 | }, 449 | { 450 | open: '`', 451 | close: '`', 452 | }, 453 | ], 454 | colorizedBracketPairs: [ 455 | ['(', ')'], 456 | ['[', ']'], 457 | ['{', '}'], 458 | ['<', '>'], 459 | ], 460 | autoCloseBefore: ';:.,=}])>` \n\t', 461 | folding: { 462 | markers: { 463 | start: /^\s*\/\/\s*#?region\b/, 464 | end: /^\s*\/\/\s*#?endregion\b/, 465 | }, 466 | }, 467 | wordPattern: 468 | /(-?\d*\.\d\w*)|([^\`\~\@\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/, 469 | indentationRules: { 470 | decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/, 471 | increaseIndentPattern: 472 | /^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/, 473 | unIndentedLinePattern: 474 | /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$|^(\t|[ ])*[ ]\*\/\s*$|^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, 475 | }, 476 | onEnterRules: [ 477 | { 478 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 479 | afterText: /^\s*\*\/$/, 480 | action: { 481 | indentAction: languages.IndentAction.IndentOutdent, 482 | appendText: ' * ', 483 | }, 484 | }, 485 | { 486 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 487 | action: { 488 | indentAction: languages.IndentAction.None, 489 | appendText: ' * ', 490 | }, 491 | }, 492 | { 493 | beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/, 494 | previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/, 495 | action: { 496 | indentAction: languages.IndentAction.None, 497 | appendText: '* ', 498 | }, 499 | }, 500 | { 501 | beforeText: /^(\t|[ ])*[ ]\*\/\s*$/, 502 | action: { 503 | indentAction: languages.IndentAction.None, 504 | removeText: 1, 505 | }, 506 | }, 507 | { 508 | beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/, 509 | action: { 510 | indentAction: languages.IndentAction.None, 511 | removeText: 1, 512 | }, 513 | }, 514 | { 515 | beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/, 516 | afterText: /^(?!\s*(\bcase\b|\bdefault\b))/, 517 | action: { 518 | indentAction: languages.IndentAction.Indent, 519 | }, 520 | }, 521 | { 522 | previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/, 523 | beforeText: /^\s+([^{i\s]|i(?!f\b))/, 524 | action: { 525 | indentAction: languages.IndentAction.Outdent, 526 | }, 527 | }, 528 | ], 529 | } 530 | -------------------------------------------------------------------------------- /src/monaco/utils.ts: -------------------------------------------------------------------------------- 1 | import { type Uri, editor } from 'monaco-editor-core' 2 | 3 | export function getOrCreateModel( 4 | uri: Uri, 5 | lang: string | undefined, 6 | value: string, 7 | ) { 8 | const model = editor.getModel(uri) 9 | if (model) { 10 | model.setValue(value) 11 | return model 12 | } 13 | return editor.createModel(value, lang, uri) 14 | } 15 | -------------------------------------------------------------------------------- /src/monaco/vue.worker.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import * as worker from 'monaco-editor-core/esm/vs/editor/editor.worker' 3 | import type * as monaco from 'monaco-editor-core' 4 | import { 5 | type LanguageServiceEnvironment, 6 | createTypeScriptWorkerLanguageService, 7 | } from '@volar/monaco/worker' 8 | import { createNpmFileSystem } from '@volar/jsdelivr' 9 | import { 10 | type VueCompilerOptions, 11 | getFullLanguageServicePlugins, 12 | createVueLanguagePlugin, 13 | resolveVueCompilerOptions, 14 | } from '@vue/language-service' 15 | import type { WorkerHost, WorkerMessage } from './env' 16 | import { URI } from 'vscode-uri' 17 | 18 | export interface CreateData { 19 | tsconfig: { 20 | compilerOptions?: import('typescript').CompilerOptions 21 | vueCompilerOptions?: Partial 22 | } 23 | dependencies: Record 24 | } 25 | 26 | let ts: typeof import('typescript') 27 | let locale: string | undefined 28 | 29 | self.onmessage = async (msg: MessageEvent) => { 30 | if (msg.data?.event === 'init') { 31 | locale = msg.data.tsLocale 32 | ts = await importTsFromCdn(msg.data.tsVersion) 33 | self.postMessage('inited') 34 | return 35 | } 36 | 37 | worker.initialize( 38 | ( 39 | ctx: monaco.worker.IWorkerContext, 40 | { tsconfig, dependencies }: CreateData, 41 | ) => { 42 | const asFileName = (uri: URI) => uri.path 43 | const asUri = (fileName: string): URI => URI.file(fileName) 44 | const env: LanguageServiceEnvironment = { 45 | workspaceFolders: [URI.file('/')], 46 | locale, 47 | fs: createNpmFileSystem( 48 | (uri) => { 49 | if (uri.scheme === 'file') { 50 | if (uri.path === '/node_modules') { 51 | return '' 52 | } else if (uri.path.startsWith('/node_modules/')) { 53 | return uri.path.slice('/node_modules/'.length) 54 | } 55 | } 56 | }, 57 | (pkgName) => dependencies[pkgName], 58 | (path, content) => { 59 | ctx.host.onFetchCdnFile( 60 | asUri('/node_modules/' + path).toString(), 61 | content, 62 | ) 63 | }, 64 | ), 65 | } 66 | 67 | const { options: compilerOptions } = ts.convertCompilerOptionsFromJson( 68 | tsconfig?.compilerOptions || {}, 69 | '', 70 | ) 71 | const vueCompilerOptions = resolveVueCompilerOptions( 72 | tsconfig.vueCompilerOptions || {}, 73 | ) 74 | 75 | return createTypeScriptWorkerLanguageService({ 76 | typescript: ts, 77 | compilerOptions, 78 | workerContext: ctx, 79 | env, 80 | uriConverter: { 81 | asFileName, 82 | asUri, 83 | }, 84 | languagePlugins: [ 85 | createVueLanguagePlugin( 86 | ts, 87 | compilerOptions, 88 | vueCompilerOptions, 89 | asFileName, 90 | ), 91 | ], 92 | languageServicePlugins: getFullLanguageServicePlugins(ts), 93 | setup({ project }) { 94 | project.vue = { compilerOptions: vueCompilerOptions } 95 | }, 96 | }) 97 | }, 98 | ) 99 | } 100 | 101 | async function importTsFromCdn(tsVersion: string) { 102 | const _module = globalThis.module 103 | ;(globalThis as any).module = { exports: {} } 104 | const tsUrl = `https://cdn.jsdelivr.net/npm/typescript@${tsVersion}/lib/typescript.js` 105 | await import(/* @vite-ignore */ tsUrl) 106 | const ts = globalThis.module.exports 107 | globalThis.module = _module 108 | return ts as typeof import('typescript') 109 | } 110 | -------------------------------------------------------------------------------- /src/output/Output.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 93 | 94 | 131 | -------------------------------------------------------------------------------- /src/output/Preview.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 35 | -------------------------------------------------------------------------------- /src/output/PreviewProxy.ts: -------------------------------------------------------------------------------- 1 | // ReplProxy and srcdoc implementation from Svelte REPL 2 | // MIT License https://github.com/sveltejs/svelte-repl/blob/master/LICENSE 3 | 4 | let uid = 1 5 | 6 | export class PreviewProxy { 7 | iframe: HTMLIFrameElement 8 | handlers: Record 9 | pending_cmds: Map< 10 | number, 11 | { resolve: (value: unknown) => void; reject: (reason?: any) => void } 12 | > 13 | handle_event: (e: any) => void 14 | 15 | constructor(iframe: HTMLIFrameElement, handlers: Record) { 16 | this.iframe = iframe 17 | this.handlers = handlers 18 | 19 | this.pending_cmds = new Map() 20 | 21 | this.handle_event = (e) => this.handle_repl_message(e) 22 | window.addEventListener('message', this.handle_event, false) 23 | } 24 | 25 | destroy() { 26 | window.removeEventListener('message', this.handle_event) 27 | } 28 | 29 | iframe_command(action: string, args: any) { 30 | return new Promise((resolve, reject) => { 31 | const cmd_id = uid++ 32 | 33 | this.pending_cmds.set(cmd_id, { resolve, reject }) 34 | 35 | this.iframe.contentWindow!.postMessage({ action, cmd_id, args }, '*') 36 | }) 37 | } 38 | 39 | handle_command_message(cmd_data: any) { 40 | let action = cmd_data.action 41 | let id = cmd_data.cmd_id 42 | let handler = this.pending_cmds.get(id) 43 | 44 | if (handler) { 45 | this.pending_cmds.delete(id) 46 | if (action === 'cmd_error') { 47 | let { message, stack } = cmd_data 48 | let e = new Error(message) 49 | e.stack = stack 50 | handler.reject(e) 51 | } 52 | 53 | if (action === 'cmd_ok') { 54 | handler.resolve(cmd_data.args) 55 | } 56 | } else if (action !== 'cmd_error' && action !== 'cmd_ok') { 57 | console.error('command not found', id, cmd_data, [ 58 | ...this.pending_cmds.keys(), 59 | ]) 60 | } 61 | } 62 | 63 | handle_repl_message(event: any) { 64 | if (event.source !== this.iframe.contentWindow) return 65 | 66 | const { action, args } = event.data 67 | 68 | switch (action) { 69 | case 'cmd_error': 70 | case 'cmd_ok': 71 | return this.handle_command_message(event.data) 72 | case 'fetch_progress': 73 | return this.handlers.on_fetch_progress(args.remaining) 74 | case 'error': 75 | return this.handlers.on_error(event.data) 76 | case 'unhandledrejection': 77 | return this.handlers.on_unhandled_rejection(event.data) 78 | case 'console': 79 | return this.handlers.on_console(event.data) 80 | case 'console_group': 81 | return this.handlers.on_console_group(event.data) 82 | case 'console_group_collapsed': 83 | return this.handlers.on_console_group_collapsed(event.data) 84 | case 'console_group_end': 85 | return this.handlers.on_console_group_end(event.data) 86 | } 87 | } 88 | 89 | eval(script: string | string[]) { 90 | return this.iframe_command('eval', { script }) 91 | } 92 | 93 | handle_links() { 94 | return this.iframe_command('catch_clicks', {}) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/output/Sandbox.vue: -------------------------------------------------------------------------------- 1 | 340 | 341 | 354 | 355 | 367 | -------------------------------------------------------------------------------- /src/output/SsrOutput.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | 33 | -------------------------------------------------------------------------------- /src/output/moduleCompiler.ts: -------------------------------------------------------------------------------- 1 | import type { File, Store } from '../store' 2 | import { 3 | MagicString, 4 | babelParse, 5 | extractIdentifiers, 6 | isInDestructureAssignment, 7 | isStaticProperty, 8 | walk, 9 | walkIdentifiers, 10 | } from 'vue/compiler-sfc' 11 | import type { ExportSpecifier, Identifier, Node } from '@babel/types' 12 | 13 | export function compileModulesForPreview(store: Store, isSSR = false) { 14 | const seen = new Set() 15 | const processed: string[] = [] 16 | processFile(store, store.files[store.mainFile], processed, seen, isSSR) 17 | 18 | if (!isSSR) { 19 | // also add css files that are not imported 20 | for (const filename in store.files) { 21 | if (filename.endsWith('.css')) { 22 | const file = store.files[filename] 23 | if (!seen.has(file)) { 24 | processed.push( 25 | `\nwindow.__css__.push(${JSON.stringify(file.compiled.css)})`, 26 | ) 27 | } 28 | } 29 | } 30 | } 31 | 32 | return processed 33 | } 34 | 35 | const modulesKey = `__modules__` 36 | const exportKey = `__export__` 37 | const dynamicImportKey = `__dynamic_import__` 38 | const moduleKey = `__module__` 39 | 40 | // similar logic with Vite's SSR transform, except this is targeting the browser 41 | function processFile( 42 | store: Store, 43 | file: File, 44 | processed: string[], 45 | seen: Set, 46 | isSSR: boolean, 47 | ) { 48 | if (seen.has(file)) { 49 | return [] 50 | } 51 | seen.add(file) 52 | 53 | if (!isSSR && file.filename.endsWith('.html')) { 54 | return processHtmlFile(store, file.code, file.filename, processed, seen) 55 | } 56 | 57 | let { 58 | code: js, 59 | importedFiles, 60 | hasDynamicImport, 61 | } = processModule( 62 | store, 63 | isSSR ? file.compiled.ssr : file.compiled.js, 64 | file.filename, 65 | ) 66 | processChildFiles( 67 | store, 68 | importedFiles, 69 | hasDynamicImport, 70 | processed, 71 | seen, 72 | isSSR, 73 | ) 74 | // append css 75 | if (file.compiled.css && !isSSR) { 76 | js += `\nwindow.__css__.push(${JSON.stringify(file.compiled.css)})` 77 | } 78 | 79 | // push self 80 | processed.push(js) 81 | } 82 | 83 | function processChildFiles( 84 | store: Store, 85 | importedFiles: Set, 86 | hasDynamicImport: boolean, 87 | processed: string[], 88 | seen: Set, 89 | isSSR: boolean, 90 | ) { 91 | if (hasDynamicImport) { 92 | // process all files 93 | for (const file of Object.values(store.files)) { 94 | if (seen.has(file)) continue 95 | processFile(store, file, processed, seen, isSSR) 96 | } 97 | } else if (importedFiles.size > 0) { 98 | // crawl child imports 99 | for (const imported of importedFiles) { 100 | processFile(store, store.files[imported], processed, seen, isSSR) 101 | } 102 | } 103 | } 104 | 105 | function processModule(store: Store, src: string, filename: string) { 106 | const s = new MagicString(src) 107 | 108 | const ast = babelParse(src, { 109 | sourceFilename: filename, 110 | sourceType: 'module', 111 | }).program.body 112 | 113 | const idToImportMap = new Map() 114 | const declaredConst = new Set() 115 | const importedFiles = new Set() 116 | const importToIdMap = new Map() 117 | 118 | function resolveImport(raw: string): string | undefined { 119 | const files = store.files 120 | let resolved = raw 121 | const file = 122 | files[resolved] || 123 | files[(resolved = raw + '.ts')] || 124 | files[(resolved = raw + '.js')] 125 | return file ? resolved : undefined 126 | } 127 | 128 | function defineImport(node: Node, source: string) { 129 | const filename = resolveImport(source.replace(/^\.\/+/, 'src/')) 130 | if (!filename) { 131 | throw new Error(`File "${source}" does not exist.`) 132 | } 133 | if (importedFiles.has(filename)) { 134 | return importToIdMap.get(filename)! 135 | } 136 | importedFiles.add(filename) 137 | const id = `__import_${importedFiles.size}__` 138 | importToIdMap.set(filename, id) 139 | s.appendLeft( 140 | node.start!, 141 | `const ${id} = ${modulesKey}[${JSON.stringify(filename)}]\n`, 142 | ) 143 | return id 144 | } 145 | 146 | function defineExport(name: string, local = name) { 147 | s.append(`\n${exportKey}(${moduleKey}, "${name}", () => ${local})`) 148 | } 149 | 150 | // 0. instantiate module 151 | s.prepend( 152 | `const ${moduleKey} = ${modulesKey}[${JSON.stringify( 153 | filename, 154 | )}] = { [Symbol.toStringTag]: "Module" }\n\n`, 155 | ) 156 | 157 | // 1. check all import statements and record id -> importName map 158 | for (const node of ast) { 159 | // import foo from 'foo' --> foo -> __import_foo__.default 160 | // import { baz } from 'foo' --> baz -> __import_foo__.baz 161 | // import * as ok from 'foo' --> ok -> __import_foo__ 162 | if (node.type === 'ImportDeclaration') { 163 | const source = node.source.value 164 | if (source.startsWith('./')) { 165 | const importId = defineImport(node, node.source.value) 166 | for (const spec of node.specifiers) { 167 | if (spec.type === 'ImportSpecifier') { 168 | idToImportMap.set( 169 | spec.local.name, 170 | `${importId}.${(spec.imported as Identifier).name}`, 171 | ) 172 | } else if (spec.type === 'ImportDefaultSpecifier') { 173 | idToImportMap.set(spec.local.name, `${importId}.default`) 174 | } else { 175 | // namespace specifier 176 | idToImportMap.set(spec.local.name, importId) 177 | } 178 | } 179 | s.remove(node.start!, node.end!) 180 | } 181 | } 182 | } 183 | 184 | // 2. check all export statements and define exports 185 | for (const node of ast) { 186 | // named exports 187 | if (node.type === 'ExportNamedDeclaration') { 188 | if (node.declaration) { 189 | if ( 190 | node.declaration.type === 'FunctionDeclaration' || 191 | node.declaration.type === 'ClassDeclaration' 192 | ) { 193 | // export function foo() {} 194 | defineExport(node.declaration.id!.name) 195 | } else if (node.declaration.type === 'VariableDeclaration') { 196 | // export const foo = 1, bar = 2 197 | for (const decl of node.declaration.declarations) { 198 | for (const id of extractIdentifiers(decl.id)) { 199 | defineExport(id.name) 200 | } 201 | } 202 | } 203 | s.remove(node.start!, node.declaration.start!) 204 | } else if (node.source) { 205 | // export { foo, bar } from './foo' 206 | const importId = defineImport(node, node.source.value) 207 | for (const spec of node.specifiers) { 208 | defineExport( 209 | (spec.exported as Identifier).name, 210 | `${importId}.${(spec as ExportSpecifier).local.name}`, 211 | ) 212 | } 213 | s.remove(node.start!, node.end!) 214 | } else { 215 | // export { foo, bar } 216 | for (const spec of node.specifiers) { 217 | const local = (spec as ExportSpecifier).local.name 218 | const binding = idToImportMap.get(local) 219 | defineExport((spec.exported as Identifier).name, binding || local) 220 | } 221 | s.remove(node.start!, node.end!) 222 | } 223 | } 224 | 225 | // default export 226 | if (node.type === 'ExportDefaultDeclaration') { 227 | if ('id' in node.declaration && node.declaration.id) { 228 | // named hoistable/class exports 229 | // export default function foo() {} 230 | // export default class A {} 231 | const { name } = node.declaration.id 232 | s.remove(node.start!, node.start! + 15) 233 | s.append(`\n${exportKey}(${moduleKey}, "default", () => ${name})`) 234 | } else { 235 | // anonymous default exports 236 | s.overwrite(node.start!, node.start! + 14, `${moduleKey}.default =`) 237 | } 238 | } 239 | 240 | // export * from './foo' 241 | if (node.type === 'ExportAllDeclaration') { 242 | const importId = defineImport(node, node.source.value) 243 | s.remove(node.start!, node.end!) 244 | s.append(`\nfor (const key in ${importId}) { 245 | if (key !== 'default') { 246 | ${exportKey}(${moduleKey}, key, () => ${importId}[key]) 247 | } 248 | }`) 249 | } 250 | } 251 | 252 | // 3. convert references to import bindings 253 | for (const node of ast) { 254 | if (node.type === 'ImportDeclaration') continue 255 | walkIdentifiers(node, (id, parent, parentStack) => { 256 | const binding = idToImportMap.get(id.name) 257 | if (!binding) { 258 | return 259 | } 260 | if (parent && isStaticProperty(parent) && parent.shorthand) { 261 | // let binding used in a property shorthand 262 | // { foo } -> { foo: __import_x__.foo } 263 | // skip for destructure patterns 264 | if ( 265 | !(parent as any).inPattern || 266 | isInDestructureAssignment(parent, parentStack) 267 | ) { 268 | s.appendLeft(id.end!, `: ${binding}`) 269 | } 270 | } else if ( 271 | parent && 272 | parent.type === 'ClassDeclaration' && 273 | id === parent.superClass 274 | ) { 275 | if (!declaredConst.has(id.name)) { 276 | declaredConst.add(id.name) 277 | // locate the top-most node containing the class declaration 278 | const topNode = parentStack[1] 279 | s.prependRight(topNode.start!, `const ${id.name} = ${binding};\n`) 280 | } 281 | } else { 282 | s.overwrite(id.start!, id.end!, binding) 283 | } 284 | }) 285 | } 286 | 287 | // 4. convert dynamic imports 288 | let hasDynamicImport = false 289 | walk(ast, { 290 | enter(node: Node, parent: Node) { 291 | if (node.type === 'Import' && parent.type === 'CallExpression') { 292 | const arg = parent.arguments[0] 293 | if (arg.type === 'StringLiteral' && arg.value.startsWith('./')) { 294 | hasDynamicImport = true 295 | s.overwrite(node.start!, node.start! + 6, dynamicImportKey) 296 | s.overwrite( 297 | arg.start!, 298 | arg.end!, 299 | JSON.stringify(arg.value.replace(/^\.\/+/, 'src/')), 300 | ) 301 | } 302 | } 303 | }, 304 | }) 305 | 306 | return { 307 | code: s.toString(), 308 | importedFiles, 309 | hasDynamicImport, 310 | } 311 | } 312 | 313 | const scriptRE = /]*>|>)([^]*?)<\/script>/gi 314 | const scriptModuleRE = 315 | /]*type\s*=\s*(?:"module"|'module')[^>]*>([^]*?)<\/script>/gi 316 | 317 | function processHtmlFile( 318 | store: Store, 319 | src: string, 320 | filename: string, 321 | processed: string[], 322 | seen: Set, 323 | ) { 324 | const deps: string[] = [] 325 | let jsCode = '' 326 | const html = src 327 | .replace(scriptModuleRE, (_, content) => { 328 | const { code, importedFiles, hasDynamicImport } = processModule( 329 | store, 330 | content, 331 | filename, 332 | ) 333 | processChildFiles( 334 | store, 335 | importedFiles, 336 | hasDynamicImport, 337 | deps, 338 | seen, 339 | false, 340 | ) 341 | jsCode += '\n' + code 342 | return '' 343 | }) 344 | .replace(scriptRE, (_, content) => { 345 | jsCode += '\n' + content 346 | return '' 347 | }) 348 | processed.push(`document.body.innerHTML = ${JSON.stringify(html)}`) 349 | processed.push(...deps) 350 | processed.push(jsCode) 351 | } 352 | -------------------------------------------------------------------------------- /src/output/srcdoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 367 | 368 | 369 | 373 | 376 | 377 | 378 | 379 | 380 | 381 | -------------------------------------------------------------------------------- /src/sourcemap.ts: -------------------------------------------------------------------------------- 1 | import type { RawSourceMap } from 'source-map-js' 2 | import type { EncodedSourceMap as TraceEncodedSourceMap } from '@jridgewell/trace-mapping' 3 | import { TraceMap, eachMapping } from '@jridgewell/trace-mapping' 4 | import type { EncodedSourceMap as GenEncodedSourceMap } from '@jridgewell/gen-mapping' 5 | import { addMapping, fromMap, toEncodedMap } from '@jridgewell/gen-mapping' 6 | 7 | // trim analyzed bindings comment 8 | export function trimAnalyzedBindings(scriptCode: string) { 9 | return scriptCode.replace(/\/\*[\s\S]*?\*\/\n/, '').trim() 10 | } 11 | /** 12 | * The merge logic of sourcemap is consistent with the logic in vite-plugin-vue 13 | */ 14 | export function getSourceMap( 15 | filename: string, 16 | scriptCode: string, 17 | scriptMap: any, 18 | templateMap: any, 19 | ): RawSourceMap { 20 | let resolvedMap: RawSourceMap | undefined = undefined 21 | if (templateMap) { 22 | // if the template is inlined into the main module (indicated by the presence 23 | // of templateMap), we need to concatenate the two source maps. 24 | const from = scriptMap ?? { 25 | file: filename, 26 | sourceRoot: '', 27 | version: 3, 28 | sources: [], 29 | sourcesContent: [], 30 | names: [], 31 | mappings: '', 32 | } 33 | const gen = fromMap( 34 | // version property of result.map is declared as string 35 | // but actually it is `3` 36 | from as Omit as TraceEncodedSourceMap, 37 | ) 38 | const tracer = new TraceMap( 39 | // same above 40 | templateMap as Omit as TraceEncodedSourceMap, 41 | ) 42 | const offset = 43 | (trimAnalyzedBindings(scriptCode).match(/\r?\n/g)?.length ?? 0) 44 | eachMapping(tracer, (m) => { 45 | if (m.source == null) return 46 | addMapping(gen, { 47 | source: m.source, 48 | original: { line: m.originalLine, column: m.originalColumn }, 49 | generated: { 50 | line: m.generatedLine + offset, 51 | column: m.generatedColumn, 52 | }, 53 | }) 54 | }) 55 | 56 | // same above 57 | resolvedMap = toEncodedMap(gen) as Omit< 58 | GenEncodedSourceMap, 59 | 'version' 60 | > as RawSourceMap 61 | // if this is a template only update, we will be reusing a cached version 62 | // of the main module compile result, which has outdated sourcesContent. 63 | resolvedMap.sourcesContent = templateMap.sourcesContent 64 | } else { 65 | resolvedMap = scriptMap 66 | } 67 | 68 | return resolvedMap! 69 | } 70 | 71 | /* 72 | * Slightly modified version of https://github.com/AriPerkkio/vite-plugin-source-map-visualizer/blob/main/src/generate-link.ts 73 | */ 74 | export function toVisualizer(code: string, sourceMap: RawSourceMap) { 75 | const map = JSON.stringify(sourceMap) 76 | const encoder = new TextEncoder() 77 | 78 | // Convert the strings to Uint8Array 79 | const codeArray = encoder.encode(code) 80 | const mapArray = encoder.encode(map) 81 | 82 | // Create Uint8Array for the lengths 83 | const codeLengthArray = encoder.encode(codeArray.length.toString()) 84 | const mapLengthArray = encoder.encode(mapArray.length.toString()) 85 | 86 | // Combine the lengths and the data 87 | const combinedArray = new Uint8Array( 88 | codeLengthArray.length + 89 | 1 + 90 | codeArray.length + 91 | mapLengthArray.length + 92 | 1 + 93 | mapArray.length, 94 | ) 95 | 96 | combinedArray.set(codeLengthArray) 97 | combinedArray.set([0], codeLengthArray.length) 98 | combinedArray.set(codeArray, codeLengthArray.length + 1) 99 | combinedArray.set( 100 | mapLengthArray, 101 | codeLengthArray.length + 1 + codeArray.length, 102 | ) 103 | combinedArray.set( 104 | [0], 105 | codeLengthArray.length + 1 + codeArray.length + mapLengthArray.length, 106 | ) 107 | combinedArray.set( 108 | mapArray, 109 | codeLengthArray.length + 1 + codeArray.length + mapLengthArray.length + 1, 110 | ) 111 | 112 | // Convert the Uint8Array to a binary string 113 | let binary = '' 114 | const len = combinedArray.byteLength 115 | for (let i = 0; i < len; i++) binary += String.fromCharCode(combinedArray[i]) 116 | 117 | // Convert the binary string to a base64 string and return it 118 | return `https://evanw.github.io/source-map-visualization#${btoa(binary)}` 119 | } 120 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ToRefs, 3 | type UnwrapRef, 4 | computed, 5 | reactive, 6 | ref, 7 | shallowRef, 8 | watch, 9 | watchEffect, 10 | } from 'vue' 11 | import * as defaultCompiler from 'vue/compiler-sfc' 12 | import { compileFile } from './transform' 13 | import { atou, utoa } from './utils' 14 | import type { 15 | SFCAsyncStyleCompileOptions, 16 | SFCScriptCompileOptions, 17 | SFCTemplateCompileOptions, 18 | } from 'vue/compiler-sfc' 19 | import type { OutputModes } from './types' 20 | import type { editor } from 'monaco-editor-core' 21 | import { type ImportMap, mergeImportMap, useVueImportMap } from './import-map' 22 | 23 | import welcomeSFCCode from './template/welcome.vue?raw' 24 | import newSFCCode from './template/new-sfc.vue?raw' 25 | 26 | export const importMapFile = 'import-map.json' 27 | export const tsconfigFile = 'tsconfig.json' 28 | 29 | export function useStore( 30 | { 31 | files = ref(Object.create(null)), 32 | activeFilename = undefined!, // set later 33 | mainFile = ref('src/App.vue'), 34 | template = ref({ 35 | welcomeSFC: welcomeSFCCode, 36 | newSFC: newSFCCode, 37 | }), 38 | builtinImportMap = undefined!, // set later 39 | 40 | errors = ref([]), 41 | showOutput = ref(false), 42 | outputMode = ref('preview'), 43 | sfcOptions = ref({}), 44 | compiler = shallowRef(defaultCompiler), 45 | vueVersion = ref(null), 46 | 47 | locale = ref(), 48 | typescriptVersion = ref('latest'), 49 | dependencyVersion = ref(Object.create(null)), 50 | reloadLanguageTools = ref(), 51 | }: Partial = {}, 52 | serializedState?: string, 53 | ): ReplStore { 54 | if (!builtinImportMap) { 55 | ;({ importMap: builtinImportMap, vueVersion } = useVueImportMap({ 56 | vueVersion: vueVersion.value, 57 | })) 58 | } 59 | const loading = ref(false) 60 | 61 | function applyBuiltinImportMap() { 62 | const importMap = mergeImportMap(builtinImportMap.value, getImportMap()) 63 | setImportMap(importMap) 64 | } 65 | 66 | function init() { 67 | watchEffect(() => { 68 | compileFile(store, activeFile.value).then((errs) => (errors.value = errs)) 69 | }) 70 | 71 | watch( 72 | () => [ 73 | files.value[tsconfigFile]?.code, 74 | typescriptVersion.value, 75 | locale.value, 76 | dependencyVersion.value, 77 | vueVersion.value, 78 | ], 79 | () => reloadLanguageTools.value?.(), 80 | { deep: true }, 81 | ) 82 | 83 | watch( 84 | builtinImportMap, 85 | () => { 86 | setImportMap(mergeImportMap(getImportMap(), builtinImportMap.value)) 87 | }, 88 | { deep: true }, 89 | ) 90 | 91 | watch( 92 | vueVersion, 93 | async (version) => { 94 | if (version) { 95 | const compilerUrl = `https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js` 96 | loading.value = true 97 | compiler.value = await import(/* @vite-ignore */ compilerUrl).finally( 98 | () => (loading.value = false), 99 | ) 100 | console.info(`[@vue/repl] Now using Vue version: ${version}`) 101 | } else { 102 | // reset to default 103 | compiler.value = defaultCompiler 104 | console.info(`[@vue/repl] Now using default Vue version`) 105 | } 106 | }, 107 | { immediate: true }, 108 | ) 109 | 110 | watch( 111 | sfcOptions, 112 | () => { 113 | sfcOptions.value.script ||= {} 114 | sfcOptions.value.script.fs = { 115 | fileExists(file: string) { 116 | if (file.startsWith('/')) file = file.slice(1) 117 | return !!store.files[file] 118 | }, 119 | readFile(file: string) { 120 | if (file.startsWith('/')) file = file.slice(1) 121 | return store.files[file].code 122 | }, 123 | } 124 | }, 125 | { immediate: true }, 126 | ) 127 | 128 | // init tsconfig 129 | if (!files.value[tsconfigFile]) { 130 | files.value[tsconfigFile] = new File( 131 | tsconfigFile, 132 | JSON.stringify(tsconfig, undefined, 2), 133 | ) 134 | } 135 | 136 | // compile rest of the files 137 | errors.value = [] 138 | for (const [filename, file] of Object.entries(files.value)) { 139 | if (filename !== mainFile.value) { 140 | compileFile(store, file).then((errs) => errors.value.push(...errs)) 141 | } 142 | } 143 | } 144 | 145 | function setImportMap(map: ImportMap, merge = false) { 146 | if (merge) { 147 | map = mergeImportMap(getImportMap(), map) 148 | } 149 | 150 | if (map.imports) 151 | for (const [key, value] of Object.entries(map.imports)) { 152 | if (value) { 153 | map.imports![key] = fixURL(value) 154 | } 155 | } 156 | 157 | const code = JSON.stringify(map, undefined, 2) 158 | if (files.value[importMapFile]) { 159 | files.value[importMapFile].code = code 160 | } else { 161 | files.value[importMapFile] = new File(importMapFile, code) 162 | } 163 | } 164 | 165 | const setActive: Store['setActive'] = (filename) => { 166 | activeFilename.value = filename 167 | } 168 | const addFile: Store['addFile'] = (fileOrFilename) => { 169 | let file: File 170 | if (typeof fileOrFilename === 'string') { 171 | file = new File( 172 | fileOrFilename, 173 | fileOrFilename.endsWith('.vue') ? template.value.newSFC : '', 174 | ) 175 | } else { 176 | file = fileOrFilename 177 | } 178 | files.value[file.filename] = file 179 | if (!file.hidden) setActive(file.filename) 180 | } 181 | const deleteFile: Store['deleteFile'] = (filename) => { 182 | if ( 183 | !confirm(`Are you sure you want to delete ${stripSrcPrefix(filename)}?`) 184 | ) { 185 | return 186 | } 187 | 188 | if (activeFilename.value === filename) { 189 | activeFilename.value = mainFile.value 190 | } 191 | delete files.value[filename] 192 | } 193 | const renameFile: Store['renameFile'] = (oldFilename, newFilename) => { 194 | const file = files.value[oldFilename] 195 | 196 | if (!file) { 197 | errors.value = [`Could not rename "${oldFilename}", file not found`] 198 | return 199 | } 200 | 201 | if (!newFilename || oldFilename === newFilename) { 202 | errors.value = [`Cannot rename "${oldFilename}" to "${newFilename}"`] 203 | return 204 | } 205 | 206 | file.filename = newFilename 207 | const newFiles: Record = {} 208 | 209 | // Preserve iteration order for files 210 | for (const [name, file] of Object.entries(files.value)) { 211 | if (name === oldFilename) { 212 | newFiles[newFilename] = file 213 | } else { 214 | newFiles[name] = file 215 | } 216 | } 217 | 218 | files.value = newFiles 219 | 220 | if (mainFile.value === oldFilename) { 221 | mainFile.value = newFilename 222 | } 223 | if (activeFilename.value === oldFilename) { 224 | activeFilename.value = newFilename 225 | } else { 226 | compileFile(store, file).then((errs) => (errors.value = errs)) 227 | } 228 | } 229 | const getImportMap: Store['getImportMap'] = () => { 230 | try { 231 | return JSON.parse(files.value[importMapFile].code) 232 | } catch (e) { 233 | errors.value = [ 234 | `Syntax error in ${importMapFile}: ${(e as Error).message}`, 235 | ] 236 | return {} 237 | } 238 | } 239 | const getTsConfig: Store['getTsConfig'] = () => { 240 | try { 241 | return JSON.parse(files.value[tsconfigFile].code) 242 | } catch { 243 | return {} 244 | } 245 | } 246 | const serialize: ReplStore['serialize'] = () => { 247 | const files = getFiles() 248 | const importMap = files[importMapFile] 249 | if (importMap) { 250 | const parsed = JSON.parse(importMap) 251 | const builtin = builtinImportMap.value.imports || {} 252 | 253 | if (parsed.imports) { 254 | for (const [key, value] of Object.entries(parsed.imports)) { 255 | if (builtin[key] === value) { 256 | delete parsed.imports[key] 257 | } 258 | } 259 | if (parsed.imports && !Object.keys(parsed.imports).length) { 260 | delete parsed.imports 261 | } 262 | } 263 | if (parsed.scopes && !Object.keys(parsed.scopes).length) { 264 | delete parsed.scopes 265 | } 266 | if (Object.keys(parsed).length) { 267 | files[importMapFile] = JSON.stringify(parsed, null, 2) 268 | } else { 269 | delete files[importMapFile] 270 | } 271 | } 272 | if (vueVersion.value) files._version = vueVersion.value 273 | if (typescriptVersion.value !== 'latest' || files._tsVersion) { 274 | files._tsVersion = typescriptVersion.value 275 | } 276 | return '#' + utoa(JSON.stringify(files)) 277 | } 278 | const deserialize: ReplStore['deserialize'] = ( 279 | serializedState: string, 280 | checkBuiltinImportMap = true, 281 | ) => { 282 | if (serializedState.startsWith('#')) 283 | serializedState = serializedState.slice(1) 284 | let saved: any 285 | try { 286 | saved = JSON.parse(atou(serializedState)) 287 | } catch (err) { 288 | console.error(err) 289 | alert('Failed to load code from URL.') 290 | return setDefaultFile() 291 | } 292 | for (const filename in saved) { 293 | if (filename === '_version') { 294 | vueVersion.value = saved[filename] 295 | } else if (filename === '_tsVersion') { 296 | typescriptVersion.value = saved[filename] 297 | } else { 298 | setFile(files.value, filename, saved[filename]) 299 | } 300 | } 301 | if (checkBuiltinImportMap) { 302 | applyBuiltinImportMap() 303 | } 304 | } 305 | const getFiles: ReplStore['getFiles'] = () => { 306 | const exported: Record = {} 307 | for (const [filename, file] of Object.entries(files.value)) { 308 | const normalized = stripSrcPrefix(filename) 309 | exported[normalized] = file.code 310 | } 311 | return exported 312 | } 313 | const setFiles: ReplStore['setFiles'] = async ( 314 | newFiles, 315 | mainFile = store.mainFile, 316 | ) => { 317 | const files: Record = Object.create(null) 318 | 319 | mainFile = addSrcPrefix(mainFile) 320 | if (!newFiles[mainFile]) { 321 | setFile(files, mainFile, template.value.welcomeSFC || welcomeSFCCode) 322 | } 323 | for (const [filename, file] of Object.entries(newFiles)) { 324 | setFile(files, filename, file) 325 | } 326 | 327 | const errors = [] 328 | for (const file of Object.values(files)) { 329 | errors.push(...(await compileFile(store, file))) 330 | } 331 | 332 | store.mainFile = mainFile 333 | store.files = files 334 | store.errors = errors 335 | applyBuiltinImportMap() 336 | setActive(store.mainFile) 337 | } 338 | const setDefaultFile = (): void => { 339 | setFile( 340 | files.value, 341 | mainFile.value, 342 | template.value.welcomeSFC || welcomeSFCCode, 343 | ) 344 | } 345 | 346 | if (serializedState) { 347 | deserialize(serializedState, false) 348 | } else { 349 | setDefaultFile() 350 | } 351 | if (!files.value[mainFile.value]) { 352 | mainFile.value = Object.keys(files.value)[0] 353 | } 354 | activeFilename ||= ref(mainFile.value) 355 | const activeFile = computed(() => files.value[activeFilename.value]) 356 | 357 | applyBuiltinImportMap() 358 | 359 | const store: ReplStore = reactive({ 360 | files, 361 | activeFile, 362 | activeFilename, 363 | mainFile, 364 | template, 365 | builtinImportMap, 366 | 367 | errors, 368 | showOutput, 369 | outputMode, 370 | sfcOptions, 371 | ssrOutput: { html: '', context: '' }, 372 | compiler, 373 | loading, 374 | vueVersion, 375 | 376 | locale, 377 | typescriptVersion, 378 | dependencyVersion, 379 | reloadLanguageTools, 380 | 381 | init, 382 | setActive, 383 | addFile, 384 | deleteFile, 385 | renameFile, 386 | getImportMap, 387 | setImportMap, 388 | getTsConfig, 389 | serialize, 390 | deserialize, 391 | getFiles, 392 | setFiles, 393 | }) 394 | return store 395 | } 396 | 397 | const tsconfig = { 398 | compilerOptions: { 399 | allowJs: true, 400 | checkJs: true, 401 | jsx: 'Preserve', 402 | target: 'ESNext', 403 | module: 'ESNext', 404 | moduleResolution: 'Bundler', 405 | allowImportingTsExtensions: true, 406 | }, 407 | vueCompilerOptions: { 408 | target: 3.4, 409 | }, 410 | } 411 | 412 | export interface SFCOptions { 413 | script?: Partial 414 | style?: Partial 415 | template?: Partial 416 | } 417 | 418 | export type StoreState = ToRefs<{ 419 | files: Record 420 | activeFilename: string 421 | mainFile: string 422 | template: { 423 | welcomeSFC?: string 424 | newSFC?: string 425 | } 426 | builtinImportMap: ImportMap 427 | 428 | // output 429 | errors: (string | Error)[] 430 | showOutput: boolean 431 | outputMode: OutputModes 432 | sfcOptions: SFCOptions 433 | ssrOutput: { 434 | html: string 435 | context: unknown 436 | } 437 | /** `@vue/compiler-sfc` */ 438 | compiler: typeof defaultCompiler 439 | /* only apply for compiler-sfc */ 440 | vueVersion: string | null 441 | 442 | // volar-related 443 | locale: string | undefined 444 | typescriptVersion: string 445 | /** \{ dependencyName: version \} */ 446 | dependencyVersion: Record 447 | reloadLanguageTools?: (() => void) | undefined 448 | }> 449 | 450 | export interface ReplStore extends UnwrapRef { 451 | activeFile: File 452 | /** Loading compiler */ 453 | loading: boolean 454 | init(): void 455 | setActive(filename: string): void 456 | addFile(filename: string | File): void 457 | deleteFile(filename: string): void 458 | renameFile(oldFilename: string, newFilename: string): void 459 | getImportMap(): ImportMap 460 | setImportMap(map: ImportMap, merge?: boolean): void 461 | getTsConfig(): Record 462 | serialize(): string 463 | /** 464 | * Deserializes the given string to restore the REPL store state. 465 | * @param serializedState - The serialized state string. 466 | * @param checkBuiltinImportMap - Whether to check the built-in import map. Default to true 467 | */ 468 | deserialize(serializedState: string, checkBuiltinImportMap?: boolean): void 469 | getFiles(): Record 470 | setFiles(newFiles: Record, mainFile?: string): Promise 471 | } 472 | 473 | export type Store = Pick< 474 | ReplStore, 475 | | 'files' 476 | | 'activeFile' 477 | | 'mainFile' 478 | | 'errors' 479 | | 'showOutput' 480 | | 'outputMode' 481 | | 'sfcOptions' 482 | | 'ssrOutput' 483 | | 'compiler' 484 | | 'vueVersion' 485 | | 'locale' 486 | | 'typescriptVersion' 487 | | 'dependencyVersion' 488 | | 'reloadLanguageTools' 489 | | 'init' 490 | | 'setActive' 491 | | 'addFile' 492 | | 'deleteFile' 493 | | 'renameFile' 494 | | 'getImportMap' 495 | | 'getTsConfig' 496 | > 497 | 498 | export class File { 499 | compiled = { 500 | js: '', 501 | css: '', 502 | ssr: '', 503 | clientMap: '', 504 | ssrMap: '', 505 | } 506 | editorViewState: editor.ICodeEditorViewState | null = null 507 | 508 | constructor( 509 | public filename: string, 510 | public code = '', 511 | public hidden = false, 512 | ) {} 513 | 514 | get language() { 515 | if (this.filename.endsWith('.vue')) { 516 | return 'vue' 517 | } 518 | if (this.filename.endsWith('.html')) { 519 | return 'html' 520 | } 521 | if (this.filename.endsWith('.css')) { 522 | return 'css' 523 | } 524 | if (this.filename.endsWith('.ts')) { 525 | return 'typescript' 526 | } 527 | return 'javascript' 528 | } 529 | } 530 | 531 | function addSrcPrefix(file: string) { 532 | return file === importMapFile || 533 | file === tsconfigFile || 534 | file.startsWith('src/') 535 | ? file 536 | : `src/${file}` 537 | } 538 | 539 | export function stripSrcPrefix(file: string) { 540 | return file.replace(/^src\//, '') 541 | } 542 | 543 | function fixURL(url: string) { 544 | return url.replace('https://sfc.vuejs', 'https://play.vuejs') 545 | } 546 | 547 | function setFile( 548 | files: Record, 549 | filename: string, 550 | content: string, 551 | ) { 552 | const normalized = addSrcPrefix(filename) 553 | files[normalized] = new File(normalized, content) 554 | } 555 | -------------------------------------------------------------------------------- /src/template/new-sfc.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /src/template/welcome.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /src/transform.ts: -------------------------------------------------------------------------------- 1 | import type { File, Store } from './store' 2 | import type { 3 | BindingMetadata, 4 | CompilerOptions, 5 | SFCDescriptor, 6 | } from 'vue/compiler-sfc' 7 | import { type Transform, transform } from 'sucrase' 8 | import hashId from 'hash-sum' 9 | import { getSourceMap, toVisualizer, trimAnalyzedBindings } from './sourcemap' 10 | 11 | export const COMP_IDENTIFIER = `__sfc__` 12 | 13 | const REGEX_JS = /\.[jt]sx?$/ 14 | function testTs(filename: string | undefined | null) { 15 | return !!(filename && /(\.|\b)tsx?$/.test(filename)) 16 | } 17 | function testJsx(filename: string | undefined | null) { 18 | return !!(filename && /(\.|\b)[jt]sx$/.test(filename)) 19 | } 20 | 21 | function transformTS(src: string, isJSX?: boolean) { 22 | return transform(src, { 23 | transforms: ['typescript', ...(isJSX ? (['jsx'] as Transform[]) : [])], 24 | jsxRuntime: 'preserve', 25 | }).code 26 | } 27 | 28 | export async function compileFile( 29 | store: Store, 30 | { filename, code, compiled }: File, 31 | ): Promise<(string | Error)[]> { 32 | if (!code.trim()) { 33 | return [] 34 | } 35 | 36 | if (filename.endsWith('.css')) { 37 | compiled.css = code 38 | return [] 39 | } 40 | 41 | if (REGEX_JS.test(filename)) { 42 | const isJSX = testJsx(filename) 43 | if (testTs(filename)) { 44 | code = transformTS(code, isJSX) 45 | } 46 | if (isJSX) { 47 | code = await import('./jsx').then(({ transformJSX }) => 48 | transformJSX(code), 49 | ) 50 | } 51 | compiled.js = compiled.ssr = code 52 | return [] 53 | } 54 | 55 | if (filename.endsWith('.json')) { 56 | let parsed 57 | try { 58 | parsed = JSON.parse(code) 59 | } catch (err: any) { 60 | console.error(`Error parsing ${filename}`, err.message) 61 | return [err.message] 62 | } 63 | compiled.js = compiled.ssr = `export default ${JSON.stringify(parsed)}` 64 | return [] 65 | } 66 | 67 | if (!filename.endsWith('.vue')) { 68 | return [] 69 | } 70 | 71 | const id = hashId(filename) 72 | const { errors, descriptor } = store.compiler.parse(code, { 73 | filename, 74 | sourceMap: true, 75 | templateParseOptions: store.sfcOptions?.template?.compilerOptions, 76 | }) 77 | if (errors.length) { 78 | return errors 79 | } 80 | 81 | const styleLangs = descriptor.styles.map((s) => s.lang).filter(Boolean) 82 | const templateLang = descriptor.template?.lang 83 | if (styleLangs.length && templateLang) { 84 | return [ 85 | `lang="${styleLangs.join( 86 | ',', 87 | )}" pre-processors for