mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: plugin docs and env manifest fields, in-app docs links, SDK 1.0.3
This commit is contained in:
1 parent
7fcda3bb01
commit
cfab6f764c
65 files changed
+1665
-294
No files matched your search
@@ -6,6 +6,7 @@ import * as esbuild from "esbuild";
|
||||
import { BACKEND_EXTERNALS, FRONTEND_EXTERNALS } from "../lib/externals.mjs";
|
||||
import { staticUrlImports } from "../lib/static-url-imports.mjs";
|
||||
import { readManifest, resolveEntry, copyDir } from "../lib/plugin-dir.mjs";
|
||||
import { openapi } from "./openapi.mjs";
|
||||
|
||||
const BACKEND_ENTRIES = [
|
||||
"src/backend/index.ts",
|
||||
@@ -241,6 +242,7 @@ export async function build({ cwd }) {
|
||||
|
||||
copyDir(path.join(cwd, "locales"), path.join(outDir, "locales"));
|
||||
copyDir(path.join(cwd, "migrations"), path.join(outDir, "migrations"));
|
||||
await openapi({ cwd, quiet: true });
|
||||
|
||||
console.log(`built ${pluginId}`);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function resolveDevOptions(args, env = process.env) {
|
||||
|
||||
const HINTS = {
|
||||
DEVELOPER_MODE_OFF:
|
||||
"Turn on developer mode in Admin settings, under Plugins, then save again.",
|
||||
"Turn on Plugin developer mode in Settings, General, then save again.",
|
||||
SIGNED_ONLY:
|
||||
"The server sets TERMIX_REQUIRE_SIGNED_PLUGINS, so it never installs unsigned files.",
|
||||
BUNDLED_ID: "That id ships with Termix. Use a different id while developing.",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { readManifest } from "../lib/plugin-dir.mjs";
|
||||
import { loadSdkModule } from "./validate.mjs";
|
||||
|
||||
/**
|
||||
* Writes dist/openapi.json from the @openapi blocks in src/backend. A plugin
|
||||
* with no documented routes gets no file.
|
||||
*/
|
||||
export async function openapi({ cwd, quiet = false }) {
|
||||
const manifest = readManifest(cwd);
|
||||
const pluginId = manifest.id ?? path.basename(cwd);
|
||||
const backendDir = path.join(cwd, "src", "backend");
|
||||
if (!fs.existsSync(backendDir)) return null;
|
||||
|
||||
const { buildOpenApi, hasOpenApiPaths } = await loadSdkModule("openapi");
|
||||
const spec = await buildOpenApi({
|
||||
title: `${manifest.name ?? pluginId} API`,
|
||||
version: manifest.version ?? "0.0.0",
|
||||
description: manifest.description,
|
||||
files: [path.join(backendDir, "**", "*.{ts,tsx,js,mjs}")],
|
||||
});
|
||||
|
||||
const target = path.join(cwd, "dist", "openapi.json");
|
||||
if (!hasOpenApiPaths(spec)) {
|
||||
fs.rmSync(target, { force: true });
|
||||
if (!quiet) console.log(`${pluginId}: no documented routes`);
|
||||
return null;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, `${JSON.stringify(spec, null, 2)}\n`);
|
||||
if (!quiet) {
|
||||
console.log(
|
||||
`wrote dist/openapi.json (${Object.keys(spec.paths).length} paths)`,
|
||||
);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
@@ -41,6 +41,10 @@ export async function validate({ cwd }) {
|
||||
problems.push(...validatePackage(cwd, raw));
|
||||
problems.push(...(await validateChangelogFile(cwd, raw)));
|
||||
|
||||
for (const warning of docsWarnings(cwd, raw)) {
|
||||
console.warn(` warn ${warning}`);
|
||||
}
|
||||
|
||||
if (problems.length > 0) {
|
||||
for (const problem of problems) console.error(` ${problem}`);
|
||||
throw new Error(`${raw.id ?? path.basename(cwd)}: manifest is not valid.`);
|
||||
@@ -49,6 +53,69 @@ export async function validate({ cwd }) {
|
||||
console.log(`ok ${raw.id ?? path.basename(cwd)}`);
|
||||
}
|
||||
|
||||
/** Env vars core sets for everyone, so a plugin need not list them. */
|
||||
const CORE_ENV = new Set([
|
||||
"DATA_DIR",
|
||||
"NODE_ENV",
|
||||
"PORT",
|
||||
"BASE_PATH",
|
||||
"VERSION",
|
||||
"HOME",
|
||||
"TMPDIR",
|
||||
"TEMP",
|
||||
"TMP",
|
||||
"PATH",
|
||||
"APPDATA",
|
||||
"LOCALAPPDATA",
|
||||
"USERPROFILE",
|
||||
"ELECTRON_EMBEDDED",
|
||||
]);
|
||||
|
||||
const ENV_READS = [
|
||||
/process\.env\.([A-Z][A-Z0-9_]*)/g,
|
||||
/process\.env\[\s*["'`]([A-Z][A-Z0-9_]*)["'`]\s*\]/g,
|
||||
/\benv\.([A-Z][A-Z0-9_]*)\b/g,
|
||||
/\benv\[\s*["'`]([A-Z][A-Z0-9_]*)["'`]\s*\]/g,
|
||||
];
|
||||
|
||||
function walkSources(dir, out = []) {
|
||||
if (!fs.existsSync(dir)) return out;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkSources(full, out);
|
||||
else if (/\.(ts|tsx|js|mjs|cjs)$/.test(entry.name)) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Docs gaps that do not block a release: no docs/index.md, or a process.env
|
||||
* read the manifest does not list in env.
|
||||
*/
|
||||
export function docsWarnings(cwd, raw) {
|
||||
const warnings = [];
|
||||
if (!fs.existsSync(path.join(cwd, "docs", "index.md"))) {
|
||||
warnings.push("docs/index.md is missing, so the plugin has no docs page");
|
||||
}
|
||||
const declared = new Set(
|
||||
Array.isArray(raw.env) ? raw.env.map((e) => e?.name) : [],
|
||||
);
|
||||
const missing = new Set();
|
||||
for (const file of walkSources(path.join(cwd, "src"))) {
|
||||
const text = fs.readFileSync(file, "utf8");
|
||||
for (const pattern of ENV_READS) {
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const name = match[1];
|
||||
if (!declared.has(name) && !CORE_ENV.has(name)) missing.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const name of [...missing].sort()) {
|
||||
warnings.push(`reads ${name} but manifest env does not list it`);
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* package.json has to agree with the manifest: the same version, and a real
|
||||
* SDK range rather than "*", so a plugin built against a newer SDK says so.
|
||||
|
||||
@@ -15,6 +15,7 @@ import { sign, verify, keygen } from "./commands/sign.mjs";
|
||||
import { migrations } from "./commands/migrations.mjs";
|
||||
import { dev } from "./commands/dev.mjs";
|
||||
import { changelog } from "./commands/changelog.mjs";
|
||||
import { openapi } from "./commands/openapi.mjs";
|
||||
|
||||
const COMMANDS = {
|
||||
build,
|
||||
@@ -32,6 +33,7 @@ const COMMANDS = {
|
||||
migrations,
|
||||
dev,
|
||||
changelog,
|
||||
openapi,
|
||||
};
|
||||
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
@@ -50,6 +52,7 @@ if (!command || command === "--help" || command === "-h") {
|
||||
" verify Check a .tmxplug's .sig <file> --key <base64>[,...]",
|
||||
" keygen Write a new signing key pair [--out dir]",
|
||||
" migrations Generate migrations from the plugin's table definitions",
|
||||
" openapi Write dist/openapi.json from the @openapi blocks in src/backend",
|
||||
" changelog Print a version's notes from CHANGELOG.md [--version x.y.z]",
|
||||
" dev Build, pack and install on a running server, again on every change",
|
||||
" [--server url] [--key tmx_...] [--once]",
|
||||
|
||||
Reference in new issue
Block a user