mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: shell controller, TabStage, layout and theme plugins with repair view
This commit is contained in:
1 parent
aff29a9581
commit
86161d66e2
120 files changed
+23693
-3381
No files matched your search
@@ -416,3 +416,71 @@ describe("rebuilding local plugins", () => {
|
||||
expect(run.sort()).toEqual(["a", "b"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("default look", () => {
|
||||
const entry = { source: "tmxplug", path: "a", sha256: SHA };
|
||||
|
||||
it("reads the defaultLook flag and refuses anything but a boolean", () => {
|
||||
const { plugins, problems } = parseBundledPlugins({
|
||||
plugins: [
|
||||
{ ...entry, id: "look", defaultLook: true },
|
||||
{ ...entry, id: "plain" },
|
||||
{ ...entry, id: "odd", defaultLook: "yes" },
|
||||
],
|
||||
});
|
||||
expect(problems).toEqual(["odd defaultLook must be a boolean"]);
|
||||
expect(
|
||||
plugins.map((p: { id: string; defaultLook: boolean }) => [
|
||||
p.id,
|
||||
p.defaultLook,
|
||||
]),
|
||||
).toEqual([
|
||||
["look", true],
|
||||
["plain", false],
|
||||
]);
|
||||
});
|
||||
|
||||
it("carries it into the bundled index and through a re-pin", () => {
|
||||
expect(
|
||||
buildBundledIndex(
|
||||
[
|
||||
{ id: "look", onboarding: null, defaultLook: true },
|
||||
{ id: "plain", onboarding: null, defaultLook: false },
|
||||
],
|
||||
["look", "plain"],
|
||||
),
|
||||
).toEqual({ version: 1, plugins: { look: { defaultLook: true } } });
|
||||
|
||||
const index = {
|
||||
plugins: [
|
||||
{
|
||||
id: "look",
|
||||
versions: [
|
||||
{ version: "1.0.0", url: "https://x/l.tmxplug", sha256: SHA },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(
|
||||
pinsFromIndex(index, null, [{ id: "look", defaultLook: true }])[0],
|
||||
).toMatchObject({ id: "look", defaultLook: true });
|
||||
});
|
||||
|
||||
it("copies a local build's theme files", () => {
|
||||
const repo = tempDir();
|
||||
fs.mkdirSync(path.join(repo, "dist"), { recursive: true });
|
||||
fs.mkdirSync(path.join(repo, "themes"), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo, "dist", "backend.js"), "export {}");
|
||||
fs.writeFileSync(path.join(repo, "themes", "paper.css"), ".paper{}");
|
||||
fs.writeFileSync(
|
||||
path.join(repo, "manifest.json"),
|
||||
JSON.stringify({
|
||||
id: "inks",
|
||||
contributes: { themes: [{ id: "paper", css: "themes/paper.css" }] },
|
||||
}),
|
||||
);
|
||||
const out = path.join(tempDir(), "inks");
|
||||
copyLocalBuild(repo, out);
|
||||
expect(fs.existsSync(path.join(out, "themes", "paper.css"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -194,6 +194,7 @@ const ALLOWED = [
|
||||
"SheetHeader",
|
||||
"SheetTitle",
|
||||
"SheetTrigger",
|
||||
"ShellConfirmCapture",
|
||||
"Skeleton",
|
||||
"Slider",
|
||||
"Sparkline",
|
||||
@@ -204,6 +205,7 @@ const ALLOWED = [
|
||||
"Switch",
|
||||
"SwitchRow",
|
||||
"TOTPDialog",
|
||||
"TabStage",
|
||||
"TabStrip",
|
||||
"TabStripItem",
|
||||
"TagInput",
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
* enabledByEnv counts the plugin as recommended when any of those
|
||||
* environment variables is set, for a server configured before first boot.
|
||||
* Plugins without it start unchecked.
|
||||
*
|
||||
* "defaultLook": true marks the plugin whose layout (and that layout's
|
||||
* theme) a fresh install shows. Core finds the default look this way, so it
|
||||
* never names a plugin. Such a plugin always starts enabled and cannot be
|
||||
* uninstalled, only swapped for another look.
|
||||
*/
|
||||
|
||||
const crypto = require("node:crypto");
|
||||
@@ -110,6 +115,12 @@ function parseBundledPlugins(raw) {
|
||||
}
|
||||
const before = problems.length;
|
||||
const onboarding = parseOnboarding(entry.onboarding, entry.id, problems);
|
||||
if (
|
||||
entry.defaultLook !== undefined &&
|
||||
typeof entry.defaultLook !== "boolean"
|
||||
) {
|
||||
problems.push(`${entry.id} defaultLook must be a boolean`);
|
||||
}
|
||||
if (problems.length > before) continue;
|
||||
plugins.push({
|
||||
id: entry.id,
|
||||
@@ -118,6 +129,7 @@ function parseBundledPlugins(raw) {
|
||||
path: hasPath ? entry.path : null,
|
||||
sha256: entry.sha256,
|
||||
onboarding,
|
||||
defaultLook: entry.defaultLook === true,
|
||||
});
|
||||
} else {
|
||||
problems.push(`${entry.id} has an unknown source "${entry.source}"`);
|
||||
@@ -218,8 +230,12 @@ function buildBundledIndex(plugins, stagedIds) {
|
||||
const staged = new Set(stagedIds);
|
||||
const out = {};
|
||||
for (const plugin of plugins) {
|
||||
if (!staged.has(plugin.id) || !plugin.onboarding) continue;
|
||||
out[plugin.id] = { onboarding: plugin.onboarding };
|
||||
if (!staged.has(plugin.id)) continue;
|
||||
if (!plugin.onboarding && !plugin.defaultLook) continue;
|
||||
out[plugin.id] = {
|
||||
...(plugin.onboarding ? { onboarding: plugin.onboarding } : {}),
|
||||
...(plugin.defaultLook ? { defaultLook: true } : {}),
|
||||
};
|
||||
}
|
||||
return { version: 1, plugins: out };
|
||||
}
|
||||
@@ -238,12 +254,14 @@ function pinsFromIndex(index, ids, current = []) {
|
||||
const latest = byId.get(id)?.versions?.[0];
|
||||
if (!latest) throw new Error(`${id} is not in the registry index`);
|
||||
const onboarding = currentById.get(id)?.onboarding;
|
||||
const defaultLook = currentById.get(id)?.defaultLook === true;
|
||||
return {
|
||||
id,
|
||||
source: "tmxplug",
|
||||
url: latest.url,
|
||||
sha256: latest.sha256,
|
||||
...(onboarding ? { onboarding } : {}),
|
||||
...(defaultLook ? { defaultLook } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -357,9 +375,26 @@ function rebuildLocalPlugins(dir, ids, run) {
|
||||
}
|
||||
|
||||
/** Copies a local plugin build into the staging folder. */
|
||||
/** Theme CSS files the repo's manifest declares, which ship with it too. */
|
||||
function localThemeFiles(repo) {
|
||||
try {
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(path.join(repo, "manifest.json"), "utf8"),
|
||||
);
|
||||
const themes = manifest?.contributes?.themes;
|
||||
return Array.isArray(themes)
|
||||
? themes
|
||||
.map((theme) => theme?.css)
|
||||
.filter((css) => typeof css === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function copyLocalBuild(repo, destination) {
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
for (const entry of LOCAL_FILES) {
|
||||
for (const entry of new Set([...LOCAL_FILES, ...localThemeFiles(repo)])) {
|
||||
const from = path.join(repo, entry);
|
||||
if (fs.existsSync(from)) {
|
||||
fs.cpSync(from, path.join(destination, entry), { recursive: true });
|
||||
|
||||
@@ -142,3 +142,29 @@ describe("termix-plugin pack", () => {
|
||||
expect(fs.readdirSync(out)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("termix-plugin pack themes", () => {
|
||||
it("ships the CSS files the manifest's themes declare", async () => {
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const dir = fixturePlugin({
|
||||
capabilities: ["ui:theme"],
|
||||
contributes: {
|
||||
themes: [
|
||||
{
|
||||
id: "paper",
|
||||
label: "Paper",
|
||||
mode: "light",
|
||||
css: "styles/paper.css",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
writeFile(dir, "styles/paper.css", ".paper { --background: #fff; }\n");
|
||||
writeFile(dir, "styles/draft.css", ".draft {}\n");
|
||||
const file = await pack({ cwd: dir, args: ["--out", tempDir("t-")] });
|
||||
const names = await entries(file);
|
||||
expect(names).toContain("styles/paper.css");
|
||||
expect(names).not.toContain("styles/draft.css");
|
||||
});
|
||||
});
|
||||
@@ -49,3 +49,53 @@ describe("termix-plugin validate migrations", () => {
|
||||
expect(output).not.toMatch(/migrations\/\w+ is missing/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("termix-plugin validate themes", () => {
|
||||
function themed(css: string | null) {
|
||||
const root = plugin([]);
|
||||
fs.writeFileSync(
|
||||
path.join(root, "manifest.json"),
|
||||
JSON.stringify({
|
||||
id: "fixture",
|
||||
version: "1.0.0",
|
||||
capabilities: ["ui:theme"],
|
||||
contributes: {
|
||||
themes: [
|
||||
{
|
||||
id: "paper",
|
||||
label: "Paper",
|
||||
mode: "light",
|
||||
css: "themes/paper.css",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (css !== null) {
|
||||
fs.mkdirSync(path.join(root, "themes"));
|
||||
fs.writeFileSync(path.join(root, "themes", "paper.css"), css);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
it("reports a missing theme file", async () => {
|
||||
expect(await problems(themed(null))).toContain(
|
||||
"theme paper: themes/paper.css does not exist",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports theme CSS that reaches outside or leaves its class", async () => {
|
||||
const output = await problems(
|
||||
themed('@import "x.css"; body { color: red }'),
|
||||
);
|
||||
expect(output).toContain("theme paper: @import is not allowed in a theme");
|
||||
expect(output).toContain(
|
||||
'theme paper: Selector "body" must start with .paper',
|
||||
);
|
||||
});
|
||||
|
||||
it("says nothing about CSS that is fine", async () => {
|
||||
const output = await problems(themed(".paper { --background: #fff; }"));
|
||||
expect(output).not.toContain("theme paper");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user