From d98c1a31e75dc2748407f20347143d0a1ebb4542 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Thu, 8 Oct 2026 23:20:38 -0400 Subject: [PATCH] Hide underscore-prefixed APIs from the reference The Mod publishes its QA and diagnostic hooks in the scripting contract under underscore-prefixed names (Game._openMap, HousePoints._probe, LocalPlayer._ragdoll, the _Beasts global and 57 others). The reference rendered all of them as public API, including a standalone page for _Beasts that TypeDoc names "Beasts". The build now copies the downloaded contract and drops every underscore-prefixed declaration, together with the doc comment above it, before handing the typings to services-cli. Removed declarations are replaced by blank lines so each remaining "Defined in: api.d.ts:N" still points at the published contract. The contract itself, its cache, and the Mod are untouched; the hooks remain callable. --- README.md | 1 + scripts/docs.mjs | 3 +- scripts/public_contract.mjs | 88 +++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 scripts/public_contract.mjs diff --git a/README.md b/README.md index 92c8c2f..3b31309 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The closed-source Mod remains authoritative for generated client and server API - Image directories live beside the Markdown document that references them. - `docs.config.json` owns the generator pin, branding, links, navigation, and community-content mapping. - `scripts/sync_contract.mjs` downloads and verifies the public scripting contract. +- `scripts/public_contract.mjs` drops underscore-prefixed declarations, the Mod's internal QA and diagnostic hooks, from a copy of the contract so the site documents only the public API. - `scripts/docs.mjs` is the single local and CI generation entrypoint. - `src/styles/production.css` is shared by the standalone site and local preview. diff --git a/scripts/docs.mjs b/scripts/docs.mjs index dbf8694..6ba0070 100644 --- a/scripts/docs.mjs +++ b/scripts/docs.mjs @@ -4,6 +4,7 @@ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { createPublicContract } from "./public_contract.mjs"; import { syncContract } from "./sync_contract.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -51,7 +52,7 @@ const createRenderConfig = async (contractRoot) => { }; const generate = async (output) => { - const contractRoot = await syncContract(); + const contractRoot = await createPublicContract(await syncContract()); const contractManifest = await readFile(path.join(contractRoot, "manifest.json"), "utf8").then(JSON.parse).catch(() => null); const contractRevision = contractManifest?.revision ?? "local"; const contractVersion = contractManifest?.version ?? null; diff --git a/scripts/public_contract.mjs b/scripts/public_contract.mjs new file mode 100644 index 0000000..7e38f65 --- /dev/null +++ b/scripts/public_contract.mjs @@ -0,0 +1,88 @@ +import { cp, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const publicRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".cache", "public-contract"); +// The Mod names its QA and diagnostic hooks with a leading underscore. They stay in the contract but not on the site. +const underscoreDeclaration = /[ \t]*(?:(?:export|declare|readonly|static|abstract|const|let|var|function|interface|type|class|enum|namespace)[ \t]+)*_[\w$]*(?=\s*[?!:(<{=;,]|\s+(?:extends|implements)\b)/y; +const continuation = /^\s*(?:[|&{=]|extends\b|implements\b)/; + +const skipCommentOrString = (source, index) => { + if (source.startsWith("//", index)) { + const end = source.indexOf("\n", index); + return end === -1 ? source.length : end; + } + if (source.startsWith("/*", index)) { + const end = source.indexOf("*/", index + 2); + return end === -1 ? source.length : end + 2; + } + const quote = source[index]; + if (quote !== '"' && quote !== "'" && quote !== "`") return index; + let end = index + 1; + while (end < source.length && source[end] !== quote) end += source[end] === "\\" ? 2 : 1; + return end + 1; +}; + +// A declaration ends at its `;` or `,`, at the line break after it unless the type carries on, or just before the +// bracket closing the scope around it. Nested brackets, strings and comments are skipped over. +const declarationEnd = (source, index) => { + let depth = 0; + let last = ""; + for (let i = index; i < source.length; i++) { + const skipped = skipCommentOrString(source, i); + if (skipped !== i) { + if (source[i] !== "/") last = source[i]; + i = skipped - 1; + continue; + } + const char = source[i]; + if ("{([<".includes(char)) depth++; + else if ("})]".includes(char) || (char === ">" && source[i - 1] !== "=")) { + if (--depth < 0) return i; + } else if (depth === 0 && (char === ";" || char === ",")) return i + 1; + else if (depth === 0 && char === "\n" && !["|", "&", ":", "=", "?", "=>"].includes(last) && !continuation.test(source.slice(i + 1, i + 128))) return i; + if (!/\s/.test(char)) last = char === ">" && source[i - 1] === "=" ? "=>" : char; + } + return source.length; +}; + +// Removes every underscore-prefixed declaration together with the doc comment directly above it. +export const stripUnderscoreDeclarations = (source) => { + let output = ""; + let copied = 0; + let comment = null; + for (let i = 0; i < source.length; i++) { + if (i === 0 || source[i - 1] === "\n" || i === comment?.end) { + underscoreDeclaration.lastIndex = i; + if (underscoreDeclaration.test(source)) { + const start = comment && /^[ \t]*(?:\r?\n)?$/.test(source.slice(comment.end, i)) ? comment.start : i; + let end = declarationEnd(source, underscoreDeclaration.lastIndex); + end += /^[ \t]*(?:\/\/[^\n]*)?/.exec(source.slice(end))[0].length; + // Blank lines stand in for the declaration so the reference's "Defined in" lines still match the published contract. + output += source.slice(copied, start) + "\n".repeat(source.slice(start, end).split("\n").length - 1); + copied = end; + comment = null; + i = end - 1; + continue; + } + } + const skipped = skipCommentOrString(source, i); + if (skipped !== i) { + const lineStart = source.lastIndexOf("\n", i - 1) + 1; + comment = source.startsWith("/*", i) && /^[ \t]*$/.test(source.slice(lineStart, i)) ? { start: lineStart, end: skipped } : null; + i = skipped - 1; + } else if (!/\s/.test(source[i])) comment = null; + } + return output + source.slice(copied); +}; + +export const createPublicContract = async (contractRoot) => { + await rm(publicRoot, { recursive: true, force: true }); + await cp(contractRoot, publicRoot, { recursive: true }); + for (const file of await readdir(publicRoot, { recursive: true })) { + if (!file.endsWith(".d.ts")) continue; + const declarations = path.join(publicRoot, file); + await writeFile(declarations, stripUnderscoreDeclarations(await readFile(declarations, "utf8"))); + } + return publicRoot; +};