├── .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 |
37 |
38 |
39 | ```
40 |
41 | ## Advanced Usage
42 |
43 | ```vue
44 |
80 |
81 |
82 |
83 |
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 |
5 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 |
62 |
63 |
65 |
66 |
67 |
68 |
94 |
--------------------------------------------------------------------------------
/src/Header.vue:
--------------------------------------------------------------------------------
1 |
55 |
56 |
57 |
72 |
73 |
74 |
261 |
--------------------------------------------------------------------------------
/src/Message.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
37 |
{{ formatMessage(err || warn) }}
38 |
39 |
40 |
41 |
42 |
43 |
122 |
--------------------------------------------------------------------------------
/src/Repl.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
84 |
--------------------------------------------------------------------------------
/src/SplitPane.vue:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
62 |
69 |
73 |
74 |
75 |
76 |
79 |
80 |
81 |
82 |
185 |
--------------------------------------------------------------------------------
/src/codemirror/CodeMirror.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
44 |
--------------------------------------------------------------------------------
/src/editor/FileSelector.vue:
--------------------------------------------------------------------------------
1 |
80 |
81 |
82 |
88 |
94 | {{
95 | file === importMapFile ? 'Import Map' : file
96 | }}
97 |
98 |
102 |
103 |
104 |
105 |
113 |
114 |
115 |
116 |
117 |
122 | Import Map
123 |
124 |
125 |
126 |
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 |
2 |
6 |
--------------------------------------------------------------------------------
/src/icons/Share.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/src/icons/Sun.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
--------------------------------------------------------------------------------
/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 |
27 |
28 |
31 |
32 |
33 |
38 |
39 |
40 |
75 |
--------------------------------------------------------------------------------
/src/output/Preview.vue:
--------------------------------------------------------------------------------
1 |
207 |
208 |
209 |
210 |
211 |
212 |
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 = /
251 |
252 |
253 |
254 |
255 |
256 |
257 |