@flisk-sh/flisk 1.4.2 → 1.4.5

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 (83) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +119 -43
  3. package/cli/dist/backend-languages/bun.js +5 -0
  4. package/cli/dist/backend-languages/go.js +3 -1
  5. package/cli/dist/cli.js +9 -0
  6. package/cli/dist/commands/completion.js +8 -1
  7. package/cli/dist/commands/config.js +130 -55
  8. package/cli/dist/commands/doctor.js +16 -15
  9. package/cli/dist/commands/logs.js +1 -0
  10. package/cli/dist/commands/project.js +1 -0
  11. package/cli/dist/commands/resource.js +39 -11
  12. package/cli/dist/commands/status.js +18 -9
  13. package/cli/dist/commands/up.js +102 -17
  14. package/cli/dist/frontend-frameworks/next.js +197 -0
  15. package/cli/dist/frontend-frameworks/react.js +81 -12
  16. package/cli/dist/frontend-frameworks/registry.js +5 -4
  17. package/cli/dist/frontend-frameworks/vue.js +82 -12
  18. package/cli/dist/generator/resource-templates.js +2 -4
  19. package/cli/dist/generator/template-engine.js +5 -3
  20. package/cli/dist/utils/cold-preflight.js +3 -2
  21. package/cli/dist/utils/config-explain.js +197 -0
  22. package/cli/dist/utils/demo-reset.js +22 -0
  23. package/cli/dist/utils/discovery-paths.js +7 -1
  24. package/cli/dist/utils/host-port-plan.js +8 -3
  25. package/cli/dist/utils/port-conflict-fix.js +16 -14
  26. package/cli/dist/utils/port-occupant.js +35 -0
  27. package/cli/dist/utils/project-features.js +7 -16
  28. package/cli/dist/utils/project-templates.js +8 -8
  29. package/cli/dist/utils/resource-features.js +5 -5
  30. package/cli/dist/utils/service-manifest.js +1 -0
  31. package/cli/dist/utils/service-urls.js +11 -0
  32. package/cli/dist/utils/startup-report.js +4 -3
  33. package/cli/dist/utils/tar.js +33 -28
  34. package/cli/dist/utils/tilt-process.js +4 -3
  35. package/cli/dist/utils/windows-doctor.js +18 -15
  36. package/cli/dist-bundle/{chunk-IGN5UC3P.js → chunk-4J2FLFUA.js} +14 -7
  37. package/cli/dist-bundle/{chunk-ETKUVKJ2.js → chunk-BMHPPX2Q.js} +10 -10
  38. package/cli/dist-bundle/chunk-BP2M4OJ4.js +2 -0
  39. package/cli/dist-bundle/{chunk-53L2JN7G.js → chunk-CGTWXWBU.js} +1 -1
  40. package/cli/dist-bundle/chunk-GPUJ55MI.js +3 -0
  41. package/cli/dist-bundle/{chunk-5QBYM33Q.js → chunk-LUUJ4JB7.js} +1 -1
  42. package/cli/dist-bundle/chunk-PEZQEZVV.js +17 -0
  43. package/cli/dist-bundle/{chunk-MWOVYIBB.js → chunk-SFOFTVYM.js} +5 -5
  44. package/cli/dist-bundle/{chunk-3XODDLRJ.js → chunk-TIYTGN5R.js} +1 -1
  45. package/cli/dist-bundle/{chunk-YNCBBJ4V.js → chunk-U42XZWXB.js} +1 -1
  46. package/cli/dist-bundle/chunk-ULMQVD7J.js +122 -0
  47. package/cli/dist-bundle/chunk-V36PT2HF.js +24 -0
  48. package/cli/dist-bundle/{chunk-I7TGIK5N.js → chunk-VMVTECCK.js} +1 -1
  49. package/cli/dist-bundle/{chunk-DU2BN46C.js → chunk-Z5QEE4SW.js} +1 -1
  50. package/cli/dist-bundle/cli.js +607 -375
  51. package/cli/dist-bundle/cold-preflight-3BNDOLGS.js +2 -0
  52. package/cli/dist-bundle/{demo-stage-OHNTP2UJ.js → demo-stage-G4W23BXW.js} +1 -1
  53. package/cli/dist-bundle/{extension-fetch-VEFSCG4B.js → extension-fetch-KIPYDIRM.js} +1 -1
  54. package/cli/dist-bundle/{open-XROJ6YEY.js → open-HQFT6WDC.js} +1 -1
  55. package/cli/dist-bundle/project-JZO2CPZZ.js +2 -0
  56. package/cli/dist-bundle/{project-templates-EYSQG2EO.js → project-templates-IMJ3OX5I.js} +1 -1
  57. package/cli/dist-bundle/{services-BIMF6MVU.js → services-GLFKZBBZ.js} +1 -1
  58. package/cli/dist-bundle/{tilt-port-owner-ZY5M2JC4.js → tilt-port-owner-PJN4MPYE.js} +1 -1
  59. package/cli/package.json +1 -1
  60. package/package.json +1 -1
  61. package/runtime/engine/topologies/platform/docker/compose/compose.star +40 -4
  62. package/runtime/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +6 -0
  63. package/runtime/engine/topologies/platform/docker/dockerfile/next_dockerfile.star +83 -0
  64. package/runtime/engine/topologies/platform/docker/index.star +4 -0
  65. package/runtime/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +7 -0
  66. package/runtime/engine/topologies/platform/docker/networking/traefik.star +85 -46
  67. package/runtime/engine/topologies/tilt/common/utils_output.star +1 -0
  68. package/runtime/engine/topologies/tilt/generators/validators.star +1 -1
  69. package/runtime/engine/topologies/tilt/generators/vite/frontend.star +2 -2
  70. package/runtime/engine/topologies/tilt/generators/vite/generators.star +3 -0
  71. package/runtime/engine/topologies/tilt/manifest/constants.star +3 -0
  72. package/runtime/engine/topologies/tilt/manifest/parser.star +9 -3
  73. package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +8 -3
  74. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +50 -24
  75. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/env.star +11 -2
  76. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +23 -9
  77. package/cli/dist-bundle/chunk-43XWL2PB.js +0 -61
  78. package/cli/dist-bundle/chunk-5F7YP5IR.js +0 -17
  79. package/cli/dist-bundle/chunk-K6SOCO6V.js +0 -3
  80. package/cli/dist-bundle/chunk-YHYRGWUH.js +0 -24
  81. package/cli/dist-bundle/chunk-YL56J2WX.js +0 -2
  82. package/cli/dist-bundle/cold-preflight-ZOPR7PWJ.js +0 -2
  83. package/cli/dist-bundle/project-7PTIOHGF.js +0 -2
@@ -0,0 +1,2 @@
1
+ import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
+ import{A as d,B as e,C as f,D as g,E as h,F as i,G as j,H as k,x as a,y as b,z as c}from"./chunk-ULMQVD7J.js";import"./chunk-SFOFTVYM.js";import"./chunk-4J2FLFUA.js";import"./chunk-GPUJ55MI.js";import"./chunk-3J6XI3NX.js";import"./chunk-V36PT2HF.js";import"./chunk-TIYTGN5R.js";import"./chunk-CGTWXWBU.js";import"./chunk-PEZQEZVV.js";import"./chunk-U42XZWXB.js";import"./chunk-KJZUBHLO.js";export{c as MACHINE_CHECK_IDS,a as RUNTIME_CHECK_IDS,b as SCAFFOLD_CHECK_IDS,i as assertMachineReadyOrExit,j as detectRuntimePrerequisites,h as formatColdPreflight,k as formatRuntimeNextSteps,d as nodeTooOld,e as requireVerifiedDockerVersions,g as runColdPreflight,f as usesPrisma};
@@ -1,5 +1,5 @@
1
1
  import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
- import{b as R}from"./chunk-5QBYM33Q.js";import{b as c}from"./chunk-YNCBBJ4V.js";import"./chunk-KJZUBHLO.js";var v=12,_=24,F={plain:e=>e,bold:c.bold,dim:c.gray,ok:c.green,warn:c.yellow,err:c.red},S={pending:"\u25CB",active:"\u25D0",done:"\u2714",failed:"\u2716"},j={pending:"dim",active:"warn",done:"ok",failed:"err"};function o(e){return R(String(e)).replace(/[\n\r\t]/g," ")}function g(e){if(!Number.isFinite(e)||e<0)return"0.0s";if(e<1e4)return`${(e/1e3).toFixed(1)}s`;if(e<6e4)return`${Math.round(e/1e3)}s`;let n=Math.floor(e/6e4),i=Math.round(e%6e4/1e3);return`${n}m ${i}s`}function $(e){return e.reduce((n,i)=>n+i.text.length,0)}function k(e,n){if(n<=0)return[];let i=[],l=0;for(let s of e){if(l>=n)break;let t=n-l;if(s.text.length<=t){i.push(s),l+=s.text.length;continue}i.push({...s,text:`${s.text.slice(0,Math.max(0,t-1))}\u2026`}),l=n}return i}function w(e,n){let i=k(e,n),l=n-$(i);return l<=0?i:[...i,{text:" ".repeat(l),style:"plain"}]}function M(e,n,i){let l=$(n),s=i-l-1;if(s<4)return w(k([...e,...n],i),i);let t=k(e,s),r=i-$(t)-l;return[...t,{text:" ".repeat(Math.max(1,r)),style:"plain"},...n]}function b(e){return[{text:` ${"\u2500".repeat(Math.max(0,e-2))}`,style:"dim"}]}function P(e,n){return M([{text:" ",style:"plain"},{text:S[e.status],style:j[e.status]},{text:" ",style:"plain"},{text:o(e.label),style:e.status==="pending"?"dim":"plain"}],e.elapsedMs===void 0?[]:[{text:g(e.elapsedMs),style:"dim"}],n)}function Y(e){let n=Math.max(0,e.servicesTotal);if(n===0)return"no services found";let l=[`${Math.min(Math.max(0,e.servicesReady),n)} of ${n} services ready`];return e.servicesBuilding>0&&l.push(`${e.servicesBuilding} building`),l.join(" \xB7 ")}function E(e,n=80){let i=Number.isFinite(n)&&n>0?n:80,l=Math.max(_,Math.min(i-1,78)),s=[],t=a=>s.push(w(a,l)),r=()=>t([]);if(e.done){t(M([{text:" ",style:"plain"},{text:"\u2714",style:"ok"},{text:" Your stack is running",style:"ok"}],[{text:g(e.elapsedMs),style:"dim"}],l)),t([{text:" ",style:"plain"},{text:o(e.subtitle),style:"dim"}]),r();for(let a of e.done.urls.slice(0,2))t([{text:" ",style:"plain"},{text:o(a),style:"plain"}]);r(),t(b(l)),t([{text:" ",style:"plain"},{text:o(e.done.summary),style:"plain"}]),t([{text:" ",style:"plain"},{text:o(e.done.hint),style:"dim"}]),r(),t([{text:" ",style:"plain"},{text:`flisk ${o(e.done.version)}`,style:"dim"}])}else if(e.failure){let a=e.phases.find(p=>p.status==="failed");t(M([{text:" ",style:"plain"},{text:"\u2716",style:"err"},{text:" ",style:"plain"},{text:o(a?.label??"The demo stopped"),style:"err"}],[{text:g(e.elapsedMs),style:"dim"}],l)),r(),t([{text:" ",style:"plain"},{text:o(e.failure.message),style:"plain"}]),e.failure.fix&&t([{text:" ",style:"plain"},{text:o(e.failure.fix),style:"dim"}]),r(),t(b(l)),e.detail&&t([{text:" ",style:"plain"},{text:o(e.detail),style:"dim"}]),e.failure.logPath&&t([{text:" ",style:"plain"},{text:`log: ${o(e.failure.logPath)}`,style:"dim"}])}else{t([{text:" ",style:"plain"},{text:o(e.title),style:"bold"}]),t([{text:" ",style:"plain"},{text:o(e.subtitle),style:"dim"}]),r();let a=e.phases.slice(0,4);for(let p of a)t(P(p,l));for(let p=a.length;p<4;p+=1)r();r(),t(b(l)),t([{text:" ",style:"plain"},{text:o(e.detail??""),style:"dim"}]),t(M([{text:" ",style:"plain"},{text:Y(e),style:"plain"}],[{text:g(e.elapsedMs),style:"dim"}],l))}for(;s.length<v;)s.push(w([],l));return s.slice(0,v)}function A(e){return e.map(n=>n.map(i=>i.text).join(""))}function D(e){return e.map(n=>F[n.style](n.text)).join("")}function H(e,n=process.stderr,i=Date.now){let l=n.isTTY===!0,s=i(),t={...e},r=!1,a="",p="",f,T=()=>{let u=!!(t.done||t.failure);return{...t,elapsedMs:u?t.elapsedMs:i()-s}},x=()=>{let u=T(),m=E(u,n.columns??80),y=A(m);if(!l){let h=y.join(`
2
+ import{b as R}from"./chunk-LUUJ4JB7.js";import{b as c}from"./chunk-U42XZWXB.js";import"./chunk-KJZUBHLO.js";var v=12,_=24,F={plain:e=>e,bold:c.bold,dim:c.gray,ok:c.green,warn:c.yellow,err:c.red},S={pending:"\u25CB",active:"\u25D0",done:"\u2714",failed:"\u2716"},j={pending:"dim",active:"warn",done:"ok",failed:"err"};function o(e){return R(String(e)).replace(/[\n\r\t]/g," ")}function g(e){if(!Number.isFinite(e)||e<0)return"0.0s";if(e<1e4)return`${(e/1e3).toFixed(1)}s`;if(e<6e4)return`${Math.round(e/1e3)}s`;let n=Math.floor(e/6e4),i=Math.round(e%6e4/1e3);return`${n}m ${i}s`}function $(e){return e.reduce((n,i)=>n+i.text.length,0)}function k(e,n){if(n<=0)return[];let i=[],l=0;for(let s of e){if(l>=n)break;let t=n-l;if(s.text.length<=t){i.push(s),l+=s.text.length;continue}i.push({...s,text:`${s.text.slice(0,Math.max(0,t-1))}\u2026`}),l=n}return i}function w(e,n){let i=k(e,n),l=n-$(i);return l<=0?i:[...i,{text:" ".repeat(l),style:"plain"}]}function M(e,n,i){let l=$(n),s=i-l-1;if(s<4)return w(k([...e,...n],i),i);let t=k(e,s),r=i-$(t)-l;return[...t,{text:" ".repeat(Math.max(1,r)),style:"plain"},...n]}function b(e){return[{text:` ${"\u2500".repeat(Math.max(0,e-2))}`,style:"dim"}]}function P(e,n){return M([{text:" ",style:"plain"},{text:S[e.status],style:j[e.status]},{text:" ",style:"plain"},{text:o(e.label),style:e.status==="pending"?"dim":"plain"}],e.elapsedMs===void 0?[]:[{text:g(e.elapsedMs),style:"dim"}],n)}function Y(e){let n=Math.max(0,e.servicesTotal);if(n===0)return"no services found";let l=[`${Math.min(Math.max(0,e.servicesReady),n)} of ${n} services ready`];return e.servicesBuilding>0&&l.push(`${e.servicesBuilding} building`),l.join(" \xB7 ")}function E(e,n=80){let i=Number.isFinite(n)&&n>0?n:80,l=Math.max(_,Math.min(i-1,78)),s=[],t=a=>s.push(w(a,l)),r=()=>t([]);if(e.done){t(M([{text:" ",style:"plain"},{text:"\u2714",style:"ok"},{text:" Your stack is running",style:"ok"}],[{text:g(e.elapsedMs),style:"dim"}],l)),t([{text:" ",style:"plain"},{text:o(e.subtitle),style:"dim"}]),r();for(let a of e.done.urls.slice(0,2))t([{text:" ",style:"plain"},{text:o(a),style:"plain"}]);r(),t(b(l)),t([{text:" ",style:"plain"},{text:o(e.done.summary),style:"plain"}]),t([{text:" ",style:"plain"},{text:o(e.done.hint),style:"dim"}]),r(),t([{text:" ",style:"plain"},{text:`flisk ${o(e.done.version)}`,style:"dim"}])}else if(e.failure){let a=e.phases.find(p=>p.status==="failed");t(M([{text:" ",style:"plain"},{text:"\u2716",style:"err"},{text:" ",style:"plain"},{text:o(a?.label??"The demo stopped"),style:"err"}],[{text:g(e.elapsedMs),style:"dim"}],l)),r(),t([{text:" ",style:"plain"},{text:o(e.failure.message),style:"plain"}]),e.failure.fix&&t([{text:" ",style:"plain"},{text:o(e.failure.fix),style:"dim"}]),r(),t(b(l)),e.detail&&t([{text:" ",style:"plain"},{text:o(e.detail),style:"dim"}]),e.failure.logPath&&t([{text:" ",style:"plain"},{text:`log: ${o(e.failure.logPath)}`,style:"dim"}])}else{t([{text:" ",style:"plain"},{text:o(e.title),style:"bold"}]),t([{text:" ",style:"plain"},{text:o(e.subtitle),style:"dim"}]),r();let a=e.phases.slice(0,4);for(let p of a)t(P(p,l));for(let p=a.length;p<4;p+=1)r();r(),t(b(l)),t([{text:" ",style:"plain"},{text:o(e.detail??""),style:"dim"}]),t(M([{text:" ",style:"plain"},{text:Y(e),style:"plain"}],[{text:g(e.elapsedMs),style:"dim"}],l))}for(;s.length<v;)s.push(w([],l));return s.slice(0,v)}function A(e){return e.map(n=>n.map(i=>i.text).join(""))}function D(e){return e.map(n=>F[n.style](n.text)).join("")}function H(e,n=process.stderr,i=Date.now){let l=n.isTTY===!0,s=i(),t={...e},r=!1,a="",p="",f,T=()=>{let u=!!(t.done||t.failure);return{...t,elapsedMs:u?t.elapsedMs:i()-s}},x=()=>{let u=T(),m=E(u,n.columns??80),y=A(m);if(!l){let h=y.join(`
3
3
  `);if(h===p)return;p=h,n.write(`${y.filter(L=>L.trim()!=="").join(`
4
4
  `)}
5
5
  `);return}let d=y.join(`
@@ -1,2 +1,2 @@
1
1
  import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
- import{b as a,c as b,d as c,e as d}from"./chunk-K6SOCO6V.js";import"./chunk-KJZUBHLO.js";export{a as applyPremiumOverlay,c as hasDddLicense,d as hasSablierLicense,b as hasVerdaccioLicense};
2
+ import{b as a,c as b,d as c,e as d}from"./chunk-GPUJ55MI.js";import"./chunk-KJZUBHLO.js";export{a as applyPremiumOverlay,c as hasDddLicense,d as hasSablierLicense,b as hasVerdaccioLicense};
@@ -1,2 +1,2 @@
1
1
  import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
- import{d as a,e as b,f as c,g as d,h as e,i as f}from"./chunk-DU2BN46C.js";import"./chunk-MINZ25AC.js";import"./chunk-YHYRGWUH.js";import"./chunk-3XODDLRJ.js";import"./chunk-53L2JN7G.js";import"./chunk-5F7YP5IR.js";import"./chunk-YNCBBJ4V.js";import"./chunk-KJZUBHLO.js";export{d as findReadyFrontend,b as isFrontendReady,a as listFrontendTargets,f as openCommand,e as openFirstFrontend,c as pickReadyFrontend};
2
+ import{d as a,e as b,f as c,g as d,h as e,i as f}from"./chunk-Z5QEE4SW.js";import"./chunk-MINZ25AC.js";import"./chunk-V36PT2HF.js";import"./chunk-TIYTGN5R.js";import"./chunk-CGTWXWBU.js";import"./chunk-PEZQEZVV.js";import"./chunk-U42XZWXB.js";import"./chunk-KJZUBHLO.js";export{d as findReadyFrontend,b as isFrontendReady,a as listFrontendTargets,f as openCommand,e as openFirstFrontend,c as pickReadyFrontend};
@@ -0,0 +1,2 @@
1
+ import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
+ import{g as a,h as b,i as c,j as d}from"./chunk-BMHPPX2Q.js";import"./chunk-BP2M4OJ4.js";import"./chunk-SFOFTVYM.js";import"./chunk-GPUJ55MI.js";import"./chunk-CGTWXWBU.js";import"./chunk-PEZQEZVV.js";import"./chunk-U42XZWXB.js";import"./chunk-KJZUBHLO.js";export{c as cloneProjectTemplate,a as enableDiscoveredStacks,d as projectCommand,b as templateTargetDir};
@@ -1,2 +1,2 @@
1
1
  import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
- import{a,b}from"./chunk-YL56J2WX.js";import"./chunk-KJZUBHLO.js";export{a as PROJECT_TEMPLATES,b as bareTemplateArg};
2
+ import{a,b}from"./chunk-BP2M4OJ4.js";import"./chunk-KJZUBHLO.js";export{a as PROJECT_TEMPLATES,b as bareTemplateArg};
@@ -1,2 +1,2 @@
1
1
  import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
- import{ca as a,da as b,ea as c,fa as d,ga as e,ha as f,ia as g,ja as h,ka as i,la as j,ma as k,na as l,oa as m,pa as n,qa as o}from"./chunk-5F7YP5IR.js";import"./chunk-YNCBBJ4V.js";import"./chunk-KJZUBHLO.js";export{m as clearMetadataCache,f as discoverResources,e as discoverResourcesFromRoot,g as discoverResourcesStrict,c as discoverResourcesWithProblems,a as discoverServiceManifestPaths,h as discoverStackNames,j as discoverStacks,i as getAllStacks,n as getResourceMetadata,k as getResourcesForStack,o as getStackMetadata,b as resetPrintedServiceWarnings,l as stackExists,d as warnAboutSkippedFiles};
2
+ import{da as a,ea as b,fa as c,ga as d,ha as e,ia as f,ja as g,ka as h,la as i,ma as j,na as k,oa as l,pa as m,qa as n,ra as o}from"./chunk-PEZQEZVV.js";import"./chunk-U42XZWXB.js";import"./chunk-KJZUBHLO.js";export{m as clearMetadataCache,f as discoverResources,e as discoverResourcesFromRoot,g as discoverResourcesStrict,c as discoverResourcesWithProblems,a as discoverServiceManifestPaths,h as discoverStackNames,j as discoverStacks,i as getAllStacks,n as getResourceMetadata,k as getResourcesForStack,o as getStackMetadata,b as resetPrintedServiceWarnings,l as stackExists,d as warnAboutSkippedFiles};
@@ -1,2 +1,2 @@
1
1
  import{createRequire as __fliskRequire}from'node:module';const require=__fliskRequire(import.meta.url);
2
- import{a,b,c}from"./chunk-I7TGIK5N.js";import"./chunk-IGN5UC3P.js";import"./chunk-3XODDLRJ.js";import"./chunk-53L2JN7G.js";import"./chunk-5F7YP5IR.js";import"./chunk-YNCBBJ4V.js";import"./chunk-KJZUBHLO.js";export{c as classifyTiltPorts,a as findTiltPortOwner,b as isForeignTilt};
2
+ import{a,b,c}from"./chunk-VMVTECCK.js";import"./chunk-4J2FLFUA.js";import"./chunk-TIYTGN5R.js";import"./chunk-CGTWXWBU.js";import"./chunk-PEZQEZVV.js";import"./chunk-U42XZWXB.js";import"./chunk-KJZUBHLO.js";export{c as classifyTiltPorts,a as findTiltPortOwner,b as isForeignTilt};
package/cli/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "flisk-cli-internal",
3
3
  "private": true,
4
- "version": "1.4.2",
4
+ "version": "1.4.5",
5
5
  "description": "Flisk CLI — start services on your machine.",
6
6
  "keywords": [
7
7
  "flisk",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flisk-sh/flisk",
3
- "version": "1.4.2",
3
+ "version": "1.4.5",
4
4
  "description": "Flisk CLI — start services on your machine.",
5
5
  "homepage": "https://flisk.sh",
6
6
  "repository": {
@@ -77,6 +77,19 @@ def _compute_env_file_rel_path(resource_path, full_resource_path, env_filename):
77
77
  # 🚦 FRONTEND / BACKEND ENTRIES
78
78
  # =============================================================================
79
79
 
80
+ # The memory anchors a frontend service merges. Next.js gets its own (next dev needs far more than the 64M an nginx SPA fits in,
81
+ # and the OOM kill exits 0, so the container looks like a clean exit). The engine finds frontend entries by these strings.
82
+ _FRONTEND_MEMORY_ANCHORS = ["<<: *frontend-memory-limit", "<<: *next-memory-limit"]
83
+
84
+
85
+ def _has_frontend_memory_anchor(entry):
86
+ """True when a compose entry merges one of the frontend memory anchors."""
87
+ for anchor in _FRONTEND_MEMORY_ANCHORS:
88
+ if anchor in entry:
89
+ return True
90
+ return False
91
+
92
+
80
93
  def generate_frontend_compose(resource_path, resource_name, res, manifest=None):
81
94
  """Generate frontend docker-compose entry with Traefik routing."""
82
95
  res_name = res['name']
@@ -91,6 +104,10 @@ def generate_frontend_compose(resource_path, resource_name, res, manifest=None):
91
104
  # healthcheck probe a port nothing listens on, which left the container marked
92
105
  # unhealthy -- and Traefik's Docker provider skips unhealthy containers, so the
93
106
  # router was never registered and every request 404'd.
107
+ # Next.js is served by its own Node server (no nginx), on the manifest port, under its basePath.
108
+ is_next = bool(manifest) and manifest.get('framework') == 'next'
109
+ if is_next:
110
+ use_nginx = False
94
111
  port = NGINX_CONTAINER_PORT if use_nginx else reject_unsafe_value(manifest.get('port', BASE_PORT_FRONTEND), 'port')
95
112
 
96
113
  # Base path for routing on {project}.localhost. Shared with the Vite config and
@@ -129,11 +146,19 @@ def generate_frontend_compose(resource_path, resource_name, res, manifest=None):
129
146
  manifest=manifest,
130
147
  )
131
148
 
149
+ memory_anchor = "<<: *next-memory-limit" if is_next else "<<: *frontend-memory-limit"
150
+ # An nginx frontend answers at /. Next.js answers at its basePath (with a trailing slash, see next.config.mjs), and the image
151
+ # has curl, not wget.
152
+ if is_next:
153
+ health_test = '["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:{}{}/"]'.format(port, base_path)
154
+ else:
155
+ health_test = '["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:{}"]'.format(port)
156
+
132
157
  return """ {res_name}:
133
158
  image: {image_name}
134
159
  init: true
135
160
  {build_config}
136
- <<: *frontend-memory-limit
161
+ {memory_anchor}
137
162
  env_file:
138
163
  - {env_file_rel_path}
139
164
  environment:
@@ -144,9 +169,11 @@ def generate_frontend_compose(resource_path, resource_name, res, manifest=None):
144
169
  - {network_traefik_public}
145
170
  - {network_backend}
146
171
  healthcheck:
147
- test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:{port}"]
172
+ test: {health_test}
148
173
  {healthcheck_timing}""".format(
174
+ health_test=health_test,
149
175
  res_name=res_name,
176
+ memory_anchor=memory_anchor,
150
177
  image_name=image_name,
151
178
  build_config=build_config,
152
179
  env_file_rel_path=env_file_rel_path,
@@ -439,7 +466,7 @@ def _generate_port_anchors(resource_entries):
439
466
  sdk_port = int(port_str)
440
467
  break
441
468
  # Check if this is a frontend (has frontend-memory-limit but not SDK)
442
- elif "<<: *frontend-memory-limit" in entry and not ("-sdk:" in entry or entry.strip().startswith("identity-sdk:")):
469
+ elif _has_frontend_memory_anchor(entry) and not ("-sdk:" in entry or entry.strip().startswith("identity-sdk:")):
443
470
  has_frontend = True
444
471
  # Extract frontend port from entry
445
472
  if "loadbalancer.server.port=" in entry:
@@ -452,7 +479,7 @@ def _generate_port_anchors(resource_entries):
452
479
  break
453
480
  # Check if this is a backend (has a port, not frontend, not SDK)
454
481
  # Backend entries have "healthcheck:" section with backend port
455
- elif "healthcheck:" in entry and "<<: *frontend-memory-limit" not in entry and not ("-sdk:" in entry or entry.strip().startswith("identity-sdk:")):
482
+ elif "healthcheck:" in entry and not _has_frontend_memory_anchor(entry) and not ("-sdk:" in entry or entry.strip().startswith("identity-sdk:")):
456
483
  has_backend = True
457
484
  # Extract backend port from entry
458
485
  if "loadbalancer.server.port=" in entry:
@@ -522,6 +549,15 @@ x-frontend-memory-limit: &frontend-memory-limit
522
549
  reservations:
523
550
  memory: 32M
524
551
 
552
+ x-next-memory-limit: &next-memory-limit
553
+ deploy:
554
+ resources:
555
+ limits:
556
+ memory: 1G
557
+ cpus: '1.0'
558
+ reservations:
559
+ memory: 256M
560
+
525
561
  services:
526
562
  """.format(port_anchors=port_anchors)
527
563
 
@@ -136,6 +136,7 @@ EXPOSE {port}
136
136
  CMD ["node", "src/index.js"]
137
137
  """
138
138
 
139
+
139
140
  # The watcher the Go development target runs. It compiles a change into a staging binary while the running app keeps serving, and stops
140
141
  # and swaps the app only when that build succeeds. Air was tried first and stops the app as soon as it sees a change (engine.go `start`
141
142
  # calls stopBin before buildRun), so the service stayed down for the whole compile; a Traefik health check landing in that gap kept the
@@ -247,6 +248,11 @@ def generate_language_dockerfile(res_path, language, port = BASE_PORT_BACKEND, d
247
248
  )
248
249
 
249
250
 
251
+ def is_next_frontend(manifest):
252
+ """True for a frontend whose `framework` is `next`: it runs on Node, not as a Vite build behind nginx."""
253
+ return bool(manifest) and manifest.get("appType") == "frontend" and manifest.get("framework") == "next"
254
+
255
+
250
256
  def is_provider_owned_language(manifest):
251
257
  """True when the manifest selects a provider-owned language (a missing language means Bun)."""
252
258
  if not manifest:
@@ -0,0 +1,83 @@
1
+ # Copyright (c) 2026 Flisk Landscape contributors
2
+ # SPDX-License-Identifier: MIT
3
+ # Dockerfile for a Next.js frontend (`framework: next`). Node image, not the Bun + nginx SPA image; see
4
+ # language_dockerfile.star for the other provider-owned runtimes.
5
+
6
+ load('../build/golden_image_constants.star', 'get_layer_reference')
7
+
8
+ BASE_PORT_FRONTEND = 3000
9
+
10
+ # Same RUN line as the backend templates in language_dockerfile.star (Starlark does not export underscored names).
11
+ _NEXT_CURL = """RUN apt-get update && apt-get install -y --no-install-recommends curl \\
12
+ && rm -rf /var/lib/apt/lists/*
13
+ """
14
+
15
+ # Next.js frontend (`framework: next`): a Node image, not the Bun + nginx SPA image. `development` runs `next dev` (what Tilt builds, with
16
+ # the sources live-updated into it); `production` is the standalone server `next build` writes. The engine's env file becomes `.env`,
17
+ # which Next loads in both modes, so next.config.mjs sees FLISK_BASE_PATH and the browser bundle gets NEXT_PUBLIC_API_URL.
18
+ # The Node runtime comes from the approved golden layer `l4-backend-node` when the golden image is enabled (the same
19
+ # `use_golden` switch the Bun generators use); otherwise it is the upstream node image.
20
+ # The `production` stage is the last one, so a plain `docker build` of the file also produces it.
21
+ _BASE_ARG = "ARG BASE_IMAGE="
22
+
23
+ _NEXT = """# Generated by Flisk for a Next.js frontend. Do not edit; change service.json instead.
24
+ # The default is the upstream node image, pinned by digest like the backend Node template; the golden path replaces it.
25
+ ARG BASE_IMAGE=node:22-slim@sha256:c3de60bf2f9dd0ac6370e6117950ff62d6e339527e7472301c9c78a017978392
26
+ FROM ${{BASE_IMAGE}} AS base
27
+ ENV NEXT_TELEMETRY_DISABLED=1
28
+ # The golden runtime runs as a non-root user; install and build as root, then drop to {run_user} for the running stages.
29
+ USER root
30
+ WORKDIR /app/{res_path}
31
+
32
+ {curl}
33
+ COPY {res_path}/package.json {res_path}/package-lock.jso[n] ./
34
+ RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi
35
+
36
+ COPY {res_path}/.autogenerated/.env.frontend.autogenerated ./.env
37
+ COPY {res_path}/next.config.mjs ./next.config.mjs
38
+ COPY {res_path}/tsconfig.json ./tsconfig.json
39
+ COPY {res_path}/public ./public
40
+ COPY {res_path}/src ./src
41
+
42
+ FROM base AS development
43
+ # WORKDIR is created by root, and next dev writes .next (and next-env.d.ts) inside it as {run_user}:
44
+ # without this the dev server dies with EACCES: mkdir '.next'. Give the run user the app folder.
45
+ ENV PORT={port} HOSTNAME=0.0.0.0
46
+ RUN chown -R {run_user}:{run_user} /app/{res_path}
47
+ USER {run_user}
48
+ EXPOSE {port}
49
+ CMD ["npx", "next", "dev", "--hostname", "0.0.0.0", "--port", "{port}"]
50
+
51
+ FROM base AS build
52
+ RUN npx next build
53
+
54
+ FROM ${{BASE_IMAGE}} AS production
55
+ ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT={port} HOSTNAME=0.0.0.0
56
+ WORKDIR /app/{res_path}
57
+ {curl}
58
+ COPY --from=build --chown={run_user}:{run_user} /app/{res_path}/.next/standalone ./
59
+ COPY --from=build --chown={run_user}:{run_user} /app/{res_path}/.next/static ./.next/static
60
+ COPY --from=build --chown={run_user}:{run_user} /app/{res_path}/public ./public
61
+ USER {run_user}
62
+ EXPOSE {port}
63
+ CMD ["node", "server.js"]
64
+ """
65
+
66
+ def generate_next_dockerfile(res_path, port = BASE_PORT_FRONTEND, use_golden = True):
67
+ """Dockerfile text for a Next.js frontend: `development` (next dev), `build`, and `production` (standalone server).
68
+
69
+ With `use_golden` the base is the approved golden `l4-backend-node` layer (alpine, curl already present, non-root `app`);
70
+ without it, the upstream node image (debian slim, which needs curl installed, non-root `node`).
71
+ """
72
+ if use_golden:
73
+ run_user = 'app'
74
+ curl = ''
75
+ else:
76
+ run_user = 'node'
77
+ curl = _NEXT_CURL
78
+ text = _NEXT.format(res_path = res_path, port = port, run_user = run_user, curl = curl)
79
+ if use_golden:
80
+ head, _, rest = text.partition(_BASE_ARG)
81
+ _, _, tail = rest.partition("\n")
82
+ text = head + _BASE_ARG + get_layer_reference('l4-backend-node') + "\n" + tail
83
+ return text
@@ -6,11 +6,13 @@ load(
6
6
  "generate_app_dockerfile",
7
7
  "generate_migrator_dockerfile",
8
8
  )
9
+ load("./dockerfile/next_dockerfile.star", "generate_next_dockerfile")
9
10
  load(
10
11
  "./dockerfile/language_dockerfile.star",
11
12
  "generate_language_dockerfile",
12
13
  "is_provider_owned_language",
13
14
  "go_live_reload_enabled",
15
+ "is_next_frontend",
14
16
  "LANGUAGE_REQUIRED_FILES",
15
17
  )
16
18
  load("./config/dockerignore.star", "generate_dockerignore")
@@ -30,6 +32,8 @@ Docker = struct(
30
32
  backend_language = generate_language_dockerfile,
31
33
  is_provider_owned_language = is_provider_owned_language,
32
34
  go_live_reload = go_live_reload_enabled,
35
+ next_frontend = generate_next_dockerfile,
36
+ is_next_frontend = is_next_frontend,
33
37
  language_required_files = LANGUAGE_REQUIRED_FILES,
34
38
  generate_dockerignore = generate_dockerignore,
35
39
  frontend_compose = generate_frontend_compose,
@@ -19,6 +19,7 @@ load(
19
19
  load(
20
20
  './constants.star',
21
21
  'APP_BUN_STORE_DIR',
22
+ 'APP_DIR',
22
23
  'APP_GENERATED_CLIENT_DIR',
23
24
  'APP_NODE_MODULES_BIN_DIR',
24
25
  'APP_NODE_MODULES_DIR',
@@ -46,10 +47,16 @@ def prisma_dir_and_permissions():
46
47
  Create directories that Prisma writes to at runtime and ensure the `bun` user owns
47
48
  them. Handles both the standard `node_modules/.prisma/client` path and custom
48
49
  output paths like `../src/generated` or `../generated-client`.
50
+
51
+ `/app` itself is owned by `bun` too: `migrate.sh` runs `prisma generate` as `bun`,
52
+ and a generator output such as `../generated/prisma` (the `flisk resource
53
+ --feature prisma` scaffold) makes Prisma create a new directory directly under
54
+ `/app`, which fails with EACCES while root owns it.
49
55
  """
50
56
  return (
51
57
  "# Ensure Prisma can write generated files including custom output paths\n"
52
58
  + "RUN mkdir -p " + APP_NODE_MODULES_DIR + " " + APP_SRC_DIR + " " + APP_GENERATED_CLIENT_DIR
59
+ + " && chown " + BUN_USER + ":" + BUN_USER + " " + APP_DIR
53
60
  + " && chown -R " + BUN_USER + ":" + BUN_USER
54
61
  + " " + APP_PRISMA_DIR + " " + APP_NODE_MODULES_DIR + " " + APP_SRC_DIR + " " + APP_GENERATED_CLIENT_DIR
55
62
  + " " + PRISMA_CONFIG_ABS_PATH + " || true\n"
@@ -38,6 +38,31 @@ load("./sablier_container_cycle.star",
38
38
  "sablier_container_labels",
39
39
  )
40
40
  load("../compose/unsafe_value.star", "reject_unsafe_value")
41
+ load("../constants.star", "PROJECT_NAME_HYPHEN")
42
+
43
+
44
+ def _project_slug():
45
+ """The project name with every character except letters and digits written as `_` plus its six-digit hex code.
46
+
47
+ The result has no `-` in it, so the first `-` in a scoped name always separates the project from the resource. That
48
+ keeps the mapping one-to-one: `team` + `app-api` and `team-app` + `api` get different names, and so do `a-b` and `a_b`.
49
+ """
50
+ return "".join([ch if ch.isalnum() else "_" + _hex6(ord(ch)) for ch in PROJECT_NAME_HYPHEN.elems()])
51
+
52
+
53
+ def _hex6(code):
54
+ """`code` in hex, zero-padded to six digits so every escape has the same width."""
55
+ return ("000000" + "%x" % code)[-6:]
56
+
57
+
58
+ def traefik_scoped_name(name):
59
+ """Router, middleware and service name for `name`, prefixed with this Flisk project.
60
+
61
+ One Traefik instance serves every Flisk project on the machine, and it keys routers, middlewares and services by name
62
+ alone. Two projects that name a resource the same (`api`, say) would define the same router twice, and Traefik drops both.
63
+ The project prefix keeps them apart.
64
+ """
65
+ return _project_slug() + "-" + name
41
66
 
42
67
 
43
68
  def get_frontend_traefik_labels(res_name, domain, base_path, port, traefik_host=None, manifest=None):
@@ -55,8 +80,9 @@ def get_frontend_traefik_labels(res_name, domain, base_path, port, traefik_host=
55
80
  TRAEFIK_FRONTEND_ENABLE_HTTP,
56
81
  TRAEFIK_FRONTEND_ENABLE_HTTPS,
57
82
  )
58
- middleware_name = res_name + TRAEFIK_MIDDLEWARE_SUFFIX
59
-
83
+ router_name = traefik_scoped_name(res_name)
84
+ middleware_name = router_name + TRAEFIK_MIDDLEWARE_SUFFIX
85
+
60
86
  # Check if maintenance feature is enabled
61
87
  features = manifest.get('featuresEnabled', []) if manifest else []
62
88
  maintenance_middleware = ""
@@ -66,20 +92,31 @@ def get_frontend_traefik_labels(res_name, domain, base_path, port, traefik_host=
66
92
  # On-demand scaling: attach the Sablier middleware when opted in via the manifest
67
93
  sablier_middleware, sablier_enabled = sablier_middleware_suffix(manifest, res_name)
68
94
 
69
- labels = """ - "{traefik_enable_label}"
70
- - 'traefik.http.routers.{res_name}.rule={frontend_route_rule}'
71
- - "traefik.http.routers.{res_name}.entrypoints={frontend_entrypoints}"
72
- - "traefik.http.routers.{res_name}.priority={router_priority}"
73
- - "traefik.http.routers.{res_name}.middlewares={middleware_name}{maintenance_middleware}{sablier_middleware}"
95
+ # Next.js serves under its own basePath, so the prefix must reach it: the route keeps /<name> and adds no stripprefix middleware.
96
+ strips_prefix = not manifest or manifest.get('framework') != 'next'
97
+ if strips_prefix:
98
+ middlewares = middleware_name + maintenance_middleware + sablier_middleware
99
+ middleware_labels = """ - "traefik.http.routers.{router_name}.middlewares={middlewares}"
74
100
  - "traefik.http.middlewares.{middleware_name}.stripprefix.prefixes={base_path}"
75
- - "traefik.http.services.{res_name}.loadbalancer.server.port={port}"
101
+ """.format(router_name=router_name, middlewares=middlewares, middleware_name=middleware_name, base_path=base_path)
102
+ else:
103
+ middlewares = (maintenance_middleware + sablier_middleware).lstrip(",")
104
+ middleware_labels = ""
105
+ if middlewares:
106
+ middleware_labels = """ - "traefik.http.routers.{router_name}.middlewares={middlewares}"
107
+ """.format(router_name=router_name, middlewares=middlewares)
108
+
109
+ labels = """ - "{traefik_enable_label}"
110
+ - 'traefik.http.routers.{router_name}.rule={frontend_route_rule}'
111
+ - "traefik.http.routers.{router_name}.entrypoints={frontend_entrypoints}"
112
+ - "traefik.http.routers.{router_name}.priority={router_priority}"
113
+ {middleware_labels} - "traefik.http.services.{router_name}.loadbalancer.server.port={port}"
76
114
  - "traefik.docker.network={traefik_network}\"""".format(
77
- res_name=res_name,
115
+ router_name=router_name,
78
116
  traefik_enable_label=TRAEFIK_ENABLE_LABEL,
79
117
  frontend_route_rule=frontend_route_rule,
80
118
  frontend_entrypoints=frontend_entrypoints,
81
- middleware_name=middleware_name,
82
- base_path=base_path,
119
+ middleware_labels=middleware_labels,
83
120
  port=port,
84
121
  router_priority=router_priority,
85
122
  traefik_network=TRAEFIK_DOCKER_NETWORK,
@@ -135,8 +172,10 @@ def get_backend_traefik_labels(
135
172
  TRAEFIK_BACKEND_ENABLE_HTTP,
136
173
  TRAEFIK_BACKEND_ENABLE_HTTPS,
137
174
  )
138
- middleware_name = resource_entry_name + TRAEFIK_MIDDLEWARE_SUFFIX
139
-
175
+ router_name = traefik_scoped_name(resource_entry_name)
176
+ service_name = traefik_scoped_name(traefik_resource_name)
177
+ middleware_name = router_name + TRAEFIK_MIDDLEWARE_SUFFIX
178
+
140
179
  # Check if maintenance feature is enabled
141
180
  features = manifest.get('featuresEnabled', []) if manifest else []
142
181
  maintenance_middleware = ""
@@ -149,13 +188,13 @@ def get_backend_traefik_labels(
149
188
  # Build middleware config only if traefik_path is not empty
150
189
  if traefik_path:
151
190
  middleware_config_lines = [
152
- ' - "traefik.http.routers.' + resource_entry_name + '.middlewares=' + middleware_name + maintenance_middleware + sablier_middleware + '"',
191
+ ' - "traefik.http.routers.' + router_name + '.middlewares=' + middleware_name + maintenance_middleware + sablier_middleware + '"',
153
192
  ' - "traefik.http.middlewares.' + middleware_name + '.stripprefix.prefixes=' + traefik_path + '"',
154
193
  ]
155
194
  middleware_config = "\n".join(middleware_config_lines)
156
195
  elif sablier_middleware:
157
196
  # No strip-prefix path, but still front the router with the Sablier middleware
158
- middleware_config = ' - "traefik.http.routers.' + resource_entry_name + '.middlewares=' + sablier_middleware.lstrip(",") + '"'
197
+ middleware_config = ' - "traefik.http.routers.' + router_name + '.middlewares=' + sablier_middleware.lstrip(",") + '"'
159
198
  else:
160
199
  # No middleware if path is empty - router has no middlewares
161
200
  middleware_config = ""
@@ -171,15 +210,15 @@ def get_backend_traefik_labels(
171
210
  if stack_shared and not backend_route_rule:
172
211
  backend_router_labels = ""
173
212
  else:
174
- backend_router_labels = """ - "traefik.http.routers.{resource_entry_name}.rule={backend_route_rule}"
175
- - "traefik.http.routers.{resource_entry_name}.entrypoints={backend_entrypoints}"
176
- - "traefik.http.routers.{resource_entry_name}.service={traefik_resource_name}"
213
+ backend_router_labels = """ - "traefik.http.routers.{router_name}.rule={backend_route_rule}"
214
+ - "traefik.http.routers.{router_name}.entrypoints={backend_entrypoints}"
215
+ - "traefik.http.routers.{router_name}.service={service_name}"
177
216
  {middleware_config}
178
217
  """.format(
179
- resource_entry_name=resource_entry_name,
218
+ router_name=router_name,
180
219
  backend_route_rule=backend_route_rule,
181
220
  backend_entrypoints=backend_entrypoints,
182
- traefik_resource_name=traefik_resource_name,
221
+ service_name=service_name,
183
222
  middleware_config=middleware_config,
184
223
  )
185
224
 
@@ -187,25 +226,25 @@ def get_backend_traefik_labels(
187
226
  # probing a path the service never serves.
188
227
  traefik_healthcheck_labels = ""
189
228
  if health_path:
190
- traefik_healthcheck_labels = """ - "traefik.http.services.{traefik_resource_name}.loadbalancer.healthcheck.path={health_path}"
191
- - "traefik.http.services.{traefik_resource_name}.loadbalancer.healthcheck.interval={health_interval}"
192
- - "traefik.http.services.{traefik_resource_name}.loadbalancer.healthcheck.timeout={health_timeout}"
193
- - "traefik.http.services.{traefik_resource_name}.loadbalancer.healthcheck.followredirects=false"
229
+ traefik_healthcheck_labels = """ - "traefik.http.services.{service_name}.loadbalancer.healthcheck.path={health_path}"
230
+ - "traefik.http.services.{service_name}.loadbalancer.healthcheck.interval={health_interval}"
231
+ - "traefik.http.services.{service_name}.loadbalancer.healthcheck.timeout={health_timeout}"
232
+ - "traefik.http.services.{service_name}.loadbalancer.healthcheck.followredirects=false"
194
233
  """.format(
195
- traefik_resource_name=traefik_resource_name,
234
+ service_name=service_name,
196
235
  health_path=health_path,
197
236
  health_interval=TRAEFIK_HEALTHCHECK_INTERVAL,
198
237
  health_timeout=TRAEFIK_HEALTHCHECK_TIMEOUT,
199
238
  )
200
239
 
201
240
  labels = """ - "{traefik_enable_label}"
202
- {backend_router_labels} - "traefik.http.services.{traefik_resource_name}.loadbalancer.server.port={internal_port}"
241
+ {backend_router_labels} - "traefik.http.services.{service_name}.loadbalancer.server.port={internal_port}"
203
242
  {traefik_healthcheck_labels} - "traefik.docker.network={traefik_network}"
204
243
  """.format(
205
- resource_entry_name=resource_entry_name,
244
+ router_name=router_name,
206
245
  traefik_enable_label=TRAEFIK_ENABLE_LABEL,
207
246
  backend_router_labels=backend_router_labels,
208
- traefik_resource_name=traefik_resource_name,
247
+ service_name=service_name,
209
248
  internal_port=internal_port,
210
249
  traefik_healthcheck_labels=traefik_healthcheck_labels,
211
250
  traefik_network=TRAEFIK_DOCKER_NETWORK,
@@ -233,15 +272,15 @@ def get_backend_traefik_labels(
233
272
  management_path = get_api_path(stack, manifest) if stack else ""
234
273
 
235
274
  labels += """
236
- - "traefik.http.routers.{resource_entry_name}-project.rule={project_rule}"
237
- - "traefik.http.routers.{resource_entry_name}-project.entrypoints={project_entrypoints}"
238
- - "traefik.http.routers.{resource_entry_name}-project.service={traefik_resource_name}"
239
- - "traefik.http.routers.{resource_entry_name}-project.middlewares={middleware_name}-project{maintenance_middleware}{sablier_middleware}"
275
+ - "traefik.http.routers.{router_name}-project.rule={project_rule}"
276
+ - "traefik.http.routers.{router_name}-project.entrypoints={project_entrypoints}"
277
+ - "traefik.http.routers.{router_name}-project.service={service_name}"
278
+ - "traefik.http.routers.{router_name}-project.middlewares={middleware_name}-project{maintenance_middleware}{sablier_middleware}"
240
279
  - "traefik.http.middlewares.{middleware_name}-project.stripprefix.prefixes={api_path}"
241
- - "traefik.http.routers.{resource_entry_name}-project.priority={router_priority}"
280
+ - "traefik.http.routers.{router_name}-project.priority={router_priority}"
242
281
  """.format(
243
- resource_entry_name=resource_entry_name,
244
- traefik_resource_name=traefik_resource_name,
282
+ router_name=router_name,
283
+ service_name=service_name,
245
284
  project_rule=project_rule,
246
285
  project_entrypoints=project_entrypoints,
247
286
  middleware_name=middleware_name,
@@ -253,15 +292,15 @@ def get_backend_traefik_labels(
253
292
 
254
293
  if management_path and management_path != api_path and not stack_shared:
255
294
  labels += """
256
- - "traefik.http.routers.{resource_entry_name}-management.rule=Host(`{api_host}`) && PathPrefix(`{management_path}`)"
257
- - "traefik.http.routers.{resource_entry_name}-management.entrypoints={project_entrypoints}"
258
- - "traefik.http.routers.{resource_entry_name}-management.service={traefik_resource_name}"
259
- - "traefik.http.routers.{resource_entry_name}-management.middlewares={middleware_name}-management{sablier_middleware}"
295
+ - "traefik.http.routers.{router_name}-management.rule=Host(`{api_host}`) && PathPrefix(`{management_path}`)"
296
+ - "traefik.http.routers.{router_name}-management.entrypoints={project_entrypoints}"
297
+ - "traefik.http.routers.{router_name}-management.service={service_name}"
298
+ - "traefik.http.routers.{router_name}-management.middlewares={middleware_name}-management{sablier_middleware}"
260
299
  - "traefik.http.middlewares.{middleware_name}-management.stripprefix.prefixes={management_path}"
261
- - "traefik.http.routers.{resource_entry_name}-management.priority={mgmt_priority}"
300
+ - "traefik.http.routers.{router_name}-management.priority={mgmt_priority}"
262
301
  """.format(
263
- resource_entry_name=resource_entry_name,
264
- traefik_resource_name=traefik_resource_name,
302
+ router_name=router_name,
303
+ service_name=service_name,
265
304
  api_host=TRAEFIK_PROJECT_API_HOST,
266
305
  management_path=management_path,
267
306
  project_entrypoints=project_entrypoints,
@@ -273,11 +312,11 @@ def get_backend_traefik_labels(
273
312
  if old_path_pattern:
274
313
  labels += """
275
314
  # Redirect middleware for URL restructuring: old path -> new path
276
- - "traefik.http.middlewares.{resource_entry_name}-redirect.redirectregex.regex=^`{old_path_pattern}/(.*)`"
277
- - "traefik.http.middlewares.{resource_entry_name}-redirect.redirectregex.replacement=`{api_path}/$$1`"
278
- - "traefik.http.middlewares.{resource_entry_name}-redirect.redirectregex.permanent=true"
315
+ - "traefik.http.middlewares.{router_name}-redirect.redirectregex.regex=^`{old_path_pattern}/(.*)`"
316
+ - "traefik.http.middlewares.{router_name}-redirect.redirectregex.replacement=`{api_path}/$$1`"
317
+ - "traefik.http.middlewares.{router_name}-redirect.redirectregex.permanent=true"
279
318
  """.format(
280
- resource_entry_name=resource_entry_name,
319
+ router_name=router_name,
281
320
  old_path_pattern=old_path_pattern,
282
321
  api_path=api_path,
283
322
  )
@@ -85,6 +85,7 @@ def get_template_header(file_type, generator_name, resource_name='', extra_info=
85
85
  {c} TO MODIFY THIS CONFIGURATION:
86
86
  {c} 1. Edit the source generator in: .tilt/topologies/
87
87
  {c} 2. Or update service.json
88
+ {c} Regenerate with: flisk config regenerate
88
89
  {c}
89
90
  {c} Generation Source: {generator}{resource_line}{extra_line}
90
91
  {border}
@@ -571,7 +571,7 @@ def generate_resource_health_check(resource_name, resource_path, manifest, check
571
571
  }.get(manifest.get("language", "bun"), ["package.json", "prisma/schema.prisma"])
572
572
  required_files = {
573
573
  "backend": backend_files,
574
- "frontend": ["package.json", "index.html", frontend_entry],
574
+ "frontend": ["package.json", "next.config.mjs", "src/app/page.tsx"] if manifest.get("framework") == "next" else ["package.json", "index.html", frontend_entry],
575
575
  "library": ["package.json"],
576
576
  }.get(app_type, ["package.json"])
577
577
 
@@ -111,9 +111,9 @@ def generate_frontend(manifest, backend_manifest=None, write_fn=None):
111
111
  stack = manifest.get('stack', 'app')
112
112
  port = manifest.get('port', BASE_PORT_FRONTEND)
113
113
  hmr_port = port + 1000 # HMR port is always port + 1000
114
- base_path = get_frontend_base_path(manifest)
115
- vite_base_path = _normalize_vite_base_path(base_path)
116
114
  app_name = manifest.get('appName', 'unknown')
115
+ base_path = get_frontend_base_path(manifest, app_name)
116
+ vite_base_path = _normalize_vite_base_path(base_path)
117
117
 
118
118
  backend_port = BASE_PORT_BACKEND
119
119
  if backend_manifest:
@@ -31,6 +31,9 @@ def generate_for_manifest(manifest, backend_manifest=None, write_fn=None):
31
31
  return None
32
32
 
33
33
  if app_type == 'frontend':
34
+ # Next.js owns its build config (next.config.mjs); there is no Vite config to generate.
35
+ if manifest.get('framework') == 'next':
36
+ return None
34
37
  return generate_frontend(manifest, backend_manifest, write_fn)
35
38
  elif app_type == 'backend':
36
39
  return generate_backend(manifest, write_fn)
@@ -196,6 +196,9 @@ DEFAULT_SYNCS = {
196
196
  'bring-your-own': [],
197
197
  }
198
198
 
199
+ # Next.js owns its entry files (no index.html) and rebuilds from package.json or next.config.*, which are not synced.
200
+ NEXT_FRONTEND_SYNCS = ['src', 'public']
201
+
199
202
  RUNTIME_CONFIGS = {
200
203
  'bun': {
201
204
  'image_base': 'oven/bun:1.3.11-alpine',