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

Winidicss support as a plugin via context render function #1026

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 60 additions & 0 deletions plugins/windi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Plugin } from "$fresh/server.ts";
import { Config } from "windicss";
import {
generateWindicss,
getClassesFromHtml,
STYLE_ELEMENT_ID,
} from "./windi/shared.ts";

/**
* @see https://windicss.org/integrations/javascript.html
*
* Windi plugin for deno fresh.
* Takes windicss or tailwind {@link config} as an argument
* and generates css inside STYLE_ELEMENT_ID via fresh render function context.
*
* This file contains common SSR styles generation for static html routes.
* `windi/main.ts` contains client logic for islands.
* `windi/shared.ts` contains API functions
*
* @function windi
*
* @param config {@link Config}
* @return {@link Plugin}
*/
export default function windi(config: Config): Plugin {
// Not able to pass windi config's plugins functions without this
// All functions will be erased because of json parse
const main = `
data:application/javascript,
import hydrate from "${new URL("./windi/main.ts", import.meta.url).href}";
import config from "${config.selfURL}";
export default function(state) { hydrate(config, state); }
`;

return {
name: "windi",
entrypoints: { "main": main },
render(ctx) {
let scripts = [];
// Render html
const res = ctx.render();
const html = res.htmlText;
// Parse HTML get classes
const classes = getClassesFromHtml(html);
// Generate windicss
const windicss = generateWindicss(config, classes, html);

// Load script to handle dynamic styles in islands on the client side
if (res.requiresHydration) {
// Pass already generated classes
scripts.push({ entrypoint: "main", state: classes });
}

return {
scripts,
styles: [{ cssText: windicss, id: STYLE_ELEMENT_ID }],
};
},
};
}
45 changes: 45 additions & 0 deletions plugins/windi/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { options, VNode } from "preact";
import { Config } from "windicss";
import _ from "https://esm.sh/lodash";
import {
generateWindicss,
getClassesFromProps,
STYLE_ELEMENT_ID,
} from "./shared.ts";

/**
* @see https://preactjs.com/guide/v10/options
*
* This function uses preact hook options to get some control over virtual node's props.
* When classes change, function compares them with the already generated classes
* and decides if new classes should be generated and appended to the style tag
*
* @function hydrate
*
* @param config {@link Config}
* @param existingClasses {string[]}
*/
export default function hydrate(config: Config, existingClasses: string[]) {
const styleEl = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement;
const styles = styleEl.childNodes[0];

const originalHook = options.vnode;
options.vnode = (vnode: VNode<JSX.DOMAttributes<any>>) => {
const isProps = typeof vnode.props === "object";
const isType = typeof vnode.type === "string";

if (isType && isProps) {
const { props } = vnode;
const classes = getClassesFromProps(props);
const classesDiff = _.difference(classes, existingClasses);

if (classesDiff.length) {
existingClasses.push(...classesDiff);
const windicss = generateWindicss(config, classesDiff);
styles.appendData(windicss);
}
}

originalHook(vnode);
};
}
74 changes: 74 additions & 0 deletions plugins/windi/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ClassParser, HTMLParser } from "windicss/utils/parser";
import { Processor } from "windicss/lib";
import { Config } from "windicss";
import _ from "https://esm.sh/lodash";

/* Style element id to render windicss inside */
export const STYLE_ELEMENT_ID = "__FRSH_WINDICSS";

/**
* @see https://github.com/windicss/windicss/blob/main/src/utils/parser/html.ts
*
* Extracts classes from html string
*
* @function getClassesFromHtml
*
* @param html {string}
* @returns {string[]}
*/
export function getClassesFromHtml(html: string): string[] {
return _.flatten(
new HTMLParser(html).parseClasses().map((i) => i.result.split(" ")),
);
}

/**
* @see https://github.com/windicss/windicss/blob/main/src/utils/parser/class.ts
*
* Extracts classes from html Preact component props
*
* @function getClassesFromProps
*
* @param props {object}
* @returns {string[]}
*/
export function getClassesFromProps(props: { className?: string }): string[] {
if (props.className) {
return new ClassParser(props.className).parse().map((i) => i.content);
}

return [];
}

/**
* @see https://windicss.org/integrations/javascript.html
*
* @function generateWindicss
*
* @param config {@link Config}
* @param classes {string[]}
* @param [html] {string}
* @returns {string}
*/
export function generateWindicss(
config: Config,
classes: string[],
html?: string,
): string {
// Get windi processor
const processor = new Processor(config);
// Process the HTML classes to an interpreted style sheet
const interpretedSheet = processor.interpret(classes.join(" ")).styleSheet;

// Build styles
const APPEND = false;
const MINIFY = false;

if (html) {
// Generate preflight based on the HTML we input
const preflightSheet = processor.preflight(html);
interpretedSheet.extend(preflightSheet, APPEND);
}

return interpretedSheet.build(MINIFY);
}