Files
wehub-resource-sync e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

212 lines
7.8 KiB
TypeScript

import * as fs from "node:fs";
import * as path from "node:path";
import { PRIMITIVE_DOCS_OUTPUT } from "./paths.mts";
import { getReactApiRenderOptions } from "./discover.mts";
import type { InheritedFrom, PropModel } from "./extract.mts";
import {
extractPrimitiveParts,
type PrimitivePartModel,
} from "./primitive-extract.mts";
// ── Projection: PrimitivePartModel[] \u2192 grouped doc shape ──────────────────
type RenderedProp = {
name: string;
type?: string;
description?: string;
default?: string;
required?: boolean;
deprecated?: string;
children?: Array<{ type?: string; parameters: RenderedProp[] }>;
};
type RenderedPart = {
element?: string;
description?: string;
deprecated?: string;
props: RenderedProp[];
};
type GroupedPrimitives = Record<string, Record<string, RenderedPart>>;
const PRIMITIVE_FILTER: ReadonlySet<InheritedFrom> = new Set([
"react",
"csstype",
"react-textarea-autosize",
"radix",
]);
function shouldDropForPrimitive(prop: PropModel): boolean {
if (prop.name.startsWith("__")) return true;
// `tw` is a polluting global JSX prop from `@vercel/og` types pulled in by
// the Next.js tsconfig. Legacy primitive-docs ran with
// `skipAddingFilesFromTsConfig: true` so it never saw this prop; we drop
// it by name here to preserve that behaviour.
if (prop.name === "tw") return true;
return (
prop.inheritedFrom !== undefined && PRIMITIVE_FILTER.has(prop.inheritedFrom)
);
}
/** Truncate very long inline object types at a token boundary. Mirrors the
* legacy primitive-docs cleanTypeText behaviour exactly. */
function presentPrimitiveType(rawType: string | undefined): string | undefined {
if (!rawType) return undefined;
if (rawType.length <= 120) return rawType;
return rawType.replace(/\{[^{}]{100,}\}/g, (match) => {
let cutoff = 80;
const semicolonIdx = match.lastIndexOf(";", cutoff);
const commaIdx = match.lastIndexOf(",", cutoff);
const breakIdx = Math.max(semicolonIdx, commaIdx);
if (breakIdx > 20) cutoff = breakIdx + 1;
return `${match.substring(0, cutoff)} ... }`;
});
}
/** Project one level of children. Legacy primitive-docs only expanded
* children one level deep (no recursion), and emitted children for the
* `components` prop without a wrapping `type` field. */
function projectChildrenShallow(
children: PropModel["children"],
parentPropName: string,
): RenderedProp["children"] | undefined {
if (!children) return undefined;
const omitType = parentPropName === "components";
const out = children
.map(({ typeName, props }) => {
const parameters = props
.filter((child) => !shouldDropForPrimitive(child))
.map((child) => projectProp(child, false));
if (parameters.length === 0) return undefined;
return omitType ? { parameters } : { type: typeName, parameters };
})
.filter((entry): entry is { type?: string; parameters: RenderedProp[] } =>
Boolean(entry),
);
return out.length > 0 ? out : undefined;
}
function isInlineObjectTypeText(typeText: string | undefined): boolean {
if (!typeText) return false;
const text = typeText.trim();
return text.startsWith("{") || text.startsWith("| {");
}
function projectProp(prop: PropModel, allowChildren = true): RenderedProp {
// Legacy primitive-docs only expanded children when:
// - prop is named `components` AND its type text contained `{`, OR
// - the prop's type-node was an inline object literal.
// Use rawType as a proxy for both checks.
const rawText = prop.rawType ?? "";
const shouldExpand =
allowChildren &&
((prop.name === "components" && rawText.includes("{")) ||
isInlineObjectTypeText(prop.rawType));
const children = shouldExpand
? projectChildrenShallow(prop.children, prop.name)
: undefined;
const type = presentPrimitiveType(prop.rawType);
const out: RenderedProp = { name: prop.name };
if (type !== undefined) out.type = type;
if (prop.description) out.description = prop.description;
if (prop.default) out.default = prop.default;
if (prop.required) out.required = true;
if (prop.deprecated) out.deprecated = prop.deprecated;
if (children) out.children = children;
return out;
}
function projectPart(part: PrimitivePartModel): RenderedPart {
let props: RenderedProp[];
if (part.isActionButton) {
// Legacy primitive-docs only documented hook-specific params for
// ActionButton parts. Both `render` (locally declared in WithRenderProp)
// and the rest of PrimitiveButtonProps are excluded here; `asChild` is
// re-injected below as a bare row. Also strip trailing `| undefined`
// from hook param types to match legacy ActionButton output.
props = part.props
.filter((p) => !shouldDropForPrimitive(p))
.filter((p) => p.name !== "render" && p.name !== "asChild")
// Legacy parity: this projection historically gated child expansion on
// the array index, so the first prop never expands its children.
.map((p, i) => projectProp(p, i !== 0))
.map((p) =>
p.type ? { ...p, type: p.type.replace(/\s*\|\s*undefined$/, "") } : p,
);
props = [{ name: "asChild" }, ...props];
} else {
props = part.props
.filter((p) => !shouldDropForPrimitive(p))
// Legacy parity: child expansion was historically gated on the array
// index, so the first prop never expands its children.
.map((p, i) => projectProp(p, i !== 0));
// Inject a bare asChild when the part supports it and the projection
// didn't already carry one.
if (part.supportsAsChild && !props.some((p) => p.name === "asChild")) {
props = [{ name: "asChild" }, ...props];
}
}
const out: RenderedPart = { props };
if (part.element) out.element = part.element;
if (part.description) out.description = part.description;
if (part.deprecated) out.deprecated = part.deprecated;
return out;
}
function projectToPrimitiveDocs(
parts: PrimitivePartModel[],
): GroupedPrimitives {
const result: GroupedPrimitives = {};
// First-wins per (primitiveName, localName) so that `Content` aliased to
// `Parts` doesn't emit a duplicate entry. Order matches barrel order from
// the underlying source file.
const seen = new Map<string, Set<string>>();
for (const part of parts) {
const seenLocals = seen.get(part.primitiveName) ?? new Set();
if (seenLocals.has(part.localName)) continue;
seenLocals.add(part.localName);
seen.set(part.primitiveName, seenLocals);
let primitive = result[part.primitiveName];
if (!primitive) {
primitive = {};
result[part.primitiveName] = primitive;
}
primitive[part.partName] = projectPart(part);
}
return result;
}
function serialize(grouped: GroupedPrimitives): string {
const lines: string[] = [
"// AUTO-GENERATED by scripts/generate-type-docs.mts",
"// Do not edit manually.",
"",
];
for (const [name, parts] of Object.entries(grouped)) {
lines.push(`export const ${name} = ${JSON.stringify(parts, null, 2)};\n`);
}
return lines.join("\n");
}
// ── Main ───────────────────────────────────────────────────────────────────
export function generatePrimitiveDocs() {
console.log("Generating primitive docs...");
const parts = extractPrimitiveParts(getReactApiRenderOptions());
const grouped = projectToPrimitiveDocs(parts);
const output = serialize(grouped);
fs.mkdirSync(path.dirname(PRIMITIVE_DOCS_OUTPUT), { recursive: true });
fs.writeFileSync(PRIMITIVE_DOCS_OUTPUT, output);
const totalParts = Object.values(grouped).reduce(
(sum, p) => sum + Object.keys(p).length,
0,
);
console.log(
`Generated docs for ${Object.keys(grouped).length} primitives with ${totalParts} parts \u2192 ${PRIMITIVE_DOCS_OUTPUT}`,
);
}