├── .gitignore ├── LICENSE ├── README.md ├── example ├── A.jsx ├── App.jsx ├── B.jsx ├── C.jsx ├── index.js └── webpack.config.js ├── index.html ├── jest.config.js ├── package.json ├── src ├── index.ts ├── shim.d.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chengzhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ant-design-vue/vue-jsx-hot-loader 2 | 3 | Tweak Vue components written in JSX in real time. 4 | 5 | ## Usage 6 | 7 | ```js 8 | // webpack.config.js 9 | module.exports = { 10 | // ... 11 | module: { 12 | loaders: [ 13 | // Enable HMR for JSX. 14 | { 15 | test: /\.jsx$/, 16 | use: ["babel-loader", "@ant-design-vue/vue-jsx-hot-loader"], 17 | }, 18 | // Remember to use babel on the rest of the JS files. 19 | { 20 | test: /\.js$/, 21 | use: "babel-loader", 22 | }, 23 | ], 24 | }, 25 | }; 26 | ``` 27 | -------------------------------------------------------------------------------- /example/A.jsx: -------------------------------------------------------------------------------- 1 | export default () =>
23411dd
; 2 | -------------------------------------------------------------------------------- /example/App.jsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import A from './A'; 3 | import { B } from './B'; 4 | import C from './C' 5 | const App = defineComponent({ 6 | data() { 7 | return { 8 | a: 1 9 | } 10 | }, 11 | render() { 12 | const { a } = this; 13 | return ( 14 | <> 15 | {a} 16 |
{ this.a++; }}>Hello World!
17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | }); 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /example/B.jsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | 3 | const B = defineComponent({ 4 | data() { 5 | return { 6 | a: 1 7 | } 8 | }, 9 | render() { 10 | const { a } = this; 11 | return ( 12 | <> 13 |
{ this.a++; }}>{a}d4s
14 | 2 15 | 16 | ); 17 | } 18 | }); 19 | 20 | export { 21 | B 22 | }; 23 | -------------------------------------------------------------------------------- /example/C.jsx: -------------------------------------------------------------------------------- 1 | import {defineComponent, onMounted, ref} from 'vue'; 2 | 3 | export default defineComponent({ 4 | setup() { 5 | onMounted(() => { 6 | console.log('C') 7 | }) 8 | const c = ref(0) 9 | return () => (<> 10 |
{ 11 | c.value++ 12 | }}> 点我加一个 13 |
14 | 我是点C 我的值是 {c.value} 15 | ) 16 | } 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App'; 3 | 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const babelConfig = { 4 | plugins: ["@vue/babel-plugin-jsx"], 5 | }; 6 | 7 | module.exports = { 8 | mode: "development", 9 | entry: { 10 | app: path.resolve(__dirname, "./index.js"), 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, "./dist"), 14 | publicPath: "/dist/", 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.(js|jsx)$/, 20 | use: [ 21 | { 22 | loader: "babel-loader", 23 | options: babelConfig, 24 | }, 25 | "vue-jsx-hot-loader", 26 | ], 27 | }, 28 | ], 29 | }, 30 | devServer: { 31 | historyApiFallback: true, 32 | hot: true, 33 | open: true, 34 | }, 35 | resolve: { 36 | extensions: [".jsx", ".js"], 37 | }, 38 | resolveLoader: { 39 | alias: { 40 | "vue-jsx-hot-loader": require.resolve("../"), 41 | }, 42 | }, 43 | devtool: "cheap-module-eval-source-map", 44 | }; 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testTimeout: 30000, 4 | testEnvironment: 'node', 5 | testPathIgnorePatterns: ['/dist/', '/node_modules/'], 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ant-design-vue/vue-jsx-hot-loader", 3 | "version": "0.1.4", 4 | "description": "Tweak Vue components written in JSX in real time.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "dev": "tsc -w", 9 | "build": "tsc", 10 | "test": "jest --coverage", 11 | "dev-example": "webpack-dev-server --config example/webpack.config.js", 12 | "prepublishOnly": "tsc" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/Amour1688/vue-jsx-hot-loader.git" 20 | }, 21 | "keywords": [ 22 | "vue", 23 | "jsx", 24 | "hmr" 25 | ], 26 | "author": "Amour1688", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/Amour1688/vue-jsx-hot-loader/issues" 30 | }, 31 | "homepage": "https://github.com/Amour1688/vue-jsx-hot-loader#readme", 32 | "dependencies": { 33 | "@babel/parser": "^7.0.0", 34 | "@babel/traverse": "^7.0.0", 35 | "hash-sum": "^2.0.0", 36 | "loader-utils": "^2.0.0", 37 | "lodash-es": "^4.17.20" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.12.10", 41 | "@types/loader-utils": "^2.0.1", 42 | "@vue/babel-plugin-jsx": "^1.0.0", 43 | "babel-loader": "^8.2.2", 44 | "jest": "^26.6.3", 45 | "typescript": "^4.0.3", 46 | "vue": "^3.0.5", 47 | "webpack": "^4.44.2", 48 | "webpack-cli": "^3.0.0", 49 | "webpack-dev-server": "^3.11.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as webpack from 'webpack'; 2 | import hash from 'hash-sum'; 3 | import * as path from 'path'; 4 | import * as loaderUtils from 'loader-utils'; 5 | import * as t from '@babel/types'; 6 | import { parse } from '@babel/parser'; 7 | import { isDefineComponentCall, parseComponentDecls } from './utils'; 8 | 9 | export default function loader( 10 | this: webpack.loader.LoaderContext, 11 | source: string, 12 | ) { 13 | const loaderContext = this; 14 | loaderContext.cacheable?.(); 15 | 16 | if (!(loaderContext.mode === 'development')) { 17 | return source; 18 | } 19 | 20 | const webpackRemainingChain = loaderUtils.getRemainingRequest(loaderContext).split('!'); 21 | const fullPath = webpackRemainingChain[webpackRemainingChain.length - 1]; 22 | const filename = path.relative(process.cwd(), fullPath); 23 | 24 | const file = parse(source, { sourceType: 'module', plugins: ['jsx', 'typescript', 'decorators-legacy'] }); 25 | 26 | if (!(filename.endsWith('.jsx') || filename.endsWith('.tsx'))) { 27 | return source; 28 | } 29 | 30 | const declaredComponents: { name: string }[] = []; 31 | const hotComponents: { 32 | local: string; 33 | id: string; 34 | }[] = []; 35 | let hasDefault = false; 36 | 37 | for (const node of file.program.body) { 38 | if (t.isVariableDeclaration(node)) { 39 | declaredComponents.push(...parseComponentDecls(node)); 40 | } else if (t.isExportNamedDeclaration(node)) { 41 | const { specifiers = [], declaration } = node; 42 | if (t.isVariableDeclaration(declaration)) { 43 | hotComponents.push(...parseComponentDecls(declaration).map(({ name }) => ({ 44 | local: name, 45 | id: hash(`${filename}-${name}`), 46 | }))); 47 | } else if (t.isClassDeclaration(declaration)) { 48 | const name = declaration.id.name 49 | hotComponents.push({ 50 | local: name, 51 | id: hash(`${filename}-${name}`), 52 | }) 53 | } else if (specifiers.length) { 54 | for (const spec of specifiers) { 55 | if (t.isExportSpecifier(spec) && t.isIdentifier(spec.exported)) { 56 | if (declaredComponents.find(d => d.name === spec.local.name)) { 57 | hotComponents.push({ 58 | local: spec.local.name, 59 | id: hash(`${filename}-${spec.exported.name}`) 60 | }); 61 | } 62 | } 63 | } 64 | } 65 | } else if (t.isExportDefaultDeclaration(node)) { 66 | const { declaration } = node; 67 | if (t.isIdentifier(declaration)) { 68 | if (declaredComponents.find(d => d.name === declaration.name)) { 69 | hotComponents.push({ 70 | local: declaration.name, 71 | id: hash(`${filename}-default`) 72 | }) 73 | } 74 | } else if (isDefineComponentCall(declaration)) { 75 | hotComponents.push({ 76 | local: '__default__', 77 | id: hash(`${filename}-default`) 78 | }); 79 | hasDefault = true 80 | } 81 | } 82 | } 83 | 84 | if (hotComponents.length) { 85 | if (hasDefault) { 86 | source = source.replace( 87 | /export default defineComponent/g, 88 | `const __default__ = defineComponent` 89 | ) + `\nexport default __default__` 90 | } 91 | 92 | let callbackCode = ''; 93 | for (const { local, id } of hotComponents) { 94 | source += 95 | `\n${local}.__hmrId = '${id}'` + 96 | `\n__VUE_HMR_RUNTIME__.createRecord('${id}', ${local})` 97 | callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", ${local})` 98 | } 99 | 100 | source += 101 | `\n/* hot reload */` + 102 | `\nif (module.hot) {` + 103 | `\n module.hot.accept()` + 104 | `\n ${callbackCode}` + 105 | `\n}` 106 | } 107 | 108 | return source; 109 | }; 110 | -------------------------------------------------------------------------------- /src/shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'hash-sum' 2 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '@babel/core'; 2 | import * as t from '@babel/types'; 3 | 4 | export function isDefineComponentCall(node?: Node | null) { 5 | return t.isCallExpression(node) && t.isIdentifier(node.callee) && node.callee.name === 'defineComponent'; 6 | } 7 | 8 | export function parseComponentDecls(node: t.VariableDeclaration) { 9 | const names = []; 10 | for (const decl of node.declarations) { 11 | if (t.isIdentifier(decl.id) && isDefineComponentCall(decl.init)) { 12 | names.push({ 13 | name: decl.id.name 14 | }); 15 | } 16 | } 17 | 18 | return names; 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "dist", 5 | "sourceMap": false, 6 | "target": "es2015", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "allowJs": false, 12 | "allowSyntheticDefaultImports": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "noImplicitAny": true, 16 | "removeComments": false, 17 | "jsx": "preserve", 18 | "lib": [ 19 | "es6", 20 | "es7", 21 | "DOM" 22 | ] 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } --------------------------------------------------------------------------------