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
|
* otherwise have no CSS at all. Appended to whatever CSS the bundle already
|
||||||
* produced (a library's stylesheet the plugin imports).
|
* 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) {
|
export async function buildTailwind(cwd, outDir) {
|
||||||
const { compile, optimize } = await import("@tailwindcss/node");
|
const { compile, optimize } = await import("@tailwindcss/node");
|
||||||
const { Scanner } = await import("@tailwindcss/oxide");
|
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
|
// Flattened and minified the way core's own CSS is, so it does not rely
|
||||||
// on native CSS nesting.
|
// on native CSS nesting.
|
||||||
const css = optimize(compiler.build(scanner.scan()), {
|
const css = lowerPluginLayers(
|
||||||
minify: true,
|
optimize(compiler.build(scanner.scan()), { minify: true }).code.trim(),
|
||||||
}).code.trim();
|
);
|
||||||
if (!css) return;
|
if (!css) return;
|
||||||
const target = path.join(outDir, "frontend.css");
|
const target = path.join(outDir, "frontend.css");
|
||||||
const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
|
const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import fs from "node:fs";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
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> = [];
|
const cleanups: Array<() => void> = [];
|
||||||
|
|
||||||
@@ -84,3 +87,38 @@ describe("termix-plugin build", () => {
|
|||||||
expect(mod.mode).toBe("production");
|
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);
|
const entry = frontendEntry(plugin);
|
||||||
if (!entry || !fileExists(entry.file)) return { ...none, locales };
|
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 {
|
return {
|
||||||
frontend: true,
|
frontend: true,
|
||||||
css: fileExists(path.join(entry.dir, "frontend.css")),
|
css,
|
||||||
assetVersion,
|
assetVersion,
|
||||||
locales: locales.sort(),
|
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
|
* 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
|
* 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 {
|
function contentVersion(files: string[], version: string): string {
|
||||||
const stat = fs.statSync(file);
|
|
||||||
// ctime, unlike mtime, cannot be set by tar, so an unpack always changes it.
|
// ctime, unlike mtime, cannot be set by tar, so an unpack always changes it.
|
||||||
const stamp = `${version}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
const stamp = [
|
||||||
const cached = versionCache.get(file);
|
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;
|
if (cached?.stamp === stamp) return cached.version;
|
||||||
const hashed = crypto
|
const hash = crypto.createHash("sha256").update(version);
|
||||||
.createHash("sha256")
|
for (const file of files) hash.update(fs.readFileSync(file));
|
||||||
.update(version)
|
const hashed = hash.digest("hex").slice(0, 12);
|
||||||
.update(fs.readFileSync(file))
|
versionCache.set(key, { stamp, version: hashed });
|
||||||
.digest("hex")
|
|
||||||
.slice(0, 12);
|
|
||||||
versionCache.set(file, { stamp, version: hashed });
|
|
||||||
return hashed;
|
return hashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,17 @@ describe("describePluginFrontend", () => {
|
|||||||
expect(info.locales).toEqual(["de_DE", "en"]);
|
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", () => {
|
it("reports nothing for an unknown plugin", () => {
|
||||||
expect(describePluginFrontend(undefined)).toEqual({
|
expect(describePluginFrontend(undefined)).toEqual({
|
||||||
frontend: false,
|
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 "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "./theme.css";
|
@import "./theme.css";
|
||||||
@@ -13,9 +16,6 @@
|
|||||||
@import "@fontsource/source-code-pro/700.css";
|
@import "@fontsource/source-code-pro/700.css";
|
||||||
@import "@fontsource/source-code-pro/700-italic.css";
|
@import "@fontsource/source-code-pro/700-italic.css";
|
||||||
|
|
||||||
/* Plugins compile their own classes (termix-plugin build). */
|
|
||||||
@source not "../../plugins";
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Caskaydia Cove Nerd Font Mono";
|
font-family: "Caskaydia Cove Nerd Font Mono";
|
||||||
src: url("/fonts/CaskaydiaCoveNerdFontMono-Regular.ttf") format("truetype");
|
src: url("/fonts/CaskaydiaCoveNerdFontMono-Regular.ttf") format("truetype");
|
||||||
|
|||||||
Reference in new issue
Block a user