@peripheries/engine 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/bin/engine.mjs +5 -0
- package/package.json +45 -4
- package/src/commands/bundle-size.mjs +4 -0
- package/src/commands/check-bundle-leak.mjs +2 -0
- package/src/commands/cutover.mjs +22 -0
- package/src/commands/deploy.mjs +5 -0
- package/src/commands/emit-contracts.mjs +12 -0
- package/src/commands/emit-knowledge.mjs +2 -0
- package/src/commands/externals.d.mts +2 -0
- package/src/commands/externals.mjs +1 -0
- package/src/commands/register-components.mjs +5 -0
- package/src/commands/upload-bundle.mjs +2 -0
- package/src/commands/validate-externals.mjs +4 -0
- package/src/index.d.mts +33 -0
- package/src/index.mjs +1 -0
- package/src/lib/config.d.mts +1 -0
- package/src/lib/config.mjs +1 -0
- package/src/lib/contract-shape.mjs +1 -0
- package/src/lib/env.mjs +1 -0
- package/src/lib/jsdoc-parser.d.mts +8 -0
- package/src/lib/jsdoc-parser.mjs +1 -0
- package/src/lib/knowledge.d.mts +15 -0
- package/src/lib/knowledge.mjs +4 -0
- package/src/lib/repo-identity.mjs +4 -0
- package/src/lib/root.d.mts +1 -0
- package/src/lib/root.mjs +1 -0
- package/src/lib/run.mjs +1 -0
package/README.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @peripheries/engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build, check, catalog knowledge and deploy for peripheries component repos:
|
|
4
|
+
one `engine` CLI, driven by `engine.config.ts` in the repo.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { defineConfig } from '@peripheries/engine'
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
org: '<org-slug>',
|
|
11
|
+
lineages: ['main', { name: '<form-lineage>', kind: 'form' }, '<page-lineage>'],
|
|
12
|
+
})
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
engine deploy [--develop|--prod] [--lineage <name>] [--skip-build]
|
|
17
|
+
engine upload | register | cutover | knowledge | contracts
|
|
18
|
+
engine bundle-size | check-bundle-leak | validate-externals
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Credentials are read from the environment of the repo that runs it; the
|
|
22
|
+
package holds none.
|
package/bin/engine.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{COMMANDS,runCommand}from"../src/lib/run.mjs";const[command,...args]=process.argv.slice(2);if(!command||command==="--help"||command==="-h"||!(command in COMMANDS)){const known=Object.keys(COMMANDS).join(", ");if(command&&!(command in COMMANDS)&&command!=="--help"&&command!=="-h")console.error(`[engine] unknown command "${command}"`);console.log(`usage: engine <command> [args]
|
|
3
|
+
commands: ${known}
|
|
4
|
+
|
|
5
|
+
engine deploy [--develop|--prod] [--lineage <name>] [--skip-build]`);process.exit(command&&!(command in COMMANDS)&&command!=="--help"&&command!=="-h"?2:0)}process.exit(runCommand(command,args));
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peripheries/engine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Build, check, knowledge and deploy for peripheries component repos",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/peripheries-components/component-engine.git"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"engine": "./bin/engine.mjs"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./src/index.d.mts",
|
|
20
|
+
"default": "./src/index.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./knowledge": {
|
|
23
|
+
"types": "./src/lib/knowledge.d.mts",
|
|
24
|
+
"default": "./src/lib/knowledge.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./jsdoc": {
|
|
27
|
+
"types": "./src/lib/jsdoc-parser.d.mts",
|
|
28
|
+
"default": "./src/lib/jsdoc-parser.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./externals": {
|
|
31
|
+
"types": "./src/commands/externals.d.mts",
|
|
32
|
+
"default": "./src/commands/externals.mjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@supabase/supabase-js": "2.112.0",
|
|
37
|
+
"semver": "7.8.5",
|
|
38
|
+
"tsx": "4.23.5"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@peripheries/renderer": ">=0.6.2 <1",
|
|
42
|
+
"@peripheries/runtime": ">=0.6.2 <1"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{existsSync,readFileSync,readdirSync,statSync,writeFileSync}from"node:fs";import{gzipSync}from"node:zlib";import{dirname,relative,resolve}from"node:path";import{fileURLToPath}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const REPO_ROOT=REPO_ROOT_;const BASELINE=resolve(REPO_ROOT,"bundle-size.json");const argv=process.argv.slice(2);const UPDATE=argv.includes("--update");const CI=argv.includes("--ci");const WARN_GZ=10*1024;const DISCUSS_GZ=25*1024;function lineages(){const found=[];const push=(name,file)=>{if(existsSync(file))found.push({name,file:relative(REPO_ROOT,file)})};push("main",resolve(REPO_ROOT,"dist/index.js"));push("public",resolve(REPO_ROOT,"dist-public/index.js"));const bundles=resolve(REPO_ROOT,"dist-bundles");if(existsSync(bundles)){for(const entry of readdirSync(bundles)){push(entry,resolve(bundles,entry,"index.js"))}}return found}function measure({name,file}){const abs=resolve(REPO_ROOT,file);const bytes=statSync(abs).size;const gzip=gzipSync(readFileSync(abs),{level:9}).length;return{name,file,bytes,gzip}}const kb=n=>`${(n/1024).toFixed(1)}KB`;const delta=n=>n===0?"\xB10":n>0?`+${kb(n)}`:`\u2212${kb(-n)}`;const measured=lineages().map(measure);if(measured.length===0){console.log("[size] no built bundle found \u2014 run `npm run build` first. Not a failure.");process.exit(0)}const baseline=existsSync(BASELINE)?JSON.parse(readFileSync(BASELINE,"utf8")):null;const prior=new Map((baseline?.lineages??[]).map(l=>[l.name,l]));if(UPDATE){writeFileSync(BASELINE,JSON.stringify({_comment:"Bundle-size baseline. Re-recorded deliberately with `npm run size -- --update`, ideally in the same commit as the growth it accounts for. Reported by CI, never gated.",recorded:new Date().toISOString().slice(0,10),lineages:measured.map(({name,file,bytes,gzip})=>({name,file,bytes,gzip}))},null,2)+"\n","utf8");console.log(`[size] baseline re-recorded for ${measured.length} lineage(s)`)}const lines=[];let worstGz=0;for(const m of measured){const was=prior.get(m.name);if(!was){lines.push(` ${m.name.padEnd(14)} ${kb(m.gzip).padStart(8)} gz ${kb(m.bytes).padStart(9)} raw (new lineage \u2014 no baseline)`);continue}const dGz=m.gzip-was.gzip;const dRaw=m.bytes-was.bytes;worstGz=Math.max(worstGz,dGz);lines.push(` ${m.name.padEnd(14)} ${kb(m.gzip).padStart(8)} gz ${kb(m.bytes).padStart(9)} raw ${delta(dGz).padStart(9)} gz ${delta(dRaw).padStart(9)} raw`)}const heading=UPDATE?baseline?` \u2014 deltas vs the previous baseline (${baseline.recorded}), now re-recorded`:" \u2014 first baseline recorded, nothing to compare against yet":baseline?` vs baseline ${baseline.recorded}`:" \u2014 no baseline recorded yet";console.log(`[size] ${measured.length} lineage(s)${heading}`);for(const line of lines)console.log(line);if(!UPDATE&&baseline){if(worstGz>=DISCUSS_GZ){const msg=`bundle grew ${delta(worstGz)} gzipped since ${baseline.recorded} \u2014 worth a strategy-table row explaining it (\xA74.3)`;console.log(CI?`::warning::${msg}`:`
|
|
3
|
+
[size] DISCUSS \u2014 ${msg}`)}else if(worstGz>=WARN_GZ){const msg=`bundle grew ${delta(worstGz)} gzipped since ${baseline.recorded}`;console.log(CI?`::warning::${msg}`:`
|
|
4
|
+
[size] ${msg}`)}}if(CI&&process.env.GITHUB_STEP_SUMMARY){const{appendFileSync}=await import("node:fs");const rows=measured.map(m=>{const was=prior.get(m.name);return`| ${m.name} | ${kb(m.gzip)} | ${kb(m.bytes)} | ${was?delta(m.gzip-was.gzip):"\u2014"} |`});appendFileSync(process.env.GITHUB_STEP_SUMMARY,[`### Bundle size${baseline?` (baseline ${baseline.recorded})`:""}`,"","| lineage | gzip | raw | \u0394 gzip |","|---|---|---|---|",...rows,""].join("\n"))}process.exit(0);
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{existsSync,readFileSync,readdirSync}from"node:fs";import{dirname,join,relative,resolve}from"node:path";import{fileURLToPath}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;const REQUIRE_BUILD=process.argv.includes("--require-build");const SENTINELS=[{needle:"VENDORED FROM THE HOST",what:"the host code kept in src/harness/host-compat"},{needle:"data-missing-component",what:"@peripheries/renderer (the page renderer)"},{needle:"[harness]",what:"harness render code"},{needle:"onUnresolvedReference",what:"the harness page renderer"}];function bundles(){const out=[];const main=resolve(repoRoot,"dist/index.js");if(existsSync(main))out.push(main);const bundleRoot=resolve(repoRoot,"dist-bundles");if(existsSync(bundleRoot)){for(const slug of readdirSync(bundleRoot)){const file=join(bundleRoot,slug,"index.js");if(existsSync(file))out.push(file)}}return out}const files=bundles();if(files.length===0){const msg="[check-bundle-leak] no built bundle found (dist/index.js, dist-bundles/*/index.js).";if(REQUIRE_BUILD){console.error(`${msg} Run \`npm run build\` first.`);process.exit(1)}console.warn(`${msg} Skipping (pass --require-build to make this fatal).`);process.exit(0)}let failed=false;for(const file of files){const source=readFileSync(file,"utf8");const hits=SENTINELS.filter(s=>source.includes(s.needle));const rel=relative(repoRoot,file);if(hits.length===0){console.log(`[check-bundle-leak] OK \u2014 ${rel} (${(source.length/1024).toFixed(0)} KB)`);continue}failed=true;console.error(`[check-bundle-leak] LEAK in ${rel}:`);for(const hit of hits){console.error(` \xB7 found "${hit.needle}" \u2014 ${hit.what} is in the shipped bundle`)}}if(failed){console.error('\n Something under src/components/ or a src/index*.ts barrel reached into\n src/harness/ or test/. Find it with:\n npx eslint "src/**/*.{ts,tsx}" (the no-restricted-imports guard)\n and move whatever is shared into src/components/_shared/ instead.');process.exit(1)}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const VERSION=parseFlagValue(argv,"version")??process.env.VERSION??null;if(!VERSION){console.error("[cutover] --version <v> is required.");process.exit(2)}const DRY_RUN=argv.includes("--dry-run");const explicitBundle=parseFlagValue(argv,"bundle");const wantsPublic=argv.includes("--public");if(wantsPublic&&explicitBundle&&explicitBundle!=="public"){console.error(`[cutover] --public conflicts with --bundle ${explicitBundle}. Pass one.`);process.exit(2)}const MAIN_LINEAGE="main";const PUBLIC_LINEAGE="public";const BUNDLE_NAME=explicitBundle??(wantsPublic?PUBLIC_LINEAGE:MAIN_LINEAGE);console.log(`
|
|
3
|
+
\u{1F3AF} cutover \u2014 env=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION}
|
|
4
|
+
`);const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[cutover] Could not resolve org "${ORG_SLUG}":`,orgError?.message??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;console.log(` org_id=${ORG_ID}`);const{data:bundleRow,error:bundleError}=await supabase.from("component_bundle").select("id, status, exports").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(bundleError){console.error("[cutover] Failed to fetch component_bundle:",bundleError.message);process.exit(1)}if(!bundleRow){console.error(`[cutover] No component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}). Has upload-bundle.mjs + register-components.mjs been run for this version?`);process.exit(1)}const BUNDLE_ID=bundleRow.id;console.log(` bundle_id=${BUNDLE_ID} status='${bundleRow.status}' exports=${bundleRow.exports?.length??0}`);if(bundleRow.status!=="registered"){console.error(`[cutover] Bundle is in status='${bundleRow.status}'. cutover requires 'registered'.
|
|
5
|
+
`+(bundleRow.status==="uploaded"?" \u2192 Run register-components.mjs for this version first.\n":bundleRow.status==="cutover"?" \u2192 This bundle is already active. Target a different version to roll forward/back.\n":" \u2192 Investigate the bundle row directly.\n"));process.exit(1)}await warnLiveDrift();if(BUNDLE_NAME===PUBLIC_LINEAGE){await cutoverPublic()}else if(BUNDLE_NAME!==MAIN_LINEAGE){await cutoverProject()}else{await cutoverMain()}async function cutoverMain(){const{data:templateRow,error:templateError}=await supabase.from("org_template").select("id, remote_version, remote_url, previous_version").eq("org_id",ORG_ID).eq("is_archived",false).single();if(templateError||!templateRow?.id){console.error(`[cutover] No active org_template for org "${ORG_SLUG}":`,templateError?.message??"not found");process.exit(1)}let previousBundleId=null;if(templateRow.remote_version){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",MAIN_LINEAGE).eq("version",templateRow.remote_version).maybeSingle();previousBundleId=prev?.id??null}const newRemoteUrl=`/api/remotes/${ORG_SLUG}/${VERSION}/index.js`;console.log(`
|
|
6
|
+
org_template:`);console.log(` remote_version: ${templateRow.remote_version??"(none)"} \u2192 ${VERSION}`);console.log(` remote_url: ${templateRow.remote_url??"(none)"} \u2192 ${newRemoteUrl}`);console.log(` previous_version: ${templateRow.previous_version??"(none)"} \u2192 ${templateRow.remote_version??"(none)"}`);console.log(` remote_storage_bucket: \u2192 ${BUNDLE_BUCKET}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` component_bundle_lineage (main):`);console.log(` active_version: ${templateRow.remote_version??"(none)"} \u2192 ${VERSION}`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:templateUpdateError}=await supabase.from("org_template").update({remote_version:VERSION,previous_version:templateRow.remote_version??null,remote_url:newRemoteUrl,remote_storage_bucket:BUNDLE_BUCKET,updated_at:nowIso}).eq("id",templateRow.id);if(templateUpdateError){console.error("[cutover] Failed to update org_template:",templateUpdateError.message);process.exit(1)}const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] org_template was updated but bundle status update failed:",bundleUpdateError.message);console.error(` Manually run: update component_bundle set status='cutover', cutover_at='${nowIso}' where id='${BUNDLE_ID}';`);process.exit(1)}await setLiveBundle(MAIN_LINEAGE);await writeLineagePointer({bundleName:MAIN_LINEAGE,previousVersion:templateRow.remote_version??null,storageBucket:BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
|
|
7
|
+
\u2705 Promoted bundle ${BUNDLE_ID} to active for org "${ORG_SLUG}" on ${target}.
|
|
8
|
+
`)}async function cutoverPublic(){const{data:templateRow,error:templateError}=await supabase.from("org_template").select("id, public_remote_version").eq("org_id",ORG_ID).eq("is_archived",false).single();if(templateError||!templateRow?.id){console.error(`[cutover] No active org_template for org "${ORG_SLUG}":`,templateError?.message??"not found");process.exit(1)}let previousBundleId=null;if(templateRow.public_remote_version){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",PUBLIC_LINEAGE).eq("version",templateRow.public_remote_version).maybeSingle();previousBundleId=prev?.id??null}console.log(`
|
|
9
|
+
org_template (public lineage):`);console.log(` public_remote_version: ${templateRow.public_remote_version??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_public: false \u2192 true`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` component_bundle_lineage (public):`);console.log(` active_version: ${templateRow.public_remote_version??"(none)"} \u2192 ${VERSION}`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,is_public:true,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}const{error:templateUpdateError}=await supabase.from("org_template").update({public_remote_version:VERSION,updated_at:nowIso}).eq("id",templateRow.id);if(templateUpdateError){console.error("[cutover] Failed to update org_template.public_remote_version:",templateUpdateError.message);process.exit(1)}await setLiveBundle(PUBLIC_LINEAGE);await writeLineagePointer({bundleName:PUBLIC_LINEAGE,previousVersion:templateRow.public_remote_version??null,storageBucket:PUBLIC_BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
|
|
10
|
+
\u2705 Promoted public bundle ${BUNDLE_ID} for org "${ORG_SLUG}".
|
|
11
|
+
`)}async function cutoverProject(){const{data:lineage}=await supabase.from("component_bundle_lineage").select("active_version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).maybeSingle();const previousVersion=lineage?.active_version??null;let previousBundleId=null;if(previousVersion){const{data:prev}=await supabase.from("component_bundle").select("id").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",previousVersion).maybeSingle();previousBundleId=prev?.id??null}console.log(`
|
|
12
|
+
component_bundle_lineage (${BUNDLE_NAME}):`);console.log(` active_version: ${previousVersion??"(none)"} \u2192 ${VERSION}`);console.log(` component_bundle:`);console.log(` status: 'registered' \u2192 'cutover'`);console.log(` is_live: \u2192 true (previous cleared)`);console.log(` previous_bundle_id: \u2192 ${previousBundleId??"(none)"}`);console.log(` org_template: (untouched \u2014 project lineages have no org-wide pointer)`);if(DRY_RUN){console.log("\n\u{1F7E1} --dry-run set. No changes written.\n");return}const nowIso=new Date().toISOString();const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"cutover",cutover_at:nowIso,previous_bundle_id:previousBundleId}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error("[cutover] Failed to update component_bundle:",bundleUpdateError.message);process.exit(1)}await setLiveBundle(BUNDLE_NAME);await writeLineagePointer({bundleName:BUNDLE_NAME,previousVersion,storageBucket:BUNDLE_BUCKET});await retirePreviousBundle(previousBundleId,nowIso);console.log(`
|
|
13
|
+
\u2705 Promoted "${BUNDLE_NAME}" bundle ${BUNDLE_ID} for org "${ORG_SLUG}" to version ${VERSION}.
|
|
14
|
+
`);console.log(` Point a template at it with project_template.bundle_name = '${BUNDLE_NAME}'
|
|
15
|
+
(upload-project-template.mjs writes this from template.json's "bundle_name" key).
|
|
16
|
+
`)}async function setLiveBundle(bundleName){const{error:clearError}=await supabase.from("component_bundle").update({is_live:false}).eq("org_id",ORG_ID).eq("bundle_name",bundleName).eq("is_live",true);if(clearError){console.error(`[cutover] Failed to clear is_live for lineage "${bundleName}":`,clearError.message);process.exit(1)}const{error:setError}=await supabase.from("component_bundle").update({is_live:true}).eq("id",BUNDLE_ID);if(setError){console.error(`[cutover] Cleared the previous live bundle but failed to set is_live on ${BUNDLE_ID}:`,setError.message);console.error(` Lineage "${bundleName}" now has NO live bundle. Fix with:`);console.error(` update component_bundle set is_live = true where id = '${BUNDLE_ID}';`);process.exit(1)}}async function writeLineagePointer({bundleName,previousVersion,storageBucket}){const{error}=await supabase.from("component_bundle_lineage").upsert({org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:bundleName,active_version:VERSION,previous_version:previousVersion,storage_bucket:storageBucket,updated_at:new Date().toISOString()},{onConflict:"org_id,bundle_name"});if(error){console.error("[cutover] Failed to write component_bundle_lineage:",error.message);console.error(` The bundle status was updated but the lineage pointer was not \u2014 a project_template
|
|
17
|
+
referencing "${bundleName}" will fail its FK until this row exists.`);process.exit(1)}}async function retirePreviousBundle(previousBundleId,nowIso){if(!previousBundleId||previousBundleId===BUNDLE_ID)return;const{error}=await supabase.from("component_bundle").update({status:"rolled_back",rolled_back_at:nowIso}).eq("id",previousBundleId).eq("status","cutover");if(error){console.warn(`[cutover] Promoted, but failed to retire previous bundle ${previousBundleId}:`,error.message)}}async function warnLiveDrift(){const{data:drifted,error}=await supabase.from("component_bundle").select("version, cutover_at").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("status","cutover").eq("is_live",false).order("cutover_at",{ascending:false});if(error){console.warn(`[cutover] Could not check for is_live drift: ${error.message}`);return}if(!drifted?.length)return;const sample=drifted.slice(0,5).map(r=>`${r.version} (cutover_at=${r.cutover_at??"null"})`).join(", ");console.warn(`
|
|
18
|
+
\u26A0\uFE0F ${drifted.length} bundle(s) in lineage "${BUNDLE_NAME}" have status='cutover' but is_live=false:
|
|
19
|
+
${sample}${drifted.length>5?", \u2026":""}
|
|
20
|
+
These are leftovers from this script's pre-rewrite version, which never set is_live.
|
|
21
|
+
This run will set is_live correctly regardless \u2014 just flagging the historical drift.
|
|
22
|
+
`)}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{execSync,spawnSync}from"node:child_process";import{loadConfig}from"../lib/config.mjs";import{parseFlagValue,parseTargetEnv}from"../lib/env.mjs";import{REPO_ROOT}from"../lib/root.mjs";import{runCommand}from"../lib/run.mjs";const root=REPO_ROOT;const argv=process.argv.slice(2);const target=parseTargetEnv(argv);const targetFlag=target==="local"?[]:[`--${target}`];const skipBuild=argv.includes("--skip-build");const only=parseFlagValue(argv,"lineage");const{lineages}=await loadConfig();const selected=only?lineages.filter(l=>l.name===only):lineages;if(only&&selected.length===0)throw new Error(`--lineage ${only} is not in the config's lineages (${lineages.map(l=>l.name).join(", ")})`);const git=args=>execSync(`git ${args}`,{cwd:root,encoding:"utf8"}).trim();const version=process.env.VERSION||(git("status --porcelain")===""?`v_${git("rev-parse --short HEAD")}`:`v_${target}_${new Date().toISOString().replace(/[-:T]/g,"").slice(0,14)}`);function check(label,status){if(status!==0){console.error(`[deploy] ${label} failed (exit ${status}). Later lineages were not deployed.`);process.exit(status??1)}}function build(lineage){const{script,env={}}=lineage.build;console.log(`
|
|
3
|
+
[deploy] ${lineage.name} build: npm run ${script}`);const r=spawnSync("npm",["run",script],{cwd:root,stdio:"inherit",env:{...process.env,VERSION:version,...env}});check(`${lineage.name} build`,r.status)}function step(lineage,command,args){console.log(`
|
|
4
|
+
[deploy] ${lineage.name} ${command}: engine ${command} ${args.join(" ")}`);check(`${lineage.name} ${command}`,runCommand(command,args,{VERSION:version}))}console.log(`[deploy] target=${target} version=${version} lineages=${selected.map(l=>l.name).join(", ")}`);for(const lineage of selected){if(!skipBuild)build(lineage);const bundle=lineage.name==="main"?[]:["--bundle",lineage.name];step(lineage,"upload",[...targetFlag,...bundle]);step(lineage,"register",[...targetFlag,...bundle]);step(lineage,"cutover",[...targetFlag,"--version",version,...bundle])}console.log(`
|
|
5
|
+
[deploy] \u2705 ${selected.map(l=>l.name).join(", ")} live at ${version} on ${target}.`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{existsSync,mkdirSync,readFileSync,writeFileSync}from"node:fs";import{dirname,resolve}from"node:path";import{fileURLToPath}from"node:url";import{hasComponentJSDoc,parseJSDocFile,projectContractsManifest,readExportsFromIndex,resolveComponentFilePath}from"../lib/jsdoc-parser.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;function flag(name,fallback){const i=process.argv.indexOf(`--${name}`);const value=i===-1?void 0:process.argv[i+1];if(i!==-1&&(value===void 0||value.startsWith("--"))){console.error(`[emit-contracts] --${name} needs a value`);process.exit(1)}return value??fallback}const entryRelative=flag("entry","src/index.ts");const outRelative=flag("out","dist/contracts.json");if(!existsSync(resolve(repoRoot,entryRelative))){console.error(`[emit-contracts] no entry barrel at ${entryRelative}. For a project lineage, check OA_PROJECT_BUNDLE is set.`);process.exit(1)}const outputPath=resolve(repoRoot,outRelative);const distRoot=dirname(outputPath);const exports=readExportsFromIndex(repoRoot,entryRelative);if(exports.length===0){console.warn(`[emit-contracts] No named exports in ${entryRelative} \u2014 nothing to emit.`)}const specs=exports.map(({name,source})=>({name,source,filePath:resolveComponentFilePath(repoRoot,source)})).filter(({name,filePath})=>hasComponentJSDoc(filePath,name)).map(({name,filePath})=>({component_name:name,...parseJSDocFile(filePath,name)}));const skippedCount=exports.length-specs.length;const manifest=projectContractsManifest(specs);const WRITTEN_TAG=/^\s*\*?\s*@(data|action|printPolicy)\s+\w/gm;let written=0;for(const{filePath}of exports.map(({name,source})=>({name,filePath:resolveComponentFilePath(repoRoot,source)})).filter(({name,filePath:filePath2})=>hasComponentJSDoc(filePath2,name))){if(!filePath)continue;written+=(readFileSync(filePath,"utf8").match(WRITTEN_TAG)??[]).length}const parsed=specs.reduce((n,spec)=>n+Object.keys(spec.props_schema?.data??{}).length+Object.keys(spec.props_schema?.actions??{}).length+(spec.print_policy?1:0),0);if(written>parsed){console.error(`[emit-contracts] ${written} @data/@action/@printPolicy tag(s) written, ${parsed} parsed \u2014 ${written-parsed} dropped SILENTLY.
|
|
3
|
+
|
|
4
|
+
A declaration the parser cannot read is not an error today: it is simply
|
|
5
|
+
absent from dist/contracts.json, which the host reads as "this component
|
|
6
|
+
declares nothing to enforce". The artifact stays internally consistent and
|
|
7
|
+
quietly wrong.
|
|
8
|
+
|
|
9
|
+
The grammar is exact \u2014 check the arrow and the \`- via\`:
|
|
10
|
+
@data <name>[?] {<Type>} - via <registry_key>
|
|
11
|
+
@action <name>[?] {<Input> \u2192 <Output>} - via <registry_key>
|
|
12
|
+
`);process.exit(1)}mkdirSync(distRoot,{recursive:true});writeFileSync(outputPath,JSON.stringify(manifest,null,2)+"\n","utf8");const componentCount=Object.keys(manifest.components).length;console.log(`[emit-contracts] Wrote ${outputPath} (contract_version=${manifest.contract_version}, ${componentCount} component${componentCount===1?"":"s"} with declarations from ${specs.length}/${exports.length} exports; ${skippedCount} non-component export${skippedCount===1?"":"s"} skipped; ${parsed}/${written} @data/@action tag(s) parsed).`);
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import{mkdirSync,writeFileSync}from"node:fs";import{dirname,resolve}from"node:path";import{checkCatalog}from"@peripheries/renderer/catalog";import{lineageConfig,loadConfig}from"../lib/config.mjs";import{buildKnowledge}from"../lib/knowledge.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const repoRoot=REPO_ROOT_;const argv=process.argv.slice(2);const flag=name=>{const i=argv.indexOf(`--${name}`);return i===-1?void 0:argv[i+1]};const bundle=flag("bundle")??"main";const checkOnly=argv.includes("--check");const outPath=resolve(repoRoot,flag("out")??`${(await lineageConfig(bundle)).dist}/knowledge.json`);const org=flag("org")??(await loadConfig()).org;const{lineage,rows,links,registry,scopeLineage,scopeRegistry}=await buildKnowledge(bundle,org);const kits=rows.filter(r=>r.kind==="kit");const patterns=rows.filter(r=>r.kind==="pattern");const diagnostics=checkCatalog({lineage,rows,links,registry,scopeRegistry,inScope:(target,kind,key)=>target===scopeLineage&&kind==="component"?Boolean(scopeRegistry?.[key]):void 0});for(const d of diagnostics)console[d.severity==="error"?"error":"warn"](`[knowledge] ${d.severity} ${d.code} ${d.path??""}: ${d.message}`);const errors=diagnostics.filter(d=>d.severity==="error").length;console.log(`[knowledge] ${lineage}: ${rows.length} rows (${rows.filter(r=>r.kind==="component").length} components, ${kits.length} kits, ${patterns.length} patterns), ${links.length} links; ${errors} error(s)`);if(errors>0&&!argv.includes("--allow-errors"))process.exit(1);if(!checkOnly){mkdirSync(dirname(outPath),{recursive:true});writeFileSync(outPath,JSON.stringify({schema:"peripheries.knowledge/1",lineage,rows,links},null,2)+"\n");console.log(`[knowledge] wrote ${outPath}`)}
|
|
@@ -0,0 +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};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{existsSync,mkdirSync,readFileSync,writeFileSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";import{initFromArgv,loadEnv,parseFlagValue,parseTargetEnv}from"../lib/env.mjs";import{collectReferencedKeys,hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toDisplayName,toRepoRelative}from"../lib/jsdoc-parser.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";import{lineageConfig,loadConfig,MAIN_LINEAGE,PUBLIC_LINEAGE}from"../lib/config.mjs";const repoRoot=REPO_ROOT_;const argv=process.argv.slice(2);function orgSlugFromConfig(){try{const raw=readFileSync(resolve(repoRoot,"template/config/org.json"),"utf8");return JSON.parse(raw).slug??null}catch{return null}}const EMIT_ROWS=parseFlagValue(argv,"emit-rows");const target=parseTargetEnv(argv);let ORG_SLUG;let supabase;if(EMIT_ROWS){loadEnv(target);ORG_SLUG=parseFlagValue(argv,"org")??process.env.ORG_SLUG??process.env.VITE_ORG_SLUG??(await loadConfig()).org??orgSlugFromConfig();if(!ORG_SLUG){console.error("[register-components] --emit-rows still needs an org: pass --org <slug>, set ORG_SLUG / VITE_ORG_SLUG, or set `org` in engine.config.ts.");process.exit(1)}}else{({orgSlug:ORG_SLUG,supabase}=initFromArgv(argv))}let VERSION=parseFlagValue(argv,"version")??process.env.VERSION??null;const BUNDLE_NAME=parseFlagValue(argv,"bundle")??"main";const LINEAGE=await lineageConfig(BUNDLE_NAME);const entryRelative=parseFlagValue(argv,"entry")??LINEAGE.entry;if(!existsSync(resolve(repoRoot,entryRelative))){console.error(`[register-components] no entry barrel at ${entryRelative} for lineage "${BUNDLE_NAME}".`);process.exit(1)}function entryRuleFor(){return{sourceRoot:LINEAGE.sourceRoot}}const ENTRY_RULE=entryRuleFor();const allExports=readExportsFromIndex(repoRoot,entryRelative);const exports=[];const skipped=[];for(const entry of allExports){const filePath=resolveComponentFilePath(repoRoot,entry.source);if(filePath&&hasComponentJSDoc(filePath,entry.name)){exports.push({...entry,filePath})}else{skipped.push(entry.name)}}if(exports.length===0){if(BUNDLE_NAME===MAIN_LINEAGE){console.error(`[register-components] No exports with an @component JSDoc block found in ${entryRelative}. The org bundle cannot be empty.`);process.exit(1)}console.warn(`[register-components] ${entryRelative} declares no components yet \u2014 registering the "${BUNDLE_NAME}" lineage with an empty component set so it can be cut over.`)}if(skipped.length>0){console.log(`[register-components] skipping ${skipped.length} export(s) with no matching @component block: ${skipped.join(", ")}`)}function buildSpecs(){return exports.map(({name,filePath})=>{const repoRelativePath=toRepoRelative(repoRoot,filePath);if(ENTRY_RULE.sourceRoot&&!repoRelativePath.startsWith(ENTRY_RULE.sourceRoot)){console.error(`[register-components] "${name}" is exported from ${entryRelative} but its source (${repoRelativePath}) is not under ${ENTRY_RULE.sourceRoot}. Refusing to register \u2014 the "${BUNDLE_NAME}" bundle may only publish components from that root.`);if(BUNDLE_NAME!==PUBLIC_LINEAGE){console.error(` If this is a shared component, do not re-export it here: it stays in the org's
|
|
3
|
+
main bundle and resolves through the host's cascade one layer down.`)}process.exit(1)}const jsdoc=parseJSDocFile(filePath,name);return{component_name:name,exposed_export:name,description:jsdoc.description??`${toDisplayName(name)} federated component`,props_schema:jsdoc.props_schema??null,file_path:filePath?`src/${toRepoRelative(repoRoot,filePath)}`:null,metadata:{source:"federated-remote",remote_org_slug:ORG_SLUG,...jsdoc.use_cases?{use_cases:jsdoc.use_cases}:{}}}})}if(EMIT_ROWS){const rows=buildSpecs();const outputPath=resolve(repoRoot,EMIT_ROWS);mkdirSync(dirname(outputPath),{recursive:true});writeFileSync(outputPath,JSON.stringify({_comment:"Generated by `register-components.mjs --emit-rows`. A dry run: these are the component_registry rows a real registration would write. Consumed by the L3 bundle harness to check the built bundle exports what the host will look for. Not checked in \u2014 regenerate rather than edit.",org_slug:ORG_SLUG,bundle_name:BUNDLE_NAME,entry:entryRelative,generated_at:new Date().toISOString(),rows},null,2)+"\n","utf8");console.log(`[register-components] --emit-rows \u2192 ${EMIT_ROWS} (${rows.length} row(s) for lineage "${BUNDLE_NAME}" from ${entryRelative}; no database touched).`);process.exit(0)}const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[register-components] Could not resolve org_id for org_slug="${ORG_SLUG}":`,orgError??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;let bundleRow;if(VERSION){const{data,error}=await supabase.from("component_bundle").select("id, status, version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(error||!data?.id){console.error(`[register-components] No component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}). Run upload-bundle.mjs for this version first.`);process.exit(1)}bundleRow=data}else{const{data,error}=await supabase.from("component_bundle").select("id, status, version").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("status","uploaded").order("uploaded_at",{ascending:false}).limit(1).maybeSingle();if(error||!data?.id){console.error(`[register-components] No 'uploaded' bundle found for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}). Either run upload-bundle.mjs first or pass --version <v>.`);process.exit(1)}bundleRow=data;VERSION=data.version;console.log(`[register-components] auto-discovered most recent 'uploaded' bundle: version=${VERSION}`)}const BUNDLE_ID=bundleRow.id;console.log(`[register-components] target=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION} bundle_id=${BUNDLE_ID} status='${bundleRow.status}' \u2014 ${exports.length} components: ${exports.map(e=>e.name).join(", ")}`);const specs=buildSpecs();const referencedKeys=collectReferencedKeys(specs);if(referencedKeys.queries.size>0||referencedKeys.mutations.size>0){const allKeys=[...referencedKeys.queries,...referencedKeys.mutations];const{data:contractRows,error:contractError}=await supabase.from("component_contract").select("kind, registry_key, is_active, contract_version_removed").in("registry_key",allKeys);if(contractError){console.error("[register-components] Failed to read component_contract:",contractError);process.exit(2)}const indexByKey=new Map;for(const row of contractRows??[]){indexByKey.set(`${row.kind}:${row.registry_key}`,row)}const waived=new Set((parseFlagValue(argv,"allow-missing-contract")??"").split(",").map(s=>s.trim()).filter(Boolean));const waiversUsed=new Set;const problems=[];const warnings=[];for(const spec of specs){const data=spec.props_schema?.data??{};const actions2=spec.props_schema?.actions??{};for(const[propName,decl]of Object.entries(data)){const row=indexByKey.get(`query:${decl.registry}`);if(!row&&waived.has(decl.registry)&&decl.required===false){waiversUsed.add(decl.registry);warnings.push(`${spec.component_name}.${propName} \u2192 query "${decl.registry}" absent here, WAIVED (optional; the component must render without it)`)}else if(!row){problems.push(`${spec.component_name}.${propName} \u2192 unknown query "${decl.registry}"`+(waived.has(decl.registry)?" \u2014 waiver REFUSED: the declaration is required":""))}else if(!row.is_active){problems.push(`${spec.component_name}.${propName} \u2192 deprecated query "${decl.registry}" (removed at v${row.contract_version_removed??"?"})`)}}for(const[propName,decl]of Object.entries(actions2)){const row=indexByKey.get(`mutation:${decl.registry}`);if(!row&&waived.has(decl.registry)&&decl.required===false){waiversUsed.add(decl.registry);warnings.push(`${spec.component_name}.${propName} \u2192 mutation "${decl.registry}" absent here, WAIVED (optional; the component must degrade without it)`)}else if(!row){problems.push(`${spec.component_name}.${propName} \u2192 unknown mutation "${decl.registry}"`+(waived.has(decl.registry)?" \u2014 waiver REFUSED: the declaration is required":""))}else if(!row.is_active){problems.push(`${spec.component_name}.${propName} \u2192 deprecated mutation "${decl.registry}" (removed at v${row.contract_version_removed??"?"})`)}}}if(problems.length>0){console.error("[register-components] Contract validation FAILED:");for(const p of problems)console.error(` \xD7 ${p}`);console.error("");console.error(" Either add the missing keys to lib/portal/contract-manifest/ on");console.error(" the host (then run `pnpm sync-contract` there), or update the");console.error(" component JSDoc to reference an existing registry_key.");console.error("");console.error(" If a key is absent from THIS environment on purpose and the");console.error(" declaration is optional, name it explicitly:");console.error(" --allow-missing-contract <key>[,<key>...]");process.exit(1)}if(warnings.length>0){console.warn("[register-components] Contract validation passed WITH WAIVERS:");for(const w of warnings)console.warn(` \u26A0 ${w}`);console.warn(` These props will be undefined at runtime in this environment. Drop the flag once the host ships them.`)}for(const key of waived){if(!waiversUsed.has(key)){console.warn(`[register-components] \u26A0 waiver "${key}" was not needed \u2014 the key resolves here, or nothing declares it. Remove it from --allow-missing-contract.`)}}}const{data:existing,error:fetchError}=await supabase.from("component_registry").select("id, component_name, delivery_mode, exposed_export, is_active, org_id, bundle_id, description, props_schema, file_path, metadata").eq("org_slug",ORG_SLUG).eq("bundle_id",BUNDLE_ID);if(fetchError){console.error("[register-components] Failed to fetch existing rows:",fetchError);process.exit(2)}const existingByName=new Map(existing?.map(row=>[row.component_name,row])??[]);const actions=[];for(const spec of specs){const row=existingByName.get(spec.component_name);const payload={...spec,org_id:ORG_ID,org_slug:ORG_SLUG,bundle_id:BUNDLE_ID,delivery_mode:"federated",is_active:true};if(row){const changed=row.is_active!==true||row.delivery_mode!=="federated"||row.exposed_export!==payload.exposed_export||row.org_id!==ORG_ID||row.bundle_id!==BUNDLE_ID||row.description!==payload.description||row.file_path!==payload.file_path||canonical(row.props_schema)!==canonical(payload.props_schema)||canonical(row.metadata)!==canonical(payload.metadata);if(changed){actions.push({kind:"update",id:row.id,patch:payload})}}else{actions.push({kind:"insert",payload})}}if(actions.length===0){console.log("[register-components] No changes \u2014 bundle registry already in sync.")}else{for(const action of actions){if(action.kind==="insert"){const{error}=await supabase.from("component_registry").insert(action.payload);if(error){console.error(`[register-components] INSERT failed for "${action.payload.component_name}":`,error);process.exit(1)}console.log(`[register-components] + inserted "${action.payload.component_name}"`)}else{const{error}=await supabase.from("component_registry").update(action.patch).eq("id",action.id);if(error){console.error(`[register-components] UPDATE failed for id=${action.id}:`,error);process.exit(1)}console.log(`[register-components] ~ updated id=${action.id}`)}}console.log(`[register-components] ${actions.length} registry change(s) applied.`)}const KNOWLEDGE_PATH=resolve(repoRoot,`${LINEAGE.dist}/knowledge.json`);if(!existsSync(KNOWLEDGE_PATH)){console.error(`[register-components] no ${KNOWLEDGE_PATH.replace(repoRoot+"/","")}: build the lineage first (the build runs scripts/emit-knowledge.mts). Agents read the catalog from it.`);process.exit(1)}const knowledge=JSON.parse(readFileSync(KNOWLEDGE_PATH,"utf8"));if(knowledge.lineage!==`${ORG_SLUG}/${BUNDLE_NAME}`){console.error(`[register-components] ${KNOWLEDGE_PATH} is for ${knowledge.lineage}, not ${ORG_SLUG}/${BUNDLE_NAME}. Rebuild.`);process.exit(1)}{const{error:clearError}=await supabase.from("component_knowledge").delete().eq("bundle_id",BUNDLE_ID);if(clearError){console.error("[register-components] Failed to clear this bundle's knowledge:",clearError);process.exit(1)}const{data:inserted,error:insertError}=await supabase.from("component_knowledge").insert(knowledge.rows.map(r=>({bundle_id:BUNDLE_ID,kind:r.kind,key:r.key,title:r.title,summary:r.summary,body:r.body??null,tags:r.tags??[],snippets:r.snippets??[],attributes:r.attributes??{},page_cell:r.page_cell??true,sort:r.sort??0}))).select("id, kind, key");if(insertError){console.error("[register-components] Failed to write knowledge rows:",insertError);process.exit(1)}const idOf=new Map(inserted.map(r=>[`${r.kind}:${r.key}`,r.id]));const linkRows=knowledge.links.map(l=>({from_id:idOf.get(`${l.from.kind}:${l.from.key}`),relation:l.relation,to_lineage:l.to.lineage??null,to_kind:l.to.kind,to_key:l.to.key,note:l.note??null}));if(linkRows.some(l=>!l.from_id)){console.error("[register-components] a knowledge link starts at a row that was not written. Rebuild.");process.exit(1)}if(linkRows.length>0){const{error:linkError}=await supabase.from("component_knowledge_link").insert(linkRows);if(linkError){console.error("[register-components] Failed to write knowledge links:",linkError);process.exit(1)}}console.log(`[register-components] knowledge: ${inserted.length} rows, ${linkRows.length} links for ${knowledge.lineage}`)}const{error:bundleUpdateError}=await supabase.from("component_bundle").update({status:"registered",registered_at:new Date().toISOString(),exports:specs.map(s=>s.component_name),manifest:{exports:specs.map(s=>({component_name:s.component_name,exposed_export:s.exposed_export,file_path:s.file_path}))}}).eq("id",BUNDLE_ID);if(bundleUpdateError){console.error(`[register-components] Failed to mark bundle 'registered':`,bundleUpdateError);process.exit(1)}console.log(`[register-components] Done. bundle ${BUNDLE_ID} is now status='registered'.
|
|
4
|
+
Promote it with the engine (the host has no cutover of its own):
|
|
5
|
+
engine cutover --version ${VERSION}${BUNDLE_NAME===MAIN_LINEAGE?"":` --bundle ${BUNDLE_NAME}`}${target==="local"?"":` --${target}`}`);function canonical(value){return JSON.stringify(sortKeys(value))}function sortKeys(value){if(Array.isArray(value))return value.map(sortKeys);if(value&&typeof value==="object"){const sorted={};for(const key of Object.keys(value).sort())sorted[key]=sortKeys(value[key]);return sorted}return value}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{readFileSync,existsSync,statSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";import{BUNDLE_BUCKET,PUBLIC_BUNDLE_BUCKET,initFromArgv,parseFlagValue}from"../lib/env.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";import{lineageConfig}from"../lib/config.mjs";const argv=process.argv.slice(2);const{target,orgSlug:ORG_SLUG,supabase}=initFromArgv(argv);const BUNDLE_NAME=parseFlagValue(argv,"bundle")??"main";const dryRun=argv.includes("--dry-run");const IS_MAIN=BUNDLE_NAME==="main";const IS_PUBLIC=BUNDLE_NAME==="public";const IS_ORG_WIDE=IS_MAIN||IS_PUBLIC;const DEFAULT_BUCKET=IS_PUBLIC?PUBLIC_BUNDLE_BUCKET:BUNDLE_BUCKET;const STORAGE_BUCKET=process.env.STORAGE_BUCKET||DEFAULT_BUCKET;if(STORAGE_BUCKET!==DEFAULT_BUCKET){console.error(`[upload-bundle] STORAGE_BUCKET must be "${DEFAULT_BUCKET}" for lineage "${BUNDLE_NAME}", got "${STORAGE_BUCKET}".`);process.exit(2)}const VERSION=process.env.VERSION||`v_${target}_${new Date().toISOString().replace(/[-:T]/g,"").replace(/\..+$/,"").slice(0,14)}`;if(!process.env.VERSION){console.log(`[upload-bundle] VERSION auto-generated: ${VERSION}`);process.env.VERSION=VERSION}const distDir=resolve(REPO_ROOT_,(await lineageConfig(BUNDLE_NAME)).dist);const bundlePath=resolve(distDir,"index.js");if(!existsSync(bundlePath)){console.error(`[upload-bundle] ${bundlePath} does not exist. `+(IS_MAIN?"Did `npm run build` run?":`Did \`OA_PROJECT_BUNDLE=${BUNDLE_NAME} npm run build:bundle\` run?`));process.exit(1)}console.log(`[upload-bundle] lineage=${BUNDLE_NAME} dist=${distDir.split("/").slice(-2).join("/")}`);const{data:orgRow,error:orgError}=await supabase.from("organization").select("id").eq("metadata->>org_slug",ORG_SLUG).maybeSingle();if(orgError||!orgRow?.id){console.error(`[upload-bundle] Could not resolve org_id for org_slug="${ORG_SLUG}":`,orgError??"no matching organization row");process.exit(1)}const ORG_ID=orgRow.id;async function uploadOne(localPath,remotePath,contentType){const body=readFileSync(localPath);if(dryRun){console.log(`[upload-bundle] would upload ${STORAGE_BUCKET}/${remotePath} (${body.byteLength} bytes)`);return}const{error}=await supabase.storage.from(STORAGE_BUCKET).upload(remotePath,body,{contentType,upsert:false});if(error){console.error(`[upload-bundle] Failed to upload ${remotePath}:`,error);process.exit(1)}console.log(`[upload-bundle] \u2191 ${remotePath} (${body.byteLength} bytes)`)}const remoteBase=IS_ORG_WIDE?`${ORG_SLUG}/${VERSION}`:`${ORG_SLUG}/${BUNDLE_NAME}/${VERSION}`;await uploadOne(bundlePath,`${remoteBase}/index.js`,"application/javascript");const mapPath=resolve(distDir,"index.js.map");if(existsSync(mapPath)){await uploadOne(mapPath,`${remoteBase}/index.js.map`,"application/json")}const contractsPath=resolve(distDir,"contracts.json");if(existsSync(contractsPath)){await uploadOne(contractsPath,`${remoteBase}/contracts.json`,"application/json")}const{data:existingBundle,error:existingBundleError}=await supabase.from("component_bundle").select("id, status").eq("org_id",ORG_ID).eq("bundle_name",BUNDLE_NAME).eq("version",VERSION).maybeSingle();if(existingBundleError){console.error(`[upload-bundle] Failed to look up existing component_bundle for (org=${ORG_SLUG}, bundle=${BUNDLE_NAME}, version=${VERSION}):`,existingBundleError);process.exit(1)}const bundleBytes=statSync(bundlePath).size;const hasSourcemap=existsSync(mapPath);const bundlePayload={org_id:ORG_ID,org_slug:ORG_SLUG,bundle_name:BUNDLE_NAME,version:VERSION,status:"uploaded",storage_bucket:STORAGE_BUCKET,storage_path:`${remoteBase}/index.js`,uploaded_at:new Date().toISOString(),registered_at:null,cutover_at:null,git_sha:process.env.DEPLOY_SHA??process.env.GITHUB_SHA??null,git_ref:process.env.DEPLOY_REF??process.env.GITHUB_REF??null,git_repo:process.env.GITHUB_REPOSITORY??null,process_id:process.env.PROCESS_ID||null,metadata:{bundle_bytes:bundleBytes,has_sourcemap:hasSourcemap,bundle_scope:BUNDLE_NAME,...process.env.GITHUB_RUN_ID?{github_run_id:process.env.GITHUB_RUN_ID}:{},...process.env.GITHUB_RUN_ATTEMPT?{github_run_attempt:process.env.GITHUB_RUN_ATTEMPT}:{}}};if(dryRun){console.log(`[upload-bundle] dry-run \u2014 would ${existingBundle?.id?`UPDATE ${existingBundle.id}`:"INSERT"} component_bundle:`);console.log(JSON.stringify(bundlePayload,null,2));console.log("[upload-bundle] dry-run \u2014 nothing written.")}else if(existingBundle?.id){const{error:updateError}=await supabase.from("component_bundle").update(bundlePayload).eq("id",existingBundle.id);if(updateError){console.error(`[upload-bundle] Failed to update component_bundle row:`,updateError);process.exit(1)}console.log(`[upload-bundle] ~ component_bundle ${existingBundle.id} reset to status='uploaded' (was '${existingBundle.status}').`)}else{const{data:inserted,error:insertError}=await supabase.from("component_bundle").insert(bundlePayload).select("id").single();if(insertError){console.error(`[upload-bundle] Failed to insert component_bundle row:`,insertError);process.exit(1)}console.log(`[upload-bundle] + component_bundle ${inserted.id} created (status='uploaded').`)}console.log(`[upload-bundle] Done \u2014 target=${target} org=${ORG_SLUG} bundle=${BUNDLE_NAME} version=${VERSION}. Run register-components.mjs next.`);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{readFileSync}from"node:fs";import{resolve,dirname}from"node:path";import{fileURLToPath}from"node:url";import semver from"semver";import{EXTERNALS}from"./externals.mjs";import{REPO_ROOT as REPO_ROOT_}from"../lib/root.mjs";const HOST_URL=process.env.HOST_URL;let hostImportMap;if(HOST_URL){const importMapUrl=new URL("/_shared/import-map.json",HOST_URL).toString();const skipLiveComparison=reason=>{console.warn(`[validate-externals] ${reason}
|
|
3
|
+
Skipping the live host-import-map comparison (warning, not a failure).
|
|
4
|
+
The offline half still ran: EXTERNALS vs peerDependencies.`);hostImportMap=void 0};try{const res=await fetch(importMapUrl);if(!res.ok){skipLiveComparison(`${importMapUrl} returned ${res.status} ${res.statusText}`)}else{hostImportMap=await res.json();if(!hostImportMap?.imports||!hostImportMap?.pins){skipLiveComparison(`${importMapUrl} is missing \`imports\` or \`pins\` \u2014 expected { imports: Record<string,string>, pins: Record<string,string> }.`)}}}catch(err){skipLiveComparison(`Could not reach ${importMapUrl}: ${err instanceof Error?err.message:err}`)}}else{console.warn("[validate-externals] HOST_URL not set \u2014 skipping live host-import-map comparison, checking only that EXTERNALS and peerDependencies agree locally.")}const pkgPath=resolve(REPO_ROOT_,"package.json");const pkg=JSON.parse(readFileSync(pkgPath,"utf8"));const peers={...pkg.peerDependencies??{}};const problems=[];for(const specifier of EXTERNALS){const basePackage=specifier.startsWith("@")?specifier.split("/").slice(0,2).join("/"):specifier.split("/")[0];const localRange=peers[basePackage];if(!localRange){problems.push(`"${basePackage}" is required external but missing from package.json peerDependencies.`);continue}if(!hostImportMap)continue;const hostUrl=hostImportMap.imports[specifier];const hostPin=hostImportMap.pins[specifier];if(!hostUrl){problems.push(`"${specifier}" is external in this repo but not in the host import map. Remove it from scripts/externals.mjs or update the host to pin it.`);continue}if(hostPin&&!semver.subset(localRange,hostPin)&&!majorsAgree(localRange,hostPin)){problems.push(`"${basePackage}": host pins ${hostPin} but this repo declares ${localRange}. Majors must agree.`)}}if(hostImportMap){for(const hostSpecifier of Object.keys(hostImportMap.imports)){if(!EXTERNALS.includes(hostSpecifier)){problems.push(`Host import map includes "${hostSpecifier}" which is not in scripts/externals.mjs. Likely fix: add it to the externals list and peerDependencies.`)}}}if(problems.length===0){console.log(hostImportMap?`[validate-externals] OK \u2014 ${EXTERNALS.length} externals match host import map.`:`[validate-externals] OK \u2014 ${EXTERNALS.length} externals have matching peerDependencies (host comparison skipped).`);process.exit(0)}for(const p of problems){console.error(`[validate-externals] ERROR: ${p}`)}process.exit(1);function majorsAgree(a,b){const aMajor=extractMajor(a);const bMajor=extractMajor(b);if(aMajor==null||bMajor==null)return true;return aMajor===bMajor}function extractMajor(range){const match=range.match(/(\d+)/);return match?Number(match[1]):null}
|
package/src/index.d.mts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type LineageKind = 'main' | 'public' | 'page' | 'form';
|
|
2
|
+
|
|
3
|
+
export type LineageSpec =
|
|
4
|
+
| string
|
|
5
|
+
| {
|
|
6
|
+
name: string;
|
|
7
|
+
kind?: LineageKind;
|
|
8
|
+
entry?: string;
|
|
9
|
+
sourceRoot?: string | null;
|
|
10
|
+
dist?: string;
|
|
11
|
+
build?: { script: string; env?: Record<string, string> };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type EngineConfig = {
|
|
15
|
+
org: string;
|
|
16
|
+
lineages?: LineageSpec[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type Lineage = {
|
|
20
|
+
name: string;
|
|
21
|
+
kind: LineageKind;
|
|
22
|
+
entry: string;
|
|
23
|
+
sourceRoot: string | null;
|
|
24
|
+
dist: string;
|
|
25
|
+
build: { script: string; env?: Record<string, string> };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const MAIN_LINEAGE: 'main';
|
|
29
|
+
export const PUBLIC_LINEAGE: 'public';
|
|
30
|
+
export function defineConfig(config: EngineConfig): EngineConfig;
|
|
31
|
+
export function lineageDefaults(name: string, kind?: LineageKind): Lineage;
|
|
32
|
+
export function loadConfig(): Promise<{ org: string; lineages: Lineage[] }>;
|
|
33
|
+
export function lineageConfig(name: string): Promise<Lineage>;
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineConfig,lineageConfig,lineageDefaults,loadConfig,MAIN_LINEAGE,PUBLIC_LINEAGE}from"./lib/config.mjs";export{MAIN_LINEAGE,PUBLIC_LINEAGE,defineConfig,lineageConfig,lineageDefaults,loadConfig};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defineConfig, lineageConfig, lineageDefaults, loadConfig, MAIN_LINEAGE, PUBLIC_LINEAGE } from '../index.mjs';
|
|
@@ -0,0 +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 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",build:{script:"build"}}}if(name===PUBLIC_LINEAGE){return{name,kind:"public",entry:"src/index.public.ts",sourceRoot:"components/public/",dist:"dist-public",build:{script:"build:public"}}}const k=kind??"page";return{name,kind:k,entry:`src/index.${name}.ts`,sourceRoot:k==="form"?`components/form/${name}/`:`components/project_page/${name}/`,dist:`dist-bundles/${name}`,build:{script:"build:bundle",env:{OA_PROJECT_BUNDLE:name}}}}function normalize(raw,origin){if(!raw||typeof raw!=="object")throw new Error(`[engine] ${origin} must export a config object`);if(typeof raw.org!=="string"||!raw.org)throw new Error(`[engine] ${origin}: \`org\` (the org slug) is required`);const list=raw.lineages??[MAIN_LINEAGE];const lineages=list.map(l=>{const spec=typeof l==="string"?{name:l}:l;if(!spec?.name)throw new Error(`[engine] ${origin}: every lineage needs a name`);return{...lineageDefaults(spec.name,spec.kind),...spec}});if(lineages[0]?.name!==MAIN_LINEAGE)throw new Error(`[engine] ${origin}: lineages must start with "main"`);return{org:raw.org,lineages}}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{MAIN_LINEAGE,PUBLIC_LINEAGE,defineConfig,lineageConfig,lineageDefaults,loadConfig};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function stripArrows(text){return text.replace(/=>/g,"@@")}function declBody(text,name){const m=new RegExp(String.raw`(?:export\s+)?(interface|type)\s+${name}\b`).exec(text);if(!m)return null;const after=m.index+m[0].length;if(m[1]==="type"){const end=text.indexOf(";",after);return end===-1?null:text.slice(after,end)}const open=text.indexOf("{",after);if(open===-1)return null;let depth=0;for(let i=open;i<text.length;i++){if(text[i]==="{")depth++;else if(text[i]==="}"&&--depth===0)return text.slice(open+1,i)}return null}const stripComments=s=>s.replace(/\/\/[^\n]*/g,"").replace(/\/\*[\s\S]*?\*\//g,"");function topLevelKeys(body){if(body===null||body===void 0)return null;const clean=stripArrows(stripComments(body));const keys=[];let depth=0;let atFieldStart=true;let buf="";for(const ch of clean){if(ch==="{"||ch==="("||ch==="<"){depth++;continue}if(ch==="}"||ch===")"||ch===">"){depth--;continue}if(depth>0)continue;if(ch===";"||ch===","||ch==="\n"){atFieldStart=true;buf="";continue}if(ch===":"){const key=buf.trim();if(atFieldStart&&key)keys.push(/^\[/.test(key)?"[index]":key.replace(/\s+/g,""));atFieldStart=false;buf="";continue}buf+=ch}return[...new Set(keys)].sort()}function topLevelFields(body){if(body===null||body===void 0)return null;const clean=stripArrows(stripComments(body));const out=new Map;let i=0;while(i<clean.length){let key="";while(i<clean.length&&!":;,\n{}".includes(clean[i]))key+=clean[i++];if(clean[i]!==":"){i++;continue}i++;key=key.trim();if(!key)continue;let depth=0;let val="";for(;i<clean.length;i++){const ch=clean[i];if("{(<".includes(ch))depth++;else if("})>".includes(ch))depth--;else if(depth===0&&";,\n".includes(ch))break;val+=ch}out.set(/^\[/.test(key)?"[index]":key.replace(/\s+/g,""),val.trim())}return out}export{declBody,stripArrows,topLevelFields,topLevelKeys};
|
package/src/lib/env.mjs
ADDED
|
@@ -0,0 +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};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function readExportsFromIndex(repoRoot: string, entryRelative?: string): Array<{ name: string; source: string }>;
|
|
2
|
+
export function resolveComponentFilePath(repoRoot: string, relative: string): string | null;
|
|
3
|
+
export function hasComponentJSDoc(filePath: string, componentName: string): boolean;
|
|
4
|
+
export function parseJSDocFile(filePath: string, componentName: string): Record<string, unknown>;
|
|
5
|
+
export function parseJSDocBlock(fileContent: string, componentName?: string): Record<string, unknown>;
|
|
6
|
+
export function toDisplayName(pascal: string): string;
|
|
7
|
+
export function toRepoRelative(repoRoot: string, absPath: string): string;
|
|
8
|
+
export function inferComponentType(name: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync,existsSync}from"node:fs";import{resolve}from"node:path";function resolveComponentFilePath(repoRoot,relative){const base=resolve(repoRoot,"src",relative);const dirBasename=base.split("/").pop();const candidates=[`${base}.ts`,`${base}.tsx`,`${base}/${dirBasename}.tsx`,`${base}/${dirBasename}.ts`,`${base}/index.ts`,`${base}/index.tsx`];return candidates.find(p=>existsSync(p))??null}function toRepoRelative(repoRoot,absPath){const srcRoot=resolve(repoRoot,"src")+"/";return absPath.startsWith(srcRoot)?absPath.slice(srcRoot.length):absPath}function parseJSDocFile(filePath,componentName){try{const content=readFileSync(filePath,"utf8");return parseJSDocBlock(content,componentName)}catch{return{}}}function hasComponentJSDoc(filePath,componentName){if(!filePath)return false;let content;try{content=readFileSync(filePath,"utf8")}catch{return false}const blocks=content.match(/\/\*\*[\s\S]*?\*\//g);if(!blocks)return false;return blocks.some(b=>componentName?new RegExp(`@component\\s+${escapeRegExp(componentName)}\\b`).test(b):b.includes("@component"))}function parseJSDocBlock(fileContent,componentName){const blocks=fileContent.match(/\/\*\*[\s\S]*?\*\//g);if(!blocks)return{};const componentBlock=componentName?blocks.find(b=>new RegExp(`@component\\s+${escapeRegExp(componentName)}\\b`).test(b))??blocks.find(b=>b.includes("@component")):blocks.find(b=>b.includes("@component"));if(!componentBlock)return{};const body=componentBlock.replace(/\/\*\*|\*\//g,"").split("\n").map(line=>line.replace(/^\s*\*\s?/,"")).join("\n");const meta={};const descMatch=body.match(/@description\s+([\s\S]+?)(?=\n\s*@|$)/);if(descMatch){meta.description=descMatch[1].replace(/\s+/g," ").trim()}const agentInstructionsMatch=body.match(/@agentInstructions\s+([\s\S]+?)(?=\n\s*@\w+|$)/);if(agentInstructionsMatch){meta.agent_instructions=agentInstructionsMatch[1].trim()}const categoryMatch=body.match(/@category\s+([\w-]+(?:\/[\w-]+)*)/);if(categoryMatch){meta.family=categoryMatch[1];meta.component_type=categoryMatch[1].split("/")[0]}const shapes={raw:readShapes(body),resolving:new Set};const propsMatches=body.match(/@props\s+\{[^}]+\}\s+[^\n]+/g);if(propsMatches){const schema={type:"object",properties:{},required:[]};for(const line of propsMatches){const m=line.match(/@props\s+\{([^}]+)\}\s+(?:\[([^=\]]+)(?:=[^\]]+)?\]|([\w-]+(?:\.[\w-]+)*))\s*-\s*(.+)/);if(!m)continue;const[,type,optName,reqName,desc]=m;const propName=(optName||reqName).split(".").pop();schema.properties[propName]={...toSchemaFragment(type,shapes),description:desc.trim()};if(!optName)schema.required.push(propName)}if(Object.keys(schema.properties).length>0){if(schema.required.length===0)delete schema.required;meta.props_schema=schema}}const examples=[];let cursor=0;while(cursor<body.length){const tagRe=/(^|\n)\s*@example\b/g;tagRe.lastIndex=cursor;const tagMatch=tagRe.exec(body);if(!tagMatch)break;const tagIdx=tagMatch.index+tagMatch[0].length-"@example".length;const braceIdx=body.indexOf("{",tagIdx);if(braceIdx===-1)break;const end=findBalancedEnd(body,braceIdx);if(end===-1)break;const raw=body.slice(braceIdx,end+1).replace(/\s+/g," ").trim();try{examples.push(JSON.parse(raw))}catch{}cursor=end+1}if(examples.length>0)meta.usage_examples=examples;const depsMatch=body.match(/@dependencies\s+(\[[\s\S]*?\])/);if(depsMatch){try{meta.dependencies=JSON.parse(depsMatch[1])}catch{}}const useCasesMatch=body.match(/@useCases\s+(\[[\s\S]*?\])/);if(useCasesMatch){try{meta.use_cases=JSON.parse(useCasesMatch[1])}catch{}}const dataDecl={};for(const m of body.matchAll(/@data\s+(\w+)(\??)\s+\{([^}]+)\}\s*-\s*via\s+([\w.]+)/g)){const[,name,optional,type,registry]=m;dataDecl[name]={registry,type:type.trim(),required:optional!=="?"}}const actionDecl={};for(const m of body.matchAll(/@action\s+(\w+)(\??)\s+\{([^}]+?)\s*(?:→|->)\s*([^}]+?)\}\s*-\s*via\s+([\w.]+)/g)){const[,name,optional,input,output,registry]=m;actionDecl[name]={registry,input:input.trim(),output:output.trim(),required:optional!=="?"}}const printPolicy={};for(const m of body.matchAll(/@printPolicy\s+([^\n*]+)/g)){for(const token of m[1].trim().split(/\s+/)){if(!token)continue;const eq=token.indexOf("=");if(eq===-1)printPolicy[token]=true;else printPolicy[token.slice(0,eq)]=token.slice(eq+1)}}if(Object.keys(printPolicy).length>0)meta.print_policy=printPolicy;const one=tag=>body.match(new RegExp(`(?:^|\\n)\\s*@${tag}\\s+([^\\n]+)`))?.[1]?.trim();const all=tag=>[...body.matchAll(new RegExp(`(?:^|\\n)\\s*@${tag}\\s+([^\\n]+)`,"g"))].map(m=>m[1].trim());const names=raw=>{if(raw.startsWith("[")){try{return JSON.parse(raw).map(String)}catch{return[]}}return raw.split(",").map(x=>x.trim()).filter(Boolean)};const summary=one("summary");if(summary)meta.summary=summary.replace(/\s+/g," ");const kit=one("kit");if(kit)meta.kit=kit;const tags=one("tags");if(tags)meta.tags=names(tags);const pageCell=one("pageCell");if(pageCell!==void 0)meta.page_cell=pageCell!=="false";const links=[];for(const raw of all("instead")){const m=raw.match(/^(\w+)\s*(?:-\s*(.+))?$/);if(m)links.push({relation:"instead_of",to:m[1],note:m[2]?.trim()??null})}for(const[tag,relation]of[["parent","child_of"],["child","parent_of"]]){for(const raw of all(tag))for(const name of names(raw))links.push({relation,to:name,note:null})}for(const raw of all("related"))for(const name of names(raw))links.push({relation:"related",to:name,note:null});if(links.length>0)meta.links=links;if(/(?:^|\n)\s*@size\b/.test(body))meta.size=true;if(Object.keys(dataDecl).length>0||Object.keys(actionDecl).length>0){meta.props_schema=meta.props_schema??{type:"object",properties:{}};meta.props_schema.contract_version=meta.props_schema.contract_version??"1.0";if(Object.keys(dataDecl).length>0)meta.props_schema.data=dataDecl;if(Object.keys(actionDecl).length>0)meta.props_schema.actions=actionDecl}return meta}function collectReferencedKeys(specs){const queries=new Set;const mutations=new Set;for(const spec of specs){const data=spec.props_schema?.data??{};const actions=spec.props_schema?.actions??{};for(const decl of Object.values(data)){if(decl?.registry)queries.add(decl.registry)}for(const decl of Object.values(actions)){if(decl?.registry)mutations.add(decl.registry)}}return{queries,mutations}}function projectContractsManifest(specs){const out={contract_version:"1.0",components:{}};for(const spec of specs){const ps=spec.props_schema;const pp=spec.print_policy;if(!pp&&(!ps||!ps.data&&!ps.actions))continue;out.components[spec.component_name]={contract_version:ps?.contract_version??"1.0",...ps?.data?{data:ps.data}:{},...ps?.actions?{actions:ps.actions}:{},...pp?{print_policy:pp}:{}}}return out}function toDisplayName(pascal){return pascal.replace(/([A-Z])/g," $1").replace(/^./,s=>s.toUpperCase()).trim()}function inferComponentType(name){const n=name.toLowerCase();if(n.includes("chart")||n.includes("graph"))return"chart";if(n.includes("simulator")||n.includes("slider")||n.includes("interactive"))return"interactive";if(n.includes("header")||n.includes("layout")||n.includes("container"))return"layout";if(n.includes("card")||n.includes("tile")||n.includes("grid")||n.includes("strip")||n.includes("box"))return"display";return"ui"}function readExportsFromIndex(repoRoot,entryRelative="src/index.ts"){const indexPath=resolve(repoRoot,entryRelative);const indexSource=readFileSync(indexPath,"utf8");const defaultAsPattern=/export\s*\{\s*default\s+as\s+([A-Z][A-Za-z0-9_]*)\s*\}\s*from\s*['"]([^'"]+)['"]/g;const namedExportPattern=/export\s*\{\s*([^}]+?)\s*\}\s*from\s*['"]([^'"]+)['"]/g;const out=[];for(const match of indexSource.matchAll(defaultAsPattern)){out.push({name:match[1],source:match[2]})}for(const match of indexSource.matchAll(namedExportPattern)){if(/\bdefault\s+as\b/.test(match[1]))continue;const source=match[2];for(const raw of match[1].split(",")){const trimmed=raw.trim();if(!trimmed)continue;const aliasMatch=trimmed.match(/^([A-Za-z0-9_]+)\s+as\s+([A-Za-z0-9_]+)$/);const name=aliasMatch?aliasMatch[2]:trimmed;if(!/^[A-Z][A-Za-z0-9_]*$/.test(name))continue;if(out.some(e=>e.name===name))continue;out.push({name,source})}}return out}function escapeRegExp(literal){return literal.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function findBalancedEnd(text,openIdx){let depth=0;let i=openIdx;let inString=null;while(i<text.length){const ch=text[i];if(inString){if(ch==="\\")i++;else if(ch===inString)inString=null}else if(ch==='"'||ch==="'"){inString=ch}else if(ch==="{"||ch==="["){depth++}else if(ch==="}"||ch==="]"){depth--;if(depth===0)return i}i++}return-1}const TYPE_ALIASES={ValueSpec:{type:"object",properties:{value:{type:"number"},format:{type:"string"},precision:{type:"number"},signed:{type:"boolean"},unit:{type:"string"}},required:["value","format"]}};TYPE_ALIASES.FormattableValue={oneOf:[{type:"string"},TYPE_ALIASES.ValueSpec]};TYPE_ALIASES.ReactNode={};TYPE_ALIASES.Point={type:"object",properties:{x:{oneOf:[{type:"string"},{type:"number"}]},y:{type:"number"}},required:["x","y"]};TYPE_ALIASES.Series={type:"object",properties:{id:{type:"string"},label:{type:"string"},color:{type:"string"},strokeDasharray:{type:"string"},data:{type:"array",items:TYPE_ALIASES.Point}},required:["id","data"]};function expandTypeAlias(raw){const key=String(raw).trim();return Object.prototype.hasOwnProperty.call(TYPE_ALIASES,key)?structuredClone(TYPE_ALIASES[key]):null}function toSchemaFragment(raw,shapes){const token=String(raw).trim();const alias=expandTypeAlias(token);if(alias)return alias;const members=splitTopLevel(token,"|");if(members.length>1){const literals=members.map(m=>m.match(/^(['"])(.*)\1$/)?.[2]);if(literals.every(l=>l!==void 0))return{type:"string",enum:literals};return{oneOf:members.map(m=>toSchemaFragment(m,shapes))}}const element=token.endsWith("[]")?token.slice(0,-2).replace(/^\((.*)\)$/,"$1"):token.match(/^(?:Readonly)?Array<(.+)>$/)?.[1];if(element!==void 0){const items=toSchemaFragment(element,shapes);return"x-type"in items?{type:"array"}:{type:"array",items}}if(token.startsWith("{")&&token.endsWith("}"))return objectLiteral(token,shapes);const shape=shapes&&Object.prototype.hasOwnProperty.call(shapes.raw,token)?resolveShape(token,shapes):null;if(shape)return shape;const literal=token.match(/^(['"])(.*)\1$/);if(literal)return{type:"string",enum:[literal[2]]};if(/^\[.*\]$/.test(token))return{type:"array"};if(token==="null")return{type:"null"};if(/^(Readonly)?Record<.*>$/.test(token))return{type:"object"};const type=normalizeType(token);return["boolean","number","string","array","object"].includes(type)?{type}:{"x-type":token}}function readShapes(body){const raw={};for(const m of body.matchAll(/@shape\s+([A-Z]\w*)\s*(?=\{)/g)){const open=m.index+m[0].length;const end=findBalancedEnd(body,open);if(end>open)raw[m[1]]=body.slice(open,end+1).replace(/\s+/g," ")}return raw}function resolveShape(name,shapes){if(shapes.resolving.has(name))return{type:"object"};shapes.resolving.add(name);try{return toSchemaFragment(shapes.raw[name],shapes)}finally{shapes.resolving.delete(name)}}function objectLiteral(token,shapes){const schema={type:"object",properties:{}};const required=[];for(const field of splitTopLevel(token.slice(1,-1),",")){if(field==="..."){schema.additionalProperties=true;continue}const m=field.match(/^(['"]?)([\w$-]+)\1(\?)?\s*:\s*([\s\S]+)$/);if(!m)continue;schema.properties[m[2]]=toSchemaFragment(m[4],shapes);if(!m[3])required.push(m[2])}if(required.length)schema.required=required;return schema}function splitTopLevel(text,sep){const parts=[];let depth=0;let quote=null;let start=0;for(let i=0;i<text.length;i++){const ch=text[i];if(quote){if(ch===quote)quote=null}else if(ch==='"'||ch==="'")quote=ch;else if("[{(<".includes(ch))depth++;else if("]})>".includes(ch))depth--;else if(ch===sep&&depth===0){parts.push(text.slice(start,i).trim());start=i+1}}parts.push(text.slice(start).trim());return parts.filter(Boolean)}function normalizeType(raw){const t=raw.toLowerCase().trim();if(t==="boolean"||t==="bool")return"boolean";if(t==="number"||t==="integer"||t==="int"||t==="float")return"number";if(t==="string")return"string";if(t.endsWith("[]")||t.startsWith("array"))return"array";if(t.startsWith("{")||t==="object")return"object";return raw.trim()}export{collectReferencedKeys,expandTypeAlias,hasComponentJSDoc,inferComponentType,parseJSDocBlock,parseJSDocFile,projectContractsManifest,readExportsFromIndex,resolveComponentFilePath,toDisplayName,toRepoRelative,toSchemaFragment};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { KnowledgeLink, KnowledgeRow } from '@peripheries/runtime';
|
|
2
|
+
export type Knowledge = {
|
|
3
|
+
lineage: string;
|
|
4
|
+
rows: KnowledgeRow[];
|
|
5
|
+
links: KnowledgeLink[];
|
|
6
|
+
registry: Record<string, {
|
|
7
|
+
props_schema?: unknown;
|
|
8
|
+
}>;
|
|
9
|
+
scopeLineage?: string;
|
|
10
|
+
scopeRegistry?: Record<string, {
|
|
11
|
+
props_schema?: unknown;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export declare const holdsReference: (v: unknown) => boolean;
|
|
15
|
+
export declare function buildKnowledge(bundle: string, org: string): Promise<Knowledge>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{existsSync,readdirSync,readFileSync}from"node:fs";import{resolve}from"node:path";import{hasComponentJSDoc,parseJSDocFile,readExportsFromIndex,resolveComponentFilePath,toDisplayName}from"./jsdoc-parser.mjs";import{pathToFileURL}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"./root.mjs";function registryOf(entry){const out={};for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(file&&hasComponentJSDoc(file,name))out[name]={props_schema:parseJSDocFile(file,name).props_schema??void 0}}return out}const repoRoot=REPO_ROOT_;function readDoc(path){const text=readFileSync(path,"utf8");const m=text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);if(!m)throw new Error(`[knowledge] ${path} has no front matter`);const meta={};for(const line of m[1].split("\n")){const kv=line.match(/^([\w-]+):\s*(.*)$/);if(!kv)continue;const raw=kv[2].trim();try{meta[kv[1]]=JSON.parse(raw)}catch{meta[kv[1]]=raw}}return{meta,body:m[2].trim()}}const docsIn=dir=>existsSync(resolve(repoRoot,dir))?readdirSync(resolve(repoRoot,dir)).filter(f=>f.endsWith(".md")).sort().map(f=>({key:f.replace(/\.md$/,""),...readDoc(resolve(repoRoot,dir,f))})):[];const str=v=>typeof v==="string"?v:void 0;const strs=v=>Array.isArray(v)?v.map(String):[];function oneLine(text,fallback){const first=(text??"").split(/(?<=[.!?])\s+/)[0]?.trim()||fallback;if(first.length<=200)return first;return`${first.slice(0,199).replace(/\s+\S*$/,"")}\u2026`}function isPlainJson(v){if(v===null||["string","number","boolean"].includes(typeof v))return true;if(Array.isArray(v))return v.every(isPlainJson);if(typeof v!=="object"||Object.getPrototypeOf(v)!==Object.prototype||"$$typeof"in v)return false;return Object.values(v).every(x=>x===void 0||isPlainJson(x))}async function fixtureExamples(componentFile){const base=componentFile.replace(/\.tsx?$/,"");const file=[".fixtures.ts",".fixtures.tsx"].map(ext=>base+ext).find(f=>existsSync(f));if(!file)return[];const mod=await import(file);return(mod.scenarios??[]).filter(s=>["cell","whole-page"].includes(s.surface??"cell")&&s.props&&Object.keys(s.props).length>0&&isPlainJson(s.props)).map(s=>({name:s.name,props:JSON.parse(JSON.stringify(s.props))})).filter(s=>JSON.stringify(s.props).length<4e3).sort((a,b)=>JSON.stringify(a.props).length-JSON.stringify(b.props).length).slice(0,3)}const holdsReference=v=>typeof v==="string"&&(v.startsWith("@")||v.includes("${"))||(Array.isArray(v)?v.some(holdsReference):typeof v==="object"&&v!==null&&Object.values(v).some(holdsReference));async function loadVocabulary(){const file=["src/catalog/vocabulary.ts","src/catalog/vocabulary.tsx"].map(f=>resolve(repoRoot,f)).find(f=>existsSync(f));if(!file)return{};return(await import(pathToFileURL(file).href)).VOCABULARY??{}}async function buildKnowledge(bundle,org){const VOCABULARY=await loadVocabulary();const lineage=`${org}/${bundle}`;const entry=bundle==="main"?"src/index.ts":`src/index.${bundle}.ts`;const scopeLineage=`${org}/main`;const scopeRegistry=bundle==="main"?void 0:registryOf("src/index.ts");const bundleDocPath=resolve(repoRoot,"src/catalog",`${bundle}.md`);if(!existsSync(bundleDocPath))throw new Error(`[knowledge] no src/catalog/${bundle}.md: every lineage says how to compose a page from it.`);const bundleDoc=readDoc(bundleDocPath);const kits=docsIn("src/catalog/kits").filter(k=>(str(k.meta.bundle)??"main")===bundle);const patterns=docsIn("src/catalog/patterns").filter(p=>(str(p.meta.bundle)??"main")===bundle);const notPageCellFamilies=new Set(strs(bundleDoc.meta.not_page_cell_families));const pageCellExceptions=new Set(strs(bundleDoc.meta.page_cells));const notPageCells=new Set(strs(bundleDoc.meta.not_page_cells));const rows=[];const links=[];const registry={};rows.push({kind:"bundle",key:bundle,title:str(bundleDoc.meta.title)??lineage,summary:str(bundleDoc.meta.summary)??"",body:bundleDoc.body,attributes:{kits:kits.map(k=>k.key),patterns:patterns.map(p=>p.key)}});for(const kit of kits){const vocabulary=str(kit.meta.vocabulary);if(vocabulary&&!VOCABULARY[vocabulary])throw new Error(`[knowledge] kit ${kit.key} names vocabulary "${vocabulary}", which has no generator`);rows.push({kind:"kit",key:kit.key,title:str(kit.meta.title)??kit.key,summary:str(kit.meta.summary)??"",body:kit.body,tags:strs(kit.meta.tags),sort:Number(kit.meta.sort??0),attributes:{families:strs(kit.meta.families),...vocabulary?{vocabulary:VOCABULARY[vocabulary]()}:{}}})}const kitOfFamily=new Map(kits.flatMap(k=>strs(k.meta.families).map(f=>[f,k.key])));const cellId=name=>name.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();for(const{name,source}of readExportsFromIndex(repoRoot,entry)){const file=resolveComponentFilePath(repoRoot,source.replace(/^\.\//,""));if(!file||!hasComponentJSDoc(file,name))continue;const j=parseJSDocFile(file,name);registry[name]={props_schema:j.props_schema??void 0};const family=j.family??"other";const pageCell=j.page_cell??(notPageCells.has(name)?false:pageCellExceptions.has(name)||!notPageCellFamilies.has(family));const weight=e=>(holdsReference(e)?1e6:0)+JSON.stringify(e).length;const examples=[...j.usage_examples??[]].sort((a,b)=>weight(a)-weight(b));const snippets=examples.length?examples.map((props,i)=>({title:`${name}, example ${i+1}`,cells:[{id:cellId(name),component:name,props}]})):(await fixtureExamples(file)).map(f=>({title:`${name}, ${f.name}`,cells:[{id:cellId(name),component:name,props:f.props}]}));const useCases=j.use_cases?.length?`
|
|
2
|
+
|
|
3
|
+
Use it for:
|
|
4
|
+
${j.use_cases.map(u=>`- ${u}`).join("\n")}`:"";rows.push({kind:"component",key:name,title:toDisplayName(name),summary:oneLine(j.summary??j.description,`${toDisplayName(name)}.`),body:`${j.agent_instructions??j.description??""}${useCases}`.trim()||null,tags:j.tags??[],snippets,page_cell:pageCell,attributes:{family,...j.print_policy||j.size?{capabilities:{...j.print_policy?{print_policy:j.print_policy}:{},...j.size?{size:true}:{}}}:{}}});const kit=j.kit??kitOfFamily.get(family);if(kit)links.push({from:{kind:"component",key:name},relation:"part_of",to:{kind:"kit",key:kit}});for(const l of j.links??[])links.push({from:{kind:"component",key:name},relation:l.relation,to:{kind:"component",key:l.to},note:l.note})}for(const pattern of patterns){const slug=str(pattern.meta.page);if(!slug)throw new Error(`[knowledge] pattern ${pattern.key} names no page`);const dir=slug.includes("/")?resolve(repoRoot,"src",slug):resolve(repoRoot,"src/pages",slug);const page=JSON.parse(readFileSync(resolve(dir,"page.json"),"utf8"));const mock=existsSync(resolve(dir,"mock.json"))?JSON.parse(readFileSync(resolve(dir,"mock.json"),"utf8")):{};const data={...mock.data??{}};delete data.params;rows.push({kind:"pattern",key:pattern.key,title:str(pattern.meta.title)??pattern.key,summary:str(pattern.meta.summary)??"",body:pattern.body,tags:strs(pattern.meta.tags),sort:Number(pattern.meta.sort??0),snippets:[{title:str(pattern.meta.title)??pattern.key,intent:str(pattern.meta.summary),cells:page.content.cells,layout:page.layout,data}],attributes:{layout:page.layout?.type??"stack",...page.layout?.profile?{profile:page.layout.profile}:{},page:slug}});for(const component of new Set([...page.content.cells.map(c=>c.component).filter(c=>Boolean(c)),...strs(pattern.meta.uses)])){const inMain=scopeRegistry&&!registry[component]&&scopeRegistry[component];links.push({from:{kind:"pattern",key:pattern.key},relation:"uses",to:{...inMain?{lineage:scopeLineage}:{},kind:"component",key:component}})}for(const kit of strs(pattern.meta.kits))links.push({from:{kind:"pattern",key:pattern.key},relation:"uses",to:{kind:"kit",key:kit}})}return{lineage,rows,links,registry,...scopeRegistry?{scopeLineage,scopeRegistry}:{}}}export{buildKnowledge,holdsReference};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{existsSync,readFileSync,readdirSync}from"node:fs";import{basename,dirname,join,resolve}from"node:path";import{fileURLToPath}from"node:url";import{REPO_ROOT as REPO_ROOT_}from"./root.mjs";const REPO_ROOT=REPO_ROOT_;function orgSlug(){try{const json=JSON.parse(readFileSync(join(REPO_ROOT,"template","config","templates.json"),"utf8"));const slug=json?.org_template?.slug;if(typeof slug==="string"&&slug.length>0)return slug}catch{}return basename(REPO_ROOT)}function selfNames(){return new Set([orgSlug(),basename(REPO_ROOT)])}let topLevelCache=null;function topLevel(){if(topLevelCache===null){topLevelCache=new Set(readdirSync(REPO_ROOT))}return topLevelCache}function namesAnotherRepo(segment){if(!segment)return false;if(selfNames().has(segment))return false;if(topLevel().has(segment))return false;return!existsSync(join(REPO_ROOT,segment))}function stripSelfPrefix(ref){for(const name of selfNames()){if(ref.startsWith(`${name}/`))return ref.slice(name.length+1)}return ref}function assertNotSelf(names,where){const mine=selfNames();const collisions=names.filter(n=>mine.has(n));if(collisions.length>0){throw new Error(`[repo-identity] ${where} lists this repo's own name (${collisions.join(", ")}) as foreign.
|
|
2
|
+
This repo is "${orgSlug()}". A repo that is foreign to itself exempts its own
|
|
3
|
+
citations wholesale and reports green having checked nothing \u2014 remove it from that
|
|
4
|
+
list. This is what templating a repo without reviewing the list does.`)}}export{REPO_ROOT,assertNotSelf,namesAnotherRepo,orgSlug,selfNames,stripSelfPrefix};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const REPO_ROOT: string;
|
package/src/lib/root.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolve}from"node:path";const REPO_ROOT=resolve(process.env.ENGINE_ROOT??process.cwd());export{REPO_ROOT};
|
package/src/lib/run.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawnSync}from"node:child_process";import{existsSync}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={deploy:"deploy.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"};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}export{COMMANDS,commandFile,runCommand};
|