@peripheries/engine 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
import ts from"typescript";import{resolve}from"node:path";import{readFileSync}from"node:fs";import{existsSync}from"node:fs";import{loadConfig}from"../lib/config.mjs";import{readExportsFromIndex,resolveComponentFilePath,hasComponentJSDoc,parseJSDocFile}from"../lib/jsdoc-parser.mjs";import{kitFamilies}from"../lib/knowledge.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;const engineConfig=await loadConfig();const BARRELS=engineConfig.lineages.map(l=>l.entry).filter(b=>existsSync(resolve(repoRoot,b)));const{propsSchema}=engineConfig.checks;const BLOCKING_FAMILIES=new Set(propsSchema.families??kitFamilies());const BLOCKING_NAMES=new Set(propsSchema.names);const BLOCKING_FLOOR=propsSchema.floor;const GATED=BLOCKING_FAMILIES.size>0||BLOCKING_NAMES.size>0;const INTERNAL_PROPS=new Map([["profile","DocumentProfile"]]);const PROPLESS=new Set(propsSchema.propless);const HOST_INJECTED=new Set(["baseProps","data","status","actions","isLoading","error","className","style","key","ref"]);const showAll=process.argv.includes("--all");const configPath=resolve(repoRoot,"tsconfig.json");const config=ts.readConfigFile(configPath,ts.sys.readFile);const parsed=ts.parseJsonConfigFileContent(config.config,ts.sys,repoRoot);const program=ts.createProgram({rootNames:BARRELS.map(b=>resolve(repoRoot,b)),options:parsed.options});const checker=program.getTypeChecker();function readBlock(filePath,name){const content=readFileSync(filePath,"utf8");const block=(content.match(/\/\*\*[\s\S]*?\*\//g)??[]).find(b=>new RegExp(`@component\\s+${name}\\b`).test(b));const declared=new Set;if(!block)return{declared,exampleTags:0};for(const m of block.matchAll(/@props\s+\{[^}]+\}\s+(?:\[([^=\]]+)(?:=[^\]]+)?\]|(\w+(?:\.\w+)*))\s*-/g)){declared.add((m[1]??m[2]??"").trim().split(".")[0]??"")}return{declared,exampleTags:(block.match(/^\s*\*?\s*@example\b/gm)??[]).length}}function propsTypeOf(symbol,at){const target=symbol.flags&ts.SymbolFlags.Alias?checker.getAliasedSymbol(symbol):symbol;const type=checker.getTypeOfSymbolAtLocation(target,at);const sig=type.getCallSignatures()[0];const param=sig?.getParameters()[0];if(!param)return null;return checker.getTypeOfSymbolAtLocation(param,at)}function ownProps(type){const out=new Set;const members=type.isUnion()?type.types:[type];for(const member of members){for(const prop of checker.getPropertiesOfType(member)){const decls=prop.getDeclarations()??[];if(decls.length===0)continue;if(decls.every(d=>d.getSourceFile().fileName.includes("/node_modules/")))continue;if(HOST_INJECTED.has(prop.name))continue;const internalType=INTERNAL_PROPS.get(prop.name);if(internalType&&checker.typeToString(checker.getNonNullableType(checker.getTypeOfSymbol(prop)))===internalType)continue;out.add(prop.name)}}return out}function elementOf(type){const t=checker.getNonNullableType(type);if(checker.isArrayType(t))return checker.getTypeArguments(t)[0]??null;return null}function shapeFindings(schema,type,path,out){const items=schema?.type==="array"?schema.items:void 0;const element=elementOf(type);if(!items?.properties||!element)return;const fields=new Map(checker.getPropertiesOfType(checker.getNonNullableType(element)).map(p=>[p.name,p]));for(const name of fields.keys())if(!(name in items.properties)&&!HOST_INJECTED.has(name))out.push({code:"SHAPE_UNDECLARED",prop:`${path}[].${name}`});for(const[name,sub]of Object.entries(items.properties)){const field=fields.get(name);if(!field){if(items.additionalProperties!==true)out.push({code:"SHAPE_PHANTOM",prop:`${path}[].${name}`});continue}shapeFindings(sub,checker.getTypeOfSymbol(field),`${path}[].${name}`,out)}}const results=[];const seen=new Set;for(const barrel of BARRELS){const barrelFile=program.getSourceFile(resolve(repoRoot,barrel));if(!barrelFile)continue;const moduleSymbol=checker.getSymbolAtLocation(barrelFile);if(!moduleSymbol)continue;const exportsByName=new Map(checker.getExportsOfModule(moduleSymbol).map(s=>[s.name,s]));for(const{name,source}of readExportsFromIndex(repoRoot,barrel)){if(seen.has(name))continue;const filePath=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(!filePath||!hasComponentJSDoc(filePath,name))continue;seen.add(name);const jsdoc=parseJSDocFile(filePath,name);const family=jsdoc.family??jsdoc.component_type??"?";const blocking=BLOCKING_FAMILIES.has(family)||BLOCKING_NAMES.has(name);const file=filePath.slice(repoRoot.length+1);const symbol=exportsByName.get(name);const type=symbol?propsTypeOf(symbol,barrelFile):null;if(!type){results.push({name,file,family,blocking,own:0,findings:[{code:"UNRESOLVED"}]});continue}const own=ownProps(type);const{declared,exampleTags}=readBlock(filePath,name);const examples=jsdoc.usage_examples??[];const findings=[];if(examples.length<exampleTags)findings.push({code:"EXAMPLE_UNPARSEABLE",prop:`${exampleTags-examples.length} of ${exampleTags}`});for(const key of new Set(examples.flatMap(e=>Object.keys(e)))){if(!own.has(key)&&!declared.has(key))findings.push({code:"EXAMPLE_UNKNOWN_PROP",prop:key})}if(blocking&&!jsdoc.agent_instructions)findings.push({code:"NO_INSTRUCTIONS"});if(blocking&&exampleTags===0)findings.push({code:"NO_EXAMPLE"});for(const p of own)if(!declared.has(p))findings.push({code:"UNDECLARED",prop:p});for(const p of declared)if(!own.has(p)&&!HOST_INJECTED.has(p))findings.push({code:"PHANTOM",prop:p});for(const[prop,schema]of Object.entries(jsdoc.props_schema?.properties??{})){const member=checker.getPropertyOfType(type,prop);if(member)shapeFindings(schema,checker.getTypeOfSymbol(member),prop,findings)}results.push({name,file,family,blocking,own:own.size,findings})}}const blockingSet=results.filter(r=>r.blocking);const failures=blockingSet.filter(r=>r.findings.length>0||r.own===0&&!PROPLESS.has(r.name));const reported=results.filter(r=>!r.blocking&&r.findings.length>0);function print(r){const detail=r.own===0&&r.findings.length===0?"no own props resolved (a component with none is a broken check, not a clean one)":r.findings.map(f=>f.prop?`${f.code} ${f.prop}`:f.code).join(", ");console.log(` ${r.name} (${r.family}) ${r.file}
|
|
2
|
+
import ts from"typescript";import{resolve}from"node:path";import{readFileSync}from"node:fs";import{existsSync}from"node:fs";import{loadConfig}from"../lib/config.mjs";import{readExportsFromIndex,resolveComponentFilePath,hasComponentJSDoc,parseJSDocFile}from"../lib/jsdoc-parser.mjs";import{kitFamilies}from"../lib/knowledge.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;const engineConfig=await loadConfig();const BARRELS=engineConfig.lineages.map(l=>l.entry).filter(b=>existsSync(resolve(repoRoot,b)));const{propsSchema}=engineConfig.checks;const BLOCKING_FAMILIES=new Set(propsSchema.families??kitFamilies());const BLOCKING_NAMES=new Set(propsSchema.names);const BLOCKING_FLOOR=propsSchema.floor;const GATED=BLOCKING_FAMILIES.size>0||BLOCKING_NAMES.size>0;const INTERNAL_PROPS=new Map([["profile","DocumentProfile"]]);const PROPLESS=new Set(propsSchema.propless);const HOST_INJECTED=new Set(["baseProps","data","status","actions","isLoading","error","navigate","onNavigate","context","renderComponent","size","selections","viewState","live","className","style","key","ref"]);const showAll=process.argv.includes("--all");const configPath=resolve(repoRoot,"tsconfig.json");const config=ts.readConfigFile(configPath,ts.sys.readFile);const parsed=ts.parseJsonConfigFileContent(config.config,ts.sys,repoRoot);const program=ts.createProgram({rootNames:BARRELS.map(b=>resolve(repoRoot,b)),options:parsed.options});const checker=program.getTypeChecker();function readBlock(filePath,name){const content=readFileSync(filePath,"utf8");const block=(content.match(/\/\*\*[\s\S]*?\*\//g)??[]).find(b=>new RegExp(`@component\\s+${name}\\b`).test(b));const declared=new Set;if(!block)return{declared,exampleTags:0};for(const m of block.matchAll(/@props\s+\{[^}]+\}\s+(?:\[([^=\]]+)(?:=(?:\[[^\]]*\]|[^\]])+)?\]|(\w+(?:\.\w+)*))\s*-/g)){declared.add((m[1]??m[2]??"").trim().split(".")[0]??"")}for(const m of block.matchAll(/(?:^|\n)\s*\*?\s*@slot\s+\{?([A-Za-z_][\w-]*)\}?/g))declared.add(m[1]);return{declared,exampleTags:(block.match(/^\s*\*?\s*@example\b/gm)??[]).length}}function propsTypeOf(symbol,at){const target=symbol.flags&ts.SymbolFlags.Alias?checker.getAliasedSymbol(symbol):symbol;const type=checker.getTypeOfSymbolAtLocation(target,at);const sig=type.getCallSignatures()[0];const param=sig?.getParameters()[0];if(!param)return null;return checker.getTypeOfSymbolAtLocation(param,at)}function ownProps(type){const out=new Set;const members=type.isUnion()?type.types:[type];for(const member of members){for(const prop of checker.getPropertiesOfType(member)){const decls=prop.getDeclarations()??[];if(decls.length===0)continue;if(decls.every(d=>d.getSourceFile().fileName.includes("/node_modules/")))continue;if(HOST_INJECTED.has(prop.name))continue;const internalType=INTERNAL_PROPS.get(prop.name);if(internalType&&checker.typeToString(checker.getNonNullableType(checker.getTypeOfSymbol(prop)))===internalType)continue;out.add(prop.name)}}return out}function elementOf(type){const t=checker.getNonNullableType(type);if(checker.isArrayType(t))return checker.getTypeArguments(t)[0]??null;return null}function shapeFindings(schema,type,path,out){const items=schema?.type==="array"?schema.items:void 0;const element=elementOf(type);if(!items?.properties||!element)return;const fields=new Map(checker.getPropertiesOfType(checker.getNonNullableType(element)).map(p=>[p.name,p]));for(const name of fields.keys())if(!(name in items.properties)&&!HOST_INJECTED.has(name))out.push({code:"SHAPE_UNDECLARED",prop:`${path}[].${name}`});for(const[name,sub]of Object.entries(items.properties)){const field=fields.get(name);if(!field){if(items.additionalProperties!==true)out.push({code:"SHAPE_PHANTOM",prop:`${path}[].${name}`});continue}shapeFindings(sub,checker.getTypeOfSymbol(field),`${path}[].${name}`,out)}}const results=[];const seen=new Set;for(const barrel of BARRELS){const barrelFile=program.getSourceFile(resolve(repoRoot,barrel));if(!barrelFile)continue;const moduleSymbol=checker.getSymbolAtLocation(barrelFile);if(!moduleSymbol)continue;const exportsByName=new Map(checker.getExportsOfModule(moduleSymbol).map(s=>[s.name,s]));for(const{name,source}of readExportsFromIndex(repoRoot,barrel)){if(seen.has(name))continue;const filePath=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(!filePath||!hasComponentJSDoc(filePath,name))continue;seen.add(name);const jsdoc=parseJSDocFile(filePath,name);const family=jsdoc.family??jsdoc.component_type??"?";const blocking=BLOCKING_FAMILIES.has(family)||BLOCKING_NAMES.has(name);const file=filePath.slice(repoRoot.length+1);const symbol=exportsByName.get(name);const type=symbol?propsTypeOf(symbol,barrelFile):null;if(!type){results.push({name,file,family,blocking,own:0,findings:[{code:"UNRESOLVED"}]});continue}const own=ownProps(type);const{declared,exampleTags}=readBlock(filePath,name);const examples=jsdoc.usage_examples??[];const findings=[];if(examples.length<exampleTags)findings.push({code:"EXAMPLE_UNPARSEABLE",prop:`${exampleTags-examples.length} of ${exampleTags}`});for(const key of new Set(examples.flatMap(e=>Object.keys(e)))){if(!own.has(key)&&!declared.has(key))findings.push({code:"EXAMPLE_UNKNOWN_PROP",prop:key})}if(blocking&&!jsdoc.agent_instructions)findings.push({code:"NO_INSTRUCTIONS"});if(blocking&&exampleTags===0)findings.push({code:"NO_EXAMPLE"});for(const p of own)if(!declared.has(p))findings.push({code:"UNDECLARED",prop:p});for(const p of declared)if(!own.has(p)&&!HOST_INJECTED.has(p))findings.push({code:"PHANTOM",prop:p});for(const[prop,schema]of Object.entries(jsdoc.props_schema?.properties??{})){const member=checker.getPropertyOfType(type,prop);if(member)shapeFindings(schema,checker.getTypeOfSymbol(member),prop,findings)}results.push({name,file,family,blocking,own:own.size,findings})}}const blockingSet=results.filter(r=>r.blocking);const failures=blockingSet.filter(r=>r.findings.length>0||r.own===0&&!PROPLESS.has(r.name));const reported=results.filter(r=>!r.blocking&&r.findings.length>0);function print(r){const detail=r.own===0&&r.findings.length===0?"no own props resolved (a component with none is a broken check, not a clean one)":r.findings.map(f=>f.prop?`${f.code} ${f.prop}`:f.code).join(", ");console.log(` ${r.name} (${r.family}) ${r.file}
|
|
3
3
|
${detail}`)}if(failures.length){console.log(`[props-schema] \u2717 ${failures.length} blocking component(s) whose @props disagree with their props type:`);failures.forEach(print)}if(showAll&&reported.length){console.log(`[props-schema] non-blocking (${reported.length}):`);reported.forEach(print)}const missingNames=[...BLOCKING_NAMES].filter(n=>!results.some(r=>r.name===n));if(missingNames.length){console.log(`[props-schema] \u2717 checks.propsSchema.names not found in any barrel: ${missingNames.join(", ")}`)}const underFloor=GATED&&blockingSet.length<BLOCKING_FLOOR;if(underFloor){console.log(`[props-schema] \u2717 only ${blockingSet.length} blocking component(s) checked; floor is ${BLOCKING_FLOOR}. The resolver or the scope broke.`)}const scope=propsSchema.families?"checks.propsSchema":"the kits";const blockingBy=GATED?`blocking: ${[...BLOCKING_FAMILIES].join(", ")||"(no families)"} from ${scope}${BLOCKING_NAMES.size?` + ${BLOCKING_NAMES.size} name(s)`:""}`:"nothing blocks (no kit families, no checks.propsSchema)";const failed=failures.length>0||missingNames.length>0||underFloor;console.log(`[props-schema] ${failed?"\u2717":"ok"} \u2014 ${blockingSet.length} blocking checked, ${failures.length} failing (${blockingBy}); ${reported.length} of ${results.length-blockingSet.length} others disagree (reported${showAll?"":", --all to list"}).`);process.exit(failed?1:0);
|
package/src/lib/jsdoc-parser.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync,existsSync}from"node:fs";import{resolve}from"node:path";function resolveComponentFilePath(repoRoot,relative){const base=resolve(repoRoot,"src",relative);const dirBasename=base.split("/").pop();const candidates=[`${base}.ts`,`${base}.tsx`,`${base}/${dirBasename}.tsx`,`${base}/${dirBasename}.ts`,`${base}/index.ts`,`${base}/index.tsx`];return candidates.find(p=>existsSync(p))??null}function toRepoRelative(repoRoot,absPath){const srcRoot=resolve(repoRoot,"src")+"/";return absPath.startsWith(srcRoot)?absPath.slice(srcRoot.length):absPath}function parseJSDocFile(filePath,componentName){try{const content=readFileSync(filePath,"utf8");return parseJSDocBlock(content,componentName)}catch{return{}}}function hasComponentJSDoc(filePath,componentName){if(!filePath)return false;let content;try{content=readFileSync(filePath,"utf8")}catch{return false}const blocks=content.match(/\/\*\*[\s\S]*?\*\//g);if(!blocks)return false;return blocks.some(b=>componentName?new RegExp(`@component\\s+${escapeRegExp(componentName)}\\b`).test(b):b.includes("@component"))}function parseJSDocBlock(fileContent,componentName){const blocks=fileContent.match(/\/\*\*[\s\S]*?\*\//g);if(!blocks)return{};const componentBlock=componentName?blocks.find(b=>new RegExp(`@component\\s+${escapeRegExp(componentName)}\\b`).test(b))??blocks.find(b=>b.includes("@component")):blocks.find(b=>b.includes("@component"));if(!componentBlock)return{};const body=componentBlock.replace(/\/\*\*|\*\//g,"").split("\n").map(line=>line.replace(/^\s*\*\s?/,"")).join("\n");const meta={};const descMatch=body.match(/@description\s+([\s\S]+?)(?=\n\s*@|$)/);if(descMatch){meta.description=descMatch[1].replace(/\s+/g," ").trim()}const agentInstructionsMatch=body.match(/@agentInstructions\s+([\s\S]+?)(?=\n\s*@\w+|$)/);if(agentInstructionsMatch){meta.agent_instructions=agentInstructionsMatch[1].trim()}const categoryMatch=body.match(/@category\s+([\w-]+(?:\/[\w-]+)*)/);if(categoryMatch){meta.family=categoryMatch[1];meta.component_type=categoryMatch[1].split("/")[0]}const shapes={raw:readShapes(body),resolving:new Set};const propsMatches=body.match(/@props\s+\{[^}]+\}\s+[^\n]+/g);if(propsMatches){const schema={type:"object",properties:{},required:[]};for(const line of propsMatches){const m=line.match(/@props\s+\{([^}]+)\}\s+(?:\[([^=\]]+)(?:=[^\]]+)?\]|([\w-]+(?:\.[\w-]+)*))\s*-\s*(.+)/);if(!m)continue;const[,type,optName,reqName,desc]=m;const propName=(optName||reqName).split(".").pop();schema.properties[propName]={...toSchemaFragment(type,shapes),description:desc.trim()};if(!optName)schema.required.push(propName)}if(Object.keys(schema.properties).length>0){if(schema.required.length===0)delete schema.required;meta.props_schema=schema}}const examples=[];let cursor=0;while(cursor<body.length){const tagRe=/(^|\n)\s*@example\b/g;tagRe.lastIndex=cursor;const tagMatch=tagRe.exec(body);if(!tagMatch)break;const tagIdx=tagMatch.index+tagMatch[0].length-"@example".length;const braceIdx=body.indexOf("{",tagIdx);if(braceIdx===-1)break;const end=findBalancedEnd(body,braceIdx);if(end===-1)break;const raw=body.slice(braceIdx,end+1).replace(/\s+/g," ").trim();try{examples.push(JSON.parse(raw))}catch{}cursor=end+1}if(examples.length>0)meta.usage_examples=examples;const depsMatch=body.match(/@dependencies\s+(\[[\s\S]*?\])/);if(depsMatch){try{meta.dependencies=JSON.parse(depsMatch[1])}catch{}}const useCasesMatch=body.match(/@useCases\s+(\[[\s\S]*?\])/);if(useCasesMatch){try{meta.use_cases=JSON.parse(useCasesMatch[1])}catch{}}const dataDecl={};for(const m of body.matchAll(/@data\s+(\w+)(\??)\s+\{([^}]+)\}\s*-\s*via\s+([\w.]+)/g)){const[,name,optional,type,registry]=m;dataDecl[name]={registry,type:type.trim(),required:optional!=="?"}}const actionDecl={};for(const m of body.matchAll(/@action\s+(\w+)(\??)\s+\{([^}]+?)\s*(?:→|->)\s*([^}]+?)\}\s*-\s*via\s+([\w.]+)/g)){const[,name,optional,input,output,registry]=m;actionDecl[name]={registry,input:input.trim(),output:output.trim(),required:optional!=="?"}}const printPolicy={};for(const m of body.matchAll(/@printPolicy\s+([^\n*]+)/g)){for(const token of m[1].trim().split(/\s+/)){if(!token)continue;const eq=token.indexOf("=");if(eq===-1)printPolicy[token]=true;else printPolicy[token.slice(0,eq)]=token.slice(eq+1)}}if(Object.keys(printPolicy).length>0)meta.print_policy=printPolicy;const one=tag=>body.match(new RegExp(`(?:^|\\n)\\s*@${tag}\\s+([^\\n]+)`))?.[1]?.trim();const all=tag=>[...body.matchAll(new RegExp(`(?:^|\\n)\\s*@${tag}\\s+([^\\n]+)`,"g"))].map(m=>m[1].trim());const names=raw=>{if(raw.startsWith("[")){try{return JSON.parse(raw).map(String)}catch{return[]}}return raw.split(",").map(x=>x.trim()).filter(Boolean)};const summary=one("summary");if(summary)meta.summary=summary.replace(/\s+/g," ");const kit=one("kit");if(kit)meta.kit=kit;const tags=one("tags");if(tags)meta.tags=names(tags);const pageCell=one("pageCell");if(pageCell!==void 0)meta.page_cell=pageCell!=="false";const links=[];for(const raw of all("instead")){const m=raw.match(/^(\w+)\s*(?:-\s*(.+))?$/);if(m)links.push({relation:"instead_of",to:m[1],note:m[2]?.trim()??null})}for(const[tag,relation]of[["parent","child_of"],["child","parent_of"]]){for(const raw of all(tag))for(const name of names(raw))links.push({relation,to:name,note:null})}for(const raw of all("related"))for(const name of names(raw))links.push({relation:"related",to:name,note:null});if(links.length>0)meta.links=links;if(/(?:^|\n)\s*@size\b/.test(body))meta.size=true;if(Object.keys(dataDecl).length>0||Object.keys(actionDecl).length>0){meta.props_schema=meta.props_schema??{type:"object",properties:{}};meta.props_schema.contract_version=meta.props_schema.contract_version??"1.0";if(Object.keys(dataDecl).length>0)meta.props_schema.data=dataDecl;if(Object.keys(actionDecl).length>0)meta.props_schema.actions=actionDecl}return meta}function collectReferencedKeys(specs){const queries=new Set;const mutations=new Set;for(const spec of specs){const data=spec.props_schema?.data??{};const actions=spec.props_schema?.actions??{};for(const decl of Object.values(data)){if(decl?.registry)queries.add(decl.registry)}for(const decl of Object.values(actions)){if(decl?.registry)mutations.add(decl.registry)}}return{queries,mutations}}function projectContractsManifest(specs){const out={contract_version:"1.0",components:{}};for(const spec of specs){const ps=spec.props_schema;const pp=spec.print_policy;if(!pp&&(!ps||!ps.data&&!ps.actions))continue;out.components[spec.component_name]={contract_version:ps?.contract_version??"1.0",...ps?.data?{data:ps.data}:{},...ps?.actions?{actions:ps.actions}:{},...pp?{print_policy:pp}:{}}}return out}function toDisplayName(pascal){return pascal.replace(/([A-Z])/g," $1").replace(/^./,s=>s.toUpperCase()).trim()}function inferComponentType(name){const n=name.toLowerCase();if(n.includes("chart")||n.includes("graph"))return"chart";if(n.includes("simulator")||n.includes("slider")||n.includes("interactive"))return"interactive";if(n.includes("header")||n.includes("layout")||n.includes("container"))return"layout";if(n.includes("card")||n.includes("tile")||n.includes("grid")||n.includes("strip")||n.includes("box"))return"display";return"ui"}function readExportsFromIndex(repoRoot,entryRelative="src/index.ts"){const indexPath=resolve(repoRoot,entryRelative);const indexSource=readFileSync(indexPath,"utf8");const defaultAsPattern=/export\s*\{\s*default\s+as\s+([A-Z][A-Za-z0-9_]*)\s*\}\s*from\s*['"]([^'"]+)['"]/g;const namedExportPattern=/export\s*\{\s*([^}]+?)\s*\}\s*from\s*['"]([^'"]+)['"]/g;const out=[];for(const match of indexSource.matchAll(defaultAsPattern)){out.push({name:match[1],source:match[2]})}for(const match of indexSource.matchAll(namedExportPattern)){if(/\bdefault\s+as\b/.test(match[1]))continue;const source=match[2];for(const raw of match[1].split(",")){const trimmed=raw.trim();if(!trimmed)continue;const aliasMatch=trimmed.match(/^([A-Za-z0-9_]+)\s+as\s+([A-Za-z0-9_]+)$/);const name=aliasMatch?aliasMatch[2]:trimmed;if(!/^[A-Z][A-Za-z0-9_]*$/.test(name))continue;if(out.some(e=>e.name===name))continue;out.push({name,source})}}return out}function escapeRegExp(literal){return literal.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function findBalancedEnd(text,openIdx){let depth=0;let i=openIdx;let inString=null;while(i<text.length){const ch=text[i];if(inString){if(ch==="\\")i++;else if(ch===inString)inString=null}else if(ch==='"'||ch==="'"){inString=ch}else if(ch==="{"||ch==="["){depth++}else if(ch==="}"||ch==="]"){depth--;if(depth===0)return i}i++}return-1}const TYPE_ALIASES={ValueSpec:{type:"object",properties:{value:{type:"number"},format:{type:"string"},precision:{type:"number"},signed:{type:"boolean"},unit:{type:"string"}},required:["value","format"]}};TYPE_ALIASES.FormattableValue={oneOf:[{type:"string"},TYPE_ALIASES.ValueSpec]};TYPE_ALIASES.ReactNode={};TYPE_ALIASES.Point={type:"object",properties:{x:{oneOf:[{type:"string"},{type:"number"}]},y:{type:"number"}},required:["x","y"]};TYPE_ALIASES.Series={type:"object",properties:{id:{type:"string"},label:{type:"string"},color:{type:"string"},strokeDasharray:{type:"string"},data:{type:"array",items:TYPE_ALIASES.Point}},required:["id","data"]};function expandTypeAlias(raw){const key=String(raw).trim();return Object.prototype.hasOwnProperty.call(TYPE_ALIASES,key)?structuredClone(TYPE_ALIASES[key]):null}function toSchemaFragment(raw,shapes){const token=String(raw).trim();const alias=expandTypeAlias(token);if(alias)return alias;const members=splitTopLevel(token,"|");if(members.length>1){const literals=members.map(m=>m.match(/^(['"])(.*)\1$/)?.[2]);if(literals.every(l=>l!==void 0))return{type:"string",enum:literals};return{oneOf:members.map(m=>toSchemaFragment(m,shapes))}}const element=token.endsWith("[]")?token.slice(0,-2).replace(/^\((.*)\)$/,"$1"):token.match(/^(?:Readonly)?Array<(.+)>$/)?.[1];if(element!==void 0){const items=toSchemaFragment(element,shapes);return"x-type"in items?{type:"array"}:{type:"array",items}}if(token.startsWith("{")&&token.endsWith("}"))return objectLiteral(token,shapes);const shape=shapes&&Object.prototype.hasOwnProperty.call(shapes.raw,token)?resolveShape(token,shapes):null;if(shape)return shape;const literal=token.match(/^(['"])(.*)\1$/);if(literal)return{type:"string",enum:[literal[2]]};if(/^\[.*\]$/.test(token))return{type:"array"};if(token==="null")return{type:"null"};if(/^(Readonly)?Record<.*>$/.test(token))return{type:"object"};const type=normalizeType(token);return["boolean","number","string","array","object"].includes(type)?{type}:{"x-type":token}}function readShapes(body){const raw={};for(const m of body.matchAll(/@shape\s+([A-Z]\w*)\s*(?=\{)/g)){const open=m.index+m[0].length;const end=findBalancedEnd(body,open);if(end>open)raw[m[1]]=body.slice(open,end+1).replace(/\s+/g," ")}return raw}function resolveShape(name,shapes){if(shapes.resolving.has(name))return{type:"object"};shapes.resolving.add(name);try{return toSchemaFragment(shapes.raw[name],shapes)}finally{shapes.resolving.delete(name)}}function objectLiteral(token,shapes){const schema={type:"object",properties:{}};const required=[];for(const field of splitTopLevel(token.slice(1,-1),",")){if(field==="..."){schema.additionalProperties=true;continue}const m=field.match(/^(['"]?)([\w$-]+)\1(\?)?\s*:\s*([\s\S]+)$/);if(!m)continue;schema.properties[m[2]]=toSchemaFragment(m[4],shapes);if(!m[3])required.push(m[2])}if(required.length)schema.required=required;return schema}function splitTopLevel(text,sep){const parts=[];let depth=0;let quote=null;let start=0;for(let i=0;i<text.length;i++){const ch=text[i];if(quote){if(ch===quote)quote=null}else if(ch==='"'||ch==="'")quote=ch;else if("[{(<".includes(ch))depth++;else if("]})>".includes(ch))depth--;else if(ch===sep&&depth===0){parts.push(text.slice(start,i).trim());start=i+1}}parts.push(text.slice(start).trim());return parts.filter(Boolean)}function normalizeType(raw){const t=raw.toLowerCase().trim();if(t==="boolean"||t==="bool")return"boolean";if(t==="number"||t==="integer"||t==="int"||t==="float")return"number";if(t==="string")return"string";if(t.endsWith("[]")||t.startsWith("array"))return"array";if(t.startsWith("{")||t==="object")return"object";return raw.trim()}export{collectReferencedKeys,expandTypeAlias,hasComponentJSDoc,inferComponentType,parseJSDocBlock,parseJSDocFile,projectContractsManifest,readExportsFromIndex,resolveComponentFilePath,toDisplayName,toRepoRelative,toSchemaFragment};
|
|
1
|
+
import{readFileSync,existsSync}from"node:fs";import{resolve}from"node:path";function resolveComponentFilePath(repoRoot,relative){const base=resolve(repoRoot,"src",relative);const dirBasename=base.split("/").pop();const candidates=[`${base}.ts`,`${base}.tsx`,`${base}/${dirBasename}.tsx`,`${base}/${dirBasename}.ts`,`${base}/index.ts`,`${base}/index.tsx`];return candidates.find(p=>existsSync(p))??null}function toRepoRelative(repoRoot,absPath){const srcRoot=resolve(repoRoot,"src")+"/";return absPath.startsWith(srcRoot)?absPath.slice(srcRoot.length):absPath}function parseJSDocFile(filePath,componentName){try{const content=readFileSync(filePath,"utf8");return parseJSDocBlock(content,componentName)}catch{return{}}}function hasComponentJSDoc(filePath,componentName){if(!filePath)return false;let content;try{content=readFileSync(filePath,"utf8")}catch{return false}const blocks=content.match(/\/\*\*[\s\S]*?\*\//g);if(!blocks)return false;return blocks.some(b=>componentName?new RegExp(`@component\\s+${escapeRegExp(componentName)}\\b`).test(b):b.includes("@component"))}function parseJSDocBlock(fileContent,componentName){const blocks=fileContent.match(/\/\*\*[\s\S]*?\*\//g);if(!blocks)return{};const componentBlock=componentName?blocks.find(b=>new RegExp(`@component\\s+${escapeRegExp(componentName)}\\b`).test(b))??blocks.find(b=>b.includes("@component")):blocks.find(b=>b.includes("@component"));if(!componentBlock)return{};const body=componentBlock.replace(/\/\*\*|\*\//g,"").split("\n").map(line=>line.replace(/^\s*\*\s?/,"")).join("\n");const meta={};const descMatch=body.match(/@description\s+([\s\S]+?)(?=\n\s*@|$)/);if(descMatch){meta.description=descMatch[1].replace(/\s+/g," ").trim()}const agentInstructionsMatch=body.match(/@agentInstructions\s+([\s\S]+?)(?=\n\s*@\w+|$)/);if(agentInstructionsMatch){meta.agent_instructions=agentInstructionsMatch[1].trim()}const categoryMatch=body.match(/@category\s+([\w-]+(?:\/[\w-]+)*)/);if(categoryMatch){meta.family=categoryMatch[1];meta.component_type=categoryMatch[1].split("/")[0]}const shapes={raw:readShapes(body),resolving:new Set};const propsMatches=body.match(/@props\s+\{[^}]+\}\s+[^\n]+/g);if(propsMatches){const schema={type:"object",properties:{},required:[]};for(const line of propsMatches){const m=line.match(/@props\s+\{([^}]+)\}\s+(?:\[([^=\]]+)(?:=(?:\[[^\]]*\]|[^\]])+)?\]|([\w-]+(?:\.[\w-]+)*))\s*-\s*(.+)/);if(!m)continue;const[,type,optName,reqName,desc]=m;const propName=(optName||reqName).split(".").pop();schema.properties[propName]={...toSchemaFragment(type,shapes),description:desc.trim()};if(!optName)schema.required.push(propName)}if(Object.keys(schema.properties).length>0){if(schema.required.length===0)delete schema.required;meta.props_schema=schema}}const slots=parseSlots(body);if(slots.length){const schema=meta.props_schema??{type:"object",properties:{}};for(const slot of slots){const{required,description,...decl}=slot;schema.properties[slot.name]={...schema.properties[slot.name]??{},...description?{description}:{},"x-slot":decl};if(required)schema.required=[...new Set([...schema.required??[],slot.name])]}meta.props_schema=schema}const examples=[];let cursor=0;while(cursor<body.length){const tagRe=/(^|\n)\s*@example\b/g;tagRe.lastIndex=cursor;const tagMatch=tagRe.exec(body);if(!tagMatch)break;const tagIdx=tagMatch.index+tagMatch[0].length-"@example".length;const braceIdx=body.indexOf("{",tagIdx);if(braceIdx===-1)break;const end=findBalancedEnd(body,braceIdx);if(end===-1)break;const raw=body.slice(braceIdx,end+1).replace(/\s+/g," ").trim();try{examples.push(JSON.parse(raw))}catch{}cursor=end+1}if(examples.length>0)meta.usage_examples=examples;const depsMatch=body.match(/@dependencies\s+(\[[\s\S]*?\])/);if(depsMatch){try{meta.dependencies=JSON.parse(depsMatch[1])}catch{}}const useCasesMatch=body.match(/@useCases\s+(\[[\s\S]*?\])/);if(useCasesMatch){try{meta.use_cases=JSON.parse(useCasesMatch[1])}catch{}}const dataDecl={};for(const m of body.matchAll(/@data\s+(\w+)(\??)\s+\{([^}]+)\}\s*-\s*via\s+([\w.]+)/g)){const[,name,optional,type,registry]=m;dataDecl[name]={registry,type:type.trim(),required:optional!=="?"}}const actionDecl={};for(const m of body.matchAll(/@action\s+(\w+)(\??)\s+\{([^}]+?)\s*(?:→|->)\s*([^}]+?)\}\s*-\s*via\s+([\w.]+)/g)){const[,name,optional,input,output,registry]=m;actionDecl[name]={registry,input:input.trim(),output:output.trim(),required:optional!=="?"}}const printPolicy={};for(const m of body.matchAll(/@printPolicy\s+([^\n*]+)/g)){for(const token of m[1].trim().split(/\s+/)){if(!token)continue;const eq=token.indexOf("=");if(eq===-1)printPolicy[token]=true;else printPolicy[token.slice(0,eq)]=token.slice(eq+1)}}if(Object.keys(printPolicy).length>0)meta.print_policy=printPolicy;const one=tag=>body.match(new RegExp(`(?:^|\\n)\\s*@${tag}\\s+([^\\n]+)`))?.[1]?.trim();const all=tag=>[...body.matchAll(new RegExp(`(?:^|\\n)\\s*@${tag}\\s+([^\\n]+)`,"g"))].map(m=>m[1].trim());const names=raw=>{if(raw.startsWith("[")){try{return JSON.parse(raw).map(String)}catch{return[]}}return raw.split(",").map(x=>x.trim()).filter(Boolean)};const summary=one("summary");if(summary)meta.summary=summary.replace(/\s+/g," ");const kit=one("kit");if(kit)meta.kit=kit;const tags=one("tags");if(tags)meta.tags=names(tags);const pageCell=one("pageCell");if(pageCell!==void 0)meta.page_cell=pageCell!=="false";const links=[];for(const raw of all("instead")){const m=raw.match(/^(\w+)\s*(?:-\s*(.+))?$/);if(m)links.push({relation:"instead_of",to:m[1],note:m[2]?.trim()??null})}for(const[tag,relation]of[["parent","child_of"],["child","parent_of"]]){for(const raw of all(tag))for(const name of names(raw))links.push({relation,to:name,note:null})}for(const raw of all("related"))for(const name of names(raw))links.push({relation:"related",to:name,note:null});if(links.length>0)meta.links=links;if(/(?:^|\n)\s*@size\b/.test(body))meta.size=true;if(Object.keys(dataDecl).length>0||Object.keys(actionDecl).length>0){meta.props_schema=meta.props_schema??{type:"object",properties:{}};meta.props_schema.contract_version=meta.props_schema.contract_version??"1.0";if(Object.keys(dataDecl).length>0)meta.props_schema.data=dataDecl;if(Object.keys(actionDecl).length>0)meta.props_schema.actions=actionDecl}return meta}function collectReferencedKeys(specs){const queries=new Set;const mutations=new Set;for(const spec of specs){const data=spec.props_schema?.data??{};const actions=spec.props_schema?.actions??{};for(const decl of Object.values(data)){if(decl?.registry)queries.add(decl.registry)}for(const decl of Object.values(actions)){if(decl?.registry)mutations.add(decl.registry)}}return{queries,mutations}}function projectContractsManifest(specs){const out={contract_version:"1.0",components:{}};for(const spec of specs){const ps=spec.props_schema;const pp=spec.print_policy;if(!pp&&(!ps||!ps.data&&!ps.actions))continue;out.components[spec.component_name]={contract_version:ps?.contract_version??"1.0",...ps?.data?{data:ps.data}:{},...ps?.actions?{actions:ps.actions}:{},...pp?{print_policy:pp}:{}}}return out}function toDisplayName(pascal){return pascal.replace(/([A-Z])/g," $1").replace(/^./,s=>s.toUpperCase()).trim()}function inferComponentType(name){const n=name.toLowerCase();if(n.includes("chart")||n.includes("graph"))return"chart";if(n.includes("simulator")||n.includes("slider")||n.includes("interactive"))return"interactive";if(n.includes("header")||n.includes("layout")||n.includes("container"))return"layout";if(n.includes("card")||n.includes("tile")||n.includes("grid")||n.includes("strip")||n.includes("box"))return"display";return"ui"}function readExportsFromIndex(repoRoot,entryRelative="src/index.ts"){const indexPath=resolve(repoRoot,entryRelative);const indexSource=readFileSync(indexPath,"utf8");const defaultAsPattern=/export\s*\{\s*default\s+as\s+([A-Z][A-Za-z0-9_]*)\s*\}\s*from\s*['"]([^'"]+)['"]/g;const namedExportPattern=/export\s*\{\s*([^}]+?)\s*\}\s*from\s*['"]([^'"]+)['"]/g;const out=[];for(const match of indexSource.matchAll(defaultAsPattern)){out.push({name:match[1],source:match[2]})}for(const match of indexSource.matchAll(namedExportPattern)){if(/\bdefault\s+as\b/.test(match[1]))continue;const source=match[2];for(const raw of match[1].split(",")){const trimmed=raw.trim();if(!trimmed)continue;const aliasMatch=trimmed.match(/^([A-Za-z0-9_]+)\s+as\s+([A-Za-z0-9_]+)$/);const name=aliasMatch?aliasMatch[2]:trimmed;if(!/^[A-Z][A-Za-z0-9_]*$/.test(name))continue;if(out.some(e=>e.name===name))continue;out.push({name,source})}}return out}function escapeRegExp(literal){return literal.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function findBalancedEnd(text,openIdx){let depth=0;let i=openIdx;let inString=null;while(i<text.length){const ch=text[i];if(inString){if(ch==="\\")i++;else if(ch===inString)inString=null}else if(ch==='"'||ch==="'"){inString=ch}else if(ch==="{"||ch==="["){depth++}else if(ch==="}"||ch==="]"){depth--;if(depth===0)return i}i++}return-1}const TYPE_ALIASES={ValueSpec:{type:"object",properties:{value:{type:"number"},format:{type:"string"},precision:{type:"number"},signed:{type:"boolean"},unit:{type:"string"}},required:["value","format"]}};TYPE_ALIASES.FormattableValue={oneOf:[{type:"string"},TYPE_ALIASES.ValueSpec]};TYPE_ALIASES.ReactNode={};TYPE_ALIASES.Point={type:"object",properties:{x:{oneOf:[{type:"string"},{type:"number"}]},y:{type:"number"}},required:["x","y"]};TYPE_ALIASES.Series={type:"object",properties:{id:{type:"string"},label:{type:"string"},color:{type:"string"},strokeDasharray:{type:"string"},data:{type:"array",items:TYPE_ALIASES.Point}},required:["id","data"]};function expandTypeAlias(raw){const key=String(raw).trim();return Object.prototype.hasOwnProperty.call(TYPE_ALIASES,key)?structuredClone(TYPE_ALIASES[key]):null}function toSchemaFragment(raw,shapes){const token=String(raw).trim();const alias=expandTypeAlias(token);if(alias)return alias;const members=splitTopLevel(token,"|");if(members.length>1){const literals=members.map(m=>m.match(/^(['"])(.*)\1$/)?.[2]);if(literals.every(l=>l!==void 0))return{type:"string",enum:literals};return{oneOf:members.map(m=>toSchemaFragment(m,shapes))}}const element=token.endsWith("[]")?token.slice(0,-2).replace(/^\((.*)\)$/,"$1"):token.match(/^(?:Readonly)?Array<(.+)>$/)?.[1];if(element!==void 0){const items=toSchemaFragment(element,shapes);return"x-type"in items?{type:"array"}:{type:"array",items}}if(token.startsWith("{")&&token.endsWith("}"))return objectLiteral(token,shapes);const shape=shapes&&Object.prototype.hasOwnProperty.call(shapes.raw,token)?resolveShape(token,shapes):null;if(shape)return shape;const literal=token.match(/^(['"])(.*)\1$/);if(literal)return{type:"string",enum:[literal[2]]};if(/^\[.*\]$/.test(token))return{type:"array"};if(token==="null")return{type:"null"};if(/^(Readonly)?Record<.*>$/.test(token))return{type:"object"};const type=normalizeType(token);return["boolean","number","string","array","object"].includes(type)?{type}:{"x-type":token}}function readShapes(body){const raw={};for(const m of body.matchAll(/@shape\s+([A-Z]\w*)\s*(?=\{)/g)){const open=m.index+m[0].length;const end=findBalancedEnd(body,open);if(end>open)raw[m[1]]=body.slice(open,end+1).replace(/\s+/g," ")}return raw}function resolveShape(name,shapes){if(shapes.resolving.has(name))return{type:"object"};shapes.resolving.add(name);try{return toSchemaFragment(shapes.raw[name],shapes)}finally{shapes.resolving.delete(name)}}function objectLiteral(token,shapes){const schema={type:"object",properties:{}};const required=[];for(const field of splitTopLevel(token.slice(1,-1),",")){if(field==="..."){schema.additionalProperties=true;continue}const m=field.match(/^(['"]?)([\w$-]+)\1(\?)?\s*:\s*([\s\S]+)$/);if(!m)continue;schema.properties[m[2]]=toSchemaFragment(m[4],shapes);if(!m[3])required.push(m[2])}if(required.length)schema.required=required;return schema}function splitTopLevel(text,sep){const parts=[];let depth=0;let quote=null;let start=0;for(let i=0;i<text.length;i++){const ch=text[i];if(quote){if(ch===quote)quote=null}else if(ch==='"'||ch==="'")quote=ch;else if("[{(<".includes(ch))depth++;else if("]})>".includes(ch))depth--;else if(ch===sep&&depth===0){parts.push(text.slice(start,i).trim());start=i+1}}parts.push(text.slice(start).trim());return parts.filter(Boolean)}function normalizeType(raw){const t=raw.toLowerCase().trim();if(t==="boolean"||t==="bool")return"boolean";if(t==="number"||t==="integer"||t==="int"||t==="float")return"number";if(t==="string")return"string";if(t.endsWith("[]")||t.startsWith("array"))return"array";if(t.startsWith("{")||t==="object")return"object";return raw.trim()}function parseSlots(body){const out=[];for(const m of body.matchAll(/(?:^|\n)\s*\*?\s*@slot\s+\{?([A-Za-z_][\w-]*)\}?([^\n]*)/g)){const[,name,rest]=m;const[opts,...descParts]=rest.split(/\s-\s/);const slot={name};const accepts=/\[?accepts=([^\]\s]+)\]?/.exec(opts)?.[1];if(accepts)slot.accepts=accepts.split(",").map(a=>a.trim()).filter(Boolean);const max=/\[?max=(\d+)\]?/.exec(opts)?.[1];if(max)slot.max=Number(max);if(/\brequired\b/.test(opts))slot.required=true;const description=descParts.join(" - ").trim();if(description)slot.description=description;out.push(slot)}return out}export{collectReferencedKeys,expandTypeAlias,hasComponentJSDoc,inferComponentType,parseJSDocBlock,parseJSDocFile,parseSlots,projectContractsManifest,readExportsFromIndex,resolveComponentFilePath,toDisplayName,toRepoRelative,toSchemaFragment};
|