├── .prettierrc.json ├── env.d.ts ├── demo ├── assets │ ├── PureTemplateDoubleRoot.html │ ├── PureTemplate.html │ ├── real.jsx │ ├── blob.tsx │ ├── input.js │ ├── Chicago.jsx │ ├── Button.vue │ ├── ButtonSetup.vue │ └── chicagoNeighbourhoods.ts ├── main.ts ├── CustomLayout.vue ├── find.polyfill.ts └── App.vue ├── .prettierignore ├── public ├── favicon.ico └── index.html ├── postcss.config.js ├── tsconfig.build.lib.json ├── vite.config.ts ├── tsconfig.vitest.json ├── cypress.config.ts ├── tsconfig.cy.json ├── cypress ├── e2e │ ├── CheckErrors.cy.ts │ └── LiveRefresh.cy.ts └── support │ ├── e2e.ts │ └── commands.ts ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.config.json ├── .releaserc.js ├── index.html ├── tsconfig.app.json ├── .gitignore ├── src ├── VueLiveDefaultLayout.vue ├── utils │ ├── requireAtRuntime.ts │ ├── requireAtRuntime.spec.ts │ ├── evalInContext.ts │ ├── checkTemplate.ts │ └── checkTemplate.spec.ts ├── index.ts ├── Editor.vue ├── VueLive.vue └── Preview.vue ├── .github └── workflows │ ├── pr.yml │ └── release.yml ├── vite.config.lib.ts ├── dangerfile.js ├── package.json └── README.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/assets/PureTemplateDoubleRoot.html: -------------------------------------------------------------------------------- 1 |
A
2 |
B
3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | lib 3 | dist 4 | demo/assets/PureTemplate.html -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vue-styleguidist/vue-live/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /tsconfig.build.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "composite": false, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/main.ts: -------------------------------------------------------------------------------- 1 | import "./find.polyfill"; 2 | import { createApp } from "vue"; 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /demo/assets/PureTemplate.html: -------------------------------------------------------------------------------- 1 | let show = true 2 | let today = new Date() 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demo/assets/real.jsx: -------------------------------------------------------------------------------- 1 | const args = { 2 | type: "button", 3 | value: "update me", 4 | }; 5 | 6 | export default { 7 | render() { 8 | return ; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }); 8 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": ["node", "jsdom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo/assets/blob.tsx: -------------------------------------------------------------------------------- 1 | const args = { 2 | type: "button", 3 | value: "update me", 4 | } as const; 5 | 6 | type Key = keyof typeof args; 7 | 8 | export default { 9 | render() { 10 | return ; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /demo/assets/input.js: -------------------------------------------------------------------------------- 1 | new Vue({ 2 | template: ` 3 |
4 | 5 |

I am checked

6 |
`, 7 | data() { 8 | return { 9 | value: false, 10 | }; 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | projectId: 'wbesaj', 5 | e2e: { 6 | specPattern: "cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}", 7 | baseUrl: "http://localhost:4173", 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /tsconfig.cy.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["cypress/**/*.ts"], 4 | "compilerOptions": { 5 | "ignoreDeprecations": "5.0", 6 | "composite": true, 7 | "isolatedModules": false, 8 | "types": ["cypress"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /cypress/e2e/CheckErrors.cy.ts: -------------------------------------------------------------------------------- 1 | describe("Render Errors", () => { 2 | beforeEach(() => { 3 | cy.visit("/"); 4 | }); 5 | 6 | it("does not render an error", () => { 7 | cy.contains("More examples").click(); 8 | cy.get("[class*='Preview_error']").should("not.exist"); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.config.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | }, 13 | { 14 | "path": "./tsconfig.cy.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["src/**/*", "src/**/*.vue"], 4 | "compilerOptions": { 5 | "rootDir": "src", 6 | "composite": true, 7 | "ignoreDeprecations": "5.0", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | }, 12 | "outDir": "lib" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "package.json" 8 | ], 9 | "compilerOptions": { 10 | "composite": true, 11 | "ignoreDeprecations": "5.0", 12 | "types": ["node"], 13 | "resolveJsonModule": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/assets/Chicago.jsx: -------------------------------------------------------------------------------- 1 | import all from "./chicagoNeighbourhoods"; 2 | let i = 4; 3 | const sel = []; 4 | const getIndex = () => { 5 | const random = Math.random() * all.length 6 | return Math.floor(random) 7 | } 8 | 9 | while (i--) { 10 | sel.push(all[getIndex()]); 11 | } 12 | 13 |

Here are {{ sel.length }} hoods

14 | 15 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: [ 3 | { name: "master" }, 4 | { name: "next", channel: "next", prerelease: "beta" }, // Only after the `next` is created in the repo 5 | ], 6 | plugins: [ 7 | "@semantic-release/commit-analyzer", 8 | "@semantic-release/release-notes-generator", 9 | "@semantic-release/npm", 10 | "@semantic-release/github", 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Demo App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/assets/Button.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*", 6 | "src/**/*.vue", 7 | "demo/**/*.vue", 8 | "demo/**/*.ts", 9 | "demo/**/*.tsx" 10 | ], 11 | "exclude": ["src/**/__tests__/*"], 12 | "compilerOptions": { 13 | "composite": true, 14 | "rootDir": ".", 15 | "noEmit": true, 16 | "jsx":"preserve" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /lib 5 | 6 | # tests artefact 7 | /tests/e2e/videos 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | cypress/videos 27 | *tsbuildinfo 28 | -------------------------------------------------------------------------------- /src/VueLiveDefaultLayout.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /src/utils/requireAtRuntime.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return module from a given map (like {app: require('app')}) or throw. 3 | */ 4 | export default function requireAtRuntime( 5 | requires: Record, 6 | filepath: string 7 | ) { 8 | requires = requires || {}; 9 | if (!(filepath in requires)) { 10 | throw new Error( 11 | "import or require() statements can be added only by setting it using the requires prop" 12 | ); 13 | } 14 | 15 | return requires[filepath]; 16 | } 17 | -------------------------------------------------------------------------------- /demo/assets/ButtonSetup.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/utils/requireAtRuntime.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import requireAtRuntime from "./requireAtRuntime"; 3 | 4 | const map = { 5 | a: () => "a", 6 | }; 7 | 8 | test("return a module from the map", () => { 9 | const result = requireAtRuntime(map, "a"); 10 | expect(result).toBeDefined(); 11 | expect(result()).toBe("a"); 12 | }); 13 | 14 | test("throw if module is not in the map", () => { 15 | const fn = () => requireAtRuntime(map, "pizza"); 16 | expect(fn).toThrowError("require() statements can be added"); 17 | }); 18 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-live 9 | 10 | 11 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/utils/evalInContext.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * evaluate es5 code in the browser 3 | * and return value if there s a return statement 4 | * @param {String} code the body of the function to execute 5 | * @param {Function} require the fake function require 6 | */ 7 | export default function evalInContext( 8 | code: string, 9 | require: (path: string) => any, 10 | adaptCreateElement: (h: any) => any, 11 | concatenate: (...ags: any[]) => any, 12 | h: (...ags: any[]) => any 13 | ): Record { 14 | // eslint-disable-next-line no-new-func 15 | const func = new Function( 16 | "require", 17 | "__pragma__", 18 | "__concatenate__", 19 | "h", 20 | code 21 | ); 22 | 23 | return func(require, adaptCreateElement, concatenate, h); 24 | } 25 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/e2e/LiveRefresh.cy.ts: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe("Live Refresh", () => { 4 | beforeEach(() => { 5 | cy.visit("/"); 6 | 7 | cy.get(".preview-code:first").as("container"); 8 | 9 | cy.get("@container").children("div:last-child").as("preview"); 10 | }); 11 | 12 | it("changes the render after code change", () => { 13 | const textToReplace = "inline component"; 14 | const textReplaced = "red component"; 15 | 16 | cy.get("@preview") 17 | .get("[data-cy=my-button]") 18 | .should("have.text", textToReplace); 19 | 20 | cy.get("@container") 21 | .find(".prism-editor-wrapper textarea").as("editor"); 22 | 23 | cy.get("@editor").invoke("val") 24 | .then((val) => { 25 | cy.get("@editor") 26 | .clear() 27 | .invoke('val', `${val}`.replace(textToReplace, textReplaced)) 28 | .trigger('input'); 29 | 30 | cy.get("@preview") 31 | .get("[data-cy=my-button]") 32 | .should("have.text", textReplaced); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | on: 3 | pull_request: 4 | types: [opened, synchronize] 5 | branches: master 6 | 7 | concurrency: 8 | group: test-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Install 20 | run: npm ci 21 | 22 | - name: Check types 23 | run: npm run types 24 | 25 | - name: Test unit 26 | run: npm run test:unit 27 | 28 | - name: Build Demo 29 | run: npm run build:demo 30 | 31 | - name: Run E2E Test 32 | uses: cypress-io/github-action@v5 33 | with: 34 | start: npm run preview 35 | record: true 36 | browser: chrome 37 | env: 38 | # Recommended: pass the GitHub token lets this action correctly 39 | # determine the unique run id necessary to re-run the checks 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 42 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import VueLive from "./VueLive.vue"; 2 | import VueLivePreview from "./Preview.vue"; 3 | import VueLiveEditor from "./Editor.vue"; 4 | 5 | // Export components individually 6 | export { VueLive }; 7 | export { VueLivePreview }; 8 | export { VueLiveEditor }; 9 | 10 | // What should happen if the user installs the library as a plugin 11 | // @ts-ignore 12 | export function install(Vue) { 13 | // @ts-ignore 14 | if (install.installed) return; 15 | // @ts-ignore 16 | install.installed = true; 17 | Vue.component("VueLive", VueLive); 18 | Vue.component("VueLivePreview", VueLivePreview); 19 | Vue.component("VueLiveEditor", VueLiveEditor); 20 | } 21 | 22 | // Create module definition for Vue.use(plugin) 23 | const plugin = { 24 | install, 25 | }; 26 | 27 | // Auto-install when vue is found (eg. in browser via 128 | 129 | 167 | -------------------------------------------------------------------------------- /demo/assets/chicagoNeighbourhoods.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | "Albany Park", 3 | "Altgeld Gardens", 4 | "Andersonville", 5 | "Archer Heights", 6 | "Armour Square", 7 | "Ashburn", 8 | "Ashburn Estates", 9 | "Auburn Gresham", 10 | "Avalon Park", 11 | "Avondale", 12 | "Avondale Gardens", 13 | "Back of the Yards", 14 | "Belmont Central", 15 | "Belmont Gardens", 16 | "Belmont Heights", 17 | "Belmont Terrace", 18 | "Beverly", 19 | "Beverly View", 20 | "Beverly Woods", 21 | "Big Oaks", 22 | "Boystown", 23 | "Bowmanville", 24 | "Brainerd", 25 | "Brickyard", 26 | "Bridgeport", 27 | "Brighton Park", 28 | "Bronzeville", 29 | "Bucktown", 30 | "Budlong Woods", 31 | "Buena Park", 32 | "Burnside", 33 | "Cabrini–Green", 34 | "Calumet Heights", 35 | "Canaryville", 36 | "Central Station", 37 | "Chatham", 38 | "Chicago Lawn", 39 | "Chinatown", 40 | "Chrysler Village", 41 | "Clarendon Park", 42 | "Clearing East", 43 | "Clearing West", 44 | "Cottage Grove Heights", 45 | "Cragin", 46 | "Crestline", 47 | "Dearborn Homes", 48 | "Dearborn Park", 49 | "Douglas Park", 50 | "Dunning", 51 | "East Beverly", 52 | "East Chatham", 53 | "East Garfield Park", 54 | "East Hyde Park", 55 | "East Pilsen", 56 | "East Side", 57 | "East Village", 58 | "Eden Green", 59 | "Edgebrook", 60 | "Edgewater", 61 | "Edgewater Beach", 62 | "Edgewater Glen", 63 | "Edison Park", 64 | "Englewood", 65 | "Fernwood", 66 | "Fifth City", 67 | "Ford City", 68 | "Forest Glen", 69 | "Fuller Park", 70 | "Fulton River District", 71 | "Gage Park", 72 | "Galewood", 73 | "The Gap", 74 | "Garfield Ridge", 75 | "Gladstone Park", 76 | "Gold Coast", 77 | "Golden Gate", 78 | "Goose Island", 79 | "Graceland West", 80 | "Grand Boulevard", 81 | "Grand Crossing", 82 | "Greater Grand Crossing", 83 | "Greektown", 84 | "Gresham", 85 | "Groveland Park", 86 | "Hamilton Park", 87 | "Hanson Park", 88 | "Heart of Chicago", 89 | "Hegewisch", 90 | "Hermosa", 91 | "Hollywood Park", 92 | "Homan Square", 93 | "Humboldt Park", 94 | "Hyde Park", 95 | "Illinois Medical District", 96 | "Irving Park", 97 | "Irving Woods", 98 | "The Island", 99 | "Jackowo", 100 | "Jackson Park Highlands", 101 | "Jefferson Park", 102 | "K-Town", 103 | "Kelvyn Park", 104 | "Kennedy Park", 105 | "Kensington", 106 | "Kenwood", 107 | "Kilbourn Park", 108 | "Kosciuszko Park", 109 | "Lake Meadows", 110 | "Lake View", 111 | "Lake View East", 112 | "Lakewood / Balmoral", 113 | "LeClaire Courts", 114 | "Legends South (Robert Taylor Homes)", 115 | "Lilydale", 116 | "Lincoln Park", 117 | "Lincoln Square", 118 | "Lithuanian Plaza", 119 | "Little Italy", 120 | "Little Village", 121 | "Logan Square", 122 | "Longwood Manor", 123 | "The Loop", 124 | "Lower West Side", 125 | "Loyola", 126 | "Magnificent Mile", 127 | "Margate Park", 128 | "Marquette Park", 129 | "Marshall Square", 130 | "Marynook", 131 | "Mayfair", 132 | "McKinley Park", 133 | "Merchant Park", 134 | "Montclare", 135 | "Morgan Park", 136 | "Mount Greenwood", 137 | "Museum Campus", 138 | "New Eastside", 139 | "Near North Side", 140 | "Near West Side", 141 | "New Chinatown", 142 | "New City", 143 | "Noble Square", 144 | "North Austin", 145 | "North Center", 146 | "North Halsted", 147 | "North Kenwood", 148 | "North Lawndale", 149 | "North Mayfair", 150 | "North Park", 151 | "Nortown", 152 | "Norwood Park East", 153 | "Norwood Park West", 154 | "Oakland", 155 | "O'Hare", 156 | "Old Edgebrook", 157 | "Old Irving Park", 158 | "Old Norwood", 159 | "Old Town", 160 | "Old Town Triangle", 161 | "Oriole Park", 162 | "Palmer Square", 163 | "Park Manor", 164 | "Park West", 165 | "Parkview", 166 | "Peterson Park", 167 | "Pill Hill", 168 | "Pilsen", 169 | "Polish Downtown", 170 | "Polish Village", 171 | "Portage Park", 172 | "Prairie Avenue Historic District", 173 | "Prairie Shores", 174 | "Princeton Park", 175 | "Printer's Row", 176 | "Pulaski Park", 177 | "Pullman", 178 | "Ranch Triangle", 179 | "Ravenswood", 180 | "Ravenswood Gardens", 181 | "Ravenswood Manor", 182 | "River North", 183 | "River West", 184 | "River's Edge", 185 | "Riverdale", 186 | "Rogers Park", 187 | "Roscoe Village", 188 | "Rosehill", 189 | "Roseland", 190 | "Rosemoor", 191 | "Saint Ben's", 192 | "Sauganash", 193 | "Schorsch Forest View", 194 | "Schorsch Village", 195 | "Scottsdale", 196 | "Sheffield Neighbors", 197 | "Sheridan Park", 198 | "Sheridan Station Corridor", 199 | "Sleepy Hollow", 200 | "Smith Park", 201 | "South Austin", 202 | "South Chicago", 203 | "South Commons", 204 | "South Deering", 205 | "South East Ravenswood", 206 | "South Edgebrook", 207 | "South Lawndale", 208 | "South Loop", 209 | "South Shore", 210 | "Stateway Gardens", 211 | "Stony Island Park", 212 | "Streeterville", 213 | "Talley's Corner", 214 | "Tri-Taylor", 215 | "Ukrainian Village", 216 | "Union Ridge", 217 | "University Village", 218 | "Uptown", 219 | "The Villa", 220 | "Vittum Park", 221 | "Wacławowo", 222 | "Washington Heights", 223 | "Washington Park", 224 | "Wentworth Gardens", 225 | "West Beverly", 226 | "West Chatham", 227 | "West Chesterfield", 228 | "West DePaul", 229 | "West Elsdon", 230 | "West Englewood", 231 | "West Garfield Park", 232 | "West Humboldt Park", 233 | "West Lakeview", 234 | "West Lawn", 235 | "West Loop", 236 | "West Morgan Park", 237 | "West Pullman", 238 | "West Ridge", 239 | "West Rogers Park", 240 | "West Town", 241 | "West Woodlawn", 242 | "Wicker Park", 243 | "Wildwood", 244 | "Woodlawn", 245 | "Wrightwood", 246 | "Wrightwood Neighbors", 247 | "Wrigleyville", 248 | ]; 249 | -------------------------------------------------------------------------------- /src/VueLive.vue: -------------------------------------------------------------------------------- 1 | 47 | 217 | -------------------------------------------------------------------------------- /demo/App.vue: -------------------------------------------------------------------------------- 1 | 89 | 140 | 141 | 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-live 2 | 3 | A lightweight playground for live editing VueJs code in the browser 4 | 5 | [![Build Status](https://travis-ci.com/vue-styleguidist/vue-live.svg?branch=master)](https://app.travis-ci.com/github/vue-styleguidist/vue-live) 6 | [![NPM Version](https://img.shields.io/npm/v/vue-live.svg)](https://www.npmjs.com/package/vue-live) [![NPM Downloads](https://img.shields.io/npm/dm/vue-live.svg)](https://www.npmjs.com/package/vue-live) 7 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 8 | 9 | --- 10 | 11 | ## Usage 12 | 13 | Install the dependency 14 | 15 | ``` 16 | npm install --save vue-live 17 | ``` 18 | 19 | The simplest way to render components is as a VueJs template: 20 | 21 | ```vue 22 | 29 | 30 | 46 | ``` 47 | 48 | Check out the [demo](http://vue-live.surge.sh) for alternative syntaxes to write your showcases. 49 | 50 | ### Install for Vue 2.X 51 | 52 | The default version at `@latest` is for vue 3 and up. 53 | 54 | To install the version for vue 2, use the following: 55 | 56 | ``` 57 | npm install --save vue-live@1 58 | ``` 59 | 60 | ## Events 61 | 62 | ### `@error` 63 | 64 | When the template compilation or the script evaluation fail, errors are returned in this box. Hooking on these errors will not prevent them from displaying on the preview panel but will allow you to provide more info to your users about how to fix what they see. 65 | 66 | ```vue 67 | 73 | ``` 74 | 75 | ### `@success` 76 | 77 | When the template compilation and the script evaluation succeed, the `@success` event is emitted. If you provided extra info to your user about previous errors, you can use this event to clear the error message. 78 | 79 | ```vue 80 | 83 | ``` 84 | 85 | ## Props 86 | 87 | ### `code` 88 | 89 | **Type** String 90 | 91 | Specifies the initial code to be evaluated 92 | 93 | ```vue 94 | 97 | ``` 98 | 99 | ### `layout` 100 | 101 | **Type** vue component 102 | 103 | Layout to be used for displaying the 104 | 105 | Example 106 | 107 | ```vue 108 | 111 | 120 | ``` 121 | 122 | `layout.vue` 123 | 124 | ```vue 125 | 135 | 149 | ``` 150 | 151 | ### `components` 152 | 153 | **Type** Object: 154 | 155 | - key: registration name 156 | - value: VueJs component object 157 | 158 | Register some components to be used in the vue-live instance. 159 | 160 | Example 161 | 162 | ```vue 163 | 166 | 179 | ``` 180 | 181 | ### `directives` 182 | 183 | **Type** Object: 184 | 185 | - key: registration name 186 | - value: VueJs directive object 187 | 188 | You can use this prop in the same fashion as `components` above. 189 | 190 | ### `requires` 191 | 192 | **Type** Object: 193 | 194 | - key: query in the require/import statement 195 | - value: value returned by an es5 reauire statement 196 | 197 | To allow require statements on a code evaluated in the browser, we have to pre-package all dependencies. This allows bundlers to know in advance what external dependencies will be allowed in the code. 198 | 199 | Example 200 | 201 | ```vue 202 | 205 | 231 | ``` 232 | 233 | ### `jsx` 234 | 235 | **Type** Boolean 236 | 237 | JSX does not always play nice with vue templates. If you want to expose vue templates, leave this option off. If you plan on using render functions with JSX, you will have to turn this option on. 238 | 239 | Example 240 | 241 | ```vue 242 | 245 | 265 | ``` 266 | 267 | ### `delay` 268 | 269 | **Type** Number 270 | 271 | Time between a change in the code and its effect in the preview. 272 | 273 | > **Note** If a change happens before the prview has changed, the timer is reset. 274 | 275 | ### `editorProps` 276 | 277 | **Type** Object 278 | 279 | Props passed directly to [vue-prism-editor](https://github.com/koca/vue-prism-editor) as a spread 280 | 281 | ### `dataScope` 282 | 283 | **Type** Object 284 | 285 | Data object that wil be merged with the output data of the preview. 286 | 287 | Example 288 | 289 | ````vue 290 | ```vue 291 | 298 | 317 | ```` 318 | 319 | ### `checkVariableAvailability` 320 | 321 | **Type** Boolean 322 | 323 | Makes sure that every variable in the template actually exists when the user starts editing. 324 | 325 | Throws an error in te preview field when the variable dont exist. 326 | 327 | ### `squiggles` 328 | 329 | **Type** Boolean default: `true` 330 | 331 | Shows a red indicator when the parser errors with the code given. 332 | 333 | ## How to contribute 334 | 335 | ```sh 336 | npm ci 337 | ``` 338 | 339 | ### Compiles and hot-reloads for development 340 | 341 | ```sh 342 | npm run start 343 | ``` 344 | 345 | ### Compiles and minifies library for production using rollup 346 | 347 | ```sh 348 | npm run build 349 | ``` 350 | 351 | ### Run unit and e2e tests 352 | 353 | ```sh 354 | npm run test:unit 355 | npm run test:e2e 356 | ``` 357 | 358 | ### Lints and fixes files 359 | 360 | ```sh 361 | npm run lint 362 | ``` 363 | 364 | 365 | -------------------------------------------------------------------------------- /src/utils/checkTemplate.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseVue } from "@vue/compiler-dom"; 2 | import { createCompilerError } from "@vue/compiler-core"; 3 | import { parse as parseEs } from "acorn"; 4 | import { ancestor, simple } from "acorn-walk"; 5 | 6 | const ELEMENT = 1; 7 | const SIMPLE_EXPRESSION = 4; 8 | const INTERPOLATION = 5; 9 | 10 | interface Options { 11 | template?: string; 12 | props?: any; 13 | data?: () => Record | Record; 14 | computed?: Record; 15 | methods?: Record; 16 | attrAllowList?: string[]; 17 | setup?: () => Record | void; 18 | } 19 | 20 | export default function ( 21 | $options: Options, 22 | checkVariableAvailability: boolean 23 | ) { 24 | if (!$options.template) { 25 | return; 26 | } 27 | let ast; 28 | try { 29 | ast = parseVue($options.template); 30 | } catch (e: any) { 31 | throw createCompilerError(e.code, e.loc); 32 | } 33 | 34 | if (!checkVariableAvailability) { 35 | return; 36 | } 37 | 38 | const propNamesArray = $options.props 39 | ? Array.isArray($options.props) 40 | ? $options.props 41 | : Object.keys($options.props) 42 | : []; 43 | 44 | const dataArray = $options.data 45 | ? typeof $options.data === "function" 46 | ? Object.keys($options.data()) 47 | : Object.keys($options.data) 48 | : []; 49 | 50 | const computedArray = $options.computed ? Object.keys($options.computed) : []; 51 | 52 | const methodsArray = 53 | $options && $options.methods ? Object.keys($options.methods) : []; 54 | 55 | const setupOutput = 56 | $options && typeof $options.setup === "function" 57 | ? Object.keys($options.setup() ?? {}) 58 | : []; 59 | 60 | const scriptVars = [ 61 | ...propNamesArray, 62 | ...dataArray, 63 | ...computedArray, 64 | ...methodsArray, 65 | ...setupOutput, 66 | ]; 67 | 68 | traverse(ast, [ 69 | (templateAst: any, parentTemplateVars: any) => { 70 | const templateVars: string[] = []; 71 | if (templateAst.type === ELEMENT) { 72 | templateAst.props.forEach( 73 | (attr: { 74 | name: string; 75 | loc: any; 76 | type: number; 77 | exp?: { content: string }; 78 | }) => { 79 | const exp = 80 | attr.type !== SIMPLE_EXPRESSION && attr.exp 81 | ? attr.exp.content 82 | : undefined; 83 | if (!exp) { 84 | return; 85 | } 86 | if (attr.name === "slot") { 87 | const astSlot = parseEs(`var ${exp}=1`, { ecmaVersion: 2020 }); 88 | simple(astSlot, { 89 | VariableDeclarator(declarator) { 90 | // @ts-ignore 91 | const { id } = declarator; 92 | switch (id.type) { 93 | case "ArrayPattern": 94 | id.elements.forEach((e: any) => { 95 | templateVars.push(e.name); 96 | }); 97 | break; 98 | case "ObjectPattern": 99 | id.properties.forEach((e: any) => { 100 | templateVars.push(e.value.name); 101 | }); 102 | break; 103 | case "Identifier": 104 | templateVars.push(id.name); 105 | break; 106 | } 107 | return false; 108 | }, 109 | }); 110 | } else if (attr.name === "for") { 111 | const [vForLeft] = exp.split(/( in | of )/); 112 | const doubleForRE = /\((\w+),(\w+)\)/; 113 | if (doubleForRE.test(vForLeft.replace(" ", ""))) { 114 | const [, var1, var2] = Array.from( 115 | doubleForRE.exec(vForLeft.replace(" ", "")) || [] 116 | ); 117 | 118 | templateVars.push(var1 || ""); 119 | templateVars.push(var2 || ""); 120 | } else { 121 | templateVars.push(vForLeft); 122 | } 123 | } else { 124 | try { 125 | checkExpression(exp, scriptVars, [ 126 | ...parentTemplateVars, 127 | ...templateVars, 128 | ]); 129 | } catch (e: any) { 130 | throw new VueLiveParseTemplateError( 131 | e.message, 132 | exp, 133 | e, 134 | attr.loc 135 | ); 136 | } 137 | } 138 | } 139 | ); 140 | } else if (templateAst.type === INTERPOLATION) { 141 | try { 142 | if (templateAst.content) { 143 | checkExpression( 144 | templateAst.content.content, 145 | scriptVars, 146 | parentTemplateVars 147 | ); 148 | } 149 | } catch (e: any) { 150 | throw new VueLiveParseTemplateError( 151 | e.message, 152 | templateAst.content, 153 | e, 154 | templateAst.loc 155 | ); 156 | } 157 | } 158 | return templateVars; 159 | }, 160 | ]); 161 | } 162 | 163 | export function checkExpression( 164 | expression: string, 165 | availableVars: string[], 166 | templateVars: string[] 167 | ) { 168 | // try and parse the expression 169 | const ast = parseEs(`(function(){return ${expression}})()`, { 170 | ecmaVersion: 2020, 171 | }); 172 | 173 | // identify all variables that would be undefined because 174 | // - not in the options object 175 | // - not defined in the template 176 | ancestor(ast, { 177 | Identifier(identifier: any, ancestors: any) { 178 | const varName = identifier.name; 179 | if ( 180 | // if the identifier is a function call leave it alone 181 | ancestors.length >= 2 && 182 | ancestors[ancestors.length - 2].type === "CallExpression" && 183 | ancestors[ancestors.length - 2].callee.name === varName 184 | ) { 185 | return; 186 | } else if ( 187 | availableVars.indexOf(varName) === -1 && 188 | templateVars.indexOf(varName) === -1 && 189 | !/^\$/.test(varName) 190 | ) { 191 | const funcs = ancestors.filter( 192 | (node: any) => 193 | node.type === "ArrowFunctionExpression" || 194 | node.type === "FunctionExpression" 195 | ); 196 | if ( 197 | funcs.some((func: any) => 198 | func.params.some((p: any) => p.name === varName) 199 | ) 200 | ) { 201 | return; 202 | } 203 | throw new VueLiveUndefinedVariableError( 204 | `Variable "${varName}" is not defined.`, 205 | varName 206 | ); 207 | } 208 | }, 209 | }); 210 | } 211 | 212 | export function traverse( 213 | templateAst: any, 214 | handlers: any[], 215 | availableVarNames: string[] = [] 216 | ) { 217 | const traverseAstChildren = ( 218 | templateAst: any, 219 | availableVarNamesChildren: string[] 220 | ) => { 221 | const { children } = templateAst; 222 | if (children) { 223 | for (const childNode of children) { 224 | traverse(childNode, handlers, availableVarNamesChildren); 225 | } 226 | } 227 | }; 228 | 229 | // we load this object with all available varnames 230 | // discovered in the template parsing on v-for and v-slot 231 | const availableVarNamesThisLevel = handlers.reduce((acc, handler) => { 232 | const result = handler(templateAst, availableVarNames); 233 | if (result && result.length) { 234 | return acc.concat(result); 235 | } 236 | return acc; 237 | }, []); 238 | 239 | traverseAstChildren(templateAst, [ 240 | ...availableVarNames, 241 | ...availableVarNamesThisLevel, 242 | ]); 243 | } 244 | 245 | export class VueLiveUndefinedVariableError extends Error { 246 | varName: string; 247 | constructor(message: string, varName: string) { 248 | super(message); 249 | this.varName = varName; 250 | } 251 | } 252 | 253 | export class VueLiveParseTemplateAttrError extends Error { 254 | loc: any; 255 | constructor(message: string, loc: any) { 256 | super(message); 257 | this.loc = loc; 258 | } 259 | } 260 | 261 | export class VueLiveParseTemplateError extends Error { 262 | expression: { content: string } | string; 263 | subError: Error; 264 | loc: any; 265 | constructor( 266 | message: string, 267 | expression: { content: string } | string, 268 | subError: Error, 269 | loc: any 270 | ) { 271 | super(message); 272 | this.expression = expression; 273 | this.subError = subError; 274 | this.loc = loc; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/utils/checkTemplate.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import checkTemplateDummy from "./checkTemplate"; 3 | 4 | const checkTemplate = (opts: any) => checkTemplateDummy(opts, true); 5 | 6 | test("parse valid template without error with a function", () => { 7 | expect(() => 8 | checkTemplate({ template: '' }) 9 | ).not.toThrow(); 10 | }); 11 | 12 | test("parse valid template without error with a value in data", () => { 13 | expect(() => 14 | checkTemplate({ 15 | template: '
hello
', 16 | data() { 17 | return { 18 | today: "hello", 19 | }; 20 | }, 21 | }) 22 | ).not.toThrow(); 23 | }); 24 | 25 | test("parse valid template without error with a value in props", () => { 26 | expect(() => 27 | checkTemplate({ 28 | template: '
hello
', 29 | props: { 30 | today: { type: String }, 31 | }, 32 | }) 33 | ).not.toThrow(); 34 | }); 35 | 36 | test("parse valid template without error with a value in computed", () => { 37 | expect(() => 38 | checkTemplate({ 39 | template: '
hello
', 40 | computed: { 41 | today() { 42 | return "bonjour"; 43 | }, 44 | }, 45 | }) 46 | ).not.toThrow(); 47 | }); 48 | 49 | test("parse valid template without error with a value in methods", () => { 50 | expect(() => 51 | checkTemplate({ 52 | template: '
hello
', 53 | methods: { 54 | today() { 55 | return "bonjour"; 56 | }, 57 | }, 58 | }) 59 | ).not.toThrow(); 60 | }); 61 | 62 | test("parse valid template without error with values returned from setup", () => { 63 | expect(() => 64 | checkTemplate({ 65 | template: '
{{ msg }}
', 66 | setup() { 67 | return { 68 | today() { 69 | return "bonjour"; 70 | }, 71 | msg: "hello" 72 | } 73 | }, 74 | }) 75 | ).not.toThrow(); 76 | }); 77 | 78 | test("parse false value as a valid value", () => { 79 | expect(() => 80 | checkTemplate({ 81 | template: ` 82 |
83 | 84 |

I am checked

85 |
`, 86 | data() { 87 | return { 88 | value: false, 89 | }; 90 | }, 91 | }) 92 | ).not.toThrow(); 93 | }); 94 | 95 | test("parse invalid template with an error in the ++", () => { 96 | expect(() => 97 | checkTemplate({ 98 | template: '
hello
', 99 | }) 100 | ).toThrow(); 101 | }); 102 | 103 | test("parse invalid template with an error in a function call", () => { 104 | expect(() => 105 | checkTemplate({ 106 | template: '
hello
', 107 | }) 108 | ).toThrow(); 109 | }); 110 | 111 | test("parse invalid template with an error in a function call and a spread", () => { 112 | expect(() => 113 | checkTemplate({ 114 | template: '
hello
', 115 | }) 116 | ).toThrow(); 117 | }); 118 | 119 | test("if it starts with dollar, it should not throw", () => { 120 | expect(() => 121 | checkTemplate({ 122 | template: '
hello
', 123 | }) 124 | ).not.toThrow(); 125 | }); 126 | 127 | test("parse a valid arrow event handler properly", () => { 128 | expect(() => 129 | checkTemplate({ 130 | template: 131 | '
hello
', 132 | methods: { 133 | test(e: any) { 134 | console.log(e.target); 135 | }, 136 | }, 137 | }) 138 | ).not.toThrow(); 139 | }); 140 | 141 | test("parse a valid standard event handler properly", () => { 142 | expect(() => 143 | checkTemplate({ 144 | template: 145 | '
hello
', 146 | methods: { 147 | test(e: any) { 148 | console.log(e.target); 149 | }, 150 | }, 151 | }) 152 | ).not.toThrow(); 153 | }); 154 | 155 | test("parse invalid template with an error if the value is not in data", () => { 156 | expect(() => 157 | checkTemplate({ 158 | template: '
hello
', 159 | }) 160 | ).toThrowErrorMatchingInlineSnapshot( 161 | `"Variable \\"today\\" is not defined."` 162 | ); 163 | }); 164 | 165 | test("parse template interpolation and detect lonely undefined variables", () => { 166 | expect(() => 167 | checkTemplate({ 168 | template: "
{{ hello }}
", 169 | }) 170 | ).toThrowErrorMatchingInlineSnapshot( 171 | `"Variable \\"hello\\" is not defined."` 172 | ); 173 | }); 174 | 175 | test("parse template interpolation and detect impacted undefined variables", () => { 176 | expect(() => 177 | checkTemplate({ 178 | template: "
{{ hello + 'bonjour' }}
", 179 | }) 180 | ).toThrowErrorMatchingInlineSnapshot( 181 | `"Variable \\"hello\\" is not defined."` 182 | ); 183 | }); 184 | 185 | test("parse template interpolation and detect impacted right variables", () => { 186 | expect(() => 187 | checkTemplate({ 188 | template: 189 | "
{{ 'bonjour' + hello + 'sayonara' }}
", 190 | }) 191 | ).toThrowErrorMatchingInlineSnapshot( 192 | `"Variable \\"hello\\" is not defined."` 193 | ); 194 | }); 195 | 196 | test("parse invalid : template by throwing an error", () => { 197 | expect(() => 198 | checkTemplate({ 199 | template: '', 200 | }) 201 | ).toThrowErrorMatchingInlineSnapshot(`"Assigning to rvalue (1:21)"`); 202 | }); 203 | 204 | test("parse invalid @ template by throwing an error", () => { 205 | expect(() => 206 | checkTemplate({ 207 | template: '', 208 | }) 209 | ).toThrowErrorMatchingInlineSnapshot(`"Assigning to rvalue (1:21)"`); 210 | }); 211 | 212 | test("parse valid object not to throw", () => { 213 | expect(() => 214 | checkTemplate({ 215 | template: '
', 216 | }) 217 | ).not.toThrow(); 218 | }); 219 | 220 | test("parse expression using multiple lines to throw", () => { 221 | expect(() => 222 | checkTemplate({ 223 | template: `
`, 227 | }) 228 | ).toThrowErrorMatchingInlineSnapshot( 229 | `"Variable \\"hello\\" is not defined."` 230 | ); 231 | }); 232 | 233 | test("parse v-for expressions and add their vars to available data", () => { 234 | expect(() => 235 | checkTemplate({ 236 | template: `
`, 240 | }) 241 | ).not.toThrow(); 242 | }); 243 | 244 | test("parse v-for expressions with index and add their vars to available data", () => { 245 | expect(() => 246 | checkTemplate({ 247 | template: `
`, 251 | }) 252 | ).not.toThrow(); 253 | }); 254 | 255 | test("parse v-slot-scope expressions without issues", () => { 256 | expect(() => 257 | checkTemplate({ 258 | template: ``, 262 | }) 263 | ).not.toThrow(); 264 | }); 265 | 266 | test("parse v-slot-scope deconstructed expressions without issues", () => { 267 | expect(() => 268 | checkTemplate({ 269 | template: ``, 273 | }) 274 | ).not.toThrow(); 275 | }); 276 | 277 | test("parse v-slot-scope deconstructed array expressions without issues", () => { 278 | expect(() => 279 | checkTemplate({ 280 | template: ``, 284 | }) 285 | ).not.toThrow(); 286 | }); 287 | 288 | test("parse v-for nested expressions and add their vars to available data", () => { 289 | expect(() => 290 | checkTemplate({ 291 | template: `
292 |
293 | 297 |
298 |
`, 299 | }) 300 | ).not.toThrow(); 301 | }); 302 | 303 | -------------------------------------------------------------------------------- /src/Preview.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 281 | 282 | 290 | --------------------------------------------------------------------------------