0.0.1Updated a month ago
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;