feat: plugin docs and env manifest fields, in-app docs links, SDK 1.0.3

This commit is contained in:
LukeGus committed 2026-10-08 00:34:50 -05:00
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}`);
}
+1 -1
View File
@@ -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.
+3
View File
@@ -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]",