first commit
This commit is contained in:
commit
51acbd50db
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@ -0,0 +1,2 @@
|
||||
# style: add trailing comma
|
||||
497f07527b162f42123ead110031f265981f4d4d
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
TODOs.md
|
||||
|
||||
# jetbrains files
|
||||
.idea
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@ -0,0 +1 @@
|
||||
lts/*
|
||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist
|
||||
CHANGELOG.md
|
||||
pnpm-lock.yaml
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
905
CHANGELOG.md
Normal file
905
CHANGELOG.md
Normal 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
21
LICENSE
Normal 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
204
README.md
Normal 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
53
eslint.config.js
Normal 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
30
index-dist.html
Normal 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
23
index.html
Normal 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
123
package.json
Normal 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
5145
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
128
src/Message.vue
Normal file
128
src/Message.vue
Normal 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
164
src/Repl.vue
Normal 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
220
src/SplitPane.vue
Normal 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>
|
||||
121
src/codemirror/CodeMirror.vue
Normal file
121
src/codemirror/CodeMirror.vue
Normal 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>
|
||||
547
src/codemirror/codemirror.css
Normal file
547
src/codemirror/codemirror.css
Normal 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);
|
||||
}
|
||||
26
src/codemirror/codemirror.ts
Normal file
26
src/codemirror/codemirror.ts
Normal 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
11
src/core.ts
Normal 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'
|
||||
56
src/editor/CodeMirrorEditor.vue
Normal file
56
src/editor/CodeMirrorEditor.vue
Normal 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>
|
||||
93
src/editor/EditorContainer.vue
Normal file
93
src/editor/EditorContainer.vue
Normal 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
293
src/editor/FileSelector.vue
Normal 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>
|
||||
35
src/editor/MonacoEditor.vue
Normal file
35
src/editor/MonacoEditor.vue
Normal 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>
|
||||
52
src/editor/ToggleButton.vue
Normal file
52
src/editor/ToggleButton.vue
Normal 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
7
src/env.d.ts
vendored
Normal 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
75
src/import-map.ts
Normal 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
5
src/index.ts
Normal 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
8
src/jsx.ts
Normal 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
195
src/monaco/Monaco.vue
Normal 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
206
src/monaco/env.ts
Normal 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
29
src/monaco/highlight.ts
Normal 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!,
|
||||
}
|
||||
}
|
||||
529
src/monaco/language-configs.ts
Normal file
529
src/monaco/language-configs.ts
Normal 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
398
src/monaco/resource.ts
Normal 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
14
src/monaco/utils.ts
Normal 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
342
src/monaco/vue.worker.ts
Normal 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
130
src/output/Output.vue
Normal 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
34
src/output/Preview.vue
Normal 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>
|
||||
96
src/output/PreviewProxy.ts
Normal file
96
src/output/PreviewProxy.ts
Normal 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
380
src/output/Sandbox.vue
Normal 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
32
src/output/SsrOutput.vue
Normal 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>
|
||||
351
src/output/moduleCompiler.ts
Normal file
351
src/output/moduleCompiler.ts
Normal 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
378
src/output/srcdoc.html
Normal 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
119
src/sourcemap.ts
Normal 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
592
src/store.ts
Normal 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
7
src/template/new-sfc.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
10
src/template/welcome.vue
Normal file
10
src/template/welcome.vue
Normal 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
409
src/transform.ts
Normal 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
35
src/types.ts
Normal 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
33
src/utils.ts
Normal 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
2
src/vue-dev-proxy.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// serve vue to the iframe sandbox during dev.
|
||||
export * from 'vue'
|
||||
2
src/vue-server-renderer-dev-proxy.ts
Normal file
2
src/vue-server-renderer-dev-proxy.ts
Normal 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
1
ssr-stub.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = {}
|
||||
83
test/main.ts
Normal file
83
test/main.ts
Normal 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
23
tsconfig.json
Normal 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
75
vite.config.ts
Normal 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
29
vite.preview.config.ts
Normal 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'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user