Skip to content

Commit b317e06

Browse files
committed
feat: WIP
1 parent 2ac92f0 commit b317e06

13 files changed

+4383
-78
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020 天泽
3+
Copyright (c) 2020 Chengzhang
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

examples/A.jsx

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default () => <div>23411dd</div>;

examples/App.jsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { defineComponent } from 'vue';
2+
import A from './A';
3+
import { B } from './B';
4+
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+
<div onClick={() => { this.a++; }}>Hello World!!</div>
17+
<A />
18+
<B />
19+
</>
20+
)
21+
}
22+
});
23+
24+
export default App;
25+
26+
if (module.hot) {
27+
App.__hmrId = "${id}"
28+
const api = __VUE_HMR_RUNTIME__
29+
module.hot.accept();
30+
if (!api.createRecord('${id}', App)) {
31+
api.reload('${id}', App)
32+
}
33+
api.rerender('${id}', App.render);
34+
}

examples/B.jsx

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { defineComponent } from 'vue';
2+
3+
const cache = {}
4+
5+
const B = defineComponent({
6+
// setup() {
7+
// const a = ref(0)
8+
// return () => <div onClick={() => { a.value++; }}>{a.value}dssdddssd</div>
9+
// }
10+
data() {
11+
return {
12+
a: 1
13+
}
14+
},
15+
render() {
16+
const { a } = this;
17+
return (
18+
<>
19+
<div onClick={() => { this.a++; }}>{a}d4s</div>
20+
<span>23dd</span>
21+
</>
22+
);
23+
}
24+
});
25+
26+
export {
27+
B
28+
};
29+
30+
if (module.hot) {
31+
B.__hmrId = "b"
32+
const api = __VUE_HMR_RUNTIME__
33+
module.hot.accept();
34+
if (!module.hot.data) {
35+
api.createRecord('b', B);
36+
} else if (cache.b === B.render) {
37+
api.rerender('b', B.render);
38+
} else {
39+
api.reload('b', B)
40+
}
41+
42+
cache.b = JSON.stringify(B.render);
43+
}

examples/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { createApp } from 'vue';
2+
import App from './App';
3+
4+
createApp(App).mount('#app');

index.html

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Demo</title>
6+
</head>
7+
<body>
8+
<div id="app"></div>
9+
<script src="/dist/app.js"></script>
10+
</body>
11+
</html>

package.json

+16-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"version": "1.0.0",
44
"description": "Tweak Vue components written in JSX in real time.",
55
"main": "dist/index.js",
6-
"scripts": {},
6+
"scripts": {
7+
"dev": "webpack-dev-server",
8+
"build": "tsc"
9+
},
710
"files": [
811
"dist"
912
],
@@ -24,13 +27,22 @@
2427
"homepage": "https://github.com/Amour1688/vue-jsx-hot-loader#readme",
2528
"dependencies": {
2629
"@babel/parser": "^7.0.0",
30+
"@babel/template": "^7.0.0",
2731
"@babel/traverse": "^7.0.0",
2832
"hash-sum": "^2.0.0",
29-
"loader-utils": "^2.0.0"
33+
"loader-utils": "^2.0.0",
34+
"lodash-es": "^4.17.20"
3035
},
3136
"devDependencies": {
32-
"@types/webpack": "^4.41.22",
37+
"@babel/core": "^7.12.10",
38+
"@types/loader-utils": "^2.0.1",
39+
"@vue/babel-plugin-jsx": "^1.0.0",
40+
"babel-loader": "^8.2.2",
41+
"jest": "^26.6.3",
3342
"typescript": "^4.0.3",
34-
"webpack": "^4.44.2"
43+
"vue": "^3.0.5",
44+
"webpack": "^4.44.2",
45+
"webpack-cli": "^3.0.0",
46+
"webpack-dev-server": "^3.11.1"
3547
}
3648
}

src/hotReload.ts

+5-13
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
11
export function genHotReloadCode(
22
id: string,
3-
jsxRequest?: string
3+
component: string
44
): string {
55
return `
66
/* hot reload */
7-
if (module.hot) {
8-
script.__hmrId = "${id}"
7+
if (module.hot && ${component}.render) {
8+
${component}.__hmrId = "${id}"
99
const api = __VUE_HMR_RUNTIME__
1010
module.hot.accept()
1111
if (!api.createRecord('${id}')) {
12-
api.reload('${id}', script)
12+
api.reload('${id}', ${component})
1313
}
14-
${jsxRequest ? genTemplateHotReloadCode(id, jsxRequest) : ''}
14+
api.rerender('b', ${component}.render);
1515
}
1616
`
1717
}
18-
19-
function genTemplateHotReloadCode(id: string, request: string) {
20-
return `
21-
module.hot.accept(${request}, () => {
22-
api.rerender('${id}', render)
23-
})
24-
`
25-
};

src/index.ts

+128-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,133 @@
1-
import webpack from 'webpack';
2-
import * as parser from "@babel/parser";
3-
import traverse from "@babel/traverse";
1+
import * as webpack from 'webpack';
2+
import * as 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 traverse from '@babel/traverse';
7+
import { File } from '@babel/types'
8+
import { parse } from "@babel/parser";
9+
import { isDefineComponentCall, parseComponentDecls } from './utils';
10+
11+
const hasJSX = (file: File) => {
12+
let fileHasJSX = false;
13+
traverse(file, {
14+
JSXElement(path) {
15+
fileHasJSX = true;
16+
path.stop();
17+
},
18+
JSXFragment(path) {
19+
fileHasJSX = true;
20+
path.stop();
21+
},
22+
});
23+
24+
return fileHasJSX;
25+
};
426

527
export default function loader(
628
this: webpack.loader.LoaderContext,
7-
source: string
8-
): string {
29+
source: string,
30+
sourceMap: string
31+
) {
932
const loaderContext = this;
10-
11-
return source;
33+
loaderContext.cacheable?.();
34+
const isDev = process.env.NODE_ENV === 'development';
35+
36+
if (isDev) {
37+
loaderContext.callback(null, source, sourceMap);
38+
}
39+
40+
const file = parse(source);
41+
42+
if (!hasJSX(file)) {
43+
loaderContext.callback(null, source, sourceMap);
44+
return;
45+
}
46+
47+
const webpackRemainingChain = loaderUtils.getRemainingRequest(loaderContext).split('!');
48+
const fullPath = webpackRemainingChain[webpackRemainingChain.length - 1];
49+
const filename = path.relative(process.cwd(), fullPath);
50+
51+
const declaredComponents: { name: string }[] = [];
52+
const hotComponents: {
53+
local: string;
54+
exported: string;
55+
id: string;
56+
}[] = [];
57+
let defaultIdentifier: t.Identifier | null = null;
58+
59+
traverse(file, {
60+
VariableDeclaration(nodePath) {
61+
declaredComponents.push(...parseComponentDecls(nodePath.node));
62+
},
63+
ExportNamedDeclaration(nodePath) {
64+
const { specifiers = [], declaration } = nodePath.node;
65+
if (t.isVariableDeclaration(declaration)) {
66+
hotComponents.push(...parseComponentDecls(declaration).map(({ name }) => ({
67+
local: name,
68+
exported: name,
69+
id: hash(`${filename}-${name}`),
70+
})));
71+
} else if (specifiers.length) {
72+
for (const spec of specifiers) {
73+
if (t.isExportSpecifier(spec) && t.isIdentifier(spec.exported)) {
74+
if (declaredComponents.find(d => d.name === spec.local.name)) {
75+
hotComponents.push({
76+
local: spec.local.name,
77+
exported: spec.exported.name,
78+
id: hash(`${filename}-${spec.exported.name}`)
79+
});
80+
}
81+
}
82+
}
83+
}
84+
},
85+
ExportDefaultDeclaration(nodePath) {
86+
const { declaration } = nodePath.node;
87+
if (t.isIdentifier(declaration)) {
88+
if (declaredComponents.find(d => d.name === declaration.name)) {
89+
hotComponents.push({
90+
local: declaration.name,
91+
exported: 'default',
92+
id: hash(`${filename}-default`)
93+
})
94+
}
95+
} else if (isDefineComponentCall(declaration)) {
96+
defaultIdentifier = nodePath.scope.generateUidIdentifier('default')
97+
hotComponents.push({
98+
local: defaultIdentifier.name,
99+
exported: 'default',
100+
id: hash(`${filename}-default`)
101+
});
102+
}
103+
}
104+
});
105+
106+
if (hotComponents.length) {
107+
if (defaultIdentifier) {
108+
const { name } = defaultIdentifier as t.Identifier;
109+
source.replace(
110+
/export default defineComponent/g,
111+
`const ${name} = defineComponent`
112+
) + `\nexport default ${name}`
113+
}
114+
115+
let callbackCode = '';
116+
for (const { local, exported, id } of hotComponents) {
117+
source +=
118+
`\n${local}.__hmrId = '${id}'` +
119+
`\n__VUE_HMR_RUNTIME__.createRecord('${id}', ${local})`
120+
callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", __${exported})`
121+
}
122+
123+
source += `
124+
/* hot reload */
125+
if (module.hot) {
126+
module.hot.accept()
127+
${callbackCode}
128+
}
129+
`
130+
}
131+
132+
loaderContext.callback(null, source, sourceMap);
12133
};

src/shim.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module 'hash-sum'

src/utils.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
1-
export function isFuntionalComponent(
2-
code: string
3-
): boolean {
4-
return !!code;
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';
56
}
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+
21+
// const x = 1;
22+
23+
export const x = 1;

webpack.config.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const path = require('path');
2+
3+
const babelConfig = {
4+
plugins: [
5+
'@vue/babel-plugin-jsx'
6+
],
7+
}
8+
9+
module.exports = {
10+
mode: 'development',
11+
entry: {
12+
app: './examples/index.js',
13+
},
14+
output: {
15+
path: path.resolve(__dirname, './dist'),
16+
publicPath: '/dist/',
17+
},
18+
module: {
19+
rules: [
20+
{
21+
test: /\.(js|jsx)$/,
22+
loader: 'babel-loader',
23+
options: babelConfig,
24+
}
25+
],
26+
},
27+
devServer: {
28+
historyApiFallback: true,
29+
hot: true,
30+
open: true
31+
},
32+
resolve: {
33+
extensions: ['.jsx', '.js']
34+
}
35+
};

0 commit comments

Comments
 (0)