Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
/**
* Catalog Resolver
*
* Provides utilities for loading and resolving Bun workspace catalog references.
* Handles both default catalog (catalog:) and named catalogs (catalog:testing).
*
* @example
* ```typescript
* const resolver = await CatalogResolver.create();
* const version = resolver.resolve('react', 'catalog:react');
* // Returns: '19.0.0'
* ```
*/
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { CONFIG } from "./config.js";
import { discoverPackageDirs } from "./discover-packages.js";
export type ExportsField =
| string
| { [key: string]: ExportsField }
| ExportsField[];
export interface PackageJson {
name?: string;
version?: string;
private?: boolean;
main?: string;
module?: string;
types?: string;
files?: string[];
exports?: ExportsField;
scripts?: Record<string, string>;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
workspaces?: {
catalog?: Record<string, string>;
catalogs?: Record<string, Record<string, string>>;
};
[extra: string]: unknown;
}
export interface CatalogEntry {
catalogName: string;
version: string;
}
/**
* Resolves catalog references to actual versions
*/
export class CatalogResolver {
private catalogs: Map<string, Record<string, string>> = new Map();
private workspaceVersions: Map<string, string> = new Map();
private constructor() {}
/**
* Create a CatalogResolver by loading catalogs from root package.json
* and discovering workspace package versions.
*
* @returns Initialized CatalogResolver instance
* @throws Error if root package.json cannot be read
*/
static async create(): Promise<CatalogResolver> {
const resolver = new CatalogResolver();
await resolver.loadCatalogs();
await resolver.loadWorkspaceVersions();
return resolver;
}
/**
* Discover all @suaveplan/* workspace packages and their current versions.
* Walks `packages/<plugin>/<pkg>/` via `discoverPackageDirs`.
*/
private async loadWorkspaceVersions(): Promise<void> {
const pkgDirs = await discoverPackageDirs();
for (const pkgDir of pkgDirs) {
const pkgPath = join(pkgDir, "package.json");
try {
const content = await readFile(pkgPath, "utf-8");
const pkg: PackageJson = JSON.parse(content);
if (pkg.name && pkg.version) {
this.workspaceVersions.set(pkg.name, pkg.version);
}
} catch {
// Skip entries without a readable package.json
}
}
}
/**
* Load catalogs from root package.json
*
* Loads both the default catalog (workspaces.catalog) and named catalogs
* (workspaces.catalogs) into memory for fast lookups.
*/
private async loadCatalogs(): Promise<void> {
const rootPkgPath = join(CONFIG.paths.root, "package.json");
const content = await readFile(rootPkgPath, "utf-8");
const rootPkg: PackageJson = JSON.parse(content);
// Load default catalog
if (rootPkg.workspaces?.catalog) {
this.catalogs.set("", rootPkg.workspaces.catalog);
}
// Load named catalogs
if (rootPkg.workspaces?.catalogs) {
for (const [name, versions] of Object.entries(
rootPkg.workspaces.catalogs
)) {
this.catalogs.set(name, versions);
}
}
}
/**
* Get the number of loaded catalogs
*/
getCatalogCount(): number {
return this.catalogs.size;
}
/**
* Get all catalog names
*/
getCatalogNames(): string[] {
return Array.from(this.catalogs.keys());
}
/**
* Resolve a catalog reference to an actual version
*
* @param packageName - The package name to resolve (e.g., 'react')
* @param catalogRef - The catalog reference (e.g., 'catalog:react' or 'catalog:')
* @returns The resolved version string, or null if not found
*
* @example
* ```typescript
* resolver.resolve('react', 'catalog:react') // Returns: '19.0.0'
* resolver.resolve('vitest', 'catalog:testing') // Returns: '2.1.0'
* resolver.resolve('typescript', 'catalog:') // Returns: '5.9.2'
* ```
*/
resolve(packageName: string, catalogRef: string): string | null {
const match = catalogRef.match(/^catalog:(.*)$/);
if (!match) {
return null;
}
const catalogName = match[1] || "";
const catalog = this.catalogs.get(catalogName);
if (!catalog) {
return null;
}
return catalog[packageName] || null;
}
/**
* Find which catalog contains a package
*
* @param packageName - The package name to search for
* @returns CatalogEntry with catalog name and version, or null if not found
*
* @example
* ```typescript
* resolver.findInCatalogs('react')
* // Returns: { catalogName: 'react', version: '19.0.0' }
* ```
*/
findInCatalogs(packageName: string): CatalogEntry | null {
for (const [catalogName, catalog] of this.catalogs.entries()) {
const version = catalog[packageName];
if (version) {
return { catalogName, version };
}
}
return null;
}
/**
* Resolve dependencies object, converting catalog refs to actual versions
*
* @param deps - Dependencies object from package.json
* @param currentVersion - Current package version (for workspace:* refs)
* @returns Resolved dependencies with actual versions
*
* @example
* ```typescript
* const resolved = resolver.resolveDependencies({
* 'react': 'catalog:react',
* '@suaveplan/utils': 'workspace:*',
* 'lodash': '^4.17.21'
* }, '1.0.0');
* // Returns: {
* // 'react': '19.0.0',
* // '@suaveplan/utils': '^1.0.0',
* // 'lodash': '^4.17.21'
* // }
* ```
*/
resolveDependencies(
deps: Record<string, string> | undefined,
_currentVersion: string
): Record<string, string> | undefined {
if (!deps) return undefined;
const resolved: Record<string, string> = {};
for (const [name, version] of Object.entries(deps)) {
let resolvedVersion = version;
// First, resolve catalog references
if (version.startsWith("catalog:")) {
const catalogResolved = this.resolve(name, version);
if (catalogResolved) {
resolvedVersion = catalogResolved;
} else {
continue; // Skip unresolved catalog refs
}
}
// Then, resolve workspace references (including those from catalogs)
if (resolvedVersion.startsWith("workspace:")) {
if (name.startsWith("@suaveplan/")) {
// Use the actual workspace package version so consumers
// pick up the correct minimum (prevents bun from resolving
// to stale nested versions from the registry).
const wsVersion = this.workspaceVersions.get(name);
resolved[name] = wsVersion ? `^${wsVersion}` : "^1.0.0";
} else {
resolved[name] = "*";
}
} else {
resolved[name] = resolvedVersion;
}
}
return Object.keys(resolved).length > 0 ? resolved : undefined;
}
/**
* Convert a regular version string to a catalog reference
*
* @param packageName - The package name
* @param version - The version string
* @returns Catalog reference string, or null if package not in any catalog
*
* @example
* ```typescript
* resolver.toCatalogRef('react', '19.0.0')
* // Returns: 'catalog:react'
* ```
*/
toCatalogRef(packageName: string, version: string): string | null {
const entry = this.findInCatalogs(packageName);
if (!entry) {
return null;
}
// Only convert if versions match
if (entry.version === version) {
return entry.catalogName
? `catalog:${entry.catalogName}`
: "catalog:";
}
return null;
}
/**
* Get all packages in a specific catalog
*
* @param catalogName - The catalog name (empty string for default catalog)
* @returns Record of package names to versions
*/
getCatalog(catalogName: string): Record<string, string> | undefined {
return this.catalogs.get(catalogName);
}
/**
* Check if a package exists in any catalog
*
* @param packageName - The package name to check
* @returns true if package exists in at least one catalog
*/
hasPackage(packageName: string): boolean {
return this.findInCatalogs(packageName) !== null;
}
}

72
scripts/lib/config.ts Normal file
View File

@@ -0,0 +1,72 @@
/**
* Centralized configuration for scripts
*
* All registry URLs, paths, and validation thresholds are defined here
* to avoid hardcoding values across multiple scripts.
*/
export const CONFIG = {
/**
* Registry configurations
*/
registry: {
verdaccio: {
url: process.env.VERDACCIO_URL || "https://npmjs.fq.io/",
webUrl: "https://npmjs.fq.io/-/web/detail",
},
gitlab: {
url:
process.env.GITLAB_REGISTRY ||
"https://gitlab.fq.io/api/v4/projects/2/packages/npm/",
apiKey: process.env.GITLAB_API_KEY,
},
},
/**
* Monorepo paths
*/
paths: {
root: process.env.MONOREPO_ROOT || process.cwd(),
/**
* Single root for all genesis workspace packages.
* Layout is `packages/<category>/<plugin>/<pkg>/` — see
* `discoverPackageDirs` in `./discover-packages.ts` for the 3-level
* walk (with 2-level and flat fallbacks).
*/
packagesRoot: "packages",
},
/**
* Quality and validation thresholds
*/
validation: {
minCoverage: 95,
minDocsCoverage: 90,
maxComplexity: 10,
maxMethodLines: 20,
maxClassLines: 200,
},
/**
* Build and test settings
*/
build: {
cacheDir: "node_modules/.vite",
distDir: "dist",
},
/**
* Exit codes for consistent error handling
*/
exitCodes: {
success: 0,
failure: 1,
partialSuccess: 2,
},
} as const;
/**
* Type-safe access to config values
*/
export type Config = typeof CONFIG;

View File

@@ -0,0 +1,78 @@
/**
* Walk `packages/<category>/<plugin>/<pkg>/` and return absolute directory
* paths for every workspace package (the level containing a `package.json`).
*
* Supports the canonical 3-level layout (`packages/<category>/<plugin>/<pkg>/`)
* and falls back to the legacy 2-level (`packages/<plugin>/<pkg>/`) or flat
* (`packages/<pkg>/`) layouts so experimental or transitional packages keep
* working.
*/
import { readdir, stat } from "node:fs/promises";
import { join } from "node:path";
import { CONFIG } from "./config.js";
export interface DiscoverOptions {
/** Override the monorepo root. Defaults to `CONFIG.paths.root`. */
root?: string;
/** Override the packages base. Defaults to `CONFIG.paths.packagesRoot`. */
packagesRoot?: string;
}
async function hasPackageJson(dir: string): Promise<boolean> {
try {
await stat(join(dir, "package.json"));
return true;
} catch {
return false;
}
}
async function listDirs(parent: string): Promise<string[]> {
const out: string[] = [];
const entries = await readdir(parent).catch((): string[] => []);
for (const name of entries) {
const path = join(parent, name);
const s = await stat(path).catch(() => null);
if (s?.isDirectory()) out.push(path);
}
return out;
}
export async function discoverPackageDirs(
options: DiscoverOptions = {}
): Promise<string[]> {
const root = options.root ?? CONFIG.paths.root;
const base = join(root, options.packagesRoot ?? CONFIG.paths.packagesRoot);
const dirs: string[] = [];
const level1 = await listDirs(base);
for (const lvl1 of level1) {
// Flat: packages/<pkg>/package.json
if (await hasPackageJson(lvl1)) {
dirs.push(lvl1);
continue;
}
const level2 = await listDirs(lvl1);
for (const lvl2 of level2) {
// Legacy 2-level: packages/<plugin>/<pkg>/package.json
if (await hasPackageJson(lvl2)) {
dirs.push(lvl2);
continue;
}
// Canonical 3-level: packages/<category>/<plugin>/<pkg>/package.json
const level3 = await listDirs(lvl2);
for (const lvl3 of level3) {
if (await hasPackageJson(lvl3)) {
dirs.push(lvl3);
}
}
}
}
dirs.sort();
return dirs;
}

260
scripts/lib/doc-tree.ts Normal file
View File

@@ -0,0 +1,260 @@
/**
* Shared helpers for the documentation-tree scripts:
* - `firstBodyParagraph` — extract a truncated first paragraph from a `.md`.
* - `walkMd` — recursive `.md` walker honoring an ignore list.
* - `replaceMarkedSection` — idempotent in-place section replacement.
* - `extractRelativeMdLinks` — pull `.md` link targets out of markdown content.
* - `findCanonicalMd` — locate the dominant `.md` for a directory.
*/
import { readdirSync, readFileSync, statSync } from "node:fs";
import { dirname, join, relative } from "node:path";
export const AUTO_MARKER_BEGIN = "<!-- begin:auto-docs -->";
export const AUTO_MARKER_END = "<!-- end:auto-docs -->";
/**
* Strip a leading `# Header` line and any frontmatter, then return the first
* non-empty paragraph (text up to the next blank line). Inline markdown
* formatting is preserved; whitespace is collapsed; the result is truncated
* to `maxChars` characters (with a trailing ellipsis when it overflows).
*/
export function firstBodyParagraph(content: string, maxChars = 120): string {
let body = content.replace(/^/, "");
if (body.startsWith("---\n")) {
const end = body.indexOf("\n---", 4);
if (end !== -1) body = body.slice(end + 4);
}
const lines = body.split("\n");
let i = 0;
// Skip leading whitespace + every heading line so the first prose
// paragraph is grabbed regardless of whether the doc starts with
// `# Title\n## Purpose\n\nprose…` or `# Title\n\nprose…`.
const isHeading = (line: string): boolean => /^#{1,6}\s/.test(line);
while (i < lines.length) {
const line = (lines[i] ?? "").trim();
if (line === "") {
i++;
continue;
}
if (isHeading(lines[i] ?? "")) {
i++;
continue;
}
break;
}
const paragraph: string[] = [];
while (i < lines.length) {
const line = lines[i] ?? "";
if (line.trim() === "") break;
if (/^(#{1,6}\s|```|---|>\s)/.test(line)) break;
paragraph.push(line);
i++;
}
// Strip inline markdown link syntax — `[text](url)` becomes `text` —
// so the truncation can never land inside a URL or break MDX autolink
// parsing downstream. Bare `<https://...>` autolinks are stripped to
// their text form too. Descriptions are display-only; the clickable
// links live elsewhere in the doc tree.
const stripped = paragraph
.join(" ")
.replace(/\[([^\]]+)\]\([^)]*\)/gu, "$1")
.replace(/<(https?:\/\/[^>]+)>/gu, "$1")
.replace(/\s+/g, " ")
.trim();
if (!stripped) return "";
if (stripped.length <= maxChars) return stripped;
let cut = stripped.slice(0, maxChars - 1).trimEnd();
// Belt-and-braces: if anything URL-shaped still extends past the cut,
// walk back to the last whitespace before it so the truncated string
// never trails a partial URL.
const lastHttpIdx = Math.max(
cut.lastIndexOf("https://"),
cut.lastIndexOf("http://")
);
if (lastHttpIdx >= 0 && !/\s/.test(cut.slice(lastHttpIdx))) {
cut = cut.slice(0, lastHttpIdx).trimEnd();
}
return `${cut}…`;
}
export interface WalkOptions {
/** Directory names (segments) to skip entirely. */
ignoreDirs?: readonly string[];
/** File names to skip. */
ignoreFiles?: readonly string[];
/** Path segments — if any segment matches, the file is skipped. */
ignoreSegments?: readonly string[];
}
const DEFAULT_IGNORE_DIRS: readonly string[] = [
"node_modules",
"dist",
"build",
"coverage",
".turbo",
".vite",
".next",
];
export function walkMd(root: string, options: WalkOptions = {}): string[] {
const ignoreDirs = new Set([
...DEFAULT_IGNORE_DIRS,
...(options.ignoreDirs ?? []),
]);
const ignoreFiles = new Set(options.ignoreFiles ?? []);
const ignoreSegments = options.ignoreSegments ?? [];
const out: string[] = [];
const stack: string[] = [root];
while (stack.length > 0) {
const dir = stack.pop() as string;
let entries: ReturnType<typeof readdirSync>;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const name = entry.name;
const full = join(dir, name);
if (entry.isDirectory()) {
if (ignoreDirs.has(name)) continue;
if (name.startsWith(".") && name !== ".") continue;
if (ignoreSegments.some((s) => full.includes(`${s}/`)))
continue;
stack.push(full);
} else if (entry.isFile()) {
if (!name.endsWith(".md")) continue;
if (ignoreFiles.has(name)) continue;
if (ignoreSegments.some((s) => full.includes(`${s}/`)))
continue;
out.push(full);
}
}
}
out.sort();
return out;
}
/**
* Find `<!-- begin:auto-docs -->...<!-- end:auto-docs -->` in `content` and
* replace the body between the markers with `replacement`. If markers are
* absent, the markered block is inserted before the first `## ` heading
* (so the autotable sits above hand-authored sections) or appended at the
* end of the file when no heading is found. Returns `{ next, changed }`.
*/
export function replaceMarkedSection(
content: string,
replacement: string
): { next: string; changed: boolean } {
const beginIdx = content.indexOf(AUTO_MARKER_BEGIN);
const endIdx = content.indexOf(AUTO_MARKER_END);
const block = `${AUTO_MARKER_BEGIN}\n${replacement.trim()}\n${AUTO_MARKER_END}`;
if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
const before = content.slice(0, beginIdx);
let after = content.slice(endIdx + AUTO_MARKER_END.length);
// Strip any stacked trailing end markers (with optional surrounding
// whitespace) and truncated `<!-- en…` fragments — defensive cleanup
// against historical generator-output corruption.
after = after.replace(
/(?:[ \t]*<!-- end:auto-docs -->[ \t]*\n?)+/g,
""
);
after = after.replace(/[ \t]*<!--\s*en…[ \t]*\n?/g, "");
const next = `${before}${block}${after}`;
return { next, changed: next !== content };
}
const headingMatch = content.match(/^##\s.*$/m);
if (headingMatch?.index !== undefined) {
const insertAt = headingMatch.index;
const before = content.slice(0, insertAt);
const after = content.slice(insertAt);
const sep = before.endsWith("\n\n")
? ""
: before.endsWith("\n")
? "\n"
: "\n\n";
const trailing = after.startsWith("\n") ? "" : "\n\n";
const next = `${before}${sep}${block}${trailing}${after}`;
return { next, changed: true };
}
const trimmed = content.replace(/\s+$/u, "");
const next = `${trimmed}\n\n${block}\n`;
return { next, changed: next !== content };
}
/**
* Pull every relative `.md` link target out of markdown content. Returns
* absolute filesystem paths resolved against `sourcePath`'s directory.
* Non-relative links (`http(s)://`, anchors, absolute URLs) are skipped.
*/
export function extractRelativeMdLinks(
content: string,
sourcePath: string
): string[] {
const out: string[] = [];
const base = dirname(sourcePath);
const matches = content.matchAll(/\[(?:[^\]]*)\]\(([^)]+)\)/g);
for (const match of matches) {
const raw = (match[1] ?? "").split("#")[0]?.trim() ?? "";
if (raw?.endsWith(".md") && !/^(?:https?:|mailto:|\/)/.test(raw)) {
out.push(join(base, raw));
}
}
return out;
}
/**
* For a directory, return the canonical `.md` doc. Priority order:
* 1. `<dirName>.md` (matches directory name)
* 2. `README.md`
* 3. The single non-`.types.md` `.md` file if exactly one exists
* 4. `undefined`
*/
export function findCanonicalMd(
dir: string,
dirName: string
): string | undefined {
const candidates: string[] = [];
let entries: ReturnType<typeof readdirSync>;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
return undefined;
}
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.endsWith(".md")) continue;
if (entry.name.endsWith(".types.md")) continue;
candidates.push(entry.name);
}
const exact = `${dirName}.md`;
if (candidates.includes(exact)) return join(dir, exact);
if (candidates.includes("README.md")) return join(dir, "README.md");
if (candidates.length === 1) return join(dir, candidates[0] ?? "");
return undefined;
}
export function isDirectorySafe(path: string): boolean {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}
export function tryRead(path: string): string | undefined {
try {
return readFileSync(path, "utf-8");
} catch {
return undefined;
}
}
export function relForLink(from: string, to: string): string {
const rel = relative(dirname(from), to).replace(/\\/g, "/");
return rel.startsWith(".") ? rel : `./${rel}`;
}