Skip to content

feat: support for Vue3 JSX HMR #2018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ff7335d
feat: support vue3 jsx hmr
liyincode Mar 20, 2024
bd6da63
fix: React is undefined in playwrihgt test
liyincode Mar 20, 2024
197d770
fix: build() to dev() for dev sever test
liyincode Mar 20, 2024
e40c9ef
feat: use babel plugin to support vue jsx hmr
liyincode Mar 25, 2024
f07111c
feat: use babel plugin to support vue jsx hmr
liyincode Mar 26, 2024
4a14932
fix: disabled babel cache and reset hotComponents in pre hook
liyincode Mar 26, 2024
6b231ae
feat: update e2e test case
liyincode Mar 26, 2024
6863223
fix: jsx src import case not be parsed by babel
liyincode Mar 27, 2024
03fa866
feat: replace a single file with an npm package
liyincode Apr 7, 2024
1f85277
feat: only isDev mode, hmr can work
liyincode Apr 7, 2024
df85101
fix: delete useless dependencies
liyincode Apr 7, 2024
67e84ff
Merge branch 'refs/heads/main' into feat/vue-jsx-hmr
liyincode Apr 7, 2024
3c9d779
fix: resolved conflicts with branch origin/main
liyincode Apr 7, 2024
c8fc932
Merge branch 'main' into feat/vue-jsx-hmr
liyincode Apr 7, 2024
28c1e7a
feat: use isUsingHMR check hmr status
liyincode Apr 7, 2024
453ec18
Merge remote-tracking branch 'origin/feat/vue-jsx-hmr' into feat/vue-…
liyincode Apr 7, 2024
a024546
style: fix lint
liyincode Apr 8, 2024
aedb3c5
test(plugin-vue-jsx): update snapshot with vue-jsx-hmr plugin changed
liyincode Apr 8, 2024
006d666
Merge branch 'main' into feat/vue-jsx-hmr
liyincode Apr 8, 2024
9391766
Merge branch 'main' into feat/vue-jsx-hmr
liyincode Apr 8, 2024
50bc1ce
Merge branch 'main' into feat/vue-jsx-hmr
liyincode Apr 9, 2024
00ae893
fix(plugin-vue-jsx): fix e2e error in windows platform
liyincode Apr 9, 2024
5bc1a56
test(plugin-vue-jsx): disabled hmr e2e test in windows
liyincode Apr 9, 2024
2de39be
test(plugin-vue-jsx): test replace with rspackOnlyTest
liyincode Apr 10, 2024
d34b7bd
Merge branch 'main' into feat/vue-jsx-hmr
liyincode Apr 10, 2024
840cbcb
fix(plugin-vue-jsx): fix pnpm-lock.yaml error
liyincode Apr 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 291 additions & 0 deletions e2e/cases/vue/jsx-hmr/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import { expect } from '@playwright/test';
import { dev, gotoPage, rspackOnlyTest } from '@e2e/helper';
import path from 'node:path';
import fs from 'node:fs';

rspackOnlyTest('should render', async ({ page }) => {
const rsbuild = await dev({
cwd: __dirname,
});

await gotoPage(page, rsbuild);

await expect(page.locator('.named')).toHaveText('named 0');
await expect(page.locator('.named-specifier')).toHaveText(
'named specifier 1',
);
await expect(page.locator('.default')).toHaveText('default 2');
await expect(page.locator('.default-tsx')).toHaveText('default tsx 3');
await expect(page.locator('.script')).toHaveText('script 4');
await expect(page.locator('.ts-import')).toHaveText('success');
await rsbuild.close();
});

rspackOnlyTest('should update', async ({ page }) => {
const rsbuild = await dev({
cwd: __dirname,
});

await gotoPage(page, rsbuild);

await page.locator('.named').click();
await expect(page.locator('.named')).toHaveText('named 1');
await page.locator('.named-specifier').click();
await expect(page.locator('.named-specifier')).toHaveText(
'named specifier 2',
);
await page.locator('.default').click();
await expect(page.locator('.default')).toHaveText('default 3');
await page.locator('.default-tsx').click();
await expect(page.locator('.default-tsx')).toHaveText('default tsx 4');
await page.locator('.script').click();
await expect(page.locator('.script')).toHaveText('script 5');
});

rspackOnlyTest('hmr: named export', async ({ page }) => {
const rsbuild = await dev({
cwd: __dirname,
});

await gotoPage(page, rsbuild);

await page.locator('.named').click();
await expect(page.locator('.named')).toHaveText('named 1');
await page.locator('.named-specifier').click();
await expect(page.locator('.named-specifier')).toHaveText(
'named specifier 2',
);
await page.locator('.default').click();
await expect(page.locator('.default')).toHaveText('default 3');
await page.locator('.default-tsx').click();
await expect(page.locator('.default-tsx')).toHaveText('default tsx 4');

editFile('Comps.jsx', (code) =>
code.replace('named {count', 'named updated {count'),
);

await untilUpdated(
() => page.locator('.named').textContent(),
'named updated 0',
);

// affect all components in same file
await expect(page.locator('.named-specifier')).toHaveText(
'named specifier 1',
);
await expect(page.locator('.default')).toHaveText('default 2');
// should not affect other components from different file
expect(await page.textContent('.default-tsx')).toMatch('default tsx 4');

// reset code
editFile('Comps.jsx', (code) =>
code.replace('named updated {count', 'named {count'),
);
await rsbuild.close();
});

rspackOnlyTest('hmr: named export via specifier', async ({ page }) => {
const rsbuild = await dev({
cwd: __dirname,
});

await gotoPage(page, rsbuild);

await page.locator('.default').click();
await expect(page.locator('.default')).toHaveText('default 3');
await page.locator('.default-tsx').click();
await expect(page.locator('.default-tsx')).toHaveText('default tsx 4');

editFile('Comps.jsx', (code) =>
code.replace('named specifier {count', 'named specifier updated {count'),
);
await untilUpdated(
() => page.locator('.named-specifier').textContent(),
'named specifier updated 1',
);

// affect all components in same file
await expect(page.locator('.default')).toHaveText('default 2');
// should not affect other components on the page
expect(await page.textContent('.default-tsx')).toMatch('default tsx 4');

// reset code
editFile('Comps.jsx', (code) =>
code.replace('named specifier updated {count', 'named specifier {count'),
);
await rsbuild.close();
});

rspackOnlyTest('hmr: default export', async ({ page }) => {
const rsbuild = await dev({
cwd: __dirname,
});

await gotoPage(page, rsbuild);

await page.locator('.default-tsx').click();
await expect(page.locator('.default-tsx')).toHaveText('default tsx 4');

editFile('Comps.jsx', (code) =>
code.replace('default {count', 'default updated {count'),
);
await untilUpdated(() => page.textContent('.default'), 'default updated 2');

// should not affect other components on the page
expect(await page.textContent('.default-tsx')).toMatch('default tsx 4');

// reset code
editFile('Comps.jsx', (code) =>
code.replace('default updated {count', 'default {count'),
);
await rsbuild.close();
});

rspackOnlyTest('hmr: default Default export', async ({ page }) => {
const rsbuild = await dev({
cwd: __dirname,
});

await gotoPage(page, rsbuild);

await page.locator('.named').click();
await expect(page.locator('.named')).toHaveText('named 1');

editFile('Comp.tsx', (code) =>
code.replace('default tsx {count', 'default tsx updated {count'),
);
await untilUpdated(
() => page.textContent('.default-tsx'),
'default tsx updated 3',
);

// should not affect other components on the page
expect(await page.textContent('.named')).toMatch('named 1');

// reset code
editFile('Comp.tsx', (code) =>
code.replace('default tsx updated {count', 'default tsx {count'),
);
await rsbuild.close();
});

// // not pass
Copy link
Member

@chenjiahan chenjiahan Apr 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add a comment here:

// TODO: see https://github.com/web-infra-dev/rsbuild/issues/153

// rspackOnlyTest('hmr: vue script lang=jsx', async ({ page }) => {
// const rsbuild = await dev({
// cwd: __dirname,
// });
//
// await gotoPage(page, rsbuild);
//
// page.locator('.script').click();
// await expect(page.locator('.script')).toHaveText('script 5');
//
// editFile('Script.vue', (code) =>
// code.replace('script {count', 'script updated {count'),
// );
//
// await untilUpdated(() => page.textContent('.script'), 'script updated 4');
//
// // reset code
// editFile('Script.vue', (code) =>
// code.replace('script updated {count', 'script {count'),
// );
// await rsbuild.close();
// });
//
// // not pass
// rspackOnlyTest('hmr: script in .vue', async ({ page }) => {
// const rsbuild = await dev({
// cwd: __dirname,
// });
//
// await gotoPage(page, rsbuild);
//
// await page.locator('.src-import').click();
// await expect(page.locator('.src-import')).toHaveText('6');
//
// editFile('Script.vue', (code) =>
// code.replace('script {count', 'script updated {count'),
// );
//
// await untilUpdated(() => page.textContent('.script'), 'script updated 4');
//
// expect(await page.textContent('.src-import')).toMatch('6');
//
// // reset code
// editFile('Script.vue', (code) =>
// code.replace('script updated {count', 'script {count'),
// );
// await rsbuild.close();
// });
//
// // not pass
// rspackOnlyTest('hmr: src import in .vue', async ({ page }) => {
// const rsbuild = await dev({
// cwd: __dirname,
// });
//
// await gotoPage(page, rsbuild);
//
// await page.locator('.script').click();
// await expect(page.locator('.script')).toHaveText('script 5');
//
// editFile('SrcImport.jsx', (code) =>
// code.replace('src import {count', 'src import updated {count'),
// );
//
// await untilUpdated(
// () => page.textContent('.src-import'),
// 'src import updated 5',
// );
//
// // reset code
// editFile('SrcImport.jsx', (code) =>
// code.replace('src import updated {count', 'src import {count'),
// );
// await rsbuild.close();
// });
//
rspackOnlyTest('hmr: setup jsx in .vue', async ({ page }) => {
const rsbuild = await dev({
cwd: __dirname,
});

await gotoPage(page, rsbuild);

editFile('setup-syntax-jsx.vue', (code) =>
code.replace('let count = ref(100)', 'let count = ref(1000)'),
);
await untilUpdated(() => page.textContent('.setup-jsx'), '1000');

// reset code
editFile('setup-syntax-jsx.vue', (code) =>
code.replace('let count = ref(1000)', 'let count = ref(100)'),
);
await rsbuild.close();
});

function editFile(filename: string, replacer: (str: string) => string): void {
filename = path.join(__dirname, 'src', filename);
const content = fs.readFileSync(filename, 'utf-8');
const modified = replacer(content);
fs.writeFileSync(filename, modified);
}

const timeout = (n: number) => new Promise((r) => setTimeout(r, n));

export async function untilUpdated(
poll: () => Promise<string | null>,
expected: string,
): Promise<void> {
const maxTries = 50;
for (let tries = 0; tries < maxTries; tries++) {
const actual = (await poll()) ?? '';
if (actual.indexOf(expected) > -1 || tries === maxTries - 1) {
expect(actual).toMatch(expected);
break;
} else {
await timeout(50);
}
}
}
19 changes: 19 additions & 0 deletions e2e/cases/vue/jsx-hmr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@e2e/vue-jsx-hmr",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "rsbuild dev",
"build": "rsbuild build",
"preview": "rsbuild preview"
},
"dependencies": {
"vue": "^3.4.19"
},
"devDependencies": {
"@rsbuild/core": "workspace:*",
"@rsbuild/plugin-vue": "workspace:*",
"@rsbuild/plugin-vue-jsx": "workspace:*",
"@rsbuild/plugin-babel": "workspace:*"
}
}
19 changes: 19 additions & 0 deletions e2e/cases/vue/jsx-hmr/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from '@rsbuild/core';
import { pluginVue } from '@rsbuild/plugin-vue';
import { pluginVueJsx } from '@rsbuild/plugin-vue-jsx';
import { pluginBabel } from '@rsbuild/plugin-babel';

export default defineConfig({
plugins: [
pluginVue(),
pluginBabel({
include: /\.(?:jsx|tsx)(\.js)?$/,
exclude: /[\\/]node_modules[\\/]/,
}),
pluginVueJsx(),
],

performance: {
buildCache: false,
},
});
14 changes: 14 additions & 0 deletions e2e/cases/vue/jsx-hmr/src/Comp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineComponent, ref } from 'vue';

const Default = defineComponent(() => {
const count = ref(3);
const inc = () => count.value++;

return () => (
<button class="default-tsx" onClick={inc}>
default tsx {count.value}
</button>
);
});

export default Default;
35 changes: 35 additions & 0 deletions e2e/cases/vue/jsx-hmr/src/Comps.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { defineComponent, ref } from 'vue';

export const Named = defineComponent(() => {
const count = ref(0);
const inc = () => count.value++;

return () => (
<button class="named" onClick={inc}>
named {count.value}
</button>
);
});

const NamedSpec = defineComponent(() => {
const count = ref(1);
const inc = () => count.value++;

return () => (
<button class="named-specifier" onClick={inc}>
named specifier {count.value}
</button>
);
});
export { NamedSpec };

export default defineComponent(() => {
const count = ref(2);
const inc = () => count.value++;

return () => (
<button class="default" onClick={inc}>
default {count.value}
</button>
);
});
14 changes: 14 additions & 0 deletions e2e/cases/vue/jsx-hmr/src/Script.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="jsx">
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const count = ref(4)
const inc = () => count.value++

return () => (
<button class="script" onClick={inc}>
script {count.value}
</button>
)
})
</script>
Loading