Skip to content

Commit

Permalink
feat: sharedDuringBuild unocss plugin (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Apr 30, 2024
1 parent 2eb4be2 commit 334df37
Show file tree
Hide file tree
Showing 13 changed files with 843 additions and 5 deletions.
1 change: 1 addition & 0 deletions examples/react-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ pnpm cf-release
- [x] css
- [x] client
- [x] server
- [x] unocss
- [ ] code split
58 changes: 58 additions & 0 deletions examples/react-server/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,61 @@ test("css hmr client @dev", async ({ page }) => {
page.getByTestId("client-component").getByRole("button", { name: "+" }),
).toHaveCSS("background-color", "rgb(255, 123, 123)");
});

test("unocss basic @js", async ({ page }) => {
usePageErrorChecker(page);
await page.goto("/");
await waitForHydration(page);
await testUnocssBasic(page);
});

testNoJs("unocss basic @nojs @build", async ({ page }) => {
usePageErrorChecker(page);
await page.goto("/");
await testUnocssBasic(page);
});

async function testUnocssBasic(page: Page) {
await expect(page.getByText("unocss (server)")).toHaveCSS(
"background-color",
"rgb(220, 220, 255)",
);
await expect(page.getByText("unocss (client)")).toHaveCSS(
"background-color",
"rgb(255, 220, 220)",
);
}

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

await using serverFile = await createEditor("src/routes/page.tsx");
await using clientFile = await createEditor("src/routes/_client.tsx");
await using _ = await createReloadChecker(page);

await expect(page.getByText("unocss (server)")).toHaveCSS(
"background-color",
"rgb(220, 220, 255)",
);
await serverFile.edit((s) =>
s.replace("rgb(220,220,255)", "rgb(199,199,255)"),
);
await expect(page.getByText("unocss (server)")).toHaveCSS(
"background-color",
"rgb(199, 199, 255)",
);

await expect(page.getByText("unocss (client)")).toHaveCSS(
"background-color",
"rgb(255, 220, 220)",
);
await clientFile.edit((s) =>
s.replace("rgb(255,220,220)", "rgb(255,199,199)"),
);
await expect(page.getByText("unocss (client)")).toHaveCSS(
"background-color",
"rgb(255, 199, 199)",
);
});
4 changes: 3 additions & 1 deletion examples/react-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"@hiogawa/vite-plugin-ssr-middleware-alpha": "workspace:*",
"@types/react": "18.2.72",
"@types/react-dom": "18.2.22",
"happy-dom": "^14.7.1"
"@unocss/vite": "^0.59.4",
"happy-dom": "^14.7.1",
"unocss": "^0.59.4"
},
"volta": {
"extends": "../../package.json"
Expand Down
10 changes: 10 additions & 0 deletions examples/react-server/src/__snapshots__/basic.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ exports[`basic 1`] = `
server
)
</div>
<div
class="flex justify-center w-36 m-1 p-1 bg-[rgb(220,220,255)]"
>
unocss (server)
</div>
<div
data-testid="server-action"
>
Expand Down Expand Up @@ -87,6 +92,11 @@ exports[`basic 1`] = `
client
)
</div>
<div
class="flex justify-center w-36 m-1 p-1 bg-[rgb(255,220,220)]"
>
unocss (client)
</div>
<div
data-hydrated="true"
>
Expand Down
1 change: 1 addition & 0 deletions examples/react-server/src/entry-client.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "virtual:unocss.css";
import React from "react";
import reactDomClient from "react-dom/client";
import type { StreamData } from "./entry-react-server";
Expand Down
6 changes: 4 additions & 2 deletions examples/react-server/src/features/style/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ export function vitePluginServerCss({
},
},
},
createVirtualPlugin(VIRTUAL_SSR_CSS.slice(8) + "?direct", async () => {
createVirtualPlugin(VIRTUAL_SSR_CSS.slice(8), async (id) => {
tinyassert($__global.server);
tinyassert(id.includes("?direct"));
return collectStyle($__global.server.environments["client"], [
ENTRY_CLIENT_BOOTSTRAP,
// TODO: split css per-route?
Expand Down Expand Up @@ -99,11 +100,12 @@ export function vitePluginServerCss({
];
}

function invalidateModule(server: DevEnvironment, id: string) {
export function invalidateModule(server: DevEnvironment, id: string) {
const mod = server.moduleGraph.getModuleById(id);
if (mod) {
server.moduleGraph.invalidateModule(mod);
}
return mod;
}

async function collectStyle(server: DevEnvironment, entries: string[]) {
Expand Down
122 changes: 122 additions & 0 deletions examples/react-server/src/features/unocss/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { debounce, objectHas, tinyassert } from "@hiogawa/utils";
import vitePluginUnocss, { type UnocssVitePluginAPI } from "@unocss/vite";
import { DevEnvironment, type Plugin } from "vite";
import { invalidateModule } from "../style/plugin";
import { createVirtualPlugin } from "../utils/plugin";

// cf.
// https://github.com/unocss/unocss/tree/47eafba27619ed26579df60fe3fdeb6122b5093c/packages/vite/src/modes/global
// https://github.com/tailwindlabs/tailwindcss/blob/719c0d488378002ff752e8dc7199c843930bb296/packages/%40tailwindcss-vite/src/index.ts

// TODO:
// - content hash not changing?
// - unocss transform plugin?
// - non global mode?
// - source map?

export function vitePluginSharedUnocss(): Plugin {
const ctx = getUnocssContext();

return {
name: vitePluginSharedUnocss.name,
sharedDuringBuild: true,
create(environment) {
const plugins: Plugin[] = [];

// [dev, build]
// extract tokens by intercepting transform
plugins.push({
name: vitePluginSharedUnocss.name + ":extract",
transform(code, id) {
if (ctx.filter(code, id)) {
ctx.extract(code, id);
}
},
});

// Following plugins are naturally applied to the environments
// which import "virtual:unocss.css".
// So, even though we only need to handle "client" environment case,
// such artificial restriction is not necessary.

// [dev]
if (environment.mode === "dev") {
// transform virtual module directly
plugins.push(
createVirtualPlugin("unocss.css", async () => {
await ctx.flushTasks();
const result = await ctx.uno.generate(ctx.tokens);
return result.css;
}),
);

// HMR
function hotUpdate() {
tinyassert(environment instanceof DevEnvironment);
const mod = invalidateModule(environment, "\0virtual:unocss.css");
if (mod) {
environment.hot.send({
type: "update",
updates: [
{
type: `${mod.type}-update`,
path: "/@id/__x00__virtual:unocss.css",
acceptedPath: "/@id/__x00__virtual:unocss.css",
timestamp: Date.now(),
},
],
});
}
}
const debounced = debounce(() => hotUpdate(), 50);
ctx.onInvalidate(debounced);
ctx.onReload(debounced);
}

// [build]
// transform virtual module during renderChunk
if (environment.mode === "build") {
const cssPlugins = environment.config.plugins.filter(
(p) => p.name === "vite:css" || p.name === "vite:css-post",
);

plugins.push(
createVirtualPlugin("unocss.css", () => "/*** tmp unocss ***/"),
{
name: vitePluginSharedUnocss.name + ":render",
async renderChunk(_code, chunk, _options) {
if (chunk.moduleIds.includes("\0virtual:unocss.css")) {
await ctx.flushTasks();
let { css } = await ctx.uno.generate(ctx.tokens);
// [feedback] environment in renderChunk context?
const pluginCtx = { ...this, environment };
for (const plugin of cssPlugins) {
tinyassert(typeof plugin.transform === "function");
const result = await plugin.transform.apply(
pluginCtx as any,
[css, "\0virtual:unocss.css"],
);
tinyassert(
objectHas(result, "code") &&
typeof result.code === "string",
);
css = result.code;
}
}
},
},
);
}

return plugins;
},
};
}

// grab internal unocss instance
function getUnocssContext() {
const plugins = vitePluginUnocss();
const plugin = plugins.find((p) => p.name === "unocss:api");
tinyassert(plugin);
return (plugin.api as UnocssVitePluginAPI).getContext();
}
7 changes: 5 additions & 2 deletions examples/react-server/src/features/utils/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ export function createVirtualPlugin(name: string, load: Plugin["load"]) {
return {
name: `virtual-${name}`,
resolveId(source, _importer, _options) {
return source === name ? "\0" + name : undefined;
if (source === name || source.startsWith(`${name}?`)) {
return `\0${source}`;
}
return;
},
load(id, options) {
if (id === "\0" + name) {
if (id === `\0${name}` || id.startsWith(`\0${name}?`)) {
return (load as any).apply(this, [id, options]);
}
},
Expand Down
3 changes: 3 additions & 0 deletions examples/react-server/src/routes/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export function ClientComponent() {
<div data-testid="client-component">
<h4>Hello Client Component</h4>
<SharedComponent message="client" />
<div className="flex justify-center w-36 m-1 p-1 bg-[rgb(255,220,220)]">
unocss (client)
</div>
<div data-hydrated={hydrated}>hydrated: {String(hydrated)}</div>
<div>Count: {count}</div>
<button className="client-btn" onClick={() => setCount((v) => v - 1)}>
Expand Down
3 changes: 3 additions & 0 deletions examples/react-server/src/routes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ async function Page() {
<div>
<h4>Hello Server Component</h4>
<SharedComponent message="server" />
<div className="flex justify-center w-36 m-1 p-1 bg-[rgb(220,220,255)]">
unocss (server)
</div>
<ServerActionDemo />
<ClientComponent />
</div>
Expand Down
5 changes: 5 additions & 0 deletions examples/react-server/uno.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineConfig, presetUno } from "unocss";

export default defineConfig({
presets: [presetUno()],
});
2 changes: 2 additions & 0 deletions examples/react-server/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "./src/features/bootstrap/plugin";
import { vitePluginServerCss } from "./src/features/style/plugin";
import { vitePluginTestReactServerStream } from "./src/features/test/plugin";
import { vitePluginSharedUnocss } from "./src/features/unocss/plugin";
import {
collectFiles,
createVirtualPlugin,
Expand All @@ -33,6 +34,7 @@ export default defineConfig((_env) => ({
plugins: [
!process.env["VITEST"] && react(),
vitePluginReactServer(),
vitePluginSharedUnocss(),
vitePluginLogger(),
vitePluginSsrMiddleware({
entry: process.env["SERVER_ENTRY"] ?? "/src/adapters/node",
Expand Down
Loading

0 comments on commit 334df37

Please sign in to comment.