diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 971c0ed5612..fa0458f85b0 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,6 +1,7 @@
{
"recommendations": [
"denoland.vscode-deno",
- "sastan.twind-intellisense"
+ "sastan.twind-intellisense",
+ "antfu.unocss"
]
}
diff --git a/docs/latest/examples/using-twind-v1.md b/docs/latest/examples/using-twind-v1.md
index 0577bead8f6..9bbf8015cbd 100644
--- a/docs/latest/examples/using-twind-v1.md
+++ b/docs/latest/examples/using-twind-v1.md
@@ -58,7 +58,7 @@ export default {
};
```
-(Note: the `as Preset` cast is required to fix a typing issue with twind.)
+Note: the `as Preset` cast is required to fix a typing issue with twind.
To see what other presets exist, you can go to the
[twind docs](https://twind.style/presets).
diff --git a/docs/latest/examples/using-unocss.md b/docs/latest/examples/using-unocss.md
new file mode 100644
index 00000000000..29dc1267168
--- /dev/null
+++ b/docs/latest/examples/using-unocss.md
@@ -0,0 +1,44 @@
+---
+description: |
+ One can use UnoCSS, an instant on-demand atomic CSS engine
+---
+
+The template generates a Twind v0 project by default. If you want to use UnoCSS
+you can update the `main.ts` as follows:
+
+```ts
+///
+///
+///
+///
+///
+
+import { start } from "$fresh/server.ts";
+import manifest from "./fresh.gen.ts";
+
+import unocssPlugin from "$fresh/plugins/unocss.ts";
+import unocssConfig from "./uno.config.ts";
+
+await start(manifest, { plugins: [unocssPlugin(unocssConfig)] });
+```
+
+The unocss config object at `uno.config.ts` can be customized to your liking.
+Refer to the [unocss docs](https://unocss.dev/guide/config-file) for more
+information. If no config is provided, the default config is used, which
+defaults to the following:
+
+```ts
+import { defineConfig } from "$fresh/plugins/unocss.ts";
+import presetUno from "https://esm.sh/@unocss/preset-uno@0.55.1";
+
+export default defineConfig({
+ presets: [presetUno()],
+ selfURL: import.meta.url,
+});
+```
+
+Note: you could also inline the config object in `main.ts` instead of using a
+separate `uno.config.ts` file.
+
+To see what other presets exist, you can go to the
+[unocss docs](https://unocss.dev/presets/).
diff --git a/docs/toc.ts b/docs/toc.ts
index 65090aa5055..54dc313d5c3 100644
--- a/docs/toc.ts
+++ b/docs/toc.ts
@@ -79,6 +79,7 @@ const toc: RawTableOfContents = {
"link:latest",
],
["using-twind-v1", "Using Twind v1", "link:latest"],
+ ["using-unocss", "Using UnoCSS", "link:latest"],
["init-the-server", "Initializing the server", "link:latest"],
[
"using-fresh-canary-version",
@@ -152,6 +153,7 @@ const toc: RawTableOfContents = {
["writing-tests", "Writing tests"],
["changing-the-src-dir", "Changing the source directory"],
["using-twind-v1", "Using Twind v1"],
+ ["using-unocss", "Using UnoCSS"],
["init-the-server", "Initializing the server"],
["using-fresh-canary-version", "Using Fresh canary version"],
["dealing-with-cors", "Dealing with CORS"],
diff --git a/plugins/unocss.ts b/plugins/unocss.ts
new file mode 100644
index 00000000000..46a3c88e899
--- /dev/null
+++ b/plugins/unocss.ts
@@ -0,0 +1,122 @@
+import { JSX, options as preactOptions, VNode } from "preact";
+
+import {
+ UnoGenerator,
+ type UserConfig,
+} from "https://esm.sh/@unocss/core@0.55.1";
+import type { Theme } from "https://esm.sh/@unocss/preset-uno@0.55.1";
+
+import { Plugin } from "$fresh/server.ts";
+import { exists } from "$fresh/src/server/deps.ts";
+
+type PreactOptions = typeof preactOptions & { __b?: (vnode: VNode) => void };
+
+// inline reset from https://esm.sh/@unocss/reset@0.54.2/tailwind.css
+const unoResetCSS = `/* reset */
+*,:before,:after{box-sizing:border-box;border:0 solid}html{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}
+`;
+
+type UnoCssPluginOptions = {
+ runtime?: boolean;
+ config?: UserConfig;
+};
+
+/**
+ * Helper function for typing of config objects
+ */
+export function defineConfig(config: UserConfig) {
+ return config;
+}
+
+/**
+ * Installs a hook in Preact to extract classes during server-side renders
+ * @param classes - Set of class strings, which will be mutated by this function.
+ */
+export function installPreactHook(classes: Set) {
+ // Hook into options._b which is called whenever a new comparison
+ // starts in Preact.
+ const originalHook = (preactOptions as PreactOptions).__b;
+ (preactOptions as PreactOptions).__b = (
+ // deno-lint-ignore no-explicit-any
+ vnode: VNode>,
+ ) => {
+ if (typeof vnode.type === "string" && typeof vnode.props === "object") {
+ const { props } = vnode;
+ if (props.class) {
+ props.class.split(" ").forEach((cls) => classes.add(cls));
+ }
+ if (props.className) {
+ props.className.split(" ").forEach((cls) => classes.add(cls));
+ }
+ }
+
+ originalHook?.(vnode);
+ };
+}
+
+/**
+ * UnoCSS plugin - automatically generates CSS utility classes
+ *
+ * @param [opts] Plugin options
+ * @param [opts.runtime] By default the UnoCSS runtime will run in the browser. Set to `false` to disable this.
+ * @param [opts.config] Explicit UnoCSS config object. By default `uno.config.ts` file. Not supported with the browser runtime.
+ */
+export default function unocss(
+ opts: UnoCssPluginOptions = {},
+): Plugin {
+ // Include the browser runtime by default
+ const runtime = opts.runtime ?? true;
+
+ // A uno.config.ts file is required in the project directory if a config object is not provided,
+ // or to use the browser runtime
+ const configURL = new URL("./uno.config.ts", Deno.mainModule);
+
+ // Create a set that will be used to hold class names encountered during SSR
+ const classes = new Set();
+
+ // Hook into Preact to add to the set of classes on each server-side render
+ installPreactHook(classes);
+
+ let uno: UnoGenerator;
+ if (opts.config !== undefined) {
+ uno = new UnoGenerator(opts.config);
+ } else {
+ import(configURL.toString()).then((mod) => {
+ uno = new UnoGenerator(mod.default);
+ }).catch((error) => {
+ exists(configURL, { isFile: true, isReadable: true }).then(
+ (configFileExists) => {
+ throw configFileExists ? error : new Error(
+ "uno.config.ts not found in the project directory! Please create it or pass a config object to the UnoCSS plugin",
+ );
+ },
+ );
+ });
+ }
+
+ return {
+ name: "unocss",
+ entrypoints: runtime
+ ? {
+ "main": `
+ data:application/javascript,
+ import config from "${configURL}";
+ import init from "https://esm.sh/@unocss/runtime@0.55.1";
+ export default function() {
+ window.__unocss = config;
+ init();
+ }`,
+ }
+ : {},
+ async renderAsync(ctx) {
+ classes.clear();
+ await ctx.renderAsync();
+ const { css } = await uno.generate(classes);
+
+ return {
+ scripts: runtime ? [{ entrypoint: "main", state: {} }] : [],
+ styles: [{ cssText: `${unoResetCSS}\n${css}` }],
+ };
+ },
+ };
+}
diff --git a/src/server/deps.ts b/src/server/deps.ts
index 23e66404377..41a9a0a99f8 100644
--- a/src/server/deps.ts
+++ b/src/server/deps.ts
@@ -7,6 +7,7 @@ export {
toFileUrl,
} from "https://deno.land/std@0.193.0/path/mod.ts";
export { walk } from "https://deno.land/std@0.193.0/fs/walk.ts";
+export { exists } from "https://deno.land/std@0.193.0/fs/exists.ts";
export * as colors from "https://deno.land/std@0.193.0/fmt/colors.ts";
export {
type Handler as ServeHandler,
diff --git a/tests/fixture_twind_hydrate/islands/InsertCssrules.tsx b/tests/fixture_twind_hydrate/islands/InsertCssrules.tsx
index beea977b0c5..845969adbe9 100644
--- a/tests/fixture_twind_hydrate/islands/InsertCssrules.tsx
+++ b/tests/fixture_twind_hydrate/islands/InsertCssrules.tsx
@@ -69,7 +69,7 @@ export default function InsertCssrules() {
}}
disabled={insertedStyles.value === "" ? false : true}
>
- Add `text-green-600` to Cureent Number Class
+ Add `text-green-600` to Current Number Class
);
diff --git a/tests/fixture_unocss_hydrate/deno.json b/tests/fixture_unocss_hydrate/deno.json
new file mode 100644
index 00000000000..226bf74767b
--- /dev/null
+++ b/tests/fixture_unocss_hydrate/deno.json
@@ -0,0 +1,18 @@
+{
+ "lock": false,
+ "tasks": {
+ "start": "deno run -A --watch=static/,routes/ dev.ts"
+ },
+ "imports": {
+ "$fresh/": "../../",
+ "preact": "https://esm.sh/preact@10.11.0",
+ "preact/": "https://esm.sh/preact@10.11.0/",
+ "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0",
+ "@preact/signals": "https://esm.sh/*@preact/signals@1.0.3",
+ "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.0.1"
+ },
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact"
+ }
+}
diff --git a/tests/fixture_unocss_hydrate/dev.ts b/tests/fixture_unocss_hydrate/dev.ts
new file mode 100755
index 00000000000..2d85d6c183c
--- /dev/null
+++ b/tests/fixture_unocss_hydrate/dev.ts
@@ -0,0 +1,5 @@
+#!/usr/bin/env -S deno run -A --watch=static/,routes/
+
+import dev from "$fresh/dev.ts";
+
+await dev(import.meta.url, "./main.ts");
diff --git a/tests/fixture_unocss_hydrate/fresh.gen.ts b/tests/fixture_unocss_hydrate/fresh.gen.ts
new file mode 100644
index 00000000000..041757d712d
--- /dev/null
+++ b/tests/fixture_unocss_hydrate/fresh.gen.ts
@@ -0,0 +1,26 @@
+// DO NOT EDIT. This file is generated by Fresh.
+// This file SHOULD be checked into source version control.
+// This file is automatically updated during development when running `dev.ts`.
+
+import * as $0 from "./routes/check-duplication.tsx";
+import * as $1 from "./routes/insert-cssrules.tsx";
+import * as $2 from "./routes/static.tsx";
+import * as $3 from "./routes/unused.tsx";
+import * as $$0 from "./islands/CheckDuplication.tsx";
+import * as $$1 from "./islands/InsertCssrules.tsx";
+
+const manifest = {
+ routes: {
+ "./routes/check-duplication.tsx": $0,
+ "./routes/insert-cssrules.tsx": $1,
+ "./routes/static.tsx": $2,
+ "./routes/unused.tsx": $3,
+ },
+ islands: {
+ "./islands/CheckDuplication.tsx": $$0,
+ "./islands/InsertCssrules.tsx": $$1,
+ },
+ baseUrl: import.meta.url,
+};
+
+export default manifest;
diff --git a/tests/fixture_unocss_hydrate/islands/CheckDuplication.tsx b/tests/fixture_unocss_hydrate/islands/CheckDuplication.tsx
new file mode 100644
index 00000000000..419e56a5813
--- /dev/null
+++ b/tests/fixture_unocss_hydrate/islands/CheckDuplication.tsx
@@ -0,0 +1,59 @@
+// https://github.com/denoland/fresh/pull/1050
+import { useEffect } from "preact/hooks";
+import { cmpCssRules } from "../utils/utils.ts";
+import { useSignal } from "@preact/signals";
+
+/**
+ * Returns a cssrulelist of styleElement matching the selector.
+ */
+function getCssrules(selector: string) {
+ const elem = document.querySelector(selector) as HTMLStyleElement;
+ return elem?.sheet?.cssRules;
+}
+
+export default function CheckDuplication() {
+ const cssRulesFRSHUNOCSS = useSignal(undefined);
+ const cssRulesClaimed = useSignal(undefined);
+
+ // Init
+ useEffect(() => {
+ // get