Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 2 additions & 1 deletion scripts/docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)), "..");
Expand Down Expand Up @@ -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;
Expand Down
88 changes: 88 additions & 0 deletions scripts/public_contract.mjs
Original file line number Diff line number Diff line change
@@ -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;
};
Loading