@peripheries/engine 0.16.0 → 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/README.md CHANGED
@@ -18,5 +18,24 @@ engine upload | register | cutover | knowledge | contracts
18
18
  engine bundle-size | check-bundle-leak | validate-externals
19
19
  ```
20
20
 
21
+ ## Static assets
22
+
23
+ Images and fonts a lineage's components import (`import url from
24
+ './photo.jpg'`, `new URL('./photo.jpg', import.meta.url)`, CSS
25
+ `url(./font.woff2)`) ship as files, not data: URIs. `engine build` emits them
26
+ into the lineage's `assets/` with content-hashed names, referenced relative to
27
+ `index.js` and `index.css`, and `engine upload` uploads them beside
28
+ `index.js`.
29
+
30
+ Only these may ship, or the build fails naming the file: `png`, `jpg`/`jpeg`,
31
+ `webp`, `avif`, `gif`, `svg` (without `<script>`, `on*=` handlers,
32
+ `javascript:` URLs or `<foreignObject>`), `woff2`, `woff`, `ttf`, `otf`; 1 MB
33
+ per asset and 8 MB per lineage unless `assets: { maxAssetBytes, maxTotalBytes }`
34
+ in the config says otherwise.
35
+
36
+ SVG assets aren't allowed in public or shared lineages, which upload to
37
+ bundles-public: that bucket refuses `image/svg+xml`. Use PNG or WebP there,
38
+ or inline the SVG in a component. Private lineages may ship a clean SVG.
39
+
21
40
  Credentials are read from the environment of the repo that runs it; the
22
41
  package holds none.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peripheries/engine",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Build, check, knowledge and deploy for peripheries component repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import{spawnSync}from"node:child_process";import{existsSync,readdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{createRequire}from"node:module";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{assetAliases,assetFileNames,assetsPlugin}from"../lib/assets.mjs";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue}from"../lib/env.mjs";import{layerCss,layerFor}from"../lib/layers.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{runCommand}from"../lib/run.mjs";import{EXTERNALS}from"./externals.mjs";const argv=process.argv.slice(2);const only=parseFlagValue(argv,"lineage");const config=await loadConfig();const lineages=only?config.lineages.filter(l=>l.name===only):config.lineages;if(only&&lineages.length===0)throw new Error(`[build] --lineage ${only} is not in the config's lineages`);const fromRepo=createRequire(resolve(REPO_ROOT,"package.json"));const importFromRepo=name=>import(pathToFileURL(fromRepo.resolve(name)).href);function check(label,status){if(status!==0){console.error(`[build] ${label} failed (exit ${status}).`);process.exit(status??1)}}if(!argv.includes("--no-typecheck")&&existsSync(resolve(REPO_ROOT,"tsconfig.build.json"))){console.log("[build] typecheck: tsc -p tsconfig.build.json --noEmit");const tsc=fromRepo.resolve("typescript/bin/tsc");check("typecheck",spawnSync(process.execPath,[tsc,"-p","tsconfig.build.json","--noEmit"],{cwd:REPO_ROOT,stdio:"inherit"}).status)}process.env.NODE_ENV="production";const{build}=await importFromRepo("vite");const react=(await importFromRepo("@vitejs/plugin-react")).default;for(const lineage of lineages){const entry=resolve(REPO_ROOT,lineage.entry);if(!existsSync(entry))throw new Error(`[build] no barrel at ${lineage.entry} for lineage "${lineage.name}"`);const external=[...new Set([...EXTERNALS,...config.shared,...lineage.shared??[]])].filter(s=>s!==lineage.specifier);console.log(`
3
- [build] ${lineage.name}: ${lineage.entry} \u2192 ${lineage.dist}/index.js`);await build({configFile:false,mode:"production",root:REPO_ROOT,base:"./",logLevel:"warn",plugins:[assetsPlugin({lineage:lineage.name,limits:config.assets}),react()],define:{"process.env.NODE_ENV":JSON.stringify("production"),__OA_BUNDLE_NAME__:JSON.stringify(config.org)},resolve:{alias:assetAliases(resolve(REPO_ROOT,"src"))},optimizeDeps:{exclude:external},build:{lib:{entry,formats:["es"],fileName:()=>"index.js"},rollupOptions:{external,output:{assetFileNames}},outDir:resolve(REPO_ROOT,lineage.dist),emptyOutDir:true,sourcemap:true}});await writeLayeredCss(lineage);check(`${lineage.name} contracts`,runCommand("contracts",["--entry",lineage.entry,"--out",`${lineage.dist}/contracts.json`]));check(`${lineage.name} knowledge`,runCommand("knowledge",["--bundle",lineage.name]))}console.log(`
2
+ import{spawnSync}from"node:child_process";import{existsSync,readdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{createRequire}from"node:module";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{assetAliases,assetFileNames,assetsPlugin}from"../lib/assets.mjs";import{loadConfig}from"../lib/config.mjs";import{bucketForLineage,parseFlagValue}from"../lib/env.mjs";import{layerCss,layerFor}from"../lib/layers.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{runCommand}from"../lib/run.mjs";import{EXTERNALS}from"./externals.mjs";const argv=process.argv.slice(2);const only=parseFlagValue(argv,"lineage");const config=await loadConfig();const lineages=only?config.lineages.filter(l=>l.name===only):config.lineages;if(only&&lineages.length===0)throw new Error(`[build] --lineage ${only} is not in the config's lineages`);const fromRepo=createRequire(resolve(REPO_ROOT,"package.json"));const importFromRepo=name=>import(pathToFileURL(fromRepo.resolve(name)).href);function check(label,status){if(status!==0){console.error(`[build] ${label} failed (exit ${status}).`);process.exit(status??1)}}if(!argv.includes("--no-typecheck")&&existsSync(resolve(REPO_ROOT,"tsconfig.build.json"))){console.log("[build] typecheck: tsc -p tsconfig.build.json --noEmit");const tsc=fromRepo.resolve("typescript/bin/tsc");check("typecheck",spawnSync(process.execPath,[tsc,"-p","tsconfig.build.json","--noEmit"],{cwd:REPO_ROOT,stdio:"inherit"}).status)}process.env.NODE_ENV="production";const{build}=await importFromRepo("vite");const react=(await importFromRepo("@vitejs/plugin-react")).default;for(const lineage of lineages){const entry=resolve(REPO_ROOT,lineage.entry);if(!existsSync(entry))throw new Error(`[build] no barrel at ${lineage.entry} for lineage "${lineage.name}"`);const external=[...new Set([...EXTERNALS,...config.shared,...lineage.shared??[]])].filter(s=>s!==lineage.specifier);console.log(`
3
+ [build] ${lineage.name}: ${lineage.entry} \u2192 ${lineage.dist}/index.js`);await build({configFile:false,mode:"production",root:REPO_ROOT,base:"./",logLevel:"warn",plugins:[assetsPlugin({lineage:lineage.name,bucket:bucketForLineage(lineage),limits:config.assets}),react()],define:{"process.env.NODE_ENV":JSON.stringify("production"),__OA_BUNDLE_NAME__:JSON.stringify(config.org)},resolve:{alias:assetAliases(resolve(REPO_ROOT,"src"))},optimizeDeps:{exclude:external},build:{lib:{entry,formats:["es"],fileName:()=>"index.js"},rollupOptions:{external,output:{assetFileNames}},outDir:resolve(REPO_ROOT,lineage.dist),emptyOutDir:true,sourcemap:true}});await writeLayeredCss(lineage);check(`${lineage.name} contracts`,runCommand("contracts",["--entry",lineage.entry,"--out",`${lineage.dist}/contracts.json`]));check(`${lineage.name} knowledge`,runCommand("knowledge",["--bundle",lineage.name]))}console.log(`
4
4
  [build] \u2705 ${lineages.map(l=>l.name).join(", ")}`);async function writeLayeredCss(lineage){const dist=resolve(REPO_ROOT,lineage.dist);const files=readdirSync(dist).filter(f=>f.endsWith(".css"));if(files.length===0)return;const css=files.map(f=>readFileSync(resolve(dist,f),"utf8")).join("\n");for(const f of files)rmSync(resolve(dist,f));const postcss=(await importFromRepo("postcss")).default;const layer=layerFor(lineage);writeFileSync(resolve(dist,"index.css"),layerCss(postcss,css,layer));console.log(`[build] ${lineage.name}: index.css in @layer ${layer}`)}
@@ -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);
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{existsSync,statSync}from"node:fs";import{resolve}from"node:path";import{bundleFiles,metadataFiles,uploadBundleFiles}from"../lib/assets.mjs";import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";import{lineageConfig}from"../lib/config.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const BUNDLE_NAME=parseFlagValue(argv,"bundle")??"main";const dryRun=argv.includes("--dry-run");const IS_MAIN=BUNDLE_NAME==="main";const IS_PUBLIC=BUNDLE_NAME==="public";const IS_ORG_WIDE=IS_MAIN||IS_PUBLIC;const IS_SHARED=(await lineageConfig(BUNDLE_NAME)).kind==="shared";const DEFAULT_BUCKET=IS_PUBLIC||IS_SHARED?PUBLIC_BUNDLE_BUCKET:BUNDLE_BUCKET;const STORAGE_BUCKET=process.env.STORAGE_BUCKET||DEFAULT_BUCKET;if(STORAGE_BUCKET!==DEFAULT_BUCKET){console.error(`[upload-bundle] STORAGE_BUCKET must be "${DEFAULT_BUCKET}" for lineage "${BUNDLE_NAME}", got "${STORAGE_BUCKET}".`);process.exit(2)}const VERSION=process.env.VERSION||`v_${target}_${new Date().toISOString().replace(/[-:T]/g,"").replace(/\..+$/,"").slice(0,14)}`;if(!process.env.VERSION){console.log(`[upload-bundle] VERSION auto-generated: ${VERSION}`);process.env.VERSION=VERSION}const distDir=resolve(REPO_ROOT_,(await lineageConfig(BUNDLE_NAME)).dist);const bundlePath=resolve(distDir,"index.js");if(!existsSync(bundlePath)){console.error(`[upload-bundle] ${bundlePath} does not exist. `+(IS_MAIN?"Did `npm run build` run?":`Did \`OA_PROJECT_BUNDLE=${BUNDLE_NAME} npm run build:bundle\` run?`));process.exit(1)}console.log(`[upload-bundle] lineage=${BUNDLE_NAME} dist=${distDir.split("/").slice(-2).join("/")}`);const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[upload-bundle] Could not resolve org_id for org_slug="${ORG_SLUG}":`,orgError??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;const remoteBase=IS_ORG_WIDE?`${ORG_SLUG}/${VERSION}`:`${ORG_SLUG}/${BUNDLE_NAME}/${VERSION}`;const files=bundleFiles(distDir);try{await uploadBundleFiles({bucket:supabase.storage.from(STORAGE_BUCKET),remoteBase,files,dryRun})}catch(e){console.error(e.message);process.exit(1)}const mapPath=resolve(distDir,"index.js.map");const{data:existingBundle,error:existingBundleError}=await supabase.from("component_bundle").select("id, status").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(existingBundleError){console.error(`[upload-bundle] Failed to look up existing component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}):`,existingBundleError);process.exit(1)}const bundleBytes=statSync(bundlePath).size;const hasSourcemap=existsSync(mapPath);const bundlePayload={org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:BUNDLE_NAME,version:VERSION,status:"uploaded",storage_bucket:STORAGE_BUCKET,storage_path:`${remoteBase}/index.js`,...IS_SHARED?{is_public:true}:{},uploaded_at:new Date().toISOString(),registered_at:null,cutover_at:null,git_sha:process.env.DEPLOY_SHA??process.env.GITHUB_SHA??null,git_ref:process.env.DEPLOY_REF??process.env.GITHUB_REF??null,git_repo:process.env.GITHUB_REPOSITORY??null,process_id:process.env.PROCESS_ID||null,metadata:{bundle_bytes:bundleBytes,has_sourcemap:hasSourcemap,bundle_scope:BUNDLE_NAME,files:metadataFiles(files),...process.env.GITHUB_RUN_ID?{github_run_id:process.env.GITHUB_RUN_ID}:{},...process.env.GITHUB_RUN_ATTEMPT?{github_run_attempt:process.env.GITHUB_RUN_ATTEMPT}:{}}};if(dryRun){console.log(`[upload-bundle] dry-run \u2014 would ${existingBundle?.id?`UPDATE ${existingBundle.id}`:"INSERT"} component_bundle:`);console.log(JSON.stringify(bundlePayload,null,2));console.log("[upload-bundle] dry-run \u2014 nothing written.")}else if(existingBundle?.id){const{error:updateError}=await supabase.from("component_bundle").update(bundlePayload).eq("id",existingBundle.id);if(updateError){console.error(`[upload-bundle] Failed to update component_bundle row:`,updateError);process.exit(1)}console.log(`[upload-bundle] ~ component_bundle ${existingBundle.id} reset to status='uploaded' (was '${existingBundle.status}').`)}else{const{data:inserted,error:insertError}=await supabase.from("component_bundle").insert(bundlePayload).select("id").single();if(insertError){console.error(`[upload-bundle] Failed to insert component_bundle row:`,insertError);process.exit(1)}console.log(`[upload-bundle] + component_bundle ${inserted.id} created (status='uploaded').`)}console.log(`[upload-bundle] Done \u2014 target=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION}. Run register-components.mjs next.`);
2
+ import{existsSync,statSync}from"node:fs";import{resolve}from"node:path";import{bundleFiles,metadataFiles,uploadBundleFiles}from"../lib/assets.mjs";import{bucketForLineage,initFromArgv,parseFlagValue}from"../lib/env.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";import{lineageConfig}from"../lib/config.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const BUNDLE_NAME=parseFlagValue(argv,"bundle")??"main";const dryRun=argv.includes("--dry-run");const IS_MAIN=BUNDLE_NAME==="main";const IS_PUBLIC=BUNDLE_NAME==="public";const IS_ORG_WIDE=IS_MAIN||IS_PUBLIC;const LINEAGE=await lineageConfig(BUNDLE_NAME);const IS_SHARED=LINEAGE.kind==="shared";const DEFAULT_BUCKET=bucketForLineage(LINEAGE);const STORAGE_BUCKET=process.env.STORAGE_BUCKET||DEFAULT_BUCKET;if(STORAGE_BUCKET!==DEFAULT_BUCKET){console.error(`[upload-bundle] STORAGE_BUCKET must be "${DEFAULT_BUCKET}" for lineage "${BUNDLE_NAME}", got "${STORAGE_BUCKET}".`);process.exit(2)}const VERSION=process.env.VERSION||`v_${target}_${new Date().toISOString().replace(/[-:T]/g,"").replace(/\..+$/,"").slice(0,14)}`;if(!process.env.VERSION){console.log(`[upload-bundle] VERSION auto-generated: ${VERSION}`);process.env.VERSION=VERSION}const distDir=resolve(REPO_ROOT_,LINEAGE.dist);const bundlePath=resolve(distDir,"index.js");if(!existsSync(bundlePath)){console.error(`[upload-bundle] ${bundlePath} does not exist. `+(IS_MAIN?"Did `npm run build` run?":`Did \`OA_PROJECT_BUNDLE=${BUNDLE_NAME} npm run build:bundle\` run?`));process.exit(1)}console.log(`[upload-bundle] lineage=${BUNDLE_NAME} dist=${distDir.split("/").slice(-2).join("/")}`);const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[upload-bundle] Could not resolve org_id for org_slug="${ORG_SLUG}":`,orgError??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;const remoteBase=IS_ORG_WIDE?`${ORG_SLUG}/${VERSION}`:`${ORG_SLUG}/${BUNDLE_NAME}/${VERSION}`;let files;try{files=bundleFiles(distDir,{bucket:STORAGE_BUCKET,lineage:BUNDLE_NAME});await uploadBundleFiles({bucket:supabase.storage.from(STORAGE_BUCKET),remoteBase,files,dryRun})}catch(e){console.error(e.message);process.exit(1)}const mapPath=resolve(distDir,"index.js.map");const{data:existingBundle,error:existingBundleError}=await supabase.from("component_bundle").select("id, status").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(existingBundleError){console.error(`[upload-bundle] Failed to look up existing component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}):`,existingBundleError);process.exit(1)}const bundleBytes=statSync(bundlePath).size;const hasSourcemap=existsSync(mapPath);const bundlePayload={org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:BUNDLE_NAME,version:VERSION,status:"uploaded",storage_bucket:STORAGE_BUCKET,storage_path:`${remoteBase}/index.js`,...IS_SHARED?{is_public:true}:{},uploaded_at:new Date().toISOString(),registered_at:null,cutover_at:null,git_sha:process.env.DEPLOY_SHA??process.env.GITHUB_SHA??null,git_ref:process.env.DEPLOY_REF??process.env.GITHUB_REF??null,git_repo:process.env.GITHUB_REPOSITORY??null,process_id:process.env.PROCESS_ID||null,metadata:{bundle_bytes:bundleBytes,has_sourcemap:hasSourcemap,bundle_scope:BUNDLE_NAME,files:metadataFiles(files),...process.env.GITHUB_RUN_ID?{github_run_id:process.env.GITHUB_RUN_ID}:{},...process.env.GITHUB_RUN_ATTEMPT?{github_run_attempt:process.env.GITHUB_RUN_ATTEMPT}:{}}};if(dryRun){console.log(`[upload-bundle] dry-run \u2014 would ${existingBundle?.id?`UPDATE ${existingBundle.id}`:"INSERT"} component_bundle:`);console.log(JSON.stringify(bundlePayload,null,2));console.log("[upload-bundle] dry-run \u2014 nothing written.")}else if(existingBundle?.id){const{error:updateError}=await supabase.from("component_bundle").update(bundlePayload).eq("id",existingBundle.id);if(updateError){console.error(`[upload-bundle] Failed to update component_bundle row:`,updateError);process.exit(1)}console.log(`[upload-bundle] ~ component_bundle ${existingBundle.id} reset to status='uploaded' (was '${existingBundle.status}').`)}else{const{data:inserted,error:insertError}=await supabase.from("component_bundle").insert(bundlePayload).select("id").single();if(insertError){console.error(`[upload-bundle] Failed to insert component_bundle row:`,insertError);process.exit(1)}console.log(`[upload-bundle] + component_bundle ${inserted.id} created (status='uploaded').`)}console.log(`[upload-bundle] Done \u2014 target=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION}. Run register-components.mjs next.`);
@@ -1,3 +1,3 @@
1
- import{existsSync,readdirSync,readFileSync,statSync}from"node:fs";import{extname,join,resolve}from"node:path";const ASSETS_DIR="assets";const ASSET_TYPES={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".avif":"image/avif",".gif":"image/gif",".svg":"image/svg+xml",".woff2":"font/woff2",".woff":"font/woff",".ttf":"font/ttf",".otf":"font/otf"};const DEFAULT_ASSET_LIMITS={maxAssetBytes:1024*1024,maxTotalBytes:8*1024*1024};const EXT_RE=Object.keys(ASSET_TYPES).map(e=>e.slice(1)).join("|");function svgProblems(text){const out=[];if(/<script\b/i.test(text))out.push("a <script> element");if(/<foreignObject\b/i.test(text))out.push("a <foreignObject> element");const handler=text.match(/<[^>]*?[\s"'/](on[a-z]+)\s*=/i);if(handler)out.push(`an event-handler attribute (${handler[1]})`);if(/javascript\s*:/i.test(text))out.push("a javascript: URL");return out}function assetProblems(name,source,limits=DEFAULT_ASSET_LIMITS){const ext=extname(name).toLowerCase();if(!ASSET_TYPES[ext]){return[`${ext||"a file without an extension"} is not an allowed asset type (images: png, jpg, jpeg, webp, avif, gif, svg; fonts: woff2, woff, ttf, otf)`]}const out=[];const bytes=typeof source==="string"?Buffer.byteLength(source):source.byteLength;if(bytes>limits.maxAssetBytes)out.push(`${formatBytes(bytes)} is over the ${formatBytes(limits.maxAssetBytes)} cap per asset (assets.maxAssetBytes)`);if(ext===".svg"){for(const p of svgProblems(typeof source==="string"?source:Buffer.from(source).toString("utf8")))out.push(`the SVG holds ${p}`)}return out}function lineageAssetProblems(assets,limits=DEFAULT_ASSET_LIMITS){const out=[];let total=0;for(const a of assets){const from=a.originalFileNames?.length?` (from ${a.originalFileNames.join(", ")})`:"";for(const p of assetProblems(a.fileName,a.source,limits))out.push(`${a.fileName}${from}: ${p}`);total+=typeof a.source==="string"?Buffer.byteLength(a.source):a.source.byteLength}if(total>limits.maxTotalBytes)out.push(`the lineage's assets total ${formatBytes(total)}, over the ${formatBytes(limits.maxTotalBytes)} cap (assets.maxTotalBytes)`);return out}function formatBytes(n){if(n>=1024*1024)return`${(n/(1024*1024)).toFixed(n%(1024*1024)?1:0)} MB`;if(n>=1024)return`${(n/1024).toFixed(n%1024?1:0)} KB`;return`${n} B`}function assetAliases(srcDir){return[{find:new RegExp(`^@/([^?#]*\\.(?:${EXT_RE}))$`,"i"),replacement:`${srcDir}/$1?no-inline`},{find:/^@\/([^?#]+)\?url$/i,replacement:`${srcDir}/$1?url&no-inline`},{find:"@",replacement:srcDir},{find:new RegExp(`^(?![a-z][a-z0-9+.-]*:|//)([^?#]+\\.(?:${EXT_RE}))$`,"i"),replacement:"$1?no-inline"},{find:/^(?![a-z][a-z0-9+.-]*:|\/\/)([^?#]+)\?url$/i,replacement:"$1?url&no-inline"}]}const META_URL_RE=new RegExp(`(new\\s+URL\\(\\s*)(['"\`])(\\.{1,2}/[^'"\`?#$]*\\.(?:${EXT_RE}))\\2(\\s*,\\s*import\\.meta\\.url\\s*\\))`,"gi");function tagImportMetaUrls(code){if(!code.includes("import.meta.url"))return null;const out=code.replace(META_URL_RE,(_,open,q,url,close)=>`${open}${q}${url}?no-inline${q}${close}`);return out===code?null:out}function assetFileNames(info){const name=info.names?.[0]??info.name??"";return name.endsWith(".css")?"[name][extname]":`${ASSETS_DIR}/[name]-[hash][extname]`}function assetsPlugin({lineage,limits=DEFAULT_ASSET_LIMITS}){return{name:"engine:assets",enforce:"pre",transform(code,id){const file=id.split("?")[0];if(file.includes("/node_modules/")||!/\.[cm]?[jt]sx?$/.test(file))return null;const out=tagImportMetaUrls(code);return out===null?null:{code:out,map:null}},generateBundle(_,bundle){const assets=Object.values(bundle).filter(o=>o.type==="asset"&&o.fileName.startsWith(`${ASSETS_DIR}/`));const problems=lineageAssetProblems(assets,limits);if(problems.length){this.error(`[build] ${lineage}: ${problems.length} asset problem(s):
2
- \xB7 ${problems.join("\n \xB7 ")}`)}}}}function bundleFiles(distDir){const files=[{local:resolve(distDir,"index.js"),remote:"index.js",contentType:"application/javascript",listed:true}];const optional=[["index.css","text/css",true],["index.js.map","application/json",false],["contracts.json","application/json",false]];for(const[name,contentType,listed]of optional){const local=resolve(distDir,name);if(existsSync(local))files.push({local,remote:name,contentType,listed})}const assetsDir=resolve(distDir,ASSETS_DIR);if(existsSync(assetsDir)){for(const name of readdirSync(assetsDir).sort()){const local=join(assetsDir,name);if(!statSync(local).isFile())continue;const contentType=ASSET_TYPES[extname(name).toLowerCase()];if(!contentType)throw new Error(`[upload-bundle] ${ASSETS_DIR}/${name} is not an allowed asset type; rebuild with \`engine build\``);files.push({local,remote:`${ASSETS_DIR}/${name}`,contentType,listed:true})}}return files}function metadataFiles(files){return files.filter(f=>f.listed).map(f=>f.remote)}async function uploadBundleFiles({bucket,remoteBase,files,dryRun=false,log=console.log}){for(const f of files){const body=readFileSync(f.local);const path=`${remoteBase}/${f.remote}`;if(dryRun){log(`[upload-bundle] would upload ${path} (${body.byteLength} bytes, ${f.contentType})`);continue}const{error}=await bucket.upload(path,body,{contentType:f.contentType,upsert:false});if(error)throw new Error(`[upload-bundle] Failed to upload ${path}: ${error.message??error}`);log(`[upload-bundle] \u2191 ${path} (${body.byteLength} bytes)`)}}function inlinedDataUris(text,threshold){const out=[];const re=/data:((?:image|font)\/[a-z0-9.+-]+|application\/(?:font-[a-z0-9]+|x-font-[a-z0-9]+))((?:;[a-z0-9-]+=[^;,]*)*)(;base64)?,/gi;let m;while(m=re.exec(text)){const start=m.index+m[0].length;let end=start;while(end<text.length&&!`"'\`)
3
- `.includes(text[end]))end++;const payload=text.slice(start,end);const bytes=m[3]?Math.floor(payload.replace(/=+$/,"").length*3/4):decodedLength(payload);if(bytes>threshold)out.push({mime:m[1].toLowerCase(),bytes});re.lastIndex=end}return out}function decodedLength(s){try{return Buffer.byteLength(decodeURIComponent(s))}catch{return s.length}}export{ASSETS_DIR,ASSET_TYPES,DEFAULT_ASSET_LIMITS,assetAliases,assetFileNames,assetProblems,assetsPlugin,bundleFiles,formatBytes,inlinedDataUris,lineageAssetProblems,metadataFiles,svgProblems,tagImportMetaUrls,uploadBundleFiles};
1
+ import{existsSync,readdirSync,readFileSync,statSync}from"node:fs";import{extname,join,resolve}from"node:path";import{PUBLIC_BUNDLE_BUCKET}from"./env.mjs";const ASSETS_DIR="assets";const ASSET_TYPES={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".avif":"image/avif",".gif":"image/gif",".svg":"image/svg+xml",".woff2":"font/woff2",".woff":"font/woff",".ttf":"font/ttf",".otf":"font/otf"};const DEFAULT_ASSET_LIMITS={maxAssetBytes:1024*1024,maxTotalBytes:8*1024*1024};const EXT_RE=Object.keys(ASSET_TYPES).map(e=>e.slice(1)).join("|");function svgProblems(text){const out=[];if(/<script\b/i.test(text))out.push("a <script> element");if(/<foreignObject\b/i.test(text))out.push("a <foreignObject> element");const handler=text.match(/<[^>]*?[\s"'/](on[a-z]+)\s*=/i);if(handler)out.push(`an event-handler attribute (${handler[1]})`);if(/javascript\s*:/i.test(text))out.push("a javascript: URL");return out}const PUBLIC_SVG_PROBLEM="SVG assets aren't allowed in public or shared lineages, which upload to bundles-public; use PNG/WebP, or inline the SVG in a component";function assetProblems(name,source,limits=DEFAULT_ASSET_LIMITS,{bucket}={}){const ext=extname(name).toLowerCase();if(!ASSET_TYPES[ext]){return[`${ext||"a file without an extension"} is not an allowed asset type (images: png, jpg, jpeg, webp, avif, gif, svg; fonts: woff2, woff, ttf, otf)`]}if(ext===".svg"&&bucket===PUBLIC_BUNDLE_BUCKET)return[PUBLIC_SVG_PROBLEM];const out=[];const bytes=typeof source==="string"?Buffer.byteLength(source):source.byteLength;if(bytes>limits.maxAssetBytes)out.push(`${formatBytes(bytes)} is over the ${formatBytes(limits.maxAssetBytes)} cap per asset (assets.maxAssetBytes)`);if(ext===".svg"){for(const p of svgProblems(typeof source==="string"?source:Buffer.from(source).toString("utf8")))out.push(`the SVG holds ${p}`)}return out}function lineageAssetProblems(assets,limits=DEFAULT_ASSET_LIMITS,{bucket}={}){const out=[];let total=0;for(const a of assets){const from=a.originalFileNames?.length?` (from ${a.originalFileNames.join(", ")})`:"";for(const p of assetProblems(a.fileName,a.source,limits,{bucket}))out.push(`${a.fileName}${from}: ${p}`);total+=typeof a.source==="string"?Buffer.byteLength(a.source):a.source.byteLength}if(total>limits.maxTotalBytes)out.push(`the lineage's assets total ${formatBytes(total)}, over the ${formatBytes(limits.maxTotalBytes)} cap (assets.maxTotalBytes)`);return out}function formatBytes(n){if(n>=1024*1024)return`${(n/(1024*1024)).toFixed(n%(1024*1024)?1:0)} MB`;if(n>=1024)return`${(n/1024).toFixed(n%1024?1:0)} KB`;return`${n} B`}function assetAliases(srcDir){return[{find:new RegExp(`^@/([^?#]*\\.(?:${EXT_RE}))$`,"i"),replacement:`${srcDir}/$1?no-inline`},{find:/^@\/([^?#]+)\?url$/i,replacement:`${srcDir}/$1?url&no-inline`},{find:"@",replacement:srcDir},{find:new RegExp(`^(?![a-z][a-z0-9+.-]*:|//)([^?#]+\\.(?:${EXT_RE}))$`,"i"),replacement:"$1?no-inline"},{find:/^(?![a-z][a-z0-9+.-]*:|\/\/)([^?#]+)\?url$/i,replacement:"$1?url&no-inline"}]}const META_URL_RE=new RegExp(`(new\\s+URL\\(\\s*)(['"\`])(\\.{1,2}/[^'"\`?#$]*\\.(?:${EXT_RE}))\\2(\\s*,\\s*import\\.meta\\.url\\s*\\))`,"gi");function tagImportMetaUrls(code){if(!code.includes("import.meta.url"))return null;const out=code.replace(META_URL_RE,(_,open,q,url,close)=>`${open}${q}${url}?no-inline${q}${close}`);return out===code?null:out}function assetFileNames(info){const name=info.names?.[0]??info.name??"";return name.endsWith(".css")?"[name][extname]":`${ASSETS_DIR}/[name]-[hash][extname]`}function assetsPlugin({lineage,bucket,limits=DEFAULT_ASSET_LIMITS}){return{name:"engine:assets",enforce:"pre",transform(code,id){const file=id.split("?")[0];if(file.includes("/node_modules/")||!/\.[cm]?[jt]sx?$/.test(file))return null;const out=tagImportMetaUrls(code);return out===null?null:{code:out,map:null}},generateBundle(_,bundle){const assets=Object.values(bundle).filter(o=>o.type==="asset"&&o.fileName.startsWith(`${ASSETS_DIR}/`));const problems=lineageAssetProblems(assets,limits,{bucket});if(problems.length){this.error(`[build] lineage "${lineage}" (uploads to ${bucket??"bundles"}): ${problems.length} asset problem(s):
2
+ \xB7 ${problems.join("\n \xB7 ")}`)}}}}function bundleFiles(distDir,{bucket,lineage="this lineage"}={}){const files=[{local:resolve(distDir,"index.js"),remote:"index.js",contentType:"application/javascript",listed:true}];const optional=[["index.css","text/css",true],["index.js.map","application/json",false],["contracts.json","application/json",false]];for(const[name,contentType,listed]of optional){const local=resolve(distDir,name);if(existsSync(local))files.push({local,remote:name,contentType,listed})}const assetsDir=resolve(distDir,ASSETS_DIR);if(existsSync(assetsDir)){for(const name of readdirSync(assetsDir).sort()){const local=join(assetsDir,name);if(!statSync(local).isFile())continue;const contentType=ASSET_TYPES[extname(name).toLowerCase()];if(!contentType)throw new Error(`[upload-bundle] ${ASSETS_DIR}/${name} is not an allowed asset type; rebuild with \`engine build\``);if(bucket===PUBLIC_BUNDLE_BUCKET&&contentType==="image/svg+xml"){throw new Error(`[upload-bundle] ${ASSETS_DIR}/${name} in lineage "${lineage}": ${PUBLIC_SVG_PROBLEM}`)}files.push({local,remote:`${ASSETS_DIR}/${name}`,contentType,listed:true})}}return files}function metadataFiles(files){return files.filter(f=>f.listed).map(f=>f.remote)}async function uploadBundleFiles({bucket,remoteBase,files,dryRun=false,log=console.log}){for(const f of files){const body=readFileSync(f.local);const path=`${remoteBase}/${f.remote}`;if(dryRun){log(`[upload-bundle] would upload ${path} (${body.byteLength} bytes, ${f.contentType})`);continue}const{error}=await bucket.upload(path,body,{contentType:f.contentType,upsert:false});if(error)throw new Error(`[upload-bundle] Failed to upload ${path}: ${error.message??error}`);log(`[upload-bundle] \u2191 ${path} (${body.byteLength} bytes)`)}}function inlinedDataUris(text,threshold){const out=[];const re=/data:((?:image|font)\/[a-z0-9.+-]+|application\/(?:font-[a-z0-9]+|x-font-[a-z0-9]+))((?:;[a-z0-9-]+=[^;,]*)*)(;base64)?,/gi;let m;while(m=re.exec(text)){const start=m.index+m[0].length;let end=start;while(end<text.length&&!`"'\`)
3
+ `.includes(text[end]))end++;const payload=text.slice(start,end);const bytes=m[3]?Math.floor(payload.replace(/=+$/,"").length*3/4):decodedLength(payload);if(bytes>threshold)out.push({mime:m[1].toLowerCase(),bytes});re.lastIndex=end}return out}function decodedLength(s){try{return Buffer.byteLength(decodeURIComponent(s))}catch{return s.length}}export{ASSETS_DIR,ASSET_TYPES,DEFAULT_ASSET_LIMITS,PUBLIC_SVG_PROBLEM,assetAliases,assetFileNames,assetProblems,assetsPlugin,bundleFiles,formatBytes,inlinedDataUris,lineageAssetProblems,metadataFiles,svgProblems,tagImportMetaUrls,uploadBundleFiles};
package/src/lib/env.mjs CHANGED
@@ -1 +1 @@
1
- import{createClient}from"@supabase/supabase-js";import{readFileSync,existsSync}from"node:fs";import{resolve}from"node:path";const ENV_FILE={local:".env.local",develop:".env.develop",prod:".env.prod"};const BUNDLE_BUCKET="bundles";const PUBLIC_BUNDLE_BUCKET="bundles-public";function parseTargetEnv(argv=process.argv.slice(2)){const wantsDevelop=argv.includes("--develop");const wantsProd=argv.includes("--prod");if(wantsDevelop&&wantsProd){throw new Error("Pass at most one of --develop / --prod (default is local).")}if(wantsProd)return"prod";if(wantsDevelop)return"develop";return"local"}function parseFlagValue(argv,flag){const longEq=`--${flag}=`;for(let i=0;i<argv.length;i++){const tok=argv[i];if(tok===`--${flag}`){const next=argv[i+1];if(!next||next.startsWith("--")){throw new Error(`--${flag} requires a value`)}return next}if(tok.startsWith(longEq)){return tok.slice(longEq.length)}}return null}function resolveOrgSlug(argv=process.argv.slice(2)){const fromFlag=parseFlagValue(argv,"org");const slug=fromFlag??process.env.ORG_SLUG??null;if(!slug){throw new Error("org slug required \u2014 pass --org <slug> or set ORG_SLUG in your env file.")}if(!process.env.ORG_SLUG)process.env.ORG_SLUG=slug;return slug}function loadEnv(target){const envFile=ENV_FILE[target];if(!envFile)throw new Error(`Unknown target env "${target}"`);const primary=resolve(process.cwd(),envFile);const fallback=target==="local"?resolve(process.cwd(),".env"):null;const fileToLoad=existsSync(primary)?primary:fallback&&existsSync(fallback)?fallback:null;if(!fileToLoad){const hasCiEnv=process.env.SUPABASE_URL&&secretKeyFromEnv();if(!hasCiEnv){console.warn(`[env] No env file found for target "${target}" (looked for ${envFile}${fallback?" or .env":""}).`)}else{console.log(`[env] No ${envFile} on disk \u2014 using env vars from process.env (CI mode).`)}return null}const content=readFileSync(fileToLoad,"utf8");for(const rawLine of content.split("\n")){const line=rawLine.trim();if(!line||line.startsWith("#"))continue;const m=line.match(/^([^=]+)=(.*)$/);if(!m)continue;const key=m[1].trim();let value=m[2].trim();if(value.startsWith('"')&&value.endsWith('"')||value.startsWith("'")&&value.endsWith("'")){value=value.slice(1,-1)}if(process.env[key]===void 0)process.env[key]=value}console.log(`[env] Loaded ${target} from ${fileToLoad}`);return fileToLoad}function secretKeyFromEnv(){return process.env.SUPABASE_SECRET_KEY||process.env.SUPABASE_SERVICE_KEY||process.env.SUPABASE_SERVICE_ROLE_KEY}function getSupabaseConfig(){const url=process.env.SUPABASE_URL;const key=secretKeyFromEnv();if(!url)throw new Error("SUPABASE_URL not set");if(!key)throw new Error("SUPABASE_SECRET_KEY (or SUPABASE_SERVICE_KEY / SUPABASE_SERVICE_ROLE_KEY) not set");return{url,key}}function initFromArgv(argv=process.argv.slice(2)){const target=parseTargetEnv(argv);loadEnv(target);const orgSlug=resolveOrgSlug(argv);const{url,key}=getSupabaseConfig();const supabase=createClient(url,key,{auth:{persistSession:false,autoRefreshToken:false}});return{target,orgSlug,supabase}}export{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,getSupabaseConfig,initFromArgv,loadEnv,parseFlagValue,parseTargetEnv,resolveOrgSlug};
1
+ import{createClient}from"@supabase/supabase-js";import{readFileSync,existsSync}from"node:fs";import{resolve}from"node:path";const ENV_FILE={local:".env.local",develop:".env.develop",prod:".env.prod"};const BUNDLE_BUCKET="bundles";const PUBLIC_BUNDLE_BUCKET="bundles-public";function bucketForLineage({name,kind}){return name==="public"||kind==="shared"?PUBLIC_BUNDLE_BUCKET:BUNDLE_BUCKET}function parseTargetEnv(argv=process.argv.slice(2)){const wantsDevelop=argv.includes("--develop");const wantsProd=argv.includes("--prod");if(wantsDevelop&&wantsProd){throw new Error("Pass at most one of --develop / --prod (default is local).")}if(wantsProd)return"prod";if(wantsDevelop)return"develop";return"local"}function parseFlagValue(argv,flag){const longEq=`--${flag}=`;for(let i=0;i<argv.length;i++){const tok=argv[i];if(tok===`--${flag}`){const next=argv[i+1];if(!next||next.startsWith("--")){throw new Error(`--${flag} requires a value`)}return next}if(tok.startsWith(longEq)){return tok.slice(longEq.length)}}return null}function resolveOrgSlug(argv=process.argv.slice(2)){const fromFlag=parseFlagValue(argv,"org");const slug=fromFlag??process.env.ORG_SLUG??null;if(!slug){throw new Error("org slug required \u2014 pass --org <slug> or set ORG_SLUG in your env file.")}if(!process.env.ORG_SLUG)process.env.ORG_SLUG=slug;return slug}function loadEnv(target){const envFile=ENV_FILE[target];if(!envFile)throw new Error(`Unknown target env "${target}"`);const primary=resolve(process.cwd(),envFile);const fallback=target==="local"?resolve(process.cwd(),".env"):null;const fileToLoad=existsSync(primary)?primary:fallback&&existsSync(fallback)?fallback:null;if(!fileToLoad){const hasCiEnv=process.env.SUPABASE_URL&&secretKeyFromEnv();if(!hasCiEnv){console.warn(`[env] No env file found for target "${target}" (looked for ${envFile}${fallback?" or .env":""}).`)}else{console.log(`[env] No ${envFile} on disk \u2014 using env vars from process.env (CI mode).`)}return null}const content=readFileSync(fileToLoad,"utf8");for(const rawLine of content.split("\n")){const line=rawLine.trim();if(!line||line.startsWith("#"))continue;const m=line.match(/^([^=]+)=(.*)$/);if(!m)continue;const key=m[1].trim();let value=m[2].trim();if(value.startsWith('"')&&value.endsWith('"')||value.startsWith("'")&&value.endsWith("'")){value=value.slice(1,-1)}if(process.env[key]===void 0)process.env[key]=value}console.log(`[env] Loaded ${target} from ${fileToLoad}`);return fileToLoad}function secretKeyFromEnv(){return process.env.SUPABASE_SECRET_KEY||process.env.SUPABASE_SERVICE_KEY||process.env.SUPABASE_SERVICE_ROLE_KEY}function getSupabaseConfig(){const url=process.env.SUPABASE_URL;const key=secretKeyFromEnv();if(!url)throw new Error("SUPABASE_URL not set");if(!key)throw new Error("SUPABASE_SECRET_KEY (or SUPABASE_SERVICE_KEY / SUPABASE_SERVICE_ROLE_KEY) not set");return{url,key}}function initFromArgv(argv=process.argv.slice(2)){const target=parseTargetEnv(argv);loadEnv(target);const orgSlug=resolveOrgSlug(argv);const{url,key}=getSupabaseConfig();const supabase=createClient(url,key,{auth:{persistSession:false,autoRefreshToken:false}});return{target,orgSlug,supabase}}export{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,bucketForLineage,getSupabaseConfig,initFromArgv,loadEnv,parseFlagValue,parseTargetEnv,resolveOrgSlug};
@@ -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};