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