@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.
- package/CHANGELOG.md +4 -0
- package/README.md +119 -43
- package/cli/dist/backend-languages/bun.js +5 -0
- package/cli/dist/backend-languages/go.js +3 -1
- package/cli/dist/cli.js +9 -0
- package/cli/dist/commands/completion.js +8 -1
- package/cli/dist/commands/config.js +130 -55
- package/cli/dist/commands/doctor.js +16 -15
- package/cli/dist/commands/logs.js +1 -0
- package/cli/dist/commands/project.js +1 -0
- package/cli/dist/commands/resource.js +39 -11
- package/cli/dist/commands/status.js +18 -9
- package/cli/dist/commands/up.js +102 -17
- package/cli/dist/frontend-frameworks/next.js +197 -0
- package/cli/dist/frontend-frameworks/react.js +81 -12
- package/cli/dist/frontend-frameworks/registry.js +5 -4
- package/cli/dist/frontend-frameworks/vue.js +82 -12
- package/cli/dist/generator/resource-templates.js +2 -4
- package/cli/dist/generator/template-engine.js +5 -3
- package/cli/dist/utils/cold-preflight.js +3 -2
- package/cli/dist/utils/config-explain.js +197 -0
- package/cli/dist/utils/demo-reset.js +22 -0
- package/cli/dist/utils/discovery-paths.js +7 -1
- package/cli/dist/utils/host-port-plan.js +8 -3
- package/cli/dist/utils/port-conflict-fix.js +16 -14
- package/cli/dist/utils/port-occupant.js +35 -0
- package/cli/dist/utils/project-features.js +7 -16
- package/cli/dist/utils/project-templates.js +8 -8
- package/cli/dist/utils/resource-features.js +5 -5
- package/cli/dist/utils/service-manifest.js +1 -0
- package/cli/dist/utils/service-urls.js +11 -0
- package/cli/dist/utils/startup-report.js +4 -3
- package/cli/dist/utils/tar.js +33 -28
- package/cli/dist/utils/tilt-process.js +4 -3
- package/cli/dist/utils/windows-doctor.js +18 -15
- package/cli/dist-bundle/{chunk-IGN5UC3P.js → chunk-4J2FLFUA.js} +14 -7
- package/cli/dist-bundle/{chunk-ETKUVKJ2.js → chunk-BMHPPX2Q.js} +10 -10
- package/cli/dist-bundle/chunk-BP2M4OJ4.js +2 -0
- package/cli/dist-bundle/{chunk-53L2JN7G.js → chunk-CGTWXWBU.js} +1 -1
- package/cli/dist-bundle/chunk-GPUJ55MI.js +3 -0
- package/cli/dist-bundle/{chunk-5QBYM33Q.js → chunk-LUUJ4JB7.js} +1 -1
- package/cli/dist-bundle/chunk-PEZQEZVV.js +17 -0
- package/cli/dist-bundle/{chunk-MWOVYIBB.js → chunk-SFOFTVYM.js} +5 -5
- package/cli/dist-bundle/{chunk-3XODDLRJ.js → chunk-TIYTGN5R.js} +1 -1
- package/cli/dist-bundle/{chunk-YNCBBJ4V.js → chunk-U42XZWXB.js} +1 -1
- package/cli/dist-bundle/chunk-ULMQVD7J.js +122 -0
- package/cli/dist-bundle/chunk-V36PT2HF.js +24 -0
- package/cli/dist-bundle/{chunk-I7TGIK5N.js → chunk-VMVTECCK.js} +1 -1
- package/cli/dist-bundle/{chunk-DU2BN46C.js → chunk-Z5QEE4SW.js} +1 -1
- package/cli/dist-bundle/cli.js +607 -375
- package/cli/dist-bundle/cold-preflight-3BNDOLGS.js +2 -0
- package/cli/dist-bundle/{demo-stage-OHNTP2UJ.js → demo-stage-G4W23BXW.js} +1 -1
- package/cli/dist-bundle/{extension-fetch-VEFSCG4B.js → extension-fetch-KIPYDIRM.js} +1 -1
- package/cli/dist-bundle/{open-XROJ6YEY.js → open-HQFT6WDC.js} +1 -1
- package/cli/dist-bundle/project-JZO2CPZZ.js +2 -0
- package/cli/dist-bundle/{project-templates-EYSQG2EO.js → project-templates-IMJ3OX5I.js} +1 -1
- package/cli/dist-bundle/{services-BIMF6MVU.js → services-GLFKZBBZ.js} +1 -1
- package/cli/dist-bundle/{tilt-port-owner-ZY5M2JC4.js → tilt-port-owner-PJN4MPYE.js} +1 -1
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/runtime/engine/topologies/platform/docker/compose/compose.star +40 -4
- package/runtime/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +6 -0
- package/runtime/engine/topologies/platform/docker/dockerfile/next_dockerfile.star +83 -0
- package/runtime/engine/topologies/platform/docker/index.star +4 -0
- package/runtime/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +7 -0
- package/runtime/engine/topologies/platform/docker/networking/traefik.star +85 -46
- package/runtime/engine/topologies/tilt/common/utils_output.star +1 -0
- package/runtime/engine/topologies/tilt/generators/validators.star +1 -1
- package/runtime/engine/topologies/tilt/generators/vite/frontend.star +2 -2
- package/runtime/engine/topologies/tilt/generators/vite/generators.star +3 -0
- package/runtime/engine/topologies/tilt/manifest/constants.star +3 -0
- package/runtime/engine/topologies/tilt/manifest/parser.star +9 -3
- package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +8 -3
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +50 -24
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/env.star +11 -2
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +23 -9
- package/cli/dist-bundle/chunk-43XWL2PB.js +0 -61
- package/cli/dist-bundle/chunk-5F7YP5IR.js +0 -17
- package/cli/dist-bundle/chunk-K6SOCO6V.js +0 -3
- package/cli/dist-bundle/chunk-YHYRGWUH.js +0 -24
- package/cli/dist-bundle/chunk-YL56J2WX.js +0 -2
- package/cli/dist-bundle/cold-preflight-ZOPR7PWJ.js +0 -2
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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{
|
|
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-
|
|
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
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- "traefik.http.routers.{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.' +
|
|
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.' +
|
|
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.{
|
|
175
|
-
- "traefik.http.routers.{
|
|
176
|
-
- "traefik.http.routers.{
|
|
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
|
-
|
|
218
|
+
router_name=router_name,
|
|
180
219
|
backend_route_rule=backend_route_rule,
|
|
181
220
|
backend_entrypoints=backend_entrypoints,
|
|
182
|
-
|
|
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.{
|
|
191
|
-
- "traefik.http.services.{
|
|
192
|
-
- "traefik.http.services.{
|
|
193
|
-
- "traefik.http.services.{
|
|
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
|
-
|
|
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.{
|
|
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
|
-
|
|
244
|
+
router_name=router_name,
|
|
206
245
|
traefik_enable_label=TRAEFIK_ENABLE_LABEL,
|
|
207
246
|
backend_router_labels=backend_router_labels,
|
|
208
|
-
|
|
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.{
|
|
237
|
-
- "traefik.http.routers.{
|
|
238
|
-
- "traefik.http.routers.{
|
|
239
|
-
- "traefik.http.routers.{
|
|
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.{
|
|
280
|
+
- "traefik.http.routers.{router_name}-project.priority={router_priority}"
|
|
242
281
|
""".format(
|
|
243
|
-
|
|
244
|
-
|
|
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.{
|
|
257
|
-
- "traefik.http.routers.{
|
|
258
|
-
- "traefik.http.routers.{
|
|
259
|
-
- "traefik.http.routers.{
|
|
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.{
|
|
300
|
+
- "traefik.http.routers.{router_name}-management.priority={mgmt_priority}"
|
|
262
301
|
""".format(
|
|
263
|
-
|
|
264
|
-
|
|
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.{
|
|
277
|
-
- "traefik.http.middlewares.{
|
|
278
|
-
- "traefik.http.middlewares.{
|
|
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
|
-
|
|
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',
|