@peripheries/engine 0.5.0 → 0.6.1
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 +1 -1
- package/src/commands/build.mjs +1 -1
- package/src/commands/check-fixtures.mjs +1 -1
- package/src/commands/cutover.mjs +4 -4
- package/src/commands/test-visual.mjs +1 -1
- package/src/commands/upload-bundle.mjs +1 -1
- package/src/harness/plugin.mjs +1 -1
- package/src/harness/server.mjs +1 -1
- package/src/index.d.mts +13 -2
- package/src/lib/config.mjs +1 -1
- package/src/lib/css-stub.mjs +1 -0
- package/src/lib/knowledge.mjs +1 -1
package/package.json
CHANGED
package/src/commands/build.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawnSync}from"node:child_process";import{existsSync}from"node:fs";import{createRequire}from"node:module";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue}from"../lib/env.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;
|
|
2
|
+
import{spawnSync}from"node:child_process";import{existsSync}from"node:fs";import{createRequire}from"node:module";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue}from"../lib/env.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
3
|
[build] ${lineage.name}: ${lineage.entry} \u2192 ${lineage.dist}/index.js`);await build({configFile:false,mode:"production",root:REPO_ROOT,logLevel:"warn",plugins:[react()],define:{"process.env.NODE_ENV":JSON.stringify("production"),__OA_BUNDLE_NAME__:JSON.stringify(config.org)},resolve:{alias:{"@":resolve(REPO_ROOT,"src")}},optimizeDeps:{exclude:external},build:{lib:{entry,formats:["es"],fileName:()=>"index.js"},rollupOptions:{external},outDir:resolve(REPO_ROOT,lineage.dist),emptyOutDir:true,sourcemap:true}});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(", ")}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
|
-
import{existsSync,readFileSync,readdirSync,statSync,writeFileSync}from"node:fs";import{dirname,join,relative,resolve}from"node:path";import{hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toRepoRelative}from"../lib/jsdoc-parser.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const REPO_ROOT=REPO_ROOT_;const argv=process.argv.slice(2);const STRICT=argv.includes("--strict");const UPDATE_BASELINE=argv.includes("--update-baseline");const BASELINE_PATH=resolve(REPO_ROOT,"scripts/__baselines__/fixtures.json");function readBaseline(){if(!existsSync(BASELINE_PATH))return{classes:{}};return JSON.parse(readFileSync(BASELINE_PATH,"utf8"))}function barrels(){const out=["src/index.ts"];for(const f of readdirSync(join(REPO_ROOT,"src"))){if(/^index\.[a-z0-9-]+\.ts$/.test(f)&&!f.endsWith(".test.ts"))out.push(`src/${f}`)}return out}function walk(dir,pred,out=[]){if(!existsSync(dir))return out;for(const e of readdirSync(dir)){const full=join(dir,e);if(statSync(full).isDirectory())walk(full,pred,out);else if(pred(full))out.push(full)}return out}function storeDeclaringComponents(){const found=new Set;const pages=["src/project_templates","src/org_templates"].flatMap(r=>walk(join(REPO_ROOT,r),f=>f.endsWith("page.json")));for(const file of pages){let json;try{json=JSON.parse(readFileSync(file,"utf8"))}catch{continue}const cells=json?.content?.cells??[];for(const cell of cells){if(!(cell?.selections||cell?.viewState||cell?.live))continue;if(typeof cell.component==="string")found.add(cell.component)}}return found}const NARROW_PX=480;const isLoading=s=>s.status?.isLoading===true;const isError=s=>s.status?.isError===true||Boolean(s.status?.errors);const isNarrow=s=>typeof s.viewport?.width==="number"&&s.viewport.width<=NARROW_PX;const declaresStore=s=>Boolean(s.selections||s.viewState||s.live||s.storeFixture);function arrayProp(s,name){const v=s.props?.[name]??s.data?.[name];return Array.isArray(v)?v:void 0}async function coveredElsewhere(){const out=new Map;for(const file of walk(join(REPO_ROOT,"src
|
|
2
|
+
import{existsSync,readFileSync,readdirSync,statSync,writeFileSync}from"node:fs";import{dirname,join,relative,resolve}from"node:path";import{hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toRepoRelative}from"../lib/jsdoc-parser.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";import"../lib/css-stub.mjs";import{loadConfig}from"../lib/config.mjs";const HARNESS_IS_NARROW=(await loadConfig()).harness.widths.some(w=>w<480);const REPO_ROOT=REPO_ROOT_;const argv=process.argv.slice(2);const STRICT=argv.includes("--strict");const UPDATE_BASELINE=argv.includes("--update-baseline");const BASELINE_PATH=resolve(REPO_ROOT,"scripts/__baselines__/fixtures.json");function readBaseline(){if(!existsSync(BASELINE_PATH))return{classes:{}};return JSON.parse(readFileSync(BASELINE_PATH,"utf8"))}function barrels(){const out=existsSync(join(REPO_ROOT,"src/index.ts"))?["src/index.ts"]:[];for(const f of readdirSync(join(REPO_ROOT,"src"))){if(/^index\.[a-z0-9-]+\.ts$/.test(f)&&!f.endsWith(".test.ts"))out.push(`src/${f}`)}return out}function walk(dir,pred,out=[]){if(!existsSync(dir))return out;for(const e of readdirSync(dir)){const full=join(dir,e);if(statSync(full).isDirectory())walk(full,pred,out);else if(pred(full))out.push(full)}return out}function storeDeclaringComponents(){const found=new Set;const pages=["src/project_templates","src/org_templates"].flatMap(r=>walk(join(REPO_ROOT,r),f=>f.endsWith("page.json")));for(const file of pages){let json;try{json=JSON.parse(readFileSync(file,"utf8"))}catch{continue}const cells=json?.content?.cells??[];for(const cell of cells){if(!(cell?.selections||cell?.viewState||cell?.live))continue;if(typeof cell.component==="string")found.add(cell.component)}}return found}const NARROW_PX=480;const isLoading=s=>s.status?.isLoading===true;const isError=s=>s.status?.isError===true||Boolean(s.status?.errors);const isNarrow=s=>typeof s.viewport?.width==="number"&&s.viewport.width<=NARROW_PX;const declaresStore=s=>Boolean(s.selections||s.viewState||s.live||s.storeFixture);function arrayProp(s,name){const v=s.props?.[name]??s.data?.[name];return Array.isArray(v)?v:void 0}async function coveredElsewhere(){const out=new Map;for(const file of walk(join(REPO_ROOT,"src"),f=>/\.fixtures\.tsx?$/.test(f))){let mod;try{mod=await import(file)}catch{continue}for(const name of mod.covers??[])out.set(name,toRepoRelative(REPO_ROOT,file))}return out}const storeComponents=storeDeclaringComponents();const covered=await coveredElsewhere();const findings=[];const byClass=new Map;const seen=new Set;let components=0;function record(cls,ok,component,message){const bucket=byClass.get(cls)??{checked:0,failed:0};bucket.checked++;if(!ok){bucket.failed++;findings.push({component,cls,message})}byClass.set(cls,bucket)}for(const entry of barrels()){for(const{name,source}of readExportsFromIndex(REPO_ROOT,entry)){if(seen.has(name))continue;const filePath=resolveComponentFilePath(REPO_ROOT,source);if(!filePath||!hasComponentJSDoc(filePath,name))continue;seen.add(name);components++;const base=filePath.replace(/\.(tsx?|jsx?)$/,"");const byExport=join(dirname(filePath),name);const fixtureFile=[`${base}.fixtures.tsx`,`${base}.fixtures.ts`,`${byExport}.fixtures.tsx`,`${byExport}.fixtures.ts`].find(existsSync);if(!fixtureFile&&covered.has(name)){record("exists",true,name,"");continue}record("exists",Boolean(fixtureFile),name,`no fixtures file beside ${toRepoRelative(REPO_ROOT,filePath)}`);if(!fixtureFile)continue;let scenarios=[];try{const mod=await import(fixtureFile);scenarios=mod.scenarios??mod.default?.scenarios??[]}catch(err){const why=err instanceof Error?err.message:String(err);record("exists",false,name,`fixtures module failed to import: ${why.slice(0,120)}`);continue}record("exists",scenarios.length>0,name,"fixtures module declares no scenarios");const first=scenarios[0];if(!first)continue;record("intent",Boolean(first.designIntent),name,`default scenario "${first.name}" has no designIntent \u2014 the eval step skips it silently`);if(!HARNESS_IS_NARROW)record("narrow",scenarios.some(isNarrow),name,`no scenario under ${NARROW_PX}px \u2014 reflow is unproven`);const jsdoc=parseJSDocFile(filePath,name)??{};const props=jsdoc.props_schema?.properties??{};const propNames=Object.keys(props);const dataBearing=Boolean(jsdoc.props_schema?.data)||propNames.includes("data")||propNames.includes("status");if(dataBearing){record("loading",scenarios.some(isLoading),name,"declares host data but no loading scenario");record("error",scenarios.some(isError),name,"declares host data but no error scenario")}const required=jsdoc.props_schema?.required??[];const listProps=propNames.filter(p=>props[p]?.type==="array"&&required.includes(p));for(const p of listProps){const values=scenarios.map(s=>arrayProp(s,p)).filter(Boolean);if(values.length===0)continue;record("empty",values.some(v=>v.length===0),name,`array prop "${p}" is never empty in any scenario`);record("dense",values.some(v=>v.length>=5),name,`array prop "${p}" never carries 5+ items \u2014 density is unproven`)}if(storeComponents.has(name)){record("store",scenarios.some(declaresStore),name,"a page.json cell declares store channels for this component, but no scenario does")}}}const order=["exists","intent","narrow","loading","error","empty","dense","store"];const baseline=readBaseline();const observed={};for(const cls of order)observed[cls]=byClass.get(cls)?.failed??0;if(UPDATE_BASELINE){const raised=order.filter(c=>observed[c]>(baseline.classes[c]??0));writeFileSync(BASELINE_PATH,JSON.stringify({_comment:"Per-class fixture-coverage failure counts. A FLOOR TO BE LOWERED, never raised \u2014 exceeding one fails `npm run check:fixtures`. Regenerate with `npm run check:fixtures -- --update-baseline` only when a number goes DOWN. See the header of scripts/check-fixtures.mts.",classes:observed},null,2)+"\n","utf8");console.log(`[fixtures] baseline written to ${relative(REPO_ROOT,BASELINE_PATH)}`);if(raised.length>0){console.warn(`
|
|
3
3
|
[fixtures] WARNING \u2014 ${raised.length} class(es) went UP and were recorded anyway:
|
|
4
4
|
`+raised.map(c=>` ${c}: ${baseline.classes[c]??0} -> ${observed[c]}`).join("\n")+"\n\n A floor is meant to be lowered. Raising one records a regression as\n acceptable, which is exactly what this file exists to prevent. If that is\n deliberate, say why in the commit message.")}process.exit(0)}console.log(`[fixtures] ${components} components
|
|
5
5
|
`);const regressions=[];for(const cls of order){const b=byClass.get(cls);if(!b)continue;const floor=baseline.classes[cls]??0;const now=observed[cls];if(STRICT?now>0:now>floor)regressions.push({cls,was:floor,now});const verdict=now===0?"ok":now>floor?`${now} (floor ${floor}) OVER`:`${now}/${floor}`;console.log(` ${cls.padEnd(8)} ${String(b.checked-now).padStart(4)}/${String(b.checked).padEnd(4)} ${verdict}`)}function print(list,label){if(list.length===0)return;console.log(`
|
package/src/commands/cutover.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const VERSION=parseFlagValue(argv,"version")??process.env.VERSION??null;if(!VERSION){console.error("[cutover] --version <v> is required.");process.exit(2)}const DRY_RUN=argv.includes("--dry-run");const explicitBundle=parseFlagValue(argv,"bundle");const wantsPublic=argv.includes("--public");if(wantsPublic&&explicitBundle&&explicitBundle!=="public"){console.error(`[cutover] --public conflicts with --bundle ${explicitBundle}. Pass one.`);process.exit(2)}const MAIN_LINEAGE="main";const PUBLIC_LINEAGE="public";const BUNDLE_NAME=explicitBundle??(wantsPublic?PUBLIC_LINEAGE:MAIN_LINEAGE);console.log(`
|
|
2
|
+
import{lineageConfig}from"../lib/config.mjs";import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const VERSION=parseFlagValue(argv,"version")??process.env.VERSION??null;if(!VERSION){console.error("[cutover] --version <v> is required.");process.exit(2)}const DRY_RUN=argv.includes("--dry-run");const explicitBundle=parseFlagValue(argv,"bundle");const wantsPublic=argv.includes("--public");if(wantsPublic&&explicitBundle&&explicitBundle!=="public"){console.error(`[cutover] --public conflicts with --bundle ${explicitBundle}. Pass one.`);process.exit(2)}const MAIN_LINEAGE="main";const PUBLIC_LINEAGE="public";const BUNDLE_NAME=explicitBundle??(wantsPublic?PUBLIC_LINEAGE:MAIN_LINEAGE);console.log(`
|
|
3
3
|
\u{1F3AF} cutover \u2014 env=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION}
|
|
4
4
|
`);const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[cutover] Could not resolve org "${ORG_SLUG}":`,orgError?.message??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;console.log(` org_id=${ORG_ID}`);const{data:bundleRow,error:bundleError}=await supabase.from("component_bundle").select("id, status, exports").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(bundleError){console.error("[cutover] Failed to fetch component_bundle:",bundleError.message);process.exit(1)}if(!bundleRow){console.error(`[cutover] No component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}). Has upload-bundle.mjs + register-components.mjs been run for this version?`);process.exit(1)}const BUNDLE_ID=bundleRow.id;console.log(` bundle_id=${BUNDLE_ID} status='${bundleRow.status}' exports=${bundleRow.exports?.length??0}`);if(bundleRow.status!=="registered"){console.error(`[cutover] Bundle is in status='${bundleRow.status}'. cutover requires 'registered'.
|
|
5
5
|
`+(bundleRow.status==="uploaded"?" \u2192 Run register-components.mjs for this version first.\n":bundleRow.status==="cutover"?" \u2192 This bundle is already active. Target a different version to roll forward/back.\n":" \u2192 Investigate the bundle row directly.\n"));process.exit(1)}await warnLiveDrift();if(BUNDLE_NAME===PUBLIC_LINEAGE){await cutoverPublic()}else if(BUNDLE_NAME!==MAIN_LINEAGE){await cutoverProject()}else{await cutoverMain()}async function cutoverMain(){const{data:templateRow,error:templateError}=await supabase.from("org_template").select("id, remote_version, remote_url, previous_version").eq("org_id",ORG_ID).eq("is_archived",false).single();if(templateError||!templateRow?.id){console.error(`[cutover] No active org_template for org "${ORG_SLUG}":`,templateError?.message??"not found");process.exit(1)}let previousBundleId=null;if(templateRow.remote_version){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",MAIN_LINEAGE).eq("version",templateRow.remote_version).maybeSingle();previousBundleId=prev?.id??null}const newRemoteUrl=`/api/remotes/${ORG_SLUG}/${VERSION}/index.js`;console.log(`
|
|
@@ -8,12 +8,12 @@ import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../li
|
|
|
8
8
|
`)}async function cutoverPublic(){const{data:templateRow,error:templateError}=await supabase.from("org_template").select("id, public_remote_version").eq("org_id",ORG_ID).eq("is_archived",false).single();if(templateError||!templateRow?.id){console.error(`[cutover] No active org_template for org "${ORG_SLUG}":`,templateError?.message??"not found");process.exit(1)}let previousBundleId=null;if(templateRow.public_remote_version){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",PUBLIC_LINEAGE).eq("version",templateRow.public_remote_version).maybeSingle();previousBundleId=prev?.id??null}console.log(`
|
|
9
9
|
org_template (public lineage):`);console.log(` public_remote_version: ${templateRow.public_remote_version??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_public: false \u2192 true`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` component_bundle_lineage (public):`);console.log(` active_version: ${templateRow.public_remote_version??"(none)"} \u2192 ${VERSION}`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,is_public:true,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}const{error:templateUpdateError}=await supabase.from("org_template").update({public_remote_version:VERSION,updated_at:nowIso}).eq("id",templateRow.id);if(templateUpdateError){console.error("[cutover] Failed to update org_template.public_remote_version:",templateUpdateError.message);process.exit(1)}await setLiveBundle(PUBLIC_LINEAGE);await writeLineagePointer({bundleName:PUBLIC_LINEAGE,previousVersion:templateRow.public_remote_version??null,storageBucket:PUBLIC_BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
|
|
10
10
|
\u2705 Promoted public bundle ${BUNDLE_ID} for org "${ORG_SLUG}".
|
|
11
|
-
`)}async function cutoverProject(){const{data:lineage}=await supabase.from("component_bundle_lineage").select("active_version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).maybeSingle();const previousVersion=lineage?.active_version??null;let previousBundleId=null;if(previousVersion){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",previousVersion).maybeSingle();previousBundleId=prev?.id??null}console.log(`
|
|
12
|
-
component_bundle_lineage (${BUNDLE_NAME}):`);console.log(` active_version: ${previousVersion??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` org_template: (untouched \u2014 project lineages have no org-wide pointer)`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}await setLiveBundle(BUNDLE_NAME);await writeLineagePointer({bundleName:BUNDLE_NAME,previousVersion,storageBucket:BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
|
|
11
|
+
`)}async function cutoverProject(){const LINEAGE=await lineageConfig(BUNDLE_NAME);const shared=LINEAGE.kind==="shared"?{specifier:LINEAGE.specifier,audience:"platform"}:null;const{data:lineage}=await supabase.from("component_bundle_lineage").select("active_version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).maybeSingle();const previousVersion=lineage?.active_version??null;let previousBundleId=null;if(previousVersion){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",previousVersion).maybeSingle();previousBundleId=prev?.id??null}console.log(`
|
|
12
|
+
component_bundle_lineage (${BUNDLE_NAME}):`);console.log(` active_version: ${previousVersion??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` org_template: (untouched \u2014 project lineages have no org-wide pointer)`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,previous_bundle_id:previousBundleId,...shared?{is_public:true}:{}}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}await setLiveBundle(BUNDLE_NAME);await writeLineagePointer({bundleName:BUNDLE_NAME,previousVersion,storageBucket:shared?PUBLIC_BUNDLE_BUCKET:BUNDLE_BUCKET,extra:shared??{}});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
|
|
13
13
|
\u2705 Promoted "${BUNDLE_NAME}" bundle ${BUNDLE_ID} for org "${ORG_SLUG}" to version ${VERSION}.
|
|
14
14
|
`);console.log(` Point a template at it with project_template.bundle_name = '${BUNDLE_NAME}'
|
|
15
15
|
(upload-project-template.mjs writes this from template.json's "bundle_name" key).
|
|
16
|
-
`)}async function setLiveBundle(bundleName){const{error:clearError}=await supabase.from("component_bundle").update({is_live:false}).eq("org_id",ORG_ID).eq("bundle_name",bundleName).eq("is_live",true);if(clearError){console.error(`[cutover] Failed to clear is_live for lineage "${bundleName}":`,clearError.message);process.exit(1)}const{error:setError}=await supabase.from("component_bundle").update({is_live:true}).eq("id",BUNDLE_ID);if(setError){console.error(`[cutover] Cleared the previous live bundle but failed to set is_live on ${BUNDLE_ID}:`,setError.message);console.error(` Lineage "${bundleName}" now has NO live bundle. Fix with:`);console.error(` update component_bundle set is_live = true where id = '${BUNDLE_ID}';`);process.exit(1)}}async function writeLineagePointer({bundleName,previousVersion,storageBucket}){const{error}=await supabase.from("component_bundle_lineage").upsert({org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:bundleName,active_version:VERSION,previous_version:previousVersion,storage_bucket:storageBucket,updated_at:new Date().toISOString()},{onConflict:"org_id,bundle_name"});if(error){console.error("[cutover] Failed to write component_bundle_lineage:",error.message);console.error(` The bundle status was updated but the lineage pointer was not \u2014 a project_template
|
|
16
|
+
`)}async function setLiveBundle(bundleName){const{error:clearError}=await supabase.from("component_bundle").update({is_live:false}).eq("org_id",ORG_ID).eq("bundle_name",bundleName).eq("is_live",true);if(clearError){console.error(`[cutover] Failed to clear is_live for lineage "${bundleName}":`,clearError.message);process.exit(1)}const{error:setError}=await supabase.from("component_bundle").update({is_live:true}).eq("id",BUNDLE_ID);if(setError){console.error(`[cutover] Cleared the previous live bundle but failed to set is_live on ${BUNDLE_ID}:`,setError.message);console.error(` Lineage "${bundleName}" now has NO live bundle. Fix with:`);console.error(` update component_bundle set is_live = true where id = '${BUNDLE_ID}';`);process.exit(1)}}async function writeLineagePointer({bundleName,previousVersion,storageBucket,extra={}}){const{error}=await supabase.from("component_bundle_lineage").upsert({org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:bundleName,active_version:VERSION,previous_version:previousVersion,storage_bucket:storageBucket,...extra,updated_at:new Date().toISOString()},{onConflict:"org_id,bundle_name"});if(error){console.error("[cutover] Failed to write component_bundle_lineage:",error.message);console.error(` The bundle status was updated but the lineage pointer was not \u2014 a project_template
|
|
17
17
|
referencing "${bundleName}" will fail its FK until this row exists.`);process.exit(1)}}async function retirePreviousBundle(previousBundleId,nowIso){if(!previousBundleId||previousBundleId===BUNDLE_ID)return;const{error}=await supabase.from("component_bundle").update({status:"rolled_back",rolled_back_at:nowIso}).eq("id",previousBundleId).eq("status","cutover");if(error){console.warn(`[cutover] Promoted, but failed to retire previous bundle ${previousBundleId}:`,error.message)}}async function warnLiveDrift(){const{data:drifted,error}=await supabase.from("component_bundle").select("version, cutover_at").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("status","cutover").eq("is_live",false).order("cutover_at",{ascending:false});if(error){console.warn(`[cutover] Could not check for is_live drift: ${error.message}`);return}if(!drifted?.length)return;const sample=drifted.slice(0,5).map(r=>`${r.version} (cutover_at=${r.cutover_at??"null"})`).join(", ");console.warn(`
|
|
18
18
|
\u26A0\uFE0F ${drifted.length} bundle(s) in lineage "${BUNDLE_NAME}" have status='cutover' but is_live=false:
|
|
19
19
|
${sample}${drifted.length>5?", \u2026":""}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{mkdir,rm,writeFile}from"node:fs/promises";import{createRequire}from"node:module";import{cpus}from"node:os";import{join,relative,resolve}from"node:path";import{pathToFileURL}from"node:url";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue}from"../lib/env.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{newDeterministicContext,prepare,settle}from"../harness/determinism.mjs";import{
|
|
2
|
+
import{mkdir,rm,writeFile}from"node:fs/promises";import{createRequire}from"node:module";import{cpus}from"node:os";import{join,relative,resolve}from"node:path";import{pathToFileURL}from"node:url";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue}from"../lib/env.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{newDeterministicContext,prepare,settle}from"../harness/determinism.mjs";import{buildAndServeHarness,createHarnessServer}from"../harness/server.mjs";const argv=process.argv.slice(2);const{harness}=await loadConfig();const OUTPUT=resolve(REPO_ROOT,harness.output);const only=parseFlagValue(argv,"only");const[shardIndex,shardTotal]=(parseFlagValue(argv,"shard")??"1/1").split("/").map(Number);if(!(shardTotal>=1&&shardIndex>=1&&shardIndex<=shardTotal))throw new Error("[test-visual] --shard expects <i>/<n>");const fromRepo=createRequire(resolve(REPO_ROOT,"package.json"));const playwright=await import(pathToFileURL(fromRepo.resolve("playwright")).href);const chromium=playwright.chromium??playwright.default.chromium;const skipped=[];const{server}=await createHarnessServer();const served=await buildAndServeHarness();const origin=served.origin;const HARNESS_PATH=served.path;let exitCode=0;try{const list=await listTargets(server);const targets=list.filter(t=>(!only||t.label.startsWith(`${only} `))&&t.index%shardTotal===shardIndex-1);await rm(OUTPUT,{recursive:true,force:true});await mkdir(OUTPUT,{recursive:true});console.log(`[test-visual] ${targets.length} target(s) (of ${list.length}) \xD7 ${harness.widths.length} width(s) + a ${harness.container}px container${skipped.length?`; ${skipped.length} public-page scenario(s) skipped (R6)`:""}`);const browser=await chromium.launch();const concurrency=Math.max(1,Number(process.env.ENGINE_VISUAL_CONCURRENCY)||Math.min(6,Math.max(2,cpus().length-1)));const reports=new Array(targets.length);let next=0;await Promise.all(Array.from({length:Math.min(concurrency,targets.length)},async()=>{while(next<targets.length){const i=next++;const t=targets[i];const renders=[...harness.widths.map(w=>({width:w,container:null})),{width:Math.max(...harness.widths),container:harness.container}];const report={target:t.label,kind:t.kind,...t.designIntent?{designIntent:t.designIntent}:{},widths:[]};for(const r of renders)report.widths.push(await renderOnce(browser,t,r));reports[i]=report;const bad=report.widths.filter(w=>w.errors.length);const over=report.widths.filter(w=>w.overflow);console.log(` ${bad.length?"\u2717":over.length?"\u25B3":"\u2713"} ${t.label}${bad.length?` \u2014 ${bad[0].errors[0].split("\n")[0].slice(0,160)}`:""}${over.length?` (overflow at ${over.map(w=>w.container?`${w.container}px container`:`${w.width}px`).join(", ")})`:""}`)}}));await browser.close();const failed=reports.filter(r=>r.widths.some(w=>w.errors.length));const overflowing=reports.filter(r=>r.widths.some(w=>w.overflow));await writeFile(join(OUTPUT,"report.json"),JSON.stringify({widths:harness.widths,container:harness.container,reports,skipped},null,2)+"\n");console.log(`[test-visual] ${reports.length-failed.length}/${reports.length} rendered clean; ${overflowing.length} overflow; report \u2192 ${relative(REPO_ROOT,join(OUTPUT,"report.json"))}`);if(failed.length||harness.overflow==="error"&&overflowing.length)exitCode=1}finally{await server.close();await served.close()}process.exit(exitCode);async function listTargets(vite){const{readdir}=await import("node:fs/promises");const walk=async dir=>{const out=[];for(const e of await readdir(dir,{withFileTypes:true}).catch(()=>[])){const p=join(dir,e.name);if(e.isDirectory())out.push(...await walk(p));else out.push(p)}return out};const targets=[];const fixtureFiles=(await walk(resolve(REPO_ROOT,"src"))).filter(f=>/\.fixtures\.tsx?$/.test(f)&&!f.includes("/node_modules/")).sort();for(const file of fixtureFiles){const name=file.split("/").pop().replace(/\.fixtures\.tsx?$/,"");const mod=await vite.ssrLoadModule(file);for(const s of mod.scenarios??[]){if(s.surface==="public-page"){skipped.push(`${name} \xB7 ${s.name}`);continue}targets.push({kind:"fixture",label:`${name} \xB7 ${s.name}`,query:`fixture=${encodeURIComponent(name)}&scenario=${encodeURIComponent(s.name)}`,designIntent:s.designIntent})}}const pageFiles=[...(await walk(resolve(REPO_ROOT,"src/pages"))).filter(f=>f.endsWith("/page.json")&&f.split("/").length-resolve(REPO_ROOT,"src/pages").split("/").length===2),...(await walk(resolve(REPO_ROOT,"src/project_templates"))).filter(f=>/\/pages\/[^/]+\/page\.json$/.test(f))].sort();for(const file of pageFiles){const m=file.match(/\/project_templates\/([^/]+)\/pages\/([^/]+)\/page\.json$/)??file.match(/\/pages\/([^/]+)\/page\.json$/);const key=m.slice(1).join("/");targets.push({kind:"page",label:`page ${key}`,query:`page=${encodeURIComponent(key)}`})}return targets.map((t,index)=>({...t,index}))}async function renderOnce(browser,target,{width,container}){const context=await newDeterministicContext(browser,{width,height:900});const page=await context.newPage();await prepare(page);const consoleErrors=[];const pageErrors=[];page.on("console",m=>m.type()==="error"&&consoleErrors.push(m.text()));page.on("pageerror",e=>pageErrors.push(e.stack||e.message));const url=`${origin}${HARNESS_PATH}?${target.query}${container?`&container=${container}`:""}`;const result={width,...container?{container}:{},errors:[],diagnostics:[],overflow:false};try{await page.goto(url,{waitUntil:"load",timeout:3e4});await settle(page,{timeout:15e3});const harnessError=await page.evaluate(()=>window.__ENGINE_HARNESS_ERROR??null);if(harnessError)result.errors.push(harnessError);result.diagnostics=await page.evaluate(()=>window.__ENGINE_HARNESS_DIAGNOSTICS??[]);result.overflow=await page.evaluate(inContainer=>{const el=inContainer?document.querySelector("[data-engine-container]"):document.scrollingElement;return!!el&&el.scrollWidth>el.clientWidth+1},Boolean(container));result.aria=await page.locator("#root").ariaSnapshot().catch(()=>null)}catch(err){result.errors.push(err instanceof Error?err.message:String(err))}const dir=join(OUTPUT,target.kind,target.label.replace(/[^A-Za-z0-9._-]+/g,"_"));await mkdir(dir,{recursive:true});const shot=join(dir,`${container?`c${container}`:width}.png`);await page.screenshot({path:shot,fullPage:true}).then(()=>result.screenshot=relative(REPO_ROOT,shot),()=>{});const attributeWarning=/Unknown event handler property|React does not recognize the|Invalid DOM property|Invalid value for prop|for a non-boolean attribute/;result.warnings=consoleErrors.filter(m=>attributeWarning.test(m));result.errors.push(...pageErrors,...consoleErrors.filter(m=>!attributeWarning.test(m)));for(const d of result.diagnostics)if(d.severity==="error")result.errors.push(`${d.code}: ${d.message}`);await context.close();return result}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync,existsSync,statSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";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 DEFAULT_BUCKET=IS_PUBLIC?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;async function uploadOne(localPath,remotePath,contentType){const body=readFileSync(localPath);if(dryRun){console.log(`[upload-bundle] would upload ${STORAGE_BUCKET}/${remotePath} (${body.byteLength} bytes)`);return}const{error}=await supabase.storage.from(STORAGE_BUCKET).upload(remotePath,body,{contentType,upsert:false});if(error){console.error(`[upload-bundle] Failed to upload ${remotePath}:`,error);process.exit(1)}console.log(`[upload-bundle] \u2191 ${remotePath} (${body.byteLength} bytes)`)}const remoteBase=IS_ORG_WIDE?`${ORG_SLUG}/${VERSION}`:`${ORG_SLUG}/${BUNDLE_NAME}/${VERSION}`;await uploadOne(bundlePath,`${remoteBase}/index.js`,"application/javascript");const mapPath=resolve(distDir,"index.js.map");if(existsSync(mapPath)){await uploadOne(mapPath,`${remoteBase}/index.js.map`,"application/json")}const contractsPath=resolve(distDir,"contracts.json");if(existsSync(contractsPath)){await uploadOne(contractsPath,`${remoteBase}/contracts.json`,"application/json")}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
|
|
2
|
+
import{readFileSync,existsSync,statSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";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;async function uploadOne(localPath,remotePath,contentType){const body=readFileSync(localPath);if(dryRun){console.log(`[upload-bundle] would upload ${STORAGE_BUCKET}/${remotePath} (${body.byteLength} bytes)`);return}const{error}=await supabase.storage.from(STORAGE_BUCKET).upload(remotePath,body,{contentType,upsert:false});if(error){console.error(`[upload-bundle] Failed to upload ${remotePath}:`,error);process.exit(1)}console.log(`[upload-bundle] \u2191 ${remotePath} (${body.byteLength} bytes)`)}const remoteBase=IS_ORG_WIDE?`${ORG_SLUG}/${VERSION}`:`${ORG_SLUG}/${BUNDLE_NAME}/${VERSION}`;await uploadOne(bundlePath,`${remoteBase}/index.js`,"application/javascript");const mapPath=resolve(distDir,"index.js.map");if(existsSync(mapPath)){await uploadOne(mapPath,`${remoteBase}/index.js.map`,"application/json")}const contractsPath=resolve(distDir,"contracts.json");if(existsSync(contractsPath)){await uploadOne(contractsPath,`${remoteBase}/contracts.json`,"application/json")}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,...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.`);
|
package/src/harness/plugin.mjs
CHANGED
|
@@ -10,4 +10,4 @@ import{existsSync}from"node:fs";import{dirname,resolve}from"node:path";import{fi
|
|
|
10
10
|
<div id="root"></div>
|
|
11
11
|
<script type="module" src="/@id/${ENTRY}"><\/script>
|
|
12
12
|
</body>
|
|
13
|
-
</html>`;function
|
|
13
|
+
</html>`;function entryCode(wrapperPath,wrapperExport="default",appPath="/@fs"+APP,wrapperPrefix="/@fs"){return[`import { mountHarness } from ${JSON.stringify(appPath)};`,wrapperPath?`import { ${wrapperExport} as Wrapper } from ${JSON.stringify(wrapperPrefix+wrapperPath)};`:"const Wrapper = undefined;",`const fixtures = import.meta.glob('/src/**/*.fixtures.{ts,tsx}', { eager: true });`,`const barrels = import.meta.glob(['/src/index.ts', '/src/index.*.ts', '!/src/index.*.test.ts'], { eager: true });`,`const pages = import.meta.glob(['/src/pages/*/page.json', '/src/project_templates/*/pages/*/page.json'], { eager: true, import: 'default' });`,`const mocks = import.meta.glob(['/src/pages/*/mock.json', '/src/project_templates/*/pages/*/mock.json'], { eager: true, import: 'default' });`,`mountHarness({ fixtures, barrels, pages, mocks, Wrapper });`].join("\n")}function engineHarness({root,wrapper=null}){const[wrapperFile,wrapperExport="default"]=wrapper?wrapper.split("#"):[];const wrapperPath=wrapperFile?resolve(root,wrapperFile):null;return{name:"engine-harness",resolveId(id){return id===ENTRY?`\0${ENTRY}`:void 0},load(id){return id===`\0${ENTRY}`?entryCode(wrapperPath,wrapperExport):void 0},configureServer(server){server.middlewares.use(async(req,res,next)=>{if(!req.url||!req.url.startsWith(HARNESS_PATH))return next();const html=await server.transformIndexHtml(req.url,HTML);res.setHeader("content-type","text/html; charset=utf-8");res.end(html)})}}}export{APP,HARNESS_PATH,HTML,engineHarness,entryCode};
|
package/src/harness/server.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createRequire}from"node:module";import{dirname,resolve}from"node:path";import{fileURLToPath,pathToFileURL}from"node:url";import{loadConfig}from"../lib/config.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{engineHarness}from"./plugin.mjs";const fromRepo=createRequire(resolve(REPO_ROOT,"package.json"));const importFromRepo=name=>import(pathToFileURL(fromRepo.resolve(name)).href);async function createHarnessServer({port=0,open=false}={}){const config=await loadConfig();const{createServer}=await importFromRepo("vite");const react=(await importFromRepo("@vitejs/plugin-react")).default;const server=await createServer({configFile:false,root:REPO_ROOT,logLevel:"warn",plugins:[react(),engineHarness({root:REPO_ROOT,wrapper:config.harness.wrapper})],define:{__OA_BUNDLE_NAME__:JSON.stringify(config.org)},resolve:{alias:{"@":resolve(REPO_ROOT,"src"),...Object.fromEntries(Object.entries(config.harness.alias).map(([k,v])=>[k,resolve(REPO_ROOT,v)]))}},server:{port,strictPort:Boolean(port),open,fs:{allow:[REPO_ROOT,dirname(fileURLToPath(import.meta.url))]}}});await server.listen();const address=server.httpServer?.address();if(!address||typeof address==="string")throw new Error("[harness] the Vite server did not bind a port");return{server,origin:`http://localhost:${address.port}`}}export{createHarnessServer};
|
|
1
|
+
import{createReadStream,existsSync,mkdirSync,statSync,writeFileSync}from"node:fs";import{createServer as createHttpServer}from"node:http";import{createRequire}from"node:module";import{dirname,extname,join,normalize,resolve}from"node:path";import{fileURLToPath,pathToFileURL}from"node:url";import{loadConfig}from"../lib/config.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{engineHarness}from"./plugin.mjs";const fromRepo=createRequire(resolve(REPO_ROOT,"package.json"));const importFromRepo=name=>import(pathToFileURL(fromRepo.resolve(name)).href);async function createHarnessServer({port=0,open=false}={}){const config=await loadConfig();const{createServer}=await importFromRepo("vite");const react=(await importFromRepo("@vitejs/plugin-react")).default;const server=await createServer({configFile:false,root:REPO_ROOT,logLevel:"warn",plugins:[react(),engineHarness({root:REPO_ROOT,wrapper:config.harness.wrapper})],define:{__OA_BUNDLE_NAME__:JSON.stringify(config.org)},optimizeDeps:{include:["react","react-dom/client","@peripheries/renderer/core","@peripheries/renderer/react","@peripheries/renderer/runtime","@peripheries/renderer/testing"]},resolve:{dedupe:["react","react-dom","@peripheries/renderer","@peripheries/runtime"],alias:{"@":resolve(REPO_ROOT,"src"),...Object.fromEntries(Object.entries(config.harness.alias).map(([k,v])=>[k,resolve(REPO_ROOT,v)]))}},server:{port,strictPort:Boolean(port),open,fs:{allow:[REPO_ROOT,dirname(fileURLToPath(import.meta.url))]}}});await server.listen();const address=server.httpServer?.address();if(!address||typeof address==="string")throw new Error("[harness] the Vite server did not bind a port");return{server,origin:`http://localhost:${address.port}`}}async function buildAndServeHarness(){const config=await loadConfig();const{build}=await importFromRepo("vite");const react=(await importFromRepo("@vitejs/plugin-react")).default;const{entryCode,HTML,APP}=await import("./plugin.mjs");const cache=resolve(REPO_ROOT,"node_modules/.cache/engine-harness");const outDir=resolve(cache,"dist");mkdirSync(cache,{recursive:true});const[wrapperFile,wrapperExport="default"]=config.harness.wrapper?config.harness.wrapper.split("#"):[];writeFileSync(resolve(cache,"entry.mjs"),entryCode(wrapperFile?resolve(REPO_ROOT,wrapperFile):null,wrapperExport,APP,""));writeFileSync(resolve(cache,"harness.html"),HTML.replace(/<script type="module" src="[^"]*"><\/script>/,'<script type="module" src="./entry.mjs"><\/script>'));await build({configFile:false,root:REPO_ROOT,mode:"development",logLevel:"warn",plugins:[react()],define:{__OA_BUNDLE_NAME__:JSON.stringify(config.org),"process.env.NODE_ENV":JSON.stringify("development")},resolve:{dedupe:["react","react-dom","@peripheries/renderer","@peripheries/runtime"],alias:{"@":resolve(REPO_ROOT,"src"),...Object.fromEntries(Object.entries(config.harness.alias).map(([k,v])=>[k,resolve(REPO_ROOT,v)]))}},build:{outDir,emptyOutDir:true,minify:false,sourcemap:false,rollupOptions:{input:resolve(cache,"harness.html")}}});const TYPES={".html":"text/html; charset=utf-8",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2"};const server=createHttpServer((req,res)=>{const path=normalize(decodeURIComponent((req.url??"/").split("?")[0])).replace(/^(\.\.[/\\])+/,"");const file=join(outDir,path);if(!file.startsWith(outDir)||!existsSync(file)||statSync(file).isDirectory()){res.statusCode=404;return res.end("not found")}res.setHeader("content-type",TYPES[extname(file)]??"application/octet-stream");createReadStream(file).pipe(res)});await new Promise(r=>server.listen(0,"127.0.0.1",r));const{port}=server.address();return{origin:`http://127.0.0.1:${port}`,path:"/node_modules/.cache/engine-harness/harness.html",close:()=>new Promise(r=>server.close(r))}}export{buildAndServeHarness,createHarnessServer};
|
package/src/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type LineageKind = 'main' | 'public' | 'page' | 'form';
|
|
1
|
+
export type LineageKind = 'main' | 'public' | 'page' | 'form' | 'shared';
|
|
2
2
|
|
|
3
3
|
export type LineageSpec =
|
|
4
4
|
| string
|
|
@@ -8,6 +8,8 @@ export type LineageSpec =
|
|
|
8
8
|
entry?: string;
|
|
9
9
|
sourceRoot?: string | null;
|
|
10
10
|
dist?: string;
|
|
11
|
+
specifier?: string;
|
|
12
|
+
shared?: string[];
|
|
11
13
|
build?: { script: string; env?: Record<string, string> };
|
|
12
14
|
};
|
|
13
15
|
|
|
@@ -37,6 +39,8 @@ export type Lineage = {
|
|
|
37
39
|
sourceRoot: string | null;
|
|
38
40
|
dist: string;
|
|
39
41
|
build?: { script: string; env?: Record<string, string> };
|
|
42
|
+
specifier?: string;
|
|
43
|
+
shared?: string[];
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
export const DEFAULT_CHECKS: string[];
|
|
@@ -44,7 +48,14 @@ export const MAIN_LINEAGE: 'main';
|
|
|
44
48
|
export const PUBLIC_LINEAGE: 'public';
|
|
45
49
|
export function defineConfig(config: EngineConfig): EngineConfig;
|
|
46
50
|
export function lineageDefaults(name: string, kind?: LineageKind): Lineage;
|
|
47
|
-
export
|
|
51
|
+
export type LoadedConfig = {
|
|
52
|
+
org: string;
|
|
53
|
+
lineages: Lineage[];
|
|
54
|
+
shared: string[];
|
|
55
|
+
checks: { run: string[]; peerRepos: string[]; gated: { projectTemplates: string[]; orgTemplates: string[] } };
|
|
56
|
+
harness: { wrapper: string | null; widths: number[]; container: number; overflow: 'warn' | 'error'; output: string; alias: Record<string, string> };
|
|
57
|
+
};
|
|
58
|
+
export function loadConfig(): Promise<LoadedConfig>;
|
|
48
59
|
export function lineageConfig(name: string): Promise<Lineage>;
|
|
49
60
|
|
|
50
61
|
export type Scenario<P extends Record<string, unknown> = Record<string, unknown>> = {
|
package/src/lib/config.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{REPO_ROOT}from"./root.mjs";const MAIN_LINEAGE="main";const DEFAULT_CHECKS=["validate-externals","check-wired","check-routes","check-design-refs","check-skills","check-work-orders","check-cutover-ownership","check-fixtures","knowledge"];const PUBLIC_LINEAGE="public";const CONFIG_FILES=["engine.config.ts","engine.config.mts","engine.config.mjs","engine.config.js"];function defineConfig(config){return config}function lineageDefaults(name,kind){if(name===MAIN_LINEAGE){return{name,kind:"main",entry:"src/index.ts",sourceRoot:null,dist:"dist"}}if(name===PUBLIC_LINEAGE){return{name,kind:"public",entry:"src/index.public.ts",sourceRoot:"components/public/",dist:"dist-public"}}const k=kind??"page";return{name,kind:k,entry:`src/index.${name}.ts`,sourceRoot:k==="form"?`components/form/${name}/`:`components/project_page/${name}/`,dist:`dist-bundles/${name}`}}function normalize(raw,origin){if(!raw||typeof raw!=="object")throw new Error(`[engine] ${origin} must export a config object`);if(typeof raw.org!=="string"||!raw.org)throw new Error(`[engine] ${origin}: \`org\` (the org slug) is required`);const list=raw.lineages??[MAIN_LINEAGE];const lineages=list.map(l=>{const spec=typeof l==="string"?{name:l}:l;if(!spec?.name)throw new Error(`[engine] ${origin}: every lineage needs a name`);return{...lineageDefaults(spec.name,spec.kind),...spec}});if(
|
|
1
|
+
import{existsSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{pathToFileURL}from"node:url";import{REPO_ROOT}from"./root.mjs";const MAIN_LINEAGE="main";const DEFAULT_CHECKS=["validate-externals","check-wired","check-routes","check-design-refs","check-skills","check-work-orders","check-cutover-ownership","check-fixtures","knowledge"];const PUBLIC_LINEAGE="public";const CONFIG_FILES=["engine.config.ts","engine.config.mts","engine.config.mjs","engine.config.js"];function defineConfig(config){return config}function lineageDefaults(name,kind){if(name===MAIN_LINEAGE){return{name,kind:"main",entry:"src/index.ts",sourceRoot:null,dist:"dist"}}if(name===PUBLIC_LINEAGE){return{name,kind:"public",entry:"src/index.public.ts",sourceRoot:"components/public/",dist:"dist-public"}}const k=kind??"page";return{name,kind:k,entry:`src/index.${name}.ts`,sourceRoot:k==="form"?`components/form/${name}/`:k==="shared"?`${name}/`:`components/project_page/${name}/`,dist:`dist-bundles/${name}`}}function normalize(raw,origin){if(!raw||typeof raw!=="object")throw new Error(`[engine] ${origin} must export a config object`);if(typeof raw.org!=="string"||!raw.org)throw new Error(`[engine] ${origin}: \`org\` (the org slug) is required`);const list=raw.lineages??[MAIN_LINEAGE];const lineages=list.map(l=>{const spec=typeof l==="string"?{name:l}:l;if(!spec?.name)throw new Error(`[engine] ${origin}: every lineage needs a name`);return{...lineageDefaults(spec.name,spec.kind),...spec}});const mainAt=lineages.findIndex(l=>l.name===MAIN_LINEAGE);if(mainAt>0)throw new Error(`[engine] ${origin}: main must be the first lineage`);if(mainAt===-1&&!lineages.every(l=>l.kind==="shared")){throw new Error(`[engine] ${origin}: lineages must start with "main", unless every lineage is shared`)}for(const l of lineages){if(l.kind==="shared"&&!/^@[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test(l.specifier??"")){throw new Error(`[engine] ${origin}: shared lineage "${l.name}" needs a specifier like "@stack/${l.name}"`)}}const gated=raw.checks?.gated??{};const checks={run:raw.checks?.run??DEFAULT_CHECKS,peerRepos:raw.checks?.peerRepos??[],gated:{projectTemplates:gated.projectTemplates??[],orgTemplates:gated.orgTemplates??[]}};const shared=raw.shared??[];const harness={wrapper:raw.harness?.wrapper??null,widths:raw.harness?.widths??[390,768,1280],container:raw.harness?.container??320,overflow:raw.harness?.overflow??"warn",output:raw.harness?.output??"test/visual/__output__",alias:raw.harness?.alias??{}};return{org:raw.org,lineages,checks,shared,harness}}const normalizeForTest=raw=>normalize(raw,"test");let cached;async function loadConfig(){if(cached)return cached;const file=CONFIG_FILES.map(f=>resolve(REPO_ROOT,f)).find(f=>existsSync(f));if(file){const mod=await import(pathToFileURL(file).href);cached=normalize(mod.default??mod,file);return cached}const pkg=JSON.parse(readFileSync(resolve(REPO_ROOT,"package.json"),"utf8"));let org=pkg.oaBundle?.name;const orgJson=resolve(REPO_ROOT,"template/config/org.json");if(existsSync(orgJson))org=JSON.parse(readFileSync(orgJson,"utf8")).slug??org;const lineages=(pkg.oaBundle?.lineages??[MAIN_LINEAGE]).map(n=>n==="workbooks"?{name:n,kind:"form"}:n);cached=normalize({org,lineages},"package.json oaBundle");return cached}async function lineageConfig(name){const{lineages}=await loadConfig();return lineages.find(l=>l.name===name)??lineageDefaults(name)}export{DEFAULT_CHECKS,MAIN_LINEAGE,PUBLIC_LINEAGE,defineConfig,lineageConfig,lineageDefaults,loadConfig,normalizeForTest};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{register}from"node:module";register("data:text/javascript,"+encodeURIComponent("export async function load(url, context, next) { return url.endsWith('.css') || /\\.css\\?/.test(url) ? { format: 'module', source: '', shortCircuit: true } : next(url, context); }"));
|
package/src/lib/knowledge.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync,readdirSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toDisplayName}from"./jsdoc-parser.mjs";import{pathToFileURL}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"./root.mjs";function registryOf(entry){const out={};for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(file&&hasComponentJSDoc(file,name))out[name]={props_schema:parseJSDocFile(file,name).props_schema??void 0}}return out}const repoRoot=REPO_ROOT_;function readDoc(path){const text=readFileSync(path,"utf8");const m=text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);if(!m)throw new Error(`[knowledge] ${path} has no front matter`);const meta={};for(const line of m[1].split("\n")){const kv=line.match(/^([\w-]+):\s*(.*)$/);if(!kv)continue;const raw=kv[2].trim();try{meta[kv[1]]=JSON.parse(raw)}catch{meta[kv[1]]=raw}}return{meta,body:m[2].trim()}}const docsIn=dir=>existsSync(resolve(repoRoot,dir))?readdirSync(resolve(repoRoot,dir)).filter(f=>f.endsWith(".md")).sort().map(f=>({key:f.replace(/\.md$/,""),...readDoc(resolve(repoRoot,dir,f))})):[];const str=v=>typeof v==="string"?v:void 0;const strs=v=>Array.isArray(v)?v.map(String):[];function oneLine(text,fallback){const first=(text??"").split(/(?<=[.!?])\s+/)[0]?.trim()||fallback;if(first.length<=200)return first;return`${first.slice(0,199).replace(/\s+\S*$/,"")}\u2026`}function isPlainJson(v){if(v===null||["string","number","boolean"].includes(typeof v))return true;if(Array.isArray(v))return v.every(isPlainJson);if(typeof v!=="object"||Object.getPrototypeOf(v)!==Object.prototype||"$$typeof"in v)return false;return Object.values(v).every(x=>x===void 0||isPlainJson(x))}async function fixtureExamples(componentFile){const base=componentFile.replace(/\.tsx?$/,"");const file=[".fixtures.ts",".fixtures.tsx"].map(ext=>base+ext).find(f=>existsSync(f));if(!file)return[];const mod=await import(file);return(mod.scenarios??[]).filter(s=>["cell","whole-page"].includes(s.surface??"cell")&&s.props&&Object.keys(s.props).length>0&&isPlainJson(s.props)).map(s=>({name:s.name,props:JSON.parse(JSON.stringify(s.props))})).filter(s=>JSON.stringify(s.props).length<4e3).sort((a,b)=>JSON.stringify(a.props).length-JSON.stringify(b.props).length).slice(0,3)}const holdsReference=v=>typeof v==="string"&&(v.startsWith("@")||v.includes("${"))||(Array.isArray(v)?v.some(holdsReference):typeof v==="object"&&v!==null&&Object.values(v).some(holdsReference));async function loadVocabulary(){const file=["src/catalog/vocabulary.ts","src/catalog/vocabulary.tsx"].map(f=>resolve(repoRoot,f)).find(f=>existsSync(f));if(!file)return{};return(await import(pathToFileURL(file).href)).VOCABULARY??{}}async function buildKnowledge(bundle,org){const VOCABULARY=await loadVocabulary();const lineage=`${org}/${bundle}`;const entry=bundle==="main"?"src/index.ts":`src/index.${bundle}.ts`;const scopeLineage=`${org}/main`;const scopeRegistry=bundle==="main"?void 0:registryOf("src/index.ts");const bundleDocPath=resolve(repoRoot,"src/catalog",`${bundle}.md`);if(!existsSync(bundleDocPath))throw new Error(`[knowledge] no src/catalog/${bundle}.md: every lineage says how to compose a page from it.`);const bundleDoc=readDoc(bundleDocPath);const kits=docsIn("src/catalog/kits").filter(k=>(str(k.meta.bundle)??"main")===bundle);const patterns=docsIn("src/catalog/patterns").filter(p=>(str(p.meta.bundle)??"main")===bundle);const notPageCellFamilies=new Set(strs(bundleDoc.meta.not_page_cell_families));const pageCellExceptions=new Set(strs(bundleDoc.meta.page_cells));const notPageCells=new Set(strs(bundleDoc.meta.not_page_cells));const rows=[];const links=[];const registry={};rows.push({kind:"bundle",key:bundle,title:str(bundleDoc.meta.title)??lineage,summary:str(bundleDoc.meta.summary)??"",body:bundleDoc.body,attributes:{kits:kits.map(k=>k.key),patterns:patterns.map(p=>p.key)}});for(const kit of kits){const vocabulary=str(kit.meta.vocabulary);if(vocabulary&&!VOCABULARY[vocabulary])throw new Error(`[knowledge] kit ${kit.key} names vocabulary "${vocabulary}", which has no generator`);rows.push({kind:"kit",key:kit.key,title:str(kit.meta.title)??kit.key,summary:str(kit.meta.summary)??"",body:kit.body,tags:strs(kit.meta.tags),sort:Number(kit.meta.sort??0),attributes:{families:strs(kit.meta.families),...vocabulary?{vocabulary:VOCABULARY[vocabulary]()}:{}}})}const kitOfFamily=new Map(kits.flatMap(k=>strs(k.meta.families).map(f=>[f,k.key])));const cellId=name=>name.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(!file||!hasComponentJSDoc(file,name))continue;const j=parseJSDocFile(file,name);registry[name]={props_schema:j.props_schema??void 0};const family=j.family??"other";const pageCell=j.page_cell??(notPageCells.has(name)?false:pageCellExceptions.has(name)||!notPageCellFamilies.has(family));const weight=e=>(holdsReference(e)?1e6:0)+JSON.stringify(e).length;const examples=[...j.usage_examples??[]].sort((a,b)=>weight(a)-weight(b));const snippets=examples.length?examples.map((props,i)=>({title:`${name}, example ${i+1}`,cells:[{id:cellId(name),component:name,props}]})):(await fixtureExamples(file)).map(f=>({title:`${name}, ${f.name}`,cells:[{id:cellId(name),component:name,props:f.props}]}));const useCases=j.use_cases?.length?`
|
|
1
|
+
import{existsSync,readdirSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toDisplayName}from"./jsdoc-parser.mjs";import{pathToFileURL}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"./root.mjs";function registryOf(entry){const out={};for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(file&&hasComponentJSDoc(file,name))out[name]={props_schema:parseJSDocFile(file,name).props_schema??void 0}}return out}const repoRoot=REPO_ROOT_;function readDoc(path){const text=readFileSync(path,"utf8");const m=text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);if(!m)throw new Error(`[knowledge] ${path} has no front matter`);const meta={};for(const line of m[1].split("\n")){const kv=line.match(/^([\w-]+):\s*(.*)$/);if(!kv)continue;const raw=kv[2].trim();try{meta[kv[1]]=JSON.parse(raw)}catch{meta[kv[1]]=raw}}return{meta,body:m[2].trim()}}const docsIn=dir=>existsSync(resolve(repoRoot,dir))?readdirSync(resolve(repoRoot,dir)).filter(f=>f.endsWith(".md")).sort().map(f=>({key:f.replace(/\.md$/,""),...readDoc(resolve(repoRoot,dir,f))})):[];const str=v=>typeof v==="string"?v:void 0;const strs=v=>Array.isArray(v)?v.map(String):[];function oneLine(text,fallback){const first=(text??"").split(/(?<=[.!?])\s+/)[0]?.trim()||fallback;if(first.length<=200)return first;return`${first.slice(0,199).replace(/\s+\S*$/,"")}\u2026`}function isPlainJson(v){if(v===null||["string","number","boolean"].includes(typeof v))return true;if(Array.isArray(v))return v.every(isPlainJson);if(typeof v!=="object"||Object.getPrototypeOf(v)!==Object.prototype||"$$typeof"in v)return false;return Object.values(v).every(x=>x===void 0||isPlainJson(x))}async function fixtureExamples(componentFile){const base=componentFile.replace(/\.tsx?$/,"");const file=[".fixtures.ts",".fixtures.tsx"].map(ext=>base+ext).find(f=>existsSync(f));if(!file)return[];const mod=await import(file);return(mod.scenarios??[]).filter(s=>["cell","whole-page"].includes(s.surface??"cell")&&s.props&&Object.keys(s.props).length>0&&isPlainJson(s.props)).map(s=>({name:s.name,props:JSON.parse(JSON.stringify(s.props))})).filter(s=>JSON.stringify(s.props).length<4e3).sort((a,b)=>JSON.stringify(a.props).length-JSON.stringify(b.props).length).slice(0,3)}const holdsReference=v=>typeof v==="string"&&(v.startsWith("@")||v.includes("${"))||(Array.isArray(v)?v.some(holdsReference):typeof v==="object"&&v!==null&&Object.values(v).some(holdsReference));async function loadVocabulary(){const file=["src/catalog/vocabulary.ts","src/catalog/vocabulary.tsx"].map(f=>resolve(repoRoot,f)).find(f=>existsSync(f));if(!file)return{};return(await import(pathToFileURL(file).href)).VOCABULARY??{}}async function buildKnowledge(bundle,org){const VOCABULARY=await loadVocabulary();const lineage=`${org}/${bundle}`;const entry=bundle==="main"?"src/index.ts":`src/index.${bundle}.ts`;const scopeLineage=`${org}/main`;const scopeRegistry=bundle==="main"||!existsSync(resolve(repoRoot,"src/index.ts"))?void 0:registryOf("src/index.ts");const bundleDocPath=resolve(repoRoot,"src/catalog",`${bundle}.md`);if(!existsSync(bundleDocPath))throw new Error(`[knowledge] no src/catalog/${bundle}.md: every lineage says how to compose a page from it.`);const bundleDoc=readDoc(bundleDocPath);const kits=docsIn("src/catalog/kits").filter(k=>(str(k.meta.bundle)??"main")===bundle);const patterns=docsIn("src/catalog/patterns").filter(p=>(str(p.meta.bundle)??"main")===bundle);const notPageCellFamilies=new Set(strs(bundleDoc.meta.not_page_cell_families));const pageCellExceptions=new Set(strs(bundleDoc.meta.page_cells));const notPageCells=new Set(strs(bundleDoc.meta.not_page_cells));const rows=[];const links=[];const registry={};rows.push({kind:"bundle",key:bundle,title:str(bundleDoc.meta.title)??lineage,summary:str(bundleDoc.meta.summary)??"",body:bundleDoc.body,attributes:{kits:kits.map(k=>k.key),patterns:patterns.map(p=>p.key)}});for(const kit of kits){const vocabulary=str(kit.meta.vocabulary);if(vocabulary&&!VOCABULARY[vocabulary])throw new Error(`[knowledge] kit ${kit.key} names vocabulary "${vocabulary}", which has no generator`);rows.push({kind:"kit",key:kit.key,title:str(kit.meta.title)??kit.key,summary:str(kit.meta.summary)??"",body:kit.body,tags:strs(kit.meta.tags),sort:Number(kit.meta.sort??0),attributes:{families:strs(kit.meta.families),...vocabulary?{vocabulary:VOCABULARY[vocabulary]()}:{}}})}const kitOfFamily=new Map(kits.flatMap(k=>strs(k.meta.families).map(f=>[f,k.key])));const cellId=name=>name.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(!file||!hasComponentJSDoc(file,name))continue;const j=parseJSDocFile(file,name);registry[name]={props_schema:j.props_schema??void 0};const family=j.family??"other";const pageCell=j.page_cell??(notPageCells.has(name)?false:pageCellExceptions.has(name)||!notPageCellFamilies.has(family));const weight=e=>(holdsReference(e)?1e6:0)+JSON.stringify(e).length;const examples=[...j.usage_examples??[]].sort((a,b)=>weight(a)-weight(b));const snippets=examples.length?examples.map((props,i)=>({title:`${name}, example ${i+1}`,cells:[{id:cellId(name),component:name,props}]})):(await fixtureExamples(file)).map(f=>({title:`${name}, ${f.name}`,cells:[{id:cellId(name),component:name,props:f.props}]}));const useCases=j.use_cases?.length?`
|
|
2
2
|
|
|
3
3
|
Use it for:
|
|
4
4
|
${j.use_cases.map(u=>`- ${u}`).join("\n")}`:"";rows.push({kind:"component",key:name,title:toDisplayName(name),summary:oneLine(j.summary??j.description,`${toDisplayName(name)}.`),body:`${j.agent_instructions??j.description??""}${useCases}`.trim()||null,tags:j.tags??[],snippets,page_cell:pageCell,attributes:{family,...j.print_policy||j.size?{capabilities:{...j.print_policy?{print_policy:j.print_policy}:{},...j.size?{size:true}:{}}}:{}}});const kit=j.kit??kitOfFamily.get(family);if(kit)links.push({from:{kind:"component",key:name},relation:"part_of",to:{kind:"kit",key:kit}});for(const l of j.links??[])links.push({from:{kind:"component",key:name},relation:l.relation,to:{kind:"component",key:l.to},note:l.note})}for(const pattern of patterns){const slug=str(pattern.meta.page);if(!slug)throw new Error(`[knowledge] pattern ${pattern.key} names no page`);const dir=slug.includes("/")?resolve(repoRoot,"src",slug):resolve(repoRoot,"src/pages",slug);const page=JSON.parse(readFileSync(resolve(dir,"page.json"),"utf8"));const mock=existsSync(resolve(dir,"mock.json"))?JSON.parse(readFileSync(resolve(dir,"mock.json"),"utf8")):{};const data={...mock.data??{}};delete data.params;rows.push({kind:"pattern",key:pattern.key,title:str(pattern.meta.title)??pattern.key,summary:str(pattern.meta.summary)??"",body:pattern.body,tags:strs(pattern.meta.tags),sort:Number(pattern.meta.sort??0),snippets:[{title:str(pattern.meta.title)??pattern.key,intent:str(pattern.meta.summary),cells:page.content.cells,layout:page.layout,data}],attributes:{layout:page.layout?.type??"stack",...page.layout?.profile?{profile:page.layout.profile}:{},page:slug}});for(const component of new Set([...page.content.cells.map(c=>c.component).filter(c=>Boolean(c)),...strs(pattern.meta.uses)])){const inMain=scopeRegistry&&!registry[component]&&scopeRegistry[component];links.push({from:{kind:"pattern",key:pattern.key},relation:"uses",to:{...inMain?{lineage:scopeLineage}:{},kind:"component",key:component}})}for(const kit of strs(pattern.meta.kits))links.push({from:{kind:"pattern",key:pattern.key},relation:"uses",to:{kind:"kit",key:kit}})}return{lineage,rows,links,registry,...scopeRegistry?{scopeLineage,scopeRegistry}:{}}}export{buildKnowledge,holdsReference};
|