first commit

This commit is contained in:
sunlei 2026-05-08 14:30:56 +08:00
commit 51acbd50db
57 changed files with 12896 additions and 0 deletions

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# style: add trailing comma
497f07527b162f42123ead110031f265981f4d4d

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules
.DS_Store
dist
TODOs.md
# jetbrains files
.idea

1
.node-version Normal file
View File

@ -0,0 +1 @@
lts/*

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
dist
CHANGELOG.md
pnpm-lock.yaml

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

905
CHANGELOG.md Normal file
View File

@ -0,0 +1,905 @@
## [4.7.2](https://github.com/vuejs/repl/compare/v4.7.1...v4.7.2) (2026-04-13)
### Bug Fixes
* resolve CDN metadata directory entries correctly ([e5c3202](https://github.com/vuejs/repl/commit/e5c320277db488dafc8b352f5ad6849e3083ccf8))
### Features
* add multiRoot marker for template-only vapor component ([#385](https://github.com/vuejs/repl/issues/385)) ([a44a715](https://github.com/vuejs/repl/commit/a44a715d72ff7a0d293f016a906580362c616533))
* all cdn resources can be replaced ([#325](https://github.com/vuejs/repl/issues/325)) ([82d5a98](https://github.com/vuejs/repl/commit/82d5a982dc6af634d394edbb696f0834882d7b19))
* **repl:** `getEditorInstance`, `getMonacoEditor` (expose) ([#364](https://github.com/vuejs/repl/issues/364)) ([7ac4b95](https://github.com/vuejs/repl/commit/7ac4b9503e4cbcab6d8c36f7c8a1898cda1cb6ef))
## [4.7.1](https://github.com/vuejs/repl/compare/v4.7.0...v4.7.1) (2025-12-04)
### Bug Fixes
* recompile vue sfc files when compiler changes ([#374](https://github.com/vuejs/repl/issues/374)) ([cd22490](https://github.com/vuejs/repl/commit/cd224909c0514d5ef076b4135b538632824835fe))
### Features
* update language tools to 3.0.7 ([29db88e](https://github.com/vuejs/repl/commit/29db88e3d79fc707ee8bc30b2affb30905097c24))
* update language tools to 3.0.8 ([8102654](https://github.com/vuejs/repl/commit/810265402e7c820af66853028b25e58660488aff))
# [4.7.0](https://github.com/vuejs/repl/compare/v4.6.3...v4.7.0) (2025-09-04)
### Bug Fixes
* add support for custom elements for CompileScript ([#354](https://github.com/vuejs/repl/issues/354)) ([c393b60](https://github.com/vuejs/repl/commit/c393b60cfe5921ca32eab16b2b0ab0f64cfd724f))
* only handle `./` prefix modules for ExportNamedDeclaration ([#327](https://github.com/vuejs/repl/issues/327)) ([8cc908a](https://github.com/vuejs/repl/commit/8cc908a519bf70a897fb91a9fbb8584fd98bf544))
### Features
* update language tools to 3.0 ([#360](https://github.com/vuejs/repl/issues/360)) ([a34f630](https://github.com/vuejs/repl/commit/a34f6306b0fc5817ce4d4f0e63ef58fa6ac102ee))
## [4.6.3](https://github.com/vuejs/repl/compare/v4.6.2...v4.6.3) (2025-08-05)
## [4.6.2](https://github.com/vuejs/repl/compare/v4.6.1...v4.6.2) (2025-07-16)
### Bug Fixes
* handle Vue 3.6+ vapor runtime imports correctly ([#357](https://github.com/vuejs/repl/issues/357)) ([c4bac9c](https://github.com/vuejs/repl/commit/c4bac9c9d22813cdb343f850da516e84a0700e4d)), closes [#356](https://github.com/vuejs/repl/issues/356)
## [4.6.1](https://github.com/vuejs/repl/compare/v4.6.0...v4.6.1) (2025-06-13)
### Bug Fixes
* the line number offset in the DEV ([#344](https://github.com/vuejs/repl/issues/344)) ([cc292d3](https://github.com/vuejs/repl/commit/cc292d3dba25ac872edf147e14b6c9bbf6a0c577))
# [4.6.0](https://github.com/vuejs/repl/compare/v4.5.1...v4.6.0) (2025-06-13)
### Features
* add support for viewing sourcemap ([#341](https://github.com/vuejs/repl/issues/341)) ([5714d5b](https://github.com/vuejs/repl/commit/5714d5b706d7c945ee5393bef545dcf70c189db3))
* show SSR output ([#343](https://github.com/vuejs/repl/issues/343)) ([52a193a](https://github.com/vuejs/repl/commit/52a193a8a658d0059ee8c30345ca10ff52af7c04))
## [4.5.1](https://github.com/vuejs/repl/compare/v4.5.0...v4.5.1) (2025-02-19)
### Bug Fixes
* apply builtin import map after deserialize ([#315](https://github.com/vuejs/repl/issues/315)) ([e62ddda](https://github.com/vuejs/repl/commit/e62ddda06fe3339a467f88b13b0271c2a5c7e96d))
* **split-pane:** rendering order comes from the store.show-output ([0bd4c17](https://github.com/vuejs/repl/commit/0bd4c17b6dd26d4e17387f50e089dd3ffefaf054))
* ts error ([a927083](https://github.com/vuejs/repl/commit/a927083734f1d4dae5be0d200aafd26e49ce82aa))
### Features
* add core entry for node usage ([#310](https://github.com/vuejs/repl/issues/310)) ([da105a4](https://github.com/vuejs/repl/commit/da105a42618899d214701a5fb6549719f73331bb))
* **editor:** scrollbar style for firefox ([#320](https://github.com/vuejs/repl/issues/320)) ([bbc740b](https://github.com/vuejs/repl/commit/bbc740bfa840dfb6c77824470f1ffdc4a6261e85))
* export `Sandbox` as standalone output component ([#309](https://github.com/vuejs/repl/issues/309)) ([b549715](https://github.com/vuejs/repl/commit/b5497152fefe8f190eca59755bedb27b2f3178f2))
* **store:** return `setImportMap` and add `merge` parameter ([9f53bd1](https://github.com/vuejs/repl/commit/9f53bd11aee1d75984e5597878e53bec4ae168e5))
* support cache selected typescript version ([#305](https://github.com/vuejs/repl/issues/305)) ([33ca3c0](https://github.com/vuejs/repl/commit/33ca3c0317aa0418c094ec8f9e6712d81fa11465))
* support vapor template-only component ([#322](https://github.com/vuejs/repl/issues/322)) ([9ae056b](https://github.com/vuejs/repl/commit/9ae056b701ff54446c5c1ec9f29444d5239e0931))
# [4.5.0](https://github.com/vuejs/repl/compare/v4.4.3...v4.5.0) (2025-02-03)
### Features
* pass on descriptor vapor flag when compiling template ([adaaceb](https://github.com/vuejs/repl/commit/adaaceb24984435ae02ab3eda071f10dba9e0362))
## [4.4.3](https://github.com/vuejs/repl/compare/v4.4.2...v4.4.3) (2025-01-02)
### Bug Fixes
* 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)
## [4.4.2](https://github.com/vuejs/repl/compare/v4.4.1...v4.4.2) (2024-09-16)
### Bug Fixes
* output toggle button ([#279](https://github.com/vuejs/repl/issues/279)) ([93051f3](https://github.com/vuejs/repl/commit/93051f35a232b53d01dd0a40623cab5b11baa3ee))
* upgrade vue language tools ([ec393cf](https://github.com/vuejs/repl/commit/ec393cfe4b8b8008976e4fd2017bd112d98fa599))
## [4.4.1](https://github.com/vuejs/repl/compare/v4.4.0...v4.4.1) (2024-09-08)
### Bug Fixes
* cancel creating new file ([#281](https://github.com/vuejs/repl/issues/281)) ([7467f38](https://github.com/vuejs/repl/commit/7467f38f65e4b05dacc389644a8001b24f86fcdb))
* type error ([6653d0e](https://github.com/vuejs/repl/commit/6653d0e4b0b30eeee4bfe0c0c92bf00a84c0c753))
### Features
* add `autoSave` toggle button ([#283](https://github.com/vuejs/repl/issues/283)) ([83d8e48](https://github.com/vuejs/repl/commit/83d8e487de724261cf709c5648cc2512b4c33732))
* export `languageToolsVersion` ([5a92a92](https://github.com/vuejs/repl/commit/5a92a9259da01da4aa30b09ed6dcedfb4503c71d))
# [4.4.0](https://github.com/vuejs/repl/compare/v4.3.1...v4.4.0) (2024-09-07)
### Bug Fixes
* 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)
* no default value for object props ([d786626](https://github.com/vuejs/repl/commit/d78662652ec18f4610facc8f3e2a50f38c01f93f))
* rollback & pin volar version ([c6f58c7](https://github.com/vuejs/repl/commit/c6f58c7f4d04799a64f788251362f8349728bea4))
### Features
* add more customization options ([#274](https://github.com/vuejs/repl/issues/274)) ([c73b786](https://github.com/vuejs/repl/commit/c73b7868d73d3dad792c80a36507ce92234443d4))
* 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)
* reactivity `autoSave` option ([#266](https://github.com/vuejs/repl/issues/266)) ([d90082a](https://github.com/vuejs/repl/commit/d90082a2e44956e08fc64d296661b30ea4590506))
## [4.3.1](https://github.com/vuejs/repl/compare/v4.3.0...v4.3.1) (2024-07-03)
### Bug Fixes
* match source file ([4cf06b6](https://github.com/vuejs/repl/commit/4cf06b63fa1807ccfbf14c0ef1f1fa9b1f268717))
### Reverts
* refactor: replace assert with assert-plus ([e55baa4](https://github.com/vuejs/repl/commit/e55baa481f70c387f24cebfdfa7f143814fb0ce2))
# [4.3.0](https://github.com/vuejs/repl/compare/v4.2.1...v4.3.0) (2024-07-02)
### Bug Fixes
* alert if deserialization fails ([071b1d1](https://github.com/vuejs/repl/commit/071b1d1216fa3df2e14c9c6e5453cfe85eed4b79)), closes [#256](https://github.com/vuejs/repl/issues/256)
* move assert-plus to devDep ([dd9f1bb](https://github.com/vuejs/repl/commit/dd9f1bb74c17f19c25aa9b0e485366781094e818))
### Features
* show view size while dragging split pane ([#253](https://github.com/vuejs/repl/issues/253)) ([a6bbeea](https://github.com/vuejs/repl/commit/a6bbeea7b8ec7c1302ba08afa0c789ad198cc8e2))
* volar 2.x ([#225](https://github.com/vuejs/repl/issues/225)) ([47030b6](https://github.com/vuejs/repl/commit/47030b66a6f1811a24d8292f9f3aa5185f7e8e23))
## [4.2.1](https://github.com/vuejs/repl/compare/v4.2.0...v4.2.1) (2024-05-31)
### Bug Fixes
* 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)
# [4.2.0](https://github.com/vuejs/repl/compare/v4.1.2...v4.2.0) (2024-05-26)
### Bug Fixes
* refine dragging view area ([#246](https://github.com/vuejs/repl/issues/246)) ([df14639](https://github.com/vuejs/repl/commit/df14639b85edc0e153ba2fd6f29656785a0af3aa))
* specify unspported pre-processors lang ([#212](https://github.com/vuejs/repl/issues/212)) ([5cea974](https://github.com/vuejs/repl/commit/5cea974451ae23b82bea0c6270ad7ac726d831a4))
### Features
* `CodeMirror` support `autoSave` option ([#249](https://github.com/vuejs/repl/issues/249)) ([ae80c5b](https://github.com/vuejs/repl/commit/ae80c5b995ffa375013b665fd9a212c5607a1236))
* add `autoSave` option ([#247](https://github.com/vuejs/repl/issues/247)) ([d47eca5](https://github.com/vuejs/repl/commit/d47eca5926dcac798171fc216fcee2e21f275dd4))
* jsx for vue ([#248](https://github.com/vuejs/repl/issues/248)) ([d5b0d40](https://github.com/vuejs/repl/commit/d5b0d40ecc7f630b89e45ebe8472bc4e7563b3e2))
### Performance Improvements
* avoid parse repeatedly ([c6b7352](https://github.com/vuejs/repl/commit/c6b735298d5ab630cdc130aad7b8acaf7c9c41bb))
## [4.1.2](https://github.com/vuejs/repl/compare/v4.1.1...v4.1.2) (2024-04-26)
### Bug Fixes
* dynamic import ([#213](https://github.com/vuejs/repl/issues/213)) ([bb6f1fe](https://github.com/vuejs/repl/commit/bb6f1fe8599f1a11cd1b8aa40a630bea09d2b577))
* fix file rename breaking ([caace63](https://github.com/vuejs/repl/commit/caace639bde964323a253bdcd252a18869973a1d))
## [4.1.1](https://github.com/vuejs/repl/compare/v4.1.0...v4.1.1) (2024-02-14)
### Bug Fixes
* add vue import maps for default import map ([c74673f](https://github.com/vuejs/repl/commit/c74673fb55d2232de55562e62a818142681bdc8b))
* reload preview style error after switching theme ([#214](https://github.com/vuejs/repl/issues/214)) ([bc4c76c](https://github.com/vuejs/repl/commit/bc4c76c3f143b5edc3546d80002cb813704b8351))
# [4.1.0](https://github.com/vuejs/repl/compare/v4.0.2...v4.1.0) (2024-02-11)
### Features
* add `previewTheme` prop ([c830fc4](https://github.com/vuejs/repl/commit/c830fc434a1781523af332d289957bc485f51a0b))
## [4.0.2](https://github.com/vuejs/repl/compare/v4.0.1...v4.0.2) (2024-02-10)
### Bug Fixes
* respect vue version at initialization ([ef22052](https://github.com/vuejs/repl/commit/ef22052055590dbbe6e85e26ce368938b0c93266))
## [4.0.1](https://github.com/vuejs/repl/compare/v4.0.0...v4.0.1) (2024-02-10)
### Bug Fixes
* save version only when serialize ([d3ee13d](https://github.com/vuejs/repl/commit/d3ee13ded3c5a162bf990ef83cb9a38991792170))
### Features
* register language configuration ([3ad7035](https://github.com/vuejs/repl/commit/3ad7035e26cb02626b06e58f53e12ffb5443a5fc))
# [4.0.0](https://github.com/vuejs/repl/compare/v4.0.0-beta.0...v4.0.0) (2024-02-10)
### Features
* expose loading status ([eee6bb3](https://github.com/vuejs/repl/commit/eee6bb38ddecbe8bf7ba3ab77d6a549e654b6313))
* save vue version ([08b4492](https://github.com/vuejs/repl/commit/08b4492fe883bdd4bbe7fd972cd3fbbd8f6416cf))
# [4.0.0-beta.0](https://github.com/vuejs/repl/compare/v4.0.0-alpha.1...v4.0.0-beta.0) (2024-02-10)
### Bug Fixes
* pass readonly in code mirror editor ([1100158](https://github.com/vuejs/repl/commit/1100158aec97dae9cf47ac04ff2bb9ec00d05e58))
# [4.0.0-alpha.1](https://github.com/vuejs/repl/compare/v4.0.0-alpha.0...v4.0.0-alpha.1) (2024-01-24)
### Bug Fixes
* don't re-create import map file ([9e6f078](https://github.com/vuejs/repl/commit/9e6f078206883821d9bc618a194cf50333f38d3d))
# [4.0.0-alpha.0](https://github.com/vuejs/repl/compare/v3.3.0...v4.0.0-alpha.0) (2024-01-21)
### Bug Fixes
* add corresponding black theme background ([#206](https://github.com/vuejs/repl/issues/206)) ([3921c85](https://github.com/vuejs/repl/commit/3921c85f90a40a871838f9740fa3588e2cfa4758))
* don't overwrite `a` tag without href ([#209](https://github.com/vuejs/repl/issues/209)) ([c7fcf38](https://github.com/vuejs/repl/commit/c7fcf381f195ffce6284cc92c26f8a90e09b484a))
* don't show tsconfig if not present ([ca548b2](https://github.com/vuejs/repl/commit/ca548b240addd4f58851a545622684a00eb09a0a))
### Features
* export `package.json` ([79a22de](https://github.com/vuejs/repl/commit/79a22deb84aa50e31cf6506a67e93f69291fb82f))
# [3.3.0](https://github.com/vuejs/repl/compare/v3.2.0...v3.3.0) (2024-01-11)
### Bug Fixes
* default to white color on dark theme ([#202](https://github.com/vuejs/repl/issues/202)) ([481035a](https://github.com/vuejs/repl/commit/481035a443031e50de26b75c8e5b86fbb8ca96f2))
* serialize import maps ([e085e30](https://github.com/vuejs/repl/commit/e085e3041a228fe0ec076056e23e8f55258120ab)), closes [#204](https://github.com/vuejs/repl/issues/204)
### Features
* add theme as classname to sandbox ([#203](https://github.com/vuejs/repl/issues/203)) ([7e9dc0f](https://github.com/vuejs/repl/commit/7e9dc0f3b1f2c488664ccfa22cdf21ba19926158))
* apply theme to preview ([#200](https://github.com/vuejs/repl/issues/200)) ([7ae1061](https://github.com/vuejs/repl/commit/7ae106129274f13393808000fd25995d919ae0bd))
* mutable sfc options ([9e83b09](https://github.com/vuejs/repl/commit/9e83b09344ecc90ad0e28024a9b260fff97ffccd))
* support custom template ([#196](https://github.com/vuejs/repl/issues/196)) ([8038b49](https://github.com/vuejs/repl/commit/8038b49cc5fb76a7dc34acffcda5b3f55ff8aa11))
# [3.2.0](https://github.com/vuejs/repl/compare/v3.1.1...v3.2.0) (2024-01-03)
### Bug Fixes
* **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))
* **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))
* **Preview:** fix style loading delay ([#191](https://github.com/vuejs/repl/issues/191)) ([ece4414](https://github.com/vuejs/repl/commit/ece4414186fab8bb19290ed047e2a4ab665ae3ef))
### Features
* **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))
## [3.1.1](https://github.com/vuejs/repl/compare/v3.1.0...v3.1.1) (2024-01-02)
### Bug Fixes
* pass sfc template options to sfc parse ([d72dfdf](https://github.com/vuejs/repl/commit/d72dfdfd2e2670592c957616fcf4e694609912a0))
# [3.1.0](https://github.com/vuejs/repl/compare/v3.0.1...v3.1.0) (2023-12-21)
### Bug Fixes
* remove onigasm dep ([e7a73ac](https://github.com/vuejs/repl/commit/e7a73ac249ce44a6f4b661f6e6ff4842f3225d6b))
### Features
* use shikiji for more accurate highlight ([#190](https://github.com/vuejs/repl/issues/190)) ([e79aa1a](https://github.com/vuejs/repl/commit/e79aa1af8dc898d9170c5f33ee031ead61f32320))
## [3.0.1](https://github.com/vuejs/repl/compare/v3.0.0...v3.0.1) (2023-12-19)
### Bug Fixes
* ensure reuse AST in non-inline mode ([5e4c710](https://github.com/vuejs/repl/commit/5e4c7101e4b6cc27fb0810390b0ca0287a101149))
# [3.0.0](https://github.com/vuejs/repl/compare/v2.9.0...v3.0.0) (2023-11-30)
### Bug Fixes
* handle main file src prefix when setting files + avoid infinite loop due to state.error push ([743b731](https://github.com/vuejs/repl/commit/743b73121dbd63f164a013c8ba722d0a8bfe5ebd))
* make main repl styles lower specificity for easier override ([fbfaa44](https://github.com/vuejs/repl/commit/fbfaa4495c9bbf3ab936bec27445c52c9521b67e))
# [2.9.0](https://github.com/vuejs/repl/compare/v2.8.1...v2.9.0) (2023-11-30)
### Bug Fixes
* **types:** fix editor prop types ([828f202](https://github.com/vuejs/repl/commit/828f2027ff3986a029de3833f521525c7ac3e1d7))
### Features
* support custom element styles ([#173](https://github.com/vuejs/repl/issues/173)) ([812730d](https://github.com/vuejs/repl/commit/812730db62b6f1865cee90b67f9f593412a0dce6))
## [2.8.1](https://github.com/vuejs/repl/compare/v2.8.0...v2.8.1) (2023-11-28)
### Bug Fixes
* new sfc file template ([79643d7](https://github.com/vuejs/repl/commit/79643d71a5eabd7e7c9c092e8501cc07f9ee5136))
* worker plugins for vite 5 ([6e66250](https://github.com/vuejs/repl/commit/6e6625084d0c9ba8c24915ebd1060b7421e0de5b))
# [2.8.0](https://github.com/vuejs/repl/compare/v2.7.0...v2.8.0) (2023-11-19)
### Features
* add template for new file ([9a0be1d](https://github.com/vuejs/repl/commit/9a0be1df8c06ffdeab2985f9e9cd5f2cde1437fe))
# [2.7.0](https://github.com/vuejs/repl/compare/v2.6.3...v2.7.0) (2023-11-12)
### Features
* support toggling between dev/prod for Vue runtime ([8d3a2e6](https://github.com/vuejs/repl/commit/8d3a2e62358104663af48531467ac8eda4bafffa))
## [2.6.3](https://github.com/vuejs/repl/compare/v2.6.2...v2.6.3) (2023-11-03)
## [2.6.2](https://github.com/vuejs/repl/compare/v2.6.1...v2.6.2) (2023-11-01)
## [2.6.1](https://github.com/vuejs/repl/compare/v2.6.0...v2.6.1) (2023-10-26)
# [2.6.0](https://github.com/vuejs/repl/compare/v2.5.8...v2.6.0) (2023-10-26)
### Features
* 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))
* support custom display placeholder content ([#160](https://github.com/vuejs/repl/issues/160)) ([9ca27a1](https://github.com/vuejs/repl/commit/9ca27a12cf92b6ac6b7132a5c2ae667a13af4faa))
## [2.5.8](https://github.com/vuejs/repl/compare/v2.5.7...v2.5.8) (2023-08-10)
### Bug Fixes
* 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)
## [2.5.7](https://github.com/vuejs/repl/compare/v2.5.6...v2.5.7) (2023-08-08)
### Bug Fixes
* respect value from monaco editor props ([49fdc71](https://github.com/vuejs/repl/commit/49fdc7161ec91fed617043aca0b751858a10289e)), closes [#145](https://github.com/vuejs/repl/issues/145)
## [2.5.6](https://github.com/vuejs/repl/compare/v2.5.5...v2.5.6) (2023-07-31)
### Bug Fixes
* 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)
## [2.5.5](https://github.com/vuejs/repl/compare/v2.5.4...v2.5.5) (2023-07-09)
### Features
* expose dependency version ([aecfd8a](https://github.com/vuejs/repl/commit/aecfd8a92e6e7814dd6dbd5d5e94f71ef9fe5b1a))
## [2.5.4](https://github.com/vuejs/repl/compare/v2.5.3...v2.5.4) (2023-07-09)
### Bug Fixes
* replace NODE_ENV ([863f8f3](https://github.com/vuejs/repl/commit/863f8f39d36d25240388a9c5bc68eff0ea7e7856))
## [2.5.3](https://github.com/vuejs/repl/compare/v2.5.2...v2.5.3) (2023-07-08)
### Bug Fixes
* filename index auto-increment ([#133](https://github.com/vuejs/repl/issues/133)) ([4f55810](https://github.com/vuejs/repl/commit/4f55810f729fc61e22eafa7ea69afe79bcfe1cb6))
* make reloadLanguageTools optional ([5ab1a2d](https://github.com/vuejs/repl/commit/5ab1a2d149820ecb737c3bc97581a87f3adc83d7))
* use dev version of compiler ([#132](https://github.com/vuejs/repl/issues/132)) ([bfc3522](https://github.com/vuejs/repl/commit/bfc3522422926b0e3f18c1368111066cf268e206))
## [2.5.2](https://github.com/vuejs/repl/compare/v2.5.1...v2.5.2) (2023-07-06)
### Bug Fixes
* remove postinstall ([8167272](https://github.com/vuejs/repl/commit/816727232d0adac0c0955c1d6bee9d7be7f70d61))
## [2.5.1](https://github.com/vuejs/repl/compare/v2.5.0...v2.5.1) (2023-07-05)
### Bug Fixes
* cannot get ts module in prod env ([0cc220d](https://github.com/vuejs/repl/commit/0cc220d7efaafaaa3b8af07c34fd27c7825caa8b))
* cdn file models were accidentally disposed ([4301d86](https://github.com/vuejs/repl/commit/4301d8659ad84d9c6b66d63f1567cf31119eb9f4))
### Features
* use ts version option for ts lib dts acquire ([376fe3b](https://github.com/vuejs/repl/commit/376fe3ba2582fc128ccc4bbb2cd4b19666a5f1ec))
# [2.5.0](https://github.com/vuejs/repl/compare/v2.4.0...v2.5.0) (2023-07-05)
### Bug Fixes
* change message toggle position ([#120](https://github.com/vuejs/repl/issues/120)) ([3f7e090](https://github.com/vuejs/repl/commit/3f7e090c143ca0a40b0bff1d13f2db3e6964b17a))
### Features
* download TS dynamically ([#125](https://github.com/vuejs/repl/issues/125)) ([97f698f](https://github.com/vuejs/repl/commit/97f698f1f88690ab371e156ac2113955fdaa5fa8))
* expose TS localized languages ([a52dd14](https://github.com/vuejs/repl/commit/a52dd1468a6d6fb8ce4927a0a6771529f6a0f1ab))
* respect browser language ([f9fedcd](https://github.com/vuejs/repl/commit/f9fedcd1ca56a6965c9617a16d2fba834f8a44a1)), closes [#123](https://github.com/vuejs/repl/issues/123)
* upgrade volar ([d925ba3](https://github.com/vuejs/repl/commit/d925ba3c8a08966eab06eaf2720a7476415e7760))
# [2.4.0](https://github.com/vuejs/repl/compare/v2.3.0...v2.4.0) (2023-06-28)
### Bug Fixes
* multiple style tags ([#116](https://github.com/vuejs/repl/issues/116)) ([f0f5512](https://github.com/vuejs/repl/commit/f0f5512f49832321a6c96631025927635a834d9a))
* strip src prefix on dialog ([d29d1de](https://github.com/vuejs/repl/commit/d29d1de3f31930005dfc0b29f7d8a0435c0f94a4))
### Features
* add `reload` function ([#103](https://github.com/vuejs/repl/issues/103)) ([12ebcea](https://github.com/vuejs/repl/commit/12ebceab49c2a56702fffdfc01bb8b0bc3a708ca))
* add monaco light theme ([#121](https://github.com/vuejs/repl/issues/121)) ([ead9667](https://github.com/vuejs/repl/commit/ead9667a85c1f217dab5955ebd9a11992b3fbe65))
* memorize show error state ([#117](https://github.com/vuejs/repl/issues/117)) ([ab4b7cd](https://github.com/vuejs/repl/commit/ab4b7cd4d2c99b2750e29feaa0b3487f4bb8ed85))
# [2.3.0](https://github.com/vuejs/repl/compare/v2.2.0...v2.3.0) (2023-06-24)
### Features
* add tsconfig file ([#114](https://github.com/vuejs/repl/issues/114)) ([29f6af5](https://github.com/vuejs/repl/commit/29f6af5037826a6d37f77ba4cae748e7297152e3))
# [2.2.0](https://github.com/vuejs/repl/compare/v2.1.4...v2.2.0) (2023-06-24)
### Bug Fixes
* don't dispose in-memory files ([5f543da](https://github.com/vuejs/repl/commit/5f543da6815e30c76dc3a595b993a380043af54b))
* set page height ([ee814e7](https://github.com/vuejs/repl/commit/ee814e7313162f19d45dcff7a3ecabedfdf081d5)), closes [#112](https://github.com/vuejs/repl/issues/112)
### Features
* add default height for Repl component ([#109](https://github.com/vuejs/repl/issues/109)) ([d9673eb](https://github.com/vuejs/repl/commit/d9673eb4c7a3e20ca7d0f1e152d177c6c1f8956d))
* add error toggle ([#98](https://github.com/vuejs/repl/issues/98)) ([51819cc](https://github.com/vuejs/repl/commit/51819ccd3adcd40c189bd216f635ca6f62c4bc56))
## [2.1.4](https://github.com/vuejs/repl/compare/v2.1.3...v2.1.4) (2023-06-23)
### Bug Fixes
* default main file path ([c0184da](https://github.com/vuejs/repl/commit/c0184da073456706c44cc5e78e2d3283f4d3fe0f))
* **monaco:** enable `fixedOverflowWidgets` option ([#110](https://github.com/vuejs/repl/issues/110)) ([c7ddf12](https://github.com/vuejs/repl/commit/c7ddf12f25b23675c12c2760297c7d7d37668943))
## [2.1.3](https://github.com/vuejs/repl/compare/v2.1.2...v2.1.3) (2023-06-22)
### Bug Fixes
* actually fix editor type lol ([95ab2ab](https://github.com/vuejs/repl/commit/95ab2abc29b01a565ba7bc25ef293f1434db5ef6))
## [2.1.2](https://github.com/vuejs/repl/compare/v2.1.1...v2.1.2) (2023-06-22)
### Bug Fixes
* ensure imported editor can be passed as prop without type error ([414b0e6](https://github.com/vuejs/repl/commit/414b0e6cb729234ccb188332b22c184e44f162e0))
## [2.1.1](https://github.com/vuejs/repl/compare/v2.1.0...v2.1.1) (2023-06-22)
### Bug Fixes
* **types:** fix editor generated dts ([e5705df](https://github.com/vuejs/repl/commit/e5705df7d1ea4a44d9f6eba4443e28712631053a))
# [2.1.0](https://github.com/vuejs/repl/compare/v2.0.0...v2.1.0) (2023-06-22)
### Features
* support custom file go to difinition ([#102](https://github.com/vuejs/repl/issues/102)) ([519b0cc](https://github.com/vuejs/repl/commit/519b0cc079dccdb08ed00f1b5d2fb0c965fbab03))
# [2.0.0](https://github.com/vuejs/repl/compare/v1.5.0...v2.0.0) (2023-06-22)
### Bug Fixes
* disable pug and script setup codeLens ([1c6e646](https://github.com/vuejs/repl/commit/1c6e6464bea009b279fe43ed401e722230bf95bd))
* don't delete dts models ([d04a1ed](https://github.com/vuejs/repl/commit/d04a1ed01a9f5aaeafc6845d6165dcc9b45b7a04))
* f@ck ([0aa0a24](https://github.com/vuejs/repl/commit/0aa0a24e9c196d846827623b48b5eace0b8498a0))
* larger font size ([#100](https://github.com/vuejs/repl/issues/100)) ([a1a3fe1](https://github.com/vuejs/repl/commit/a1a3fe1294cf0daa89e30902d607addbdd525b6e))
* make monaco editor works normally ([f538199](https://github.com/vuejs/repl/commit/f538199e5cd99c380b3b5468cc660d47a6910783))
* minor fix ([aacba06](https://github.com/vuejs/repl/commit/aacba0673414b759377e3cbfa764bff82d64f857))
* split monaco out ([fd0b06a](https://github.com/vuejs/repl/commit/fd0b06af78e25632ee4fd6525ae5c90ef2c51f0b))
* styles ([6aee9cf](https://github.com/vuejs/repl/commit/6aee9cfa7df19de0f60f0d9fe2ccbd2291998cbb))
* update exports and types ([e4988b0](https://github.com/vuejs/repl/commit/e4988b0a9ddbc9888e481fd8f1c000b1756a5ba5))
* use monaco-volar ([9846c8e](https://github.com/vuejs/repl/commit/9846c8e67720c2d7402d52bc1c2106a9a1b28c08))
* use worker ([e1e80b9](https://github.com/vuejs/repl/commit/e1e80b9e40805bd541ccd80d48b01228378bf2c7))
### Features
* add ls ([a7bffd6](https://github.com/vuejs/repl/commit/a7bffd64c43e8ff375f1e59b62c78ca13969c723))
* complete provideDefinition ([2035fc4](https://github.com/vuejs/repl/commit/2035fc49977e5c2f5e8c57fe80c0bb53bb85df70))
* completed provideCompletionItems ([81ef510](https://github.com/vuejs/repl/commit/81ef51028f9188fcf598e78e45836f09b9f191ae))
* completed provideHover ([3922239](https://github.com/vuejs/repl/commit/39222398316adfda107f2b00d2cc27523f6cef52))
* completed provideSignatureHelp ([c599f2b](https://github.com/vuejs/repl/commit/c599f2bdf136d0d02e0c2f50198d8f71f0e858f2))
* completed setModelMarkers ([86079ad](https://github.com/vuejs/repl/commit/86079ad2030559052cef8b32e0d4a301a62b5f65))
* implemented provideCodeActions, resolveCodeAction ([1d37f55](https://github.com/vuejs/repl/commit/1d37f55eacb4124171813d1db777496aa1bddeb3))
* implemented provideCodeLenses, resolveCodeLens ([bddb65b](https://github.com/vuejs/repl/commit/bddb65b19053f069de3a3366666e3db1fd6bd6b1))
* implemented provideDeclaration ([8e437f0](https://github.com/vuejs/repl/commit/8e437f0e8796ba3b8d7a19478fc1b9bcde6f2e43))
* implemented provideDocumentColors, provideColorPresentations ([ddde114](https://github.com/vuejs/repl/commit/ddde1140945ac819765d93c7e33b07046fca55a6))
* implemented provideDocumentFormattingEdits ([73f046d](https://github.com/vuejs/repl/commit/73f046d80cd17c9dea7051f6d488e3b757366ad9))
* implemented provideDocumentHighlights ([f782496](https://github.com/vuejs/repl/commit/f7824960cb34f4ee2b58ad37a2af1a9364367b7a))
* implemented provideDocumentRangeFormattingEdits ([0e64a81](https://github.com/vuejs/repl/commit/0e64a81a57e2e973d95bcf3bba03c43f48abc508))
* implemented provideDocumentSymbols ([1cda6c6](https://github.com/vuejs/repl/commit/1cda6c638223c3d26f1b07deb24c6ba7b699014b))
* implemented provideFoldingRanges ([dd083d3](https://github.com/vuejs/repl/commit/dd083d3b246b5db8e7a158585e0fc912c9dd03ce))
* implemented provideImplementation ([49d8d7e](https://github.com/vuejs/repl/commit/49d8d7e52b34e94956a1e93bd7c98452d96831dd))
* implemented provideInlayHints ([61d18c9](https://github.com/vuejs/repl/commit/61d18c97d9ea309a48ff50188661bcbfe97312a4))
* implemented provideLinkedEditingRanges ([1046ac8](https://github.com/vuejs/repl/commit/1046ac8a29045b7a326002cb3fec9387675d9b33))
* implemented provideLinks ([3bc4063](https://github.com/vuejs/repl/commit/3bc406352ac7ce697e7d6c4b83f7ff737adede08))
* implemented provideOnTypeFormattingEdits ([f50e1c9](https://github.com/vuejs/repl/commit/f50e1c9c1ef7ed0ea02d51cdaaa5802f69911c7f))
* implemented provideReferences ([6b05d17](https://github.com/vuejs/repl/commit/6b05d1726d3e06c36008b847a55978e1e7b99843))
* implemented provideRenameEdits ([5ca6318](https://github.com/vuejs/repl/commit/5ca63181a2beebcc180282cd70f6732e572fd51d))
* implemented provideSelectionRanges ([2037d73](https://github.com/vuejs/repl/commit/2037d73ded9944b10a4379d369aeec06a304df97))
* implemented provideTypeDefinition ([b80bb0d](https://github.com/vuejs/repl/commit/b80bb0da90481cbebb214abf4a333daa7c4a42e9))
* implemented resolveCompletionItem ([3ef6ffd](https://github.com/vuejs/repl/commit/3ef6ffdf58a66f74a8903b7479b43195b13aeb69))
* **monaco:** support to keep selection and cursor position ([#99](https://github.com/vuejs/repl/issues/99)) ([db8c1bd](https://github.com/vuejs/repl/commit/db8c1bdd13ac44c15336795387aa8e7a449dfd74))
* pass vue dts module version from store ([2a0dfc0](https://github.com/vuejs/repl/commit/2a0dfc011a547d61523e5f64d882e5ed940bbb30))
* support auto close tag / auto .value ([f765d54](https://github.com/vuejs/repl/commit/f765d54a69ef6aca5586873d19cf3f666adeb0c5))
* support omitting ts/js extensions ([130a137](https://github.com/vuejs/repl/commit/130a137dd9640ea912e68d27e390dd03664e2699))
* upgrade volar ([0783d93](https://github.com/vuejs/repl/commit/0783d93fdd310e92ae3b2e22b0dc6ea78f93beaf))
* upgrade volar ([0aac8d1](https://github.com/vuejs/repl/commit/0aac8d11632e0963e0ae3de4e02cf47a16deec60))
* use monaco ([e833cf1](https://github.com/vuejs/repl/commit/e833cf14d447063654a02a83ba12fd23c8619c77))
### Performance Improvements
* cache `ts.ScriptSnapshot.fromString` ([6f34b78](https://github.com/vuejs/repl/commit/6f34b78d7d637d8fc238ca68c8beb87884f110b0))
* preset failed node_modules paths to speed up dts acquisition ([e93f049](https://github.com/vuejs/repl/commit/e93f0499719595aad61473b0b7819ece1b46818e))
# [1.5.0](https://github.com/vuejs/repl/compare/v1.4.1...v1.5.0) (2023-06-14)
### Bug Fixes
* add ts and json mode ([#37](https://github.com/vuejs/repl/issues/37)) ([0e467af](https://github.com/vuejs/repl/commit/0e467afbb52c759fdad0a2bfc263812b0df285c5))
* console logging for component instance proxies ([#62](https://github.com/vuejs/repl/issues/62)) ([bb0e143](https://github.com/vuejs/repl/commit/bb0e1430bff586b5505c3e9d11e8331359ee23d2))
* 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)
* Fix reason.message not existing case ([#54](https://github.com/vuejs/repl/issues/54)) ([2508030](https://github.com/vuejs/repl/commit/2508030241504d750a3226eb9a70fddd45d3299d))
* improve code gen when using cssVars in SSR ([#85](https://github.com/vuejs/repl/issues/85)) ([7e2bcc8](https://github.com/vuejs/repl/commit/7e2bcc864360e302d8b2a48e6904b7ec6c099f3f))
* improve code with optional chain ([#72](https://github.com/vuejs/repl/issues/72)) ([b8caeae](https://github.com/vuejs/repl/commit/b8caeaef0368609fa3c41e992304d21d526de08c))
* 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))
* process all files when dynamic import ([#60](https://github.com/vuejs/repl/issues/60)) ([7049ae0](https://github.com/vuejs/repl/commit/7049ae006f8687d2dafce38b7f54d7281410062a))
### Features
* add `sublime` keymap ([#45](https://github.com/vuejs/repl/issues/45)) ([29263d8](https://github.com/vuejs/repl/commit/29263d83d2d28e2ea3fc85c59de6d6d7ef92cca6))
* add file renaming ([#63](https://github.com/vuejs/repl/issues/63)) ([eb41c3a](https://github.com/vuejs/repl/commit/eb41c3a180eb720ba0959ba2da8064442f1b25e6))
* add search and replace ([#67](https://github.com/vuejs/repl/issues/67)) ([4ca3d94](https://github.com/vuejs/repl/commit/4ca3d94c98ed2029ccd61197780d45f348b2fcde))
* local JSON files ([#82](https://github.com/vuejs/repl/issues/82)) ([db076eb](https://github.com/vuejs/repl/commit/db076eb2b07e104ef460d7e2bd99769b5653e1a5))
* support for sandbox page customization ([#42](https://github.com/vuejs/repl/issues/42)) ([a22b969](https://github.com/vuejs/repl/commit/a22b96968894dcaf4fa096edf8a1dd7d7f903e5e))
## [1.4.1](https://github.com/vuejs/repl/compare/v1.4.0...v1.4.1) (2023-04-21)
# [1.4.0](https://github.com/vuejs/repl/compare/v1.3.6...v1.4.0) (2023-04-13)
### Features
* provide fs option to support 3.3 external type resolving ([f0e826a](https://github.com/vuejs/repl/commit/f0e826a1ff9eae7c008f2b92b4af35a518dd0c7f))
## [1.3.6](https://github.com/vuejs/repl/compare/v1.3.5...v1.3.6) (2023-04-13)
### Bug Fixes
* **types:** make sfc options partial ([9916f28](https://github.com/vuejs/repl/commit/9916f2862b327891604f3282fedf626759694e2c))
## [1.3.5](https://github.com/vuejs/repl/compare/v1.3.4...v1.3.5) (2023-04-06)
### Bug Fixes
* avoid including vue in import map if using default URLs ([37ce32b](https://github.com/vuejs/repl/commit/37ce32b107864332eeebbc406a817d78ae8d982a))
## [1.3.4](https://github.com/vuejs/repl/compare/v1.3.3...v1.3.4) (2023-04-06)
### Bug Fixes
* fix legacy domain in import maps ([7e7c7f9](https://github.com/vuejs/repl/commit/7e7c7f9dd62995f2f27448e72effb4c8fe879d72))
## [1.3.3](https://github.com/vuejs/repl/compare/v1.3.2...v1.3.3) (2023-03-17)
### Bug Fixes
* ignore polyfill error in Safari ([39f4ab1](https://github.com/vuejs/repl/commit/39f4ab1956af85383e6616eafec3efc616313d28))
## [1.3.2](https://github.com/vuejs/repl/compare/v1.3.1...v1.3.2) (2022-09-27)
### Bug Fixes
* 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)
## [1.3.1](https://github.com/vuejs/repl/compare/v1.3.0...v1.3.1) (2022-09-27)
# [1.3.0](https://github.com/vuejs/repl/compare/v1.2.4...v1.3.0) (2022-06-26)
## [1.2.4](https://github.com/vuejs/repl/compare/v1.2.3...v1.2.4) (2022-06-26)
### Bug Fixes
* compile error when no script ([#38](https://github.com/vuejs/repl/issues/38)) ([6b9b7bc](https://github.com/vuejs/repl/commit/6b9b7bc9ea3f89772eaf1807e3b7478d39f3ef9c))
### Features
* export Preview component ([#39](https://github.com/vuejs/repl/issues/39)) ([0b93cd6](https://github.com/vuejs/repl/commit/0b93cd66f5dc0beb2e44f271efa3868a155bff21))
* gzip serialized state ([#43](https://github.com/vuejs/repl/issues/43)) ([b12eb88](https://github.com/vuejs/repl/commit/b12eb885deb080246d372495f443fe543de1eb6d))
## [1.2.3](https://github.com/vuejs/repl/compare/v1.2.2...v1.2.3) (2022-05-25)
### Bug Fixes
* also reset import map when resetting to defauilt vue version ([5e89f07](https://github.com/vuejs/repl/commit/5e89f074ea5d33b301e079c5f4fe7860e1e5ca82))
* improve new filename logic ([9647666](https://github.com/vuejs/repl/commit/9647666554407b32f16b8b5581333542769a5ea0))
* warn versions that do not support in browser SSR ([01cb5b2](https://github.com/vuejs/repl/commit/01cb5b20cd15c3dcbe9f1b6d3dbc8797702924e9))
## [1.2.2](https://github.com/vuejs/repl/compare/v1.2.1...v1.2.2) (2022-05-25)
### Bug Fixes
* do not start compiling until sfc options are set ([b6f86d9](https://github.com/vuejs/repl/commit/b6f86d920d22d83fde3bb77b11e8f44fff1a244d))
## [1.2.1](https://github.com/vuejs/repl/compare/v1.2.0...v1.2.1) (2022-05-25)
### Bug Fixes
* fix html initialization in ssr mode ([152f2fa](https://github.com/vuejs/repl/commit/152f2fad88fa87fb617a8a69ff8f9f2c1b1eba33))
# [1.2.0](https://github.com/vuejs/repl/compare/v1.1.2...v1.2.0) (2022-05-25)
### Bug Fixes
* avoid using native crypto ([c22e216](https://github.com/vuejs/repl/commit/c22e216b1c6d8bbce3cbb4376d82ce15ce149433)), closes [#25](https://github.com/vuejs/repl/issues/25)
### Features
* **FileSelector:** add an increment counter for new files ([#36](https://github.com/vuejs/repl/issues/36)) ([63b8f22](https://github.com/vuejs/repl/commit/63b8f22a991984ce1ce6c56d14ae4f35f8b4a436))
* support ssr + hydration ([098aa89](https://github.com/vuejs/repl/commit/098aa8992ad860c8529fb285552c6c26e7518e9e))
## [1.1.2](https://github.com/vuejs/repl/compare/v1.1.1...v1.1.2) (2022-05-20)
### Bug Fixes
* apply TS transform to template when inine is disabled ([ec2ae81](https://github.com/vuejs/repl/commit/ec2ae811bd25da4be74b9df3bb8fcf9ba5d34cfb))
## [1.1.1](https://github.com/vuejs/repl/compare/v1.1.0...v1.1.1) (2022-05-17)
### Bug Fixes
* adding file using enter emits error ([#23](https://github.com/vuejs/repl/issues/23)) ([918de7f](https://github.com/vuejs/repl/commit/918de7f3646a24db083e54301d6ac5c3a970c0df))
# [1.1.0](https://github.com/vuejs/repl/compare/v1.0.1...v1.1.0) (2022-05-17)
## [1.0.1](https://github.com/vuejs/repl/compare/f8bb46f969860539e3105ff56d092f0184a70eba...v1.0.1) (2022-05-17)
### Bug Fixes
* also generate render function if inline mode is disabled ([9a325bb](https://github.com/vuejs/repl/commit/9a325bbf66b61403cd4df5ace31d0e7e1532fddf))
* avoid reloading the iframe when switching output tabs ([20bde55](https://github.com/vuejs/repl/commit/20bde550e481c0a9c9218f8a583eae7b27ca42d2))
* css double # ([#14](https://github.com/vuejs/repl/issues/14)) ([8bcf7f0](https://github.com/vuejs/repl/commit/8bcf7f0f22553214f7936863de3d9780272781b0))
* fix module instantiation order ([879f084](https://github.com/vuejs/repl/commit/879f08495c061afa11e058a3e059365fe09277c6))
* fix rewriteDefault TS edge case ([d277d7f](https://github.com/vuejs/repl/commit/d277d7f50113c45b8ae71afcda9aa369c64fba32))
* fix setFiles with multi files cross imports ([424e00d](https://github.com/vuejs/repl/commit/424e00d2ac50636b3a2a9739620435b156f1a94a))
* force app name ([18863af](https://github.com/vuejs/repl/commit/18863af803922f3966a80922db7c8a45a0cdd78d))
* small screen error msg covered code button ([#18](https://github.com/vuejs/repl/issues/18)) ([02da79d](https://github.com/vuejs/repl/commit/02da79d0a238b8777fcd95675c8c5dbd1b626fd4))
* toggler should be absolute ([f8bb46f](https://github.com/vuejs/repl/commit/f8bb46f969860539e3105ff56d092f0184a70eba))
* update import map when setting vue versions ([15cc696](https://github.com/vuejs/repl/commit/15cc696054b49fe5ea6879b9492b96cca611c945))
### Features
* add hidden file ([#17](https://github.com/vuejs/repl/issues/17)) ([35b6f1a](https://github.com/vuejs/repl/commit/35b6f1a38611e31b9adbe7540d789be144e33bdc))
* allow starting on a specific view ([#15](https://github.com/vuejs/repl/issues/15)) ([7e63511](https://github.com/vuejs/repl/commit/7e635110bb5e11e8103b66c5d347cf959be8bd55))
* export compileFile ([#13](https://github.com/vuejs/repl/issues/13)) ([60db549](https://github.com/vuejs/repl/commit/60db54905699e005d3117a693410c0cd50f154fe))
* file-selector add horizontal scroll ([#10](https://github.com/vuejs/repl/issues/10)) ([d0c961e](https://github.com/vuejs/repl/commit/d0c961e7b20939f0e028fd0cb89ce75123f32aa7))
* support passing in compiler-sfc options ([f6c7049](https://github.com/vuejs/repl/commit/f6c7049f9bc4a5e1dd3e1c1948ba2ecb43fad3c3))
* support ts in template expressions ([a1e9881](https://github.com/vuejs/repl/commit/a1e98814699c020a2d82c8c5aad664e99bd6ef52))
* vertical mode ([d59bb6c](https://github.com/vuejs/repl/commit/d59bb6cd0eb0e03fa548595f5c64b990cecd133e))

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

204
README.md Normal file
View File

@ -0,0 +1,204 @@
# @vue/repl
Vue SFC REPL as a Vue 3 component.
## Basic Usage
**Note: `@vue/repl` >= 2 now supports Monaco Editor, but also requires explicitly passing in the editor to be used for tree-shaking.**
```ts
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: {
exclude: ['@vue/repl'],
},
// ...
})
```
### With CodeMirror Editor
Basic editing experience with no intellisense. Lighter weight, fewer network requests, better for embedding use cases.
```vue
<script setup>
import { Repl } from '@vue/repl'
import CodeMirror from '@vue/repl/codemirror-editor'
// import '@vue/repl/style.css'
// ^ no longer needed after 3.0
</script>
<template>
<Repl :editor="CodeMirror" />
</template>
```
### With Monaco Editor
With Volar support, autocomplete, type inference, and semantic highlighting. Heavier bundle, loads dts files from CDN, better for standalone use cases.
```vue
<script setup>
import { Repl } from '@vue/repl'
import Monaco from '@vue/repl/monaco-editor'
// import '@vue/repl/style.css'
// ^ no longer needed after 3.0
</script>
<template>
<Repl :editor="Monaco" />
</template>
```
## Advanced Usage
Customize the behavior of the REPL by manually initializing the store.
See [v4 Migration Guide](https://github.com/vuejs/repl/releases/tag/v4.0.0)
```vue
<script setup>
import { watchEffect, ref } from 'vue'
import { Repl, useStore, useVueImportMap } from '@vue/repl'
import Monaco from '@vue/repl/monaco-editor'
// retrieve some configuration options from the URL
const query = new URLSearchParams(location.search)
const {
importMap: builtinImportMap,
vueVersion,
productionMode,
} = useVueImportMap({
// specify the default URL to import Vue runtime from in the sandbox
// default is the CDN link from jsdelivr.com with version matching Vue's version
// from peerDependency
runtimeDev: 'cdn link to vue.runtime.esm-browser.js',
runtimeProd: 'cdn link to vue.runtime.esm-browser.prod.js',
serverRenderer: 'cdn link to server-renderer.esm-browser.js',
})
const store = useStore(
{
// pre-set import map
builtinImportMap,
vueVersion,
// starts on the output pane (mobile only) if the URL has a showOutput query
showOutput: ref(query.has('showOutput')),
// starts on a different tab on the output pane if the URL has a outputMode query
// and default to the "preview" tab
outputMode: ref(query.get('outputMode') || 'preview'),
},
// initialize repl with previously serialized state
location.hash,
)
// persist state to URL hash
watchEffect(() => history.replaceState({}, '', store.serialize()))
// use a specific version of Vue
vueVersion.value = '3.2.8'
// production mode is enabled
productionMode.value = true
</script>
<template>
<Repl :store="store" :editor="Monaco" :showCompileOutput="true" />
</template>
```
Use only the Preview without the editor
```vue
<script setup>
import { ref } from 'vue'
import { Sandbox, useStore } from '@vue/repl'
// retrieve some configuration options from the URL
const query = new URLSearchParams(location.search)
const store = useStore(
{
// custom vue version
vueVersion: ref(query.get('vue')),
},
// initialize repl with previously serialized state
location.hash,
)
</script>
<template>
<Sandbox :store="store" />
</template>
```
<details>
<summary>Configuration options for resource links. (replace CDN resources)</summary>
```ts
export type ResourceLinkConfigs = {
/** URL for ES Module Shims. */
esModuleShims?: string
/** Function that generates the Vue compiler URL based on the version. */
vueCompilerUrl?: (version: string) => string
/** Function that generates the TypeScript library URL based on the version. */
typescriptLib?: (version: string) => string
/** [monaco] Function that generates a URL to fetch the latest version of a package. */
pkgLatestVersionUrl?: (pkgName: string) => string
/** [monaco] Function that generates a URL to browse a package directory. */
pkgDirUrl?: (pkgName: string, pkgVersion: string, pkgPath: string) => string
/** [monaco] Function that generates a URL to fetch the content of a file from a package. */
pkgFileTextUrl?: (
pkgName: string,
pkgVersion: string | undefined,
pkgPath: string,
) => string
}
```
**unpkg**
```ts
const store = useStore({
resourceLinks: ref({
esModuleShims:
'https://unpkg.com/es-module-shims@1.5.18/dist/es-module-shims.wasm.js',
vueCompilerUrl: (version) =>
`https://unpkg.com/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js`,
typescriptLib: (version) =>
`https://unpkg.com/typescript@${version}/lib/typescript.js`,
pkgLatestVersionUrl: (pkgName) =>
`https://unpkg.com/${pkgName}@latest/package.json`,
pkgDirUrl: (pkgName, pkgVersion, pkgPath) =>
`https://unpkg.com/${pkgName}@${pkgVersion}/${pkgPath}/?meta`,
pkgFileTextUrl: (pkgName, pkgVersion, pkgPath) =>
`https://unpkg.com/${pkgName}@${pkgVersion || 'latest'}/${pkgPath}`,
}),
})
```
**npmmirror**
```ts
const store = useStore({
resourceLinks: ref({
esModuleShims:
'https://registry.npmmirror.com/es-module-shims/1.5.18/files/dist/es-module-shims.wasm.js',
vueCompilerUrl: (version) =>
`https://registry.npmmirror.com/@vue/compiler-sfc/${version}/files/dist/compiler-sfc.esm-browser.js`,
typescriptLib: (version) =>
`https://registry.npmmirror.com/typescript/${version}/files/lib/typescript.js`,
pkgLatestVersionUrl: (pkgName) =>
`https://registry.npmmirror.com/${pkgName}/latest/files/package.json`,
pkgDirUrl: (pkgName, pkgVersion, pkgPath) =>
`https://registry.npmmirror.com/${pkgName}/${pkgVersion}/files/${pkgPath}/?meta`,
pkgFileTextUrl: (pkgName, pkgVersion, pkgPath) =>
`https://registry.npmmirror.com/${pkgName}/${pkgVersion || 'latest'}/files/${pkgPath}`,
}),
})
```
</details>

53
eslint.config.js Normal file
View File

@ -0,0 +1,53 @@
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginVue from 'eslint-plugin-vue'
export default tseslint.config(
{ ignores: ['**/node_modules', '**/dist'] },
eslint.configs.recommended,
tseslint.configs.base,
...pluginVue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: '@typescript-eslint/parser',
},
},
},
{
rules: {
'no-debugger': 'error',
'no-console': ['error', { allow: ['warn', 'error', 'info', 'clear'] }],
'no-unused-vars': 'off',
'no-undef': 'off',
'prefer-const': 'error',
'sort-imports': ['error', { ignoreDeclarationSort: true }],
'no-duplicate-imports': 'error',
// This rule enforces the preference for using '@ts-expect-error' comments in TypeScript
// code to indicate intentional type errors, improving code clarity and maintainability.
'@typescript-eslint/prefer-ts-expect-error': 'error',
// Enforce the use of 'import type' for importing types
'@typescript-eslint/consistent-type-imports': [
'error',
{
fixStyle: 'inline-type-imports',
disallowTypeAnnotations: false,
},
],
// Enforce the use of top-level import type qualifier when an import only has specifiers with inline type qualifiers
'@typescript-eslint/no-import-type-side-effects': 'error',
'vue/max-attributes-per-line': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/multi-word-component-names': 'off',
'vue/html-self-closing': [
'error',
{
html: { component: 'always', normal: 'always', void: 'any' },
math: 'always',
svg: 'always',
},
],
},
},
)

30
index-dist.html Normal file
View File

@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue SFC Playground</title>
<style>
body {
margin: 0;
}
#app {
height: 100vh;
height: 100dvh;
}
</style>
<script type="module">
import { createApp } from 'vue'
import { Repl } from './dist/vue-repl'
import Monaco from './dist/monaco-editor'
createApp(Repl, {
editor: Monaco,
}).mount('#app')
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>

23
index.html Normal file
View File

@ -0,0 +1,23 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue SFC Playground</title>
<style>
body {
margin: 0;
}
#app {
height: 100vh;
height: 100dvh;
}
</style>
<script type="module" src="./test/main.ts"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

123
package.json Normal file
View File

@ -0,0 +1,123 @@
{
"name": "@vue/repl",
"version": "4.7.2",
"description": "Vue component for editing Vue components",
"packageManager": "pnpm@10.14.0",
"type": "module",
"main": "dist/ssr-stub.js",
"module": "dist/vue-repl.js",
"files": [
"dist"
],
"types": "dist/vue-repl.d.ts",
"exports": {
".": {
"types": "./dist/vue-repl.d.ts",
"import": "./dist/vue-repl.js",
"require": "./dist/ssr-stub.js"
},
"./monaco-editor": {
"types": "./dist/monaco-editor.d.ts",
"import": "./dist/monaco-editor.js",
"require": null
},
"./codemirror-editor": {
"types": "./dist/codemirror-editor.d.ts",
"import": "./dist/codemirror-editor.js",
"require": null
},
"./core": {
"types": "./dist/core.d.ts",
"import": "./dist/core.js",
"require": null
},
"./package.json": "./package.json",
"./style.css": "./dist/vue-repl.css",
"./dist/style.css": "./dist/vue-repl.css"
},
"typesVersions": {
"*": {
"*": [
"./dist/*",
"./*"
]
}
},
"publishConfig": {
"tag": "latest"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"build-preview": "vite build -c vite.preview.config.ts",
"format": "prettier --write .",
"lint": "eslint .",
"typecheck": "vue-tsc --noEmit",
"release": "bumpp --all",
"version": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"prepublishOnly": "npm run build"
},
"simple-git-hooks": {
"pre-commit": "pnpm exec lint-staged --concurrent false"
},
"lint-staged": {
"*": [
"prettier --write --cache --ignore-unknown"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/repl.git"
},
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/repl/issues"
},
"homepage": "https://github.com/vuejs/repl#readme",
"devDependencies": {
"@babel/standalone": "^7.28.2",
"@babel/types": "^7.28.2",
"@eslint/js": "^9.32.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.29",
"@rollup/plugin-replace": "^6.0.2",
"@shikijs/monaco": "^4.0.2",
"@types/babel__standalone": "^7.1.9",
"@types/codemirror": "^5.60.16",
"@types/hash-sum": "^1.0.2",
"@types/node": "^24.2.0",
"@vitejs/plugin-vue": "^6.0.1",
"@volar/jsdelivr": "2.4.23",
"@volar/language-service": "~2.4.11",
"@volar/monaco": "2.4.23",
"@volar/typescript": "2.4.23",
"@vue/babel-plugin-jsx": "^2.0.1",
"@vue/language-core": "3.0.8",
"@vue/language-service": "3.0.8",
"@vue/typescript-plugin": "3.0.8",
"assert": "^2.1.0",
"bumpp": "^11.0.1",
"codemirror": "^5.65.18",
"conventional-changelog-cli": "^5.0.0",
"eslint": "^9.32.0",
"eslint-plugin-vue": "^10.4.0",
"fflate": "^0.8.2",
"hash-sum": "^2.0.0",
"lint-staged": "^16.1.4",
"monaco-editor-core": "^0.52.2",
"prettier": "^3.6.2",
"shiki": "^4.0.2",
"simple-git-hooks": "^2.13.1",
"source-map-js": "^1.2.1",
"sucrase": "^3.35.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.0",
"vite": "^8.0.8",
"vite-plugin-dts": "^4.5.4",
"vscode-uri": "^3.1.0",
"volar-service-typescript": "0.0.65",
"vue": "^3.5.18",
"vue-tsc": "3.0.8"
}
}

5145
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

128
src/Message.vue Normal file
View File

@ -0,0 +1,128 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { CompilerError } from 'vue/compiler-sfc'
const props = defineProps<{
err?: string | Error | false
warn?: string | Error
}>()
const dismissed = ref(false)
watch(
() => [props.err, props.warn],
() => {
dismissed.value = false
},
)
function formatMessage(err: string | Error): string {
if (typeof err === 'string') {
return err
} else {
let msg = err.message
const loc = (err as CompilerError).loc
if (loc && loc.start) {
msg = `(${loc.start.line}:${loc.start.column}) ` + msg
}
return msg
}
}
</script>
<template>
<Transition name="fade">
<div
v-if="!dismissed && (err || warn)"
class="msg"
:class="err ? 'err' : 'warn'"
>
<pre>{{ formatMessage(err || warn!) }}</pre>
<button class="dismiss" @click="dismissed = true"></button>
</div>
</Transition>
</template>
<style scoped>
.msg.err {
--color: #f56c6c;
--bg-color: #fef0f0;
}
.dark .msg.err {
--bg-color: #2b1d1d;
}
.msg.warn {
--color: #e6a23c;
--bg-color: #fdf6ec;
}
.dark .msg.warn {
--bg-color: #292218;
}
pre {
margin: 0;
padding: 12px 20px;
overflow: auto;
}
.msg {
position: absolute;
bottom: 0;
left: 8px;
right: 8px;
z-index: 20;
border: 2px solid transparent;
border-radius: 6px;
font-family: var(--font-code);
white-space: pre-wrap;
margin-bottom: 8px;
max-height: calc(100% - 300px);
min-height: 40px;
display: flex;
align-items: stretch;
color: var(--color);
border-color: var(--color);
background-color: var(--bg-color);
}
.dismiss {
position: absolute;
top: 2px;
right: 2px;
width: 18px;
height: 18px;
line-height: 18px;
border-radius: 9px;
text-align: center;
display: block;
font-size: 9px;
padding: 0;
color: var(--bg-color);
background-color: var(--color);
}
@media (max-width: 720px) {
.dismiss {
top: -9px;
right: -9px;
}
.msg {
bottom: 50px;
}
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.15s ease-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translate(0, 10px);
}
</style>

164
src/Repl.vue Normal file
View File

@ -0,0 +1,164 @@
<script setup lang="ts">
import SplitPane from './SplitPane.vue'
import Output from './output/Output.vue'
import { type Store, useStore } from './store'
import { computed, provide, toRefs, useTemplateRef } from 'vue'
import {
type EditorComponentType,
type EditorMethods,
injectKeyPreviewRef,
injectKeyProps,
} from './types'
import EditorContainer from './editor/EditorContainer.vue'
import type * as monaco from 'monaco-editor-core'
export interface Props {
theme?: 'dark' | 'light'
previewTheme?: boolean
editor: EditorComponentType
store?: Store
autoResize?: boolean
showCompileOutput?: boolean
showOpenSourceMap?: boolean
showImportMap?: boolean
showSsrOutput?: boolean
showTsConfig?: boolean
clearConsole?: boolean
layout?: 'horizontal' | 'vertical'
layoutReverse?: boolean
ssr?: boolean
previewOptions?: {
headHTML?: string
bodyHTML?: string
placeholderHTML?: string
customCode?: {
importCode?: string
useCode?: string
}
showRuntimeError?: boolean
showRuntimeWarning?: boolean
}
editorOptions?: {
showErrorText?: string | false
autoSaveText?: string | false
monacoOptions?: monaco.editor.IStandaloneEditorConstructionOptions
}
splitPaneOptions?: {
codeTogglerText?: string
outputTogglerText?: string
}
}
const autoSave = defineModel<boolean>({ default: true })
const props = withDefaults(defineProps<Props>(), {
theme: 'light',
previewTheme: false,
store: () => useStore(),
autoResize: true,
showCompileOutput: true,
showOpenSourceMap: false,
showImportMap: true,
showSsrOutput: false,
showTsConfig: true,
clearConsole: true,
layoutReverse: false,
ssr: false,
layout: 'horizontal',
previewOptions: () => ({}),
editorOptions: () => ({}),
splitPaneOptions: () => ({}),
})
if (!props.editor) {
throw new Error('The "editor" prop is now required.')
}
const outputRef = useTemplateRef('output')
const editorContainerRef = useTemplateRef('editorContainer')
props.store.init()
const editorSlotName = computed(() => (props.layoutReverse ? 'right' : 'left'))
const outputSlotName = computed(() => (props.layoutReverse ? 'left' : 'right'))
provide(injectKeyProps, {
...toRefs(props),
autoSave,
})
provide(
injectKeyPreviewRef,
computed(() => outputRef.value?.previewRef?.container ?? null),
)
/**
* Reload the preview iframe
*/
function reload() {
outputRef.value?.reload()
}
defineExpose({
reload,
getEditorInstance: (() =>
editorContainerRef.value?.getEditorIns()) as EditorMethods['getEditorIns'],
getMonacoEditor: () => editorContainerRef.value?.getMonacoEditor?.(),
})
</script>
<template>
<div class="vue-repl">
<SplitPane :layout="layout">
<template #[editorSlotName]>
<EditorContainer ref="editorContainer" :editor-component="editor" />
</template>
<template #[outputSlotName]>
<Output
ref="output"
:editor-component="editor"
:show-compile-output="props.showCompileOutput"
:show-open-source-map="props.showOpenSourceMap"
:show-ssr-output="props.showSsrOutput"
:ssr="!!props.ssr"
/>
</template>
</SplitPane>
</div>
</template>
<style>
.vue-repl {
--bg: #fff;
--bg-soft: #f8f8f8;
--border: #ddd;
--text-light: #888;
--font-code: Menlo, Monaco, Consolas, 'Courier New', monospace;
--color-branding: #42b883;
--color-branding-dark: #416f9c;
--header-height: 38px;
height: 100%;
margin: 0;
overflow: hidden;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: var(--bg-soft);
}
.dark .vue-repl {
--bg: #1a1a1a;
--bg-soft: #242424;
--border: #383838;
--text-light: #aaa;
--color-branding: #42d392;
--color-branding-dark: #89ddff;
}
.vue-repl button {
border: none;
outline: none;
cursor: pointer;
margin: 0;
background-color: transparent;
}
</style>

220
src/SplitPane.vue Normal file
View File

@ -0,0 +1,220 @@
<script setup lang="ts">
import { computed, inject, reactive, useTemplateRef } from 'vue'
import { injectKeyPreviewRef, injectKeyProps } from './types'
const props = defineProps<{ layout?: 'horizontal' | 'vertical' }>()
const isVertical = computed(() => props.layout === 'vertical')
const containerRef = useTemplateRef('container')
const previewRef = inject(injectKeyPreviewRef)!
// mobile only
const { store, layoutReverse, splitPaneOptions } = inject(injectKeyProps)!
const state = reactive({
dragging: false,
split: 50,
viewHeight: 0,
viewWidth: 0,
})
const boundSplit = computed(() => {
const { split } = state
return split < 20 ? 20 : split > 80 ? 80 : split
})
let startPosition = 0
let startSplit = 0
function dragStart(e: MouseEvent) {
state.dragging = true
startPosition = isVertical.value ? e.pageY : e.pageX
startSplit = boundSplit.value
changeViewSize()
}
function dragMove(e: MouseEvent) {
if (containerRef.value && state.dragging) {
const position = isVertical.value ? e.pageY : e.pageX
const totalSize = isVertical.value
? containerRef.value.offsetHeight
: containerRef.value.offsetWidth
const dp = position - startPosition
state.split = startSplit + +((dp / totalSize) * 100).toFixed(2)
changeViewSize()
}
}
function dragEnd() {
state.dragging = false
}
function changeViewSize() {
const el = previewRef.value
if (!el) return
state.viewHeight = el.offsetHeight
state.viewWidth = el.offsetWidth
}
</script>
<template>
<div
ref="container"
class="split-pane"
:class="{
dragging: state.dragging,
'show-output': store.showOutput,
reverse: layoutReverse,
vertical: isVertical,
}"
@mousemove="dragMove"
@mouseup="dragEnd"
@mouseleave="dragEnd"
>
<div
class="left"
:style="{ [isVertical ? 'height' : 'width']: boundSplit + '%' }"
>
<slot name="left" />
<div class="dragger" @mousedown.prevent="dragStart" />
</div>
<div
class="right"
:style="{ [isVertical ? 'height' : 'width']: 100 - boundSplit + '%' }"
>
<div v-show="state.dragging" class="view-size">
{{ `${state.viewWidth}px x ${state.viewHeight}px` }}
</div>
<slot name="right" />
</div>
<button class="toggler" @click="store.showOutput = !store.showOutput">
{{
store.showOutput
? splitPaneOptions?.codeTogglerText || '< Code'
: splitPaneOptions?.outputTogglerText || 'Output >'
}}
</button>
</div>
</template>
<style scoped>
.split-pane {
display: flex;
height: 100%;
position: relative;
}
.split-pane.dragging {
cursor: ew-resize;
}
.dragging .left,
.dragging .right {
pointer-events: none;
}
.left,
.right {
position: relative;
height: 100%;
}
.view-size {
position: absolute;
top: 40px;
left: 10px;
font-size: 12px;
color: var(--text-light);
z-index: 100;
}
.left {
border-right: 1px solid var(--border);
}
.dragger {
position: absolute;
z-index: 3;
top: 0;
bottom: 0;
right: -5px;
width: 10px;
cursor: ew-resize;
}
.toggler {
display: none;
z-index: 3;
font-family: var(--font-code);
color: var(--text-light);
position: absolute;
left: 50%;
bottom: 20px;
background-color: var(--bg);
padding: 8px 12px;
border-radius: 8px;
transform: translateX(-50%);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25);
}
.dark .toggler {
background-color: var(--bg);
}
/* vertical */
@media (min-width: 721px) {
.split-pane.vertical {
display: block;
}
.split-pane.vertical.dragging {
cursor: ns-resize;
}
.vertical .dragger {
top: auto;
height: 10px;
width: 100%;
left: 0;
right: 0;
bottom: -5px;
cursor: ns-resize;
}
.vertical .left,
.vertical .right {
width: 100%;
}
.vertical .left {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
/* mobile */
@media (max-width: 720px) {
.left,
.right {
position: absolute;
inset: 0;
width: auto !important;
height: auto !important;
}
.dragger {
display: none;
}
.split-pane .toggler {
display: block;
}
.split-pane .right,
.split-pane.show-output.reverse .right,
.split-pane.show-output .left,
.split-pane.reverse .left {
z-index: -1;
pointer-events: none;
}
.split-pane .left,
.split-pane.show-output.reverse .left,
.split-pane.show-output .right,
.split-pane.reverse .right {
z-index: 0;
pointer-events: all;
}
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<div
ref="container"
class="editor"
@keydown.ctrl.s.prevent="emitChangeEvent"
@keydown.meta.s.prevent="emitChangeEvent"
/>
</template>
<script setup lang="ts">
import type { ModeSpec, ModeSpecOptions } from 'codemirror'
import {
inject,
onMounted,
onWatcherCleanup,
useTemplateRef,
watch,
watchEffect,
} from 'vue'
import { debounce } from '../utils'
import CodeMirror from './codemirror'
import { injectKeyProps } from '../../src/types'
export interface Props {
mode?: string | ModeSpec<ModeSpecOptions>
value?: string
readonly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
mode: 'htmlmixed',
value: '',
readonly: false,
})
const emit = defineEmits<(e: 'change', value: string) => void>()
const el = useTemplateRef('container')
const { autoResize, autoSave } = inject(injectKeyProps)!
let editor: CodeMirror.Editor
const emitChangeEvent = () => {
emit('change', editor.getValue())
}
onMounted(() => {
const addonOptions = props.readonly
? {}
: {
autoCloseBrackets: true,
autoCloseTags: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
keyMap: 'sublime',
}
editor = CodeMirror(el.value!, {
value: '',
mode: props.mode,
readOnly: props.readonly,
tabSize: 2,
lineWrapping: true,
lineNumbers: true,
...addonOptions,
})
watchEffect(() => {
const cur = editor.getValue()
if (props.value !== cur) {
editor.setValue(props.value)
}
})
watchEffect(() => {
editor.setOption('mode', props.mode)
})
setTimeout(() => {
editor.refresh()
}, 50)
if (autoResize.value) {
window.addEventListener(
'resize',
debounce(() => {
editor.refresh()
}),
)
}
watch(
autoSave,
(autoSave) => {
if (autoSave) {
editor.on('change', emitChangeEvent)
onWatcherCleanup(() => editor.off('change', emitChangeEvent))
}
},
{ immediate: true },
)
})
defineExpose({
getEditorIns: () => editor,
})
</script>
<style>
.editor {
position: relative;
height: 100%;
width: 100%;
overflow: hidden;
}
.CodeMirror {
font-family: var(--font-code);
line-height: 1.5;
height: 100%;
}
</style>

View File

@ -0,0 +1,547 @@
/* BASICS */
.CodeMirror {
color: var(--symbols);
--symbols: #777;
--base: #545281;
--comment: hsl(210, 25%, 60%);
--keyword: #af4ab1;
--variable: var(--base);
--function: #c25205;
--string: #2ba46d;
--number: #c25205;
--tags: #dd0000;
--brackets: var(--comment);
--qualifier: #ff6032;
--important: var(--string);
--attribute: #9c3eda;
--property: #6182b8;
--selected-bg: #d7d4f0;
--selected-bg-non-focus: #d9d9d9;
--cursor: #000;
direction: ltr;
font-family: var(--font-code);
height: auto;
}
.dark .CodeMirror {
color: var(--symbols);
--symbols: #89ddff;
--base: #a6accd;
--comment: #6d6d6d;
--keyword: #89ddff;
--string: #c3e88d;
--variable: #82aaff;
--number: #f78c6c;
--tags: #f07178;
--brackets: var(--symbols);
--property: #f07178;
--attribute: #c792ea;
--cursor: #fff;
--selected-bg: rgba(255, 255, 255, 0.1);
--selected-bg-non-focus: rgba(255, 255, 255, 0.15);
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler,
.CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid var(--border);
background-color: transparent;
white-space: nowrap;
}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: var(--comment);
white-space: nowrap;
opacity: 0.6;
}
.CodeMirror-guttermarker {
color: black;
}
.CodeMirror-guttermarker-subtle {
color: #999;
}
/* FOLD GUTTER */
.CodeMirror-foldmarker {
color: #414141;
text-shadow:
#ff9966 1px 1px 2px,
#ff9966 -1px -1px 2px,
#ff9966 1px -1px 2px,
#ff9966 -1px 1px 2px;
font-family: arial;
line-height: 0.3;
cursor: pointer;
}
.CodeMirror-foldgutter {
width: 0.7em;
}
.CodeMirror-foldgutter-open,
.CodeMirror-foldgutter-folded {
cursor: pointer;
}
.CodeMirror-foldgutter-open:after,
.CodeMirror-foldgutter-folded:after {
content: '>';
font-size: 0.8em;
opacity: 0.8;
transition: transform 0.2s;
display: inline-block;
top: -0.1em;
position: relative;
transform: rotate(90deg);
}
.CodeMirror-foldgutter-folded:after {
transform: none;
}
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid var(--cursor);
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {
}
50% {
background-color: transparent;
}
100% {
}
}
@-webkit-keyframes blink {
0% {
}
50% {
background-color: transparent;
}
100% {
}
}
@keyframes blink {
0% {
}
50% {
background-color: transparent;
}
100% {
}
}
.cm-tab {
display: inline-block;
text-decoration: inherit;
}
.CodeMirror-rulers {
position: absolute;
left: 0;
right: 0;
top: -50px;
bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0;
bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default.CodeMirror {
background-color: transparent;
}
.cm-s-default .cm-header {
color: blue;
}
.cm-s-default .cm-quote {
color: #090;
}
.cm-negative {
color: #d44;
}
.cm-positive {
color: #292;
}
.cm-header,
.cm-strong {
font-weight: bold;
}
.cm-em {
font-style: italic;
}
.cm-link {
text-decoration: underline;
}
.cm-strikethrough {
text-decoration: line-through;
}
.cm-s-default .cm-atom,
.cm-s-default .cm-def,
.cm-s-default .cm-variable-2,
.cm-s-default .cm-variable-3,
.cm-s-default .cm-punctuation {
color: var(--base);
}
.cm-s-default .cm-property {
color: var(--property);
}
.cm-s-default .cm-hr,
.cm-s-default .cm-comment {
color: var(--comment);
}
.cm-s-default .cm-attribute {
color: var(--attribute);
}
.cm-s-default .cm-keyword {
color: var(--keyword);
}
.cm-s-default .cm-variable {
color: var(--variable);
}
.cm-s-default .cm-tag {
color: var(--tags);
}
.cm-s-default .cm-bracket {
color: var(--brackets);
}
.cm-s-default .cm-number {
color: var(--number);
}
.cm-s-default .cm-string,
.cm-s-default .cm-string-2 {
color: var(--string);
}
.cm-s-default .cm-type {
color: rgb(255, 208, 0);
}
.cm-s-default .cm-meta {
color: #555;
}
.cm-s-default .cm-qualifier {
color: var(--qualifier);
}
.cm-s-default .cm-builtin {
color: #7539ff;
}
.cm-s-default .cm-link {
color: var(--flash);
}
.cm-s-default .cm-error {
color: #ff008c;
}
.cm-invalidchar {
color: #ff008c;
}
.CodeMirror-composing {
border-bottom: 2px solid;
}
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {
color: #0b0;
}
div.CodeMirror span.CodeMirror-nonmatchingbracket {
color: #a22;
}
.CodeMirror-matchingtag {
background: rgba(255, 150, 0, 0.3);
}
.CodeMirror-activeline-background {
background: #e8f2ff;
}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px;
margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar,
.CodeMirror-hscrollbar,
.CodeMirror-scrollbar-filler,
.CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0;
top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0;
left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0;
bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0;
bottom: 0;
}
.CodeMirror-gutters {
position: absolute;
left: 0;
top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0;
bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection {
background-color: transparent;
}
.CodeMirror-gutter-wrapper ::-moz-selection {
background-color: transparent;
}
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-rtl pre {
direction: rtl;
}
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre {
position: static;
}
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected {
background: var(--selected-bg-non-focus);
}
.CodeMirror-focused .CodeMirror-selected {
background: var(--selected-bg);
}
.CodeMirror-crosshair {
cursor: crosshair;
}
.CodeMirror-line::selection,
.CodeMirror-line > span::selection,
.CodeMirror-line > span > span::selection {
background: var(--selected-bg);
}
.CodeMirror-line::-moz-selection,
.CodeMirror-line > span::-moz-selection,
.CodeMirror-line > span > span::-moz-selection {
background: var(--selected-bg);
}
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, 0.4);
}
/* Used to force a border model for a node */
.cm-force-border {
padding-right: 0.1px;
}
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after {
content: '';
}
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext {
background: none;
}
.CodeMirror-dialog {
background-color: var(--bg);
}

View File

@ -0,0 +1,26 @@
import CodeMirror from 'codemirror'
import 'codemirror/addon/dialog/dialog.css'
import './codemirror.css'
// modes
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/mode/css/css.js'
import 'codemirror/mode/htmlmixed/htmlmixed.js'
// addons
import 'codemirror/addon/edit/closebrackets.js'
import 'codemirror/addon/edit/closetag.js'
import 'codemirror/addon/comment/comment.js'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/brace-fold.js'
import 'codemirror/addon/fold/indent-fold.js'
import 'codemirror/addon/fold/comment-fold.js'
import 'codemirror/addon/search/search.js'
import 'codemirror/addon/search/searchcursor.js'
import 'codemirror/addon/dialog/dialog.js'
// keymap
import 'codemirror/keymap/sublime.js'
export default CodeMirror

11
src/core.ts Normal file
View File

@ -0,0 +1,11 @@
export {
useStore,
File,
type SFCOptions,
type StoreState,
type Store,
type ReplStore,
} from './store'
export { useVueImportMap, mergeImportMap, type ImportMap } from './import-map'
export { compileFile } from './transform'
export { version as languageToolsVersion } from '@vue/language-service/package.json'

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import CodeMirror, { type Props } from '../codemirror/CodeMirror.vue'
import { computed, useTemplateRef } from 'vue'
import type { EditorEmits, EditorMethods, EditorProps } from '../types'
defineOptions({
editorType: 'codemirror',
})
const props = defineProps<EditorProps>()
const emit = defineEmits<EditorEmits>()
const codeMirrorRef = useTemplateRef('codeMirror')
const onChange = (code: string) => {
emit('change', code)
}
const modes: Record<string, Props['mode']> = {
css: 'css',
html: 'htmlmixed',
js: {
name: 'javascript',
},
json: {
name: 'javascript',
json: true,
},
ts: {
name: 'javascript',
typescript: true,
},
vue: 'htmlmixed',
}
const activeMode = computed(() => {
const { mode: forcedMode, filename } = props
const mode = modes[forcedMode || filename.split('.').pop()!]
return filename.lastIndexOf('.') !== -1 && mode ? mode : modes.js
})
defineExpose({
getEditorIns: (() =>
codeMirrorRef.value?.getEditorIns()) as EditorMethods['getEditorIns'],
})
</script>
<template>
<CodeMirror
ref="codeMirror"
:value="value"
:mode="activeMode"
:readonly="readonly"
@change="onChange"
/>
</template>

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import FileSelector from './FileSelector.vue'
import Message from '../Message.vue'
import { debounce } from '../utils'
import { inject, ref, useTemplateRef, watch } from 'vue'
import ToggleButton from './ToggleButton.vue'
import {
type EditorComponentType,
type EditorMethods,
injectKeyProps,
} from '../types'
const SHOW_ERROR_KEY = 'repl_show_error'
const props = defineProps<{
editorComponent: EditorComponentType
}>()
const { store, autoSave, editorOptions } = inject(injectKeyProps)!
const showMessage = ref(getItem())
const editorRef = useTemplateRef<EditorMethods>('editor')
const onChange = debounce((code: string) => {
store.value.activeFile.code = code
}, 250)
function setItem() {
localStorage.setItem(SHOW_ERROR_KEY, showMessage.value ? 'true' : 'false')
}
function getItem() {
const item = localStorage.getItem(SHOW_ERROR_KEY)
return item !== 'false'
}
watch(showMessage, () => {
setItem()
})
defineExpose({
getEditorIns: (() =>
editorRef.value?.getEditorIns?.()) as EditorMethods['getEditorIns'],
getMonacoEditor: () => editorRef.value?.getMonacoEditor?.(),
})
</script>
<template>
<FileSelector />
<div class="editor-container">
<props.editorComponent
ref="editor"
:value="store.activeFile.code"
:filename="store.activeFile.filename"
@change="onChange"
/>
<Message v-show="showMessage" :err="store.errors[0]" />
<div class="editor-floating">
<ToggleButton
v-if="editorOptions?.showErrorText !== false"
v-model="showMessage"
:text="editorOptions?.showErrorText || 'Show Error'"
/>
<ToggleButton
v-if="editorOptions?.autoSaveText !== false"
v-model="autoSave"
:text="editorOptions?.autoSaveText || 'Auto Save'"
/>
</div>
</div>
</template>
<style scoped>
.editor-container {
height: calc(100% - var(--header-height));
overflow: hidden;
position: relative;
}
.editor-floating {
position: absolute;
bottom: 16px;
right: 16px;
z-index: 11;
display: flex;
flex-direction: column;
align-items: end;
gap: 8px;
background-color: var(--bg);
color: var(--text-light);
padding: 8px;
}
</style>

293
src/editor/FileSelector.vue Normal file
View File

@ -0,0 +1,293 @@
<script setup lang="ts">
import { injectKeyProps } from '../../src/types'
import { importMapFile, stripSrcPrefix, tsconfigFile } from '../store'
import { type VNode, computed, inject, ref, useTemplateRef } from 'vue'
const { store, showTsConfig, showImportMap } = inject(injectKeyProps)!
/**
* When `true`: indicates adding a new file
* When `string`: indicates renaming a file, and holds the old filename in case
* of cancel.
*/
const pending = ref<boolean | string>(false)
/**
* Text shown in the input box when editing a file's name
* This is a display name so it should always strip off the `src/` prefix.
*/
const pendingFilename = ref('Comp.vue')
const files = computed(() =>
Object.entries(store.value.files)
.filter(
([name, file]) =>
name !== importMapFile && name !== tsconfigFile && !file.hidden,
)
.map(([name]) => name),
)
function startAddFile() {
let i = 0
let name = `Comp.vue`
while (true) {
let hasConflict = false
for (const filename in store.value.files) {
if (stripSrcPrefix(filename) === name) {
hasConflict = true
name = `Comp${++i}.vue`
break
}
}
if (!hasConflict) {
break
}
}
pendingFilename.value = name
pending.value = true
}
function cancelNameFile() {
pending.value = false
}
function focus({ el }: VNode) {
;(el as HTMLInputElement).focus()
}
function doneNameFile() {
if (!pending.value) return
if (!pendingFilename.value) {
pending.value = false
return
}
// add back the src prefix
const filename = 'src/' + pendingFilename.value
const oldFilename = pending.value === true ? '' : pending.value
if (!/\.(vue|jsx?|tsx?|css|json)$/.test(filename)) {
store.value.errors = [
`Playground only supports *.vue, *.jsx?, *.tsx?, *.css, *.json files.`,
]
return
}
if (filename !== oldFilename && filename in store.value.files) {
store.value.errors = [`File "${filename}" already exists.`]
return
}
store.value.errors = []
cancelNameFile()
if (filename === oldFilename) {
return
}
if (oldFilename) {
store.value.renameFile(oldFilename, filename)
} else {
store.value.addFile(filename)
}
}
function editFileName(file: string) {
pendingFilename.value = stripSrcPrefix(file)
pending.value = file
}
const fileSelector = useTemplateRef('fileSelector')
function horizontalScroll(e: WheelEvent) {
e.preventDefault()
const el = fileSelector.value!
const direction =
Math.abs(e.deltaX) >= Math.abs(e.deltaY) ? e.deltaX : e.deltaY
const distance = 30 * (direction > 0 ? 1 : -1)
el.scrollTo({
left: el.scrollLeft + distance,
})
}
</script>
<template>
<div
ref="fileSelector"
class="file-selector"
:class="{ 'has-import-map': showImportMap }"
@wheel="horizontalScroll"
>
<template v-for="(file, i) in files" :key="file">
<div
v-if="pending !== file"
class="file"
:class="{ active: store.activeFile.filename === file }"
@click="store.setActive(file)"
@dblclick="i > 0 && editFileName(file)"
>
<span class="label">{{ stripSrcPrefix(file) }}</span>
<span v-if="i > 0" class="remove" @click.stop="store.deleteFile(file)">
<svg class="icon" width="12" height="12" viewBox="0 0 24 24">
<line stroke="#999" x1="18" y1="6" x2="6" y2="18" />
<line stroke="#999" x1="6" y1="6" x2="18" y2="18" />
</svg>
</span>
</div>
<div
v-if="(pending === true && i === files.length - 1) || pending === file"
class="file pending"
:class="{ active: pending === file }"
>
<span class="file pending">{{ pendingFilename }}</span>
<input
v-model="pendingFilename"
spellcheck="false"
@blur="doneNameFile"
@keyup.enter="doneNameFile"
@keyup.esc="cancelNameFile"
@vue:mounted="focus"
/>
</div>
</template>
<button class="add" @click="startAddFile">+</button>
<div class="import-map-wrapper">
<div
v-if="showTsConfig && store.files[tsconfigFile]"
class="file"
:class="{ active: store.activeFile.filename === tsconfigFile }"
@click="store.setActive(tsconfigFile)"
>
<span class="label">tsconfig.json</span>
</div>
<div
v-if="showImportMap"
class="file"
:class="{ active: store.activeFile.filename === importMapFile }"
@click="store.setActive(importMapFile)"
>
<span class="label">Import Map</span>
</div>
</div>
</div>
</template>
<style scoped>
.file-selector {
display: flex;
box-sizing: border-box;
border-bottom: 1px solid var(--border);
background-color: var(--bg);
overflow-y: hidden;
overflow-x: auto;
white-space: nowrap;
position: relative;
height: var(--header-height);
}
.file-selector::-webkit-scrollbar {
height: 1px;
}
.file-selector::-webkit-scrollbar-track {
background-color: var(--border);
}
.file-selector::-webkit-scrollbar-thumb {
background-color: var(--color-branding);
}
@-moz-document url-prefix() {
.file-selector {
scrollbar-width: thin;
scrollbar-color: var(--color-branding) var(--border);
}
}
.file-selector.has-import-map .add {
margin-right: 10px;
}
.file {
position: relative;
display: inline-block;
font-size: 13px;
font-family: var(--font-code);
cursor: pointer;
color: var(--text-light);
box-sizing: border-box;
}
.file.active {
color: var(--color-branding);
border-bottom: 3px solid var(--color-branding);
cursor: text;
}
.file span {
display: inline-block;
padding: 8px 10px 6px;
line-height: 20px;
}
.file.pending span {
min-width: 50px;
min-height: 34px;
padding-right: 32px;
background-color: rgba(200, 200, 200, 0.2);
color: transparent;
}
.file.pending input {
position: absolute;
inset: 8px 7px auto;
font-size: 13px;
font-family: var(--font-code);
line-height: 20px;
outline: none;
border: none;
padding: 0 3px;
min-width: 1px;
color: inherit;
background-color: transparent;
}
.file .remove {
display: inline-block;
vertical-align: middle;
line-height: 12px;
cursor: pointer;
padding-left: 0;
}
.add {
font-size: 18px;
font-family: var(--font-code);
color: #999;
vertical-align: middle;
margin-left: 6px;
position: relative;
top: -1px;
}
.add:hover {
color: var(--color-branding);
}
.icon {
margin-top: -1px;
}
.import-map-wrapper {
position: sticky;
margin-left: auto;
top: 0;
right: 0;
padding-left: 30px;
background-color: var(--bg);
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 25%
);
}
.dark .import-map-wrapper {
background: linear-gradient(
90deg,
rgba(26, 26, 26, 0) 0%,
rgba(26, 26, 26, 1) 25%
);
}
</style>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import Monaco from '../monaco/Monaco.vue'
import type { EditorEmits, EditorMethods, EditorProps } from '../types'
import { useTemplateRef } from 'vue'
defineProps<EditorProps>()
const emit = defineEmits<EditorEmits>()
defineOptions({
editorType: 'monaco',
})
const monacoRef = useTemplateRef('monaco')
const onChange = (code: string) => {
emit('change', code)
}
defineExpose({
getEditorIns: (() =>
monacoRef.value?.getEditorIns()) as EditorMethods['getEditorIns'],
getMonacoEditor: () => monacoRef.value?.getMonacoEditor(),
})
</script>
<template>
<Monaco
ref="monaco"
:filename="filename"
:value="value"
:readonly="readonly"
:mode="mode"
@change="onChange"
/>
</template>

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
defineProps<{ text: string }>()
const active = defineModel<boolean>()
</script>
<template>
<div class="wrapper" @click="active = !active">
<span>{{ text }}</span>
<div class="toggle" :class="[{ active: modelValue }]">
<div class="indicator" />
</div>
</div>
</template>
<style scoped>
.wrapper {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.toggle {
display: inline-block;
margin-left: 4px;
width: 32px;
height: 18px;
border-radius: 12px;
position: relative;
background-color: var(--border);
}
.indicator {
font-size: 12px;
background-color: var(--text-light);
width: 14px;
height: 14px;
border-radius: 50%;
transition: transform ease-in-out 0.2s;
position: absolute;
left: 2px;
top: 2px;
color: var(--bg);
text-align: center;
}
.active .indicator {
background-color: var(--color-branding);
transform: translateX(14px);
color: white;
}
</style>

7
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { ComponentOptions } from 'vue'
const comp: ComponentOptions
export default comp
}

75
src/import-map.ts Normal file
View File

@ -0,0 +1,75 @@
import { computed, version as currentVersion, ref } from 'vue'
export function getVersions(version: string): number[] {
return version.split('.').map((v) => parseInt(v, 10))
}
export function isVaporSupported(version: string): boolean{
const [major, minor] = getVersions(version)
// vapor mode is supported in v3.6+
return major > 3 || (major === 3 && minor >= 6)
}
export function useVueImportMap(
defaults: {
runtimeDev?: string | (() => string)
runtimeProd?: string | (() => string)
serverRenderer?: string | (() => string)
vueVersion?: string | null
} = {},
) {
function normalizeDefaults(defaults?: string | (() => string)) {
if (!defaults) return
return typeof defaults === 'string' ? defaults : defaults()
}
const productionMode = ref(false)
const vueVersion = ref<string | null>(defaults.vueVersion || null)
function getVueURL() {
const version = vueVersion.value || currentVersion
return isVaporSupported(version)
? `https://cdn.jsdelivr.net/npm/vue@${version}/dist/vue.runtime-with-vapor.esm-browser${productionMode.value ? `.prod` : ``}.js`
: `https://cdn.jsdelivr.net/npm/@vue/runtime-dom@${version}/dist/runtime-dom.esm-browser${productionMode.value ? `.prod` : ``}.js`
}
const importMap = computed<ImportMap>(() => {
const vue =
(!vueVersion.value &&
normalizeDefaults(
productionMode.value ? defaults.runtimeProd : defaults.runtimeDev,
)) ||
getVueURL()
const serverRenderer =
(!vueVersion.value && normalizeDefaults(defaults.serverRenderer)) ||
`https://cdn.jsdelivr.net/npm/@vue/server-renderer@${
vueVersion.value || currentVersion
}/dist/server-renderer.esm-browser.js`
return {
imports: {
vue,
'vue/server-renderer': serverRenderer,
},
}
})
return {
productionMode,
importMap,
vueVersion,
defaultVersion: currentVersion,
}
}
export interface ImportMap {
imports?: Record<string, string | undefined>
scopes?: Record<string, Record<string, string>>
}
export function mergeImportMap(a: ImportMap, b: ImportMap): ImportMap {
return {
imports: { ...a.imports, ...b.imports },
scopes: { ...a.scopes, ...b.scopes },
}
}

5
src/index.ts Normal file
View File

@ -0,0 +1,5 @@
export { default as Repl, type Props as ReplProps } from './Repl.vue'
export { default as Preview } from './output/Preview.vue'
export { default as Sandbox, type SandboxProps } from './output/Sandbox.vue'
export type { OutputModes } from './types'
export * from './core'

8
src/jsx.ts Normal file
View File

@ -0,0 +1,8 @@
import { transform } from '@babel/standalone'
import jsx from '@vue/babel-plugin-jsx'
export function transformJSX(src: string) {
return transform(src, {
plugins: [jsx],
}).code!
}

195
src/monaco/Monaco.vue Normal file
View File

@ -0,0 +1,195 @@
<script lang="ts" setup>
import {
computed,
inject,
onBeforeUnmount,
onMounted,
onWatcherCleanup,
shallowRef,
useTemplateRef,
watch,
} from 'vue'
import * as monaco from 'monaco-editor-core'
import { initMonaco } from './env'
import { getOrCreateModel } from './utils'
import { type EditorMode, injectKeyProps } from '../types'
import { registerHighlighter } from './highlight'
const props = withDefaults(
defineProps<{
filename: string
value?: string
readonly?: boolean
mode?: EditorMode
}>(),
{
readonly: false,
value: '',
mode: undefined,
},
)
const emit = defineEmits<{
(e: 'change', value: string): void
}>()
const containerRef = useTemplateRef('container')
const editor = shallowRef<monaco.editor.IStandaloneCodeEditor>()
const {
store,
autoSave,
theme: replTheme,
editorOptions,
} = inject(injectKeyProps)!
initMonaco(store.value)
const lang = computed(() => (props.mode === 'css' ? 'css' : 'javascript'))
let editorInstance: monaco.editor.IStandaloneCodeEditor
function emitChangeEvent() {
emit('change', editorInstance.getValue())
}
onMounted(() => {
const theme = registerHighlighter()
if (!containerRef.value) {
throw new Error('Cannot find containerRef')
}
editorInstance = monaco.editor.create(containerRef.value, {
...(props.readonly
? { value: props.value, language: lang.value }
: { model: null }),
fontSize: 13,
tabSize: 2,
theme: replTheme.value === 'light' ? theme.light : theme.dark,
readOnly: props.readonly,
automaticLayout: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
inlineSuggest: {
enabled: false,
},
fixedOverflowWidgets: true,
...editorOptions.value.monacoOptions,
})
editor.value = editorInstance
// Support for semantic highlighting
const t = (editorInstance as any)._themeService._theme
t.semanticHighlighting = true
t.getTokenStyleMetadata = (
type: string,
modifiers: string[],
_language: string,
) => {
const _readonly = modifiers.includes('readonly')
switch (type) {
case 'function':
case 'method':
return { foreground: 12 }
case 'class':
return { foreground: 11 }
case 'variable':
case 'property':
return { foreground: _readonly ? 19 : 9 }
default:
return { foreground: 0 }
}
}
watch(
() => props.value,
(value) => {
if (editorInstance.getValue() === value) return
editorInstance.setValue(value || '')
},
{ immediate: true },
)
watch(lang, (lang) =>
monaco.editor.setModelLanguage(editorInstance.getModel()!, lang),
)
if (!props.readonly) {
watch(
() => props.filename,
(_, oldFilename) => {
if (!editorInstance) return
const file = store.value.files[props.filename]
if (!file) return null
const model = getOrCreateModel(
monaco.Uri.parse(`file:///${props.filename}`),
file.language,
file.code,
)
const oldFile = oldFilename ? store.value.files[oldFilename] : null
if (oldFile) {
oldFile.editorViewState = editorInstance.saveViewState()
}
editorInstance.setModel(model)
if (file.editorViewState) {
editorInstance.restoreViewState(file.editorViewState)
editorInstance.focus()
}
},
{ immediate: true },
)
}
editorInstance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
// ignore save event
})
watch(
autoSave,
(autoSave) => {
if (autoSave) {
const disposable =
editorInstance.onDidChangeModelContent(emitChangeEvent)
onWatcherCleanup(() => disposable.dispose())
}
},
{ immediate: true },
)
// update theme
watch(replTheme, (n) => {
editorInstance.updateOptions({
theme: n === 'light' ? theme.light : theme.dark,
})
})
})
onBeforeUnmount(() => {
editor.value?.dispose()
})
defineExpose({
getEditorIns: () => editor.value,
getMonacoEditor: () => monaco.editor,
})
</script>
<template>
<div
ref="container"
class="editor"
@keydown.ctrl.s.prevent="emitChangeEvent"
@keydown.meta.s.prevent="emitChangeEvent"
/>
</template>
<style>
.editor {
position: relative;
height: 100%;
width: 100%;
overflow: hidden;
}
</style>

206
src/monaco/env.ts Normal file
View File

@ -0,0 +1,206 @@
import * as volar from '@volar/monaco'
import { Uri, editor, languages } from 'monaco-editor-core'
import editorWorker from 'monaco-editor-core/esm/vs/editor/editor.worker?worker'
import { watchEffect } from 'vue'
import type { Store } from '../store'
import { getOrCreateModel } from './utils'
import type { CreateData } from './vue.worker'
import vueWorker from './vue.worker?worker'
import * as languageConfigs from './language-configs'
import type { WorkerLanguageService } from '@volar/monaco/worker'
import { debounce } from '../utils'
let initted = false
export function initMonaco(store: Store) {
if (initted) return
loadMonacoEnv(store)
watchEffect(() => {
// create a model for each file in the store
for (const filename in store.files) {
const file = store.files[filename]
if (editor.getModel(Uri.parse(`file:///${filename}`))) continue
getOrCreateModel(
Uri.parse(`file:///${filename}`),
file.language,
file.code,
)
}
// dispose of any models that are not in the store
for (const model of editor.getModels()) {
const uri = model.uri.toString()
if (store.files[uri.substring('file:///'.length)]) continue
if (uri.startsWith('file:///node_modules')) continue
if (uri.startsWith('inmemory://')) continue
model.dispose()
}
})
initted = true
}
export class WorkerHost {
onFetchCdnFile(uri: string, text: string) {
getOrCreateModel(Uri.parse(uri), undefined, text)
}
}
let disposeVue: undefined | (() => void)
export async function reloadLanguageTools(store: Store) {
disposeVue?.()
let dependencies: Record<string, string> = {
...store.dependencyVersion,
}
if (store.vueVersion) {
dependencies = {
...dependencies,
vue: store.vueVersion,
'@vue/compiler-core': store.vueVersion,
'@vue/compiler-dom': store.vueVersion,
'@vue/compiler-sfc': store.vueVersion,
'@vue/compiler-ssr': store.vueVersion,
'@vue/reactivity': store.vueVersion,
'@vue/runtime-core': store.vueVersion,
'@vue/runtime-dom': store.vueVersion,
'@vue/shared': store.vueVersion,
}
}
if (store.typescriptVersion) {
dependencies = {
...dependencies,
typescript: store.typescriptVersion,
}
}
const worker = editor.createWebWorker<WorkerLanguageService>({
moduleId: 'vs/language/vue/vueWorker',
label: 'vue',
host: new WorkerHost(),
createData: {
tsconfig: store.getTsConfig?.() || {},
dependencies,
} satisfies CreateData,
})
const languageId = ['vue', 'javascript', 'typescript']
const getSyncUris = () =>
Object.keys(store.files).map((filename) => Uri.parse(`file:///${filename}`))
const { dispose: disposeMarkers } = volar.activateMarkers(
worker,
languageId,
'vue',
getSyncUris,
editor,
)
const { dispose: disposeAutoInsertion } = volar.activateAutoInsertion(
worker,
languageId,
getSyncUris,
editor,
)
const { dispose: disposeProvides } = await volar.registerProviders(
worker,
languageId,
getSyncUris,
languages,
)
disposeVue = () => {
disposeMarkers()
disposeAutoInsertion()
disposeProvides()
}
}
export interface WorkerMessage {
event: 'init'
tsVersion: string
tsLocale?: string
pkgDirUrl?: string
pkgFileTextUrl?: string
pkgLatestVersionUrl?: string
typescriptLib?: string
}
export function loadMonacoEnv(store: Store) {
;(self as any).MonacoEnvironment = {
async getWorker(_: any, label: string) {
if (label === 'vue') {
const worker = new vueWorker()
const init = new Promise<void>((resolve) => {
worker.addEventListener('message', (data) => {
if (data.data === 'inited') {
resolve()
}
})
const {
pkgDirUrl,
pkgFileTextUrl,
pkgLatestVersionUrl,
typescriptLib,
} = store.resourceLinks || {}
const message: WorkerMessage = {
event: 'init',
tsVersion: store.typescriptVersion,
tsLocale: store.locale,
pkgDirUrl: pkgDirUrl ? String(pkgDirUrl) : undefined,
pkgFileTextUrl: pkgFileTextUrl ? String(pkgFileTextUrl) : undefined,
pkgLatestVersionUrl: pkgLatestVersionUrl
? String(pkgLatestVersionUrl)
: undefined,
typescriptLib: typescriptLib ? String(typescriptLib) : undefined,
}
worker.postMessage(message)
})
await init
return worker
}
return new editorWorker()
},
}
languages.register({ id: 'vue', extensions: ['.vue'] })
languages.register({ id: 'javascript', extensions: ['.js'] })
languages.register({ id: 'typescript', extensions: ['.ts'] })
languages.register({ id: 'css', extensions: ['.css'] })
languages.setLanguageConfiguration('vue', languageConfigs.vue)
languages.setLanguageConfiguration('javascript', languageConfigs.js)
languages.setLanguageConfiguration('typescript', languageConfigs.ts)
languages.setLanguageConfiguration('css', languageConfigs.css)
let languageToolsPromise: Promise<void> | undefined
store.reloadLanguageTools = debounce(async () => {
;(languageToolsPromise ||= reloadLanguageTools(store)).finally(() => {
languageToolsPromise = undefined
})
}, 250)
languages.onLanguage('vue', () => store.reloadLanguageTools!())
// Support for go to definition
editor.registerEditorOpener({
openCodeEditor(_, resource) {
if (resource.toString().startsWith('file:///node_modules')) {
return true
}
const path = resource.path
if (/^\//.test(path)) {
const fileName = path.replace('/', '')
if (fileName !== store.activeFile.filename) {
store.setActive(fileName)
return true
}
}
return false
},
})
}

29
src/monaco/highlight.ts Normal file
View File

@ -0,0 +1,29 @@
import * as monaco from 'monaco-editor-core'
import { createHighlighterCoreSync } from 'shiki/core'
import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs'
import { shikiToMonaco } from '@shikijs/monaco'
import langVue from 'shiki/langs/vue.mjs'
import langTsx from 'shiki/langs/tsx.mjs'
import langJsx from 'shiki/langs/jsx.mjs'
import themeDark from 'shiki/themes/dark-plus.mjs'
import themeLight from 'shiki/themes/light-plus.mjs'
let registered = false
export function registerHighlighter() {
if (!registered) {
const highlighter = createHighlighterCoreSync({
themes: [themeDark, themeLight],
langs: [langVue, langTsx, langJsx],
engine: createJavaScriptRegexEngine(),
})
monaco.languages.register({ id: 'vue' })
shikiToMonaco(highlighter, monaco)
registered = true
}
return {
light: themeLight.name!,
dark: themeDark.name!,
}
}

View File

@ -0,0 +1,529 @@
import { languages } from 'monaco-editor-core'
// export const html: languages.LanguageConfiguration = {
// comments: {
// blockComment: ['<!--', '-->'],
// },
// brackets: [
// ['<!--', '-->'],
// ['{', '}'],
// ['(', ')'],
// ],
// autoClosingPairs: [
// { open: '{', close: '}' },
// { open: '[', close: ']' },
// { open: '(', close: ')' },
// { open: "'", close: "'" },
// { open: '"', close: '"' },
// { open: '<!--', close: '-->', notIn: ['comment', 'string'] },
// ],
// surroundingPairs: [
// { open: "'", close: "'" },
// { open: '"', close: '"' },
// { open: '{', close: '}' },
// { open: '[', close: ']' },
// { open: '(', close: ')' },
// { open: '<', close: '>' },
// ],
// colorizedBracketPairs: [],
// folding: {
// markers: {
// start: /^\s*<!--\s*#region\b.*-->/,
// end: /^\s*<!--\s*#endregion\b.*-->/,
// },
// },
// wordPattern: new RegExp(
// '(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\$\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\\'\\"\\,\\.\\<\\>\\/\\s]+)',
// ),
// onEnterRules: [
// {
// beforeText: new RegExp(
// '<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^\'"/>]|"[^"]*"|\'[^\']*\')*?(?!\\/)>)[^<]*$',
// 'i',
// ),
// afterText: new RegExp('^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>', 'i'),
// action: {
// indentAction: languages.IndentAction.IndentOutdent,
// },
// },
// {
// beforeText: new RegExp(
// '<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^\'"/>]|"[^"]*"|\'[^\']*\')*?(?!\\/)>)[^<]*$',
// 'i',
// ),
// action: {
// indentAction: languages.IndentAction.Indent,
// },
// },
// ],
// indentationRules: {
// increaseIndentPattern: new RegExp(
// '<(?!\\?|(?: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>)|<!--(?!.*-->)|\\{[^}"\']*$',
// ),
// decreaseIndentPattern: new RegExp(
// '^\\s*(<\\/(?!html)[-_\\.A-Za-z0-9]+\\b[^>]*>|-->|\\})',
// ),
// },
// }
export const css: languages.LanguageConfiguration = {
comments: {
blockComment: ['/*', '*/'],
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}', notIn: ['string', 'comment'] },
{ open: '[', close: ']', notIn: ['string', 'comment'] },
{ open: '(', close: ')', notIn: ['string', 'comment'] },
{ open: '"', close: '"', notIn: ['string', 'comment'] },
{ open: "'", close: "'", notIn: ['string', 'comment'] },
],
surroundingPairs: [
{
open: "'",
close: "'",
},
{
open: '"',
close: '"',
},
{
open: '{',
close: '}',
},
{
open: '[',
close: ']',
},
{
open: '(',
close: ')',
},
],
folding: {
markers: {
start: new RegExp('^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/'),
end: new RegExp('^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/'),
},
},
indentationRules: {
increaseIndentPattern: new RegExp('(^.*\\{[^}]*$)'),
decreaseIndentPattern: new RegExp('^\\s*\\}'),
},
wordPattern: new RegExp(
'(#?-?\\d*\\.\\d\\w*%?)|(::?[\\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\\w-?]+%?|[@#!.])',
),
}
export const vue: languages.LanguageConfiguration = {
comments: {
blockComment: ['<!--', '-->'],
},
brackets: [
['<!--', '-->'],
['<', '>'],
['{', '}'],
['(', ')'],
],
autoClosingPairs: [
{
open: '{',
close: '}',
},
{
open: '[',
close: ']',
},
{
open: '(',
close: ')',
},
{
open: "'",
close: "'",
},
{
open: '"',
close: '"',
},
{
open: '<!--',
close: '-->',
notIn: ['comment', 'string'],
},
{
open: '`',
close: '`',
notIn: ['string', 'comment'],
},
{
open: '/**',
close: ' */',
notIn: ['string'],
},
],
autoCloseBefore: ';:.,=}])><`\'" \n\t',
surroundingPairs: [
{
open: "'",
close: "'",
},
{
open: '"',
close: '"',
},
{
open: '{',
close: '}',
},
{
open: '[',
close: ']',
},
{
open: '(',
close: ')',
},
{
open: '<',
close: '>',
},
{
open: '`',
close: '`',
},
],
colorizedBracketPairs: [],
folding: {
markers: {
start: /^\s*<!--\s*#region\b.*-->/,
end: /^\s*<!--\s*#endregion\b.*-->/,
},
},
wordPattern:
/(-?\d*\.\d\w*)|([^\`\@\~\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/,
onEnterRules: [
{
beforeText:
/<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr|script|style))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i,
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i,
action: {
indentAction: languages.IndentAction.IndentOutdent,
},
},
{
beforeText:
/<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr|script|style))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i,
action: {
indentAction: languages.IndentAction.Indent,
},
},
],
indentationRules: {
increaseIndentPattern:
/<(?!\?|(?: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,
decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/,
},
}
export const js: languages.LanguageConfiguration = {
comments: {
lineComment: '//',
blockComment: ['/*', '*/'],
},
brackets: [
['${', '}'],
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{
open: '{',
close: '}',
},
{
open: '[',
close: ']',
},
{
open: '(',
close: ')',
},
{
open: "'",
close: "'",
notIn: ['string', 'comment'],
},
{
open: '"',
close: '"',
notIn: ['string'],
},
{
open: '`',
close: '`',
notIn: ['string', 'comment'],
},
{
open: '/**',
close: ' */',
notIn: ['string'],
},
],
surroundingPairs: [
{
open: "'",
close: "'",
},
{
open: '"',
close: '"',
},
{
open: '{',
close: '}',
},
{
open: '[',
close: ']',
},
{
open: '(',
close: ')',
},
{
open: '<',
close: '>',
},
{
open: '`',
close: '`',
},
],
autoCloseBefore: ';:.,=}])>` \n\t',
folding: {
markers: {
start: /^\s*\/\/\s*#?region\b/,
end: /^\s*\/\/\s*#?endregion\b/,
},
},
wordPattern:
/(-?\d*\.\d\w*)|([^\`\~\@\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/,
indentationRules: {
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
increaseIndentPattern:
/^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/,
unIndentedLinePattern:
/^(\t|[ ])*[ ]\*[^/]*\*\/\s*$|^(\t|[ ])*[ ]\*\/\s*$|^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
},
onEnterRules: [
{
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
afterText: /^\s*\*\/$/,
action: {
indentAction: languages.IndentAction.IndentOutdent,
appendText: ' * ',
},
},
{
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
action: {
indentAction: languages.IndentAction.None,
appendText: ' * ',
},
},
{
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/,
action: {
indentAction: languages.IndentAction.None,
appendText: '* ',
},
},
{
beforeText: /^(\t|[ ])*[ ]\*\/\s*$/,
action: {
indentAction: languages.IndentAction.None,
removeText: 1,
},
},
{
beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
action: {
indentAction: languages.IndentAction.None,
removeText: 1,
},
},
{
beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/,
afterText: /^(?!\s*(\bcase\b|\bdefault\b))/,
action: {
indentAction: languages.IndentAction.Indent,
},
},
{
previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/,
beforeText: /^\s+([^{i\s]|i(?!f\b))/,
action: {
indentAction: languages.IndentAction.Outdent,
},
},
],
}
export const ts: languages.LanguageConfiguration = {
comments: {
lineComment: '//',
blockComment: ['/*', '*/'],
},
brackets: [
['${', '}'],
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{
open: '{',
close: '}',
},
{
open: '[',
close: ']',
},
{
open: '(',
close: ')',
},
{
open: "'",
close: "'",
notIn: ['string', 'comment'],
},
{
open: '"',
close: '"',
notIn: ['string'],
},
{
open: '`',
close: '`',
notIn: ['string', 'comment'],
},
{
open: '/**',
close: ' */',
notIn: ['string'],
},
],
surroundingPairs: [
{
open: "'",
close: "'",
},
{
open: '"',
close: '"',
},
{
open: '{',
close: '}',
},
{
open: '[',
close: ']',
},
{
open: '(',
close: ')',
},
{
open: '<',
close: '>',
},
{
open: '`',
close: '`',
},
],
colorizedBracketPairs: [
['(', ')'],
['[', ']'],
['{', '}'],
['<', '>'],
],
autoCloseBefore: ';:.,=}])>` \n\t',
folding: {
markers: {
start: /^\s*\/\/\s*#?region\b/,
end: /^\s*\/\/\s*#?endregion\b/,
},
},
wordPattern:
/(-?\d*\.\d\w*)|([^\`\~\@\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/,
indentationRules: {
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
increaseIndentPattern:
/^((?!\/\/).)*(\{([^}"'`/]*|(\t|[ ])*\/\/.*)|\([^)"'`/]*|\[[^\]"'`/]*)$/,
unIndentedLinePattern:
/^(\t|[ ])*[ ]\*[^/]*\*\/\s*$|^(\t|[ ])*[ ]\*\/\s*$|^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
},
onEnterRules: [
{
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
afterText: /^\s*\*\/$/,
action: {
indentAction: languages.IndentAction.IndentOutdent,
appendText: ' * ',
},
},
{
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
action: {
indentAction: languages.IndentAction.None,
appendText: ' * ',
},
},
{
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/,
action: {
indentAction: languages.IndentAction.None,
appendText: '* ',
},
},
{
beforeText: /^(\t|[ ])*[ ]\*\/\s*$/,
action: {
indentAction: languages.IndentAction.None,
removeText: 1,
},
},
{
beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
action: {
indentAction: languages.IndentAction.None,
removeText: 1,
},
},
{
beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/,
afterText: /^(?!\s*(\bcase\b|\bdefault\b))/,
action: {
indentAction: languages.IndentAction.Indent,
},
},
{
previousLineText: /^\s*(((else ?)?if|for|while)\s*\(.*\)\s*|else\s*)$/,
beforeText: /^\s+([^{i\s]|i(?!f\b))/,
action: {
indentAction: languages.IndentAction.Outdent,
},
},
],
}

398
src/monaco/resource.ts Normal file
View File

@ -0,0 +1,398 @@
/**
* base on @volar/jsdelivr
* MIT License https://github.com/volarjs/volar.js/blob/master/packages/jsdelivr/LICENSE
*/
import type { FileStat, FileSystem, FileType } from '@volar/language-service'
import type { URI } from 'vscode-uri'
const textCache = new Map<string, Promise<string | undefined>>()
const jsonCache = new Map<string, Promise<any>>()
export type CreateNpmFileSystemOptions = {
getPackageLatestVersionUrl?: (pkgName: string) => string
getPackageDirectoryUrl?: (
pkgName: string,
pkgVersion: string,
pkgPath: string,
) => string
getPackageFileTextUrl?: (
pkgName: string,
pkgVersion: string | undefined,
pkgPath: string,
) => string
}
const defaultUnpkgOptions: Required<CreateNpmFileSystemOptions> = {
getPackageLatestVersionUrl: (pkgName) =>
`https://unpkg.com/${pkgName}@latest/package.json`,
getPackageDirectoryUrl: (pkgName, pkgVersion, pkgPath) =>
`https://unpkg.com/${pkgName}@${pkgVersion}/${pkgPath}/?meta`,
getPackageFileTextUrl: (pkgName, pkgVersion, pkgPath) =>
`https://unpkg.com/${pkgName}@${pkgVersion || 'latest'}/${pkgPath}`,
}
export function createNpmFileSystem(
getCdnPath = (uri: URI): string | undefined => {
if (uri.path === '/node_modules') {
return ''
} else if (uri.path.startsWith('/node_modules/')) {
return uri.path.slice('/node_modules/'.length)
}
},
getPackageVersion?: (pkgName: string) => string | undefined,
onFetch?: (path: string, content: string) => void,
options?: CreateNpmFileSystemOptions,
): FileSystem {
const {
getPackageDirectoryUrl = defaultUnpkgOptions.getPackageDirectoryUrl,
getPackageFileTextUrl = defaultUnpkgOptions.getPackageFileTextUrl,
getPackageLatestVersionUrl = defaultUnpkgOptions.getPackageLatestVersionUrl,
} = options || {}
const fetchResults = new Map<string, Promise<string | undefined>>()
const statCache = new Map<string, { type: FileType }>()
const dirCache = new Map<string, [string, FileType][]>()
return {
async stat(uri) {
const path = normalizePath(getCdnPath(uri))
if (path === undefined) {
return
}
if (path === '') {
return {
type: 2 satisfies FileType.Directory,
size: -1,
ctime: -1,
mtime: -1,
}
}
return await _stat(path)
},
async readFile(uri) {
const path = normalizePath(getCdnPath(uri))
if (path === undefined) {
return
}
return await _readFile(path)
},
readDirectory(uri) {
const path = normalizePath(getCdnPath(uri))
if (path === undefined) {
return []
}
return _readDirectory(path)
},
}
async function _stat(path: string) {
if (hasNodeModulesSegment(path)) {
return
}
if (statCache.has(path)) {
return {
...statCache.get(path),
ctime: -1,
mtime: -1,
size: -1,
} as FileStat
}
const [modName, pkgName, , pkgFilePath] = resolvePackageName(path)
if (!pkgName) {
if (modName.startsWith('@')) {
return {
type: 2 satisfies FileType.Directory,
ctime: -1,
mtime: -1,
size: -1,
}
} else {
return
}
}
if (!(await isValidPackageName(pkgName))) {
return
}
if (!pkgFilePath || pkgFilePath === '/') {
const result = {
type: 2 as FileType.Directory,
}
statCache.set(path, result)
return { ...result, ctime: -1, mtime: -1, size: -1 }
}
try {
const parentDir = path.substring(0, path.lastIndexOf('/'))
const fileName = path.substring(path.lastIndexOf('/') + 1)
const dirContent = await _readDirectory(parentDir)
const fileEntry = dirContent.find(([name]) => name === fileName)
if (fileEntry) {
const result = {
type: fileEntry[1] as FileType,
}
statCache.set(path, result)
return { ...result, ctime: -1, mtime: -1, size: -1 }
}
return
} catch {
return
}
}
async function _readDirectory(path: string): Promise<[string, FileType][]> {
if (hasNodeModulesSegment(path)) {
return []
}
if (dirCache.has(path)) {
return dirCache.get(path)!
}
const [, pkgName, pkgVersion, pkgPath] = resolvePackageName(path)
if (!pkgName || !(await isValidPackageName(pkgName))) {
return []
}
const resolvedVersion = pkgVersion || 'latest'
let actualVersion = resolvedVersion
if (resolvedVersion === 'latest') {
try {
const data = await fetchJson<{ version: string }>(
getPackageLatestVersionUrl(pkgName),
)
if (data?.version) {
actualVersion = data.version
}
} catch {
// ignore
}
}
const endpoint = getPackageDirectoryUrl(pkgName, actualVersion, pkgPath)
try {
const data = await fetchJson<{
files: {
path: string
type: 'file' | 'directory'
size?: number
}[]
}>(endpoint)
if (!data?.files) {
return []
}
const result = getDirectDirectoryEntries(path, pkgPath, data.files)
dirCache.set(path, result)
return result
} catch {
return []
}
}
function getDirectDirectoryEntries(
path: string,
pkgPath: string,
files: {
path: string
type: 'file' | 'directory'
size?: number
}[],
): [string, FileType][] {
const entries = new Map<string, FileType>()
const prefix = trimSlashes(pkgPath)
for (const file of files) {
const isRootedPath = file.path.startsWith('/')
const filePath = trimSlashes(file.path)
if (!filePath) continue
const relativePath =
prefix && filePath.startsWith(`${prefix}/`)
? filePath.slice(prefix.length + 1)
: prefix && isRootedPath
? undefined
: filePath
if (!relativePath) continue
const [name, ...rest] = relativePath.split('/')
const type =
rest.length > 0 || file.type === 'directory'
? (2 as FileType.Directory)
: (1 as FileType.File)
entries.set(name, type)
statCache.set(joinPath(path, name), { type })
}
return [...entries]
}
async function _readFile(path: string): Promise<string | undefined> {
const [_modName, pkgName, _version, pkgFilePath] = resolvePackageName(path)
if (
!pkgName ||
!pkgFilePath ||
hasNodeModulesSegment(pkgFilePath) ||
!(await isValidPackageName(pkgName))
) {
return
}
if (!fetchResults.has(path)) {
fetchResults.set(
path,
(async () => {
if ((await _stat(path))?.type !== (1 satisfies FileType.File)) {
return
}
const text = await fetchText(
getPackageFileTextUrl(pkgName, _version, pkgFilePath),
)
if (text !== undefined) {
onFetch?.(path, text)
}
return text
})(),
)
}
return await fetchResults.get(path)!
}
function hasNodeModulesSegment(path: string) {
return path.split('/').includes('node_modules')
}
function joinPath(base: string, path: string) {
return [trimSlashes(base), trimSlashes(path)].filter(Boolean).join('/')
}
function normalizePath(path: string | undefined) {
return path === undefined ? undefined : trimSlashes(path)
}
function trimSlashes(path: string) {
return path.replace(/^\/+|\/+$/g, '')
}
async function isValidPackageName(pkgName: string) {
// ignore nested node_modules probes like /node_modules/node_modules
if (pkgName === 'node_modules' || pkgName.endsWith('/node_modules')) {
return false
}
// hard code to skip known invalid package
if (
pkgName.endsWith('.d.ts') ||
pkgName.startsWith('@typescript/') ||
pkgName.startsWith('@types/typescript__')
) {
return false
}
// don't check @types if original package already having types
if (pkgName.startsWith('@types/')) {
let originalPkgName = pkgName.slice('@types/'.length)
if (originalPkgName.indexOf('__') >= 0) {
originalPkgName = '@' + originalPkgName.replace('__', '/')
}
const packageJson = await _readFile(`${originalPkgName}/package.json`)
if (!packageJson) {
return false
}
const packageJsonObj = JSON.parse(packageJson)
if (packageJsonObj.types || packageJsonObj.typings) {
return false
}
const indexDts = await _stat(`${originalPkgName}/index.d.ts`)
if (indexDts?.type === (1 satisfies FileType.File)) {
return false
}
}
return true
}
/**
* @example
* "a/b/c" -> ["a", "a", undefined, "b/c"]
* "@a" -> ["@a", undefined, undefined, ""]
* "@a/b/c" -> ["@a/b", "@a/b", undefined, "c"]
* "@a/b@1.2.3/c" -> ["@a/b@1.2.3", "@a/b", "1.2.3", "c"]
*/
function resolvePackageName(
input: string,
): [
modName: string,
pkgName: string | undefined,
version: string | undefined,
path: string,
] {
const parts = input.split('/')
let modName = parts[0]
let path: string
if (modName.startsWith('@')) {
if (!parts[1]) {
return [modName, undefined, undefined, '']
}
modName += '/' + parts[1]
path = parts.slice(2).join('/')
} else {
path = parts.slice(1).join('/')
}
let pkgName = modName
let version: string | undefined
if (modName.lastIndexOf('@') >= 1) {
pkgName = modName.substring(0, modName.lastIndexOf('@'))
version = modName.substring(modName.lastIndexOf('@') + 1)
}
if (!version && getPackageVersion) {
version = getPackageVersion?.(pkgName)
}
return [modName, pkgName, version, path]
}
}
async function fetchText(url: string) {
if (!textCache.has(url)) {
textCache.set(
url,
(async () => {
try {
const res = await fetch(url)
if (res.status === 200) {
return await res.text()
}
} catch {
// ignore
}
})(),
)
}
return await textCache.get(url)!
}
async function fetchJson<T>(url: string) {
if (!jsonCache.has(url)) {
jsonCache.set(
url,
(async () => {
try {
const res = await fetch(url)
if (res.status === 200) {
return await res.json()
}
} catch {
// ignore
}
})(),
)
}
return (await jsonCache.get(url)!) as T
}

14
src/monaco/utils.ts Normal file
View File

@ -0,0 +1,14 @@
import { type Uri, editor } from 'monaco-editor-core'
export function getOrCreateModel(
uri: Uri,
lang: string | undefined,
value: string,
) {
const model = editor.getModel(uri)
if (model) {
model.setValue(value)
return model
}
return editor.createModel(value, lang, uri)
}

342
src/monaco/vue.worker.ts Normal file
View File

@ -0,0 +1,342 @@
import {
type LanguageServiceEnvironment,
createTypeScriptWorkerLanguageService,
Language,
} from '@volar/monaco/worker'
import {
type VueCompilerOptions,
VueVirtualCode,
createVueLanguagePlugin,
getDefaultCompilerOptions,
generateGlobalTypes,
getGlobalTypesFileName,
} from '@vue/language-core'
import {
LanguageService,
createVueLanguageServicePlugins,
} from '@vue/language-service'
import type * as monaco from 'monaco-editor-core'
// @ts-expect-error
import * as worker from 'monaco-editor-core/esm/vs/editor/editor.worker'
import { create as createTypeScriptDirectiveCommentPlugin } from 'volar-service-typescript/lib/plugins/directiveComment'
import { create as createTypeScriptSemanticPlugin } from 'volar-service-typescript/lib/plugins/semantic'
import { URI } from 'vscode-uri'
import type { WorkerHost, WorkerMessage } from './env'
import { createVueLanguageServiceProxy } from '@vue/typescript-plugin/lib/common'
import { getComponentDirectives } from '@vue/typescript-plugin/lib/requests/getComponentDirectives'
import { getComponentEvents } from '@vue/typescript-plugin/lib/requests/getComponentEvents'
import { getComponentNames } from '@vue/typescript-plugin/lib/requests/getComponentNames'
import { getComponentProps } from '@vue/typescript-plugin/lib/requests/getComponentProps'
import { getComponentSlots } from '@vue/typescript-plugin/lib/requests/getComponentSlots'
import { getElementAttrs } from '@vue/typescript-plugin/lib/requests/getElementAttrs'
import { getElementNames } from '@vue/typescript-plugin/lib/requests/getElementNames'
import { isRefAtPosition } from '@vue/typescript-plugin/lib/requests/isRefAtPosition'
import { createNpmFileSystem } from './resource'
export interface CreateData {
tsconfig: {
compilerOptions?: import('typescript').CompilerOptions
vueCompilerOptions?: Partial<VueCompilerOptions>
}
dependencies: Record<string, string>
}
const asFileName = (uri: URI) => uri.path
const asUri = (fileName: string): URI => URI.file(fileName)
const createFunc = (func?: string) => (func && typeof func === 'string') ? Function(`return ${func}`)() : undefined
let ts: typeof import('typescript')
let locale: string | undefined
let resourceLinks: Record<
keyof Pick<
WorkerMessage,
'pkgDirUrl' | 'pkgFileTextUrl' | 'pkgLatestVersionUrl'
>,
((...args: any[]) => string) | undefined
>
self.onmessage = async (msg: MessageEvent<WorkerMessage>) => {
if (msg.data?.event === 'init') {
locale = msg.data.tsLocale
ts = await importTsFromCdn(
msg.data.tsVersion,
createFunc(msg.data.typescriptLib),
)
resourceLinks = {
pkgDirUrl: createFunc(msg.data.pkgDirUrl),
pkgFileTextUrl: createFunc(msg.data.pkgFileTextUrl),
pkgLatestVersionUrl: createFunc(msg.data.pkgLatestVersionUrl),
}
self.postMessage('inited')
return
}
worker.initialize(
(
ctx: monaco.worker.IWorkerContext<WorkerHost>,
{ tsconfig, dependencies }: CreateData,
) => {
const env: LanguageServiceEnvironment = {
workspaceFolders: [URI.file('/')],
locale,
fs: createNpmFileSystem(
(uri) => {
if (uri.scheme === 'file') {
if (uri.path === '/node_modules') {
return ''
} else if (uri.path.startsWith('/node_modules/')) {
return uri.path.slice('/node_modules/'.length)
}
}
},
(pkgName) => dependencies[pkgName],
(path, content) => {
ctx.host.onFetchCdnFile(
asUri('/node_modules/' + path).toString(),
content,
)
},
{
getPackageDirectoryUrl: resourceLinks.pkgDirUrl,
getPackageFileTextUrl: resourceLinks.pkgFileTextUrl,
getPackageLatestVersionUrl: resourceLinks.pkgLatestVersionUrl,
},
),
}
const { options: compilerOptions } = ts.convertCompilerOptionsFromJson(
tsconfig?.compilerOptions || {},
'',
)
const vueCompilerOptions: VueCompilerOptions = {
...getDefaultCompilerOptions(),
...tsconfig.vueCompilerOptions,
}
setupGlobalTypes(vueCompilerOptions, env)
const workerService = createTypeScriptWorkerLanguageService({
typescript: ts,
compilerOptions,
workerContext: ctx,
env,
uriConverter: {
asFileName,
asUri,
},
languagePlugins: [
createVueLanguagePlugin(
ts,
compilerOptions,
vueCompilerOptions,
asFileName,
),
],
languageServicePlugins: [
...getTsLanguageServicePlugins(),
...getVueLanguageServicePlugins(),
],
})
return workerService
function setupGlobalTypes(
options: VueCompilerOptions,
env: LanguageServiceEnvironment,
) {
const globalTypes = generateGlobalTypes(options)
const globalTypesPath =
'/node_modules/' + getGlobalTypesFileName(options)
options.globalTypesPath = () => globalTypesPath
const { stat, readFile } = env.fs!
const ctime = Date.now()
env.fs!.stat = async (uri) => {
if (uri.path === globalTypesPath) {
return {
type: 1,
ctime: ctime,
mtime: ctime,
size: globalTypes.length,
}
}
return stat(uri)
}
env.fs!.readFile = async (uri) => {
if (uri.path === globalTypesPath) {
return globalTypes
}
return readFile(uri)
}
}
function getTsLanguageServicePlugins() {
const semanticPlugin = createTypeScriptSemanticPlugin(ts)
const { create } = semanticPlugin
semanticPlugin.create = (context) => {
const created = create(context)
const ls = created.provide[
'typescript/languageService'
]() as import('typescript').LanguageService
const proxy = createVueLanguageServiceProxy(
ts,
new Proxy(
{},
{
get(_target, prop, receiver) {
return Reflect.get(context.language, prop, receiver)
},
},
) as unknown as Language,
ls,
vueCompilerOptions,
asUri,
)
ls.getCompletionsAtPosition = proxy.getCompletionsAtPosition
ls.getCompletionEntryDetails = proxy.getCompletionEntryDetails
ls.getCodeFixesAtPosition = proxy.getCodeFixesAtPosition
ls.getDefinitionAndBoundSpan = proxy.getDefinitionAndBoundSpan
ls.getQuickInfoAtPosition = proxy.getQuickInfoAtPosition
return created
}
return [semanticPlugin, createTypeScriptDirectiveCommentPlugin()]
}
function getVueLanguageServicePlugins() {
const plugins = createVueLanguageServicePlugins(ts, {
getComponentDirectives(fileName) {
return getComponentDirectives(ts, getProgram(), fileName)
},
getComponentEvents(fileName, tag) {
return getComponentEvents(ts, getProgram(), fileName, tag)
},
getComponentNames(fileName) {
return getComponentNames(ts, getProgram(), fileName)
},
getComponentProps(fileName, tag) {
return getComponentProps(ts, getProgram(), fileName, tag)
},
getComponentSlots(fileName) {
const { virtualCode } = getVirtualCode(fileName)
return getComponentSlots(ts, getProgram(), virtualCode)
},
getElementAttrs(fileName, tag) {
return getElementAttrs(ts, getProgram(), fileName, tag)
},
getElementNames(fileName) {
return getElementNames(ts, getProgram(), fileName)
},
isRefAtPosition(fileName, position) {
const { sourceScript, virtualCode } = getVirtualCode(fileName)
return isRefAtPosition(
ts,
getLanguageService().context.language,
getProgram(),
sourceScript,
virtualCode,
position,
)
},
async getQuickInfoAtPosition(fileName, position) {
const uri = asUri(fileName)
const sourceScript =
getLanguageService().context.language.scripts.get(uri)
if (!sourceScript) {
return
}
const hover = await getLanguageService().getHover(uri, position)
let text = ''
if (typeof hover?.contents === 'string') {
text = hover.contents
} else if (Array.isArray(hover?.contents)) {
text = hover.contents
.map((c) => (typeof c === 'string' ? c : c.value))
.join('\n')
} else if (hover) {
text = hover.contents.value
}
text = text.replace(/```typescript/g, '')
text = text.replace(/```/g, '')
text = text.replace(/---/g, '')
text = text.trim()
while (true) {
const newText = text.replace(/\n\n/g, '\n')
if (newText === text) {
break
}
text = newText
}
text = text.replace(/\n/g, ' | ')
return text
},
collectExtractProps() {
throw new Error('Not implemented')
},
getImportPathForFile() {
throw new Error('Not implemented')
},
getDocumentHighlights() {
throw new Error('Not implemented')
},
getEncodedSemanticClassifications() {
throw new Error('Not implemented')
},
getReactiveReferences() {
throw new Error('Not implemented')
},
})
const ignoreVueServicePlugins = new Set([
'vue-extract-file',
'vue-document-drop',
'vue-document-highlights',
'typescript-semantic-tokens',
])
return plugins.filter(
(plugin) => !ignoreVueServicePlugins.has(plugin.name!),
)
function getVirtualCode(fileName: string) {
const uri = asUri(fileName)
const sourceScript =
getLanguageService().context.language.scripts.get(uri)
if (!sourceScript) {
throw new Error('No source script found for file: ' + fileName)
}
const virtualCode = sourceScript.generated?.root
if (!(virtualCode instanceof VueVirtualCode)) {
throw new Error('No virtual code found for file: ' + fileName)
}
return {
sourceScript,
virtualCode,
}
}
function getProgram() {
const tsService: import('typescript').LanguageService =
getLanguageService().context.inject('typescript/languageService')
return tsService.getProgram()!
}
function getLanguageService() {
return (workerService as any).languageService as LanguageService
}
}
},
)
}
async function importTsFromCdn(
tsVersion: string,
getTsCdn?: (version?: string) => string,
) {
const _module = globalThis.module
;(globalThis as any).module = { exports: {} }
const tsUrl =
getTsCdn?.(tsVersion) ||
`https://cdn.jsdelivr.net/npm/typescript@${tsVersion}/lib/typescript.js`
await import(/* @vite-ignore */ tsUrl)
const ts = globalThis.module.exports
globalThis.module = _module
return ts as typeof import('typescript')
}

130
src/output/Output.vue Normal file
View File

@ -0,0 +1,130 @@
<script setup lang="ts">
import Preview from './Preview.vue'
import SsrOutput from './SsrOutput.vue'
import { computed, inject, useTemplateRef, watchEffect } from 'vue'
import {
type EditorComponentType,
type OutputModes,
injectKeyProps,
} from '../types'
const props = defineProps<{
editorComponent: EditorComponentType
showCompileOutput?: boolean
showOpenSourceMap?: boolean
showSsrOutput?: boolean
ssr: boolean
}>()
const { store } = inject(injectKeyProps)!
const previewRef = useTemplateRef('preview')
const modes = computed(() => {
const outputModes: OutputModes[] = ['preview']
if (props.showCompileOutput) {
outputModes.push('js', 'css', 'ssr')
}
if (props.ssr && props.showSsrOutput) {
outputModes.push('ssr output')
}
return outputModes
})
const mode = computed<OutputModes>({
get: () => store.value.outputMode,
set: (value) => (store.value.outputMode = value),
})
watchEffect(() => {
if (!modes.value.includes(mode.value)) {
mode.value = modes.value[0]
}
})
const showSourceMap = computed(() => {
return props.showOpenSourceMap && (mode.value === 'js' || mode.value === 'ssr')
})
function openSourceMap() {
const { clientMap, ssrMap } = store.value.activeFile.compiled
window.open(mode.value === 'js' ? clientMap : ssrMap)
}
function reload() {
previewRef.value?.reload()
}
defineExpose({ reload, previewRef })
</script>
<template>
<div class="tab-buttons">
<button
v-for="m of modes"
:key="m"
:class="{ active: mode === m }"
@click="mode = m"
>
<span>{{ m }}</span>
</button>
</div>
<div class="output-container">
<Preview ref="preview" :show="mode === 'preview'" :ssr="ssr" />
<SsrOutput
v-if="mode === 'ssr output'"
:context="store.ssrOutput.context"
:html="store.ssrOutput.html"
/>
<props.editorComponent
v-else-if="mode !== 'preview'"
readonly
:filename="store.activeFile.filename"
:value="store.activeFile.compiled[mode]"
:mode="mode"
/>
</div>
<div v-if="showSourceMap" class="tab-buttons open-sourcemap">
<button>
<span @click="openSourceMap">open sourceMap</span>
</button>
</div>
</template>
<style scoped>
.output-container {
height: calc(100% - var(--header-height));
overflow: hidden;
position: relative;
}
.tab-buttons {
box-sizing: border-box;
border-bottom: 1px solid var(--border);
background-color: var(--bg);
height: var(--header-height);
overflow: hidden;
}
.tab-buttons button {
padding: 0;
box-sizing: border-box;
}
.tab-buttons span {
font-size: 13px;
font-family: var(--font-code);
text-transform: uppercase;
color: var(--text-light);
display: inline-block;
padding: 8px 16px 6px;
line-height: 20px;
}
button.active {
color: var(--color-branding-dark);
border-bottom: 3px solid var(--color-branding-dark);
}
.open-sourcemap {
position: absolute;
right: 0;
top: 0;
}
</style>

34
src/output/Preview.vue Normal file
View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import { computed, inject, useTemplateRef } from 'vue'
import { injectKeyProps } from '../../src/types'
import Sandbox from './Sandbox.vue'
const props = defineProps<{ show: boolean; ssr: boolean }>()
const { store, clearConsole, theme, previewTheme, previewOptions } =
inject(injectKeyProps)!
const sandboxTheme = computed(() =>
previewTheme.value ? theme.value : undefined,
)
const sandboxRef = useTemplateRef('sandbox')
const container = computed(() => sandboxRef.value?.container)
defineExpose({
reload: () => sandboxRef.value?.reload(),
container,
})
</script>
<template>
<Sandbox
ref="sandbox"
:show="props.show"
:store="store"
:theme="sandboxTheme"
:preview-options="previewOptions"
:ssr="props.ssr"
:clear-console="clearConsole"
/>
</template>

View File

@ -0,0 +1,96 @@
// ReplProxy and srcdoc implementation from Svelte REPL
// MIT License https://github.com/sveltejs/svelte-repl/blob/master/LICENSE
let uid = 1
export class PreviewProxy {
iframe: HTMLIFrameElement
handlers: Record<string, Function>
pending_cmds: Map<
number,
{ resolve: (value: unknown) => void; reject: (reason?: any) => void }
>
handle_event: (e: any) => void
constructor(iframe: HTMLIFrameElement, handlers: Record<string, Function>) {
this.iframe = iframe
this.handlers = handlers
this.pending_cmds = new Map()
this.handle_event = (e) => this.handle_repl_message(e)
window.addEventListener('message', this.handle_event, false)
}
destroy() {
window.removeEventListener('message', this.handle_event)
}
iframe_command(action: string, args: any) {
return new Promise((resolve, reject) => {
const cmd_id = uid++
this.pending_cmds.set(cmd_id, { resolve, reject })
this.iframe.contentWindow!.postMessage({ action, cmd_id, args }, '*')
})
}
handle_command_message(cmd_data: any) {
let action = cmd_data.action
let id = cmd_data.cmd_id
let handler = this.pending_cmds.get(id)
if (handler) {
this.pending_cmds.delete(id)
if (action === 'cmd_error') {
let { message, stack } = cmd_data
let e = new Error(message)
e.stack = stack
handler.reject(e)
}
if (action === 'cmd_ok') {
handler.resolve(cmd_data.args)
}
} else if (action !== 'cmd_error' && action !== 'cmd_ok') {
console.error('command not found', id, cmd_data, [
...this.pending_cmds.keys(),
])
}
}
handle_repl_message(event: any) {
if (event.source !== this.iframe.contentWindow) return
const { action, args } = event.data
switch (action) {
case 'cmd_error':
case 'cmd_ok':
return this.handle_command_message(event.data)
case 'fetch_progress':
return this.handlers.on_fetch_progress(args.remaining)
case 'error':
return this.handlers.on_error(event.data)
case 'unhandledrejection':
return this.handlers.on_unhandled_rejection(event.data)
case 'console':
return this.handlers.on_console(event.data)
case 'console_group':
return this.handlers.on_console_group(event.data)
case 'console_group_collapsed':
return this.handlers.on_console_group_collapsed(event.data)
case 'console_group_end':
return this.handlers.on_console_group_end(event.data)
}
}
eval(script: string | string[]) {
return this.iframe_command('eval', { script })
}
handle_links() {
return this.iframe_command('catch_clicks', {})
}
}

380
src/output/Sandbox.vue Normal file
View File

@ -0,0 +1,380 @@
<script setup lang="ts">
import Message from '../Message.vue'
import {
type WatchStopHandle,
inject,
onMounted,
onUnmounted,
ref,
toRefs,
useTemplateRef,
watch,
watchEffect,
} from 'vue'
import srcdoc from './srcdoc.html?raw'
import { PreviewProxy } from './PreviewProxy'
import { compileModulesForPreview } from './moduleCompiler'
import type { Store } from '../store'
import { injectKeyProps } from '../types'
import { getVersions, isVaporSupported } from '../import-map'
export interface SandboxProps {
store: Store
show?: boolean
ssr?: boolean
clearConsole?: boolean
theme?: 'dark' | 'light'
previewOptions?: {
headHTML?: string
bodyHTML?: string
placeholderHTML?: string
customCode?: {
importCode?: string
useCode?: string
}
showRuntimeError?: boolean
showRuntimeWarning?: boolean
}
/** @default true */
autoStoreInit?: boolean
}
const props = withDefaults(defineProps<SandboxProps>(), {
show: true,
ssr: false,
theme: 'light',
clearConsole: true,
previewOptions: () => ({}),
autoStoreInit: true,
})
const { store, theme, clearConsole, previewOptions } = toRefs(props)
const keyProps = inject(injectKeyProps)
if (keyProps === undefined && props.autoStoreInit) {
props.store?.init?.()
}
const containerRef = useTemplateRef('container')
const runtimeError = ref<string>()
const runtimeWarning = ref<string>()
let sandbox: HTMLIFrameElement
let proxy: PreviewProxy
let stopUpdateWatcher: WatchStopHandle | undefined
// create sandbox on mount
onMounted(createSandbox)
// reset sandbox when import map changes
watch(
() => store.value.getImportMap(),
() => {
try {
createSandbox()
} catch (e: any) {
store.value.errors = [e as Error]
return
}
},
)
function switchPreviewTheme() {
const html = sandbox.contentDocument?.documentElement
if (html) {
html.className = theme.value
} else {
// re-create sandbox
createSandbox()
}
}
watch(theme, switchPreviewTheme)
onUnmounted(() => {
proxy.destroy()
stopUpdateWatcher && stopUpdateWatcher()
})
function createSandbox() {
if (sandbox) {
// clear prev sandbox
proxy.destroy()
stopUpdateWatcher && stopUpdateWatcher()
containerRef.value?.removeChild(sandbox)
}
sandbox = document.createElement('iframe')
sandbox.setAttribute(
'sandbox',
[
'allow-forms',
'allow-modals',
'allow-pointer-lock',
'allow-popups',
'allow-same-origin',
'allow-scripts',
'allow-top-navigation-by-user-activation',
].join(' '),
)
const importMap = store.value.getImportMap()
const sandboxSrc = srcdoc
.replace(/<html>/, `<html class="${theme.value}">`)
.replace(/<!--IMPORT_MAP-->/, JSON.stringify(importMap))
.replace(
/<!-- PREVIEW-OPTIONS-HEAD-HTML -->/,
previewOptions.value?.headHTML || '',
)
.replace(
/<!--PREVIEW-OPTIONS-PLACEHOLDER-HTML-->/,
previewOptions.value?.placeholderHTML || '',
)
.replace(
/<!--ES-MODULE-SHIMS-LINK-->/,
store.value.resourceLinks?.esModuleShims ||
'https://cdn.jsdelivr.net/npm/es-module-shims@1.5.18/dist/es-module-shims.wasm.js',
)
sandbox.srcdoc = sandboxSrc
containerRef.value?.appendChild(sandbox)
proxy = new PreviewProxy(sandbox, {
on_fetch_progress: (progress: any) => {
// pending_imports = progress;
},
on_error: (event: any) => {
const msg =
event.value instanceof Error ? event.value.message : event.value
if (
msg.includes('Failed to resolve module specifier') ||
msg.includes('Error resolving module specifier')
) {
runtimeError.value =
msg.replace(/\. Relative references must.*$/, '') +
`.\nTip: edit the "Import Map" tab to specify import paths for dependencies.`
} else {
runtimeError.value = event.value
}
},
on_unhandled_rejection: (event: any) => {
let error = event.value
if (typeof error === 'string') {
error = { message: error }
}
runtimeError.value = 'Uncaught (in promise): ' + error.message
},
on_console: (log: any) => {
if (log.duplicate) {
return
}
if (log.level === 'error') {
if (log.args[0] instanceof Error) {
runtimeError.value = log.args[0].message
} else {
runtimeError.value = log.args[0]
}
} else if (log.level === 'warn') {
if (log.args[0].toString().includes('[Vue warn]')) {
runtimeWarning.value = log.args
.join('')
.replace(/\[Vue warn\]:/, '')
.trim()
}
}
},
on_console_group: (action: any) => {
// group_logs(action.label, false);
},
on_console_group_end: () => {
// ungroup_logs();
},
on_console_group_collapsed: (action: any) => {
// group_logs(action.label, true);
},
})
sandbox.addEventListener('load', () => {
proxy.handle_links()
stopUpdateWatcher = watchEffect(updatePreview)
switchPreviewTheme()
})
}
async function updatePreview() {
if (import.meta.env.PROD && clearConsole.value) {
console.clear()
}
runtimeError.value = undefined
runtimeWarning.value = undefined
let isSSR = props.ssr
if (store.value.vueVersion) {
const [major, minor, patch] = getVersions(store.value.vueVersion)
if (major === 3 && (minor < 2 || (minor === 2 && patch < 27))) {
alert(
`The selected version of Vue (${store.value.vueVersion}) does not support in-browser SSR.` +
` Rendering in client mode instead.`,
)
isSSR = false
}
}
const vaporSupported = isVaporSupported(
store.value.vueVersion || store.value.compiler?.version
)
try {
const { mainFile } = store.value
// if SSR, generate the SSR bundle and eval it to render the HTML
if (isSSR && mainFile.endsWith('.vue')) {
const ssrModules = compileModulesForPreview(store.value, true)
console.info(
`[@vue/repl] successfully compiled ${ssrModules.length} modules for SSR.`,
)
store.value.ssrOutput.html = store.value.ssrOutput.context = ''
const response = await proxy.eval([
`const __modules__ = {};`,
...ssrModules,
`import { renderToString as _renderToString } from 'vue/server-renderer'
import { createSSRApp as _createApp ${vaporSupported ? ', createVaporSSRApp as _createVaporApp' : ''} } from 'vue'
const AppComponent = __modules__["${mainFile}"].default
AppComponent.name = 'Repl'
const vaporSupported = ${vaporSupported}
const app = (vaporSupported && AppComponent.__vapor ? _createVaporApp : _createApp)(AppComponent)
if (!app.config.hasOwnProperty('unwrapInjectedRef')) {
app.config.unwrapInjectedRef = true
}
app.config.warnHandler = () => {}
const rawContext = {}
window.__ssr_promise__ = _renderToString(app, rawContext).then(html => {
document.body.innerHTML = '<div id="app">' + html + '</div>' + \`${
previewOptions.value?.bodyHTML || ''
}\`
const safeContext = {}
const isSafe = (v) =>
v === null ||
typeof v === 'boolean' ||
typeof v === 'string' ||
Number.isFinite(v)
const toSafe = (v) => (isSafe(v) ? v : '[' + typeof v + ']')
for (const prop in rawContext) {
const value = rawContext[prop]
safeContext[prop] = isSafe(value)
? value
: Array.isArray(value)
? value.map(toSafe)
: typeof value === 'object'
? Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, toSafe(v)]),
)
: toSafe(value)
}
return { ssrHtml: html, ssrContext: safeContext }
}).catch(err => {
console.error("SSR Error", err)
})
`,
])
if (response) {
store.value.ssrOutput.html = String((response as any).ssrHtml ?? '')
store.value.ssrOutput.context = (response as any).ssrContext || ''
}
}
// compile code to simulated module system
const modules = compileModulesForPreview(store.value)
console.info(
`[@vue/repl] successfully compiled ${modules.length} module${
modules.length > 1 ? `s` : ``
}.`,
)
const codeToEval = [
`window.__modules__ = {};window.__css__ = [];` +
`if (window.__app__) window.__app__.unmount();` +
(isSSR
? ``
: `document.body.innerHTML = '<div id="app"></div>' + \`${
previewOptions.value?.bodyHTML || ''
}\``),
...modules,
`document.querySelectorAll('style[css]').forEach(el => el.remove())
document.head.insertAdjacentHTML('beforeend', window.__css__.map(s => \`<style css>\${s}</style>\`).join('\\n'))`,
]
// if main file is a vue file, mount it.
if (mainFile.endsWith('.vue')) {
codeToEval.push(
`import { ${isSSR ? `createSSRApp` : `createApp`} as _createApp ${
vaporSupported
? `, ${
isSSR ? 'createVaporSSRApp' : 'createVaporApp'
} as _createVaporApp`
: ''
} } from "vue"
${previewOptions.value?.customCode?.importCode || ''}
const _mount = () => {
const AppComponent = __modules__["${mainFile}"].default
AppComponent.name = 'Repl'
const vaporSupported = ${vaporSupported}
const app = window.__app__ = (vaporSupported && AppComponent.__vapor ? _createVaporApp : _createApp)(AppComponent)
if (!app.config.hasOwnProperty('unwrapInjectedRef')) {
app.config.unwrapInjectedRef = true
}
app.config.errorHandler = e => console.error(e)
${previewOptions.value?.customCode?.useCode || ''}
app.mount('#app')
}
if (window.__ssr_promise__) {
window.__ssr_promise__.then(_mount)
} else {
_mount()
}`,
)
}
// eval code in sandbox
await proxy.eval(codeToEval)
} catch (e: any) {
console.error(e)
runtimeError.value = (e as Error).message
}
}
/**
* Reload the preview iframe
*/
function reload() {
sandbox.contentWindow?.location.reload()
}
defineExpose({ reload, container: containerRef })
</script>
<template>
<div
v-show="props.show"
ref="container"
class="iframe-container"
:class="theme"
/>
<Message :err="(previewOptions?.showRuntimeError ?? true) && runtimeError" />
<Message
v-if="!runtimeError && (previewOptions?.showRuntimeWarning ?? true)"
:warn="runtimeWarning"
/>
</template>
<style scoped>
.iframe-container,
.iframe-container :deep(iframe) {
width: 100%;
height: 100%;
border: none;
background-color: #fff;
}
.iframe-container.dark :deep(iframe) {
background-color: #1e1e1e;
}
</style>

32
src/output/SsrOutput.vue Normal file
View File

@ -0,0 +1,32 @@
<script setup lang="ts">
defineProps<{
html: string
context: unknown
}>()
</script>
<template>
<div class="ssr-output">
<strong>HTML</strong>
<pre class="ssr-output-pre">{{ html }}</pre>
<strong>Context</strong>
<pre class="ssr-output-pre">{{ context }}</pre>
</div>
</template>
<style scoped>
.ssr-output {
background: var(--bg);
box-sizing: border-box;
color: var(--text-light);
height: 100%;
overflow: auto;
padding: 10px;
width: 100%;
}
.ssr-output-pre {
font-family: var(--font-code);
white-space: pre-wrap;
}
</style>

View File

@ -0,0 +1,351 @@
import type { File, Store } from '../store'
import {
MagicString,
babelParse,
extractIdentifiers,
isInDestructureAssignment,
isStaticProperty,
walk,
walkIdentifiers,
} from 'vue/compiler-sfc'
import type { ExportSpecifier, Identifier, Node } from '@babel/types'
export function compileModulesForPreview(store: Store, isSSR = false) {
const seen = new Set<File>()
const processed: string[] = []
processFile(store, store.files[store.mainFile], processed, seen, isSSR)
if (!isSSR) {
// also add css files that are not imported
for (const filename in store.files) {
if (filename.endsWith('.css')) {
const file = store.files[filename]
if (!seen.has(file)) {
processed.push(
`\nwindow.__css__.push(${JSON.stringify(file.compiled.css)})`,
)
}
}
}
}
return processed
}
const modulesKey = `__modules__`
const exportKey = `__export__`
const dynamicImportKey = `__dynamic_import__`
const moduleKey = `__module__`
// similar logic with Vite's SSR transform, except this is targeting the browser
function processFile(
store: Store,
file: File,
processed: string[],
seen: Set<File>,
isSSR: boolean,
) {
if (seen.has(file)) {
return []
}
seen.add(file)
if (!isSSR && file.filename.endsWith('.html')) {
return processHtmlFile(store, file.code, file.filename, processed, seen)
}
let {
code: js,
importedFiles,
hasDynamicImport,
} = processModule(
store,
isSSR ? file.compiled.ssr : file.compiled.js,
file.filename,
)
processChildFiles(
store,
importedFiles,
hasDynamicImport,
processed,
seen,
isSSR,
)
// append css
if (file.compiled.css && !isSSR) {
js += `\nwindow.__css__.push(${JSON.stringify(file.compiled.css)})`
}
// push self
processed.push(js)
}
function processChildFiles(
store: Store,
importedFiles: Set<string>,
hasDynamicImport: boolean,
processed: string[],
seen: Set<File>,
isSSR: boolean,
) {
if (hasDynamicImport) {
// process all files
for (const file of Object.values(store.files)) {
if (seen.has(file)) continue
processFile(store, file, processed, seen, isSSR)
}
} else if (importedFiles.size > 0) {
// crawl child imports
for (const imported of importedFiles) {
processFile(store, store.files[imported], processed, seen, isSSR)
}
}
}
function processModule(store: Store, src: string, filename: string) {
const s = new MagicString(src)
const ast = babelParse(src, {
sourceFilename: filename,
sourceType: 'module',
}).program.body
const idToImportMap = new Map<string, string>()
const declaredConst = new Set<string>()
const importedFiles = new Set<string>()
const importToIdMap = new Map<string, string>()
function resolveImport(raw: string): string | undefined {
const files = store.files
let resolved = raw
const file =
files[resolved] ||
files[(resolved = raw + '.ts')] ||
files[(resolved = raw + '.js')]
return file ? resolved : undefined
}
function defineImport(node: Node, source: string) {
const filename = resolveImport(source.replace(/^\.\/+/, 'src/'))
if (!filename) {
throw new Error(`File "${source}" does not exist.`)
}
if (importedFiles.has(filename)) {
return importToIdMap.get(filename)!
}
importedFiles.add(filename)
const id = `__import_${importedFiles.size}__`
importToIdMap.set(filename, id)
s.appendLeft(
node.start!,
`const ${id} = ${modulesKey}[${JSON.stringify(filename)}]\n`,
)
return id
}
function defineExport(name: string, local = name) {
s.append(`\n${exportKey}(${moduleKey}, "${name}", () => ${local})`)
}
// 0. instantiate module
s.prepend(
`const ${moduleKey} = ${modulesKey}[${JSON.stringify(
filename,
)}] = { [Symbol.toStringTag]: "Module" }\n\n`,
)
// 1. check all import statements and record id -> importName map
for (const node of ast) {
// import foo from 'foo' --> foo -> __import_foo__.default
// import { baz } from 'foo' --> baz -> __import_foo__.baz
// import * as ok from 'foo' --> ok -> __import_foo__
if (node.type === 'ImportDeclaration') {
const source = node.source.value
if (source.startsWith('./')) {
const importId = defineImport(node, node.source.value)
for (const spec of node.specifiers) {
if (spec.type === 'ImportSpecifier') {
idToImportMap.set(
spec.local.name,
`${importId}.${(spec.imported as Identifier).name}`,
)
} else if (spec.type === 'ImportDefaultSpecifier') {
idToImportMap.set(spec.local.name, `${importId}.default`)
} else {
// namespace specifier
idToImportMap.set(spec.local.name, importId)
}
}
s.remove(node.start!, node.end!)
}
}
}
// 2. check all export statements and define exports
for (const node of ast) {
// named exports
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
if (
node.declaration.type === 'FunctionDeclaration' ||
node.declaration.type === 'ClassDeclaration'
) {
// export function foo() {}
defineExport(node.declaration.id!.name)
} else if (node.declaration.type === 'VariableDeclaration') {
// export const foo = 1, bar = 2
for (const decl of node.declaration.declarations) {
for (const id of extractIdentifiers(decl.id)) {
defineExport(id.name)
}
}
}
s.remove(node.start!, node.declaration.start!)
} else if (node.source && node.source.value.startsWith('./')) {
// export { foo, bar } from './foo'
const importId = defineImport(node, node.source.value)
for (const spec of node.specifiers) {
defineExport(
(spec.exported as Identifier).name,
`${importId}.${(spec as ExportSpecifier).local.name}`,
)
}
s.remove(node.start!, node.end!)
} else {
// export { foo, bar }
for (const spec of node.specifiers) {
const local = (spec as ExportSpecifier).local.name
const binding = idToImportMap.get(local)
defineExport((spec.exported as Identifier).name, binding || local)
}
s.remove(node.start!, node.end!)
}
}
// default export
if (node.type === 'ExportDefaultDeclaration') {
if ('id' in node.declaration && node.declaration.id) {
// named hoistable/class exports
// export default function foo() {}
// export default class A {}
const { name } = node.declaration.id
s.remove(node.start!, node.start! + 15)
s.append(`\n${exportKey}(${moduleKey}, "default", () => ${name})`)
} else {
// anonymous default exports
s.overwrite(node.start!, node.start! + 14, `${moduleKey}.default =`)
}
}
// export * from './foo'
if (node.type === 'ExportAllDeclaration') {
const importId = defineImport(node, node.source.value)
s.remove(node.start!, node.end!)
s.append(`\nfor (const key in ${importId}) {
if (key !== 'default') {
${exportKey}(${moduleKey}, key, () => ${importId}[key])
}
}`)
}
}
// 3. convert references to import bindings
for (const node of ast) {
if (node.type === 'ImportDeclaration') continue
walkIdentifiers(node, (id, parent, parentStack) => {
const binding = idToImportMap.get(id.name)
if (!binding) {
return
}
if (parent && isStaticProperty(parent) && parent.shorthand) {
// let binding used in a property shorthand
// { foo } -> { foo: __import_x__.foo }
// skip for destructure patterns
if (
!(parent as any).inPattern ||
isInDestructureAssignment(parent, parentStack)
) {
s.appendLeft(id.end!, `: ${binding}`)
}
} else if (
parent &&
parent.type === 'ClassDeclaration' &&
id === parent.superClass
) {
if (!declaredConst.has(id.name)) {
declaredConst.add(id.name)
// locate the top-most node containing the class declaration
const topNode = parentStack[1]
s.prependRight(topNode.start!, `const ${id.name} = ${binding};\n`)
}
} else {
s.overwrite(id.start!, id.end!, binding)
}
})
}
// 4. convert dynamic imports
let hasDynamicImport = false
walk(ast, {
enter(node: Node, parent: Node) {
if (node.type === 'Import' && parent.type === 'CallExpression') {
const arg = parent.arguments[0]
if (arg.type === 'StringLiteral' && arg.value.startsWith('./')) {
hasDynamicImport = true
s.overwrite(node.start!, node.start! + 6, dynamicImportKey)
s.overwrite(
arg.start!,
arg.end!,
JSON.stringify(arg.value.replace(/^\.\/+/, 'src/')),
)
}
}
},
})
return {
code: s.toString(),
importedFiles,
hasDynamicImport,
}
}
const scriptRE = /<script\b(?:\s[^>]*>|>)([^]*?)<\/script>/gi
const scriptModuleRE =
/<script\b[^>]*type\s*=\s*(?:"module"|'module')[^>]*>([^]*?)<\/script>/gi
function processHtmlFile(
store: Store,
src: string,
filename: string,
processed: string[],
seen: Set<File>,
) {
const deps: string[] = []
let jsCode = ''
const html = src
.replace(scriptModuleRE, (_, content) => {
const { code, importedFiles, hasDynamicImport } = processModule(
store,
content,
filename,
)
processChildFiles(
store,
importedFiles,
hasDynamicImport,
deps,
seen,
false,
)
jsCode += '\n' + code
return ''
})
.replace(scriptRE, (_, content) => {
jsCode += '\n' + content
return ''
})
processed.push(`document.body.innerHTML = ${JSON.stringify(html)}`)
processed.push(...deps)
processed.push(jsCode)
}

378
src/output/srcdoc.html Normal file
View File

@ -0,0 +1,378 @@
<!doctype html>
<html>
<head>
<style>
html.dark {
color-scheme: dark;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
</style>
<!-- PREVIEW-OPTIONS-HEAD-HTML -->
<script>
;(() => {
let scriptEls = []
window.process = { env: {} }
window.__modules__ = {}
window.__export__ = (mod, key, get) => {
Object.defineProperty(mod, key, {
enumerable: true,
configurable: true,
get,
})
}
window.__dynamic_import__ = (key) => {
return Promise.resolve(window.__modules__[key])
}
async function handle_message(ev) {
let { action, cmd_id } = ev.data
const send_message = (payload) =>
parent.postMessage({ ...payload }, ev.origin)
const send_reply = (payload) => send_message({ ...payload, cmd_id })
const send_ok = (response) =>
send_reply({ action: 'cmd_ok', args: response })
const send_error = (message, stack) =>
send_reply({ action: 'cmd_error', message, stack })
if (action === 'eval') {
try {
if (scriptEls.length) {
scriptEls.forEach((el) => {
document.head.removeChild(el)
})
scriptEls.length = 0
}
let { script: scripts } = ev.data.args
if (typeof scripts === 'string') scripts = [scripts]
for (const script of scripts) {
const scriptEl = document.createElement('script')
scriptEl.setAttribute('type', 'module')
// send ok in the module script to ensure sequential evaluation
// of multiple proxy.eval() calls
const done = new Promise((resolve) => {
window.__next__ = resolve
})
scriptEl.innerHTML = script + `\nwindow.__next__()`
document.head.appendChild(scriptEl)
scriptEl.onerror = (err) => send_error(err.message, err.stack)
scriptEls.push(scriptEl)
await done
}
if (window.__ssr_promise__) {
send_ok(await window.__ssr_promise__)
} else {
send_ok()
}
} catch (e) {
send_error(e.message, e.stack)
}
}
if (action === 'catch_clicks') {
try {
const top_origin = ev.origin
document.body.addEventListener('click', (event) => {
if (event.which !== 1) return
if (event.metaKey || event.ctrlKey || event.shiftKey) return
if (event.defaultPrevented) return
// ensure target is a link
let el = event.target
while (el && el.nodeName !== 'A') el = el.parentNode
if (!el || el.nodeName !== 'A') return
if (
el.hasAttribute('download') ||
el.getAttribute('rel') === 'external' ||
el.target ||
el.href.startsWith('javascript:') ||
!el.href
)
return
event.preventDefault()
if (el.href.startsWith(top_origin)) {
const url = new URL(el.href)
if (url.hash[0] === '#') {
window.location.hash = url.hash
return
}
}
window.open(el.href, '_blank')
})
send_ok()
} catch (e) {
send_error(e.message, e.stack)
}
}
}
window.addEventListener('message', handle_message, false)
window.onerror = function (msg, url, lineNo, columnNo, error) {
// ignore errors from import map polyfill - these are necessary for
// it to detect browser support
if (msg.includes('module specifier “vue”')) {
// firefox only error, ignore
return false
}
if (msg.includes("Module specifier, 'vue")) {
// Safari only
return false
}
try {
parent.postMessage({ action: 'error', value: error }, '*')
} catch (e) {
parent.postMessage({ action: 'error', value: msg }, '*')
}
}
window.addEventListener('unhandledrejection', (event) => {
if (
event.reason.message &&
event.reason.message.includes('Cross-origin')
) {
event.preventDefault()
return
}
try {
parent.postMessage(
{ action: 'unhandledrejection', value: event.reason },
'*',
)
} catch (e) {
parent.postMessage(
{ action: 'unhandledrejection', value: event.reason.message },
'*',
)
}
})
let previous = { level: null, args: null }
;['clear', 'log', 'info', 'dir', 'warn', 'error', 'table'].forEach(
(level) => {
const original = console[level]
console[level] = (...args) => {
const msg = args[0]
if (typeof msg === 'string') {
if (
msg.includes('You are running a development build of Vue') ||
msg.includes('You are running the esm-bundler build of Vue')
) {
return
}
}
original(...args)
const stringifiedArgs = stringify(args)
if (
previous.level === level &&
previous.args &&
previous.args === stringifiedArgs
) {
parent.postMessage(
{ action: 'console', level, duplicate: true },
'*',
)
} else {
previous = { level, args: stringifiedArgs }
try {
parent.postMessage({ action: 'console', level, args }, '*')
} catch (err) {
parent.postMessage(
{ action: 'console', level, args: args.map(toString) },
'*',
)
}
}
}
},
)
;[
{ method: 'group', action: 'console_group' },
{ method: 'groupEnd', action: 'console_group_end' },
{ method: 'groupCollapsed', action: 'console_group_collapsed' },
].forEach((group_action) => {
const original = console[group_action.method]
console[group_action.method] = (label) => {
parent.postMessage({ action: group_action.action, label }, '*')
original(label)
}
})
const timers = new Map()
const original_time = console.time
const original_timelog = console.timeLog
const original_timeend = console.timeEnd
console.time = (label = 'default') => {
original_time(label)
timers.set(label, performance.now())
}
console.timeLog = (label = 'default') => {
original_timelog(label)
const now = performance.now()
if (timers.has(label)) {
parent.postMessage(
{
action: 'console',
level: 'system-log',
args: [`${label}: ${now - timers.get(label)}ms`],
},
'*',
)
} else {
parent.postMessage(
{
action: 'console',
level: 'system-warn',
args: [`Timer '${label}' does not exist`],
},
'*',
)
}
}
console.timeEnd = (label = 'default') => {
original_timeend(label)
const now = performance.now()
if (timers.has(label)) {
parent.postMessage(
{
action: 'console',
level: 'system-log',
args: [`${label}: ${now - timers.get(label)}ms`],
},
'*',
)
} else {
parent.postMessage(
{
action: 'console',
level: 'system-warn',
args: [`Timer '${label}' does not exist`],
},
'*',
)
}
timers.delete(label)
}
const original_assert = console.assert
console.assert = (condition, ...args) => {
if (condition) {
const stack = new Error().stack
parent.postMessage(
{ action: 'console', level: 'assert', args, stack },
'*',
)
}
original_assert(condition, ...args)
}
const counter = new Map()
const original_count = console.count
const original_countreset = console.countReset
console.count = (label = 'default') => {
counter.set(label, (counter.get(label) || 0) + 1)
parent.postMessage(
{
action: 'console',
level: 'system-log',
args: `${label}: ${counter.get(label)}`,
},
'*',
)
original_count(label)
}
console.countReset = (label = 'default') => {
if (counter.has(label)) {
counter.set(label, 0)
} else {
parent.postMessage(
{
action: 'console',
level: 'system-warn',
args: `Count for '${label}' does not exist`,
},
'*',
)
}
original_countreset(label)
}
const original_trace = console.trace
console.trace = (...args) => {
const stack = new Error().stack
parent.postMessage(
{ action: 'console', level: 'trace', args, stack },
'*',
)
original_trace(...args)
}
function toString(value) {
if (value instanceof Error) {
return value.message
}
for (const fn of [
String,
(v) => Object.prototype.toString.call(v),
(v) => typeof v,
]) {
try {
return fn(value)
} catch (err) {}
}
}
function isComponentProxy(value) {
return (
value &&
typeof value === 'object' &&
value.__v_skip === true &&
typeof value.$nextTick === 'function' &&
value.$ &&
value._
)
}
function stringify(args) {
try {
return JSON.stringify(args, (key, value) => {
return isComponentProxy(value) ? '{component proxy}' : value
})
} catch (error) {
return null
}
}
})()
</script>
<!-- ES Module Shims: Import maps polyfill for modules browsers without import maps support (all except Chrome 89+) -->
<script async src="<!--ES-MODULE-SHIMS-LINK-->"></script>
<script type="importmap">
<!--IMPORT_MAP-->
</script>
</head>
<body>
<!--PREVIEW-OPTIONS-PLACEHOLDER-HTML-->
</body>
</html>

119
src/sourcemap.ts Normal file
View File

@ -0,0 +1,119 @@
import type { RawSourceMap } from 'source-map-js'
import type { EncodedSourceMap as TraceEncodedSourceMap } from '@jridgewell/trace-mapping'
import { TraceMap, eachMapping } from '@jridgewell/trace-mapping'
import type { EncodedSourceMap as GenEncodedSourceMap } from '@jridgewell/gen-mapping'
import { addMapping, fromMap, toEncodedMap } from '@jridgewell/gen-mapping'
// trim analyzed bindings comment
export function trimAnalyzedBindings(scriptCode: string) {
return scriptCode.replace(/\/\*[\s\S]*?\*\/\n/, '').trim()
}
/**
* The merge logic of sourcemap is consistent with the logic in vite-plugin-vue
*/
export function getSourceMap(
filename: string,
scriptCode: string,
scriptMap: any,
templateMap: any,
): RawSourceMap {
let resolvedMap: RawSourceMap | undefined = undefined
if (templateMap) {
// if the template is inlined into the main module (indicated by the presence
// of templateMap), we need to concatenate the two source maps.
const from = scriptMap ?? {
file: filename,
sourceRoot: '',
version: 3,
sources: [],
sourcesContent: [],
names: [],
mappings: '',
}
const gen = fromMap(
// version property of result.map is declared as string
// but actually it is `3`
from as Omit<RawSourceMap, 'version'> as TraceEncodedSourceMap,
)
const tracer = new TraceMap(
// same above
templateMap as Omit<RawSourceMap, 'version'> as TraceEncodedSourceMap,
)
const offset =
(trimAnalyzedBindings(scriptCode).match(/\r?\n/g)?.length ?? 0)
eachMapping(tracer, (m) => {
if (m.source == null) return
addMapping(gen, {
source: m.source,
original: { line: m.originalLine, column: m.originalColumn },
generated: {
line: m.generatedLine + offset,
column: m.generatedColumn,
},
})
})
// same above
resolvedMap = toEncodedMap(gen) as Omit<
GenEncodedSourceMap,
'version'
> as RawSourceMap
// if this is a template only update, we will be reusing a cached version
// of the main module compile result, which has outdated sourcesContent.
resolvedMap.sourcesContent = templateMap.sourcesContent
} else {
resolvedMap = scriptMap
}
return resolvedMap!
}
/*
* Slightly modified version of https://github.com/AriPerkkio/vite-plugin-source-map-visualizer/blob/main/src/generate-link.ts
*/
export function toVisualizer(code: string, sourceMap: RawSourceMap) {
const map = JSON.stringify(sourceMap)
const encoder = new TextEncoder()
// Convert the strings to Uint8Array
const codeArray = encoder.encode(code)
const mapArray = encoder.encode(map)
// Create Uint8Array for the lengths
const codeLengthArray = encoder.encode(codeArray.length.toString())
const mapLengthArray = encoder.encode(mapArray.length.toString())
// Combine the lengths and the data
const combinedArray = new Uint8Array(
codeLengthArray.length +
1 +
codeArray.length +
mapLengthArray.length +
1 +
mapArray.length,
)
combinedArray.set(codeLengthArray)
combinedArray.set([0], codeLengthArray.length)
combinedArray.set(codeArray, codeLengthArray.length + 1)
combinedArray.set(
mapLengthArray,
codeLengthArray.length + 1 + codeArray.length,
)
combinedArray.set(
[0],
codeLengthArray.length + 1 + codeArray.length + mapLengthArray.length,
)
combinedArray.set(
mapArray,
codeLengthArray.length + 1 + codeArray.length + mapLengthArray.length + 1,
)
// Convert the Uint8Array to a binary string
let binary = ''
const len = combinedArray.byteLength
for (let i = 0; i < len; i++) binary += String.fromCharCode(combinedArray[i])
// Convert the binary string to a base64 string and return it
return `https://evanw.github.io/source-map-visualization#${btoa(binary)}`
}

592
src/store.ts Normal file
View File

@ -0,0 +1,592 @@
import {
type ToRefs,
type UnwrapRef,
computed,
reactive,
ref,
shallowRef,
watch,
watchEffect,
} from 'vue'
import * as defaultCompiler from 'vue/compiler-sfc'
import { compileFile } from './transform'
import { atou, utoa } from './utils'
import type {
SFCAsyncStyleCompileOptions,
SFCScriptCompileOptions,
SFCTemplateCompileOptions,
} from 'vue/compiler-sfc'
import type { OutputModes } from './types'
import type { editor } from 'monaco-editor-core'
import { type ImportMap, mergeImportMap, useVueImportMap } from './import-map'
import welcomeSFCCode from './template/welcome.vue?raw'
import newSFCCode from './template/new-sfc.vue?raw'
export const importMapFile = 'import-map.json'
export const tsconfigFile = 'tsconfig.json'
export function useStore(
{
files = ref(Object.create(null)),
activeFilename = undefined!, // set later
mainFile = ref('src/App.vue'),
template = ref({
welcomeSFC: welcomeSFCCode,
newSFC: newSFCCode,
}),
builtinImportMap = undefined!, // set later
errors = ref([]),
showOutput = ref(false),
outputMode = ref('preview'),
sfcOptions = ref({}),
compiler = shallowRef(defaultCompiler),
vueVersion = ref(null),
locale = ref(),
typescriptVersion = ref('latest'),
dependencyVersion = ref(Object.create(null)),
reloadLanguageTools = ref(),
resourceLinks = undefined,
}: Partial<StoreState> = {},
serializedState?: string,
): ReplStore {
if (!builtinImportMap) {
;({ importMap: builtinImportMap, vueVersion } = useVueImportMap({
vueVersion: vueVersion.value,
}))
}
const loading = ref(false)
function applyBuiltinImportMap() {
const importMap = mergeImportMap(builtinImportMap.value, getImportMap())
setImportMap(importMap)
}
function init() {
watchEffect(() => {
compileFile(store, activeFile.value).then((errs) => (errors.value = errs))
})
watch(
() => [
files.value[tsconfigFile]?.code,
typescriptVersion.value,
locale.value,
dependencyVersion.value,
vueVersion.value,
],
() => reloadLanguageTools.value?.(),
{ deep: true },
)
watch(
builtinImportMap,
() => {
setImportMap(mergeImportMap(getImportMap(), builtinImportMap.value))
},
{ deep: true },
)
watch(
vueVersion,
async (version) => {
if (version) {
const compilerUrl =
resourceLinks?.value?.vueCompilerUrl?.(version) ||
`https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js`
loading.value = true
compiler.value = await import(/* @vite-ignore */ compilerUrl).finally(
() => (loading.value = false),
)
console.info(`[@vue/repl] Now using Vue version: ${version}`)
} else {
// reset to default
compiler.value = defaultCompiler
console.info(`[@vue/repl] Now using default Vue version`)
}
},
{ immediate: true },
)
// Recompile all Vue SFC files when the compiler changes.
// This ensures that when switching Vue versions (e.g., from <3.6 to >=3.6),
// all vue sfc files are recompiled with the new compiler to correctly handle
// vapor components.
watch(compiler, (_, oldCompiler) => {
if (!oldCompiler) return
for (const file of Object.values(files.value)) {
if (file.filename.endsWith('.vue')) {
compileFile(store, file).then((errs) => errors.value.push(...errs))
}
}
})
watch(
sfcOptions,
() => {
sfcOptions.value.script ||= {}
sfcOptions.value.script.fs = {
fileExists(file: string) {
if (file.startsWith('/')) file = file.slice(1)
return !!store.files[file]
},
readFile(file: string) {
if (file.startsWith('/')) file = file.slice(1)
return store.files[file].code
},
}
},
{ immediate: true },
)
// init tsconfig
if (!files.value[tsconfigFile]) {
files.value[tsconfigFile] = new File(
tsconfigFile,
JSON.stringify(tsconfig, undefined, 2),
)
}
// compile rest of the files
errors.value = []
for (const [filename, file] of Object.entries(files.value)) {
if (filename !== mainFile.value) {
compileFile(store, file).then((errs) => errors.value.push(...errs))
}
}
}
function setImportMap(map: ImportMap, merge = false) {
if (merge) {
map = mergeImportMap(getImportMap(), map)
}
if (map.imports)
for (const [key, value] of Object.entries(map.imports)) {
if (value) {
map.imports![key] = fixURL(value)
}
}
const code = JSON.stringify(map, undefined, 2)
if (files.value[importMapFile]) {
files.value[importMapFile].code = code
} else {
files.value[importMapFile] = new File(importMapFile, code)
}
}
const setActive: Store['setActive'] = (filename) => {
activeFilename.value = filename
}
const addFile: Store['addFile'] = (fileOrFilename) => {
let file: File
if (typeof fileOrFilename === 'string') {
file = new File(
fileOrFilename,
fileOrFilename.endsWith('.vue') ? template.value.newSFC : '',
)
} else {
file = fileOrFilename
}
files.value[file.filename] = file
if (!file.hidden) setActive(file.filename)
}
const deleteFile: Store['deleteFile'] = (filename) => {
if (
!confirm(`Are you sure you want to delete ${stripSrcPrefix(filename)}?`)
) {
return
}
if (activeFilename.value === filename) {
activeFilename.value = mainFile.value
}
delete files.value[filename]
}
const renameFile: Store['renameFile'] = (oldFilename, newFilename) => {
const file = files.value[oldFilename]
if (!file) {
errors.value = [`Could not rename "${oldFilename}", file not found`]
return
}
if (!newFilename || oldFilename === newFilename) {
errors.value = [`Cannot rename "${oldFilename}" to "${newFilename}"`]
return
}
file.filename = newFilename
const newFiles: Record<string, File> = {}
// Preserve iteration order for files
for (const [name, file] of Object.entries(files.value)) {
if (name === oldFilename) {
newFiles[newFilename] = file
} else {
newFiles[name] = file
}
}
files.value = newFiles
if (mainFile.value === oldFilename) {
mainFile.value = newFilename
}
if (activeFilename.value === oldFilename) {
activeFilename.value = newFilename
} else {
compileFile(store, file).then((errs) => (errors.value = errs))
}
}
const getImportMap: Store['getImportMap'] = () => {
try {
return JSON.parse(files.value[importMapFile].code)
} catch (e) {
errors.value = [
`Syntax error in ${importMapFile}: ${(e as Error).message}`,
]
return {}
}
}
const getTsConfig: Store['getTsConfig'] = () => {
try {
return JSON.parse(files.value[tsconfigFile].code)
} catch {
return {}
}
}
const serialize: ReplStore['serialize'] = () => {
const files = getFiles()
const importMap = files[importMapFile]
if (importMap) {
const parsed = JSON.parse(importMap)
const builtin = builtinImportMap.value.imports || {}
if (parsed.imports) {
for (const [key, value] of Object.entries(parsed.imports)) {
if (builtin[key] === value) {
delete parsed.imports[key]
}
}
if (parsed.imports && !Object.keys(parsed.imports).length) {
delete parsed.imports
}
}
if (parsed.scopes && !Object.keys(parsed.scopes).length) {
delete parsed.scopes
}
if (Object.keys(parsed).length) {
files[importMapFile] = JSON.stringify(parsed, null, 2)
} else {
delete files[importMapFile]
}
}
if (vueVersion.value) files._version = vueVersion.value
if (typescriptVersion.value !== 'latest' || files._tsVersion) {
files._tsVersion = typescriptVersion.value
}
return '#' + utoa(JSON.stringify(files))
}
const deserialize: ReplStore['deserialize'] = (
serializedState: string,
checkBuiltinImportMap = true,
) => {
if (serializedState.startsWith('#'))
serializedState = serializedState.slice(1)
let saved: any
try {
saved = JSON.parse(atou(serializedState))
} catch (err) {
console.error(err)
alert('Failed to load code from URL.')
return setDefaultFile()
}
for (const filename in saved) {
if (filename === '_version') {
vueVersion.value = saved[filename]
} else if (filename === '_tsVersion') {
typescriptVersion.value = saved[filename]
} else {
setFile(files.value, filename, saved[filename])
}
}
if (checkBuiltinImportMap) {
applyBuiltinImportMap()
}
}
const getFiles: ReplStore['getFiles'] = () => {
const exported: Record<string, string> = {}
for (const [filename, file] of Object.entries(files.value)) {
const normalized = stripSrcPrefix(filename)
exported[normalized] = file.code
}
return exported
}
const setFiles: ReplStore['setFiles'] = async (
newFiles,
mainFile = store.mainFile,
) => {
const files: Record<string, File> = Object.create(null)
mainFile = addSrcPrefix(mainFile)
if (!newFiles[mainFile]) {
setFile(files, mainFile, template.value.welcomeSFC || welcomeSFCCode)
}
for (const [filename, file] of Object.entries(newFiles)) {
setFile(files, filename, file)
}
const errors = []
for (const file of Object.values(files)) {
errors.push(...(await compileFile(store, file)))
}
store.mainFile = mainFile
store.files = files
store.errors = errors
applyBuiltinImportMap()
setActive(store.mainFile)
}
const setDefaultFile = (): void => {
setFile(
files.value,
mainFile.value,
template.value.welcomeSFC || welcomeSFCCode,
)
}
if (serializedState) {
deserialize(serializedState, false)
} else {
setDefaultFile()
}
if (!files.value[mainFile.value]) {
mainFile.value = Object.keys(files.value)[0]
}
activeFilename ||= ref(mainFile.value)
const activeFile = computed(() => files.value[activeFilename.value])
applyBuiltinImportMap()
const store: ReplStore = reactive({
files,
activeFile,
activeFilename,
mainFile,
template,
builtinImportMap,
errors,
showOutput,
outputMode,
sfcOptions,
ssrOutput: { html: '', context: '' },
compiler,
loading,
vueVersion,
locale,
typescriptVersion,
dependencyVersion,
reloadLanguageTools,
init,
setActive,
addFile,
deleteFile,
renameFile,
getImportMap,
setImportMap,
getTsConfig,
serialize,
deserialize,
getFiles,
setFiles,
resourceLinks,
})
return store
}
const tsconfig = {
compilerOptions: {
allowJs: true,
checkJs: true,
jsx: 'Preserve',
target: 'ESNext',
module: 'ESNext',
moduleResolution: 'Bundler',
allowImportingTsExtensions: true,
},
vueCompilerOptions: {
target: 3.4,
},
}
export interface SFCOptions {
script?: Partial<SFCScriptCompileOptions>
style?: Partial<SFCAsyncStyleCompileOptions>
template?: Partial<SFCTemplateCompileOptions>
}
export type ResourceLinkConfigs = {
esModuleShims?: string
vueCompilerUrl?: (version: string) => string
typescriptLib?: (version: string) => string
// for monaco
pkgLatestVersionUrl?: (pkgName: string) => string
pkgDirUrl?: (pkgName: string, pkgVersion: string, pkgPath: string) => string
pkgFileTextUrl?: (
pkgName: string,
pkgVersion: string | undefined,
pkgPath: string,
) => string
}
export type StoreState = ToRefs<{
files: Record<string, File>
activeFilename: string
mainFile: string
template: {
welcomeSFC?: string
newSFC?: string
}
builtinImportMap: ImportMap
// output
errors: (string | Error)[]
showOutput: boolean
outputMode: OutputModes
sfcOptions: SFCOptions
ssrOutput: {
html: string
context: unknown
}
/** `@vue/compiler-sfc` */
compiler: typeof defaultCompiler
/* only apply for compiler-sfc */
vueVersion: string | null
// volar-related
locale: string | undefined
typescriptVersion: string
/** \{ dependencyName: version \} */
dependencyVersion: Record<string, string>
reloadLanguageTools?: (() => void) | undefined
/** Custom online resources */
resourceLinks?: ResourceLinkConfigs
}>
export interface ReplStore extends UnwrapRef<StoreState> {
activeFile: File
/** Loading compiler */
loading: boolean
init(): void
setActive(filename: string): void
addFile(filename: string | File): void
deleteFile(filename: string): void
renameFile(oldFilename: string, newFilename: string): void
getImportMap(): ImportMap
setImportMap(map: ImportMap, merge?: boolean): void
getTsConfig(): Record<string, any>
serialize(): string
/**
* Deserializes the given string to restore the REPL store state.
* @param serializedState - The serialized state string.
* @param checkBuiltinImportMap - Whether to check the built-in import map. Default to true
*/
deserialize(serializedState: string, checkBuiltinImportMap?: boolean): void
getFiles(): Record<string, string>
setFiles(newFiles: Record<string, string>, mainFile?: string): Promise<void>
/** Custom online resources */
resourceLinks?: ResourceLinkConfigs
}
export type Store = Pick<
ReplStore,
| 'files'
| 'activeFile'
| 'mainFile'
| 'errors'
| 'showOutput'
| 'outputMode'
| 'sfcOptions'
| 'ssrOutput'
| 'compiler'
| 'vueVersion'
| 'locale'
| 'typescriptVersion'
| 'dependencyVersion'
| 'reloadLanguageTools'
| 'init'
| 'setActive'
| 'addFile'
| 'deleteFile'
| 'renameFile'
| 'getImportMap'
| 'getTsConfig'
| 'resourceLinks'
>
export class File {
compiled = {
js: '',
css: '',
ssr: '',
clientMap: '',
ssrMap: '',
}
editorViewState: editor.ICodeEditorViewState | null = null
constructor(
public filename: string,
public code = '',
public hidden = false,
) {}
get language() {
if (this.filename.endsWith('.vue')) {
return 'vue'
}
if (this.filename.endsWith('.html')) {
return 'html'
}
if (this.filename.endsWith('.css')) {
return 'css'
}
if (this.filename.endsWith('.ts')) {
return 'typescript'
}
return 'javascript'
}
}
function addSrcPrefix(file: string) {
return file === importMapFile ||
file === tsconfigFile ||
file.startsWith('src/')
? file
: `src/${file}`
}
export function stripSrcPrefix(file: string) {
return file.replace(/^src\//, '')
}
function fixURL(url: string) {
return url.replace('https://sfc.vuejs', 'https://play.vuejs')
}
function setFile(
files: Record<string, File>,
filename: string,
content: string,
) {
const normalized = addSrcPrefix(filename)
files[normalized] = new File(normalized, content)
}

7
src/template/new-sfc.vue Normal file
View File

@ -0,0 +1,7 @@
<script setup></script>
<template>
<div>
<slot />
</div>
</template>

10
src/template/welcome.vue Normal file
View File

@ -0,0 +1,10 @@
<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1>{{ msg }}</h1>
<input v-model="msg" />
</template>

409
src/transform.ts Normal file
View File

@ -0,0 +1,409 @@
import type { File, Store } from './store'
import type {
BindingMetadata,
CompilerOptions,
SFCDescriptor,
} from 'vue/compiler-sfc'
import { type Transform, transform } from 'sucrase'
import hashId from 'hash-sum'
import { getSourceMap, toVisualizer, trimAnalyzedBindings } from './sourcemap'
export const COMP_IDENTIFIER = `__sfc__`
const REGEX_JS = /\.[jt]sx?$/
function testTs(filename: string | undefined | null) {
return !!(filename && /(\.|\b)tsx?$/.test(filename))
}
function testJsx(filename: string | undefined | null) {
return !!(filename && /(\.|\b)[jt]sx$/.test(filename))
}
function transformTS(src: string, isJSX?: boolean) {
return transform(src, {
transforms: ['typescript', ...(isJSX ? (['jsx'] as Transform[]) : [])],
jsxRuntime: 'preserve',
}).code
}
export async function compileFile(
store: Store,
{ filename, code, compiled }: File,
): Promise<(string | Error)[]> {
if (!code.trim()) {
return []
}
if (filename.endsWith('.css')) {
compiled.css = code
return []
}
if (REGEX_JS.test(filename)) {
const isJSX = testJsx(filename)
if (testTs(filename)) {
code = transformTS(code, isJSX)
}
if (isJSX) {
code = await import('./jsx').then(({ transformJSX }) =>
transformJSX(code),
)
}
compiled.js = compiled.ssr = code
return []
}
if (filename.endsWith('.json')) {
let parsed
try {
parsed = JSON.parse(code)
} catch (err: any) {
console.error(`Error parsing ${filename}`, err.message)
return [err.message]
}
compiled.js = compiled.ssr = `export default ${JSON.stringify(parsed)}`
return []
}
if (!filename.endsWith('.vue')) {
return []
}
const id = hashId(filename)
const { errors, descriptor } = store.compiler.parse(code, {
filename,
sourceMap: true,
templateParseOptions: store.sfcOptions?.template?.compilerOptions,
})
if (errors.length) {
return errors
}
const styleLangs = descriptor.styles.map((s) => s.lang).filter(Boolean)
const templateLang = descriptor.template?.lang
if (styleLangs.length && templateLang) {
return [
`lang="${styleLangs.join(
',',
)}" pre-processors for <style> and lang="${templateLang}" ` +
`for <template> are currently not supported.`,
]
} else if (styleLangs.length) {
return [
`lang="${styleLangs.join(
',',
)}" pre-processors for <style> are currently not supported.`,
]
} else if (templateLang) {
return [
`lang="${templateLang}" pre-processors for ` +
`<template> are currently not supported.`,
]
}
const scriptLang = descriptor.script?.lang || descriptor.scriptSetup?.lang
const isTS = testTs(scriptLang)
const isJSX = testJsx(scriptLang)
if (scriptLang && scriptLang !== 'js' && !isTS && !isJSX) {
return [`Unsupported lang "${scriptLang}" in <script> blocks.`]
}
const hasScoped = descriptor.styles.some((s) => s.scoped)
let clientCode = ''
let ssrCode = ''
let ssrScript = ''
let clientScriptMap: any
let clientTemplateMap: any
let ssrScriptMap: any
let ssrTemplateMap: any
const appendSharedCode = (code: string) => {
clientCode += code
ssrCode += code
}
const ceFilter = store.sfcOptions.script?.customElement || /\.ce\.vue$/
function isCustomElement(filters: typeof ceFilter): boolean {
if (typeof filters === 'boolean') {
return filters
}
if (typeof filters === 'function') {
return filters(filename)
}
return filters.test(filename)
}
let isCE = isCustomElement(ceFilter)
let clientScript: string
let bindings: BindingMetadata | undefined
try {
const res = await doCompileScript(store, descriptor, id, false, isTS, isJSX, isCE)
clientScript = res.code
bindings = res.bindings
clientScriptMap = res.map
} catch (e: any) {
return [e.stack.split('\n').slice(0, 12).join('\n')]
}
clientCode += clientScript
// script ssr needs to be performed if :
// 1.using <script setup> where the render fn is inlined.
// 2.using cssVars, as it do not need to be injected during SSR.
if (descriptor.scriptSetup || descriptor.cssVars.length > 0) {
try {
const ssrScriptResult = await doCompileScript(
store,
descriptor,
id,
true,
isTS,
isJSX,
isCE
)
ssrScript = ssrScriptResult.code
ssrCode += ssrScript
ssrScriptMap = ssrScriptResult.map
} catch (e) {
ssrCode = `/* SSR compile error: ${e} */`
}
} else {
// the script result will be identical.
ssrCode += clientScript
}
// template
// only need dedicated compilation if not using <script setup>
if (
descriptor.template &&
(!descriptor.scriptSetup ||
store.sfcOptions?.script?.inlineTemplate === false)
) {
const clientTemplateResult = await doCompileTemplate(
store,
descriptor,
id,
bindings,
false,
isTS,
isJSX,
)
if (clientTemplateResult.errors.length) {
return clientTemplateResult.errors
}
clientCode += `;${clientTemplateResult.code}`
clientTemplateMap = clientTemplateResult.map
const ssrTemplateResult = await doCompileTemplate(
store,
descriptor,
id,
bindings,
true,
isTS,
isJSX,
)
if (ssrTemplateResult.code) {
// ssr compile failure is fine
ssrCode += `;${ssrTemplateResult.code}`
ssrTemplateMap = ssrTemplateResult.map
} else {
ssrCode = `/* SSR compile error: ${ssrTemplateResult.errors[0]} */`
}
}
if (isJSX) {
const { transformJSX } = await import('./jsx')
clientCode &&= transformJSX(clientCode)
ssrCode &&= transformJSX(ssrCode)
}
if (hasScoped) {
appendSharedCode(
`\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(`data-v-${id}`)}`,
)
}
// styles
let css = ''
let styles: string[] = []
for (const style of descriptor.styles) {
if (style.module) {
return [`<style module> is not supported in the playground.`]
}
const styleResult = await store.compiler.compileStyleAsync({
...store.sfcOptions?.style,
source: style.content,
filename,
id,
scoped: style.scoped,
modules: !!style.module,
})
if (styleResult.errors.length) {
// postcss uses pathToFileURL which isn't polyfilled in the browser
// ignore these errors for now
if (!styleResult.errors[0].message.includes('pathToFileURL')) {
store.errors = styleResult.errors
}
// proceed even if css compile errors
} else {
isCE ? styles.push(styleResult.code) : (css += styleResult.code + '\n')
}
}
if (css) {
compiled.css = css.trim()
} else {
compiled.css = isCE
? (compiled.css =
'/* The component style of the custom element will be compiled into the component object */')
: '/* No <style> tags present */'
}
if (clientCode || ssrCode) {
const ceStyles = isCE
? `\n${COMP_IDENTIFIER}.styles = ${JSON.stringify(styles)}`
: ''
appendSharedCode(
`\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}` +
ceStyles +
`\nexport default ${COMP_IDENTIFIER}`,
)
compiled.js = clientCode.trimStart()
compiled.ssr = ssrCode.trimStart()
compiled.clientMap = toVisualizer(
trimAnalyzedBindings(compiled.js),
getSourceMap(filename, clientScript, clientScriptMap, clientTemplateMap),
)
compiled.ssrMap = toVisualizer(
trimAnalyzedBindings(compiled.ssr),
getSourceMap(
filename,
ssrScript || clientScript,
ssrScriptMap,
ssrTemplateMap,
),
)
}
return []
}
async function doCompileScript(
store: Store,
descriptor: SFCDescriptor,
id: string,
ssr: boolean,
isTS: boolean,
isJSX: boolean,
isCustomElement: boolean,
): Promise<{ code: string; bindings: BindingMetadata | undefined; map?: any }> {
if (descriptor.script || descriptor.scriptSetup) {
const expressionPlugins: CompilerOptions['expressionPlugins'] = []
if (isTS) {
expressionPlugins.push('typescript')
}
if (isJSX) {
expressionPlugins.push('jsx')
}
const compiledScript = store.compiler.compileScript(descriptor, {
inlineTemplate: true,
...store.sfcOptions?.script,
id,
genDefaultAs: COMP_IDENTIFIER,
templateOptions: {
...store.sfcOptions?.template,
ssr,
ssrCssVars: descriptor.cssVars,
compilerOptions: {
...store.sfcOptions?.template?.compilerOptions,
expressionPlugins,
},
},
customElement: isCustomElement,
})
let code = compiledScript.content
if (isTS) {
code = await transformTS(code, isJSX)
}
if (compiledScript.bindings) {
code =
`/* Analyzed bindings: ${JSON.stringify(
compiledScript.bindings,
null,
2,
)} */\n` + code
}
return { code, bindings: compiledScript.bindings, map: compiledScript.map }
} else {
// @ts-expect-error TODO remove when 3.6 is out
const vaporFlag = descriptor.vapor ? '__vapor: true' : ''
return {
code: `\nconst ${COMP_IDENTIFIER} = { ${vaporFlag} }`,
bindings: {},
map: undefined,
}
}
}
async function doCompileTemplate(
store: Store,
descriptor: SFCDescriptor,
id: string,
bindingMetadata: BindingMetadata | undefined,
ssr: boolean,
isTS: boolean,
isJSX: boolean,
) {
const expressionPlugins: CompilerOptions['expressionPlugins'] = []
if (isTS) {
expressionPlugins.push('typescript')
}
if (isJSX) {
expressionPlugins.push('jsx')
}
const res = store.compiler.compileTemplate({
isProd: false,
...store.sfcOptions?.template,
// @ts-expect-error TODO remove expect-error after 3.6
vapor: descriptor.vapor,
ast: descriptor.template!.ast,
source: descriptor.template!.content,
filename: descriptor.filename,
id,
scoped: descriptor.styles.some((s) => s.scoped),
slotted: descriptor.slotted,
ssr,
ssrCssVars: descriptor.cssVars,
compilerOptions: {
...store.sfcOptions?.template?.compilerOptions,
bindingMetadata,
expressionPlugins,
},
})
// @ts-expect-error multiRoot in 3.6
let { code, errors, map, multiRoot } = res
if (errors.length) {
return { code, map, errors }
}
const fnName = ssr ? `ssrRender` : `render`
code =
`\n${code.replace(
/\nexport (function|const) (render|ssrRender)/,
`$1 ${fnName}`,
)}` + `\n${COMP_IDENTIFIER}.${fnName} = ${fnName}`
// @ts-expect-error multiRoot in 3.6
if(descriptor.vapor && !ssr) {
code += `\n${COMP_IDENTIFIER}.__multiRoot = ${multiRoot}`
}
if (isTS) {
code = await transformTS(code, isJSX)
}
return { code, map, errors: [] }
}

35
src/types.ts Normal file
View File

@ -0,0 +1,35 @@
import type { Component, ComputedRef, InjectionKey, ToRefs } from 'vue'
import { Props } from './Repl.vue'
import type * as monaco from 'monaco-editor-core'
import type CodeMirror from 'codemirror'
export type EditorMode = 'js' | 'css' | 'ssr'
export interface EditorProps {
value: string
filename: string
readonly?: boolean
mode?: EditorMode
}
export interface EditorEmits {
(e: 'change', code: string): void
}
export interface EditorMethods {
getEditorIns<T extends 'monaco' | 'codemirror' = 'monaco'>():
| (T extends 'codemirror'
? CodeMirror.Editor
: monaco.editor.IStandaloneCodeEditor)
| undefined
getMonacoEditor?(): typeof monaco.editor | undefined
}
export type EditorComponentType = Component<EditorProps>
export type OutputModes = 'preview' | 'ssr output' | EditorMode
export const injectKeyProps: InjectionKey<
ToRefs<Required<Props & { autoSave: boolean }>>
> = Symbol('props')
export const injectKeyPreviewRef: InjectionKey<
ComputedRef<HTMLDivElement | null>
> = Symbol('preview-ref')

33
src/utils.ts Normal file
View File

@ -0,0 +1,33 @@
import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate'
export function debounce(fn: Function, n = 100) {
let handle: any
return (...args: any[]) => {
if (handle) clearTimeout(handle)
handle = setTimeout(() => {
fn(...args)
}, n)
}
}
export function utoa(data: string): string {
const buffer = strToU8(data)
const zipped = zlibSync(buffer, { level: 9 })
const binary = strFromU8(zipped, true)
return btoa(binary)
}
export function atou(base64: string): string {
const binary = atob(base64)
// zlib header (x78), level 9 (xDA)
if (binary.startsWith('\x78\xDA')) {
const buffer = strToU8(binary, true)
const unzipped = unzlibSync(buffer)
return strFromU8(unzipped)
}
// old unicode hacks for backward compatibility
// https://base64.guru/developers/javascript/examples/unicode-strings
return decodeURIComponent(escape(binary))
}

2
src/vue-dev-proxy.ts Normal file
View File

@ -0,0 +1,2 @@
// serve vue to the iframe sandbox during dev.
export * from 'vue'

View File

@ -0,0 +1,2 @@
// serve server renderer to the iframe sandbox during dev.
export * from 'vue/server-renderer'

1
ssr-stub.js Normal file
View File

@ -0,0 +1 @@
module.exports = {}

83
test/main.ts Normal file
View File

@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/prefer-ts-expect-error */
import { createApp, h, ref, watchEffect } from 'vue'
import { type OutputModes, Repl, useStore, useVueImportMap } from '../src'
// @ts-ignore
import MonacoEditor from '../src/editor/MonacoEditor.vue'
// @ts-ignore
import CodeMirrorEditor from '../src/editor/CodeMirrorEditor.vue'
const window = globalThis.window as any
window.process = { env: {} }
const App = {
setup() {
const query = new URLSearchParams(location.search)
const { importMap: builtinImportMap, vueVersion } = useVueImportMap({
runtimeDev: import.meta.env.PROD
? undefined
: `${location.origin}/src/vue-dev-proxy`,
serverRenderer: import.meta.env.PROD
? undefined
: `${location.origin}/src/vue-server-renderer-dev-proxy`,
})
const store = (window.store = useStore(
{
builtinImportMap,
vueVersion,
showOutput: ref(query.has('so')),
outputMode: ref((query.get('om') as OutputModes) || 'preview'),
},
location.hash,
))
console.info(store)
watchEffect(() => history.replaceState({}, '', store.serialize()))
// setTimeout(() => {
// store.setFiles(
// {
// 'src/index.html': '<h1>yo</h1>',
// 'src/main.js': 'document.body.innerHTML = "<h1>hello</h1>"',
// 'src/foo.js': 'document.body.innerHTML = "<h1>hello</h1>"',
// 'src/bar.js': 'document.body.innerHTML = "<h1>hello</h1>"',
// 'src/baz.js': 'document.body.innerHTML = "<h1>hello</h1>"',
// },
// 'src/index.html',
// )
// }, 1000)
// store.vueVersion = '3.4.1'
const theme = ref<'light' | 'dark'>('dark')
window.theme = theme
const previewTheme = ref(false)
window.previewTheme = previewTheme
return () =>
h(Repl, {
store,
theme: theme.value,
previewTheme: previewTheme.value,
editor: MonacoEditor,
showOpenSourceMap: true,
// layout: 'vertical',
ssr: true,
showSsrOutput: true,
sfcOptions: {
script: {
// inlineTemplate: false
},
},
// showCompileOutput: false,
// showImportMap: false
editorOptions: {
autoSaveText: '💾',
monacoOptions: {
// wordWrap: 'on',
},
},
// autoSave: false,
})
},
}
createApp(App).mount('#app')

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"outDir": "dist",
"sourceMap": false,
"target": "esnext",
"useDefineForClassFields": false,
"module": "esnext",
"moduleResolution": "bundler",
"allowJs": false,
"strict": true,
"noUnusedLocals": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"removeComments": false,
"lib": ["esnext", "dom"],
"jsx": "preserve",
"rootDir": ".",
"skipLibCheck": true
},
"include": ["src", "test", "vite.config.ts"],
"exclude": ["src/template"]
}

75
vite.config.ts Normal file
View File

@ -0,0 +1,75 @@
import { type Plugin, mergeConfig } from 'vite'
import dts from 'vite-plugin-dts'
import base from './vite.preview.config'
import fs from 'node:fs'
import path from 'node:path'
const genStub: Plugin = {
name: 'gen-stub',
apply: 'build',
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'ssr-stub.js',
source: `module.exports = {}`,
})
},
}
/**
* Patch generated entries and import their corresponding CSS files.
* Also normalize MonacoEditor.css
*/
const patchCssFiles: Plugin = {
name: 'patch-css',
apply: 'build',
writeBundle() {
// inject css imports to the files
const outDir = path.resolve('dist')
;['vue-repl', 'monaco-editor', 'codemirror-editor'].forEach((file) => {
const filePath = path.resolve(outDir, file + '.js')
const content = fs.readFileSync(filePath, 'utf-8')
fs.writeFileSync(filePath, `import './${file}.css'\n${content}`)
})
},
}
export default mergeConfig(base, {
plugins: [
dts({
rollupTypes: true,
}),
genStub,
patchCssFiles,
],
optimizeDeps: {
// avoid late discovered deps
include: [
'typescript',
'monaco-editor-core/esm/vs/editor/editor.worker',
'vue/server-renderer',
],
},
base: './',
build: {
target: 'esnext',
minify: false,
lib: {
entry: {
'vue-repl': './src/index.ts',
core: './src/core.ts',
'monaco-editor': './src/editor/MonacoEditor.vue',
'codemirror-editor': './src/editor/CodeMirrorEditor.vue',
},
formats: ['es'],
fileName: () => '[name].js',
},
cssCodeSplit: true,
rollupOptions: {
output: {
chunkFileNames: 'chunks/[name]-[hash].js',
},
external: ['vue', 'vue/compiler-sfc'],
},
},
})

29
vite.preview.config.ts Normal file
View File

@ -0,0 +1,29 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import replace from '@rollup/plugin-replace'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@vue/compiler-dom': '@vue/compiler-dom/dist/compiler-dom.cjs.js',
'@vue/compiler-core': '@vue/compiler-core/dist/compiler-core.cjs.js',
},
},
build: {
commonjsOptions: {
ignore: ['typescript'],
},
},
worker: {
format: 'es',
plugins: () => [
replace({
preventAssignment: true,
values: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
}),
],
},
})