├── .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 | }
--------------------------------------------------------------------------------