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