import * as tailwind from "npm:tailwindcss";
import { join } from "https://deno.land/std@0.224.0/path/mod.ts";
import { walkSync } from "https://deno.land/std@0.224.0/fs/mod.ts";
const cssFile = Deno.readTextFileSync("./app/tailwind/style.css");
const KV = await Deno.openKv();
const TAILWIND_LIBRARIES = [
{
name: "tailwindcss",
path: "tailwindcss@4.1.11/index.css",
content: null
},
{
name: "tailwindcss/preflight",
path: "tailwindcss@4.1.11/preflight.css",
content: null
}
] as { name: string, path: string, content: string | null }[];
for(const library of TAILWIND_LIBRARIES) {
library.content = (await KV.get<string>(['tailwind', library.name])).value || null;
if(!library.content) {
library.content = await (await fetch(`https://unpkg.com/${library.path}`)).text();
await KV.set(['tailwind', library.name], library.content);
}
}
export class Tailwind {
private static pluginClassMap = new Map<string, Set<string>>();
private static globalClassSet = new Set<string>();
static async handler() {
return new Response(await this.Stylesheet(), {
headers: {
"Content-Type": "text/css"
}
})
}
static AddClass(className: string) {
if(!this.globalClassSet.has(className.toLowerCase())) this.globalClassSet.add(className.toLowerCase());
}
static TrackPlugin(pluginId: string, html: string): boolean {
const regex = /class(?:Name)?=["'`]([^"']+)["'`]/g;
let match;
const pluginSet = this.pluginClassMap.get(pluginId) ?? new Set<string>();
let changed = false;
while ((match = regex.exec(html)) !== null) {
const classes = match[1].split(/\s+/);
for (const cls of classes) {
if (!pluginSet.has(cls)) {
pluginSet.add(cls);
this.globalClassSet.add(cls);
changed = true;
}
}
}
this.pluginClassMap.set(pluginId, pluginSet);
if(changed) this.__cached_stylesheet = null;
return changed;
}
static Flush(pluginId: string) {
const hadPlugin = this.pluginClassMap.delete(pluginId);
if (!hadPlugin) return false;
this.ForceRebuild();
return true;
}
private static __cached_stylesheet: string | null = null;
static async Stylesheet() {
if (this.__cached_stylesheet) return this.__cached_stylesheet;
await this.GenerateStylesheet();
return this.__cached_stylesheet as string;
}
private static async GenerateStylesheet() {
this.__cached_stylesheet = (await tailwind.compile(cssFile, {
// deno-lint-ignore require-await
async loadStylesheet(id: string, base: string) {
return {
path: id,
base,
content: TAILWIND_LIBRARIES.find(l => l.name == id)?.content || "",
};
},
async loadModule(id, base, _resourceHint) {
if(id == 'daisyui/theme') id += '/index.js';
const mod = await import(id);
return {
path: id,
base,
module: mod.default ?? mod,
};
},
})).build(Array.from(this.globalClassSet));
}
static ForceRebuild() {
this.__cached_stylesheet = null;
this.globalClassSet.clear();
for (const set of this.pluginClassMap.values()) {
for (const cls of set) this.globalClassSet.add(cls);
}
this.GenerateStylesheet();
}
static init() {
for(const file of walkSync(join(Deno.cwd(), 'app'), {
includeFiles: true
})) {
if(!file.path.match(/tsx?$/)) continue;
const contents = Deno.readTextFileSync(file.path);
let match;
while((match = class_regex.exec(contents)) !== null) {
for (const cls of match[0].split(/\s+/)) {
Tailwind.AddClass(cls);
}
}
}
}
}
const class_regex = /[\w\-:.\/\[#%\]\(\)\!]+(?<!:)/g;