gemi 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/bin/gemi.js +500 -16
  2. package/dist/bin/gemi.js.map +10 -5
  3. package/dist/{chunk-n412aa9s.js → chunk-khf9xda6.js} +1 -1
  4. package/dist/{chunk-gfma8e03.js → chunk-x8beq9c4.js} +1 -1
  5. package/dist/chunk-y3zz410b.js +6 -0
  6. package/dist/{chunk-txhcx69q.js.map → chunk-y3zz410b.js.map} +2 -2
  7. package/dist/config/index.d.ts +2 -0
  8. package/dist/config/index.d.ts.map +1 -1
  9. package/dist/config/index.js +2 -2
  10. package/dist/config/index.js.map +3 -3
  11. package/dist/server/index.js +1 -1
  12. package/package.json +3 -2
  13. package/skills/gemi-react-best-practices/SKILL.md +231 -0
  14. package/skills/gemi-react-best-practices/rules/_sections.md +56 -0
  15. package/skills/gemi-react-best-practices/rules/_template.md +28 -0
  16. package/skills/gemi-react-best-practices/rules/bundle-deep-imports.md +48 -0
  17. package/skills/gemi-react-best-practices/rules/bundle-mount-gate-heavy-panels.md +63 -0
  18. package/skills/gemi-react-best-practices/rules/client-form-vs-mutation-hooks.md +57 -0
  19. package/skills/gemi-react-best-practices/rules/client-loading-error-exports.md +54 -0
  20. package/skills/gemi-react-best-practices/rules/client-no-effect-data-flow.md +68 -0
  21. package/skills/gemi-react-best-practices/rules/client-typed-links.md +51 -0
  22. package/skills/gemi-react-best-practices/rules/controller-authorize-every-tenant-read.md +65 -0
  23. package/skills/gemi-react-best-practices/rules/controller-parse-request-at-the-boundary.md +54 -0
  24. package/skills/gemi-react-best-practices/rules/controller-redirect-facade-throws.md +68 -0
  25. package/skills/gemi-react-best-practices/rules/controller-request-schema.md +61 -0
  26. package/skills/gemi-react-best-practices/rules/controller-throw-framework-errors.md +57 -0
  27. package/skills/gemi-react-best-practices/rules/i18n-define-dictionary-inline.md +58 -0
  28. package/skills/gemi-react-best-practices/rules/orm-analytics-connection.md +53 -0
  29. package/skills/gemi-react-best-practices/rules/orm-include-not-n-plus-one.md +56 -0
  30. package/skills/gemi-react-best-practices/rules/orm-paginate-helper.md +69 -0
  31. package/skills/gemi-react-best-practices/rules/orm-plain-rows-by-default.md +55 -0
  32. package/skills/gemi-react-best-practices/rules/orm-select-narrow.md +58 -0
  33. package/skills/gemi-react-best-practices/rules/orm-transaction-no-io.md +54 -0
  34. package/skills/gemi-react-best-practices/rules/orm-transaction-sequential.md +64 -0
  35. package/skills/gemi-react-best-practices/rules/payload-dont-overprefetch.md +54 -0
  36. package/skills/gemi-react-best-practices/rules/payload-instant-vs-prefetch.md +58 -0
  37. package/skills/gemi-react-best-practices/rules/payload-minimal-view-props.md +51 -0
  38. package/skills/gemi-react-best-practices/rules/payload-parallel-controller-work.md +56 -0
  39. package/skills/gemi-react-best-practices/rules/payload-prefetch-late-queries.md +58 -0
  40. package/skills/gemi-react-best-practices/rules/payload-prefetch-mirrors-usequery.md +52 -0
  41. package/skills/gemi-react-best-practices/rules/query-debounce-search-variant.md +52 -0
  42. package/skills/gemi-react-best-practices/rules/query-keep-previous-data.md +40 -0
  43. package/skills/gemi-react-best-practices/rules/query-lazy-vs-mount-gate.md +54 -0
  44. package/skills/gemi-react-best-practices/rules/query-mutate-over-refetch.md +55 -0
  45. package/skills/gemi-react-best-practices/rules/query-no-hand-rolled-fetch.md +60 -0
  46. package/skills/gemi-react-best-practices/rules/query-revalidate-on-focus.md +44 -0
  47. package/skills/gemi-react-best-practices/rules/query-share-cache-key.md +51 -0
  48. package/skills/gemi-react-best-practices/rules/query-suspense-default.md +52 -0
  49. package/skills/gemi-react-best-practices/rules/routing-cache-policy-constants.md +53 -0
  50. package/skills/gemi-react-best-practices/rules/routing-middleware-dsl.md +60 -0
  51. package/skills/gemi-react-best-practices/rules/routing-resource-routes.md +59 -0
  52. package/skills/gemi-react-best-practices/rules/routing-routers-are-classes.md +55 -0
  53. package/skills/gemi-react-best-practices/rules/service-lazy-not-module-scope.md +63 -0
  54. package/skills/gemi-react-best-practices/rules/service-queue-is-in-memory.md +52 -0
  55. package/skills/gemi-react-best-practices/rules/service-static-token-and-name.md +52 -0
  56. package/skills/gemi-react-best-practices/rules/structure-discovered-vs-registered.md +71 -0
  57. package/skills/gemi-react-best-practices/rules/structure-do-not-reinvent-the-framework.md +58 -0
  58. package/skills/gemi-react-best-practices/rules/testing-assert-behaviour-over-markup.md +54 -0
  59. package/skills/gemi-react-best-practices/rules/testing-match-the-suite.md +57 -0
  60. package/skills/gemi-react-best-practices/rules/testing-page-seeds-real-inputs.md +65 -0
  61. package/dist/chunk-txhcx69q.js +0 -6
  62. /package/dist/{chunk-n412aa9s.js.map → chunk-khf9xda6.js.map} +0 -0
  63. /package/dist/{chunk-gfma8e03.js.map → chunk-x8beq9c4.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- import{$ as q,_ as D}from"./chunk-txhcx69q.js";import"./chunk-tkdxv6cq.js";import{Ac as V,oc as P}from"./chunk-dgasxgsm.js";import"./chunk-rkbv3df7.js";import"./chunk-z1e55w67.js";import"./chunk-q0y0j3ne.js";import"./chunk-4mcyyh1v.js";import"./chunk-z2tcxwyr.js";import"./chunk-f6dd4gd8.js";import"./chunk-3296fwss.js";import"./chunk-x6y1z4cq.js";import"./chunk-tds3xq6b.js";import"./chunk-j23cengk.js";import{Df as v}from"./chunk-hxb1eg2z.js";import"./chunk-4dan69s4.js";import{join as E}from"path";import{Duplex as o}from"stream";import{constants as T,createBrotliCompress as l,createGzip as n}from"zlib";var i=["br","gzip"],t=1024,a=5,s=19,r=6;function e($){if($===void 0)return 1;if(!/^\d+(\.\d{0,3})?$/.test($.trim()))return 1;let J=Number($.trim());return Number.isFinite(J)?Math.min(J,1):1}function JJ($){if($===null||$===void 0)return null;let J=new Map;for(let H of $.split(",")){let[Z,...X]=H.split(";"),j=Z.trim().toLowerCase();if(j==="")continue;let x=1;for(let K of X){let O=K.indexOf("=");if(O===-1)continue;if(K.slice(0,O).trim().toLowerCase()==="q"){x=e(K.slice(O+1));break}}J.set(j,x)}return J}function $J($,J=i){let H=JJ($);if(!H)return null;let Z=H.get("*"),X=null,j=0;for(let x of J){let K=H.get(x)??Z??0;if(K>j)X=x,j=K}return X}function HJ($,J){if(!$)return!1;return $.split(",").some((H)=>H.trim().toLowerCase().split("=")[0].trim()===J)}function UJ($,J){if($==="HEAD")return!1;if(!J.body)return!1;if(J.status<200||J.status===204||J.status===206||J.status===304)return!1;if(J.headers.has("Content-Range"))return!1;let H=J.headers.get("Content-Type");if(!H||!/^\s*text\/html\s*(;|$)/i.test(H))return!1;let Z=J.headers.get("Content-Encoding");if(Z&&Z.trim().toLowerCase()!=="identity")return!1;if(HJ(J.headers.get("Cache-Control"),"no-transform"))return!1;let X=J.headers.get("Content-Length");if(X!==null&&Number(X)<t)return!1;return!0}function ZJ($){let J=$==="br"?l({params:{[T.BROTLI_PARAM_QUALITY]:a,[T.BROTLI_PARAM_LGWIN]:s},flush:T.BROTLI_OPERATION_FLUSH}):n({level:r,flush:T.Z_SYNC_FLUSH});return o.toWeb(J)}function zJ($){let J=$.get("Vary");if(!J){$.set("Vary","Accept-Encoding");return}let H=J.split(",").map((Z)=>Z.trim().toLowerCase());if(H.includes("*")||H.includes("accept-encoding"))return;$.append("Vary","Accept-Encoding")}function F($,J){if(!UJ($.method,J))return J;let H=new Headers(J.headers);zJ(H);let Z=$J($.headers.get("Accept-Encoding"));if(!Z)return new Response(J.body,{status:J.status,statusText:J.statusText,headers:H});let X;try{X=ZJ(Z)}catch{return new Response(J.body,{status:J.status,statusText:J.statusText,headers:H})}return H.set("Content-Encoding",Z),H.delete("Content-Length"),new Response(J.body.pipeThrough(X),{status:J.status,statusText:J.statusText,headers:H})}function y($){let J=$.toString(),H=new Bun.CryptoHasher("sha1");return H.update(J),H.digest("hex")}if(!globalThis.URLPattern)globalThis.URLPattern=P;import{exists as GJ}from"fs/promises";var g="app/client.tsx";function w($){let J=$["app/client.tsx"]?.file;if(!J)return;return{module:`/${J}`,preload:S($,"app/client.tsx")}}function S($,J){let H=[],Z=new Set,X=(j)=>{if(Z.has(j))return;Z.add(j);let x=$[j];if(!x?.file)return;H.push(`/${x.file}`);for(let K of x.imports??[])X(K)};return X(J),H}var I=v(),XJ=E(I,"app"),L=E(I,"dist");async function VJ($,J){let H=await import(`${L}/client/.vite/manifest.json`),Z=await import(`${L}/server/.vite/manifest.json`);process.env.ROOT_DIR=I,process.env.APP_DIR=XJ,process.env.DIST_DIR=L;let X={},j={},x={},K={},O={},f=(U,G)=>`"${U}": () => import("${G}")`,k=[];for(let U of["404",...$.getFlatComponentTree.call($)]){let G=Z[`app/views/${U}.tsx`];if(!G?.file){console.log(`Server file not found for ${U}`),console.log(G);let W=Object.keys(Z),A=`app/views/${U}.tsx`;console.log(`${A} not found in server manifest`),console.log(W)}let Q=await import(`${process.env.DIST_DIR}/server/${G?.file}`);X[U]=Q.default,j[U]=Q.OpenGraph,x[U]=Q;let z=H[`app/views/${U}.tsx`];if(z?.css&&z?.css.length>0)K[U]=z?.css;if(z)k.push(f(U,`/${z?.file}`)),O[U]=S(H,`app/views/${U}.tsx`)}let u=`{${k.join(",")}}`,N=w(H);if(!N)console.error(`Client manifest has no "${g}" entry \u2014 dist/client is incomplete, so documents will render but never hydrate.`);let m=H["app/client.tsx"]?.css??[],h=(await Promise.all(m.map((U)=>Bun.file(`${L}/client/${U}`).text()))).join(`
2
+ import{$ as q,_ as D}from"./chunk-y3zz410b.js";import"./chunk-tkdxv6cq.js";import{Ac as V,oc as P}from"./chunk-dgasxgsm.js";import"./chunk-rkbv3df7.js";import"./chunk-z1e55w67.js";import"./chunk-q0y0j3ne.js";import"./chunk-4mcyyh1v.js";import"./chunk-z2tcxwyr.js";import"./chunk-f6dd4gd8.js";import"./chunk-3296fwss.js";import"./chunk-x6y1z4cq.js";import"./chunk-tds3xq6b.js";import"./chunk-j23cengk.js";import{Df as v}from"./chunk-hxb1eg2z.js";import"./chunk-4dan69s4.js";import{join as E}from"path";import{Duplex as o}from"stream";import{constants as T,createBrotliCompress as l,createGzip as n}from"zlib";var i=["br","gzip"],t=1024,a=5,s=19,r=6;function e($){if($===void 0)return 1;if(!/^\d+(\.\d{0,3})?$/.test($.trim()))return 1;let J=Number($.trim());return Number.isFinite(J)?Math.min(J,1):1}function JJ($){if($===null||$===void 0)return null;let J=new Map;for(let H of $.split(",")){let[Z,...X]=H.split(";"),j=Z.trim().toLowerCase();if(j==="")continue;let x=1;for(let K of X){let O=K.indexOf("=");if(O===-1)continue;if(K.slice(0,O).trim().toLowerCase()==="q"){x=e(K.slice(O+1));break}}J.set(j,x)}return J}function $J($,J=i){let H=JJ($);if(!H)return null;let Z=H.get("*"),X=null,j=0;for(let x of J){let K=H.get(x)??Z??0;if(K>j)X=x,j=K}return X}function HJ($,J){if(!$)return!1;return $.split(",").some((H)=>H.trim().toLowerCase().split("=")[0].trim()===J)}function UJ($,J){if($==="HEAD")return!1;if(!J.body)return!1;if(J.status<200||J.status===204||J.status===206||J.status===304)return!1;if(J.headers.has("Content-Range"))return!1;let H=J.headers.get("Content-Type");if(!H||!/^\s*text\/html\s*(;|$)/i.test(H))return!1;let Z=J.headers.get("Content-Encoding");if(Z&&Z.trim().toLowerCase()!=="identity")return!1;if(HJ(J.headers.get("Cache-Control"),"no-transform"))return!1;let X=J.headers.get("Content-Length");if(X!==null&&Number(X)<t)return!1;return!0}function ZJ($){let J=$==="br"?l({params:{[T.BROTLI_PARAM_QUALITY]:a,[T.BROTLI_PARAM_LGWIN]:s},flush:T.BROTLI_OPERATION_FLUSH}):n({level:r,flush:T.Z_SYNC_FLUSH});return o.toWeb(J)}function zJ($){let J=$.get("Vary");if(!J){$.set("Vary","Accept-Encoding");return}let H=J.split(",").map((Z)=>Z.trim().toLowerCase());if(H.includes("*")||H.includes("accept-encoding"))return;$.append("Vary","Accept-Encoding")}function F($,J){if(!UJ($.method,J))return J;let H=new Headers(J.headers);zJ(H);let Z=$J($.headers.get("Accept-Encoding"));if(!Z)return new Response(J.body,{status:J.status,statusText:J.statusText,headers:H});let X;try{X=ZJ(Z)}catch{return new Response(J.body,{status:J.status,statusText:J.statusText,headers:H})}return H.set("Content-Encoding",Z),H.delete("Content-Length"),new Response(J.body.pipeThrough(X),{status:J.status,statusText:J.statusText,headers:H})}function y($){let J=$.toString(),H=new Bun.CryptoHasher("sha1");return H.update(J),H.digest("hex")}if(!globalThis.URLPattern)globalThis.URLPattern=P;import{exists as GJ}from"fs/promises";var g="app/client.tsx";function w($){let J=$["app/client.tsx"]?.file;if(!J)return;return{module:`/${J}`,preload:S($,"app/client.tsx")}}function S($,J){let H=[],Z=new Set,X=(j)=>{if(Z.has(j))return;Z.add(j);let x=$[j];if(!x?.file)return;H.push(`/${x.file}`);for(let K of x.imports??[])X(K)};return X(J),H}var I=v(),XJ=E(I,"app"),L=E(I,"dist");async function VJ($,J){let H=await import(`${L}/client/.vite/manifest.json`),Z=await import(`${L}/server/.vite/manifest.json`);process.env.ROOT_DIR=I,process.env.APP_DIR=XJ,process.env.DIST_DIR=L;let X={},j={},x={},K={},O={},f=(U,G)=>`"${U}": () => import("${G}")`,k=[];for(let U of["404",...$.getFlatComponentTree.call($)]){let G=Z[`app/views/${U}.tsx`];if(!G?.file){console.log(`Server file not found for ${U}`),console.log(G);let W=Object.keys(Z),A=`app/views/${U}.tsx`;console.log(`${A} not found in server manifest`),console.log(W)}let Q=await import(`${process.env.DIST_DIR}/server/${G?.file}`);X[U]=Q.default,j[U]=Q.OpenGraph,x[U]=Q;let z=H[`app/views/${U}.tsx`];if(z?.css&&z?.css.length>0)K[U]=z?.css;if(z)k.push(f(U,`/${z?.file}`)),O[U]=S(H,`app/views/${U}.tsx`)}let u=`{${k.join(",")}}`,N=w(H);if(!N)console.error(`Client manifest has no "${g}" entry \u2014 dist/client is incomplete, so documents will render but never hydrate.`);let m=H["app/client.tsx"]?.css??[],h=(await Promise.all(m.map((U)=>Bun.file(`${L}/client/${U}`).text()))).join(`
3
3
  `),c=new P({pathname:"/*.:filetype(png|txt|js|css|jpg|svg|jpeg|avif|webp|ico|ttf|map)"});async function p(U){let{pathname:G}=new URL(U.url),Q=G.startsWith("/.well-known"),z=c.test({pathname:G})||Q,W=G.startsWith("/api");if(z&&!W){let A=new URL(U.url),b=U.url.replace(A.origin,"").split("?")[0],B=`${L}/client${b.replace("/assets/assets","/assets")}`,C=await GJ(B);if(!C&&A.pathname.includes(".js"))return new Response(`if(caches){caches?.delete("${B}")}window.location.reload();export {}`,{headers:{"Content-Type":"application/javascript"}});if(!C){if(G===V||G.startsWith(`${V}/`))return new Response("Not found",{status:404});return await R(U,G)}try{let Y=Bun.file(B),_=y(Y.lastModified);return new Response(Y.stream(),{headers:{"Content-Type":Y.type,"Cache-Control":"public, max-age=31536000, must-revalidate","Content-Length":String(Y.size),ETag:_}})}catch(Y){return $.onException?.(Y),new Response("Not found",{status:404})}}return await R(U,G)}async function R(U,G){let Q=$.fetch.bind($);try{let z=await Q(U);if(z instanceof Response)return z;else{let W=[];return W.push({content:h}),await z({getStyles:async(b)=>{if(!b)return D([]);for(let B of b){let C=H[`app/views/${B}.tsx`];for(let Y of C?.css??[]){let _=Bun.file(`${process.env.DIST_DIR}/client/${Y}`);W.push({id:Y,content:await _.text()})}}return D(W)},clientEntry:N,modulePreloadManifest:O,loaders:u,viewImportMap:X,viewModules:x,ogMap:j,cssManifest:K})}}catch(z){if(console.error(z),G.startsWith("/api"))return new Response(JSON.stringify({error:z.message}),{status:500,headers:{"Content-Type":"application/json"}});return $.onException?.(z),new Response(z.stack,{status:500})}}let d=(process.env.GEMI_COMPRESSION??"auto").toLowerCase()!=="off",M=Bun.serve({maxRequestBodySize:10737418240,fetch:async(U,G)=>{if(!U.headers.get("x-forwarded-for")){let z=G.requestIP(U);if(z)U.headers.set("x-forwarded-for",z.address)}let Q=await J(U,p);return d?F(U,Q):Q},idleTimeout:Number(process.env.SERVER_IDLE_TIMEOUT??10),port:process.env.PORT||5173});return q({port:M.port,rootDir:I}),M}export{VJ as httpProd};
4
4
 
5
5
  //# debugId=BC47A4F2D8EBF4CE64756E2164756E21
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- import{$ as f,Z as S}from"./chunk-txhcx69q.js";import{ia as x,ja as D}from"./chunk-v7y8zm61.js";import"./chunk-5ad8era8.js";import"./chunk-ejrcyzx8.js";import"./chunk-tkdxv6cq.js";import"./chunk-xnytbrda.js";import"./chunk-4dan69s4.js";import{IncomingMessage as g,ServerResponse as w}from"http";import{Socket as E}from"net";import{join as k}from"path";import{createServer as q}from"vite";function T(H){return H.replace(/\x1b\[[0-9;]*m/g,"").replace(/\[[0-9;]*m/g,"").replace(/\n\s*\n/g,`
2
+ import{$ as f,Z as S}from"./chunk-y3zz410b.js";import{ia as x,ja as D}from"./chunk-v7y8zm61.js";import"./chunk-5ad8era8.js";import"./chunk-ejrcyzx8.js";import"./chunk-tkdxv6cq.js";import"./chunk-xnytbrda.js";import"./chunk-4dan69s4.js";import{IncomingMessage as g,ServerResponse as w}from"http";import{Socket as E}from"net";import{join as k}from"path";import{createServer as q}from"vite";function T(H){return H.replace(/\x1b\[[0-9;]*m/g,"").replace(/\[[0-9;]*m/g,"").replace(/\n\s*\n/g,`
3
3
  `).split(`
4
4
  `).map((J)=>J.trim()).filter((J)=>J.length>0).join(`
5
5
  `).trim()}function b(H){return`
@@ -0,0 +1,6 @@
1
+ // @bun
2
+ import{pa as h}from"./chunk-tkdxv6cq.js";import{createElement as x}from"react";function j(i,n){let e=Object.keys(n).sort((u,b)=>b.length-u.length).map((u)=>u.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),r=new RegExp(e.join("|"),"g");return i.replace(r,(u)=>n[u])}async function J(i,n,e=[]){let r=[...e.map((t)=>`${i}/views/${t}.tsx`),`${i}/views/RootLayout.tsx`],u=new Set;for(let t of r){let s=n.moduleGraph.getModulesByFile(t);if(s)for(let d of s)u.add(d)}let b=[],p=[],c={};for(let t of u)if(t)for(let s of t.importedModules){if(s.file.includes("module.css"))c[s.file]=s.ssrTransformResult.map.sourcesContent.join("");if(s.file.includes(".css"))p.push(s.file)}for(let t of p){let s=await n.transformRequest(t+"?direct"),d=t.includes("module.css"),o="";if(d)o=j(c[t],s.default);b.push({isDev:!0,id:t,content:d?o:s.code})}return b.map((t,s)=>{return x("style",{key:s,type:"text/css","data-vite-dev-id":t.id,dangerouslySetInnerHTML:{__html:t.content}})})}async function L(i=[]){return i.map((n,e)=>{return x("style",{key:e,id:n?.id,type:"text/css",dangerouslySetInnerHTML:{__html:n.content}})})}import{existsSync as f}from"fs";import{basename as k}from"path";import{networkInterfaces as v}from"os";var y={name:"gemi",version:"0.60.0",private:!1,license:"MIT",author:"Enes Tufekci <enes@gemijs.dev>",repository:{type:"git",url:"git+https://github.com/nstfkc/gemi.git",directory:"packages/gemi"},bin:{gemi:"./dist/bin/gemi.js","gemi-orm-generator":"./dist/bin/orm-generator.js"},files:["dist/**/*","ide/typescript-plugin/package.json","skills/**/*"],module:!0,exports:{"./http":"./http/index.ts","./client":"./client/index.ts","./testing":"./testing/index.ts","./app":"./app/index.ts","./facades":"./facades/index.ts","./email":"./email/index.ts","./vite":"./dist/vite/index.mjs","./kernel":"./kernel/index.ts","./services":"./services/index.ts","./broadcasting":"./broadcasting/index.ts","./i18n":"./i18n/index.ts","./dictionary":"./i18n/dictionaryRuntime.ts","./server":"./server/index.ts","./config":"./config/index.ts","./container":"./container/index.ts","./foundation":"./foundation/index.ts","./database":"./database/index.ts","./orm":"./orm/index.ts","./support":"./support/index.ts","./console/run":"./console/run.ts","./ide/typescript-plugin":"./ide/typescript-plugin/index.ts","./bun/preload":"./bun/preload.ts","./bun/plugin":"./bun/plugin.ts"},scripts:{lint:"oxlint",typecheck:"tsc --noEmit -p tsconfig.json && tsc --noEmit -p ide/typescript-plugin/tsconfig.json",build:"NODE_ENV=production bun run build:core && bun run build:bin && bun run build:types && bun run build:client && bun run build:plugin && bun run build:ts-plugin && bun run build:client-types && bun run build:augmentation","build:core":"bun ./scripts/build.ts","build:bin":"bun build --outdir=./dist/bin --target=bun --external=vite --external=react-dom --external=react/jsx-runtime --external=bun --external=sharp --external=@azure/storage-blob --sourcemap ./bin/gemi.ts ./bin/orm-generator.ts && bun ./scripts/prepare-bin.ts","build:client":"rm -rf dist/client dist/testing dist/chunks && vite build -c vite.client.config.mts","build:plugin":"vite build -c vite.plugin.config.mts","build:ts-plugin":"bun ./scripts/build-ts-plugin.ts","build:types":"tsc","build:client-types":"tsc -p tsconfig.browser.json",test:"bun --bun vitest","test:types":"vitest run --typecheck.only","test:packaging":"bun --bun vitest run --config vitest.packaging.config.ts","build:publish":"bun scripts/build-publish.ts",prepublishOnly:"echo 'Do not publish from this directory \u2014 its exports point at TS source. Run: bun run build:publish, then publish from .publish/' && exit 1","build:augmentation":"bun ./scripts/wire-augmentation.ts"},dependencies:{"@aws-sdk/client-s3":"^3.629.0","@aws-sdk/s3-request-presigner":"^3.629.0","ast-types":"^0.14.2",commander:"^12.1.0",history:"^5.3.0","jsx-email":"2.0.0-rc2.1","magic-string":"^0.30.17",open:"^10.1.0","react-error-boundary":"^6.0.0",recast:"^0.23.9",resend:"^6.12.3",satori:"^0.12.2","temporal-polyfill":"^0.2.5","twitter-api-v2":"^1.20.2","urlpattern-polyfill":"^10.0.0",uuid:"^9.0.1"},devDependencies:{"@babel/parser":"^7.25.4","@prisma/generator-helper":"^6.5.0","@repo/typescript-config":"*","@testing-library/react":"^16.3.2","@types/bun":"^1.3.14","@types/eslint":"^8.56.5","@types/node":"^20.11.24","@types/react":"^19.0.2","@types/react-dom":"^19.0.2",eslint:"^8.57.0",jsdom:"^30.0.1",oxlint:"^1.39.0",react:"19.2.3","react-dom":"19.2.3",typescript:"^5.7.3",vitest:"^4.0.0"},peerDependencies:{"@azure/storage-blob":"^12.28.0",react:">=19","react-dom":">=19",sharp:"^0.34.2",vite:"^8.0.0"},peerDependenciesMeta:{"@azure/storage-blob":{optional:!0}}};var z=[" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2557","\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551","\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551"," \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D"],O=Boolean(process.stdout.isTTY)&&!process.env.NO_COLOR,g=(i,n)=>O?`\x1B[${i}m${n}\x1B[0m`:n,T=(i)=>g("1",i),l=(i)=>g("2",i),a=(i)=>g("36",i),q=(i)=>g("32",i),E=(i)=>g("35",i);function R(){return y.version??"?"}function _(i){for(let n of Object.values(v()))for(let e of n??[])if((e.family==="IPv4"||e.family===4)&&!e.internal)return`http://${e.address}:${i}`;return null}function G(i){return h(i).filter((n)=>f(n)).map((n)=>k(n))}function A(i){let{port:n,rootDir:e}=i,r=`http://localhost:${n}`,u=_(n),b=G(e),p=(t,s)=>` ${q("\u279C")} ${T(t.padEnd(9))}${s}`,c=["",...z.map((t)=>E(t)),"",` ${l("gemi")} ${a(`v${R()}`)}`,"",p("Local:",a(r)),p("Network:",u?a(u):l("unavailable")),p("Env:",b.length?b.join(", "):l("none")),""];console.log(c.join(`
3
+ `))}
4
+ export{J as Z,L as _,A as $};
5
+
6
+ //# debugId=487B21B42AFDBDAC64756E2164756E21
@@ -5,7 +5,7 @@
5
5
  "import { createElement } from \"react\";\nimport { type ModuleNode, type ViteDevServer } from \"vite\";\n\nfunction replaceStrings(text: string, record: Record<string, string>): string {\n const escapedKeys = Object.keys(record)\n .sort((a, b) => b.length - a.length)\n .map((key) => key.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n\n // Create a single regex with all keys\n const regex = new RegExp(escapedKeys.join(\"|\"), \"g\");\n\n return text.replace(regex, (match) => record[match]);\n}\n\nexport async function createDevStyles(\n appDir: string,\n vite: ViteDevServer,\n currentViews: string[] = [],\n) {\n const views = [\n ...currentViews.map((view) => `${appDir}/views/${view}.tsx`),\n `${appDir}/views/RootLayout.tsx`,\n ];\n\n const modules = new Set<ModuleNode>();\n for (const view of views) {\n const mod = vite.moduleGraph.getModulesByFile(view);\n if (mod) {\n for (const m of mod) modules.add(m);\n }\n }\n\n const styles = [];\n const cssModules = [];\n const cssModuleContent: Record<string, string> = {};\n for (const mod of modules as any) {\n if (mod) {\n for (const imported of mod.importedModules) {\n if (imported.file.includes(\"module.css\")) {\n cssModuleContent[imported.file] = imported.ssrTransformResult.map.sourcesContent.join(\"\");\n }\n if (imported.file.includes(\".css\")) {\n cssModules.push(imported.file);\n }\n }\n }\n }\n\n for (const cssModulePath of cssModules) {\n const transform = await vite.transformRequest(cssModulePath + \"?direct\");\n\n const isCssModule = cssModulePath.includes(\"module.css\");\n\n let transformedCssModule = \"\";\n\n if (isCssModule) {\n transformedCssModule = replaceStrings(\n cssModuleContent[cssModulePath],\n // Vite's `TransformResult` type has no `default`, but the CSS-module\n // transform attaches the class-name mapping there at runtime.\n (transform as unknown as { default: Record<string, string> }).default,\n );\n }\n\n styles.push({\n isDev: true,\n id: cssModulePath,\n content: isCssModule ? transformedCssModule : transform.code,\n });\n }\n\n return styles.map((style, i) => {\n return createElement(\"style\", {\n key: i,\n type: \"text/css\",\n \"data-vite-dev-id\": style.id,\n dangerouslySetInnerHTML: { __html: style.content },\n });\n });\n}\n\nexport async function createStyles(styles = []) {\n return styles.map((style, i) => {\n return createElement(\"style\", {\n key: i,\n id: style?.id,\n type: \"text/css\",\n dangerouslySetInnerHTML: { __html: style.content },\n });\n });\n}\n",
6
6
  "import { existsSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { networkInterfaces } from \"node:os\";\nimport { envFiles } from \"./watchEnv\";\nimport pkg from \"../package.json\";\n\n// \"GEMI\" — ANSI Shadow figlet font.\nconst ART = [\n \" ██████╗ ███████╗███╗ ███╗██╗\",\n \"██╔════╝ ██╔════╝████╗ ████║██║\",\n \"██║ ███╗█████╗ ██╔████╔██║██║\",\n \"██║ ██║██╔══╝ ██║╚██╔╝██║██║\",\n \"╚██████╔╝███████╗██║ ╚═╝ ██║██║\",\n \" ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝\",\n];\n\nconst useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;\nconst paint = (code: string, text: string) =>\n useColor ? `\\x1b[${code}m${text}\\x1b[0m` : text;\nconst bold = (t: string) => paint(\"1\", t);\nconst dim = (t: string) => paint(\"2\", t);\nconst cyan = (t: string) => paint(\"36\", t);\nconst green = (t: string) => paint(\"32\", t);\nconst magenta = (t: string) => paint(\"35\", t);\n\nfunction gemiVersion(): string {\n return (pkg as { version?: string }).version ?? \"?\";\n}\n\nfunction networkUrl(port: number | string): string | null {\n for (const ifaces of Object.values(networkInterfaces())) {\n for (const iface of ifaces ?? []) {\n // Node <18 reported `family` as the number 4; newer Bun/Node use \"IPv4\".\n const isV4 = iface.family === \"IPv4\" || (iface.family as unknown) === 4;\n if (isV4 && !iface.internal) return `http://${iface.address}:${port}`;\n }\n }\n return null;\n}\n\nfunction loadedEnvFiles(rootDir: string): string[] {\n return envFiles(rootDir)\n .filter((file) => existsSync(file))\n .map((file) => basename(file));\n}\n\nexport function printStartupBanner(params: {\n port: number | string;\n rootDir: string;\n}) {\n const { port, rootDir } = params;\n const local = `http://localhost:${port}`;\n const network = networkUrl(port);\n const env = loadedEnvFiles(rootDir);\n\n const row = (label: string, value: string) =>\n ` ${green(\"➜\")} ${bold(label.padEnd(9))}${value}`;\n\n const out = [\n \"\",\n ...ART.map((line) => magenta(line)),\n \"\",\n ` ${dim(\"gemi\")} ${cyan(`v${gemiVersion()}`)}`,\n \"\",\n row(\"Local:\", cyan(local)),\n row(\"Network:\", network ? cyan(network) : dim(\"unavailable\")),\n row(\"Env:\", env.length ? env.join(\", \") : dim(\"none\")),\n \"\",\n ];\n\n console.log(out.join(\"\\n\"));\n}\n"
7
7
  ],
8
- "mappings": "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",
9
- "debugId": "729A5B226DC8C37F64756E2164756E21",
8
+ "mappings": "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",
9
+ "debugId": "487B21B42AFDBDAC64756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -12,4 +12,6 @@ export interface GemiConfig {
12
12
  bun?: GemiBunConfig;
13
13
  }
14
14
  export declare function defineConfig(config: GemiConfig): GemiConfig;
15
+ export declare function reactCompiler(): boolean;
16
+ export declare function reactCompiler<T extends object>(options: T): T | false;
15
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../config/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAMrC,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAKD,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB;AAOD,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAE3D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../config/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAMrC,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAKD,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB;AAOD,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAE3D;AA0BD,wBAAgB,aAAa,IAAI,OAAO,CAAC;AACzC,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC"}
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- import"../chunk-4dan69s4.js";function j(h){return h}export{j as defineConfig};
2
+ import"../chunk-4dan69s4.js";function k(b){return b}function q(b){if((process.env.GEMI_REACT_COMPILER??"auto").toLowerCase()==="off")return!1;return b??!0}export{q as reactCompiler,k as defineConfig};
3
3
 
4
- //# debugId=FB1D7FB1C6C96D0964756E2164756E21
4
+ //# debugId=2F7F86887FCF6F1A64756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../config/index.ts"],
4
4
  "sourcesContent": [
5
- "import type { PluginOption } from \"vite\";\nimport type { BunPlugin } from \"bun\";\n\n// Vite configuration contributed by the app. `plugins` are appended to gemi's\n// own Vite plugins; every other key is a standard Vite `UserConfig` field and is\n// merged (by Vite) on top of gemi's base config for the client + SSR view\n// builds. See `gemi/vite`.\nexport interface GemiViteConfig {\n plugins?: PluginOption[];\n [key: string]: unknown;\n}\n\n// Bun configuration contributed by the app. `plugins` are applied both at build\n// time (the server `Bun.build`) and at runtime (registered via `--preload` for\n// `dev`/`start`), alongside gemi's built-in custom-request plugin.\nexport interface GemiBunConfig {\n plugins?: BunPlugin[];\n}\n\nexport interface GemiConfig {\n vite?: GemiViteConfig;\n bun?: GemiBunConfig;\n}\n\n// Identity helper that gives `gemi.config.ts` full type-checking and editor\n// autocomplete. Usage:\n//\n// import { defineConfig } from \"gemi/config\";\n// export default defineConfig({ vite: { plugins: [] }, bun: { plugins: [] } });\nexport function defineConfig(config: GemiConfig): GemiConfig {\n return config;\n}\n"
5
+ "import type { PluginOption } from \"vite\";\nimport type { BunPlugin } from \"bun\";\n\n// Vite configuration contributed by the app. `plugins` are appended to gemi's\n// own Vite plugins; every other key is a standard Vite `UserConfig` field and is\n// merged (by Vite) on top of gemi's base config for the client + SSR view\n// builds. See `gemi/vite`.\nexport interface GemiViteConfig {\n plugins?: PluginOption[];\n [key: string]: unknown;\n}\n\n// Bun configuration contributed by the app. `plugins` are applied both at build\n// time (the server `Bun.build`) and at runtime (registered via `--preload` for\n// `dev`/`start`), alongside gemi's built-in custom-request plugin.\nexport interface GemiBunConfig {\n plugins?: BunPlugin[];\n}\n\nexport interface GemiConfig {\n vite?: GemiViteConfig;\n bun?: GemiBunConfig;\n}\n\n// Identity helper that gives `gemi.config.ts` full type-checking and editor\n// autocomplete. Usage:\n//\n// import { defineConfig } from \"gemi/config\";\n// export default defineConfig({ vite: { plugins: [] }, bun: { plugins: [] } });\nexport function defineConfig(config: GemiConfig): GemiConfig {\n return config;\n}\n\n// The value to pass as `@vitejs/plugin-react`'s `compiler` option, with an\n// environment switch in front of it:\n//\n// import react from \"@vitejs/plugin-react\";\n// import { defineConfig, reactCompiler } from \"gemi/config\";\n// export default defineConfig({ vite: { plugins: [react({ compiler: reactCompiler() })] } });\n//\n// `GEMI_REACT_COMPILER=off` turns the React Compiler off; any other value (or\n// none) leaves it on. Case-insensitive, and the same shape as `GEMI_COMPRESSION`\n// — a named opt-out rather than a boolean, because `=0`, `=false` and `=no` all\n// read as \"off\" to a human and only one of them can be the one that works.\n//\n// It lives here rather than as a `process.env` read in each app's\n// `gemi.config.ts` so the flag means the same thing in every gemi project, and\n// so bisecting a compiler-shaped bug is one variable rather than one edit:\n//\n// GEMI_REACT_COMPILER=off bun dev # is it the compiler?\n//\n// The variable is read where `gemi.config.ts` is loaded — inside the Vite\n// process that `dev` and `build` spawn — so it is inherited from the shell and\n// also picked up from `.env`, which Bun loads before the config imports.\n//\n// Pass compiler options through it to keep the switch and the options together;\n// `false` is returned in their place when the environment opts out.\nexport function reactCompiler(): boolean;\nexport function reactCompiler<T extends object>(options: T): T | false;\nexport function reactCompiler(options?: object): boolean | object {\n const enabled = (process.env.GEMI_REACT_COMPILER ?? \"auto\").toLowerCase() !== \"off\";\n if (!enabled) return false;\n return options ?? true;\n}\n"
6
6
  ],
7
- "mappings": ";6BA6BO,SAAS,CAAY,CAAC,EAAgC,CAC3D,OAAO",
8
- "debugId": "FB1D7FB1C6C96D0964756E2164756E21",
7
+ "mappings": ";6BA6BO,SAAS,CAAY,CAAC,EAAgC,CAC3D,OAAO,EA6BF,SAAS,CAAa,CAAC,EAAoC,CAEhE,IADiB,QAAQ,IAAI,qBAAuB,QAAQ,YAAY,IAAM,MAChE,MAAO,GACrB,OAAO,GAAW",
8
+ "debugId": "2F7F86887FCF6F1A64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- import{qa as z}from"../chunk-tkdxv6cq.js";import{ra as b}from"../chunk-xnytbrda.js";import{Df as g}from"../chunk-hxb1eg2z.js";import{If as S}from"../chunk-4dan69s4.js";class B{app;instrumentation;constructor(f){this.app=new b({kernel:f.kernel}),this.instrumentation=f.instrumentation??((C,F)=>F(C))}async start(){if(process.env.ROOT_DIR=g(),await this.app.waitForBoot(),Bun.env.NODE_ENV==="production"){let{httpProd:f}=await import("../chunk-n412aa9s.js");await f(this.app,this.instrumentation.bind(this))}else{z();let{httpDev:f}=await import("../chunk-gfma8e03.js");await f(this.app,this.instrumentation.bind(this))}}}export{B as Server};
2
+ import{qa as z}from"../chunk-tkdxv6cq.js";import{ra as b}from"../chunk-xnytbrda.js";import{Df as g}from"../chunk-hxb1eg2z.js";import{If as S}from"../chunk-4dan69s4.js";class B{app;instrumentation;constructor(f){this.app=new b({kernel:f.kernel}),this.instrumentation=f.instrumentation??((C,F)=>F(C))}async start(){if(process.env.ROOT_DIR=g(),await this.app.waitForBoot(),Bun.env.NODE_ENV==="production"){let{httpProd:f}=await import("../chunk-khf9xda6.js");await f(this.app,this.instrumentation.bind(this))}else{z();let{httpDev:f}=await import("../chunk-x8beq9c4.js");await f(this.app,this.instrumentation.bind(this))}}}export{B as Server};
3
3
 
4
4
  //# debugId=D570F7183E61436864756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemi",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Enes Tufekci <enes@gemijs.dev>",
@@ -15,7 +15,8 @@
15
15
  },
16
16
  "files": [
17
17
  "dist/**/*",
18
- "ide/typescript-plugin/package.json"
18
+ "ide/typescript-plugin/package.json",
19
+ "skills/**/*"
19
20
  ],
20
21
  "module": true,
21
22
  "exports": {
@@ -0,0 +1,231 @@
1
+ ---
2
+ name: gemi-react-best-practices
3
+ description: Best practices for the gemi framework (Bun + Vite + React 19 SSR full-stack TypeScript framework) — routing, controllers, the ORM, the SSR data payload, useQuery, i18n, jobs/services, and testing. Use when writing, reviewing, or refactoring anything under app/ — views, routers, controllers, models, dictionaries — or when a task mentions slow pages, extra round-trips, N+1 queries, bundle size, or re-renders.
4
+ ---
5
+
6
+ # gemi Best Practices
7
+
8
+ Rules for building on **gemi**, the full-stack TypeScript framework this app runs on
9
+ (Bun + Vite, React 19 SSR). 45 rules across 11 categories, ordered by impact.
10
+
11
+ Every rule is derived from the gemi documentation and from patterns that recur across
12
+ gemi codebases. This skill ships with the `gemi` package, so it describes the
13
+ framework; where it and your app disagree, your app wins (ground rule 2).
14
+
15
+ ## When to Apply
16
+
17
+ - Adding or changing a route, view, layout, controller, or middleware
18
+ - Writing a `useQuery` read, a mutation, or a `Query.prefetch`
19
+ - Writing ORM queries — especially list reads, aggregations, and transactions
20
+ - Adding a Job, Service, or one-off Command
21
+ - Writing dictionaries or tests
22
+ - Diagnosing extra round-trips, slow first paint, blank flashes, or bundle growth
23
+
24
+ ## Ground Rules That Precede Everything Below
25
+
26
+ 1. **Fetch the docs; do not guess an API.** Index (page list + one-line summaries):
27
+ <https://nstfkc.github.io/gemi/llms.txt> — fetch only the pages you need.
28
+ Everything in one file: <https://nstfkc.github.io/gemi/llms-full.txt> (~170 KB).
29
+ 2. **`CLAUDE.md` in this app wins** where it and either the docs or this skill
30
+ disagree — it describes *our* app, not the framework in general.
31
+ 3. **Mirror existing code.** Don't invent gemi patterns. When a rule here and an
32
+ existing pattern disagree, check git history before "fixing" it.
33
+ 4. **Routing is class-based and data is loaded in controllers.** File location
34
+ registers nothing; a view's file name has no relation to its URL.
35
+ 5. **Check whether the React Compiler is on before hand-memoizing.** It is enabled
36
+ per app in `gemi.config.ts` via the React plugin's `compiler` option, which the
37
+ gemi template turns on by default — and `GEMI_REACT_COMPILER=off` in the
38
+ environment disables it, so the config is not the last word. With it on, the client build is
39
+ auto-memoized and a hand-written `useMemo` is usually redundant; with it off,
40
+ memoization is manual and load-bearing. The compiler never runs on the SSR view
41
+ build — server rendering is a single pass — so it changes nothing about what a
42
+ controller does.
43
+
44
+ ## Rule Categories by Priority
45
+
46
+ | Priority | Category | Impact | Prefix |
47
+ |----------|----------|--------|--------|
48
+ | 0 | Project Structure | HIGH | `structure-` |
49
+ | 1 | Server Payload & Waterfalls | CRITICAL | `payload-` |
50
+ | 2 | Client Data Fetching | CRITICAL | `query-` |
51
+ | 3 | Data Access (ORM) | HIGH | `orm-` |
52
+ | 4 | Routing & Middleware | HIGH | `routing-` |
53
+ | 5 | Controllers & Request Handling | HIGH | `controller-` |
54
+ | 6 | Services, Jobs & Commands | HIGH | `service-` |
55
+ | 7 | Client Components & Navigation | MEDIUM | `client-` |
56
+ | 8 | Bundle Size | MEDIUM | `bundle-` |
57
+ | 9 | Internationalization | MEDIUM | `i18n-` |
58
+ | 10 | Testing | MEDIUM | `testing-` |
59
+
60
+ ## Project Structure
61
+
62
+ Everything the framework guarantees lives under `app/`. This is the layout a
63
+ scaffolded project has; the parts marked *discovered* are read by walking the
64
+ directory, so writing the file is the registration.
65
+
66
+ ```
67
+ app/
68
+ server.ts server entry - boots the kernel, starts the HTTP server
69
+ client.tsx browser entry - hydrates the React app
70
+ preload.ts optional Bun preload, runs before the server starts
71
+ kernel/Kernel.ts declares `config`, `providers`, and `models`
72
+ config/ runtime config, one file per framework service
73
+ providers/ your own container bindings
74
+ http/
75
+ routes/ api.ts and view.ts - the root routers
76
+ controllers/ controller classes
77
+ requests/ HttpRequest subclasses used for validation
78
+ models/ ORM models (declared on the Kernel)
79
+ generated/ OUTPUT - never hand-edit
80
+ views/ React views, layouts, RootLayout (path is NOT the URL)
81
+ email/ jsx-email templates
82
+ i18n/ dictionaries
83
+ cron/ CronJob classes <- discovered at boot
84
+ jobs/ Job classes <- discovered at boot
85
+ listeners/ Listener classes <- discovered at boot
86
+ commands/ defineCommand chains <- discovered by `gemi run`
87
+ database/prisma.ts the Prisma client instance
88
+ ```
89
+
90
+ `app/listeners/` is absent from a fresh scaffold; that is not an error, it means
91
+ the app has no listeners yet.
92
+
93
+ ### Where a new file goes
94
+
95
+ | Adding a… | Goes in | Reached by |
96
+ |---|---|---|
97
+ | page / layout | `app/views/` | a **router**, never its file path |
98
+ | URL, or middleware on one | `app/http/routes/` | mounted from `view.ts` / `api.ts` |
99
+ | handler logic for a route | `app/http/controllers/` | named by a router |
100
+ | request validation | `app/http/requests/` | named by a controller method |
101
+ | ORM model | `app/models/` | the Kernel's `models` |
102
+ | background work | `app/jobs/` | discovery |
103
+ | scheduled work | `app/cron/` | discovery |
104
+ | event handler | `app/listeners/` | discovery |
105
+ | one-off ops task | `app/commands/` | discovery, via `gemi run` |
106
+ | anything with I/O, a client, or state | `app/services/` | the container, by `static token` |
107
+ | container bindings | `app/providers/` | the Kernel's `providers` |
108
+ | settings for a framework service | `app/config/` | the Kernel's `config` |
109
+ | translations | `app/i18n/` | `defineDictionary` |
110
+ | build config (Vite/Bun plugins) | `gemi.config.ts` **at the root** | the CLI — *not* `app/config/` |
111
+
112
+ Two mistakes this table exists to prevent, both of which typecheck and then do
113
+ nothing: putting a view at a path that looks like its URL and expecting it to
114
+ route, and writing a model without declaring it on the Kernel. See
115
+ `structure-discovered-vs-registered` and `structure-do-not-reinvent-the-framework`.
116
+
117
+ ## Quick Reference
118
+
119
+ ### 0. Project Structure (HIGH)
120
+
121
+ - `structure-discovered-vs-registered` - Four directories register by being walked; models, routers, config and providers are declared
122
+ - `structure-do-not-reinvent-the-framework` - `lib/`, a singleton, a hand-rolled registry: gemi already has each of these with a name
123
+
124
+ ### 1. Server Payload & Waterfalls (CRITICAL)
125
+
126
+ The biggest lever in the app. gemi ships a data payload with the SSR HTML; every read
127
+ the client discovers only after hydration costs a round-trip the payload could have
128
+ carried for free.
129
+
130
+ - `payload-prefetch-mirrors-usequery` - A prefetch lands only if path + params + search match the cache key exactly
131
+ - `payload-prefetch-late-queries` - Prefetch reads render discovers late (nested under suspense, conditionally rendered)
132
+ - `payload-instant-vs-prefetch` - `Query.instant` blocks the response; `Query.prefetch` runs in parallel
133
+ - `payload-parallel-controller-work` - `Promise.all` independent work in a controller method
134
+ - `payload-dont-overprefetch` - Popover and heavy-collection reads belong behind a mount gate
135
+ - `payload-minimal-view-props` - Ship the shape the view renders, not the row you loaded
136
+
137
+ ### 2. Client Data Fetching (CRITICAL)
138
+
139
+ - `query-no-hand-rolled-fetch` - `useQuery` / mutation hooks, never a raw `fetch`
140
+ - `query-suspense-default` - `suspense: true` is the DEFAULT and throws to the nearest boundary
141
+ - `query-lazy-vs-mount-gate` - A lazy query does not refetch when its variant changes
142
+ - `query-share-cache-key` - Identical path + params + search dedupes across components for free
143
+ - `query-keep-previous-data` - Keep the previous page rendered while the next variant loads
144
+ - `query-mutate-over-refetch` - Write the cache with `mutate` / `useMutate` instead of refetching
145
+ - `query-debounce-search-variant` - Debounce a value before it becomes a query variant
146
+ - `query-revalidate-on-focus` - Opt in only for cross-tab-mutable data; let `staleTime` gate it
147
+
148
+ ### 3. Data Access — ORM (HIGH)
149
+
150
+ - `orm-include-not-n-plus-one` - One `include` tree beats a loop of queries (lateral strategy = one round trip)
151
+ - `orm-transaction-sequential` - `Promise.all` inside `Model.transaction` is unsafe — one reserved connection
152
+ - `orm-transaction-no-io` - Keep network calls, uploads and queue pushes out of a transaction callback
153
+ - `orm-analytics-connection` - Heavy admin/cron aggregations run on the analytics pool
154
+ - `orm-paginate-helper` - Use `paginate()` from `gemi/orm`; know its 100-row ceiling
155
+ - `orm-select-narrow` - `select` the columns the response actually serializes
156
+ - `orm-plain-rows-by-default` - Plain rows are free; `track` costs ~100% on a large read
157
+
158
+ ### 4. Routing & Middleware (HIGH)
159
+
160
+ - `routing-routers-are-classes` - Routes are declared on router classes, not by file location
161
+ - `routing-middleware-dsl` - Middleware is a string DSL at router or route level; `-name` cancels
162
+ - `routing-cache-policy-constants` - Name cache policies once and reuse the constant
163
+ - `routing-resource-routes` - `resource()` for standard REST, with per-method middleware
164
+
165
+ ### 5. Controllers & Request Handling (HIGH)
166
+
167
+ - `controller-request-schema` - Validate with a request schema, not inline checks
168
+ - `controller-throw-framework-errors` - Throw `ValidationError` / auth errors; never invent a response shape
169
+ - `controller-redirect-facade-throws` - Never wrap the `Redirect` facade in try/catch — it works by throwing
170
+ - `controller-authorize-every-tenant-read` - Scope every tenant read, in middleware or an ORM policy
171
+ - `controller-parse-request-at-the-boundary` - Parse the request in the controller; keep utils framework-free
172
+
173
+ ### 6. Services, Jobs & Commands (HIGH)
174
+
175
+ - `service-static-token-and-name` - `static token` / `static name` survive minification; class names do not
176
+ - `service-queue-is-in-memory` - The queue is in-process; enqueued work is lost on restart
177
+ - `service-lazy-not-module-scope` - Construct clients lazily — discovery imports every module
178
+
179
+ ### 7. Client Components & Navigation (MEDIUM)
180
+
181
+ - `client-typed-links` - Navigate with a typed `Link` / `useNavigate`, not an interpolated path
182
+ - `client-form-vs-mutation-hooks` - `<Form>` first; mutation hooks when you need control
183
+ - `client-loading-error-exports` - A route module's `Loading` / `Error` exports ARE its suspense boundary
184
+ - `client-no-effect-data-flow` - Derive in `useMemo`, reset in the handler, debounce before use
185
+
186
+ ### 8. Bundle Size (MEDIUM)
187
+
188
+ - `bundle-deep-imports` - Import UI primitives by deep path; a barrel drags the whole library in
189
+ - `bundle-mount-gate-heavy-panels` - Put heavy subtrees inside the thing that unmounts them
190
+
191
+ ### 9. Internationalization (MEDIUM)
192
+
193
+ - `i18n-define-dictionary-inline` - The `defineDictionary` literal must be inline — a helper fails the BUILD
194
+
195
+ ### 10. Testing (MEDIUM)
196
+
197
+ - `testing-page-seeds-real-inputs` - Mount with `<Page>` from `gemi/testing`; never mock `gemi/client`
198
+ - `testing-assert-behaviour-over-markup` - Query the DOM; don't scrape `renderToStaticMarkup` output
199
+ - `testing-match-the-suite` - Copy the runner and naming already in the directory; component tests need a DOM
200
+
201
+ ## The Gotchas Most Likely to Bite
202
+
203
+ Counter-intuitive behaviours that produce silent failures rather than errors:
204
+
205
+ | Gotcha | Rule |
206
+ |---|---|
207
+ | `Redirect` works by **throwing** — a `try/catch` swallows it | `controller-redirect-facade-throws` |
208
+ | A `defineDictionary` behind a helper passes tests and fails the **build** | `i18n-define-dictionary-inline` |
209
+ | `Promise.all` inside a transaction shares one reserved connection | `orm-transaction-sequential` |
210
+ | A lazy query never refetches when its variant changes | `query-lazy-vs-mount-gate` |
211
+ | A prefetch whose `search` differs primes a slot nothing reads | `payload-prefetch-mirrors-usequery` |
212
+ | A class name is minified in prod — jobs and services need a static string | `service-static-token-and-name` |
213
+ | `paginate()` silently caps `perPage` at 100 | `orm-paginate-helper` |
214
+ | Command discovery **imports** every file under `app/commands` | `service-lazy-not-module-scope` |
215
+ | A suspending query blanks everything up to the nearest boundary | `query-suspense-default` |
216
+ | Returning `{ error }` from a controller is a **200** the client reads as success | `controller-throw-framework-errors` |
217
+
218
+ ## How to Use
219
+
220
+ Read a rule file for the full explanation and examples:
221
+
222
+ ```
223
+ rules/payload-prefetch-mirrors-usequery.md
224
+ rules/orm-transaction-sequential.md
225
+ ```
226
+
227
+ Each contains a short why, an incorrect example, a correct example, and — where one
228
+ exists — a link to the gemi documentation page that covers it.
229
+
230
+ `rules/_sections.md` holds the category metadata; `rules/_template.md` is the shape a
231
+ new rule follows.
@@ -0,0 +1,56 @@
1
+ # Sections
2
+
3
+ The section ID (in parentheses) is the filename prefix used to group rules.
4
+ Rules are sorted by title within a section. Files starting with `_` are not rules.
5
+
6
+ ---
7
+
8
+ ## 1. Server Payload & Waterfalls (payload)
9
+
10
+ **Impact:** CRITICAL
11
+ **Description:** gemi renders on the server and ships a data payload with the HTML. Every read the client discovers only after hydration costs a full round-trip the payload could have carried. This is the largest lever in the app.
12
+
13
+ ## 2. Client Data Fetching (query)
14
+
15
+ **Impact:** CRITICAL
16
+ **Description:** `useQuery` defaults (suspense on, keepPreviousData on, 5s staleTime, focus revalidation off) are opinionated. Fighting them — or not knowing them — produces blank flashes, dead pagination, and duplicate requests.
17
+
18
+ ## 3. Data Access — ORM (orm)
19
+
20
+ **Impact:** HIGH
21
+ **Description:** The ORM is Prisma-typed but gemi-executed, with its own relation-loading strategies, transaction semantics, connection selection, and row-provenance model. Its constraints are not Prisma's.
22
+
23
+ ## 4. Routing & Middleware (routing)
24
+
25
+ **Impact:** HIGH
26
+ **Description:** Routing is class-based: routers declare a routes object, and middleware attaches as a string DSL. This is where a URL, its cache policy, and its auth boundary are all defined.
27
+
28
+ ## 5. Controllers & Request Handling (controller)
29
+
30
+ **Impact:** HIGH
31
+ **Description:** Controllers hold server logic and own the request boundary — validation, authorization, error shape. The client's error handling is a contract with what a controller throws.
32
+
33
+ ## 6. Services, Jobs & Commands (service)
34
+
35
+ **Impact:** HIGH
36
+ **Description:** Long-lived singletons, background work, and one-off ops. Most failures here are invisible in development and appear only in the production build or on a restart.
37
+
38
+ ## 7. Client Components & Navigation (client)
39
+
40
+ **Impact:** MEDIUM
41
+ **Description:** React 19 SSR with hydration. Typed navigation, forms, suspense boundaries declared by route module exports, and data flow that does not go through effects.
42
+
43
+ ## 8. Bundle Size (bundle)
44
+
45
+ **Impact:** MEDIUM
46
+ **Description:** Vite code-splits per route chunk. Client cost is determined by which module graph a route pulls in and where a heavy subtree is mounted.
47
+
48
+ ## 9. Internationalization (i18n)
49
+
50
+ **Impact:** MEDIUM
51
+ **Description:** Two dictionary systems coexist. The newer one is rewritten at build time by a Vite plugin, which constrains how it may be written.
52
+
53
+ ## 10. Testing (testing)
54
+
55
+ **Impact:** MEDIUM
56
+ **Description:** Views mount for real with seeded framework inputs, through `<Page>` from `gemi/testing`. Match whichever runner and naming convention the directory already uses — a test the runner never selects is a test that does not exist.
@@ -0,0 +1,28 @@
1
+ ---
2
+ title: Rule Title Here
3
+ impact: MEDIUM
4
+ impactDescription: Optional description of impact (e.g., "20-50% improvement")
5
+ tags: tag1, tag2
6
+ ---
7
+
8
+ ## Rule Title Here
9
+
10
+ **Impact: MEDIUM (optional impact description)**
11
+
12
+ Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
13
+
14
+ **Incorrect (description of what's wrong):**
15
+
16
+ ```typescript
17
+ // Bad code example here
18
+ const bad = example()
19
+ ```
20
+
21
+ **Correct (description of what's right):**
22
+
23
+ ```typescript
24
+ // Good code example here
25
+ const good = example()
26
+ ```
27
+
28
+ Reference: [Link to documentation or resource](https://example.com)
@@ -0,0 +1,48 @@
1
+ ---
2
+ title: Import UI Primitives By Deep Path
3
+ impact: HIGH
4
+ impactDescription: keeps a component library's dependencies out of routes that do not use it
5
+ tags: bundle, imports, barrel, ui
6
+ ---
7
+
8
+ ## Import UI Primitives By Deep Path
9
+
10
+ A component library where every primitive is its own module — the shadcn/ui shape,
11
+ and the shape most in-repo `packages/ui` workspaces take — should be imported by deep
12
+ path. A route then pulls in exactly the primitives it uses.
13
+
14
+ A barrel import, or adding a barrel that re-exports everything, pulls the whole
15
+ library and its transitive dependencies (for a Radix-based kit, ~25 packages) into
16
+ every route that touches one button. Tree-shaking does not reliably save you here:
17
+ the library is often source-only with no build step, and a single side-effecting
18
+ module in the barrel's graph defeats it.
19
+
20
+ **Incorrect (drags the library in):**
21
+
22
+ ```tsx
23
+ import { Button, Input, Badge } from "@acme/ui";
24
+ ```
25
+
26
+ **Correct (one module each):**
27
+
28
+ ```tsx
29
+ import { Button } from "@acme/ui/components/ui/button";
30
+ import { Input } from "@acme/ui/components/ui/input";
31
+ import { Badge } from "@acme/ui/components/ui/badge";
32
+ import { cn } from "@acme/ui/lib/utils";
33
+ ```
34
+
35
+ **Do not add a barrel** to make the imports shorter. The deep paths are the
36
+ mechanism, not a style preference.
37
+
38
+ Two related notes:
39
+
40
+ - **Check whether your repo has a legacy copy.** A local `app/views/components/ui`
41
+ alongside a shared `packages/ui` means two divergent sets of the same primitives;
42
+ pick the shared one and say so in `CLAUDE.md`.
43
+ - **Keep import paths statically analyzable.** A computed specifier defeats
44
+ tree-shaking and widens what the build has to trace.
45
+
46
+ This matters more under gemi than under a bundler that ships one chunk: each view is
47
+ its own build entry (`preserveEntrySignatures: "strict"`), so a barrel import in one
48
+ view inflates that view's chunk and every chunk that shares its graph.
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: Put Heavy Subtrees Inside the Thing That Unmounts Them
3
+ impact: HIGH
4
+ impactDescription: ~47 kB and two queries off the initial load, measured
5
+ tags: bundle, mounting, radix, queries
6
+ ---
7
+
8
+ ## Put Heavy Subtrees Inside the Thing That Unmounts Them
9
+
10
+ The cheapest work is work that never mounts. **Let an unmounted subtree be the
11
+ gate**: Radix `PopoverContent`, `DialogContent`,
12
+ `DropdownMenuContent` and the tab primitives do not render their children while
13
+ closed. A component placed inside them costs nothing until the user opens the thing.
14
+
15
+ Hoisting a query or a heavy component *above* that boundary "to keep the parent
16
+ tidy" is how it starts running on every page load for a panel nobody opened. In this
17
+ app that cost ~47 kB resolved across two reads, on every load of the customer app.
18
+
19
+ **Incorrect (reads run on every page load to fill a closed popover):**
20
+
21
+ ```tsx
22
+ function ProductPicker() {
23
+ const { data: products } = useQuery("/app/:orgId/products/search", { params });
24
+ const { data: lists } = useQuery("/app/:orgId/lists", { params });
25
+
26
+ return (
27
+ <Popover>
28
+ <PopoverTrigger>Add product</PopoverTrigger>
29
+ <PopoverContent>
30
+ <CatalogSearchPanel products={products} lists={lists} />
31
+ </PopoverContent>
32
+ </Popover>
33
+ );
34
+ }
35
+ ```
36
+
37
+ **Correct (the reads move down, inside the content Radix unmounts):**
38
+
39
+ ```tsx
40
+ function ProductPicker() {
41
+ return (
42
+ <Popover>
43
+ <PopoverTrigger>Add product</PopoverTrigger>
44
+ <PopoverContent>
45
+ <CatalogSearchPanel orgId={orgId} />
46
+ </PopoverContent>
47
+ </Popover>
48
+ );
49
+ }
50
+
51
+ function CatalogSearchPanel({ orgId }: { orgId: string }) {
52
+ const { data: products = [] } = useQuery(/* … */, { suspense: false });
53
+ const { data: lists = [] } = useQuery(/* … */, { suspense: false });
54
+ }
55
+ ```
56
+
57
+ **Why mounting rather than `{ lazy: true }`:** a lazy query does not refetch when
58
+ its variant changes, so a searchable or paginated read breaks under it — see
59
+ `query-lazy-vs-mount-gate`.
60
+
61
+ **For a genuinely heavy module** (a chart library, an editor, a PDF renderer) plain
62
+ `React.lazy` + `Suspense` is the right tool; gemi ships no wrapper of its own for
63
+ it, and none is needed.