@zhama/tego-plugin-sdk 1.37.0 → 1.37.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/dist/dev.cjs +92 -69
- package/dist/dev.js +94 -71
- package/package.json +1 -1
package/dist/dev.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var W=Object.defineProperty;var o=(e,t)=>W(e,"name",{value:t,configurable:!0});var chunkIMAVYQ4U_cjs=require("./chunk-IMAVYQ4U.cjs"),chunk4LKRXDYY_cjs=require("./chunk-4LKRXDYY.cjs");require("./chunk-HZ4YWRYX.cjs");var chunkMWMGJJ74_cjs=require("./chunk-MWMGJJ74.cjs");require("./chunk-SKNPNPJP.cjs"),require("./chunk-45Y4CR4R.cjs");var chunk2P6UEAPV_cjs=require("./chunk-2P6UEAPV.cjs");require("./chunk-N4SXZZPY.cjs"),require("./chunk-6HNG5OZI.cjs");var chunkBN6TGHSB_cjs=require("./chunk-BN6TGHSB.cjs");require("./chunk-AJY7ZHXK.cjs"),require("./chunk-OP64Y4IN.cjs");var crypto=require("crypto"),fs=require("fs"),promises=require("fs/promises"),http=require("http"),module$1=require("module"),path=require("path"),child_process=require("child_process"),net=require("net"),DEV_USER_HEADER="x-tego-dev-user-id",DEV_ROLES_HEADER="x-tego-dev-roles",DEV_TENANT_ID="dev-tenant",DEFAULT_DEV_USER_ID="00000000-0000-4000-8000-000000000001";function devCallerOf(e){const t=o(n=>(Array.isArray(n)?n[0]:n)?.trim()??"","single"),r=t(e[DEV_USER_HEADER])||DEFAULT_DEV_USER_ID;let a=[];try{const n=JSON.parse(t(e[DEV_ROLES_HEADER])||"[]");Array.isArray(n)&&(a=n.filter(i=>typeof i=="string"))}catch{}return{userId:r,roles:a}}o(devCallerOf,"devCallerOf");function gatewayHeaders(e,t,r,a=Date.now()){const n={};for(const[s,l]of Object.entries(e)){const d=s.toLowerCase();l===void 0||d.startsWith("x-tego-")||d==="cookie"||d==="authorization"||d==="host"||d==="connection"||(n[d]=Array.isArray(l)?l.join(", "):l)}const i=JSON.stringify(t.roles),u=String(a);return n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.userId]=t.userId,n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.tenantId]=DEV_TENANT_ID,n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.roles]=i,n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.issuedAt]=u,n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.signature]=chunkIMAVYQ4U_cjs.signPluginContext({userId:t.userId,tenantId:DEV_TENANT_ID,rolesHeader:i,avatarId:"",issuedAt:u},r),n}o(gatewayHeaders,"gatewayHeaders");var DEV_MIGRATIONS_TABLE="tego_dev_migrations";function devSchemaName(e){return`tego_dev_${e.toLowerCase().replace(/[^a-z0-9_]/g,"_")}`.slice(0,63)}o(devSchemaName,"devSchemaName");function devDatabaseUrl(e,t){const r=new URL(e);r.searchParams.delete("options");const a=r.search.replace(/^\?/,"");return r.search=`?${a?`${a}&`:""}options=${encodeURIComponent(`-c search_path=${t}`)}`,r.toString()}o(devDatabaseUrl,"devDatabaseUrl");async function connect(e){let t;try{t=await import("pg")}catch(n){throw new Error(`The local backend needs the "pg" driver; add it to the plugin's dependencies.`,{cause:n})}const r=t.default?.Client??t.Client;if(!r)throw new Error('The "pg" module has no Client constructor.');const a=new r({connectionString:e});try{await a.connect()}catch(n){throw new Error(`Cannot connect to DATABASE_URL (${redactUrl(e)}): ${n instanceof Error?n.message:String(n)}`,{cause:n})}return a}o(connect,"connect");function redactUrl(e){try{const t=new URL(e);return t.password&&(t.password="***"),t.toString()}catch{return"<unparseable DATABASE_URL>"}}o(redactUrl,"redactUrl");var quote=o(e=>`"${e.replace(/"/g,'""')}"`,"quote");async function applyDevMigrations(e){const t=await promises.readdir(e.migrationsDir).then(i=>i.filter(u=>u.endsWith(".sql")).sort(),i=>{if(i.code==="ENOENT")return[];throw i}),r=await connect(e.databaseUrl),a=quote(e.schema),n=`${a}.${quote(DEV_MIGRATIONS_TABLE)}`;try{await r.query(`CREATE SCHEMA IF NOT EXISTS ${a}`),await r.query(`CREATE TABLE IF NOT EXISTS ${n} (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`);const i=new Set((await r.query(`SELECT name FROM ${n}`)).rows.map(s=>String(s.name))),u=[];for(const s of t){if(i.has(s))continue;const l=await promises.readFile(path.join(e.migrationsDir,s),"utf8");await r.query("BEGIN");try{await r.query(`SET LOCAL search_path TO ${a}`),await r.query("SELECT set_config('app.tenant_id', $1, true)",[e.tenantId]),await r.query(l),await r.query(`INSERT INTO ${n} (name) VALUES ($1)`,[s]),await r.query("COMMIT")}catch(d){throw await r.query("ROLLBACK").catch(()=>{}),new Error(`Migration ${s} failed: ${d instanceof Error?d.message:String(d)}`,{cause:d})}u.push(s)}return u}finally{await r.end().catch(()=>{})}}o(applyDevMigrations,"applyDevMigrations");var REBUILD_DEBOUNCE_MS=250,STOP_TIMEOUT_MS=5e3;function freePort(){return new Promise((e,t)=>{const r=net.createServer();r.once("error",t),r.listen(0,"127.0.0.1",()=>{const a=r.address(),n=typeof a=="object"&&a?a.port:0;r.close(()=>e(n))})})}o(freePort,"freePort");function stopChild(e){return!e||e.exitCode!==null||e.signalCode!==null?Promise.resolve():new Promise(t=>{const r=setTimeout(()=>{e.kill("SIGKILL"),t()},STOP_TIMEOUT_MS);e.once("exit",()=>{clearTimeout(r),t()}),e.kill("SIGTERM")})}o(stopChild,"stopChild");async function startDevBackend(e){const{part:t,log:r}=e;if(t.runtime==="python")throw new Error("The local preview runs TypeScript backends only; a python backend runs through the platform.");const a=await freePort(),n=devSchemaName(e.pluginId),i=path.resolve(e.root,t.migrationsDirPath),u=path.join(e.outDir,t.entryPath),s={...process.env,NODE_ENV:"development",TEGO_PLUGIN_ID:e.pluginId,TEGO_PLUGIN_VERSION:e.version,TEGO_PLUGIN_INSTALL_ID:"dev-install",TEGO_TENANT_ID:DEV_TENANT_ID,TEGO_PLUGIN_PORT:String(a),TEGO_PLUGIN_HEALTH_PATH:t.healthPath,TEGO_PLUGIN_HOST_API_URL:e.hostApiUrl,TEGO_PLUGIN_HOST_API_TOKEN:e.hostApiToken,TEGO_PLUGIN_GATEWAY_KEY:e.gatewayKey,DATABASE_URL:devDatabaseUrl(e.databaseUrl,n)};let l=null,d=!1;async function f(){await promises.rm(e.outDir,{recursive:!0,force:!0}),await chunk4LKRXDYY_cjs.buildPluginBackend(t,{root:e.root,outDir:e.outDir});const g=await applyDevMigrations({databaseUrl:e.databaseUrl,schema:n,migrationsDir:i,tenantId:DEV_TENANT_ID});for(const E of g)r(`[backend] applied migration ${E}`)}o(f,"build");function v(){const g=child_process.spawn(process.execPath,[u],{env:s,stdio:["ignore","pipe","pipe"]});l=g,g.stdout?.on("data",E=>process.stdout.write(E)),g.stderr?.on("data",E=>process.stderr.write(E)),g.on("exit",E=>{l===g&&!d&&(l=null,r(`[backend] exited (code ${E??"null"}); it starts again on the next save`))})}o(v,"start"),await f(),v(),r(`[backend] ${e.pluginId} on 127.0.0.1:${a}, schema ${n} in ${redactUrl(e.databaseUrl)}`);let y=null,m=null,D=!1;const A=o(async()=>{if(m){D=!0;return}m=(async()=>{try{await f();const g=l;l=null,await stopChild(g),d||(v(),r("[backend] rebuilt and restarted"))}catch(g){r(`[backend] rebuild failed, the previous build keeps running: ${g instanceof Error?g.message:String(g)}`)}})(),await m,m=null,D&&!d&&(D=!1,await A())},"rebuild"),N=[];for(const g of[t.dir,"shared"]){const E=path.resolve(e.root,g);fs.existsSync(E)&&N.push(fs.watch(E,{recursive:!0},(_,k)=>{k&&/(^|[\\/])(node_modules|dist)([\\/]|$)/.test(String(k))||(y&&clearTimeout(y),y=setTimeout(()=>void A(),REBUILD_DEBOUNCE_MS))}))}return{port:a,schema:n,async close(){d=!0,y&&clearTimeout(y);for(const g of N)g.close();await m,await stopChild(l)}}}o(startDevBackend,"startDevBackend");var FAKE_HOST_PREFIX="/__tego/host",FAKE_FILES_PREFIX="/__tego/files",MAX_JSON_BYTES=1024*1024;async function readBody(e,t=1/0){const r=[];let a=0;for await(const n of e){if(a+=n.length,a>t)throw Object.assign(new Error("body too large"),{status:413});r.push(n)}return Buffer.concat(r)}o(readBody,"readBody");function send(e,t,r){if(r===void 0){e.writeHead(t),e.end();return}e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(r))}o(send,"send");function storagePath(e,t){const r=path.resolve(e),a=path.resolve(r,t);if(!t||path.isAbsolute(t)||!a.startsWith(r+path.sep))throw Object.assign(new Error(`invalid storage key ${JSON.stringify(t)}`),{status:400});return a}o(storagePath,"storagePath");var metaOf=o(e=>`${e}.tego-meta.json`,"metaOf");async function head(e){const t=await promises.stat(e).catch(()=>null);if(!t?.isFile())return null;const r=await promises.readFile(metaOf(e),"utf8").then(a=>JSON.parse(a),()=>({}));return{size:t.size,contentType:r.contentType??null,versionTag:crypto.createHash("sha1").update(`${t.size}:${t.mtimeMs}`).digest("hex")}}o(head,"head");async function put(e,t,r){await promises.mkdir(path.dirname(e),{recursive:!0}),await promises.writeFile(e,t),await promises.writeFile(metaOf(e),JSON.stringify({contentType:r??null}))}o(put,"put");async function handleFakeHost(e,t,r){const a=new URL(e.url??"/","http://preview.local");try{if(a.pathname.startsWith(FAKE_FILES_PREFIX)){const s=storagePath(r.storageDir,a.searchParams.get("key")??""),l=await head(s);if(!l)return send(t,404);t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(await promises.readFile(s));return}if(e.headers.authorization!==`Bearer ${r.token}`)return send(t,401,{error:"bad host API token"});const n=a.pathname.slice(FAKE_HOST_PREFIX.length)||"/",i=o(async()=>JSON.parse((await readBody(e,MAX_JSON_BYTES)).toString("utf8")||"{}"),"json"),u=o(s=>storagePath(r.storageDir,String(s??"")),"keyFile");switch(n){case"/config":return send(t,200,{data:{values:r.config()}});case"/log":{const{level:s,message:l,fields:d}=await i();return r.log(`[backend ${String(s??"info")}] ${String(l??"")}${d?` ${JSON.stringify(d)}`:""}`),send(t,200,{data:{}})}case"/events":{const{name:s,payload:l}=await i();return r.log(`[event] ${String(s)} ${JSON.stringify(l??null)}`),send(t,200,{data:{}})}case"/notifications":return r.log(`[notification] ${JSON.stringify(await i())}`),send(t,200,{data:{id:crypto.randomUUID()}});case"/directory/identity":{const{userId:s}=await i(),l=r.users().get(String(s));return send(t,200,{data:{active:!!l,roleNames:l??[],memberships:[],checkedAt:new Date().toISOString()}})}case"/directory/list":{const{kind:s,search:l,ids:d}=await i(),f=typeof l=="string"?l.toLowerCase():"",v=Array.isArray(d)?new Set(d.map(String)):void 0,y=s==="users"?[...r.users().keys()].filter(m=>(!v||v.has(m))&&m.toLowerCase().includes(f)).map(m=>({id:m,displayName:m,active:!0})):[];return send(t,200,{data:{items:y,hasMore:!1}})}case"/directory/match":{const{userId:s,principals:l}=await i(),d=Array.isArray(l)?l:[];return send(t,200,{data:{active:r.users().has(String(s)),matched:d.flatMap((f,v)=>f.kind==="user"&&f.id===s?[v]:[]),checkedAt:new Date().toISOString()}})}case"/directory/validate":{const{principals:s}=await i(),l=Array.isArray(s)?s:[],d=r.users();return send(t,200,{data:{invalid:l.flatMap((f,v)=>f.kind==="user"&&d.has(String(f.id))?[]:[v])}})}case"/storage/get":case"/storage/stream":{const s=u(a.searchParams.get("key"));if(e.method==="POST"&&n==="/storage/stream")return await put(s,await readBody(e),e.headers["content-type"]),send(t,200,{data:{}});const l=await head(s);if(!l)return send(t,404);let d=await promises.readFile(s);const f=Number(a.searchParams.get("bytes"));Number.isFinite(f)&&f>0&&(d=d.subarray(0,f)),t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(d);return}case"/storage/put":return await put(u(a.searchParams.get("key")),await readBody(e),e.headers["content-type"]),send(t,200,{data:{}});case"/storage/head":{const s=await head(u((await i()).key));return s?send(t,200,{data:s}):send(t,404)}case"/storage/delete":{const s=u((await i()).key);return await promises.rm(s,{force:!0}),await promises.rm(metaOf(s),{force:!0}),send(t,200,{data:{}})}case"/storage/copy":{const{sourceKey:s,destinationKey:l}=await i(),d=u(s),f=u(l);return await head(d)?(await promises.mkdir(path.dirname(f),{recursive:!0}),await promises.copyFile(d,f),await promises.copyFile(metaOf(d),metaOf(f)).catch(()=>{}),send(t,200,{data:await head(f)})):send(t,404)}case"/storage/signed-url":{const{key:s}=await i();u(s);const l=`${FAKE_FILES_PREFIX}?key=${encodeURIComponent(String(s))}`;return send(t,200,{data:{url:`${r.origin()}${l}`,path:l}})}default:return r.log(`[host] ${n} is not available in local preview`),send(t,501,{error:{code:"not_available_in_preview",message:`${n} is not available in local preview`}})}}catch(n){const i=n.status??500;send(t,i,{error:{message:n instanceof Error?n.message:String(n)}})}}o(handleFakeHost,"handleFakeHost");var SDK_PACKAGE="@tego/plugin-sdk",SDK_IMPORT=JSON.stringify(SDK_PACKAGE),SDK_FRONTEND_IMPORT=JSON.stringify(`${SDK_PACKAGE}/frontend`);function shellFiles(e){return[{path:"index.html",contents:indexHtml(e)},{path:"main.tsx",contents:mainTsx(e)},{path:"shell.css",contents:SHELL_CSS},{path:"plugin-surface.tsx",contents:PLUGIN_SURFACE_STUB}]}o(shellFiles,"shellFiles");var escapeHtml=o(e=>e.replace(/[&<>"]/g,t=>({"&":"&","<":"<",">":">",'"':"""})[t]),"escapeHtml");function indexHtml({displayName:e}){return`<!doctype html>
|
|
1
|
+
"use strict";var W=Object.defineProperty;var o=(e,t)=>W(e,"name",{value:t,configurable:!0});var chunkIMAVYQ4U_cjs=require("./chunk-IMAVYQ4U.cjs"),chunk4LKRXDYY_cjs=require("./chunk-4LKRXDYY.cjs");require("./chunk-HZ4YWRYX.cjs");var chunkMWMGJJ74_cjs=require("./chunk-MWMGJJ74.cjs");require("./chunk-SKNPNPJP.cjs"),require("./chunk-45Y4CR4R.cjs");var chunk2P6UEAPV_cjs=require("./chunk-2P6UEAPV.cjs");require("./chunk-N4SXZZPY.cjs"),require("./chunk-6HNG5OZI.cjs");var chunkBN6TGHSB_cjs=require("./chunk-BN6TGHSB.cjs");require("./chunk-AJY7ZHXK.cjs"),require("./chunk-OP64Y4IN.cjs");var crypto=require("crypto"),fs=require("fs"),promises=require("fs/promises"),http=require("http"),module$1=require("module"),path=require("path"),child_process=require("child_process"),net=require("net"),DEV_USER_HEADER="x-tego-dev-user-id",DEV_ROLES_HEADER="x-tego-dev-roles",DEV_TENANT_ID="dev-tenant",DEFAULT_DEV_USER_ID="00000000-0000-4000-8000-000000000001";function devCallerOf(e){const t=o(n=>(Array.isArray(n)?n[0]:n)?.trim()??"","single"),r=t(e[DEV_USER_HEADER])||DEFAULT_DEV_USER_ID;let a=[];try{const n=JSON.parse(t(e[DEV_ROLES_HEADER])||"[]");Array.isArray(n)&&(a=n.filter(i=>typeof i=="string"))}catch{}return{userId:r,roles:a}}o(devCallerOf,"devCallerOf");function gatewayHeaders(e,t,r,a=Date.now()){const n={};for(const[s,l]of Object.entries(e)){const d=s.toLowerCase();l===void 0||d.startsWith("x-tego-")||d==="cookie"||d==="authorization"||d==="host"||d==="connection"||(n[d]=Array.isArray(l)?l.join(", "):l)}const i=JSON.stringify(t.roles),u=String(a);return n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.userId]=t.userId,n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.tenantId]=DEV_TENANT_ID,n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.roles]=i,n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.issuedAt]=u,n[chunkBN6TGHSB_cjs.PLUGIN_CONTEXT_HEADERS.signature]=chunkIMAVYQ4U_cjs.signPluginContext({userId:t.userId,tenantId:DEV_TENANT_ID,rolesHeader:i,avatarId:"",issuedAt:u},r),n}o(gatewayHeaders,"gatewayHeaders");var DEV_MIGRATIONS_TABLE="tego_dev_migrations";function devSchemaName(e){return`tego_dev_${e.toLowerCase().replace(/[^a-z0-9_]/g,"_")}`.slice(0,63)}o(devSchemaName,"devSchemaName");function devDatabaseUrl(e,t){const r=new URL(e);r.searchParams.delete("options");const a=r.search.replace(/^\?/,"");return r.search=`?${a?`${a}&`:""}options=${encodeURIComponent(`-c search_path=${t}`)}`,r.toString()}o(devDatabaseUrl,"devDatabaseUrl");async function connect(e){let t;try{t=await import("pg")}catch(n){throw new Error(`The local backend needs the "pg" driver; add it to the plugin's dependencies.`,{cause:n})}const r=t.default?.Client??t.Client;if(!r)throw new Error('The "pg" module has no Client constructor.');const a=new r({connectionString:e});try{await a.connect()}catch(n){throw new Error(`Cannot connect to DATABASE_URL (${redactUrl(e)}): ${n instanceof Error?n.message:String(n)}`,{cause:n})}return a}o(connect,"connect");function redactUrl(e){try{const t=new URL(e);return t.password&&(t.password="***"),t.toString()}catch{return"<unparseable DATABASE_URL>"}}o(redactUrl,"redactUrl");var quote=o(e=>`"${e.replace(/"/g,'""')}"`,"quote");async function applyDevMigrations(e){const t=await promises.readdir(e.migrationsDir).then(i=>i.filter(u=>u.endsWith(".sql")).sort(),i=>{if(i.code==="ENOENT")return[];throw i}),r=await connect(e.databaseUrl),a=quote(e.schema),n=`${a}.${quote(DEV_MIGRATIONS_TABLE)}`;try{await r.query(`CREATE SCHEMA IF NOT EXISTS ${a}`),await r.query(`CREATE TABLE IF NOT EXISTS ${n} (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`);const i=new Set((await r.query(`SELECT name FROM ${n}`)).rows.map(s=>String(s.name))),u=[];for(const s of t){if(i.has(s))continue;const l=await promises.readFile(path.join(e.migrationsDir,s),"utf8");await r.query("BEGIN");try{await r.query(`SET LOCAL search_path TO ${a}`),await r.query("SELECT set_config('app.tenant_id', $1, true)",[e.tenantId]),await r.query(l),await r.query(`INSERT INTO ${n} (name) VALUES ($1)`,[s]),await r.query("COMMIT")}catch(d){throw await r.query("ROLLBACK").catch(()=>{}),new Error(`Migration ${s} failed: ${d instanceof Error?d.message:String(d)}`,{cause:d})}u.push(s)}return u}finally{await r.end().catch(()=>{})}}o(applyDevMigrations,"applyDevMigrations");var REBUILD_DEBOUNCE_MS=250,STOP_TIMEOUT_MS=5e3;function freePort(){return new Promise((e,t)=>{const r=net.createServer();r.once("error",t),r.listen(0,"127.0.0.1",()=>{const a=r.address(),n=typeof a=="object"&&a?a.port:0;r.close(()=>e(n))})})}o(freePort,"freePort");function stopChild(e){return!e||e.exitCode!==null||e.signalCode!==null?Promise.resolve():new Promise(t=>{const r=setTimeout(()=>{e.kill("SIGKILL"),t()},STOP_TIMEOUT_MS);e.once("exit",()=>{clearTimeout(r),t()}),e.kill("SIGTERM")})}o(stopChild,"stopChild");async function startDevBackend(e){const{part:t,log:r}=e;if(t.runtime==="python")throw new Error("The local preview runs TypeScript backends only; a python backend runs through the platform.");const a=await freePort(),n=devSchemaName(e.pluginId),i=path.resolve(e.root,t.migrationsDirPath),u=path.join(e.outDir,t.entryPath),s={...process.env,NODE_ENV:"development",TEGO_PLUGIN_ID:e.pluginId,TEGO_PLUGIN_VERSION:e.version,TEGO_PLUGIN_INSTALL_ID:"dev-install",TEGO_TENANT_ID:DEV_TENANT_ID,TEGO_PLUGIN_PORT:String(a),TEGO_PLUGIN_HEALTH_PATH:t.healthPath,TEGO_PLUGIN_HOST_API_URL:e.hostApiUrl,TEGO_PLUGIN_HOST_API_TOKEN:e.hostApiToken,TEGO_PLUGIN_GATEWAY_KEY:e.gatewayKey,DATABASE_URL:devDatabaseUrl(e.databaseUrl,n)};let l=null,d=!1;async function g(){await promises.rm(e.outDir,{recursive:!0,force:!0}),await chunk4LKRXDYY_cjs.buildPluginBackend(t,{root:e.root,outDir:e.outDir});const f=await applyDevMigrations({databaseUrl:e.databaseUrl,schema:n,migrationsDir:i,tenantId:DEV_TENANT_ID});for(const E of f)r(`[backend] applied migration ${E}`)}o(g,"build");function v(){const f=child_process.spawn(process.execPath,[u],{env:s,stdio:["ignore","pipe","pipe"]});l=f,f.stdout?.on("data",E=>process.stdout.write(E)),f.stderr?.on("data",E=>process.stderr.write(E)),f.on("exit",E=>{l===f&&!d&&(l=null,r(`[backend] exited (code ${E??"null"}); it starts again on the next save`))})}o(v,"start"),await g(),v(),r(`[backend] ${e.pluginId} on 127.0.0.1:${a}, schema ${n} in ${redactUrl(e.databaseUrl)}`);let y=null,m=null,N=!1;const D=o(async()=>{if(m){N=!0;return}m=(async()=>{try{await g();const f=l;l=null,await stopChild(f),d||(v(),r("[backend] rebuilt and restarted"))}catch(f){r(`[backend] rebuild failed, the previous build keeps running: ${f instanceof Error?f.message:String(f)}`)}})(),await m,m=null,N&&!d&&(N=!1,await D())},"rebuild"),A=[];for(const f of[t.dir,"shared"]){const E=path.resolve(e.root,f);fs.existsSync(E)&&A.push(fs.watch(E,{recursive:!0},(x,k)=>{k&&/(^|[\\/])(node_modules|dist)([\\/]|$)/.test(String(k))||(y&&clearTimeout(y),y=setTimeout(()=>void D(),REBUILD_DEBOUNCE_MS))}))}return{port:a,schema:n,async close(){d=!0,y&&clearTimeout(y);for(const f of A)f.close();await m,await stopChild(l)}}}o(startDevBackend,"startDevBackend");var FAKE_HOST_PREFIX="/__tego/host",FAKE_FILES_PREFIX="/__tego/files",MAX_JSON_BYTES=1024*1024;async function readBody(e,t=1/0){const r=[];let a=0;for await(const n of e){if(a+=n.length,a>t)throw Object.assign(new Error("body too large"),{status:413});r.push(n)}return Buffer.concat(r)}o(readBody,"readBody");function send(e,t,r){if(r===void 0){e.writeHead(t),e.end();return}e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(r))}o(send,"send");function storagePath(e,t){const r=path.resolve(e),a=path.resolve(r,t);if(!t||path.isAbsolute(t)||!a.startsWith(r+path.sep))throw Object.assign(new Error(`invalid storage key ${JSON.stringify(t)}`),{status:400});return a}o(storagePath,"storagePath");var metaOf=o(e=>`${e}.tego-meta.json`,"metaOf");async function head(e){const t=await promises.stat(e).catch(()=>null);if(!t?.isFile())return null;const r=await promises.readFile(metaOf(e),"utf8").then(a=>JSON.parse(a),()=>({}));return{size:t.size,contentType:r.contentType??null,versionTag:crypto.createHash("sha1").update(`${t.size}:${t.mtimeMs}`).digest("hex")}}o(head,"head");async function put(e,t,r){await promises.mkdir(path.dirname(e),{recursive:!0}),await promises.writeFile(e,t),await promises.writeFile(metaOf(e),JSON.stringify({contentType:r??null}))}o(put,"put");async function handleFakeHost(e,t,r){const a=new URL(e.url??"/","http://preview.local");try{if(a.pathname.startsWith(FAKE_FILES_PREFIX)){const s=storagePath(r.storageDir,a.searchParams.get("key")??""),l=await head(s);if(!l)return send(t,404);t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(await promises.readFile(s));return}if(e.headers.authorization!==`Bearer ${r.token}`)return send(t,401,{error:"bad host API token"});const n=a.pathname.slice(FAKE_HOST_PREFIX.length)||"/",i=o(async()=>JSON.parse((await readBody(e,MAX_JSON_BYTES)).toString("utf8")||"{}"),"json"),u=o(s=>storagePath(r.storageDir,String(s??"")),"keyFile");switch(n){case"/config":return send(t,200,{data:{values:r.config()}});case"/log":{const{level:s,message:l,fields:d}=await i();return r.log(`[backend ${String(s??"info")}] ${String(l??"")}${d?` ${JSON.stringify(d)}`:""}`),send(t,200,{data:{}})}case"/events":{const{name:s,payload:l}=await i();return r.log(`[event] ${String(s)} ${JSON.stringify(l??null)}`),send(t,200,{data:{}})}case"/notifications":return r.log(`[notification] ${JSON.stringify(await i())}`),send(t,200,{data:{id:crypto.randomUUID()}});case"/directory/identity":{const{userId:s}=await i(),l=r.users().get(String(s));return send(t,200,{data:{active:!!l,roleNames:l??[],memberships:[],checkedAt:new Date().toISOString()}})}case"/directory/list":{const{kind:s,search:l,ids:d}=await i(),g=typeof l=="string"?l.toLowerCase():"",v=Array.isArray(d)?new Set(d.map(String)):void 0,y=s==="users"?[...r.users().keys()].filter(m=>(!v||v.has(m))&&m.toLowerCase().includes(g)).map(m=>({id:m,displayName:m,active:!0})):[];return send(t,200,{data:{items:y,hasMore:!1}})}case"/directory/match":{const{userId:s,principals:l}=await i(),d=Array.isArray(l)?l:[];return send(t,200,{data:{active:r.users().has(String(s)),matched:d.flatMap((g,v)=>g.kind==="user"&&g.id===s?[v]:[]),checkedAt:new Date().toISOString()}})}case"/directory/validate":{const{principals:s}=await i(),l=Array.isArray(s)?s:[],d=r.users();return send(t,200,{data:{invalid:l.flatMap((g,v)=>g.kind==="user"&&d.has(String(g.id))?[]:[v])}})}case"/storage/get":case"/storage/stream":{const s=u(a.searchParams.get("key"));if(e.method==="POST"&&n==="/storage/stream")return await put(s,await readBody(e),e.headers["content-type"]),send(t,200,{data:{}});const l=await head(s);if(!l)return send(t,404);let d=await promises.readFile(s);const g=Number(a.searchParams.get("bytes"));Number.isFinite(g)&&g>0&&(d=d.subarray(0,g)),t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(d);return}case"/storage/put":return await put(u(a.searchParams.get("key")),await readBody(e),e.headers["content-type"]),send(t,200,{data:{}});case"/storage/head":{const s=await head(u((await i()).key));return s?send(t,200,{data:s}):send(t,404)}case"/storage/delete":{const s=u((await i()).key);return await promises.rm(s,{force:!0}),await promises.rm(metaOf(s),{force:!0}),send(t,200,{data:{}})}case"/storage/copy":{const{sourceKey:s,destinationKey:l}=await i(),d=u(s),g=u(l);return await head(d)?(await promises.mkdir(path.dirname(g),{recursive:!0}),await promises.copyFile(d,g),await promises.copyFile(metaOf(d),metaOf(g)).catch(()=>{}),send(t,200,{data:await head(g)})):send(t,404)}case"/storage/signed-url":{const{key:s}=await i();u(s);const l=`${FAKE_FILES_PREFIX}?key=${encodeURIComponent(String(s))}`;return send(t,200,{data:{url:`${r.origin()}${l}`,path:l}})}default:return r.log(`[host] ${n} is not available in local preview`),send(t,501,{error:{code:"not_available_in_preview",message:`${n} is not available in local preview`}})}}catch(n){const i=n.status??500;send(t,i,{error:{message:n instanceof Error?n.message:String(n)}})}}o(handleFakeHost,"handleFakeHost");var SDK_PACKAGE="@tego/plugin-sdk",SDK_IMPORT=JSON.stringify(SDK_PACKAGE),SDK_FRONTEND_IMPORT=JSON.stringify(`${SDK_PACKAGE}/frontend`);function shellFiles(e){return[{path:"index.html",contents:indexHtml(e)},{path:"main.tsx",contents:mainTsx(e)},{path:"shell.css",contents:SHELL_CSS},{path:"plugin-surface.tsx",contents:PLUGIN_SURFACE_STUB}]}o(shellFiles,"shellFiles");var escapeHtml=o(e=>e.replace(/[&<>"]/g,t=>({"&":"&","<":"<",">":">",'"':"""})[t]),"escapeHtml");function indexHtml({displayName:e}){return`<!doctype html>
|
|
2
2
|
<html lang="en" class="h-full">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
@@ -55,12 +55,17 @@ import type { PluginFrontendModule, PluginHostContext, PluginRoute } from ${SDK_
|
|
|
55
55
|
import {
|
|
56
56
|
Button,
|
|
57
57
|
Input,
|
|
58
|
+
Label,
|
|
59
|
+
Popover,
|
|
60
|
+
PopoverContent,
|
|
61
|
+
PopoverTrigger,
|
|
58
62
|
Select,
|
|
59
63
|
SelectContent,
|
|
60
64
|
SelectItem,
|
|
61
65
|
SelectTrigger,
|
|
62
66
|
SelectValue,
|
|
63
67
|
Toaster,
|
|
68
|
+
Switch,
|
|
64
69
|
TooltipProvider,
|
|
65
70
|
} from '@tego/ui';
|
|
66
71
|
import i18next from 'i18next';
|
|
@@ -247,55 +252,39 @@ function DevBar() {
|
|
|
247
252
|
store('roles', roles);
|
|
248
253
|
location.reload();
|
|
249
254
|
};
|
|
255
|
+
// One quiet row: what the preview imitates, the plugin's pages, CSP problems, and the
|
|
256
|
+
// rarely changed settings (language, theme, who is signed in) behind one button.
|
|
250
257
|
return (
|
|
251
|
-
<div className='flex shrink-0 items-center gap-2 border-b border-border bg-card px-
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
|
|
258
|
+
<div className='flex h-10 shrink-0 items-center gap-2 border-b border-border bg-card px-2 text-sm'>
|
|
259
|
+
<div className='flex shrink-0 items-center rounded-control bg-sunken p-0.5' role='group' aria-label='Shell'>
|
|
260
|
+
{(['web', 'desktop'] as const).map(target => (
|
|
261
|
+
<button
|
|
262
|
+
key={target}
|
|
263
|
+
type='button'
|
|
264
|
+
aria-pressed={TARGET === target}
|
|
265
|
+
onClick={() => TARGET !== target && switchTarget(target)}
|
|
266
|
+
className={
|
|
267
|
+
'h-6 rounded-control px-2 text-xs font-medium transition-colors ' +
|
|
268
|
+
(TARGET === target ? 'bg-card text-foreground shadow-card' : 'text-muted-foreground hover:text-foreground')
|
|
269
|
+
}
|
|
270
|
+
>
|
|
271
|
+
{target === 'web' ? 'Web' : 'Desktop'}
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
<nav className='flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto' aria-label='Pages'>
|
|
255
276
|
{DEV.nav.map(entry => (
|
|
256
277
|
<Button
|
|
257
278
|
key={entry.key}
|
|
258
279
|
size='sm'
|
|
259
280
|
variant={current.path === entry.path ? 'secondary' : 'ghost'}
|
|
281
|
+
className='h-7 shrink-0'
|
|
260
282
|
onClick={() => navigate(entry.path)}
|
|
261
283
|
>
|
|
262
284
|
{i18next.t(entry.labelKey, { ns: NAMESPACE, lng: lang })}
|
|
263
285
|
</Button>
|
|
264
286
|
))}
|
|
265
287
|
</nav>
|
|
266
|
-
<Select
|
|
267
|
-
value={lang}
|
|
268
|
-
onValueChange={async next => {
|
|
269
|
-
await loadLocale(next);
|
|
270
|
-
await i18next.changeLanguage(next);
|
|
271
|
-
store('lang', next);
|
|
272
|
-
setLang(next);
|
|
273
|
-
}}
|
|
274
|
-
>
|
|
275
|
-
<SelectTrigger className='h-8 w-28' aria-label='Language'>
|
|
276
|
-
<SelectValue />
|
|
277
|
-
</SelectTrigger>
|
|
278
|
-
<SelectContent>
|
|
279
|
-
{DEV.locales.map(tag => (
|
|
280
|
-
<SelectItem key={tag} value={tag}>
|
|
281
|
-
{tag}
|
|
282
|
-
</SelectItem>
|
|
283
|
-
))}
|
|
284
|
-
</SelectContent>
|
|
285
|
-
</Select>
|
|
286
|
-
<div className='flex shrink-0 items-center rounded-control border border-border p-0.5' role='group' aria-label='Shell'>
|
|
287
|
-
{(['web', 'desktop'] as const).map(target => (
|
|
288
|
-
<Button
|
|
289
|
-
key={target}
|
|
290
|
-
size='sm'
|
|
291
|
-
variant={TARGET === target ? 'secondary' : 'ghost'}
|
|
292
|
-
aria-pressed={TARGET === target}
|
|
293
|
-
onClick={() => TARGET !== target && switchTarget(target)}
|
|
294
|
-
>
|
|
295
|
-
{target === 'web' ? 'Web' : 'Desktop'}
|
|
296
|
-
</Button>
|
|
297
|
-
))}
|
|
298
|
-
</div>
|
|
299
288
|
{blocked > 0 && (
|
|
300
289
|
<span
|
|
301
290
|
className='shrink-0 rounded-control bg-destructive-soft px-1.5 py-0.5 text-xs text-destructive'
|
|
@@ -304,36 +293,70 @@ function DevBar() {
|
|
|
304
293
|
CSP blocked {blocked}
|
|
305
294
|
</span>
|
|
306
295
|
)}
|
|
307
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
296
|
+
<Popover>
|
|
297
|
+
<PopoverTrigger asChild>
|
|
298
|
+
<Button size='sm' variant='ghost' className='h-7 shrink-0 font-mono text-xs text-muted-foreground'>
|
|
299
|
+
{lang} \xB7 {user.userId.slice(0, 8)}
|
|
300
|
+
</Button>
|
|
301
|
+
</PopoverTrigger>
|
|
302
|
+
<PopoverContent align='end' className='w-72 space-y-3'>
|
|
303
|
+
<div className='space-y-1.5'>
|
|
304
|
+
<Label>Language</Label>
|
|
305
|
+
<Select
|
|
306
|
+
value={lang}
|
|
307
|
+
onValueChange={async next => {
|
|
308
|
+
await loadLocale(next);
|
|
309
|
+
await i18next.changeLanguage(next);
|
|
310
|
+
store('lang', next);
|
|
311
|
+
setLang(next);
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<SelectTrigger className='h-8' aria-label='Language'>
|
|
315
|
+
<SelectValue />
|
|
316
|
+
</SelectTrigger>
|
|
317
|
+
<SelectContent>
|
|
318
|
+
{DEV.locales.map(tag => (
|
|
319
|
+
<SelectItem key={tag} value={tag}>
|
|
320
|
+
{tag}
|
|
321
|
+
</SelectItem>
|
|
322
|
+
))}
|
|
323
|
+
</SelectContent>
|
|
324
|
+
</Select>
|
|
325
|
+
</div>
|
|
326
|
+
<div className='flex items-center justify-between'>
|
|
327
|
+
<Label htmlFor='tego-dev-dark'>Dark theme</Label>
|
|
328
|
+
<Switch
|
|
329
|
+
id='tego-dev-dark'
|
|
330
|
+
checked={dark}
|
|
331
|
+
onCheckedChange={next => {
|
|
332
|
+
document.documentElement.classList.toggle('dark', next);
|
|
333
|
+
store('theme', next ? 'dark' : 'light');
|
|
334
|
+
setDark(next);
|
|
335
|
+
}}
|
|
336
|
+
/>
|
|
337
|
+
</div>
|
|
338
|
+
<form
|
|
339
|
+
className='space-y-1.5 border-t border-border pt-3'
|
|
340
|
+
onSubmit={event => {
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
switchUser();
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
<Label>Signed in as</Label>
|
|
346
|
+
<Input className='h-8 font-mono text-xs' aria-label='User id' value={userId} onChange={event => setUserId(event.target.value)} />
|
|
347
|
+
<Input
|
|
348
|
+
className='h-8'
|
|
349
|
+
aria-label='Roles, comma-separated'
|
|
350
|
+
placeholder='Roles, comma-separated'
|
|
351
|
+
value={roles}
|
|
352
|
+
onChange={event => setRoles(event.target.value)}
|
|
353
|
+
/>
|
|
354
|
+
<Button size='sm' variant='outline' type='submit' className='w-full'>
|
|
355
|
+
Sign in
|
|
356
|
+
</Button>
|
|
357
|
+
</form>
|
|
358
|
+
</PopoverContent>
|
|
359
|
+
</Popover>
|
|
337
360
|
</div>
|
|
338
361
|
);
|
|
339
362
|
}
|
|
@@ -355,4 +378,4 @@ if (frontend) {
|
|
|
355
378
|
DEV.pluginId +
|
|
356
379
|
'/</p>';
|
|
357
380
|
}
|
|
358
|
-
`}o(mainTsx,"mainTsx");var CSP_REPORT_PATH="/__tego/csp-report",CSP_REPORT_PREFIX="TEGO_DEV_CSP ",TARGET_COOKIE="tego-dev-target",DESKTOP_MAX_RESPONSE_BYTES=100*1024*1024,SHARED=["default-src 'self'","object-src 'none'","base-uri 'self'"],PREVIEW_CSP={web:[...SHARED,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline'","img-src 'self' data: blob: https:","font-src 'self' data:","connect-src 'self' wss: ws:","frame-src 'self'","worker-src 'self' blob:"].join("; "),desktop:[...SHARED,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline' https:","img-src 'self' data: blob: http: https:","font-src 'self' data: blob: https:","media-src 'self' data: blob:","worker-src 'self' blob:","connect-src 'self' http: https: ws: wss:","frame-src 'self'"].join("; ")};function isPreviewTarget(e){return e==="web"||e==="desktop"}o(isPreviewTarget,"isPreviewTarget");function targetOf(e,t){const r=new URL(e.url??"/","http://preview.local").searchParams.get("target");if(isPreviewTarget(r))return r;const a=/(?:^|;\s*)tego-dev-target=(web|desktop)(?:;|$)/.exec(String(e.headers.cookie??""))?.[1];return isPreviewTarget(a)?a:t}o(targetOf,"targetOf");function parseCspReport(e){try{const t=JSON.parse(e)["csp-report"];if(!t)return;const r=String(t["effective-directive"]??t["violated-directive"]??""),a=String(t["blocked-uri"]??"");if(!r)return;const n=typeof t["source-file"]=="string"?t["source-file"]:void 0,i=typeof t["line-number"]=="number"?t["line-number"]:void 0;return{directive:r,blocked:a,...n?{source:n}:{},...i?{line:i}:{}}}catch{return}}o(parseCspReport,"parseCspReport");var DEFAULT_DEV_PORT=5280,PORT_ATTEMPTS=20,DEV_READY_PREFIX="TEGO_DEV_READY ",DEV_DIR=".tego-dev",PLUGIN_API_PREFIX="/api/v1/tego-os/plugin-api/";function displayNameOf(e){const{displayName:t}=e;return typeof t=="string"?t:t.en??t["zh-CN"]??Object.values(t)[0]??e.id}o(displayNameOf,"displayNameOf");function packageDir(e,t){for(let r=path.resolve(e);;r=path.dirname(r)){const a=path.join(r,"node_modules",...t.split("/"));if(fs.existsSync(path.join(a,"package.json")))return fs.realpathSync(a);if(path.dirname(r)===r)return}}o(packageDir,"packageDir");function sendJson(e,t,r){e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(r))}o(sendJson,"sendJson");function listen(e,t,r){return new Promise((a,n)=>{const i=o(s=>{e.off("listening",u),n(s)},"onError"),u=o(()=>{e.off("error",i);const s=e.address();a(typeof s=="object"&&s?s.port:t)},"onListening");e.once("error",i),e.once("listening",u),e.listen(t,r)})}o(listen,"listen");async function shellTailwind(e,t){const r=module$1.createRequire(path.join(e,"vite.config.ts"));let a,n;try{a=r("@tego/ui/tailwind-preset"),n=path.dirname(r.resolve("@tego/ui"))}catch(u){throw new Error(`The preview cannot load @tego/ui's Tailwind preset from ${e}: ${u instanceof Error?u.message:String(u)}. Install the plugin's dependencies.`,{cause:u})}const i=(await import("tailwindcss")).default;return i({presets:[a],content:[path.join(n,"**/*.{js,cjs}"),path.join(t,"*.tsx")]})}o(shellTailwind,"shellTailwind");function perStylesheet(e,t,r){return{postcssPlugin:"tego-dev-stylesheets",async Once(a,n){const i=a.source?.input.file,u=i&&path.resolve(i)===e?[t]:r;if(u.length===0)return;const s=await n.postcss(u).process(a,{from:i});n.result.messages.push(...s.messages)}}}o(perStylesheet,"perStylesheet");function servePlugins(e){return Array.isArray(e)?e.flat(1/0).map(t=>{if(!t||typeof t!="object")return t;const{buildStart:r}=t;return typeof r!="function"?t:{...t,buildStart(...a){const n=new Proxy(this,{get(i,u){if(u==="emitFile")return()=>"";const s=Reflect.get(i,u);return typeof s=="function"?s.bind(i):s}});return r.apply(n,a)}}}):e}o(servePlugins,"servePlugins");async function startDevServer(e){const t=path.resolve(e.root),r=e.log??(c=>console.log(c)),a=await chunk4LKRXDYY_cjs.readManifest(t),n=chunk2P6UEAPV_cjs.resolvePluginParts(a),i=path.join(t,DEV_DIR),u=path.join(i,"shell"),s=crypto.randomBytes(32).toString("hex"),l=crypto.randomBytes(24).toString("hex");let d="";const f=new Map;let v,y={};const m=n.frontend?path.resolve(t,n.frontend.dir):t,{loadConfigFromFile:D,createServer:A,mergeConfig:N}=await import("vite");if(n.frontend){const c=path.join(m,"vite.config.ts");fs.existsSync(c)&&(y=(await D({command:"serve",mode:"development"},c,t))?.config??{});const p=y.build?.rollupOptions?.input,w=path.resolve(t,typeof p=="string"?p:`${n.frontend.dir}/src/index.tsx`);if(!fs.existsSync(w))throw new Error(`The frontend entry ${path.relative(t,w)} does not exist.`);v=path.relative(u,w).split(path.sep).join("/"),v.startsWith(".")||(v=`./${v}`)}const g=o(()=>n.frontend?chunk4LKRXDYY_cjs.readPluginLocales(t,a,n.frontend.i18nDirPath):Promise.resolve(new Map),"localeFiles"),E=[...(await g()).keys()].map(c=>c.replace(/\.json$/,""));await promises.mkdir(u,{recursive:!0});for(const c of shellFiles({pluginId:a.id,displayName:displayNameOf(a),nav:n.frontend?.nav??[],config:n.frontend?.config?chunkMWMGJJ74_cjs.applyConfigDefaults(n.frontend.config.schema,{}):{},entry:v,locales:E,defaultTarget:e.target??"web"}))await promises.writeFile(path.join(u,c.path),c.contents);let _,k;const F=o(()=>n.backend?.config?chunkMWMGJJ74_cjs.applyConfigDefaults(n.backend.config.schema,{}):{},"backendConfig"),R=http.createServer((c,p)=>void M(c,p));async function M(c,p){const w=(c.url??"/").split("?")[0],T=targetOf(c,e.target??"web");try{if(w==="/favicon.ico"){p.writeHead(204),p.end();return}if(w===CSP_REPORT_PATH){const h=await new Promise(L=>{let P="";c.setEncoding("utf8"),c.on("data",x=>P+=x),c.on("end",()=>L(P))}),b=parseCspReport(h);b&&(r(`[csp:${T}] blocked ${b.blocked||"(inline)"} by ${b.directive}${b.source?` in ${b.source}${b.line?`:${b.line}`:""}`:""}`),r(`${CSP_REPORT_PREFIX}${JSON.stringify({target:T,...b})}`)),p.writeHead(204),p.end();return}if(w.startsWith(FAKE_HOST_PREFIX)||w.startsWith(FAKE_FILES_PREFIX)){await handleFakeHost(c,p,{token:l,storageDir:path.join(i,"storage"),config:F,origin:o(()=>d,"origin"),users:o(()=>f,"users"),log:r});return}const S=/^\/i18n\/([A-Za-z-]+)\.json$/.exec(w);if(S){const h=(await g()).get(`${S[1]}.json`);if(h===void 0)return sendJson(p,404,{error:`no ${S[1]}.json`});p.writeHead(200,{"content-type":"application/json"}),p.end(h);return}const I=`${PLUGIN_API_PREFIX}${a.id}`;if(w===I||w.startsWith(`${I}/`)){q(c,p,(c.url??"/").slice(I.length)||"/",T);return}if(String(c.headers.accept??"").includes("text/html")){p.setHeader("Content-Security-Policy",`${PREVIEW_CSP[T]}; report-uri ${CSP_REPORT_PATH}`);const h=new URL(c.url??"/","http://preview.local").searchParams.get("target");isPreviewTarget(h)&&p.setHeader("Set-Cookie",`${TARGET_COOKIE}=${h}; Path=/; SameSite=Lax`)}C.middlewares(c,p,()=>sendJson(p,404,{error:`${w} not found`}))}catch(S){sendJson(p,500,{error:S instanceof Error?S.message:String(S)})}}o(M,"route");function q(c,p,w,T){if(!_){sendJson(p,503,{error:k??"The plugin has no backend part."});return}const S=devCallerOf(c.headers);f.set(S.userId,S.roles);const I=http.request({host:"127.0.0.1",port:_.port,method:c.method,path:w,headers:{...gatewayHeaders(c.headers,S,s),...T==="desktop"?{"accept-encoding":"identity"}:{}}},h=>{if(T!=="desktop"){p.writeHead(h.statusCode??502,h.headers),h.pipe(p);return}const b=[];let L=0,P=!1;h.on("data",x=>{if(!P){if(L+=x.length,L>DESKTOP_MAX_RESPONSE_BYTES){P=!0,h.destroy(),r(`[desktop] ${c.method} ${w}: response over 100 MB refused (413)`),sendJson(p,413,{error:"Response over 100 MB: the desktop app refuses it. Serve large files through storage paths."});return}b.push(x)}}),h.on("end",()=>{if(P)return;const x=Buffer.concat(b),G={...h.headers};delete G["transfer-encoding"],G["content-length"]=String(x.length),p.writeHead(h.statusCode??502,G),p.end(x)})});I.on("error",h=>{p.headersSent?p.destroy():sendJson(p,502,{error:`The backend is not answering (${h.message}); see the terminal.`})}),c.pipe(I)}o(q,"proxy");const K=path.join(u,"shell.css"),J=(()=>{const c=y.css?.postcss;return c&&typeof c=="object"?c.plugins??[]:[]})(),$=(()=>{const c=packageDir(m,"react-i18next");return(c&&packageDir(c,"i18next"))??packageDir(m,"i18next")})(),{css:X,mode:Y,build:z,...U}=y;U.plugins=servePlugins(U.plugins);const C=await A(N(U,{configFile:!1,root:u,mode:"development",appType:"spa",clearScreen:!1,logLevel:"info",esbuild:{jsx:"automatic",jsxDev:!1},css:{postcss:{plugins:[perStylesheet(K,await shellTailwind(m,u),J)]}},resolve:{alias:[{find:"@tego/chat/plugin-surface",replacement:path.join(u,"plugin-surface.tsx")},...$?[{find:/^i18next$/,replacement:$}]:[]]},server:{middlewareMode:!0,hmr:{server:R},fs:{allow:[t,u,...$?[$]:[]]}},optimizeDeps:{entries:[path.join(u,"index.html")]}})),O=e.host??"127.0.0.1";let j;const H=e.port??DEFAULT_DEV_PORT;for(let c=0;j===void 0;c++)try{j=await listen(R,H===0?0:H+c,O)}catch(p){if(p.code!=="EADDRINUSE"||H===0||c>=PORT_ATTEMPTS)throw await C.close(),p}const V=O==="0.0.0.0"||O==="::"?"127.0.0.1":O;if(d=`http://${V.includes(":")?`[${V}]`:V}:${j}`,!n.backend)k="The plugin has no backend part.";else if(e.backend===!1)k="The backend is off (--no-backend).";else if(!e.databaseUrl)k="The backend needs a PostgreSQL: start the preview with DATABASE_URL=postgres://\u2026 (each plugin gets its own schema).";else try{_=await startDevBackend({root:t,pluginId:a.id,version:a.version,part:n.backend,databaseUrl:e.databaseUrl,outDir:path.join(i,"backend"),hostApiUrl:`${d}${FAKE_HOST_PREFIX}`,hostApiToken:l,gatewayKey:s,log:r})}catch(c){k=`The backend did not start: ${c instanceof Error?c.message:String(c)}`}k&&n.backend&&r(`[backend] ${k}`);const B=`${d}/p/${a.id}/`;return r(`${a.id} ${a.version}: local preview at ${B}`),e.databaseUrl&&_&&r(` database ${redactUrl(e.databaseUrl)}, schema ${_.schema}`),r(`${DEV_READY_PREFIX}${JSON.stringify({url:B})}`),{url:B,async close(){await _?.close(),await C.close(),R.closeAllConnections(),await new Promise(c=>R.close(()=>c()))}}}o(startDevServer,"startDevServer"),exports.CSP_REPORT_PREFIX=CSP_REPORT_PREFIX,exports.DEFAULT_DEV_PORT=DEFAULT_DEV_PORT,exports.DEV_DIR=DEV_DIR,exports.DEV_READY_PREFIX=DEV_READY_PREFIX,exports.DEV_ROLES_HEADER=DEV_ROLES_HEADER,exports.DEV_TENANT_ID=DEV_TENANT_ID,exports.DEV_USER_HEADER=DEV_USER_HEADER,exports.PREVIEW_CSP=PREVIEW_CSP,exports.applyDevMigrations=applyDevMigrations,exports.devCallerOf=devCallerOf,exports.devDatabaseUrl=devDatabaseUrl,exports.devSchemaName=devSchemaName,exports.gatewayHeaders=gatewayHeaders,exports.handleFakeHost=handleFakeHost,exports.parseCspReport=parseCspReport,exports.shellFiles=shellFiles,exports.startDevServer=startDevServer,exports.storagePath=storagePath,exports.targetOf=targetOf;
|
|
381
|
+
`}o(mainTsx,"mainTsx");var CSP_REPORT_PATH="/__tego/csp-report",CSP_REPORT_PREFIX="TEGO_DEV_CSP ",TARGET_COOKIE="tego-dev-target",DESKTOP_MAX_RESPONSE_BYTES=100*1024*1024,SHARED=["default-src 'self'","object-src 'none'","base-uri 'self'"],PREVIEW_CSP={web:[...SHARED,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline'","img-src 'self' data: blob: https:","font-src 'self' data:","connect-src 'self' wss: ws:","frame-src 'self'","worker-src 'self' blob:"].join("; "),desktop:[...SHARED,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline' https:","img-src 'self' data: blob: http: https:","font-src 'self' data: blob: https:","media-src 'self' data: blob:","worker-src 'self' blob:","connect-src 'self' http: https: ws: wss:","frame-src 'self'"].join("; ")};function isPreviewTarget(e){return e==="web"||e==="desktop"}o(isPreviewTarget,"isPreviewTarget");function targetOf(e,t){const r=new URL(e.url??"/","http://preview.local").searchParams.get("target");if(isPreviewTarget(r))return r;const a=/(?:^|;\s*)tego-dev-target=(web|desktop)(?:;|$)/.exec(String(e.headers.cookie??""))?.[1];return isPreviewTarget(a)?a:t}o(targetOf,"targetOf");function parseCspReport(e){try{const t=JSON.parse(e)["csp-report"];if(!t)return;const r=String(t["effective-directive"]??t["violated-directive"]??""),a=String(t["blocked-uri"]??"");if(!r)return;const n=typeof t["source-file"]=="string"?t["source-file"]:void 0,i=typeof t["line-number"]=="number"?t["line-number"]:void 0;return{directive:r,blocked:a,...n?{source:n}:{},...i?{line:i}:{}}}catch{return}}o(parseCspReport,"parseCspReport");var DEFAULT_DEV_PORT=5280,PORT_ATTEMPTS=20,DEV_READY_PREFIX="TEGO_DEV_READY ",DEV_DIR=".tego-dev",PLUGIN_API_PREFIX="/api/v1/tego-os/plugin-api/";function displayNameOf(e){const{displayName:t}=e;return typeof t=="string"?t:t.en??t["zh-CN"]??Object.values(t)[0]??e.id}o(displayNameOf,"displayNameOf");function packageDir(e,t){for(let r=path.resolve(e);;r=path.dirname(r)){const a=path.join(r,"node_modules",...t.split("/"));if(fs.existsSync(path.join(a,"package.json")))return fs.realpathSync(a);if(path.dirname(r)===r)return}}o(packageDir,"packageDir");function sendJson(e,t,r){e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(r))}o(sendJson,"sendJson");function listen(e,t,r){return new Promise((a,n)=>{const i=o(s=>{e.off("listening",u),n(s)},"onError"),u=o(()=>{e.off("error",i);const s=e.address();a(typeof s=="object"&&s?s.port:t)},"onListening");e.once("error",i),e.once("listening",u),e.listen(t,r)})}o(listen,"listen");async function shellTailwind(e,t){const r=module$1.createRequire(path.join(e,"vite.config.ts"));let a,n;try{a=r("@tego/ui/tailwind-preset"),n=path.dirname(r.resolve("@tego/ui"))}catch(u){throw new Error(`The preview cannot load @tego/ui's Tailwind preset from ${e}: ${u instanceof Error?u.message:String(u)}. Install the plugin's dependencies.`,{cause:u})}const i=(await import("tailwindcss")).default;return i({presets:[a],content:[path.join(n,"**/*.{js,cjs}"),path.join(t,"*.tsx")]})}o(shellTailwind,"shellTailwind");function perStylesheet(e,t,r){return{postcssPlugin:"tego-dev-stylesheets",async Once(a,n){const i=a.source?.input.file,u=i&&path.resolve(i)===e?[t]:r;if(u.length===0)return;const s=await n.postcss(u).process(a,{from:i});n.result.messages.push(...s.messages)}}}o(perStylesheet,"perStylesheet");function servePlugins(e){return Array.isArray(e)?e.flat(1/0).map(t=>{if(!t||typeof t!="object")return t;const{buildStart:r}=t;return typeof r!="function"?t:{...t,buildStart(...a){const n=new Proxy(this,{get(i,u){if(u==="emitFile")return()=>"";const s=Reflect.get(i,u);return typeof s=="function"?s.bind(i):s}});return r.apply(n,a)}}}):e}o(servePlugins,"servePlugins");async function startDevServer(e){const t=path.resolve(e.root),r=e.log??(c=>console.log(c)),a=await chunk4LKRXDYY_cjs.readManifest(t),n=chunk2P6UEAPV_cjs.resolvePluginParts(a),i=path.join(t,DEV_DIR),u=path.join(i,"shell"),s=crypto.randomBytes(32).toString("hex"),l=crypto.randomBytes(24).toString("hex");let d="";const g=new Map;let v,y={};const m=n.frontend?path.resolve(t,n.frontend.dir):t,{loadConfigFromFile:N,createServer:D,mergeConfig:A}=await import("vite");if(n.frontend){const c=path.join(m,"vite.config.ts");fs.existsSync(c)&&(y=(await N({command:"serve",mode:"development"},c,t))?.config??{});const p=y.build?.rollupOptions?.input,w=path.resolve(t,typeof p=="string"?p:`${n.frontend.dir}/src/index.tsx`);if(!fs.existsSync(w))throw new Error(`The frontend entry ${path.relative(t,w)} does not exist.`);v=path.relative(u,w).split(path.sep).join("/"),v.startsWith(".")||(v=`./${v}`)}const f=o(()=>n.frontend?chunk4LKRXDYY_cjs.readPluginLocales(t,a,n.frontend.i18nDirPath):Promise.resolve(new Map),"localeFiles"),E=[...(await f()).keys()].map(c=>c.replace(/\.json$/,""));await promises.mkdir(u,{recursive:!0});for(const c of shellFiles({pluginId:a.id,displayName:displayNameOf(a),nav:n.frontend?.nav??[],config:n.frontend?.config?chunkMWMGJJ74_cjs.applyConfigDefaults(n.frontend.config.schema,{}):{},entry:v,locales:E,defaultTarget:e.target??"web"}))await promises.writeFile(path.join(u,c.path),c.contents);let x,k;const G=o(()=>n.backend?.config?chunkMWMGJJ74_cjs.applyConfigDefaults(n.backend.config.schema,{}):{},"backendConfig"),R=http.createServer((c,p)=>void M(c,p));async function M(c,p){const w=(c.url??"/").split("?")[0],T=targetOf(c,e.target??"web");try{if(w==="/favicon.ico"){p.writeHead(204),p.end();return}if(w===CSP_REPORT_PATH){const h=await new Promise(O=>{let I="";c.setEncoding("utf8"),c.on("data",P=>I+=P),c.on("end",()=>O(I))}),b=parseCspReport(h);b&&(r(`[csp:${T}] blocked ${b.blocked||"(inline)"} by ${b.directive}${b.source?` in ${b.source}${b.line?`:${b.line}`:""}`:""}`),r(`${CSP_REPORT_PREFIX}${JSON.stringify({target:T,...b})}`)),p.writeHead(204),p.end();return}if(w.startsWith(FAKE_HOST_PREFIX)||w.startsWith(FAKE_FILES_PREFIX)){await handleFakeHost(c,p,{token:l,storageDir:path.join(i,"storage"),config:G,origin:o(()=>d,"origin"),users:o(()=>g,"users"),log:r});return}const S=/^\/i18n\/([A-Za-z-]+)\.json$/.exec(w);if(S){const h=(await f()).get(`${S[1]}.json`);if(h===void 0)return sendJson(p,404,{error:`no ${S[1]}.json`});p.writeHead(200,{"content-type":"application/json"}),p.end(h);return}const _=`${PLUGIN_API_PREFIX}${a.id}`;if(w===_||w.startsWith(`${_}/`)){q(c,p,(c.url??"/").slice(_.length)||"/",T);return}if(String(c.headers.accept??"").includes("text/html")){p.setHeader("Content-Security-Policy",`${PREVIEW_CSP[T]}; report-uri ${CSP_REPORT_PATH}`);const h=new URL(c.url??"/","http://preview.local").searchParams.get("target");isPreviewTarget(h)&&p.setHeader("Set-Cookie",`${TARGET_COOKIE}=${h}; Path=/; SameSite=Lax`)}U.middlewares(c,p,()=>sendJson(p,404,{error:`${w} not found`}))}catch(S){sendJson(p,500,{error:S instanceof Error?S.message:String(S)})}}o(M,"route");function q(c,p,w,T){if(!x){sendJson(p,503,{error:k??"The plugin has no backend part."});return}const S=devCallerOf(c.headers);g.set(S.userId,S.roles);const _=http.request({host:"127.0.0.1",port:x.port,method:c.method,path:w,headers:{...gatewayHeaders(c.headers,S,s),...T==="desktop"?{"accept-encoding":"identity"}:{}}},h=>{if(T!=="desktop"){p.writeHead(h.statusCode??502,h.headers),h.pipe(p);return}const b=[];let O=0,I=!1;h.on("data",P=>{if(!I){if(O+=P.length,O>DESKTOP_MAX_RESPONSE_BYTES){I=!0,h.destroy(),r(`[desktop] ${c.method} ${w}: response over 100 MB refused (413)`),sendJson(p,413,{error:"Response over 100 MB: the desktop app refuses it. Serve large files through storage paths."});return}b.push(P)}}),h.on("end",()=>{if(I)return;const P=Buffer.concat(b),F={...h.headers};delete F["transfer-encoding"],F["content-length"]=String(P.length),p.writeHead(h.statusCode??502,F),p.end(P)})});_.on("error",h=>{p.headersSent?p.destroy():sendJson(p,502,{error:`The backend is not answering (${h.message}); see the terminal.`})}),c.pipe(_)}o(q,"proxy");const K=path.join(u,"shell.css"),J=(()=>{const c=y.css?.postcss;return c&&typeof c=="object"?c.plugins??[]:[]})(),$=(()=>{const c=packageDir(m,"react-i18next");return(c&&packageDir(c,"i18next"))??packageDir(m,"i18next")})(),{css:X,mode:Y,build:z,...C}=y;C.plugins=servePlugins(C.plugins);const U=await D(A(C,{configFile:!1,root:u,mode:"development",appType:"spa",clearScreen:!1,logLevel:"info",esbuild:{jsx:"automatic",jsxDev:!1},css:{postcss:{plugins:[perStylesheet(K,await shellTailwind(m,u),J)]}},resolve:{alias:[{find:"@tego/chat/plugin-surface",replacement:path.join(u,"plugin-surface.tsx")},...$?[{find:/^i18next$/,replacement:$}]:[]]},server:{middlewareMode:!0,hmr:{server:R},fs:{allow:[t,u,...$?[$]:[]]}},optimizeDeps:{entries:[path.join(u,"index.html")]}})),L=e.host??"127.0.0.1";let j;const H=e.port??DEFAULT_DEV_PORT;for(let c=0;j===void 0;c++)try{j=await listen(R,H===0?0:H+c,L)}catch(p){if(p.code!=="EADDRINUSE"||H===0||c>=PORT_ATTEMPTS)throw await U.close(),p}const V=L==="0.0.0.0"||L==="::"?"127.0.0.1":L;if(d=`http://${V.includes(":")?`[${V}]`:V}:${j}`,!n.backend)k="The plugin has no backend part.";else if(e.backend===!1)k="The backend is off (--no-backend).";else if(!e.databaseUrl)k="The backend needs a PostgreSQL: start the preview with DATABASE_URL=postgres://\u2026 (each plugin gets its own schema).";else try{x=await startDevBackend({root:t,pluginId:a.id,version:a.version,part:n.backend,databaseUrl:e.databaseUrl,outDir:path.join(i,"backend"),hostApiUrl:`${d}${FAKE_HOST_PREFIX}`,hostApiToken:l,gatewayKey:s,log:r})}catch(c){k=`The backend did not start: ${c instanceof Error?c.message:String(c)}`}k&&n.backend&&r(`[backend] ${k}`);const B=`${d}/p/${a.id}/`;return r(`${a.id} ${a.version}: local preview at ${B}`),e.databaseUrl&&x&&r(` database ${redactUrl(e.databaseUrl)}, schema ${x.schema}`),r(`${DEV_READY_PREFIX}${JSON.stringify({url:B})}`),{url:B,async close(){await x?.close(),await U.close(),R.closeAllConnections(),await new Promise(c=>R.close(()=>c()))}}}o(startDevServer,"startDevServer"),exports.CSP_REPORT_PREFIX=CSP_REPORT_PREFIX,exports.DEFAULT_DEV_PORT=DEFAULT_DEV_PORT,exports.DEV_DIR=DEV_DIR,exports.DEV_READY_PREFIX=DEV_READY_PREFIX,exports.DEV_ROLES_HEADER=DEV_ROLES_HEADER,exports.DEV_TENANT_ID=DEV_TENANT_ID,exports.DEV_USER_HEADER=DEV_USER_HEADER,exports.PREVIEW_CSP=PREVIEW_CSP,exports.applyDevMigrations=applyDevMigrations,exports.devCallerOf=devCallerOf,exports.devDatabaseUrl=devDatabaseUrl,exports.devSchemaName=devSchemaName,exports.gatewayHeaders=gatewayHeaders,exports.handleFakeHost=handleFakeHost,exports.parseCspReport=parseCspReport,exports.shellFiles=shellFiles,exports.startDevServer=startDevServer,exports.storagePath=storagePath,exports.targetOf=targetOf;
|
package/dist/dev.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var Me=Object.defineProperty;var s=(e,t)=>Me(e,"name",{value:t,configurable:!0});import{signPluginContext as Fe}from"./chunk-WLTASJF6.js";import{readManifest as Ke,readPluginLocales as Je,buildPluginBackend as ze}from"./chunk-FCYAAOHC.js";import"./chunk-I5Z3ECC4.js";import{applyConfigDefaults as fe}from"./chunk-DEGQDJF6.js";import"./chunk-EIVA5BR6.js";import"./chunk-2NTAWYCG.js";import{resolvePluginParts as We}from"./chunk-IVJECPGG.js";import"./chunk-INCA3GFW.js";import"./chunk-56QOZQTY.js";import{PLUGIN_CONTEXT_HEADERS as L}from"./chunk-QKSPPMVN.js";import"./chunk-VMCGDDGP.js";import"./chunk-SNUCO2MK.js";import{randomUUID as qe,randomBytes as pe,createHash as Xe}from"crypto";import{existsSync as K,realpathSync as Ye,watch as Qe}from"fs";import{readdir as Ze,readFile as J,mkdir as ee,copyFile as ge,rm as te,writeFile as ne,stat as et}from"fs/promises";import{createServer as tt,request as nt}from"http";import{createRequire as rt}from"module";import{join as w,resolve as x,isAbsolute as at,sep as he,dirname as O,relative as me}from"path";import{spawn as ot}from"child_process";import{createServer as st}from"net";var re="x-tego-dev-user-id",ae="x-tego-dev-roles",C="dev-tenant",ve="00000000-0000-4000-8000-000000000001";function we(e){const t=s(r=>(Array.isArray(r)?r[0]:r)?.trim()??"","single"),n=t(e[re])||ve;let a=[];try{const r=JSON.parse(t(e[ae])||"[]");Array.isArray(r)&&(a=r.filter(i=>typeof i=="string"))}catch{}return{userId:n,roles:a}}s(we,"devCallerOf");function ye(e,t,n,a=Date.now()){const r={};for(const[o,l]of Object.entries(e)){const d=o.toLowerCase();l===void 0||d.startsWith("x-tego-")||d==="cookie"||d==="authorization"||d==="host"||d==="connection"||(r[d]=Array.isArray(l)?l.join(", "):l)}const i=JSON.stringify(t.roles),u=String(a);return r[L.userId]=t.userId,r[L.tenantId]=C,r[L.roles]=i,r[L.issuedAt]=u,r[L.signature]=Fe({userId:t.userId,tenantId:C,rolesHeader:i,avatarId:"",issuedAt:u},n),r}s(ye,"gatewayHeaders");var it="tego_dev_migrations";function Se(e){return`tego_dev_${e.toLowerCase().replace(/[^a-z0-9_]/g,"_")}`.slice(0,63)}s(Se,"devSchemaName");function be(e,t){const n=new URL(e);n.searchParams.delete("options");const a=n.search.replace(/^\?/,"");return n.search=`?${a?`${a}&`:""}options=${encodeURIComponent(`-c search_path=${t}`)}`,n.toString()}s(be,"devDatabaseUrl");async function ct(e){let t;try{t=await import("pg")}catch(r){throw new Error(`The local backend needs the "pg" driver; add it to the plugin's dependencies.`,{cause:r})}const n=t.default?.Client??t.Client;if(!n)throw new Error('The "pg" module has no Client constructor.');const a=new n({connectionString:e});try{await a.connect()}catch(r){throw new Error(`Cannot connect to DATABASE_URL (${oe(e)}): ${r instanceof Error?r.message:String(r)}`,{cause:r})}return a}s(ct,"connect");function oe(e){try{const t=new URL(e);return t.password&&(t.password="***"),t.toString()}catch{return"<unparseable DATABASE_URL>"}}s(oe,"redactUrl");var Ee=s(e=>`"${e.replace(/"/g,'""')}"`,"quote");async function ke(e){const t=await Ze(e.migrationsDir).then(i=>i.filter(u=>u.endsWith(".sql")).sort(),i=>{if(i.code==="ENOENT")return[];throw i}),n=await ct(e.databaseUrl),a=Ee(e.schema),r=`${a}.${Ee(it)}`;try{await n.query(`CREATE SCHEMA IF NOT EXISTS ${a}`),await n.query(`CREATE TABLE IF NOT EXISTS ${r} (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`);const i=new Set((await n.query(`SELECT name FROM ${r}`)).rows.map(o=>String(o.name))),u=[];for(const o of t){if(i.has(o))continue;const l=await J(w(e.migrationsDir,o),"utf8");await n.query("BEGIN");try{await n.query(`SET LOCAL search_path TO ${a}`),await n.query("SELECT set_config('app.tenant_id', $1, true)",[e.tenantId]),await n.query(l),await n.query(`INSERT INTO ${r} (name) VALUES ($1)`,[o]),await n.query("COMMIT")}catch(d){throw await n.query("ROLLBACK").catch(()=>{}),new Error(`Migration ${o} failed: ${d instanceof Error?d.message:String(d)}`,{cause:d})}u.push(o)}return u}finally{await n.end().catch(()=>{})}}s(ke,"applyDevMigrations");var lt=250,ut=5e3;function dt(){return new Promise((e,t)=>{const n=st();n.once("error",t),n.listen(0,"127.0.0.1",()=>{const a=n.address(),r=typeof a=="object"&&a?a.port:0;n.close(()=>e(r))})})}s(dt,"freePort");function Te(e){return!e||e.exitCode!==null||e.signalCode!==null?Promise.resolve():new Promise(t=>{const n=setTimeout(()=>{e.kill("SIGKILL"),t()},ut);e.once("exit",()=>{clearTimeout(n),t()}),e.kill("SIGTERM")})}s(Te,"stopChild");async function ft(e){const{part:t,log:n}=e;if(t.runtime==="python")throw new Error("The local preview runs TypeScript backends only; a python backend runs through the platform.");const a=await dt(),r=Se(e.pluginId),i=x(e.root,t.migrationsDirPath),u=w(e.outDir,t.entryPath),o={...process.env,NODE_ENV:"development",TEGO_PLUGIN_ID:e.pluginId,TEGO_PLUGIN_VERSION:e.version,TEGO_PLUGIN_INSTALL_ID:"dev-install",TEGO_TENANT_ID:C,TEGO_PLUGIN_PORT:String(a),TEGO_PLUGIN_HEALTH_PATH:t.healthPath,TEGO_PLUGIN_HOST_API_URL:e.hostApiUrl,TEGO_PLUGIN_HOST_API_TOKEN:e.hostApiToken,TEGO_PLUGIN_GATEWAY_KEY:e.gatewayKey,DATABASE_URL:be(e.databaseUrl,r)};let l=null,d=!1;async function p(){await te(e.outDir,{recursive:!0,force:!0}),await ze(t,{root:e.root,outDir:e.outDir});const h=await ke({databaseUrl:e.databaseUrl,schema:r,migrationsDir:i,tenantId:C});for(const E of h)n(`[backend] applied migration ${E}`)}s(p,"build");function y(){const h=ot(process.execPath,[u],{env:o,stdio:["ignore","pipe","pipe"]});l=h,h.stdout?.on("data",E=>process.stdout.write(E)),h.stderr?.on("data",E=>process.stderr.write(E)),h.on("exit",E=>{l===h&&!d&&(l=null,n(`[backend] exited (code ${E??"null"}); it starts again on the next save`))})}s(y,"start"),await p(),y(),n(`[backend] ${e.pluginId} on 127.0.0.1:${a}, schema ${r} in ${oe(e.databaseUrl)}`);let b=null,v=null,R=!1;const H=s(async()=>{if(v){R=!0;return}v=(async()=>{try{await p();const h=l;l=null,await Te(h),d||(y(),n("[backend] rebuilt and restarted"))}catch(h){n(`[backend] rebuild failed, the previous build keeps running: ${h instanceof Error?h.message:String(h)}`)}})(),await v,v=null,R&&!d&&(R=!1,await H())},"rebuild"),V=[];for(const h of[t.dir,"shared"]){const E=x(e.root,h);K(E)&&V.push(Qe(E,{recursive:!0},(P,I)=>{I&&/(^|[\\/])(node_modules|dist)([\\/]|$)/.test(String(I))||(b&&clearTimeout(b),b=setTimeout(()=>void H(),lt))}))}return{port:a,schema:r,async close(){d=!0,b&&clearTimeout(b);for(const h of V)h.close();await v,await Te(l)}}}s(ft,"startDevBackend");var se="/__tego/host",ie="/__tego/files",pt=1024*1024;async function ce(e,t=1/0){const n=[];let a=0;for await(const r of e){if(a+=r.length,a>t)throw Object.assign(new Error("body too large"),{status:413});n.push(r)}return Buffer.concat(n)}s(ce,"readBody");function g(e,t,n){if(n===void 0){e.writeHead(t),e.end();return}e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(n))}s(g,"send");function le(e,t){const n=x(e),a=x(n,t);if(!t||at(t)||!a.startsWith(n+he))throw Object.assign(new Error(`invalid storage key ${JSON.stringify(t)}`),{status:400});return a}s(le,"storagePath");var U=s(e=>`${e}.tego-meta.json`,"metaOf");async function B(e){const t=await et(e).catch(()=>null);if(!t?.isFile())return null;const n=await J(U(e),"utf8").then(a=>JSON.parse(a),()=>({}));return{size:t.size,contentType:n.contentType??null,versionTag:Xe("sha1").update(`${t.size}:${t.mtimeMs}`).digest("hex")}}s(B,"head");async function Ie(e,t,n){await ee(O(e),{recursive:!0}),await ne(e,t),await ne(U(e),JSON.stringify({contentType:n??null}))}s(Ie,"put");async function xe(e,t,n){const a=new URL(e.url??"/","http://preview.local");try{if(a.pathname.startsWith(ie)){const o=le(n.storageDir,a.searchParams.get("key")??""),l=await B(o);if(!l)return g(t,404);t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(await J(o));return}if(e.headers.authorization!==`Bearer ${n.token}`)return g(t,401,{error:"bad host API token"});const r=a.pathname.slice(se.length)||"/",i=s(async()=>JSON.parse((await ce(e,pt)).toString("utf8")||"{}"),"json"),u=s(o=>le(n.storageDir,String(o??"")),"keyFile");switch(r){case"/config":return g(t,200,{data:{values:n.config()}});case"/log":{const{level:o,message:l,fields:d}=await i();return n.log(`[backend ${String(o??"info")}] ${String(l??"")}${d?` ${JSON.stringify(d)}`:""}`),g(t,200,{data:{}})}case"/events":{const{name:o,payload:l}=await i();return n.log(`[event] ${String(o)} ${JSON.stringify(l??null)}`),g(t,200,{data:{}})}case"/notifications":return n.log(`[notification] ${JSON.stringify(await i())}`),g(t,200,{data:{id:qe()}});case"/directory/identity":{const{userId:o}=await i(),l=n.users().get(String(o));return g(t,200,{data:{active:!!l,roleNames:l??[],memberships:[],checkedAt:new Date().toISOString()}})}case"/directory/list":{const{kind:o,search:l,ids:d}=await i(),p=typeof l=="string"?l.toLowerCase():"",y=Array.isArray(d)?new Set(d.map(String)):void 0,b=o==="users"?[...n.users().keys()].filter(v=>(!y||y.has(v))&&v.toLowerCase().includes(p)).map(v=>({id:v,displayName:v,active:!0})):[];return g(t,200,{data:{items:b,hasMore:!1}})}case"/directory/match":{const{userId:o,principals:l}=await i(),d=Array.isArray(l)?l:[];return g(t,200,{data:{active:n.users().has(String(o)),matched:d.flatMap((p,y)=>p.kind==="user"&&p.id===o?[y]:[]),checkedAt:new Date().toISOString()}})}case"/directory/validate":{const{principals:o}=await i(),l=Array.isArray(o)?o:[],d=n.users();return g(t,200,{data:{invalid:l.flatMap((p,y)=>p.kind==="user"&&d.has(String(p.id))?[]:[y])}})}case"/storage/get":case"/storage/stream":{const o=u(a.searchParams.get("key"));if(e.method==="POST"&&r==="/storage/stream")return await Ie(o,await ce(e),e.headers["content-type"]),g(t,200,{data:{}});const l=await B(o);if(!l)return g(t,404);let d=await J(o);const p=Number(a.searchParams.get("bytes"));Number.isFinite(p)&&p>0&&(d=d.subarray(0,p)),t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(d);return}case"/storage/put":return await Ie(u(a.searchParams.get("key")),await ce(e),e.headers["content-type"]),g(t,200,{data:{}});case"/storage/head":{const o=await B(u((await i()).key));return o?g(t,200,{data:o}):g(t,404)}case"/storage/delete":{const o=u((await i()).key);return await te(o,{force:!0}),await te(U(o),{force:!0}),g(t,200,{data:{}})}case"/storage/copy":{const{sourceKey:o,destinationKey:l}=await i(),d=u(o),p=u(l);return await B(d)?(await ee(O(p),{recursive:!0}),await ge(d,p),await ge(U(d),U(p)).catch(()=>{}),g(t,200,{data:await B(p)})):g(t,404)}case"/storage/signed-url":{const{key:o}=await i();u(o);const l=`${ie}?key=${encodeURIComponent(String(o))}`;return g(t,200,{data:{url:`${n.origin()}${l}`,path:l}})}default:return n.log(`[host] ${r} is not available in local preview`),g(t,501,{error:{code:"not_available_in_preview",message:`${r} is not available in local preview`}})}}catch(r){const i=r.status??500;g(t,i,{error:{message:r instanceof Error?r.message:String(r)}})}}s(xe,"handleFakeHost");var _e="@tego/plugin-sdk",gt=JSON.stringify(_e),ht=JSON.stringify(`${_e}/frontend`);function Pe(e){return[{path:"index.html",contents:vt(e)},{path:"main.tsx",contents:St(e)},{path:"shell.css",contents:wt},{path:"plugin-surface.tsx",contents:yt}]}s(Pe,"shellFiles");var mt=s(e=>e.replace(/[&<>"]/g,t=>({"&":"&","<":"<",">":">",'"':"""})[t]),"escapeHtml");function vt({displayName:e}){return`<!doctype html>
|
|
1
|
+
var Me=Object.defineProperty;var s=(e,t)=>Me(e,"name",{value:t,configurable:!0});import{signPluginContext as Fe}from"./chunk-WLTASJF6.js";import{readManifest as Ke,readPluginLocales as Je,buildPluginBackend as We}from"./chunk-FCYAAOHC.js";import"./chunk-I5Z3ECC4.js";import{applyConfigDefaults as ge}from"./chunk-DEGQDJF6.js";import"./chunk-EIVA5BR6.js";import"./chunk-2NTAWYCG.js";import{resolvePluginParts as ze}from"./chunk-IVJECPGG.js";import"./chunk-INCA3GFW.js";import"./chunk-56QOZQTY.js";import{PLUGIN_CONTEXT_HEADERS as L}from"./chunk-QKSPPMVN.js";import"./chunk-VMCGDDGP.js";import"./chunk-SNUCO2MK.js";import{randomUUID as qe,randomBytes as fe,createHash as Xe}from"crypto";import{existsSync as K,realpathSync as Ye,watch as Qe}from"fs";import{readdir as Ze,readFile as J,mkdir as ee,copyFile as pe,rm as te,writeFile as ne,stat as et}from"fs/promises";import{createServer as tt,request as nt}from"http";import{createRequire as rt}from"module";import{join as w,resolve as I,isAbsolute as at,sep as he,dirname as C,relative as me}from"path";import{spawn as ot}from"child_process";import{createServer as st}from"net";var re="x-tego-dev-user-id",ae="x-tego-dev-roles",O="dev-tenant",ve="00000000-0000-4000-8000-000000000001";function we(e){const t=s(r=>(Array.isArray(r)?r[0]:r)?.trim()??"","single"),n=t(e[re])||ve;let a=[];try{const r=JSON.parse(t(e[ae])||"[]");Array.isArray(r)&&(a=r.filter(i=>typeof i=="string"))}catch{}return{userId:n,roles:a}}s(we,"devCallerOf");function ye(e,t,n,a=Date.now()){const r={};for(const[o,l]of Object.entries(e)){const u=o.toLowerCase();l===void 0||u.startsWith("x-tego-")||u==="cookie"||u==="authorization"||u==="host"||u==="connection"||(r[u]=Array.isArray(l)?l.join(", "):l)}const i=JSON.stringify(t.roles),d=String(a);return r[L.userId]=t.userId,r[L.tenantId]=O,r[L.roles]=i,r[L.issuedAt]=d,r[L.signature]=Fe({userId:t.userId,tenantId:O,rolesHeader:i,avatarId:"",issuedAt:d},n),r}s(ye,"gatewayHeaders");var it="tego_dev_migrations";function be(e){return`tego_dev_${e.toLowerCase().replace(/[^a-z0-9_]/g,"_")}`.slice(0,63)}s(be,"devSchemaName");function Se(e,t){const n=new URL(e);n.searchParams.delete("options");const a=n.search.replace(/^\?/,"");return n.search=`?${a?`${a}&`:""}options=${encodeURIComponent(`-c search_path=${t}`)}`,n.toString()}s(Se,"devDatabaseUrl");async function ct(e){let t;try{t=await import("pg")}catch(r){throw new Error(`The local backend needs the "pg" driver; add it to the plugin's dependencies.`,{cause:r})}const n=t.default?.Client??t.Client;if(!n)throw new Error('The "pg" module has no Client constructor.');const a=new n({connectionString:e});try{await a.connect()}catch(r){throw new Error(`Cannot connect to DATABASE_URL (${oe(e)}): ${r instanceof Error?r.message:String(r)}`,{cause:r})}return a}s(ct,"connect");function oe(e){try{const t=new URL(e);return t.password&&(t.password="***"),t.toString()}catch{return"<unparseable DATABASE_URL>"}}s(oe,"redactUrl");var Ee=s(e=>`"${e.replace(/"/g,'""')}"`,"quote");async function Te(e){const t=await Ze(e.migrationsDir).then(i=>i.filter(d=>d.endsWith(".sql")).sort(),i=>{if(i.code==="ENOENT")return[];throw i}),n=await ct(e.databaseUrl),a=Ee(e.schema),r=`${a}.${Ee(it)}`;try{await n.query(`CREATE SCHEMA IF NOT EXISTS ${a}`),await n.query(`CREATE TABLE IF NOT EXISTS ${r} (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`);const i=new Set((await n.query(`SELECT name FROM ${r}`)).rows.map(o=>String(o.name))),d=[];for(const o of t){if(i.has(o))continue;const l=await J(w(e.migrationsDir,o),"utf8");await n.query("BEGIN");try{await n.query(`SET LOCAL search_path TO ${a}`),await n.query("SELECT set_config('app.tenant_id', $1, true)",[e.tenantId]),await n.query(l),await n.query(`INSERT INTO ${r} (name) VALUES ($1)`,[o]),await n.query("COMMIT")}catch(u){throw await n.query("ROLLBACK").catch(()=>{}),new Error(`Migration ${o} failed: ${u instanceof Error?u.message:String(u)}`,{cause:u})}d.push(o)}return d}finally{await n.end().catch(()=>{})}}s(Te,"applyDevMigrations");var lt=250,dt=5e3;function ut(){return new Promise((e,t)=>{const n=st();n.once("error",t),n.listen(0,"127.0.0.1",()=>{const a=n.address(),r=typeof a=="object"&&a?a.port:0;n.close(()=>e(r))})})}s(ut,"freePort");function ke(e){return!e||e.exitCode!==null||e.signalCode!==null?Promise.resolve():new Promise(t=>{const n=setTimeout(()=>{e.kill("SIGKILL"),t()},dt);e.once("exit",()=>{clearTimeout(n),t()}),e.kill("SIGTERM")})}s(ke,"stopChild");async function gt(e){const{part:t,log:n}=e;if(t.runtime==="python")throw new Error("The local preview runs TypeScript backends only; a python backend runs through the platform.");const a=await ut(),r=be(e.pluginId),i=I(e.root,t.migrationsDirPath),d=w(e.outDir,t.entryPath),o={...process.env,NODE_ENV:"development",TEGO_PLUGIN_ID:e.pluginId,TEGO_PLUGIN_VERSION:e.version,TEGO_PLUGIN_INSTALL_ID:"dev-install",TEGO_TENANT_ID:O,TEGO_PLUGIN_PORT:String(a),TEGO_PLUGIN_HEALTH_PATH:t.healthPath,TEGO_PLUGIN_HOST_API_URL:e.hostApiUrl,TEGO_PLUGIN_HOST_API_TOKEN:e.hostApiToken,TEGO_PLUGIN_GATEWAY_KEY:e.gatewayKey,DATABASE_URL:Se(e.databaseUrl,r)};let l=null,u=!1;async function f(){await te(e.outDir,{recursive:!0,force:!0}),await We(t,{root:e.root,outDir:e.outDir});const h=await Te({databaseUrl:e.databaseUrl,schema:r,migrationsDir:i,tenantId:O});for(const E of h)n(`[backend] applied migration ${E}`)}s(f,"build");function y(){const h=ot(process.execPath,[d],{env:o,stdio:["ignore","pipe","pipe"]});l=h,h.stdout?.on("data",E=>process.stdout.write(E)),h.stderr?.on("data",E=>process.stderr.write(E)),h.on("exit",E=>{l===h&&!u&&(l=null,n(`[backend] exited (code ${E??"null"}); it starts again on the next save`))})}s(y,"start"),await f(),y(),n(`[backend] ${e.pluginId} on 127.0.0.1:${a}, schema ${r} in ${oe(e.databaseUrl)}`);let S=null,v=null,R=!1;const H=s(async()=>{if(v){R=!0;return}v=(async()=>{try{await f();const h=l;l=null,await ke(h),u||(y(),n("[backend] rebuilt and restarted"))}catch(h){n(`[backend] rebuild failed, the previous build keeps running: ${h instanceof Error?h.message:String(h)}`)}})(),await v,v=null,R&&!u&&(R=!1,await H())},"rebuild"),V=[];for(const h of[t.dir,"shared"]){const E=I(e.root,h);K(E)&&V.push(Qe(E,{recursive:!0},(_,x)=>{x&&/(^|[\\/])(node_modules|dist)([\\/]|$)/.test(String(x))||(S&&clearTimeout(S),S=setTimeout(()=>void H(),lt))}))}return{port:a,schema:r,async close(){u=!0,S&&clearTimeout(S);for(const h of V)h.close();await v,await ke(l)}}}s(gt,"startDevBackend");var se="/__tego/host",ie="/__tego/files",ft=1024*1024;async function ce(e,t=1/0){const n=[];let a=0;for await(const r of e){if(a+=r.length,a>t)throw Object.assign(new Error("body too large"),{status:413});n.push(r)}return Buffer.concat(n)}s(ce,"readBody");function p(e,t,n){if(n===void 0){e.writeHead(t),e.end();return}e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(n))}s(p,"send");function le(e,t){const n=I(e),a=I(n,t);if(!t||at(t)||!a.startsWith(n+he))throw Object.assign(new Error(`invalid storage key ${JSON.stringify(t)}`),{status:400});return a}s(le,"storagePath");var U=s(e=>`${e}.tego-meta.json`,"metaOf");async function B(e){const t=await et(e).catch(()=>null);if(!t?.isFile())return null;const n=await J(U(e),"utf8").then(a=>JSON.parse(a),()=>({}));return{size:t.size,contentType:n.contentType??null,versionTag:Xe("sha1").update(`${t.size}:${t.mtimeMs}`).digest("hex")}}s(B,"head");async function xe(e,t,n){await ee(C(e),{recursive:!0}),await ne(e,t),await ne(U(e),JSON.stringify({contentType:n??null}))}s(xe,"put");async function Ie(e,t,n){const a=new URL(e.url??"/","http://preview.local");try{if(a.pathname.startsWith(ie)){const o=le(n.storageDir,a.searchParams.get("key")??""),l=await B(o);if(!l)return p(t,404);t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(await J(o));return}if(e.headers.authorization!==`Bearer ${n.token}`)return p(t,401,{error:"bad host API token"});const r=a.pathname.slice(se.length)||"/",i=s(async()=>JSON.parse((await ce(e,ft)).toString("utf8")||"{}"),"json"),d=s(o=>le(n.storageDir,String(o??"")),"keyFile");switch(r){case"/config":return p(t,200,{data:{values:n.config()}});case"/log":{const{level:o,message:l,fields:u}=await i();return n.log(`[backend ${String(o??"info")}] ${String(l??"")}${u?` ${JSON.stringify(u)}`:""}`),p(t,200,{data:{}})}case"/events":{const{name:o,payload:l}=await i();return n.log(`[event] ${String(o)} ${JSON.stringify(l??null)}`),p(t,200,{data:{}})}case"/notifications":return n.log(`[notification] ${JSON.stringify(await i())}`),p(t,200,{data:{id:qe()}});case"/directory/identity":{const{userId:o}=await i(),l=n.users().get(String(o));return p(t,200,{data:{active:!!l,roleNames:l??[],memberships:[],checkedAt:new Date().toISOString()}})}case"/directory/list":{const{kind:o,search:l,ids:u}=await i(),f=typeof l=="string"?l.toLowerCase():"",y=Array.isArray(u)?new Set(u.map(String)):void 0,S=o==="users"?[...n.users().keys()].filter(v=>(!y||y.has(v))&&v.toLowerCase().includes(f)).map(v=>({id:v,displayName:v,active:!0})):[];return p(t,200,{data:{items:S,hasMore:!1}})}case"/directory/match":{const{userId:o,principals:l}=await i(),u=Array.isArray(l)?l:[];return p(t,200,{data:{active:n.users().has(String(o)),matched:u.flatMap((f,y)=>f.kind==="user"&&f.id===o?[y]:[]),checkedAt:new Date().toISOString()}})}case"/directory/validate":{const{principals:o}=await i(),l=Array.isArray(o)?o:[],u=n.users();return p(t,200,{data:{invalid:l.flatMap((f,y)=>f.kind==="user"&&u.has(String(f.id))?[]:[y])}})}case"/storage/get":case"/storage/stream":{const o=d(a.searchParams.get("key"));if(e.method==="POST"&&r==="/storage/stream")return await xe(o,await ce(e),e.headers["content-type"]),p(t,200,{data:{}});const l=await B(o);if(!l)return p(t,404);let u=await J(o);const f=Number(a.searchParams.get("bytes"));Number.isFinite(f)&&f>0&&(u=u.subarray(0,f)),t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(u);return}case"/storage/put":return await xe(d(a.searchParams.get("key")),await ce(e),e.headers["content-type"]),p(t,200,{data:{}});case"/storage/head":{const o=await B(d((await i()).key));return o?p(t,200,{data:o}):p(t,404)}case"/storage/delete":{const o=d((await i()).key);return await te(o,{force:!0}),await te(U(o),{force:!0}),p(t,200,{data:{}})}case"/storage/copy":{const{sourceKey:o,destinationKey:l}=await i(),u=d(o),f=d(l);return await B(u)?(await ee(C(f),{recursive:!0}),await pe(u,f),await pe(U(u),U(f)).catch(()=>{}),p(t,200,{data:await B(f)})):p(t,404)}case"/storage/signed-url":{const{key:o}=await i();d(o);const l=`${ie}?key=${encodeURIComponent(String(o))}`;return p(t,200,{data:{url:`${n.origin()}${l}`,path:l}})}default:return n.log(`[host] ${r} is not available in local preview`),p(t,501,{error:{code:"not_available_in_preview",message:`${r} is not available in local preview`}})}}catch(r){const i=r.status??500;p(t,i,{error:{message:r instanceof Error?r.message:String(r)}})}}s(Ie,"handleFakeHost");var Pe="@tego/plugin-sdk",pt=JSON.stringify(Pe),ht=JSON.stringify(`${Pe}/frontend`);function _e(e){return[{path:"index.html",contents:vt(e)},{path:"main.tsx",contents:bt(e)},{path:"shell.css",contents:wt},{path:"plugin-surface.tsx",contents:yt}]}s(_e,"shellFiles");var mt=s(e=>e.replace(/[&<>"]/g,t=>({"&":"&","<":"<",">":">",'"':"""})[t]),"escapeHtml");function vt({displayName:e}){return`<!doctype html>
|
|
2
2
|
<html lang="en" class="h-full">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
@@ -46,21 +46,26 @@ export const ChatPanel = Unavailable;
|
|
|
46
46
|
export function useTopic() {
|
|
47
47
|
return { topic: undefined, topics: [], loading: false };
|
|
48
48
|
}
|
|
49
|
-
`;function
|
|
49
|
+
`;function bt(e){const t=JSON.stringify({pluginId:e.pluginId,displayName:e.displayName,nav:e.nav.map(a=>({key:a.key,labelKey:a.labelKey,path:a.path??"/"})),config:e.config,locales:e.locales,defaultTarget:e.defaultTarget??"web",userHeader:re,rolesHeader:ae,defaultUser:ve},null,2),n=e.entry?`import plugin from ${JSON.stringify(e.entry)};`:"const plugin = undefined;";return`// Generated by \`tego-plugin dev\` on every start; edits here are overwritten.
|
|
50
50
|
import '@tego/ui/styles/tokens.css';
|
|
51
51
|
import './shell.css';
|
|
52
52
|
|
|
53
|
-
import { pluginI18nNamespace } from ${
|
|
53
|
+
import { pluginI18nNamespace } from ${pt};
|
|
54
54
|
import type { PluginFrontendModule, PluginHostContext, PluginRoute } from ${ht};
|
|
55
55
|
import {
|
|
56
56
|
Button,
|
|
57
57
|
Input,
|
|
58
|
+
Label,
|
|
59
|
+
Popover,
|
|
60
|
+
PopoverContent,
|
|
61
|
+
PopoverTrigger,
|
|
58
62
|
Select,
|
|
59
63
|
SelectContent,
|
|
60
64
|
SelectItem,
|
|
61
65
|
SelectTrigger,
|
|
62
66
|
SelectValue,
|
|
63
67
|
Toaster,
|
|
68
|
+
Switch,
|
|
64
69
|
TooltipProvider,
|
|
65
70
|
} from '@tego/ui';
|
|
66
71
|
import i18next from 'i18next';
|
|
@@ -247,55 +252,39 @@ function DevBar() {
|
|
|
247
252
|
store('roles', roles);
|
|
248
253
|
location.reload();
|
|
249
254
|
};
|
|
255
|
+
// One quiet row: what the preview imitates, the plugin's pages, CSP problems, and the
|
|
256
|
+
// rarely changed settings (language, theme, who is signed in) behind one button.
|
|
250
257
|
return (
|
|
251
|
-
<div className='flex shrink-0 items-center gap-2 border-b border-border bg-card px-
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
|
|
258
|
+
<div className='flex h-10 shrink-0 items-center gap-2 border-b border-border bg-card px-2 text-sm'>
|
|
259
|
+
<div className='flex shrink-0 items-center rounded-control bg-sunken p-0.5' role='group' aria-label='Shell'>
|
|
260
|
+
{(['web', 'desktop'] as const).map(target => (
|
|
261
|
+
<button
|
|
262
|
+
key={target}
|
|
263
|
+
type='button'
|
|
264
|
+
aria-pressed={TARGET === target}
|
|
265
|
+
onClick={() => TARGET !== target && switchTarget(target)}
|
|
266
|
+
className={
|
|
267
|
+
'h-6 rounded-control px-2 text-xs font-medium transition-colors ' +
|
|
268
|
+
(TARGET === target ? 'bg-card text-foreground shadow-card' : 'text-muted-foreground hover:text-foreground')
|
|
269
|
+
}
|
|
270
|
+
>
|
|
271
|
+
{target === 'web' ? 'Web' : 'Desktop'}
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
<nav className='flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto' aria-label='Pages'>
|
|
255
276
|
{DEV.nav.map(entry => (
|
|
256
277
|
<Button
|
|
257
278
|
key={entry.key}
|
|
258
279
|
size='sm'
|
|
259
280
|
variant={current.path === entry.path ? 'secondary' : 'ghost'}
|
|
281
|
+
className='h-7 shrink-0'
|
|
260
282
|
onClick={() => navigate(entry.path)}
|
|
261
283
|
>
|
|
262
284
|
{i18next.t(entry.labelKey, { ns: NAMESPACE, lng: lang })}
|
|
263
285
|
</Button>
|
|
264
286
|
))}
|
|
265
287
|
</nav>
|
|
266
|
-
<Select
|
|
267
|
-
value={lang}
|
|
268
|
-
onValueChange={async next => {
|
|
269
|
-
await loadLocale(next);
|
|
270
|
-
await i18next.changeLanguage(next);
|
|
271
|
-
store('lang', next);
|
|
272
|
-
setLang(next);
|
|
273
|
-
}}
|
|
274
|
-
>
|
|
275
|
-
<SelectTrigger className='h-8 w-28' aria-label='Language'>
|
|
276
|
-
<SelectValue />
|
|
277
|
-
</SelectTrigger>
|
|
278
|
-
<SelectContent>
|
|
279
|
-
{DEV.locales.map(tag => (
|
|
280
|
-
<SelectItem key={tag} value={tag}>
|
|
281
|
-
{tag}
|
|
282
|
-
</SelectItem>
|
|
283
|
-
))}
|
|
284
|
-
</SelectContent>
|
|
285
|
-
</Select>
|
|
286
|
-
<div className='flex shrink-0 items-center rounded-control border border-border p-0.5' role='group' aria-label='Shell'>
|
|
287
|
-
{(['web', 'desktop'] as const).map(target => (
|
|
288
|
-
<Button
|
|
289
|
-
key={target}
|
|
290
|
-
size='sm'
|
|
291
|
-
variant={TARGET === target ? 'secondary' : 'ghost'}
|
|
292
|
-
aria-pressed={TARGET === target}
|
|
293
|
-
onClick={() => TARGET !== target && switchTarget(target)}
|
|
294
|
-
>
|
|
295
|
-
{target === 'web' ? 'Web' : 'Desktop'}
|
|
296
|
-
</Button>
|
|
297
|
-
))}
|
|
298
|
-
</div>
|
|
299
288
|
{blocked > 0 && (
|
|
300
289
|
<span
|
|
301
290
|
className='shrink-0 rounded-control bg-destructive-soft px-1.5 py-0.5 text-xs text-destructive'
|
|
@@ -304,36 +293,70 @@ function DevBar() {
|
|
|
304
293
|
CSP blocked {blocked}
|
|
305
294
|
</span>
|
|
306
295
|
)}
|
|
307
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
296
|
+
<Popover>
|
|
297
|
+
<PopoverTrigger asChild>
|
|
298
|
+
<Button size='sm' variant='ghost' className='h-7 shrink-0 font-mono text-xs text-muted-foreground'>
|
|
299
|
+
{lang} \xB7 {user.userId.slice(0, 8)}
|
|
300
|
+
</Button>
|
|
301
|
+
</PopoverTrigger>
|
|
302
|
+
<PopoverContent align='end' className='w-72 space-y-3'>
|
|
303
|
+
<div className='space-y-1.5'>
|
|
304
|
+
<Label>Language</Label>
|
|
305
|
+
<Select
|
|
306
|
+
value={lang}
|
|
307
|
+
onValueChange={async next => {
|
|
308
|
+
await loadLocale(next);
|
|
309
|
+
await i18next.changeLanguage(next);
|
|
310
|
+
store('lang', next);
|
|
311
|
+
setLang(next);
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<SelectTrigger className='h-8' aria-label='Language'>
|
|
315
|
+
<SelectValue />
|
|
316
|
+
</SelectTrigger>
|
|
317
|
+
<SelectContent>
|
|
318
|
+
{DEV.locales.map(tag => (
|
|
319
|
+
<SelectItem key={tag} value={tag}>
|
|
320
|
+
{tag}
|
|
321
|
+
</SelectItem>
|
|
322
|
+
))}
|
|
323
|
+
</SelectContent>
|
|
324
|
+
</Select>
|
|
325
|
+
</div>
|
|
326
|
+
<div className='flex items-center justify-between'>
|
|
327
|
+
<Label htmlFor='tego-dev-dark'>Dark theme</Label>
|
|
328
|
+
<Switch
|
|
329
|
+
id='tego-dev-dark'
|
|
330
|
+
checked={dark}
|
|
331
|
+
onCheckedChange={next => {
|
|
332
|
+
document.documentElement.classList.toggle('dark', next);
|
|
333
|
+
store('theme', next ? 'dark' : 'light');
|
|
334
|
+
setDark(next);
|
|
335
|
+
}}
|
|
336
|
+
/>
|
|
337
|
+
</div>
|
|
338
|
+
<form
|
|
339
|
+
className='space-y-1.5 border-t border-border pt-3'
|
|
340
|
+
onSubmit={event => {
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
switchUser();
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
<Label>Signed in as</Label>
|
|
346
|
+
<Input className='h-8 font-mono text-xs' aria-label='User id' value={userId} onChange={event => setUserId(event.target.value)} />
|
|
347
|
+
<Input
|
|
348
|
+
className='h-8'
|
|
349
|
+
aria-label='Roles, comma-separated'
|
|
350
|
+
placeholder='Roles, comma-separated'
|
|
351
|
+
value={roles}
|
|
352
|
+
onChange={event => setRoles(event.target.value)}
|
|
353
|
+
/>
|
|
354
|
+
<Button size='sm' variant='outline' type='submit' className='w-full'>
|
|
355
|
+
Sign in
|
|
356
|
+
</Button>
|
|
357
|
+
</form>
|
|
358
|
+
</PopoverContent>
|
|
359
|
+
</Popover>
|
|
337
360
|
</div>
|
|
338
361
|
);
|
|
339
362
|
}
|
|
@@ -355,4 +378,4 @@ if (frontend) {
|
|
|
355
378
|
DEV.pluginId +
|
|
356
379
|
'/</p>';
|
|
357
380
|
}
|
|
358
|
-
`}s(
|
|
381
|
+
`}s(bt,"mainTsx");var Ae="/__tego/csp-report",$e="TEGO_DEV_CSP ",St="tego-dev-target",Et=100*1024*1024,Ne=["default-src 'self'","object-src 'none'","base-uri 'self'"],De={web:[...Ne,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline'","img-src 'self' data: blob: https:","font-src 'self' data:","connect-src 'self' wss: ws:","frame-src 'self'","worker-src 'self' blob:"].join("; "),desktop:[...Ne,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline' https:","img-src 'self' data: blob: http: https:","font-src 'self' data: blob: https:","media-src 'self' data: blob:","worker-src 'self' blob:","connect-src 'self' http: https: ws: wss:","frame-src 'self'"].join("; ")};function de(e){return e==="web"||e==="desktop"}s(de,"isPreviewTarget");function Re(e,t){const n=new URL(e.url??"/","http://preview.local").searchParams.get("target");if(de(n))return n;const a=/(?:^|;\s*)tego-dev-target=(web|desktop)(?:;|$)/.exec(String(e.headers.cookie??""))?.[1];return de(a)?a:t}s(Re,"targetOf");function Le(e){try{const t=JSON.parse(e)["csp-report"];if(!t)return;const n=String(t["effective-directive"]??t["violated-directive"]??""),a=String(t["blocked-uri"]??"");if(!n)return;const r=typeof t["source-file"]=="string"?t["source-file"]:void 0,i=typeof t["line-number"]=="number"?t["line-number"]:void 0;return{directive:n,blocked:a,...r?{source:r}:{},...i?{line:i}:{}}}catch{return}}s(Le,"parseCspReport");var Ce=5280,Tt=20,Oe="TEGO_DEV_READY ",Ue=".tego-dev",kt="/api/v1/tego-os/plugin-api/";function xt(e){const{displayName:t}=e;return typeof t=="string"?t:t.en??t["zh-CN"]??Object.values(t)[0]??e.id}s(xt,"displayNameOf");function ue(e,t){for(let n=I(e);;n=C(n)){const a=w(n,"node_modules",...t.split("/"));if(K(w(a,"package.json")))return Ye(a);if(C(n)===n)return}}s(ue,"packageDir");function D(e,t,n){e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(n))}s(D,"sendJson");function It(e,t,n){return new Promise((a,r)=>{const i=s(o=>{e.off("listening",d),r(o)},"onError"),d=s(()=>{e.off("error",i);const o=e.address();a(typeof o=="object"&&o?o.port:t)},"onListening");e.once("error",i),e.once("listening",d),e.listen(t,n)})}s(It,"listen");async function Pt(e,t){const n=rt(w(e,"vite.config.ts"));let a,r;try{a=n("@tego/ui/tailwind-preset"),r=C(n.resolve("@tego/ui"))}catch(d){throw new Error(`The preview cannot load @tego/ui's Tailwind preset from ${e}: ${d instanceof Error?d.message:String(d)}. Install the plugin's dependencies.`,{cause:d})}const i=(await import("tailwindcss")).default;return i({presets:[a],content:[w(r,"**/*.{js,cjs}"),w(t,"*.tsx")]})}s(Pt,"shellTailwind");function _t(e,t,n){return{postcssPlugin:"tego-dev-stylesheets",async Once(a,r){const i=a.source?.input.file,d=i&&I(i)===e?[t]:n;if(d.length===0)return;const o=await r.postcss(d).process(a,{from:i});r.result.messages.push(...o.messages)}}}s(_t,"perStylesheet");function At(e){return Array.isArray(e)?e.flat(1/0).map(t=>{if(!t||typeof t!="object")return t;const{buildStart:n}=t;return typeof n!="function"?t:{...t,buildStart(...a){const r=new Proxy(this,{get(i,d){if(d==="emitFile")return()=>"";const o=Reflect.get(i,d);return typeof o=="function"?o.bind(i):o}});return n.apply(r,a)}}}):e}s(At,"servePlugins");async function $t(e){const t=I(e.root),n=e.log??(c=>console.log(c)),a=await Ke(t),r=ze(a),i=w(t,Ue),d=w(i,"shell"),o=fe(32).toString("hex"),l=fe(24).toString("hex");let u="";const f=new Map;let y,S={};const v=r.frontend?I(t,r.frontend.dir):t,{loadConfigFromFile:R,createServer:H,mergeConfig:V}=await import("vite");if(r.frontend){const c=w(v,"vite.config.ts");K(c)&&(S=(await R({command:"serve",mode:"development"},c,t))?.config??{});const g=S.build?.rollupOptions?.input,b=I(t,typeof g=="string"?g:`${r.frontend.dir}/src/index.tsx`);if(!K(b))throw new Error(`The frontend entry ${me(t,b)} does not exist.`);y=me(d,b).split(he).join("/"),y.startsWith(".")||(y=`./${y}`)}const h=s(()=>r.frontend?Je(t,a,r.frontend.i18nDirPath):Promise.resolve(new Map),"localeFiles"),E=[...(await h()).keys()].map(c=>c.replace(/\.json$/,""));await ee(d,{recursive:!0});for(const c of _e({pluginId:a.id,displayName:xt(a),nav:r.frontend?.nav??[],config:r.frontend?.config?ge(r.frontend.config.schema,{}):{},entry:y,locales:E,defaultTarget:e.target??"web"}))await ne(w(d,c.path),c.contents);let _,x;const Be=s(()=>r.backend?.config?ge(r.backend.config.schema,{}):{},"backendConfig"),j=tt((c,g)=>void He(c,g));async function He(c,g){const b=(c.url??"/").split("?")[0],P=Re(c,e.target??"web");try{if(b==="/favicon.ico"){g.writeHead(204),g.end();return}if(b===Ae){const m=await new Promise(F=>{let N="";c.setEncoding("utf8"),c.on("data",A=>N+=A),c.on("end",()=>F(N))}),k=Le(m);k&&(n(`[csp:${P}] blocked ${k.blocked||"(inline)"} by ${k.directive}${k.source?` in ${k.source}${k.line?`:${k.line}`:""}`:""}`),n(`${$e}${JSON.stringify({target:P,...k})}`)),g.writeHead(204),g.end();return}if(b.startsWith(se)||b.startsWith(ie)){await Ie(c,g,{token:l,storageDir:w(i,"storage"),config:Be,origin:s(()=>u,"origin"),users:s(()=>f,"users"),log:n});return}const T=/^\/i18n\/([A-Za-z-]+)\.json$/.exec(b);if(T){const m=(await h()).get(`${T[1]}.json`);if(m===void 0)return D(g,404,{error:`no ${T[1]}.json`});g.writeHead(200,{"content-type":"application/json"}),g.end(m);return}const $=`${kt}${a.id}`;if(b===$||b.startsWith(`${$}/`)){Ve(c,g,(c.url??"/").slice($.length)||"/",P);return}if(String(c.headers.accept??"").includes("text/html")){g.setHeader("Content-Security-Policy",`${De[P]}; report-uri ${Ae}`);const m=new URL(c.url??"/","http://preview.local").searchParams.get("target");de(m)&&g.setHeader("Set-Cookie",`${St}=${m}; Path=/; SameSite=Lax`)}z.middlewares(c,g,()=>D(g,404,{error:`${b} not found`}))}catch(T){D(g,500,{error:T instanceof Error?T.message:String(T)})}}s(He,"route");function Ve(c,g,b,P){if(!_){D(g,503,{error:x??"The plugin has no backend part."});return}const T=we(c.headers);f.set(T.userId,T.roles);const $=nt({host:"127.0.0.1",port:_.port,method:c.method,path:b,headers:{...ye(c.headers,T,o),...P==="desktop"?{"accept-encoding":"identity"}:{}}},m=>{if(P!=="desktop"){g.writeHead(m.statusCode??502,m.headers),m.pipe(g);return}const k=[];let F=0,N=!1;m.on("data",A=>{if(!N){if(F+=A.length,F>Et){N=!0,m.destroy(),n(`[desktop] ${c.method} ${b}: response over 100 MB refused (413)`),D(g,413,{error:"Response over 100 MB: the desktop app refuses it. Serve large files through storage paths."});return}k.push(A)}}),m.on("end",()=>{if(N)return;const A=Buffer.concat(k),Z={...m.headers};delete Z["transfer-encoding"],Z["content-length"]=String(A.length),g.writeHead(m.statusCode??502,Z),g.end(A)})});$.on("error",m=>{g.headersSent?g.destroy():D(g,502,{error:`The backend is not answering (${m.message}); see the terminal.`})}),c.pipe($)}s(Ve,"proxy");const je=w(d,"shell.css"),Ge=(()=>{const c=S.css?.postcss;return c&&typeof c=="object"?c.plugins??[]:[]})(),G=(()=>{const c=ue(v,"react-i18next");return(c&&ue(c,"i18next"))??ue(v,"i18next")})(),{css:Nt,mode:Dt,build:Rt,...W}=S;W.plugins=At(W.plugins);const z=await H(V(W,{configFile:!1,root:d,mode:"development",appType:"spa",clearScreen:!1,logLevel:"info",esbuild:{jsx:"automatic",jsxDev:!1},css:{postcss:{plugins:[_t(je,await Pt(v,d),Ge)]}},resolve:{alias:[{find:"@tego/chat/plugin-surface",replacement:w(d,"plugin-surface.tsx")},...G?[{find:/^i18next$/,replacement:G}]:[]]},server:{middlewareMode:!0,hmr:{server:j},fs:{allow:[t,d,...G?[G]:[]]}},optimizeDeps:{entries:[w(d,"index.html")]}})),M=e.host??"127.0.0.1";let q;const X=e.port??Ce;for(let c=0;q===void 0;c++)try{q=await It(j,X===0?0:X+c,M)}catch(g){if(g.code!=="EADDRINUSE"||X===0||c>=Tt)throw await z.close(),g}const Y=M==="0.0.0.0"||M==="::"?"127.0.0.1":M;if(u=`http://${Y.includes(":")?`[${Y}]`:Y}:${q}`,!r.backend)x="The plugin has no backend part.";else if(e.backend===!1)x="The backend is off (--no-backend).";else if(!e.databaseUrl)x="The backend needs a PostgreSQL: start the preview with DATABASE_URL=postgres://\u2026 (each plugin gets its own schema).";else try{_=await gt({root:t,pluginId:a.id,version:a.version,part:r.backend,databaseUrl:e.databaseUrl,outDir:w(i,"backend"),hostApiUrl:`${u}${se}`,hostApiToken:l,gatewayKey:o,log:n})}catch(c){x=`The backend did not start: ${c instanceof Error?c.message:String(c)}`}x&&r.backend&&n(`[backend] ${x}`);const Q=`${u}/p/${a.id}/`;return n(`${a.id} ${a.version}: local preview at ${Q}`),e.databaseUrl&&_&&n(` database ${oe(e.databaseUrl)}, schema ${_.schema}`),n(`${Oe}${JSON.stringify({url:Q})}`),{url:Q,async close(){await _?.close(),await z.close(),j.closeAllConnections(),await new Promise(c=>j.close(()=>c()))}}}s($t,"startDevServer");export{$e as CSP_REPORT_PREFIX,Ce as DEFAULT_DEV_PORT,Ue as DEV_DIR,Oe as DEV_READY_PREFIX,ae as DEV_ROLES_HEADER,O as DEV_TENANT_ID,re as DEV_USER_HEADER,De as PREVIEW_CSP,Te as applyDevMigrations,we as devCallerOf,Se as devDatabaseUrl,be as devSchemaName,ye as gatewayHeaders,Ie as handleFakeHost,Le as parseCspReport,_e as shellFiles,$t as startDevServer,le as storagePath,Re as targetOf};
|