Skip to content
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

feat(plugins): Add unocss plugin (WIP) #1966

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ce00199
feat(plugins): add unocss plugin (WIP)
miguelrk Jun 15, 2023
f75ece6
fix(plugins/unocss): use presetUno instead of presetWind by default
miguelrk Jul 25, 2023
8d33431
chore(plugins): add comment to inline reset for unocss
miguelrk Aug 1, 2023
8a6736e
chore(plugins): update comment of inline reset for unocss
miguelrk Aug 1, 2023
5fd6ffe
docs(plugins): add docs for unocss
miguelrk Aug 8, 2023
6321ba5
fix(plugins): remove unnecessary `?bundle&no-check` from `@unocss/pre…
miguelrk Aug 8, 2023
c09a4a1
docs(plugins): remove redundant text for unocss
miguelrk Aug 8, 2023
95fa44f
fix(plugins): remove defaults from unocss plugin
miguelrk Aug 8, 2023
0767888
chore(plugins): add `fixture_unocss_hydrate` test
miguelrk Aug 9, 2023
f223bc2
chore(plugins): bump `@unocss/reset` version
miguelrk Aug 9, 2023
488de1e
chore(plugins): bump `@unocss/core` version
miguelrk Aug 9, 2023
b5e49a2
chore(plugins): bump `@unocss/core` and `@unocss/preset-uno` version
miguelrk Aug 9, 2023
de9b22b
chore(plugins): format via `deno fmt`
miguelrk Aug 9, 2023
55e7906
feat(plugins): add `antfu.unocss` to recommended vscode extensions
miguelrk Aug 9, 2023
e660f1b
chore(ci): try fixing broken types causing CI to fail
miguelrk Aug 9, 2023
f6f204b
chore(plugins): order imports
miguelrk Aug 9, 2023
7dd2bfa
chore(plugins): use import map instead of url imports for unocss
miguelrk Aug 9, 2023
b0a1bcf
feat(plugins): add client runtime script to unocss plugin by default
miguelrk Aug 10, 2023
ae545fc
chore(plugins): unify imports form `@unocss/core`
miguelrk Aug 10, 2023
1c91354
Re-export UserConfig type from UnoCSS plugin
adamgreg Aug 17, 2023
25df128
Import @unocss/preset-uno from full URL in test
adamgreg Aug 17, 2023
d301e1b
Update UnoCSS in plugin to 0.55.1
adamgreg Aug 17, 2023
af7175d
In the UnoCSS plugin pass the config to runtime
adamgreg Aug 20, 2023
5adfdcd
chore(plugins): format via `deno fmt`
miguelrk Aug 20, 2023
9a52a2b
Update plugins/unocss.ts
miguelrk Aug 23, 2023
3364741
chore(plugins): update querySelector to style[data-unocss-runtime-lay…
miguelrk Aug 23, 2023
bd03d64
Update docs/latest/examples/using-unocss.md
miguelrk Aug 23, 2023
3bc0bd8
UnoCSS plugin use uno.config.ts
adamgreg Sep 13, 2023
2392657
UnoCSS plugin: Add defineConfig() helper function
adamgreg Sep 13, 2023
33eb302
Make UnoCSS plugin function synchronous
adamgreg Sep 13, 2023
70677a0
UnoCSS Plugin: Use Preact hook for SSR
adamgreg Oct 4, 2023
3a70496
UnoCSS plugin: Update the inline reset
adamgreg Oct 9, 2023
7ce943c
Support AOT build in UnoCSS plugin
adamgreg Oct 20, 2023
597f3b1
docs: Update UnoCSS plugin docs
adamgreg Nov 7, 2023
0b4e8cc
UnoCSS plugin: fix CSS link format in return of renderAsync()
adamgreg Dec 3, 2023
4716624
UnoCSS plugin: Fix typo in stylesheet link
adamgreg Dec 19, 2023
5d845c4
UnoCSS plugin updates
adamgreg Dec 20, 2023
e3bcce7
UnoCSS plugin: Check config file exists for CSR mode
adamgreg Dec 20, 2023
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
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"recommendations": [
"denoland.vscode-deno"
"denoland.vscode-deno",
"antfu.unocss"
]
}
39 changes: 39 additions & 0 deletions docs/canary/examples/using-unocss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
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,
update the `main.ts` as follows:

```ts
/// <reference no-default-lib="true" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />

import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";

import unocssPlugin from "$fresh/plugins/unocss.ts";

await start(manifest, { plugins: [unocssPlugin()] });
```

Your project folder should contain a `uno.config.ts` file, which can be
customized to your liking. Refer to the
[unocss docs](https://unocss.dev/guide/config-file) for more information.

The behaviour of the plugin may be customised by passing it an argument. The
argument is an options object, which may contain any of the following options:

- `aot` (`boolean`): Enable AOT mode - run UnoCSS to extract styles during the
build task. Default: `true`.
- `ssr` (`boolean`): Enable SSR mode - run UnoCSS live to extract styles during
server renders. Default: `false`.
- `csr` (`boolean`): Enable CSR mode - run the UnoCSS runtime on the client. It
will generate styles live in response to DOM events. Default: `false`.
- `config`: An inline UnoCSS `UserConfig` object, as an alternative to a
`uno.config.ts` file. Note that this is not compatible with the `csr` option,
as the config object can not be bundled and sent to the client.
2 changes: 1 addition & 1 deletion docs/latest/examples/using-twind-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,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).
1 change: 1 addition & 0 deletions docs/toc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const toc: RawTableOfContents = {
"link:latest",
],
["using-twind-v1", "Using Twind v1", "link:latest"],
["using-unocss", "Using UnoCSS", "link:canary"],
["init-the-server", "Initializing the server", "link:latest"],
[
"using-fresh-canary-version",
Expand Down
254 changes: 254 additions & 0 deletions plugins/unocss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { JSX, options as preactOptions, VNode } from "preact";

import {
UnoGenerator,
type UserConfig,
} from "https://esm.sh/@unocss/core@0.56.5";
import type { Theme } from "https://esm.sh/@unocss/preset-uno@0.56.5";

import { Plugin } from "$fresh/server.ts";
import {
dirname,
exists,
fromFileUrl,
join,
walk,
} from "$fresh/src/server/deps.ts";

type PreactOptions = typeof preactOptions & { __b?: (vnode: VNode) => void };

// Regular expression to support @unocss-skip-start/end comments in source code
const SKIP_START_COMMENT = "@unocss-skip-start";
const SKIP_END_COMMENT = "@unocss-skip-end";
const SKIP_COMMENT_RE = new RegExp(
`(//\\s*?${SKIP_START_COMMENT}\\s*?|\\/\\*\\s*?${SKIP_START_COMMENT}\\s*?\\*\\/|<!--\\s*?${SKIP_START_COMMENT}\\s*?-->)[\\s\\S]*?(//\\s*?${SKIP_END_COMMENT}\\s*?|\\/\\*\\s*?${SKIP_END_COMMENT}\\s*?\\*\\/|<!--\\s*?${SKIP_END_COMMENT}\\s*?-->)`,
"g",
);

// Inline reset from https://esm.sh/@unocss/reset@0.56.5/tailwind-compat.css
const unoResetCSS = `/* reset */
a,hr{color:inherit}progress,sub,sup{vertical-align:baseline}blockquote,body,dd,dl,fieldset,figure,h1,h2,h3,h4,h5,h6,hr,menu,ol,p,pre,ul{margin:0}fieldset,legend,menu,ol,ul{padding:0}*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:var(--un-default-border-color,#e5e7eb)}html{line-height:1.5;-webkit-text-size-adjust:100%;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"}body{line-height:inherit}hr{height:0;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}small{font-size:80%}sub,sup{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-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}::-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}menu,ol,ul{list-style:none}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}
`;

type UnoCssPluginOptions = {
/**
* Explicit UnoCSS config object, alternative to `uno.config.ts` file.
* Not supported for the client runtime in CSR mode.
*/
config?: UserConfig;
/**
* Enable AOT mode - run UnoCSS to extract styles during the build task.
* Enabled by default.
*/
aot?: boolean;
/**
* Enable SSR mode - run UnoCSS live to extract styles during server renders.
* Disabled by default.
*/
ssr?: boolean;
/**
* Enable CSR mode - run the UnoCSS runtime on the client.
* It will generate styles live in response to DOM events.
* Disabled by default.
*/
csr?: boolean;
};

/**
* Helper function for typing of config objects
*/
export function defineConfig<T extends object = Theme>(config: UserConfig<T>) {
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<string>) {
// 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<JSX.DOMAttributes<any>>,
) => {
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);
};
}

/**
* Runs UnoCSS over the source code of the project
* @param uno The UnoCSS generator object
* @returns The generated CSS content
*/
async function runOverSource(uno: UnoGenerator): Promise<string> {
// Find source files
const projectDir = fromFileUrl(dirname(Deno.mainModule));
const sourceFiles = [];
for await (
const entry of walk(projectDir, {
includeDirs: false,
exts: [".tsx", ".ts", ".jsx", ".html"],
})
) {
sourceFiles.push(entry.path);
}

// Read the source code into a single string
const sourceCode = await Promise.all(
sourceFiles.map((filename) => Deno.readTextFile(filename)),
).then((x) => x.join("\n").replace(SKIP_COMMENT_RE, ""));

// Extract UnoCSS classes from the source code and generate CSS
const { css } = await uno.generate(sourceCode);

// Include the inline reset CSS
return unoResetCSS + css;
}

/** UnoCSS plugin - automatically generates CSS utility classes */
export default function unocss(
{ config, aot = true, ssr = false, csr = false }: UnoCssPluginOptions = {},
): Plugin {
// 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);

// Link to CSS file, if AOT mode is enabled
const links = aot ? [{ rel: "stylesheet", href: "/uno.css" }] : [];

// Add entrypoint, if CSR mode is enabled
const scripts = csr ? [{ entrypoint: "main", state: {} }] : [];

// In CSR-only mode, include the style resets using an inline style tag
const styles = csr && !aot && !ssr ? [{ cssText: unoResetCSS }] : [];

const middlewares: Plugin["middlewares"] = [];

// Created in configResolved()
let uno: UnoGenerator;

// Create a set that may be used to hold class names encountered during SSR
const ssrClasses = new Set<string>();

return {
name: "unocss",
middlewares,

// Optional client runtime
entrypoints: csr
? {
"main": `
data:application/javascript,
import config from "${configURL}";
import init from "https://esm.sh/@unocss/runtime@0.56.5";
export default function() {
window.__unocss = config;
init();
}`,
}
: {},

async configResolved(freshConfig) {
// Load config from file if required
const configFileExists = await exists(configURL, {
isFile: true,
isReadable: true,
});
if (config === undefined) {
try {
config = (await import(configURL.toString())).default;
} catch (error) {
if (configFileExists) {
throw error;
} else {
throw new Error(
"uno.config.ts not found in the project directory! Please create it or pass a config object to the UnoCSS plugin",
);
}
}
}

if (csr && !configFileExists) {
throw new Error(
"uno.config.ts not found in the project directory! Required for CSR mode.",
);
}

// Create the generator object
uno = new UnoGenerator(config);

if (ssr) {
// Hook into Preact to add to the set of classes during the render
installPreactHook(ssrClasses);
} else if (aot && freshConfig.dev) {
// Extract classes from source code now, and generate CSS
console.log(
"%cGenerating UnoCSS stylesheet...",
"color: blue; font-weight: bold",
);
const css = await runOverSource(uno);

// Craft a response for requests for the generated CSS file
const resp = new Response(css, {
headers: {
"Content-Type": "text/css",
"Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
},
});

// Add a middleware to handle requests for the generated CSS file
middlewares.push({
path: "/",
middleware: {
handler: (_req, ctx) =>
ctx.url.pathname === "/uno.css" ? resp : ctx.next(),
},
});
}
},

async renderAsync(ctx) {
// Generate inline styles, if SSR mode is enabled
if (ssr) {
// Clear any classes extracted during previous renders
ssrClasses.clear();

// Render. Preact will populate the list of classes.
await ctx.renderAsync();

// Run UnoCSS over the classes to generate CSS
const { css } = await uno.generate(ssrClasses);

// Include SSR CSS, and possibly CSR script
return { scripts, styles: [{ cssText: `${unoResetCSS}\n${css}` }] };
} else {
// Include link to AOT-generated CSS file and/or CSR script
await ctx.renderAsync();
return { scripts, links, styles };
}
},

async buildStart({ build: { outDir } }) {
// Generate a static CSS file, if AOT mode is enabled
if (aot) {
// Extract classes from source code and generate CSS
const css = await runOverSource(uno);

// Write the generated CSS to a static file
await Deno.writeTextFile(join(outDir, "static", "uno.css"), css);
}
},
};
}
1 change: 1 addition & 0 deletions src/server/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
toFileUrl,
} from "https://deno.land/std@0.208.0/path/mod.ts";
export { walk } from "https://deno.land/std@0.208.0/fs/walk.ts";
export { exists } from "https://deno.land/std@0.208.0/fs/exists.ts";
export * as colors from "https://deno.land/std@0.208.0/fmt/colors.ts";
export {
type Handler as ServeHandler,
Expand Down
2 changes: 1 addition & 1 deletion tests/fixture_twind_hydrate/islands/InsertCssrules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
</button>
</div>
);
Expand Down
18 changes: 18 additions & 0 deletions tests/fixture_unocss_hydrate/deno.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
5 changes: 5 additions & 0 deletions tests/fixture_unocss_hydrate/dev.ts
Original file line number Diff line number Diff line change
@@ -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");
26 changes: 26 additions & 0 deletions tests/fixture_unocss_hydrate/fresh.gen.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading