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(unocss): support css transformers #516

Merged
merged 10 commits into from
Nov 27, 2023
Merged
12 changes: 8 additions & 4 deletions deps/unocss.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export { default } from "npm:@unocss/[email protected]";
export { presetUno } from "npm:@unocss/[email protected]";

export type { UserConfig } from "npm:@unocss/[email protected]";
export {
createGenerator,
type PluginOptions,
type UnocssPluginContext,
type UserConfig,
} from "npm:@unocss/[email protected]";
export { presetUno } from "npm:@unocss/[email protected]";
export const resetUrl = "https://unpkg.com/@unocss/[email protected]";
3 changes: 0 additions & 3 deletions init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,6 @@ function initPlugins(plugins: string[], denoConfig: DenoConfigResult) {
// Ensure that tailwindcss is loaded before postcss
fixPluginOrder(plugins, "tailwindcss", "postcss");

// Ensure that unocss is loaded before postcss
fixPluginOrder(plugins, "unocss", "postcss");

// Ensure that picture is loaded before imagick
fixPluginOrder(plugins, "picture", "imagick");
}
Expand Down
149 changes: 105 additions & 44 deletions plugins/unocss.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,133 @@
import unocss, { presetUno } from "../deps/unocss.ts";
import { getExtension } from "../core/utils/path.ts";
import { merge } from "../core/utils/object.ts";
import { read } from "../core/utils/read.ts";
import { createGenerator, presetUno, resetUrl } from "../deps/unocss.ts";

import type { UserConfig } from "../deps/unocss.ts";
import type Site from "../core/site.ts";
import type {
PluginOptions,
UnocssPluginContext,
UserConfig,
} from "../deps/unocss.ts";

export interface Options {
/** Extensions processed by this plugin to extract the utility classes */
extensions?: string[];

/**
* Options passed to UnoCSS.
* @see https://unocss.dev/guide/config-file
* Configurations for UnoCSS.
* @see {@link https://unocss.dev/guide/config-file}
*/
config?: UserConfig;
/**
* Set the css filename for all generated styles,
* Set to `false` to insert a <style> tag per page.
* @defaultValue `false`
*/
cssFile?: false | string;
/**
* Process CSS files using UnoCSS transformers.
* @defaultValue `undefined`
*/
cssFileTransformers?: PluginOptions["transformers"];
/**
* Supported CSS reset options.
* @see {@link https://github.com/unocss/unocss/tree/main/packages/reset}
* @defaultValue `tailwind`
*/
options?: Omit<UserConfig, "content">;
reset?: false | "tailwind" | "tailwind-compat" | "eric-meyer";
}

export const defaults: Options = {
extensions: [".html"],
options: {
config: {
presets: [presetUno()],
},
cssFile: false,
reset: "tailwind",
};

export default function (userOptions?: Options) {
const options = merge(defaults, userOptions);

return (site: Site) => {
// deno-lint-ignore no-explicit-any
let unoPlugins: any[];
const uno = createGenerator(options.config);

if (site.hooks.postcss) {
throw new Error(
"PostCSS plugin is required to be installed AFTER UnoCSS plugin",
);
if (Array.isArray(options.cssFileTransformers)) {
site.process([".css"], async (files) => {
const { default: MagicString } = await import(
"npm:[email protected]"
kwaa marked this conversation as resolved.
Show resolved Hide resolved
);
for await (const file of files) {
if (file.content) {
const code = new MagicString(file.content.toString());
for await (const { transform } of options.cssFileTransformers!) {
await transform(
code,
file.src.path,
{ uno } as unknown as UnocssPluginContext,
);
}
file.content = code.toString();
}
}
});
}

site.process(options.extensions, (pages) => {
// Get the content of all HTML pages (sorted by path)
const content = pages.sort((a, b) => a.src.path.localeCompare(b.src.path))
.map((page) => ({
raw: page.content as string,
extension: getExtension(page.outputPath).substring(1),
}));
if (options.cssFile === false) {
// Insert a <style> tag for each page
site.process([".html"], async (pages) => {
const reset = await getResetCss(options);

Promise.all(pages.map(async (page) => {
const document = page.document!;
const result = await uno.generate(
document.documentElement?.innerHTML ?? "",
);
const css = reset ? `${reset}\n${result.css}` : result.css;

// Create UnoCSS plugin
// @ts-ignore: This expression is not callable.
const plugin = unocss({
configOrPath: options.options,
content,
if (css) {
const style = document.createElement("style");
style.innerText = css;
page.document?.head?.appendChild(style);
}
}));
});
return;
}

// Ensure PostCSS plugin is installed
if (!site.hooks.postcss) {
throw new Error(
"PostCSS plugin is required to be installed AFTER UnoCSS plugin",
);
}
// Generate the stylesheets for all pages
site.process([".html"], async (pages) => {
const classes = new Set<string>();

// Replace the old UnoCSS plugin configuration from PostCSS plugins
// deno-lint-ignore no-explicit-any
site.hooks.postcss((runner: any) => {
unoPlugins?.forEach((plugin) => {
runner.plugins.splice(runner.plugins.indexOf(plugin), 1);
});
unoPlugins = runner.normalize([plugin]);
runner.plugins = runner.plugins.concat(unoPlugins);
});
await Promise.all(
pages.map(async (page) =>
await uno.generate(
page.document?.documentElement?.innerHTML ?? "",
)
.then((res) => res.matched)
.then((matched) => matched.forEach((match) => classes.add(match)))
),
);

// Create & merge stylesheets for all pages
const reset = await getResetCss(options);
const result = await uno.generate(classes);
const css = reset ? `${reset}\n${result.css}` : result.css;

// Output the CSS file
const output = await site.getOrCreatePage(options.cssFile as string);
if (output.content) {
output.content += `\n${css}`;
} else {
output.content = css;
}
});
};
}

/**
* TODO: Replace with CSS Modules Import
* @remarks Deno does not currently support CSS Modules.
* @see {@link https://github.com/denoland/deno/issues/11961}
*/
async function getResetCss(options: Options) {
return options.reset === false
? ""
: await read(`${resetUrl}/${options.reset}.css`, false);
}
Loading