mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix: keep plugin css below core utilities and version plugin assets by their css
This commit is contained in:
1 parent
80944301dd
commit
1a6f9ffad6
5 files changed
+92
-21
No files matched your search
@@ -112,6 +112,19 @@ const THEME_CSS = path.join(
|
||||
* otherwise have no CSS at all. Appended to whatever CSS the bundle already
|
||||
* produced (a library's stylesheet the plugin imports).
|
||||
*/
|
||||
/**
|
||||
* Moves a plugin's Tailwind layers under their own names. Core orders
|
||||
* termix-plugin-utilities above its base styles and below its own utilities,
|
||||
* so a plugin's copy of `.hidden` can never beat core's `md:flex` just
|
||||
* because the plugin's file loaded later.
|
||||
*/
|
||||
export function lowerPluginLayers(css) {
|
||||
return css.replace(
|
||||
/@layer (properties|utilities)(?=[\s{;,])/g,
|
||||
"@layer termix-plugin-$1",
|
||||
);
|
||||
}
|
||||
|
||||
export async function buildTailwind(cwd, outDir) {
|
||||
const { compile, optimize } = await import("@tailwindcss/node");
|
||||
const { Scanner } = await import("@tailwindcss/oxide");
|
||||
@@ -138,9 +151,9 @@ export async function buildTailwind(cwd, outDir) {
|
||||
});
|
||||
// Flattened and minified the way core's own CSS is, so it does not rely
|
||||
// on native CSS nesting.
|
||||
const css = optimize(compiler.build(scanner.scan()), {
|
||||
minify: true,
|
||||
}).code.trim();
|
||||
const css = lowerPluginLayers(
|
||||
optimize(compiler.build(scanner.scan()), { minify: true }).code.trim(),
|
||||
);
|
||||
if (!css) return;
|
||||
const target = path.join(outDir, "frontend.css");
|
||||
const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
|
||||
|
||||
@@ -3,7 +3,10 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { build } from "../packages/plugin-sdk/cli/commands/build.mjs";
|
||||
import {
|
||||
build,
|
||||
lowerPluginLayers,
|
||||
} from "../packages/plugin-sdk/cli/commands/build.mjs";
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
@@ -84,3 +87,38 @@ describe("termix-plugin build", () => {
|
||||
expect(mod.mode).toBe("production");
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin CSS layers", () => {
|
||||
it("renames the Tailwind layers so core's utilities always win", () => {
|
||||
expect(
|
||||
lowerPluginLayers(
|
||||
"@layer properties{a{b:c}}@layer utilities{.flex{display:flex}}",
|
||||
),
|
||||
).toBe(
|
||||
"@layer termix-plugin-properties{a{b:c}}@layer termix-plugin-utilities{.flex{display:flex}}",
|
||||
);
|
||||
expect(lowerPluginLayers("@layer utilities-extra{}")).toBe(
|
||||
"@layer utilities-extra{}",
|
||||
);
|
||||
});
|
||||
|
||||
it("is ordered in core's CSS between base and core's utilities", () => {
|
||||
const css = fs.readFileSync(
|
||||
path.join(__dirname, "..", "src", "ui", "index.css"),
|
||||
"utf8",
|
||||
);
|
||||
const order = /@layer ([^;{]+);/.exec(css)?.[1].split(/[\s,]+/);
|
||||
expect(order).toEqual([
|
||||
"properties",
|
||||
"termix-plugin-properties",
|
||||
"theme",
|
||||
"base",
|
||||
"components",
|
||||
"termix-plugin-utilities",
|
||||
"utilities",
|
||||
]);
|
||||
expect(css.indexOf("@layer properties,")).toBeLessThan(
|
||||
css.indexOf('@import "tailwindcss"'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -93,11 +93,16 @@ export function describePluginFrontend(
|
||||
const entry = frontendEntry(plugin);
|
||||
if (!entry || !fileExists(entry.file)) return { ...none, locales };
|
||||
|
||||
const assetVersion = contentVersion(entry.file, plugin.manifest.version);
|
||||
const cssFile = path.join(entry.dir, "frontend.css");
|
||||
const css = fileExists(cssFile);
|
||||
const assetVersion = contentVersion(
|
||||
css ? [entry.file, cssFile] : [entry.file],
|
||||
plugin.manifest.version,
|
||||
);
|
||||
|
||||
return {
|
||||
frontend: true,
|
||||
css: fileExists(path.join(entry.dir, "frontend.css")),
|
||||
css,
|
||||
assetVersion,
|
||||
locales: locales.sort(),
|
||||
};
|
||||
@@ -108,21 +113,25 @@ const versionCache = new Map<string, { stamp: string; version: string }>();
|
||||
/**
|
||||
* A hash of the bundle's bytes, not its mtime: assets are cached as immutable
|
||||
* for a year, and a rebuilt .tmxplug has a fixed mtime, so a same-size rebuild
|
||||
* would otherwise keep serving the stale copy.
|
||||
* would otherwise keep serving the stale copy. The CSS is hashed with the JS
|
||||
* because both are served under the same version.
|
||||
*/
|
||||
function contentVersion(file: string, version: string): string {
|
||||
const stat = fs.statSync(file);
|
||||
function contentVersion(files: string[], version: string): string {
|
||||
// ctime, unlike mtime, cannot be set by tar, so an unpack always changes it.
|
||||
const stamp = `${version}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
||||
const cached = versionCache.get(file);
|
||||
const stamp = [
|
||||
version,
|
||||
...files.map((file) => {
|
||||
const stat = fs.statSync(file);
|
||||
return `${file}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
||||
}),
|
||||
].join("|");
|
||||
const key = files.join("|");
|
||||
const cached = versionCache.get(key);
|
||||
if (cached?.stamp === stamp) return cached.version;
|
||||
const hashed = crypto
|
||||
.createHash("sha256")
|
||||
.update(version)
|
||||
.update(fs.readFileSync(file))
|
||||
.digest("hex")
|
||||
.slice(0, 12);
|
||||
versionCache.set(file, { stamp, version: hashed });
|
||||
const hash = crypto.createHash("sha256").update(version);
|
||||
for (const file of files) hash.update(fs.readFileSync(file));
|
||||
const hashed = hash.digest("hex").slice(0, 12);
|
||||
versionCache.set(key, { stamp, version: hashed });
|
||||
return hashed;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,17 @@ describe("describePluginFrontend", () => {
|
||||
expect(info.locales).toEqual(["de_DE", "en"]);
|
||||
});
|
||||
|
||||
it("changes the version when only the stylesheet changes", () => {
|
||||
const before = describePluginFrontend(plugin).assetVersion;
|
||||
const css = path.join(root, "gizmo", "dist", "frontend.css");
|
||||
fs.writeFileSync(css, ".gizmo{color:red}");
|
||||
try {
|
||||
expect(describePluginFrontend(plugin).assetVersion).not.toBe(before);
|
||||
} finally {
|
||||
fs.writeFileSync(css, ".gizmo{}");
|
||||
}
|
||||
});
|
||||
|
||||
it("reports nothing for an unknown plugin", () => {
|
||||
expect(describePluginFrontend(undefined)).toEqual({
|
||||
frontend: false,
|
||||
|
||||
+3
-3
@@ -1,3 +1,6 @@
|
||||
/* Plugin utilities (termix-plugin build) rank above base, below core's own. */
|
||||
@layer properties, termix-plugin-properties, theme, base, components,
|
||||
termix-plugin-utilities, utilities;
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "./theme.css";
|
||||
@@ -13,9 +16,6 @@
|
||||
@import "@fontsource/source-code-pro/700.css";
|
||||
@import "@fontsource/source-code-pro/700-italic.css";
|
||||
|
||||
/* Plugins compile their own classes (termix-plugin build). */
|
||||
@source not "../../plugins";
|
||||
|
||||
@font-face {
|
||||
font-family: "Caskaydia Cove Nerd Font Mono";
|
||||
src: url("/fonts/CaskaydiaCoveNerdFontMono-Regular.ttf") format("truetype");
|
||||
|
||||
Reference in new issue
Block a user