UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

254 lines • 11.6 kB
/** * The kit's docs, generated from its TypeScript types with the compiler API: each exported * component, its props (name, type, required, JSDoc description, `@default`), and the element whose * native attributes it also takes — so the model writes the props the components really have. * * `typescript` is an optional peer dependency (every TypeScript project has it). */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { resolveSandboxKitFiles, } from './files.js'; /** React's own DOM props a component inherits — listed only when they say something. */ const KEPT_DOM_PROPS = new Set([ 'children', 'className', 'disabled', 'value', 'defaultValue', 'onChange', 'onClick', 'placeholder', 'type', 'checked', 'defaultChecked', 'min', 'max', 'step', 'href', ]); const ELEMENT_OF_INTERFACE = { ButtonHTMLAttributes: 'button', InputHTMLAttributes: 'input', AnchorHTMLAttributes: 'a', TextareaHTMLAttributes: 'textarea', SelectHTMLAttributes: 'select', LabelHTMLAttributes: 'label', ImgHTMLAttributes: 'img', FormHTMLAttributes: 'form', TableHTMLAttributes: 'table', TdHTMLAttributes: 'td', ThHTMLAttributes: 'th', OptionHTMLAttributes: 'option', }; const ELEMENT_OF_CLASS = { Div: 'div', Span: 'span', Button: 'button', Input: 'input', Anchor: 'a', Paragraph: 'p', Heading: 'h3', TextArea: 'textarea', Select: 'select', Label: 'label', Image: 'img', Form: 'form', Table: 'table', TableRow: 'tr', TableCell: 'td', TableSection: 'tbody', UList: 'ul', OList: 'ol', LI: 'li', }; function hasCompilerApi(mod) { return typeof mod?.createProgram === 'function'; } async function loadTypescript(root, given) { const unwrap = (mod) => mod.default ?? mod; if (given !== undefined) { const mod = unwrap(given); if (hasCompilerApi(mod)) return mod; throw new TypeError('genui: `typescript` must be the TypeScript module (with createProgram)'); } let found; try { const { createRequire } = await import('node:module'); // The app's own TypeScript: the version its code is written for. found = unwrap(await import(createRequire(resolve(root, 'package.json')).resolve('typescript'))); } catch { found = undefined; } if (hasCompilerApi(found)) return found; throw new Error(found === undefined ? 'genui: generating the sandbox kit docs needs `typescript` (npm i -D typescript)' : "genui: the project's `typescript` has no JavaScript compiler API (TypeScript 7's native compiler) — add typescript@5 for the kit docs (npm i -D typescript5@npm:typescript@5 and pass `typescript: await import('typescript5')`)"); } function compilerOptionsFor(ts, files, tsconfig, root) { const defaults = { jsx: ts.JsxEmit.ReactJSX, allowJs: true, strict: true, target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, skipLibCheck: true, noEmit: true, esModuleInterop: true, }; const start = dirname(files.files[0] ?? resolve(root, 'index.ts')); const path = tsconfig !== undefined ? resolve(root, tsconfig) : ts.findConfigFile(start, ts.sys.fileExists, 'tsconfig.json'); if (path === undefined || !existsSync(path)) return defaults; const read = ts.readConfigFile(path, ts.sys.readFile); if (read.error !== undefined) return defaults; const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(path)); return { ...parsed.options, noEmit: true, allowJs: true, skipLibCheck: true, jsx: parsed.options.jsx ?? ts.JsxEmit.ReactJSX, }; } const fromReactTypes = (fileName) => /[\\/]node_modules[\\/](?:\.pnpm[\\/][^\\/]+[\\/]node_modules[\\/])?(?:@types[\\/]react|react)[\\/]/.test(fileName); /** * The docs of every component the kit exports, in declaration order. A component is an export named * in PascalCase whose type is callable with one props argument and returns JSX (function components, * `forwardRef`, `memo`). */ export async function generateSandboxKitDocs(options = {}) { const root = options.root ?? process.cwd(); const files = options.files ?? resolveSandboxKitFiles(root, options); if (files === null || (files.files.length === 0 && files.packages.length === 0)) return []; const ts = await loadTypescript(root, options.typescript); // Packages (`@mui/material`) are read through a module re-exporting them, inside the app's // node_modules so they resolve as the app's imports do. const roots = [...files.files]; if (files.packages.length > 0) { const dir = join(root, 'node_modules', '.cache', 'genui-sandbox-kit'); const path = join(dir, 'kit-packages.ts'); const text = `${files.packages.map((spec) => `export * from ${JSON.stringify(spec)};`).join('\n')}\n`; mkdirSync(dir, { recursive: true }); if (!existsSync(path) || readFileSync(path, 'utf8') !== text) writeFileSync(path, text); roots.push(path); } const program = ts.createProgram(roots, compilerOptionsFor(ts, files, options.tsconfig, root)); const checker = program.getTypeChecker(); const format = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | ts.TypeFormatFlags.WriteArrowStyleSignature; const docs = []; const seen = new Set(); const maxComponents = options.maxComponents ?? 120; for (const file of roots) { if (docs.length >= maxComponents) break; const source = program.getSourceFile(file); if (source === undefined) continue; const moduleSymbol = checker.getSymbolAtLocation(source); if (moduleSymbol === undefined) continue; for (const exported of checker.getExportsOfModule(moduleSymbol)) { if (docs.length >= maxComponents) break; const name = exported.getName(); if (!/^[A-Z][A-Za-z0-9]*$/.test(name) || seen.has(name)) continue; const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported; const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0]; if (declaration === undefined) continue; const type = checker.getTypeOfSymbolAtLocation(symbol, declaration); const signature = type.getCallSignatures()[0]; if (signature === undefined) continue; const returns = checker.typeToString(signature.getReturnType(), undefined, format); if (!/Element|ReactNode|ReactElement|JSX|null/.test(returns)) continue; const param = signature.getParameters()[0]; const propsType = param === undefined ? undefined : checker.getTypeOfSymbolAtLocation(param, declaration); const props = []; const inheritedFrom = new Map(); let elementHint; for (const prop of propsType === undefined ? [] : checker.getPropertiesOfType(checker.getApparentType(propsType))) { const propName = prop.getName(); if (propName === 'ref' || propName === 'key' || propName === 'asChild') continue; const declarations = prop.getDeclarations() ?? []; const propDeclaration = prop.valueDeclaration ?? declarations[0]; const inherited = declarations.length > 0 && declarations.every((each) => fromReactTypes(each.getSourceFile().fileName)); if (inherited) { for (const each of declarations) { const parent = each.parent; if (parent !== undefined && ts.isInterfaceDeclaration(parent)) { const iface = parent.name.text; inheritedFrom.set(iface, (inheritedFrom.get(iface) ?? 0) + 1); } } if (elementHint === undefined && (propName === 'onClick' || propName === 'onChange')) { const text = checker.typeToString(checker.getTypeOfSymbolAtLocation(prop, propDeclaration ?? declaration), undefined, format); const match = /HTML(\w+?)Element/.exec(text); if (match?.[1] !== undefined) elementHint = ELEMENT_OF_CLASS[match[1]]; } // Of the element's own attributes, only what a model reaches for: children, className, // and what the element's specific interface declares (a button's `type`, an input's // `value`/`onChange`…) — never the hundreds every element shares. const specific = declarations.some((each) => { const parent = each.parent; return (parent !== undefined && ts.isInterfaceDeclaration(parent) && ELEMENT_OF_INTERFACE[parent.name.text] !== undefined); }); if (!KEPT_DOM_PROPS.has(propName) || !(specific || propName === 'children' || propName === 'className')) continue; } const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0; let propType = checker.getTypeOfSymbolAtLocation(prop, propDeclaration ?? declaration); if (optional) propType = checker.getNonNullableType(propType); let text = checker.typeToString(propType, undefined, format); if (propName === 'children') text = 'ReactNode'; const description = ts.displayPartsToString(prop.getDocumentationComment(checker)).trim(); const defaultTag = prop.getJsDocTags(checker).find((tag) => tag.name === 'default'); const defaultValue = defaultTag?.text !== undefined ? ts.displayPartsToString(defaultTag.text).trim() : ''; props.push({ name: propName, type: text, required: !optional, ...(description !== '' ? { description } : {}), ...(defaultValue !== '' ? { default: defaultValue } : {}), }); } const specific = [...inheritedFrom.keys()] .map((iface) => ELEMENT_OF_INTERFACE[iface]) .find((element) => element !== undefined); const inherits = inheritedFrom.size > 0 ? (specific ?? elementHint ?? 'div') : undefined; const description = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim(); seen.add(name); docs.push({ name, ...(description !== '' ? { description } : {}), props: props.slice(0, 40), ...(inherits !== undefined ? { inherits } : {}), }); } } return docs; } //# sourceMappingURL=docs.js.map