@peripheries/engine 0.8.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peripheries/engine",
3
- "version": "0.8.3",
3
+ "version": "0.10.0",
4
4
  "description": "Build, check, knowledge and deploy for peripheries component repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1 +1 @@
1
- const EXTERNALS=Object.freeze(["react","react/jsx-runtime","react/jsx-dev-runtime","react-dom","react-dom/client","zustand","@oa/store","@stack/store"]);export{EXTERNALS};
1
+ const EXTERNALS=Object.freeze(["react","react/jsx-runtime","react/jsx-dev-runtime","react-dom","react-dom/client","zustand","@stack/store"]);export{EXTERNALS};
@@ -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{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??[]);const missing=await page.$$eval("[data-missing-component]",els=>els.map(e=>e.getAttribute("data-missing-component")));for(const name of new Set(missing))result.errors.push(`${name} isn't exported by any barrel or shared lineage`);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}
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";import{selectChanged}from"../harness/changed.mjs";const argv=process.argv.slice(2);const config=await loadConfig();const{harness}=config;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 base=parseFlagValue(argv,"changed");const listOnly=argv.includes("--list");const{fixtureFiles,pageFiles}=await sourceFiles();let selection=null;if(base){const s=selection=await selectChanged({root:REPO_ROOT,base,config,fixtureFiles,pageFiles,pageKey});if(s.all)console.log(`[test-visual] --changed ${base}: running everything \u2014 ${s.all}`);else console.log(`[test-visual] --changed ${base}: ${s.changed.length} changed file(s) \u2192 ${s.fixtures.size} fixture file(s), ${s.pages.size} page(s)`);if(!s.all&&!s.fixtures.size&&!s.pages.size){console.log("[test-visual] nothing the changes affect renders here; nothing to do");if(!listOnly){await rm(OUTPUT,{recursive:true,force:true});await mkdir(OUTPUT,{recursive:true});await writeFile(join(OUTPUT,"report.json"),JSON.stringify({widths:harness.widths,container:harness.container,mode:"changed",base,selected:[],reports:[],skipped:[]},null,2)+"\n")}process.exit(0)}if(listOnly){if(!s.all){for(const[file,why]of s.fixtures)console.log(` fixture ${relative(REPO_ROOT,file)} \u2014 ${why}`);for(const[key,why]of s.pages)console.log(` page ${key} \u2014 ${why}`)}process.exit(0)}}const reasonOf=t=>!selection?void 0:selection.all?`everything: ${selection.all}`:t.kind==="page"?selection.pages.get(t.key):selection.fixtures.get(t.file);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&&(!selection||selection.all||reasonOf(t)));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));const changed=selection?{mode:"changed",base,selected:targets.map(t=>({target:t.label,reason:reasonOf(t)}))}:{};await writeFile(join(OUTPUT,"report.json"),JSON.stringify({widths:harness.widths,container:harness.container,...changed,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 sourceFiles(){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 fixtureFiles2=(await walk(resolve(REPO_ROOT,"src"))).filter(f=>/\.fixtures\.tsx?$/.test(f)&&!relative(REPO_ROOT,f).includes("node_modules/")).sort();const pageFiles2=[...(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();return{fixtureFiles:fixtureFiles2,pageFiles:pageFiles2}}function pageKey(file){const m=file.match(/\/project_templates\/([^/]+)\/pages\/([^/]+)\/page\.json$/)??file.match(/\/pages\/([^/]+)\/page\.json$/);return m.slice(1).join("/")}async function listTargets(vite){const targets=[];for(const file of fixtureFiles){const name=file.split("/").pop().replace(/\.fixtures\.tsx?$/,"");if(selection&&!selection.all&&!selection.fixtures.has(file))continue;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",file,label:`${name} \xB7 ${s.name}`,query:`fixture=${encodeURIComponent(name)}&scenario=${encodeURIComponent(s.name)}`,designIntent:s.designIntent})}}for(const file of pageFiles){const key=pageKey(file);targets.push({kind:"page",key,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??[]);const missing=await page.$$eval("[data-missing-component]",els=>els.map(e=>e.getAttribute("data-missing-component")));for(const name of new Set(missing))result.errors.push(`${name} isn't exported by any barrel or shared lineage`);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
  import{Fragment,jsx,jsxs}from"react/jsx-runtime";import"@peripheries/renderer/styles.css";import{pageRuntimeFromValues,projectCellActions}from"@peripheries/renderer/core";import{PageRenderer}from"@peripheries/renderer/react";import{getStackStore}from"@peripheries/renderer/runtime";import{FIXTURE_BASE_PROPS}from"@peripheries/renderer/testing";import{Component,isValidElement,useEffect,useMemo}from"react";import{createRoot}from"react-dom/client";const isComponent=v=>typeof v==="function"||typeof v==="object"&&v!==null&&"$$typeof"in v;function sharedBundle({specifier,module}){const components=Object.fromEntries(Object.entries(module).filter(([name,v])=>/^[A-Z]/.test(name)&&isComponent(v)));return{id:specifier,label:specifier,specifier,components,contracts:{}}}const fixtureName=path=>path.split("/").pop().replace(/\.fixtures\.tsx?$/,"");const pageKey=path=>{const m=path.match(/\/project_templates\/([^/]+)\/pages\/([^/]+)\/page\.json$/)??path.match(/\/pages\/([^/]+)\/page\.json$/);return m?m.slice(1).join("/"):path};function isJson(v){if(v===null||["string","number","boolean"].includes(typeof v))return true;if(Array.isArray(v))return v.every(isJson);if(typeof v!=="object"||isValidElement(v))return false;const proto=Object.getPrototypeOf(v);return(proto===Object.prototype||proto===null)&&Object.values(v).every(isJson)}class Boundary extends Component{state={error:null};static getDerivedStateFromError(error){return{error}}componentDidCatch(error,info){window.__ENGINE_HARNESS_ERROR=`${error.message}
2
- ${info.componentStack??""}`}render(){return this.state.error?jsx("pre",{role:"alert",children:this.state.error.message}):this.props.children}}function Ready({children}){useEffect(()=>{const id=requestAnimationFrame(()=>requestAnimationFrame(()=>window.__ENGINE_HARNESS_READY=true));return()=>cancelAnimationFrame(id)},[]);return jsx(Fragment,{children})}function RenderedPage({row,values,actions,baseProps,components,shared,Wrapper,statusOverride}){const runtime=useMemo(()=>{const rt=pageRuntimeFromValues(row,{values});if(statusOverride&&(statusOverride.isLoading||statusOverride.error)){const error=statusOverride.error?new Error(statusOverride.error):null;for(const cell of rt.cells)rt.cellData[cell.id]??={static:{},declared:{}};for(const own of Object.values(rt.cellData)){const keys=Object.keys(own.static).length?Object.keys(own.static):["fixture"];for(const k of keys)own.static[k]={data:void 0,isLoading:Boolean(statusOverride.isLoading),error}}}return rt},[row,values,statusOverride]);const bundles=useMemo(()=>{const ThemeProvider=Wrapper?({children})=>jsx(Wrapper,{baseProps,children}):void 0;const bundle={id:"harness",label:"harness",components,contracts:{},...ThemeProvider?{ThemeProvider}:{}};return{scope:[bundle,...shared],forCell:()=>null}},[components,shared,Wrapper,baseProps]);const environment=useMemo(()=>{const pageActions=Object.fromEntries(Object.keys(actions??{}).map(k=>[k,async input=>(console.info(`[harness] action ${k}`,input),{ok:true})]));window.__ENGINE_HARNESS_DIAGNOSTICS=[];return{core:{},navigate:()=>{},store:getStackStore(),actionsFor:cell=>cell.actions?projectCellActions(cell.actions,pageActions):Object.keys(pageActions).length?pageActions:void 0,onCellError:(cellId,error)=>console.error(`[harness] cell "${cellId}" failed:`,error),onDiagnostic:d=>{window.__ENGINE_HARNESS_DIAGNOSTICS.push({code:d.code,severity:d.severity,message:d.message,...d.cellId?{cellId:d.cellId}:{}});if(d.severity==="error")console.error(`[harness] ${d.code}: ${d.message}`)}}},[actions]);return jsx(PageRenderer,{runtime,bundles,params:{},baseProps,context:{environment:"harness"},environment})}function pageRow(id,json){return{id,name:json.name,org_id:null,project_id:null,process_id:null,graph_node_id:null,page_type:json.page_type??null,status:"published",is_template:json.is_template??false,content:json.content??{cells:[]},layout:json.layout??{type:"stack",gap:16},metadata:json.metadata??null}}function Index({fixtures,pages}){useEffect(()=>void(window.__ENGINE_HARNESS_READY=true),[]);const link=q=>`?${q}`;return jsxs("main",{style:{fontFamily:"system-ui, sans-serif",padding:24,maxWidth:960},children:[jsx("h1",{children:"Harness"}),jsx("h2",{children:"Fixtures"}),fixtures.map(([name,scenarios])=>jsxs("section",{children:[jsx("h3",{children:name}),jsx("ul",{children:scenarios.map(s=>jsxs("li",{children:[jsx("a",{href:link(`fixture=${encodeURIComponent(name)}&scenario=${encodeURIComponent(s.name)}`),children:s.name})," \xB7 ",jsx("a",{href:link(`fixture=${encodeURIComponent(name)}&scenario=${encodeURIComponent(s.name)}&container=320`),children:"in 320px"})]},s.name))})]},name)),jsx("h2",{children:"Pages"}),jsx("ul",{children:pages.map(p=>jsx("li",{children:jsx("a",{href:link(`page=${encodeURIComponent(p)}`),children:p})},p))})]})}function mountHarness(sources){const q=new URLSearchParams(window.location.search);const components=Object.assign({},...Object.values(sources.barrels));const shared=(sources.shared??[]).map(sharedBundle);const fixtures=Object.entries(sources.fixtures).map(([path,mod])=>[fixtureName(path),[...mod.scenarios??[]]]).sort(([a],[b])=>a.localeCompare(b));const pages=Object.fromEntries(Object.entries(sources.pages).map(([path,json])=>[pageKey(path),{json,mock:sources.mocks[path.replace(/page\.json$/,"mock.json")]}]));let body;const fixture=q.get("fixture");const page=q.get("page");if(fixture){const scenario=fixtures.find(([n])=>n===fixture)?.[1].find(s=>s.name===q.get("scenario"));if(!scenario){window.__ENGINE_HARNESS_ERROR=`no scenario "${q.get("scenario")}" for ${fixture}`;body=null}else if(!isJson(scenario.props??{})){const Direct=components[fixture]??shared.find(b=>b.components[fixture])?.components[fixture];if(!Direct)window.__ENGINE_HARNESS_ERROR=`${fixture} is not exported from any barrel or shared lineage`;const baseProps={...FIXTURE_BASE_PROPS,...scenario.baseProps};const element=Direct?jsx("div",{className:"cell","data-engine-direct":"",children:jsx(Direct,{...scenario.props,baseProps})}):null;body=sources.Wrapper?jsx(sources.Wrapper,{baseProps,children:element}):element}else{const cell={id:"fixture",component:fixture,props:scenario.props??{},...scenario.data?{data:{static:scenario.data}}:{},...scenario.selections?{selections:scenario.selections}:{},...scenario.viewState?{viewState:scenario.viewState}:{},...scenario.live?{live:scenario.live}:{}};body=jsx(RenderedPage,{row:pageRow(`${fixture}/${scenario.name}`,{name:`${fixture} \xB7 ${scenario.name}`,content:{cells:[cell]}}),values:{},actions:scenario.actions,baseProps:{...FIXTURE_BASE_PROPS,...scenario.baseProps},components,shared,Wrapper:sources.Wrapper,statusOverride:scenario.status})}}else if(page){const entry=pages[page];if(!entry){window.__ENGINE_HARNESS_ERROR=`no page "${page}"`;body=null}else{const values={...entry.mock?.data};body=jsx(RenderedPage,{row:pageRow(page,entry.json),values,actions:entry.mock?.actions,baseProps:{...FIXTURE_BASE_PROPS,...entry.mock?.baseProps},components,shared,Wrapper:sources.Wrapper})}}else{body=jsx(Index,{fixtures,pages:Object.keys(pages).sort()})}const container=Number(q.get("container"))||null;const framed=container?jsx("div",{"data-engine-container":"",style:{width:container,containerType:"inline-size",outline:"1px dashed #bbb"},children:body}):body;createRoot(document.getElementById("root")).render(jsx(Boundary,{children:jsx(Ready,{children:framed})}))}export{mountHarness};
2
+ ${info.componentStack??""}`}render(){return this.state.error?jsx("pre",{role:"alert",children:this.state.error.message}):this.props.children}}function Ready({children}){useEffect(()=>{const id=requestAnimationFrame(()=>requestAnimationFrame(()=>window.__ENGINE_HARNESS_READY=true));return()=>cancelAnimationFrame(id)},[]);return jsx(Fragment,{children})}function RenderedPage({row,values,actions,baseProps,components,shared,Wrapper,statusOverride}){const runtime=useMemo(()=>{const rt=pageRuntimeFromValues(row,{values});if(statusOverride&&(statusOverride.isLoading||statusOverride.error)){const error=statusOverride.error?new Error(statusOverride.error):null;for(const cell of rt.cells)rt.cellData[cell.id]??={static:{},declared:{}};for(const own of Object.values(rt.cellData)){const keys=Object.keys(own.static).length?Object.keys(own.static):["fixture"];for(const k of keys)own.static[k]={data:void 0,isLoading:Boolean(statusOverride.isLoading),error}}}return rt},[row,values,statusOverride]);const bundles=useMemo(()=>{const bundle={id:"harness",label:"harness",components,contracts:{}};return{scope:[bundle,...shared],forCell:()=>null}},[components,shared]);const environment=useMemo(()=>{const pageActions=Object.fromEntries(Object.keys(actions??{}).map(k=>[k,async input=>(console.info(`[harness] action ${k}`,input),{ok:true})]));window.__ENGINE_HARNESS_DIAGNOSTICS=[];return{core:{},navigate:()=>{},store:getStackStore(),actionsFor:cell=>cell.actions?projectCellActions(cell.actions,pageActions):Object.keys(pageActions).length?pageActions:void 0,onCellError:(cellId,error)=>console.error(`[harness] cell "${cellId}" failed:`,error),onDiagnostic:d=>{window.__ENGINE_HARNESS_DIAGNOSTICS.push({code:d.code,severity:d.severity,message:d.message,...d.cellId?{cellId:d.cellId}:{}});if(d.severity==="error")console.error(`[harness] ${d.code}: ${d.message}`)}}},[actions]);const page=jsx(PageRenderer,{runtime,bundles,params:{},baseProps,context:{environment:"harness"},environment});return Wrapper?jsx(Wrapper,{baseProps,children:page}):page}function pageRow(id,json){return{id,name:json.name,org_id:null,project_id:null,process_id:null,graph_node_id:null,page_type:json.page_type??null,status:"published",is_template:json.is_template??false,content:json.content??{cells:[]},layout:json.layout??{type:"stack",gap:16},metadata:json.metadata??null}}function Index({fixtures,pages}){useEffect(()=>void(window.__ENGINE_HARNESS_READY=true),[]);const link=q=>`?${q}`;return jsxs("main",{style:{fontFamily:"system-ui, sans-serif",padding:24,maxWidth:960},children:[jsx("h1",{children:"Harness"}),jsx("h2",{children:"Fixtures"}),fixtures.map(([name,scenarios])=>jsxs("section",{children:[jsx("h3",{children:name}),jsx("ul",{children:scenarios.map(s=>jsxs("li",{children:[jsx("a",{href:link(`fixture=${encodeURIComponent(name)}&scenario=${encodeURIComponent(s.name)}`),children:s.name})," \xB7 ",jsx("a",{href:link(`fixture=${encodeURIComponent(name)}&scenario=${encodeURIComponent(s.name)}&container=320`),children:"in 320px"})]},s.name))})]},name)),jsx("h2",{children:"Pages"}),jsx("ul",{children:pages.map(p=>jsx("li",{children:jsx("a",{href:link(`page=${encodeURIComponent(p)}`),children:p})},p))})]})}function mountHarness(sources){const q=new URLSearchParams(window.location.search);const components=Object.assign({},...Object.values(sources.barrels));const shared=(sources.shared??[]).map(sharedBundle);const fixtures=Object.entries(sources.fixtures).map(([path,mod])=>[fixtureName(path),[...mod.scenarios??[]]]).sort(([a],[b])=>a.localeCompare(b));const pages=Object.fromEntries(Object.entries(sources.pages).map(([path,json])=>[pageKey(path),{json,mock:sources.mocks[path.replace(/page\.json$/,"mock.json")]}]));let body;const fixture=q.get("fixture");const page=q.get("page");if(fixture){const scenario=fixtures.find(([n])=>n===fixture)?.[1].find(s=>s.name===q.get("scenario"));if(!scenario){window.__ENGINE_HARNESS_ERROR=`no scenario "${q.get("scenario")}" for ${fixture}`;body=null}else if(!isJson(scenario.props??{})){const Direct=components[fixture]??shared.find(b=>b.components[fixture])?.components[fixture];if(!Direct)window.__ENGINE_HARNESS_ERROR=`${fixture} is not exported from any barrel or shared lineage`;const baseProps={...FIXTURE_BASE_PROPS,...scenario.baseProps};const element=Direct?jsx("div",{className:"cell","data-engine-direct":"",children:jsx(Direct,{...scenario.props,baseProps})}):null;body=sources.Wrapper?jsx(sources.Wrapper,{baseProps,children:element}):element}else{const cell={id:"fixture",component:fixture,props:scenario.props??{},...scenario.data?{data:{static:scenario.data}}:{},...scenario.selections?{selections:scenario.selections}:{},...scenario.viewState?{viewState:scenario.viewState}:{},...scenario.live?{live:scenario.live}:{}};body=jsx(RenderedPage,{row:pageRow(`${fixture}/${scenario.name}`,{name:`${fixture} \xB7 ${scenario.name}`,content:{cells:[cell]}}),values:{},actions:scenario.actions,baseProps:{...FIXTURE_BASE_PROPS,...scenario.baseProps},components,shared,Wrapper:sources.Wrapper,statusOverride:scenario.status})}}else if(page){const entry=pages[page];if(!entry){window.__ENGINE_HARNESS_ERROR=`no page "${page}"`;body=null}else{const values={...entry.mock?.data};body=jsx(RenderedPage,{row:pageRow(page,entry.json),values,actions:entry.mock?.actions,baseProps:{...FIXTURE_BASE_PROPS,...entry.mock?.baseProps},components,shared,Wrapper:sources.Wrapper})}}else{body=jsx(Index,{fixtures,pages:Object.keys(pages).sort()})}const container=Number(q.get("container"))||null;const framed=container?jsx("div",{"data-engine-container":"",style:{width:container,containerType:"inline-size",outline:"1px dashed #bbb"},children:body}):body;createRoot(document.getElementById("root")).render(jsx(Boundary,{children:jsx(Ready,{children:framed})}))}export{mountHarness};
@@ -0,0 +1 @@
1
+ import{execFileSync}from"node:child_process";import{existsSync,readFileSync,readdirSync,statSync}from"node:fs";import{dirname,join,relative,resolve,sep}from"node:path";import{compilerOptions,loadTypeScript}from"../lib/shared-lineages.mjs";const SOURCE=/\.(?:[cm]?[jt]sx?)$/;const EXTENSIONS=[".ts",".tsx",".mts",".js",".jsx",".mjs"];const git=(root,args)=>execFileSync("git",["-C",root,...args],{encoding:"utf8",stdio:["ignore","pipe","pipe"]});const lines=s=>s.split("\n").map(l=>l.trim()).filter(Boolean);function changedFiles(root,base){try{git(root,["rev-parse","--verify","--quiet",`${base}^{commit}`]);git(root,["merge-base",base,"HEAD"])}catch{return{error:`can't resolve "${base}" here (a shallow clone? fetch it: git fetch origin develop, or check out with fetch-depth: 0)`}}const files=new Set([...lines(git(root,["diff","--name-only","--relative",`${base}...HEAD`])),...lines(git(root,["diff","--name-only","--relative","HEAD"])),...lines(git(root,["ls-files","--others","--exclude-standard"]))]);return{files:[...files].sort()}}function globalReason(path,{root,barrels}){const name=path.split("/").pop();if(name==="package.json"||/^(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/.test(name))return`${path} (dependencies)`;if(/^engine\.config\.[cm]?[jt]s$/.test(path))return`${path} (the engine config)`;if(/^tsconfig[^/]*\.json$/.test(path)||/^vite[^/]*\.[cm]?[jt]s$/.test(path))return`${path} (build config)`;if(path.startsWith(".github/"))return`${path} (CI)`;if(path.startsWith("test/harness/"))return`${path} (the harness)`;if(path.startsWith("src/theme/")||path.startsWith("tokens/")||path.includes("/tokens/"))return`${path} (theme or tokens)`;if(path.split("/").includes("_shared"))return`${path} (a _shared helper)`;if(barrels.has(path))return`${path} (a barrel)`;if(path.endsWith(".css")){const stem=join(root,path.slice(0,-".css".length));if(!EXTENSIONS.some(e=>existsSync(stem+e)))return`${path} (CSS that isn't one component's)`}if(path.startsWith("src/")&&(SOURCE.test(path)||path.endsWith(".css"))&&!existsSync(join(root,path)))return`${path} (a source file was deleted)`;return null}async function importGraph(root,config){const ts=await loadTypeScript();const options=ts?compilerOptions(ts,root):null;const aliases=[["@",resolve(root,"src")],...Object.entries(config.harness.alias??{}).map(([k,v])=>[k,resolve(root,v)])];const asFile=p=>{if(existsSync(p)&&statSync(p).isFile())return p;for(const e of EXTENSIONS)if(existsSync(p+e))return p+e;const bare=p.replace(/\.[cm]?jsx?$/,"");if(bare!==p){for(const e of EXTENSIONS)if(existsSync(bare+e))return bare+e}for(const e of EXTENSIONS)if(existsSync(join(p,"index"+e)))return join(p,"index"+e);return null};const resolveImport=(spec,from)=>{if(spec.startsWith("."))return asFile(resolve(dirname(from),spec));for(const[find,to]of aliases){if(spec===find)return asFile(to);if(find==="@"&&spec.startsWith("@/"))return asFile(join(to,spec.slice(2)))}if(!ts)return null;const r=ts.resolveModuleName(spec,from,options,ts.sys).resolvedModule;return r?.resolvedFileName&&!r.isExternalLibraryImport&&!r.resolvedFileName.includes(`${sep}node_modules${sep}`)?r.resolvedFileName:null};const specsOf=(file,text)=>{if(file.endsWith(".css"))return[...text.matchAll(/@import\s+(?:url\()?\s*['"]([^'"]+)['"]/g)].map(m=>m[1]);if(ts)return ts.preProcessFile(text,true,true).importedFiles.map(f=>f.fileName);return[...text.matchAll(/(?:\bfrom\s*|\bimport\s*\(?\s*)['"]([^'"]+)['"]/g)].map(m=>m[1])};const edges=new Map;const depsOf=file=>{if(!edges.has(file)){let text="";try{text=readFileSync(file,"utf8")}catch{}const deps=new Set;if(SOURCE.test(file)||file.endsWith(".css"))for(const s of specsOf(file,text)){const to=resolveImport(s,file);if(to&&to.startsWith(root+sep))deps.add(to)}edges.set(file,deps)}return edges.get(file)};const closure=file=>{const seen=new Set;const stack=[file];while(stack.length){const f=stack.pop();if(seen.has(f))continue;seen.add(f);for(const d of depsOf(f))stack.push(d)}return seen};return{closure,resolveImport}}function exportMap(files,resolveImport){const names=new Map;const stars=new Set;for(const file of files){const text=readFileSync(file,"utf8");for(const m of text.matchAll(/export\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)){const to=resolveImport(m[2],file);if(!to)continue;for(const part of m[1].split(",")){const name=part.trim().split(/\s+as\s+/).pop()?.trim();if(name&&!names.has(name))names.set(name,to)}}for(const m of text.matchAll(/export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|const|let|class)\s+([A-Za-z_$][\w$]*)/g))if(!names.has(m[1]))names.set(m[1],file);for(const m of text.matchAll(/export\s*\*\s*(?:as\s+\w+\s*)?from\s*['"]([^'"]+)['"]/g)){const to=resolveImport(m[1],file);if(to)stars.add(to)}}return{names,stars}}function componentsOf(json){const out=new Set;const visit=v=>{if(Array.isArray(v))v.forEach(visit);else if(v&&typeof v==="object"){if(typeof v.component==="string")out.add(v.component);Object.values(v).forEach(visit)}};visit(json);return out}async function selectChanged({root,base,config,fixtureFiles,pageFiles,pageKey}){const found=changedFiles(root,base);const result={base,changed:found.files??[],all:null,fixtures:new Map,pages:new Map};if(found.error)return{...result,all:found.error};const barrelFiles=[...new Set([...config.lineages.map(l=>l.entry),...existsSync(join(root,"src"))?readdirSync(join(root,"src")).filter(f=>/^index[^/]*\.[cm]?tsx?$/.test(f)&&!/\.test\./.test(f)).map(f=>`src/${f}`):[]])];const barrels=new Set(barrelFiles);for(const path of result.changed){const why=globalReason(path,{root,barrels});if(why)return{...result,all:why}}const graph=await importGraph(root,config);const changedAbs=new Set(result.changed.map(p=>resolve(root,p)));const hit=file=>{for(const f of graph.closure(file))if(changedAbs.has(f))return relative(root,f);return null};if(config.harness.wrapper){const wrapper=resolve(root,config.harness.wrapper.split("#")[0]);const why=hit(wrapper);if(why)return{...result,all:`${why} (the harness wrapper imports it)`}}const local=Object.values(config.harness.alias??{}).map(p=>resolve(root,p));const{names,stars}=exportMap([...barrelFiles.map(f=>resolve(root,f)),...local].filter(f=>existsSync(f)),graph.resolveImport);const componentHit=new Map;const componentChanged=name=>{if(!componentHit.has(name)){let why=null;if(names.has(name))why=hit(names.get(name));else for(const s of stars)if(why=hit(s))break;componentHit.set(name,why)}return componentHit.get(name)};for(const file of fixtureFiles){const own=hit(file);const name=file.split("/").pop().replace(/\.fixtures\.tsx?$/,"");const beside=EXTENSIONS.map(e=>join(dirname(file),name+e)).find(f=>existsSync(f));const why=own??componentChanged(name)??(beside&&!names.has(name)?hit(beside):null);if(why)result.fixtures.set(file,why===relative(root,file)?`its fixtures changed (${why})`:`${why} changed`)}for(const file of pageFiles){const key=pageKey(file);const own=["page.json","mock.json"].map(f=>relative(root,join(dirname(file),f))).find(p=>result.changed.includes(p));if(own){result.pages.set(key,`${own} changed`);continue}let json;try{json=JSON.parse(readFileSync(file,"utf8"))}catch{result.pages.set(key,"its page.json does not parse");continue}for(const name of componentsOf(json)){const why=componentChanged(name);if(why){result.pages.set(key,`names ${name} (${why} changed)`);break}}}return result}export{changedFiles,selectChanged};