Skip to content

Commit

Permalink
feat(vue-server): server component hmr (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored May 11, 2024
1 parent 8facddb commit 692cf14
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 8 deletions.
59 changes: 58 additions & 1 deletion vue-server/e2e/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Page, expect, test } from "@playwright/test";
import { testNoJs, waitForHydration } from "./helper";
import { createEditor, testNoJs, waitForHydration } from "./helper";

test("basic @js", async ({ page }) => {
await page.goto("/");
Expand Down Expand Up @@ -80,3 +80,60 @@ async function testFormNavigation(page: Page, options: { js: boolean }) {
options.js ? "hello" : "",
);
}

test("hmr server @dev", async ({ page }) => {
await page.goto("/");
await waitForHydration(page);
await page.pause();

// check client state is preserved
await page.getByRole("button", { name: "client sfc: 0" }).click();
await page.getByRole("button", { name: "client sfc: 1" }).isVisible();
await page.getByRole("heading", { name: "Vue Server Component" }).click();

using file = createEditor("src/demo/routes/layout.tsx");
file.edit((s) =>
s.replace("Vue Server Component", "Vue [EDIT] Server Component"),
);

await page
.getByRole("heading", { name: "Vue [EDIT] Server Component" })
.click();
await page.getByRole("button", { name: "client sfc: 1" }).isVisible();

await page.reload();
await page
.getByRole("heading", { name: "Vue [EDIT] Server Component" })
.click();
await page.getByRole("button", { name: "client sfc: 0" }).isVisible();
});

test("hmr sfc @dev", async ({ page }) => {
await page.goto("/sfc");
await waitForHydration(page);
await page.pause();

await page.getByRole("button", { name: "client counter 0" }).first().click();
await page.getByRole("button", { name: "client counter 1" }).click();
await page.getByRole("button", { name: "client counter 2" }).isVisible();
await expect(page.getByText("server random")).toHaveCount(2);

using clientFile = createEditor("src/demo/routes/_slot.vue");
clientFile.edit((s) => s.replace("client counter", "client [EDIT] counter"));
await page
.getByRole("button", { name: "client [EDIT] counter 2" })
.isVisible();
await page
.getByRole("button", { name: "client [EDIT] counter 0" })
.isVisible();

using serverFile = createEditor("src/demo/routes/_slot.server.vue");
serverFile.edit((s) => s.replace("server random", "server [EDIT] random"));
await expect(page.getByText("server [EDIT] random")).toHaveCount(2);
await page
.getByRole("button", { name: "client [EDIT] counter 2" })
.isVisible();
await page
.getByRole("button", { name: "client [EDIT] counter 0" })
.isVisible();
});
15 changes: 15 additions & 0 deletions vue-server/e2e/helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from "node:fs";
import test, { type Page, expect } from "@playwright/test";

export const testNoJs = test.extend({
Expand All @@ -7,3 +8,17 @@ export const testNoJs = test.extend({
export async function waitForHydration(page: Page) {
await expect(page.getByText("[mounted: 1]")).toBeVisible();
}

export function createEditor(filepath: string) {
let init = fs.readFileSync(filepath, "utf-8");
let data = init;
return {
edit(editFn: (data: string) => string) {
data = editFn(data);
fs.writeFileSync(filepath, data);
},
[Symbol.dispose]() {
fs.writeFileSync(filepath, init);
},
};
}
7 changes: 7 additions & 0 deletions vue-server/src/demo/entry-browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ function main() {
document.title = "🚨 HYDRATE ERROR";
});
app.mount(el);

if (import.meta.hot) {
import.meta.hot.on("vue-server:update", (e) => {
console.log("[vue-server] hot update", e.file);
window.history.replaceState({}, "", window.location.href);
});
}
}

function listenHistory(onNavigation: () => void) {
Expand Down
50 changes: 43 additions & 7 deletions vue-server/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ export default defineConfig((env) => ({
}));

function vitePluginVueServer(): PluginOption {
const clientIds = new Set<string>();
const serverIds = new Set<string>();

return [
// non server (i.e. browser and ssr)
// client sfc (i.e. browser and ssr)
vue({
exclude: ["**/*.server.vue"],
}),
// server
// server sfc
patchServerVue(
vue({
include: ["**/*.server.vue"],
Expand All @@ -57,19 +60,52 @@ function vitePluginVueServer(): PluginOption {
}
},
},
{
name: vitePluginVueServer.name + ":hmr",
handleHotUpdate(ctx) {
if (
ctx.modules.length > 0 &&
ctx.modules.every((m) => m.id && !clientIds.has(m.id))
) {
console.log(`[vue-server] update ${ctx.file}`);
ctx.server.hot.send({
type: "custom",
event: "vue-server:update",
data: {
file: ctx.file,
},
});
// server module/transform cache is already invalidated up to server entry,
// so we simply return empty to avoid full-reload
// https://github.com/vitejs/vite/blob/f71ba5b94a6e862460a96c7bf5e16d8ae66f9fe7/packages/vite/src/node/server/index.ts#L796-L798
return [];
}
},
},
{
// track which id is processed in which environment
// by intercepting transform
name: vitePluginLogger.name + ":track-environment",
transform(_code, id, options) {
if (options?.ssr) {
serverIds.add(id);
} else {
clientIds.add(id);
}
},
},
];
}

// force non-ssr transform to always render vnode
function patchServerVue(plugin: Plugin): Plugin {
tinyassert(typeof plugin.transform === "function");
const oldTransform = plugin.transform;
plugin.transform = function (code, id, _options) {
return oldTransform.apply(this, [code, id]);
plugin.transform = async function (code, id, _options) {
// need to force non-ssr transform to always render vnode
return oldTransform.apply(this, [code, id, { ssr: false }]);
};

// also remove handleHotUpdate
// otherwise we get `TypeError: true is not a function` somewhere...
// also remove handleHotUpdate and handle server component hmr on our own
tinyassert(typeof plugin.handleHotUpdate === "function");
delete plugin.handleHotUpdate;

Expand Down

0 comments on commit 692cf14

Please sign in to comment.