├── .changeset ├── README.md └── config.json ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ ├── npm-publish-github-packages.yml │ └── static.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .vscode └── extensions.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── api-extractor.json ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public └── vue.svg ├── src ├── App.vue ├── Header.vue ├── Message.vue ├── Repl.vue ├── SplitPane.vue ├── codemirror │ ├── CodeMirror.vue │ ├── codemirror.css │ └── codemirror.ts ├── editor │ ├── Editor.vue │ └── FileSelector.vue ├── env.d.ts ├── icons │ ├── Moon.vue │ ├── Share.vue │ └── Sun.vue ├── index.ts ├── main.ts ├── output │ ├── Output.vue │ ├── Preview.vue │ ├── PreviewProxy.ts │ ├── moduleCompiler.ts │ ├── srcdoc.html │ └── types.ts ├── store.ts ├── transform.ts └── utils.ts ├── ssr-stub.js ├── tsconfig.build.json ├── tsconfig.json ├── vite.config.docs.ts └── vite.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint" 5 | ], 6 | "rules": { 7 | "no-var": "error", // 不能使用var声明变量 8 | "no-extra-semi": "error", 9 | "@typescript-eslint/indent": [ 10 | "error", 11 | 2 12 | ], 13 | "import/extensions": "off", 14 | "linebreak-style": [ 15 | 0, 16 | "error", 17 | "windows" 18 | ], 19 | "indent": [ 20 | "error", 21 | 2, 22 | { 23 | "SwitchCase": 1 24 | } 25 | ], // error类型,缩进2个空格 26 | "space-before-function-paren": 0, // 在函数左括号的前面是否有空格 27 | "eol-last": 0, // 不检测新文件末尾是否有空行 28 | "semi": [ 29 | "error", 30 | "always" 31 | ], // 在语句后面加分号 32 | "quotes": [ 33 | "error", 34 | "single" 35 | ], // 字符串使用单双引号,double,single 36 | "no-console": [ 37 | "error", 38 | { 39 | "allow": [ 40 | "log", 41 | "warn" 42 | ] 43 | } 44 | ], // 允许使用console.log() 45 | "arrow-parens": 0, 46 | "no-new": 0, //允许使用 new 关键字 47 | "comma-dangle": [ 48 | 2, 49 | "never" 50 | ], // 数组和对象键值对最后一个逗号, never参数:不能带末尾的逗号, always参数:必须带末尾的逗号,always-multiline多行模式必须带逗号,单行模式不能带逗号 51 | "no-undef": 0 52 | }, 53 | "parserOptions": { 54 | "ecmaVersion": 6, 55 | "sourceType": "module", 56 | "ecmaFeatures": { 57 | "modules": true 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-github-packages.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | 19 | publish-gpr: 20 | needs: build 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | packages: write 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: 16 30 | registry-url: https://npm.pkg.github.com/ 31 | - run: npm ci 32 | - run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 35 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Setup Node 34 | uses: actions/setup-node@v3.6.0 35 | with: 36 | node-version: '18.x' 37 | - name: Setup pnpm 38 | uses: pnpm/action-setup@v2.2.4 39 | with: 40 | run_install: true 41 | - name: Install Dependencies 42 | run: | 43 | pnpm install 44 | pnpm docs:build 45 | - name: Setup Pages 46 | uses: actions/configure-pages@v3 47 | - name: Upload artifact 48 | uses: actions/upload-pages-artifact@v1 49 | with: 50 | # Upload entire repository 51 | path: './docs' 52 | - name: Deploy to GitHub Pages 53 | id: deployment 54 | uses: actions/deploy-pages@v1 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | TODOs.md 5 | 6 | # jetbrains files 7 | .idea 8 | docs 9 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @Thy3634:registry=https://npm.pkg.github.com -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # vue2-repl 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - 4b296e8: feat: import as text; import as url 8 | 9 | ## 0.2.0 10 | 11 | ### Minor Changes 12 | 13 | - 18482a1: feat: 支持 import css 和 json 14 | 15 | ## 0.1.2 16 | 17 | ### Patch Changes 18 | 19 | - 81203ff: feat: add ' 35 | 36 | 39 | ``` 40 | 41 | ## Advanced Usage 42 | 43 | ```vue 44 | 80 | 81 | 84 | ``` 85 | -------------------------------------------------------------------------------- /api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | 4 | "projectFolder": ".", 5 | 6 | "mainEntryPointFilePath": "./dist/src/index.d.ts", 7 | 8 | "dtsRollup": { 9 | "enabled": true 10 | }, 11 | 12 | "apiReport": { 13 | "enabled": false 14 | }, 15 | 16 | "docModel": { 17 | "enabled": false 18 | }, 19 | 20 | "tsdocMetadata": { 21 | "enabled": false 22 | }, 23 | 24 | "messages": { 25 | "compilerMessageReporting": { 26 | "default": { 27 | "logLevel": "warning" 28 | } 29 | }, 30 | 31 | "extractorMessageReporting": { 32 | "default": { 33 | "logLevel": "warning", 34 | "addToApiReportFile": true 35 | }, 36 | 37 | "ae-forgotten-export": { 38 | "logLevel": "none" 39 | }, 40 | 41 | "ae-missing-release-tag": { 42 | "logLevel": "none" 43 | } 44 | }, 45 | 46 | "tsdocMessageReporting": { 47 | "default": { 48 | "logLevel": "warning" 49 | }, 50 | 51 | "tsdoc-undefined-tag": { 52 | "logLevel": "none" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue2 SFC Playground 9 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2-repl", 3 | "version": "0.2.1", 4 | "description": "Vue3 component for editing Vue2 components", 5 | "main": "dist/vue2-repl.umd.js", 6 | "module": "dist/vue2-repl.mjs", 7 | "packageManager": "pnpm@7.1.0", 8 | "files": [ 9 | "dist" 10 | ], 11 | "types": "dist/vue2-repl.d.ts", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/vue2-repl.mjs", 15 | "types": "./dist/vue2-repl.d.ts", 16 | "require": "./dist/vue2-repl.umd.js" 17 | }, 18 | "./style.css": "./dist/style.css" 19 | }, 20 | "scripts": { 21 | "dev": "vite", 22 | "build": "vite build && pnpm build:types", 23 | "build:types": "vue-tsc -p tsconfig.build.json && api-extractor run -c api-extractor.json && rimraf dist/src", 24 | "docs:build": "vite build -c vite.config.docs.ts", 25 | "docs:preview": "vite preview -c vite.config.docs.ts", 26 | "commit": "pnpm changeset && cz", 27 | "prepublish": "pnpm build && pnpm changeset version", 28 | "publish": "pnpm changeset publish" 29 | }, 30 | "repository": "https://github.com/Thy3634/vue2-repl.git", 31 | "author": "Thy3634", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/Thy3634/vue2-repl/issues" 35 | }, 36 | "homepage": "https://github.com/Thy3634/vue2-repl#readme", 37 | "dependencies": { 38 | "codemirror": "^5.62.3", 39 | "fflate": "^0.7.3", 40 | "hash-sum": "^2.0.0", 41 | "rimraf": "^3.0.2", 42 | "sucrase": "^3.20.1" 43 | }, 44 | "devDependencies": { 45 | "@babel/types": "^7.15.6", 46 | "@changesets/cli": "^2.24.4", 47 | "@commitlint/cli": "^17.1.2", 48 | "@commitlint/config-conventional": "^17.1.0", 49 | "@microsoft/api-extractor": "^7.19.2", 50 | "@types/codemirror": "^5.60.2", 51 | "@types/node": "^16.11.12", 52 | "@typescript-eslint/eslint-plugin": "^5.38.0", 53 | "@typescript-eslint/parser": "^5.38.0", 54 | "@vitejs/plugin-vue": "^3.0.0-beta.0", 55 | "commitizen": "^4.2.5", 56 | "cz-conventional-changelog": "^3.3.0", 57 | "eslint": "^8.24.0", 58 | "husky": "^8.0.1", 59 | "lint-staged": "^13.0.3", 60 | "typescript": "^4.5.4", 61 | "vite": "^3.0.0-beta.3", 62 | "vue": "^3.2.37", 63 | "vue-tsc": "^0.34.15", 64 | "vue2": "npm:vue@^2.7.10" 65 | }, 66 | "peerDependencies": { 67 | "vue": "^3.2.13", 68 | "vue2": "npm:vue@^2.7.10" 69 | }, 70 | "lint-staged": { 71 | "*.ts": [ 72 | "eslint --fix" 73 | ] 74 | } 75 | } -------------------------------------------------------------------------------- /public/vue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 67 | 68 | 94 | -------------------------------------------------------------------------------- /src/Header.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 73 | 74 | 261 | -------------------------------------------------------------------------------- /src/Message.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 42 | 43 | 122 | -------------------------------------------------------------------------------- /src/Repl.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 47 | 48 | 84 | -------------------------------------------------------------------------------- /src/SplitPane.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 81 | 82 | 185 | -------------------------------------------------------------------------------- /src/codemirror/CodeMirror.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 74 | 75 | 89 | -------------------------------------------------------------------------------- /src/codemirror/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | color: var(--symbols); 5 | --symbols: #777; 6 | --base: #545281; 7 | --comment: hsl(210, 25%, 60%); 8 | --keyword: #af4ab1; 9 | --variable: var(--base); 10 | --function: #c25205; 11 | --string: #2ba46d; 12 | --number: #c25205; 13 | --tags: #dd0000; 14 | --brackets: var(--comment); 15 | --qualifier: #ff6032; 16 | --important: var(--string); 17 | --attribute: #9c3eda; 18 | --property: #6182b8; 19 | 20 | --selected-bg: #d7d4f0; 21 | --selected-bg-non-focus: #d9d9d9; 22 | --cursor: #000; 23 | 24 | direction: ltr; 25 | font-family: var(--font-code); 26 | height: auto; 27 | } 28 | 29 | .dark .CodeMirror { 30 | color: var(--symbols); 31 | --symbols: #89ddff; 32 | --base: #a6accd; 33 | --comment: #6d6d6d; 34 | --keyword: #89ddff; 35 | --string: #c3e88d; 36 | --variable: #82aaff; 37 | --number: #f78c6c; 38 | --tags: #f07178; 39 | --brackets: var(--symbols); 40 | --property: #f07178; 41 | --attribute: #c792ea; 42 | --cursor: #fff; 43 | 44 | --selected-bg: rgba(255, 255, 255, 0.1); 45 | --selected-bg-non-focus: rgba(255, 255, 255, 0.15); 46 | } 47 | 48 | /* PADDING */ 49 | 50 | .CodeMirror-lines { 51 | padding: 4px 0; /* Vertical padding around content */ 52 | } 53 | .CodeMirror pre { 54 | padding: 0 4px; /* Horizontal padding of content */ 55 | } 56 | 57 | .CodeMirror-scrollbar-filler, 58 | .CodeMirror-gutter-filler { 59 | background-color: white; /* The little square between H and V scrollbars */ 60 | } 61 | 62 | /* GUTTER */ 63 | 64 | .CodeMirror-gutters { 65 | border-right: 1px solid var(--border); 66 | background-color: transparent; 67 | white-space: nowrap; 68 | } 69 | .CodeMirror-linenumber { 70 | padding: 0 3px 0 5px; 71 | min-width: 20px; 72 | text-align: right; 73 | color: var(--comment); 74 | white-space: nowrap; 75 | opacity: 0.6; 76 | } 77 | 78 | .CodeMirror-guttermarker { 79 | color: black; 80 | } 81 | .CodeMirror-guttermarker-subtle { 82 | color: #999; 83 | } 84 | 85 | /* FOLD GUTTER */ 86 | 87 | .CodeMirror-foldmarker { 88 | color: #414141; 89 | text-shadow: #ff9966 1px 1px 2px, #ff9966 -1px -1px 2px, #ff9966 1px -1px 2px, 90 | #ff9966 -1px 1px 2px; 91 | font-family: arial; 92 | line-height: 0.3; 93 | cursor: pointer; 94 | } 95 | .CodeMirror-foldgutter { 96 | width: 0.7em; 97 | } 98 | .CodeMirror-foldgutter-open, 99 | .CodeMirror-foldgutter-folded { 100 | cursor: pointer; 101 | } 102 | .CodeMirror-foldgutter-open:after, 103 | .CodeMirror-foldgutter-folded:after { 104 | content: '>'; 105 | font-size: 0.8em; 106 | opacity: 0.8; 107 | transition: transform 0.2s; 108 | display: inline-block; 109 | top: -0.1em; 110 | position: relative; 111 | transform: rotate(90deg); 112 | } 113 | .CodeMirror-foldgutter-folded:after { 114 | transform: none; 115 | } 116 | 117 | /* CURSOR */ 118 | 119 | .CodeMirror-cursor { 120 | border-left: 1px solid var(--cursor); 121 | border-right: none; 122 | width: 0; 123 | } 124 | /* Shown when moving in bi-directional text */ 125 | .CodeMirror div.CodeMirror-secondarycursor { 126 | border-left: 1px solid silver; 127 | } 128 | .cm-fat-cursor .CodeMirror-cursor { 129 | width: auto; 130 | border: 0 !important; 131 | background: #7e7; 132 | } 133 | .cm-fat-cursor div.CodeMirror-cursors { 134 | z-index: 1; 135 | } 136 | .cm-fat-cursor-mark { 137 | background-color: rgba(20, 255, 20, 0.5); 138 | -webkit-animation: blink 1.06s steps(1) infinite; 139 | -moz-animation: blink 1.06s steps(1) infinite; 140 | animation: blink 1.06s steps(1) infinite; 141 | } 142 | .cm-animate-fat-cursor { 143 | width: auto; 144 | border: 0; 145 | -webkit-animation: blink 1.06s steps(1) infinite; 146 | -moz-animation: blink 1.06s steps(1) infinite; 147 | animation: blink 1.06s steps(1) infinite; 148 | background-color: #7e7; 149 | } 150 | @-moz-keyframes blink { 151 | 0% { 152 | } 153 | 50% { 154 | background-color: transparent; 155 | } 156 | 100% { 157 | } 158 | } 159 | @-webkit-keyframes blink { 160 | 0% { 161 | } 162 | 50% { 163 | background-color: transparent; 164 | } 165 | 100% { 166 | } 167 | } 168 | @keyframes blink { 169 | 0% { 170 | } 171 | 50% { 172 | background-color: transparent; 173 | } 174 | 100% { 175 | } 176 | } 177 | 178 | .cm-tab { 179 | display: inline-block; 180 | text-decoration: inherit; 181 | } 182 | 183 | .CodeMirror-rulers { 184 | position: absolute; 185 | left: 0; 186 | right: 0; 187 | top: -50px; 188 | bottom: -20px; 189 | overflow: hidden; 190 | } 191 | .CodeMirror-ruler { 192 | border-left: 1px solid #ccc; 193 | top: 0; 194 | bottom: 0; 195 | position: absolute; 196 | } 197 | 198 | /* DEFAULT THEME */ 199 | .cm-s-default.CodeMirror { 200 | background-color: transparent; 201 | } 202 | .cm-s-default .cm-header { 203 | color: blue; 204 | } 205 | .cm-s-default .cm-quote { 206 | color: #090; 207 | } 208 | .cm-negative { 209 | color: #d44; 210 | } 211 | .cm-positive { 212 | color: #292; 213 | } 214 | .cm-header, 215 | .cm-strong { 216 | font-weight: bold; 217 | } 218 | .cm-em { 219 | font-style: italic; 220 | } 221 | .cm-link { 222 | text-decoration: underline; 223 | } 224 | .cm-strikethrough { 225 | text-decoration: line-through; 226 | } 227 | 228 | .cm-s-default .cm-atom, 229 | .cm-s-default .cm-def, 230 | .cm-s-default .cm-variable-2, 231 | .cm-s-default .cm-variable-3, 232 | .cm-s-default .cm-punctuation { 233 | color: var(--base); 234 | } 235 | .cm-s-default .cm-property { 236 | color: var(--property); 237 | } 238 | .cm-s-default .cm-hr, 239 | .cm-s-default .cm-comment { 240 | color: var(--comment); 241 | } 242 | .cm-s-default .cm-attribute { 243 | color: var(--attribute); 244 | } 245 | .cm-s-default .cm-keyword { 246 | color: var(--keyword); 247 | } 248 | .cm-s-default .cm-variable { 249 | color: var(--variable); 250 | } 251 | .cm-s-default .cm-tag { 252 | color: var(--tags); 253 | } 254 | .cm-s-default .cm-bracket { 255 | color: var(--brackets); 256 | } 257 | .cm-s-default .cm-number { 258 | color: var(--number); 259 | } 260 | .cm-s-default .cm-string, 261 | .cm-s-default .cm-string-2 { 262 | color: var(--string); 263 | } 264 | .cm-s-default .cm-type { 265 | color: #085; 266 | } 267 | .cm-s-default .cm-meta { 268 | color: #555; 269 | } 270 | .cm-s-default .cm-qualifier { 271 | color: var(--qualifier); 272 | } 273 | .cm-s-default .cm-builtin { 274 | color: #7539ff; 275 | } 276 | .cm-s-default .cm-link { 277 | color: var(--flash); 278 | } 279 | .cm-s-default .cm-error { 280 | color: #ff008c; 281 | } 282 | .cm-invalidchar { 283 | color: #ff008c; 284 | } 285 | 286 | .CodeMirror-composing { 287 | border-bottom: 2px solid; 288 | } 289 | 290 | /* Default styles for common addons */ 291 | 292 | div.CodeMirror span.CodeMirror-matchingbracket { 293 | color: #0b0; 294 | } 295 | div.CodeMirror span.CodeMirror-nonmatchingbracket { 296 | color: #a22; 297 | } 298 | .CodeMirror-matchingtag { 299 | background: rgba(255, 150, 0, 0.3); 300 | } 301 | .CodeMirror-activeline-background { 302 | background: #e8f2ff; 303 | } 304 | 305 | /* STOP */ 306 | 307 | /* The rest of this file contains styles related to the mechanics of 308 | the editor. You probably shouldn't touch them. */ 309 | 310 | .CodeMirror { 311 | position: relative; 312 | overflow: hidden; 313 | background: white; 314 | } 315 | 316 | .CodeMirror-scroll { 317 | overflow: scroll !important; /* Things will break if this is overridden */ 318 | /* 30px is the magic margin used to hide the element's real scrollbars */ 319 | /* See overflow: hidden in .CodeMirror */ 320 | margin-bottom: -30px; 321 | margin-right: -30px; 322 | padding-bottom: 30px; 323 | height: 100%; 324 | outline: none; /* Prevent dragging from highlighting the element */ 325 | position: relative; 326 | } 327 | .CodeMirror-sizer { 328 | position: relative; 329 | border-right: 30px solid transparent; 330 | } 331 | 332 | /* The fake, visible scrollbars. Used to force redraw during scrolling 333 | before actual scrolling happens, thus preventing shaking and 334 | flickering artifacts. */ 335 | .CodeMirror-vscrollbar, 336 | .CodeMirror-hscrollbar, 337 | .CodeMirror-scrollbar-filler, 338 | .CodeMirror-gutter-filler { 339 | position: absolute; 340 | z-index: 6; 341 | display: none; 342 | } 343 | .CodeMirror-vscrollbar { 344 | right: 0; 345 | top: 0; 346 | overflow-x: hidden; 347 | overflow-y: scroll; 348 | } 349 | .CodeMirror-hscrollbar { 350 | bottom: 0; 351 | left: 0; 352 | overflow-y: hidden; 353 | overflow-x: scroll; 354 | } 355 | .CodeMirror-scrollbar-filler { 356 | right: 0; 357 | bottom: 0; 358 | } 359 | .CodeMirror-gutter-filler { 360 | left: 0; 361 | bottom: 0; 362 | } 363 | 364 | .CodeMirror-gutters { 365 | position: absolute; 366 | left: 0; 367 | top: 0; 368 | min-height: 100%; 369 | z-index: 3; 370 | } 371 | .CodeMirror-gutter { 372 | white-space: normal; 373 | height: 100%; 374 | display: inline-block; 375 | vertical-align: top; 376 | margin-bottom: -30px; 377 | } 378 | .CodeMirror-gutter-wrapper { 379 | position: absolute; 380 | z-index: 4; 381 | background: none !important; 382 | border: none !important; 383 | } 384 | .CodeMirror-gutter-background { 385 | position: absolute; 386 | top: 0; 387 | bottom: 0; 388 | z-index: 4; 389 | } 390 | .CodeMirror-gutter-elt { 391 | position: absolute; 392 | cursor: default; 393 | z-index: 4; 394 | } 395 | .CodeMirror-gutter-wrapper ::selection { 396 | background-color: transparent; 397 | } 398 | .CodeMirror-gutter-wrapper ::-moz-selection { 399 | background-color: transparent; 400 | } 401 | 402 | .CodeMirror-lines { 403 | cursor: text; 404 | min-height: 1px; /* prevents collapsing before first draw */ 405 | } 406 | .CodeMirror pre { 407 | /* Reset some styles that the rest of the page might have set */ 408 | -moz-border-radius: 0; 409 | -webkit-border-radius: 0; 410 | border-radius: 0; 411 | border-width: 0; 412 | background: transparent; 413 | font-family: inherit; 414 | font-size: inherit; 415 | margin: 0; 416 | white-space: pre; 417 | word-wrap: normal; 418 | line-height: inherit; 419 | color: inherit; 420 | z-index: 2; 421 | position: relative; 422 | overflow: visible; 423 | -webkit-tap-highlight-color: transparent; 424 | -webkit-font-variant-ligatures: contextual; 425 | font-variant-ligatures: contextual; 426 | } 427 | .CodeMirror-wrap pre { 428 | word-wrap: break-word; 429 | white-space: pre-wrap; 430 | word-break: normal; 431 | } 432 | 433 | .CodeMirror-linebackground { 434 | position: absolute; 435 | left: 0; 436 | right: 0; 437 | top: 0; 438 | bottom: 0; 439 | z-index: 0; 440 | } 441 | 442 | .CodeMirror-linewidget { 443 | position: relative; 444 | z-index: 2; 445 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 446 | } 447 | 448 | .CodeMirror-rtl pre { 449 | direction: rtl; 450 | } 451 | 452 | .CodeMirror-code { 453 | outline: none; 454 | } 455 | 456 | /* Force content-box sizing for the elements where we expect it */ 457 | .CodeMirror-scroll, 458 | .CodeMirror-sizer, 459 | .CodeMirror-gutter, 460 | .CodeMirror-gutters, 461 | .CodeMirror-linenumber { 462 | -moz-box-sizing: content-box; 463 | box-sizing: content-box; 464 | } 465 | 466 | .CodeMirror-measure { 467 | position: absolute; 468 | width: 100%; 469 | height: 0; 470 | overflow: hidden; 471 | visibility: hidden; 472 | } 473 | 474 | .CodeMirror-cursor { 475 | position: absolute; 476 | pointer-events: none; 477 | } 478 | .CodeMirror-measure pre { 479 | position: static; 480 | } 481 | 482 | div.CodeMirror-cursors { 483 | visibility: hidden; 484 | position: relative; 485 | z-index: 3; 486 | } 487 | div.CodeMirror-dragcursors { 488 | visibility: visible; 489 | } 490 | 491 | .CodeMirror-focused div.CodeMirror-cursors { 492 | visibility: visible; 493 | } 494 | 495 | .CodeMirror-selected { 496 | background: var(--selected-bg-non-focus); 497 | } 498 | .CodeMirror-focused .CodeMirror-selected { 499 | background: var(--selected-bg); 500 | } 501 | .CodeMirror-crosshair { 502 | cursor: crosshair; 503 | } 504 | .CodeMirror-line::selection, 505 | .CodeMirror-line > span::selection, 506 | .CodeMirror-line > span > span::selection { 507 | background: var(--selected-bg); 508 | } 509 | .CodeMirror-line::-moz-selection, 510 | .CodeMirror-line > span::-moz-selection, 511 | .CodeMirror-line > span > span::-moz-selection { 512 | background: var(--selected-bg); 513 | } 514 | 515 | .cm-searching { 516 | background-color: #ffa; 517 | background-color: rgba(255, 255, 0, 0.4); 518 | } 519 | 520 | /* Used to force a border model for a node */ 521 | .cm-force-border { 522 | padding-right: 0.1px; 523 | } 524 | 525 | @media print { 526 | /* Hide the cursor when printing */ 527 | .CodeMirror div.CodeMirror-cursors { 528 | visibility: hidden; 529 | } 530 | } 531 | 532 | /* See issue #2901 */ 533 | .cm-tab-wrap-hack:after { 534 | content: ''; 535 | } 536 | 537 | /* Help users use markselection to safely style text background */ 538 | span.CodeMirror-selectedtext { 539 | background: none; 540 | } 541 | -------------------------------------------------------------------------------- /src/codemirror/codemirror.ts: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror' 2 | import './codemirror.css' 3 | 4 | // modes 5 | import 'codemirror/mode/javascript/javascript.js' 6 | import 'codemirror/mode/css/css.js' 7 | import 'codemirror/mode/htmlmixed/htmlmixed.js' 8 | 9 | // addons 10 | import 'codemirror/addon/edit/closebrackets.js' 11 | import 'codemirror/addon/edit/closetag.js' 12 | import 'codemirror/addon/comment/comment.js' 13 | import 'codemirror/addon/fold/foldcode.js' 14 | import 'codemirror/addon/fold/foldgutter.js' 15 | import 'codemirror/addon/fold/brace-fold.js' 16 | import 'codemirror/addon/fold/indent-fold.js' 17 | import 'codemirror/addon/fold/comment-fold.js' 18 | 19 | export default CodeMirror 20 | -------------------------------------------------------------------------------- /src/editor/Editor.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 36 | 37 | 44 | -------------------------------------------------------------------------------- /src/editor/FileSelector.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 127 | 128 | 231 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { ComponentOptions } from 'vue' 5 | const comp: ComponentOptions 6 | export default comp 7 | } 8 | -------------------------------------------------------------------------------- /src/icons/Moon.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/Share.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/Sun.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Repl } from './Repl.vue' 2 | export { default as Preview } from './output/Preview.vue' 3 | export { ReplStore, File } from './store' 4 | export { compileFile } from './transform' 5 | export type { Props as ReplProps } from './Repl.vue' 6 | export type { Store, StoreOptions, SFCOptions, StoreState } from './store' 7 | export type { OutputModes } from './output/types' 8 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | // @ts-expect-error Custom window property 5 | window.VUE_DEVTOOLS_CONFIG = { 6 | defaultSelectedAppId: 'repl' 7 | } 8 | 9 | createApp(App).mount('#app') 10 | -------------------------------------------------------------------------------- /src/output/Output.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | 40 | 75 | -------------------------------------------------------------------------------- /src/output/Preview.vue: -------------------------------------------------------------------------------- 1 | 207 | 208 | 213 | 214 | 223 | -------------------------------------------------------------------------------- /src/output/PreviewProxy.ts: -------------------------------------------------------------------------------- 1 | // ReplProxy and srcdoc implementation from Svelte REPL 2 | // MIT License https://github.com/sveltejs/svelte-repl/blob/master/LICENSE 3 | 4 | let uid = 1 5 | 6 | export class PreviewProxy { 7 | iframe: HTMLIFrameElement 8 | handlers: Record 9 | pending_cmds: Map< 10 | number, 11 | { resolve: (value: unknown) => void; reject: (reason?: any) => void } 12 | > 13 | handle_event: (e: any) => void 14 | 15 | constructor(iframe: HTMLIFrameElement, handlers: Record) { 16 | this.iframe = iframe 17 | this.handlers = handlers 18 | 19 | this.pending_cmds = new Map() 20 | 21 | this.handle_event = e => this.handle_repl_message(e) 22 | window.addEventListener('message', this.handle_event, false) 23 | } 24 | 25 | destroy() { 26 | window.removeEventListener('message', this.handle_event) 27 | } 28 | 29 | iframe_command(action: string, args: any) { 30 | return new Promise((resolve, reject) => { 31 | const cmd_id = uid++ 32 | 33 | this.pending_cmds.set(cmd_id, { resolve, reject }) 34 | 35 | this.iframe.contentWindow!.postMessage({ action, cmd_id, args }, '*') 36 | }) 37 | } 38 | 39 | handle_command_message(cmd_data: any) { 40 | let action = cmd_data.action 41 | let id = cmd_data.cmd_id 42 | let handler = this.pending_cmds.get(id) 43 | 44 | if (handler) { 45 | this.pending_cmds.delete(id) 46 | if (action === 'cmd_error') { 47 | let { message, stack } = cmd_data 48 | let e = new Error(message) 49 | e.stack = stack 50 | handler.reject(e) 51 | } 52 | 53 | if (action === 'cmd_ok') { 54 | handler.resolve(cmd_data.args) 55 | } 56 | } else { 57 | console.error('command not found', id, cmd_data, [ 58 | ...this.pending_cmds.keys() 59 | ]) 60 | } 61 | } 62 | 63 | handle_repl_message(event: any) { 64 | if (event.source !== this.iframe.contentWindow) return 65 | 66 | const { action, args } = event.data 67 | 68 | switch (action) { 69 | case 'cmd_error': 70 | case 'cmd_ok': 71 | return this.handle_command_message(event.data) 72 | case 'fetch_progress': 73 | return this.handlers.on_fetch_progress(args.remaining) 74 | case 'error': 75 | return this.handlers.on_error(event.data) 76 | case 'unhandledrejection': 77 | return this.handlers.on_unhandled_rejection(event.data) 78 | case 'console': 79 | return this.handlers.on_console(event.data) 80 | case 'console_group': 81 | return this.handlers.on_console_group(event.data) 82 | case 'console_group_collapsed': 83 | return this.handlers.on_console_group_collapsed(event.data) 84 | case 'console_group_end': 85 | return this.handlers.on_console_group_end(event.data) 86 | } 87 | } 88 | 89 | eval(script: string | string[]) { 90 | return this.iframe_command('eval', { script }) 91 | } 92 | 93 | handle_links() { 94 | return this.iframe_command('catch_clicks', {}) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/output/moduleCompiler.ts: -------------------------------------------------------------------------------- 1 | import { File, Store } from '../store' 2 | import { 3 | babelParse, 4 | MagicString, 5 | walk, 6 | walkIdentifiers, 7 | extractIdentifiers, 8 | isInDestructureAssignment, 9 | isStaticProperty 10 | } from 'vue/compiler-sfc' 11 | import { ExportSpecifier, Identifier, Node } from '@babel/types' 12 | 13 | export function compileModulesForPreview(store: Store, isSSR = false) { 14 | const seen = new Set() 15 | const processed: string[] = [] 16 | processFile( 17 | store, 18 | store.state.files[store.state.mainFile], 19 | processed, 20 | seen, 21 | isSSR 22 | ) 23 | 24 | if (!isSSR) { 25 | // also add css files that are not imported 26 | for (const filename in store.state.files) { 27 | if (filename.endsWith('.css')) { 28 | const file = store.state.files[filename] 29 | if (!seen.has(file)) { 30 | processed.push( 31 | `\nwindow.__css__ += ${JSON.stringify(file.compiled.css)}` 32 | ) 33 | } 34 | } 35 | } 36 | } 37 | 38 | return processed 39 | } 40 | 41 | const modulesKey = `__modules__` 42 | const exportKey = `__export__` 43 | const dynamicImportKey = `__dynamic_import__` 44 | const moduleKey = `__module__` 45 | 46 | // similar logic with Vite's SSR transform, except this is targeting the browser 47 | function processFile( 48 | store: Store, 49 | file: File, 50 | processed: string[], 51 | seen: Set, 52 | isSSR: boolean 53 | ) { 54 | if (seen.has(file)) { 55 | return [] 56 | } 57 | seen.add(file) 58 | 59 | if (!isSSR && file.filename.endsWith('.html')) { 60 | return processHtmlFile(store, file.code, file.filename, processed, seen) 61 | } 62 | 63 | let [js, importedFiles] = processModule( 64 | store, 65 | isSSR ? file.compiled.ssr : file.compiled.js, 66 | file.filename 67 | ) 68 | // append css 69 | if (!isSSR && file.compiled.css) { 70 | js += `\nwindow.__css__ += ${JSON.stringify(file.compiled.css)}` 71 | } 72 | // crawl child imports 73 | if (importedFiles.size) { 74 | for (const imported of importedFiles) { 75 | processFile(store, store.state.files[imported], processed, seen, isSSR) 76 | } 77 | } 78 | // push self 79 | processed.push(js) 80 | } 81 | 82 | function processModule( 83 | store: Store, 84 | src: string, 85 | filename: string 86 | ): [string, Set] { 87 | const s = new MagicString(src) 88 | 89 | const ast = babelParse(src, { 90 | sourceFilename: filename, 91 | sourceType: 'module' 92 | }).program.body 93 | 94 | const idToImportMap = new Map() 95 | const declaredConst = new Set() 96 | const importedFiles = new Set() 97 | const importToIdMap = new Map() 98 | 99 | function defineImport(node: Node, source: string) { 100 | const filename = source.replace(/^\.\/+/, '') 101 | if (!(filename in store.state.files)) { 102 | throw new Error(`File "${filename}" does not exist.`) 103 | } 104 | if (importedFiles.has(filename)) { 105 | return importToIdMap.get(filename)! 106 | } 107 | importedFiles.add(filename) 108 | const id = `__import_${importedFiles.size}__` 109 | importToIdMap.set(filename, id) 110 | s.appendLeft( 111 | node.start!, 112 | `const ${id} = ${modulesKey}[${JSON.stringify(filename)}]\n` 113 | ) 114 | return id 115 | } 116 | 117 | function defineExport(name: string, local = name) { 118 | s.append(`\n${exportKey}(${moduleKey}, "${name}", () => ${local})`) 119 | } 120 | 121 | // 0. instantiate module 122 | s.prepend( 123 | `const ${moduleKey} = ${modulesKey}[${JSON.stringify( 124 | filename 125 | )}] = { [Symbol.toStringTag]: "Module" }\n\n` 126 | ) 127 | 128 | // 1. check all import statements and record id -> importName map 129 | for (const node of ast) { 130 | // import foo from 'foo' --> foo -> __import_foo__.default 131 | // import { baz } from 'foo' --> baz -> __import_foo__.baz 132 | // import * as ok from 'foo' --> ok -> __import_foo__ 133 | if (node.type === 'ImportDeclaration') { 134 | const source = node.source.value 135 | if (source.endsWith('?raw')) { 136 | const url = source.slice(0, -4) 137 | s.overwrite(node.start!, node.end!, `const ${node.specifiers[0].local.name} = await (await fetch(${url.startsWith('http') ? `'${url}'` : `import.meta.resolve('${url}')`})).text()`) 138 | } else if (source.endsWith('.css')) { 139 | // import 'foo/style.css' --> , href is import.meta.resolve('foo/style.css') 140 | // import 'http://127.0.0.1/style.css' --> 141 | s.overwrite(node.start!, node.end!, `if(true){ 142 | const link = document.createElement('link'); 143 | link.rel = 'stylesheet'; 144 | link.href = ${source.startsWith('http') || source.startsWith('/') ? `'${source}'` : `import.meta.resolve('${source}')`}; 145 | document.head.appendChild(link); 146 | }`) 147 | } else if (source.endsWith('.json')) { 148 | // import data from 'foo/data.json' --> const data = await (await fetch(import.meta.resolve('foo/data.json'))).json() 149 | s.overwrite(node.start!, node.end!, `const ${node.specifiers[0].local.name} = await (await fetch(${source.startsWith('http') ? `'${source}'` : `import.meta.resolve('${source}'))`})).json()`) 150 | } else if (source.match(/(\.(ttf|otf|woff2?|eot|jpe?g|png|jfif|pjpeg|pjp|gif|svg|ico|webp|avif|mp4|webm|ogg|mp3|wav|flac|aac)|\?url)$/)) { 151 | // import font from 'foo/font.ttf' --> const font = import.meta.resolve('foo/font.ttf') 152 | s.overwrite(node.start!, node.end!, `const ${node.specifiers[0].local.name} = ${source.startsWith('http') ? `'${source}'` : `import.meta.resolve('${source}')`}`) 153 | } else if (source.startsWith('./')) { 154 | const importId = defineImport(node, node.source.value) 155 | for (const spec of node.specifiers) { 156 | if (spec.type === 'ImportSpecifier') { 157 | idToImportMap.set( 158 | spec.local.name, 159 | `${importId}.${(spec.imported as Identifier).name}` 160 | ) 161 | } else if (spec.type === 'ImportDefaultSpecifier') { 162 | idToImportMap.set(spec.local.name, `${importId}.default`) 163 | } else { 164 | // namespace specifier 165 | idToImportMap.set(spec.local.name, importId) 166 | } 167 | } 168 | s.remove(node.start!, node.end!) 169 | } 170 | } 171 | } 172 | 173 | // 2. check all export statements and define exports 174 | for (const node of ast) { 175 | // named exports 176 | if (node.type === 'ExportNamedDeclaration') { 177 | if (node.declaration) { 178 | if ( 179 | node.declaration.type === 'FunctionDeclaration' || 180 | node.declaration.type === 'ClassDeclaration' 181 | ) { 182 | // export function foo() {} 183 | defineExport(node.declaration.id!.name) 184 | } else if (node.declaration.type === 'VariableDeclaration') { 185 | // export const foo = 1, bar = 2 186 | for (const decl of node.declaration.declarations) { 187 | for (const id of extractIdentifiers(decl.id)) { 188 | defineExport(id.name) 189 | } 190 | } 191 | } 192 | s.remove(node.start!, node.declaration.start!) 193 | } else if (node.source) { 194 | // export { foo, bar } from './foo' 195 | const importId = defineImport(node, node.source.value) 196 | for (const spec of node.specifiers) { 197 | defineExport( 198 | (spec.exported as Identifier).name, 199 | `${importId}.${(spec as ExportSpecifier).local.name}` 200 | ) 201 | } 202 | s.remove(node.start!, node.end!) 203 | } else { 204 | // export { foo, bar } 205 | for (const spec of node.specifiers) { 206 | const local = (spec as ExportSpecifier).local.name 207 | const binding = idToImportMap.get(local) 208 | defineExport((spec.exported as Identifier).name, binding || local) 209 | } 210 | s.remove(node.start!, node.end!) 211 | } 212 | } 213 | 214 | // default export 215 | if (node.type === 'ExportDefaultDeclaration') { 216 | if ('id' in node.declaration && node.declaration.id) { 217 | // named hoistable/class exports 218 | // export default function foo() {} 219 | // export default class A {} 220 | const { name } = node.declaration.id 221 | s.remove(node.start!, node.start! + 15) 222 | s.append(`\n${exportKey}(${moduleKey}, "default", () => ${name})`) 223 | } else { 224 | // anonymous default exports 225 | s.overwrite(node.start!, node.start! + 14, `${moduleKey}.default =`) 226 | } 227 | } 228 | 229 | // export * from './foo' 230 | if (node.type === 'ExportAllDeclaration') { 231 | const importId = defineImport(node, node.source.value) 232 | s.remove(node.start!, node.end!) 233 | s.append(`\nfor (const key in ${importId}) { 234 | if (key !== 'default') { 235 | ${exportKey}(${moduleKey}, key, () => ${importId}[key]) 236 | } 237 | }`) 238 | } 239 | } 240 | 241 | // 3. convert references to import bindings 242 | for (const node of ast) { 243 | if (node.type === 'ImportDeclaration') continue 244 | walkIdentifiers(node, (id, parent, parentStack) => { 245 | const binding = idToImportMap.get(id.name) 246 | if (!binding) { 247 | return 248 | } 249 | if (isStaticProperty(parent) && parent.shorthand) { 250 | // let binding used in a property shorthand 251 | // { foo } -> { foo: __import_x__.foo } 252 | // skip for destructure patterns 253 | if ( 254 | !(parent as any).inPattern || 255 | isInDestructureAssignment(parent, parentStack) 256 | ) { 257 | s.appendLeft(id.end!, `: ${binding}`) 258 | } 259 | } else if ( 260 | parent.type === 'ClassDeclaration' && 261 | id === parent.superClass 262 | ) { 263 | if (!declaredConst.has(id.name)) { 264 | declaredConst.add(id.name) 265 | // locate the top-most node containing the class declaration 266 | const topNode = parentStack[1] 267 | s.prependRight(topNode.start!, `const ${id.name} = ${binding};\n`) 268 | } 269 | } else { 270 | s.overwrite(id.start!, id.end!, binding) 271 | } 272 | }) 273 | } 274 | 275 | // 4. convert dynamic imports 276 | ; (walk as any)(ast, { 277 | enter(node: Node, parent: Node) { 278 | if (node.type === 'Import' && parent.type === 'CallExpression') { 279 | const arg = parent.arguments[0] 280 | if (arg.type === 'StringLiteral' && arg.value.startsWith('./')) { 281 | s.overwrite(node.start!, node.start! + 6, dynamicImportKey) 282 | s.overwrite( 283 | arg.start!, 284 | arg.end!, 285 | JSON.stringify(arg.value.replace(/^\.\/+/, '')) 286 | ) 287 | } 288 | } 289 | } 290 | }) 291 | 292 | return [s.toString(), importedFiles] 293 | } 294 | 295 | const scriptRE = /]*>|>)([^]*?)<\/script>/gi 296 | const scriptModuleRE = 297 | /]*type\s*=\s*(?:"module"|'module')[^>]*>([^]*?)<\/script>/gi 298 | 299 | function processHtmlFile( 300 | store: Store, 301 | src: string, 302 | filename: string, 303 | processed: string[], 304 | seen: Set 305 | ) { 306 | const deps: string[] = [] 307 | let jsCode = '' 308 | const html = src 309 | .replace(scriptModuleRE, (_, content) => { 310 | const [code, importedFiles] = processModule(store, content, filename) 311 | if (importedFiles.size) { 312 | for (const imported of importedFiles) { 313 | processFile(store, store.state.files[imported], deps, seen, false) 314 | } 315 | } 316 | jsCode += '\n' + code 317 | return '' 318 | }) 319 | .replace(scriptRE, (_, content) => { 320 | jsCode += '\n' + content 321 | return '' 322 | }) 323 | processed.push(`document.body.innerHTML = ${JSON.stringify(html)}`) 324 | processed.push(...deps) 325 | processed.push(jsCode) 326 | } 327 | -------------------------------------------------------------------------------- /src/output/srcdoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /src/output/types.ts: -------------------------------------------------------------------------------- 1 | export type OutputModes = 'preview' | 'js' | 'css' | 'ssr' 2 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { reactive, watchEffect } from 'vue' 2 | import { compileFile } from './transform' 3 | import { utoa, atou } from './utils' 4 | import { 5 | SFCScriptCompileOptions, 6 | SFCAsyncStyleCompileOptions, 7 | SFCTemplateCompileOptions 8 | } from 'vue/compiler-sfc' 9 | import { OutputModes } from './output/types' 10 | 11 | const defaultMainFile = 'App.vue' 12 | 13 | const welcomeCode = ` 14 | 26 | 27 | 33 | `.trim() 34 | 35 | export class File { 36 | filename: string 37 | code: string 38 | hidden: boolean 39 | compiled = { 40 | js: '', 41 | css: '', 42 | ssr: '' 43 | } 44 | 45 | constructor(filename: string, code = '', hidden = false) { 46 | this.filename = filename 47 | this.code = code 48 | this.hidden = hidden 49 | } 50 | } 51 | 52 | export interface StoreState { 53 | mainFile: string 54 | files: Record 55 | activeFile: File 56 | errors: (string | Error)[] 57 | vueRuntimeURL: string 58 | } 59 | 60 | export interface SFCOptions { 61 | script?: Omit 62 | style?: SFCAsyncStyleCompileOptions 63 | template?: SFCTemplateCompileOptions 64 | } 65 | 66 | export interface Store { 67 | state: StoreState 68 | options?: SFCOptions 69 | vueVersion?: string 70 | init: () => void 71 | setActive: (filename: string) => void 72 | addFile: (filename: string | File) => void 73 | deleteFile: (filename: string) => void 74 | getImportMap: () => any 75 | initialShowOutput: boolean 76 | initialOutputMode: OutputModes 77 | } 78 | 79 | export interface StoreOptions { 80 | serializedState?: string 81 | showOutput?: boolean 82 | // loose type to allow getting from the URL without inducing a typing error 83 | outputMode?: OutputModes | string 84 | defaultVueRuntimeURL?: string 85 | defaultVueServerRendererURL?: string 86 | } 87 | 88 | export class ReplStore implements Store { 89 | state: StoreState 90 | vueVersion?: string 91 | options?: SFCOptions 92 | initialShowOutput: boolean 93 | initialOutputMode: OutputModes 94 | 95 | private defaultVueRuntimeURL: string 96 | 97 | constructor({ 98 | serializedState = '', 99 | defaultVueRuntimeURL = `https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.esm.browser.js`, 100 | showOutput = false, 101 | outputMode = 'preview' 102 | }: StoreOptions = {}) { 103 | let files: StoreState['files'] = {} 104 | 105 | if (serializedState) { 106 | const saved = JSON.parse(atou(serializedState)) 107 | for (const filename in saved) { 108 | files[filename] = new File(filename, saved[filename]) 109 | } 110 | } else { 111 | files = { 112 | [defaultMainFile]: new File(defaultMainFile, welcomeCode) 113 | } 114 | } 115 | 116 | this.defaultVueRuntimeURL = defaultVueRuntimeURL 117 | this.initialShowOutput = showOutput 118 | this.initialOutputMode = outputMode as OutputModes 119 | 120 | let mainFile = defaultMainFile 121 | if (!files[mainFile]) { 122 | mainFile = Object.keys(files)[0] 123 | } 124 | this.state = reactive({ 125 | mainFile, 126 | files, 127 | activeFile: files[mainFile], 128 | errors: [], 129 | vueRuntimeURL: this.defaultVueRuntimeURL 130 | }) 131 | 132 | this.initImportMap() 133 | } 134 | 135 | // don't start compiling until the options are set 136 | init() { 137 | watchEffect(() => compileFile(this, this.state.activeFile)) 138 | for (const file in this.state.files) { 139 | if (file !== defaultMainFile) { 140 | compileFile(this, this.state.files[file]) 141 | } 142 | } 143 | } 144 | 145 | setActive(filename: string) { 146 | this.state.activeFile = this.state.files[filename] 147 | } 148 | 149 | addFile(fileOrFilename: string | File): void { 150 | const file = 151 | typeof fileOrFilename === 'string' 152 | ? new File(fileOrFilename) 153 | : fileOrFilename 154 | this.state.files[file.filename] = file 155 | if (!file.hidden) this.setActive(file.filename) 156 | } 157 | 158 | deleteFile(filename: string) { 159 | if (confirm(`Are you sure you want to delete ${filename}?`)) { 160 | if (this.state.activeFile.filename === filename) { 161 | this.state.activeFile = this.state.files[this.state.mainFile] 162 | } 163 | delete this.state.files[filename] 164 | } 165 | } 166 | 167 | serialize() { 168 | return '#' + utoa(JSON.stringify(this.getFiles())) 169 | } 170 | 171 | getFiles() { 172 | const exported: Record = {} 173 | for (const filename in this.state.files) { 174 | exported[filename] = this.state.files[filename].code 175 | } 176 | return exported 177 | } 178 | 179 | async setFiles(newFiles: Record, mainFile = defaultMainFile) { 180 | const files: Record = {} 181 | if (mainFile === defaultMainFile && !newFiles[mainFile]) { 182 | files[mainFile] = new File(mainFile, welcomeCode) 183 | } 184 | for (const filename in newFiles) { 185 | files[filename] = new File(filename, newFiles[filename]) 186 | } 187 | for (const file in files) { 188 | await compileFile(this, files[file]) 189 | } 190 | this.state.mainFile = mainFile 191 | this.state.files = files 192 | this.initImportMap() 193 | this.setActive(mainFile) 194 | } 195 | 196 | private initImportMap() { 197 | const map = this.state.files['import-map.json'] 198 | if (!map) { 199 | this.state.files['import-map.json'] = new File( 200 | 'import-map.json', 201 | JSON.stringify( 202 | { 203 | imports: { 204 | vue: this.defaultVueRuntimeURL 205 | } 206 | }, 207 | null, 208 | 2 209 | ) 210 | ) 211 | } else { 212 | try { 213 | const json = JSON.parse(map.code) 214 | if (!json.imports.vue) { 215 | json.imports.vue = this.defaultVueRuntimeURL 216 | map.code = JSON.stringify(json, null, 2) 217 | } 218 | } catch (e) {} 219 | } 220 | } 221 | 222 | getImportMap() { 223 | try { 224 | return JSON.parse(this.state.files['import-map.json'].code) 225 | } catch (e) { 226 | this.state.errors = [ 227 | `Syntax error in import-map.json: ${(e as Error).message}` 228 | ] 229 | return {} 230 | } 231 | } 232 | 233 | setImportMap(map: { 234 | imports: Record 235 | scopes?: Record> 236 | }) { 237 | this.state.files['import-map.json']!.code = JSON.stringify(map, null, 2) 238 | } 239 | 240 | async setVueVersion(version: string) { 241 | this.vueVersion = version 242 | const runtimeUrl = `https://cdn.jsdelivr.net/npm/vue@${version}/dist/vue.esm.browser.js` 243 | this.state.vueRuntimeURL = runtimeUrl 244 | const importMap = this.getImportMap() 245 | const imports = importMap.imports || (importMap.imports = {}) 246 | imports.vue = runtimeUrl 247 | this.setImportMap(importMap) 248 | console.info(`[@vue/repl] Now using Vue version: ${version}`) 249 | } 250 | 251 | resetVueVersion() { 252 | this.vueVersion = undefined 253 | this.state.vueRuntimeURL = this.defaultVueRuntimeURL 254 | const importMap = this.getImportMap() 255 | const imports = importMap.imports || (importMap.imports = {}) 256 | imports.vue = this.defaultVueRuntimeURL 257 | this.setImportMap(importMap) 258 | console.info(`[@vue/repl] Now using default Vue version`) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/transform.ts: -------------------------------------------------------------------------------- 1 | import { Store, File } from './store' 2 | import { 3 | SFCDescriptor, 4 | BindingMetadata, 5 | shouldTransformRef, 6 | transformRef, 7 | // CompilerOptions, 8 | parse, 9 | compileStyleAsync, 10 | compileScript, 11 | rewriteDefault 12 | // compileTemplate 13 | } from 'vue/compiler-sfc' 14 | import { transform } from 'sucrase' 15 | // @ts-ignore 16 | import hashId from 'hash-sum' 17 | 18 | export const COMP_IDENTIFIER = `__sfc__` 19 | 20 | async function transformTS(src: string) { 21 | return transform(src, { 22 | transforms: ['typescript'] 23 | }).code 24 | } 25 | 26 | export async function compileFile( 27 | store: Store, 28 | { filename, code, compiled }: File 29 | ) { 30 | if (!code.trim()) { 31 | store.state.errors = [] 32 | return 33 | } 34 | 35 | if (filename.endsWith('.css')) { 36 | compiled.css = code 37 | store.state.errors = [] 38 | return 39 | } 40 | 41 | if (filename.endsWith('.js') || filename.endsWith('.ts')) { 42 | if (shouldTransformRef(code)) { 43 | code = transformRef(code, { filename }).code 44 | } 45 | if (filename.endsWith('.ts')) { 46 | code = await transformTS(code) 47 | } 48 | compiled.js = compiled.ssr = code 49 | store.state.errors = [] 50 | return 51 | } 52 | 53 | if (!filename.endsWith('.vue')) { 54 | store.state.errors = [] 55 | return 56 | } 57 | 58 | const id = hashId(filename) 59 | 60 | const { errors, descriptor } = parse(code, { 61 | filename, 62 | sourceMap: true 63 | }) 64 | if (errors.length) { 65 | store.state.errors = errors 66 | return 67 | } 68 | 69 | if ( 70 | descriptor.styles.some((s) => s.lang) || 71 | (descriptor.template && descriptor.template.lang) 72 | ) { 73 | store.state.errors = [ 74 | `lang="x" pre-processors for