@flisk-sh/flisk 1.4.2 → 1.4.6

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 (134) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +148 -49
  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 +66 -27
  9. package/cli/dist/commands/down.js +33 -1
  10. package/cli/dist/commands/logs.js +39 -1
  11. package/cli/dist/commands/project.js +12 -0
  12. package/cli/dist/commands/resource.js +39 -11
  13. package/cli/dist/commands/status.js +57 -19
  14. package/cli/dist/commands/up.js +221 -18
  15. package/cli/dist/commands/upgrade.js +2 -2
  16. package/cli/dist/frontend-frameworks/next.js +197 -0
  17. package/cli/dist/frontend-frameworks/react.js +81 -12
  18. package/cli/dist/frontend-frameworks/registry.js +5 -4
  19. package/cli/dist/frontend-frameworks/vue.js +82 -12
  20. package/cli/dist/generator/resource-templates.js +2 -4
  21. package/cli/dist/generator/template-engine.js +5 -3
  22. package/cli/dist/utils/bun-requirement.js +7 -4
  23. package/cli/dist/utils/cold-preflight.js +55 -34
  24. package/cli/dist/utils/config-explain.js +197 -0
  25. package/cli/dist/utils/demo-reset.js +22 -0
  26. package/cli/dist/utils/discovery-paths.js +7 -1
  27. package/cli/dist/utils/doctor-native.js +96 -0
  28. package/cli/dist/utils/host-port-plan.js +8 -3
  29. package/cli/dist/utils/native-project.js +41 -0
  30. package/cli/dist/utils/native-runtime.js +513 -0
  31. package/cli/dist/utils/native-service.js +95 -0
  32. package/cli/dist/utils/native-up.js +156 -0
  33. package/cli/dist/utils/platform.js +1 -1
  34. package/cli/dist/utils/port-conflict-fix.js +16 -14
  35. package/cli/dist/utils/port-occupant.js +35 -0
  36. package/cli/dist/utils/project-features.js +7 -16
  37. package/cli/dist/utils/project-templates.js +9 -9
  38. package/cli/dist/utils/resource-features.js +5 -5
  39. package/cli/dist/utils/service-manifest.js +20 -3
  40. package/cli/dist/utils/service-urls.js +11 -0
  41. package/cli/dist/utils/services.js +15 -2
  42. package/cli/dist/utils/startup-report.js +4 -3
  43. package/cli/dist/utils/tar.js +33 -28
  44. package/cli/dist/utils/tilt-logs.js +8 -0
  45. package/cli/dist/utils/tilt-process.js +4 -3
  46. package/cli/dist/utils/windows-doctor.js +18 -15
  47. package/cli/dist-bundle/chunk-BIVAAWBX.js +24 -0
  48. package/cli/dist-bundle/{chunk-YNCBBJ4V.js → chunk-C4UFBXRD.js} +1 -1
  49. package/cli/dist-bundle/{chunk-53L2JN7G.js → chunk-CAEMWNPF.js} +1 -1
  50. package/cli/dist-bundle/chunk-CDKEHFW6.js +35 -0
  51. package/cli/dist-bundle/{chunk-5QBYM33Q.js → chunk-DPQK5TIS.js} +1 -1
  52. package/cli/dist-bundle/chunk-ETXZ5LUG.js +18 -0
  53. package/cli/dist-bundle/chunk-GPUJ55MI.js +3 -0
  54. package/cli/dist-bundle/chunk-GT76VMUL.js +130 -0
  55. package/cli/dist-bundle/{chunk-IGN5UC3P.js → chunk-IDNES5PE.js} +14 -7
  56. package/cli/dist-bundle/{chunk-MWOVYIBB.js → chunk-MUDE4DZD.js} +5 -5
  57. package/cli/dist-bundle/chunk-QEBN6ZVQ.js +2 -0
  58. package/cli/dist-bundle/{chunk-I7TGIK5N.js → chunk-R7DWXISM.js} +1 -1
  59. package/cli/dist-bundle/{chunk-DU2BN46C.js → chunk-U5FCQF42.js} +1 -1
  60. package/cli/dist-bundle/{chunk-3XODDLRJ.js → chunk-WCGBVETR.js} +1 -1
  61. package/cli/dist-bundle/cli.js +626 -389
  62. package/cli/dist-bundle/cold-preflight-KUWOL2Q2.js +2 -0
  63. package/cli/dist-bundle/{demo-stage-OHNTP2UJ.js → demo-stage-QCC4IWXR.js} +1 -1
  64. package/cli/dist-bundle/{extension-fetch-VEFSCG4B.js → extension-fetch-KIPYDIRM.js} +1 -1
  65. package/cli/dist-bundle/{open-XROJ6YEY.js → open-AZNX4AC5.js} +1 -1
  66. package/cli/dist-bundle/project-5SWIGCNP.js +2 -0
  67. package/cli/dist-bundle/{project-templates-EYSQG2EO.js → project-templates-2VQPUBSQ.js} +1 -1
  68. package/cli/dist-bundle/{services-BIMF6MVU.js → services-EY5WKPHQ.js} +1 -1
  69. package/cli/dist-bundle/{tilt-port-owner-ZY5M2JC4.js → tilt-port-owner-UBU6JWOA.js} +1 -1
  70. package/cli/package.json +1 -1
  71. package/examples/{tdk-example → flisk-example}/README.md +2 -2
  72. package/package.json +2 -2
  73. package/runtime/discovery/AGENTS.md +1 -1
  74. package/runtime/discovery/TILT_DISCOVERY.star +1 -1
  75. package/runtime/discovery/deterministic.star +1 -1
  76. package/runtime/engine/AGENTS.md +3 -3
  77. package/runtime/engine/README.md +1 -1
  78. package/runtime/engine/REFACTORING_MANIFEST_SYSTEM.md +1 -1
  79. package/runtime/engine/assets/docker/wake-gateway/decide.ts +1 -1
  80. package/runtime/engine/assets/docker/wake-gateway/server.ts +2 -2
  81. package/runtime/engine/assets/scripts/mac-m1-resource-monitor.sh +6 -6
  82. package/runtime/engine/schemas/service-schema.json +99 -21
  83. package/runtime/engine/scripts/debug-container.sh +1 -1
  84. package/runtime/engine/scripts/generate-manifest.sh +2 -2
  85. package/runtime/engine/topologies/platform/docker/compose/compose.star +40 -4
  86. package/runtime/engine/topologies/platform/docker/compose/traefik_standalone.star +19 -4
  87. package/runtime/engine/topologies/platform/docker/constants.star +2 -2
  88. package/runtime/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +6 -0
  89. package/runtime/engine/topologies/platform/docker/dockerfile/next_dockerfile.star +83 -0
  90. package/runtime/engine/topologies/platform/docker/index.star +4 -0
  91. package/runtime/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +7 -0
  92. package/runtime/engine/topologies/platform/docker/networking/traefik.star +85 -46
  93. package/runtime/engine/topologies/platform/docker/networking/traefik_static_routes.star +2 -2
  94. package/runtime/engine/topologies/tilt/common/utils_output.star +1 -0
  95. package/runtime/engine/topologies/tilt/generators/validators.star +1 -1
  96. package/runtime/engine/topologies/tilt/generators/vite/frontend.star +2 -2
  97. package/runtime/engine/topologies/tilt/generators/vite/generators.star +3 -0
  98. package/runtime/engine/topologies/tilt/manifest/constants.star +3 -0
  99. package/runtime/engine/topologies/tilt/manifest/mapper.star +1 -1
  100. package/runtime/engine/topologies/tilt/manifest/parser.star +9 -3
  101. package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +9 -4
  102. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +50 -24
  103. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/env.star +11 -2
  104. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +23 -9
  105. package/runtime/ext/ide-components/README.md +1 -1
  106. package/runtime/ext/ui-enhancements/extension.yaml +2 -2
  107. package/runtime/specs/specs/TILT_SERVICE_DEFAULTS.star +1 -1
  108. package/runtime/specs/specs/TILT_TECH_STACK.star +1 -1
  109. package/cli/dist-bundle/chunk-43XWL2PB.js +0 -61
  110. package/cli/dist-bundle/chunk-5F7YP5IR.js +0 -17
  111. package/cli/dist-bundle/chunk-ETKUVKJ2.js +0 -35
  112. package/cli/dist-bundle/chunk-K6SOCO6V.js +0 -3
  113. package/cli/dist-bundle/chunk-YHYRGWUH.js +0 -24
  114. package/cli/dist-bundle/chunk-YL56J2WX.js +0 -2
  115. package/cli/dist-bundle/cold-preflight-ZOPR7PWJ.js +0 -2
  116. package/cli/dist-bundle/project-7PTIOHGF.js +0 -2
  117. /package/examples/{tdk-example → flisk-example}/LICENSE +0 -0
  118. /package/examples/{tdk-example → flisk-example}/TILT_SERVICE_DEFAULTS.star +0 -0
  119. /package/examples/{tdk-example → flisk-example}/TILT_TECH_STACK.star +0 -0
  120. /package/examples/{tdk-example → flisk-example}/Tiltfile +0 -0
  121. /package/examples/{tdk-example → flisk-example}/services/platform/messaging/docker-compose.yml +0 -0
  122. /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/package.json +0 -0
  123. /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/service.json +0 -0
  124. /package/examples/{tdk-example → flisk-example}/services/shop/orders-api/src/index.ts +0 -0
  125. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/index.html +0 -0
  126. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/package.json +0 -0
  127. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/public/.gitkeep +0 -0
  128. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/service.json +0 -0
  129. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/main.tsx +0 -0
  130. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/orders.tsx +0 -0
  131. /package/examples/{tdk-example → flisk-example}/services/shop/orders-app/src/style.css +0 -0
  132. /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/package.json +0 -0
  133. /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/service.json +0 -0
  134. /package/examples/{tdk-example → flisk-example}/services/shop/orders-worker/src/index.ts +0 -0
@@ -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
  )
@@ -17,7 +17,7 @@
17
17
  # Config for a request travels to the gateway via headers (X-Wake-*), not
18
18
  # the path/query. The route still strips the same `apiPath` prefix the real
19
19
  # Docker-label `-project` router strips (get_backend_traefik_labels) before
20
- # forwarding -- confirmed live against tdk-erp-system (task 7.2) that a
20
+ # forwarding -- confirmed live against flisk-erp-system (task 7.2) that a
21
21
  # backend's app only serves its bare health path (e.g. `/health`), not the
22
22
  # full `/api/{name}/health` the public route is matched on; without this, the
23
23
  # gateway's own proxy-through would forward the unstripped path once healthy
@@ -61,7 +61,7 @@ def _container_name(compose_project_name, res_name):
61
61
  Confirmed empirically against a live Flisk container (analytics-autogenerated
62
62
  -data-governance-api-1) -- see design.md D2 Context. Compose projects are now
63
63
  <project>-<stack>-autogenerated, so the name starts with the Flisk project too
64
- (tdk-erp-system-analytics-autogenerated-data-governance-api-1). Always replica 1: a
64
+ (flisk-erp-system-analytics-autogenerated-data-governance-api-1). Always replica 1: a
65
65
  deferred resource's replica count is unrelated to which single instance
66
66
  the wake gateway needs to address."""
67
67
  return compose_project_name + "-" + res_name + "-1"
@@ -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',
@@ -190,7 +190,7 @@ def map_manifest(manifest, target_format='internal'):
190
190
  def _to_yaml_format(internal_manifest):
191
191
  """Convert internal format to YAML hierarchical format."""
192
192
  return {
193
- 'apiVersion': 'tilt.Flisk Landscape.io/v1',
193
+ 'apiVersion': 'tilt.flisk.io/v1',
194
194
  'kind': 'ResourceManifest',
195
195
  'metadata': {
196
196
  'name': internal_manifest.get('appName', ''),
@@ -20,6 +20,7 @@ load("./constants.star",
20
20
  "MANIFEST_FILENAME",
21
21
  "VALID_APP_TYPES",
22
22
  "DEFAULT_SYNCS",
23
+ "NEXT_FRONTEND_SYNCS",
23
24
  "AUTOGENERATED_CONFIG_PATHS",
24
25
  "TRAEFIK_DEFAULTS",
25
26
  )
@@ -90,6 +91,7 @@ _GENERATED_STRING_RULES = [
90
91
  (['databaseName'], _ALPHANUM + '_-', ''),
91
92
  (['stack'], _ALPHANUM + '_-', ''),
92
93
  (['image'], _ALPHANUM + '._/:@-', ''),
94
+ (['basePath'], _PATH_CHARS, '/'),
93
95
  ]
94
96
 
95
97
  def generated_string_error(manifest, resource_path=""):
@@ -200,7 +202,8 @@ def normalize(manifest, resource_path=""):
200
202
  if normalized.get('syncs') == None or normalized.get('syncs') == []:
201
203
  normalized['syncs'] = _compute_syncs(
202
204
  normalized['appType'],
203
- normalized.get('featuresEnabled', [])
205
+ normalized.get('featuresEnabled', []),
206
+ normalized.get('framework')
204
207
  )
205
208
 
206
209
  # Compute backendName for frontends if missing
@@ -339,13 +342,14 @@ def _determine_app_type(manifest, resource_path):
339
342
  # Default to backend
340
343
  return "backend"
341
344
 
342
- def _compute_syncs(app_type, features):
345
+ def _compute_syncs(app_type, features, framework=None):
343
346
  """
344
347
  Compute default sync paths based on app type and features.
345
348
 
346
349
  Args:
347
350
  app_type: App type string
348
351
  features: List of feature strings
352
+ framework: Frontend framework id (Next.js has no index.html, so it syncs less than a Vite SPA)
349
353
 
350
354
  Returns:
351
355
  List of sync paths
@@ -353,7 +357,9 @@ def _compute_syncs(app_type, features):
353
357
  syncs = []
354
358
 
355
359
  # Start with default syncs for app type
356
- if app_type in DEFAULT_SYNCS:
360
+ if app_type == 'frontend' and framework == 'next':
361
+ syncs.extend(NEXT_FRONTEND_SYNCS)
362
+ elif app_type in DEFAULT_SYNCS:
357
363
  syncs.extend(DEFAULT_SYNCS[app_type])
358
364
 
359
365
  # Add feature-specific syncs
@@ -321,7 +321,7 @@ def _live_update_sync_source(project_root, full_res_path, sync_path):
321
321
  return relative_path
322
322
 
323
323
 
324
- def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', language='bun', go_live_reload=False):
324
+ def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', language='bun', go_live_reload=False, node_frontend=False):
325
325
  """Build live update sync and run rules for a resource.
326
326
 
327
327
  Python backends reload their own process after a sync (uvicorn --reload), so they have
@@ -363,7 +363,7 @@ def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', la
363
363
  dest = '/app/' + full_res_path + '/' + sync_path
364
364
  live_update_rules.append(sync(full_sync_path, dest))
365
365
 
366
- if language == 'python' or language == 'node':
366
+ if language == 'python' or language == 'node' or node_frontend:
367
367
  return live_update_rules
368
368
 
369
369
  package_json_path = '/app/' + full_res_path + '/package.json'
@@ -407,6 +407,9 @@ def _wake_compose_invocation(project_root, resource_path, res_name, resource_nam
407
407
 
408
408
  def _image_target(config):
409
409
  """The Dockerfile target Tilt builds and the deferStart prebuild must match: `development` for Go with `dev.liveReload`."""
410
+ # Next.js runs `next dev` in the development image, so a saved file hot-reloads through the synced sources.
411
+ if Docker.is_next_frontend(config.get('manifest', {})):
412
+ return 'development'
410
413
  return 'development' if Docker.go_live_reload(config.get('manifest', {})) else 'production'
411
414
 
412
415
 
@@ -426,7 +429,7 @@ def _register_deferred_image_prebuild(config, auto_init_apps):
426
429
  """Build a deferStart resource's image during `flisk up` without creating its container.
427
430
 
428
431
  `auto_init=False` on the dc_resource skips its docker_build too (confirmed
429
- live on tdk-erp-system, task 7.2), so a fresh project's first request would
432
+ live on flisk-erp-system, task 7.2), so a fresh project's first request would
430
433
  find no image and the wake gateway would fall back to `tilt trigger`, which
431
434
  queues behind every other build. This local_resource builds the same image
432
435
  (same Dockerfile and target as docker_build: `production`, or `development` for Go with `dev.liveReload`; host network, and an allowlist
@@ -548,6 +551,8 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
548
551
  config['syncs'],
549
552
  project_root=config.get('project_root', ''),
550
553
  language=config.get('manifest', {}).get('language', 'bun'),
554
+ # A Next.js image is Node with no Bun install step: syncing sources is enough, `next dev` reloads itself.
555
+ node_frontend=Docker.is_next_frontend(config.get('manifest', {})),
551
556
  go_live_reload=Docker.go_live_reload(config.get('manifest', {})),
552
557
  )
553
558
  _register_docker_build(config, live_update_rules)
@@ -610,7 +615,7 @@ def _register_frontend_typecheck(res, service_dir, typecheck_cmd):
610
615
  # Fixed script, variable parts passed as argv ($1 = service dir, $2 = typecheck command), never spliced into the shell text.
611
616
  cmd=[
612
617
  'sh', '-c',
613
- 'cd "$1" && { bunx tsc --version >/dev/null 2>&1 || { echo "typecheck needs host dependencies: run bun install in $1"; exit 1; }; } && eval "$2"',
618
+ 'cd "$1" && { bunx tsc --version >/dev/null 2>&1 && test -f "$1/node_modules/vite/package.json" || { echo "typecheck needs host dependencies: run bun install in $1"; exit 1; }; } && eval "$2"',
614
619
  'sh', service_dir, typecheck_cmd,
615
620
  ],
616
621
  deps=[service_dir + '/src', service_dir + '/tsconfig.json'],
@@ -7,37 +7,63 @@
7
7
  # Auto-inject configuration from internal dependencies
8
8
  # =============================================================================
9
9
 
10
- load("../../../../../../../discovery/registry.star", "get_resource_by_name")
10
+ load("../../../../../../../discovery/registry.star", "get_app_resources", "get_resource_by_name")
11
+ load("../../../../../platform/docker/networking/traefik_helpers.star", "cli_api_path")
12
+
13
+ def _find_backend_resource(resource_name):
14
+ """Find a discovered resource by its own name, e.g. "api".
15
+
16
+ Registry services are stacks (`app`) whose resources are the runnable pieces (`api`, `web`), so the lookup
17
+ must search resources rather than service names. Frontends are skipped.
18
+ """
19
+ for service in get_app_resources():
20
+ for resource in service.get("resources", []):
21
+ if resource.get("name") != resource_name:
22
+ continue
23
+ app_type = resource.get("_manifest", {}).get("appType") or resource.get("appType")
24
+ if app_type == "frontend":
25
+ continue
26
+ return resource
27
+ return None
28
+
29
+
30
+ def _find_frontend_backend(manifest):
31
+ """Find the backend a frontend talks to: `backendName` first, then the first API `dependsOn` entry.
32
+
33
+ Returns:
34
+ tuple: (backend resource name, backend manifest), or (None, None) when the frontend has no discovered backend
35
+ """
36
+ backend_name = manifest.get("backendName")
37
+ if backend_name:
38
+ backend = _find_backend_resource(backend_name)
39
+ if backend:
40
+ return backend_name, backend.get("_manifest", {})
41
+
42
+ for dep_name in manifest.get("dependsOn", []):
43
+ dep = _find_backend_resource(dep_name)
44
+ if dep and dep.get("_manifest", {}).get("appType") in ["backend", "mcp", "agent"]:
45
+ return dep_name, dep.get("_manifest", {})
46
+
47
+ return None, None
48
+
11
49
 
12
50
  def resolve_backend_api_path(manifest):
13
- """Resolve API path from backend manifest if backendName is specified.
14
-
15
- Frontend manifests often reference a backend via backendName.
16
- This function looks up the backend's apiPath for consistency.
17
-
51
+ """Resolve the URL path a frontend uses to reach its backend, e.g. "/api/api".
52
+
53
+ The path is the backend's route on api.{project}.localhost: its explicit `apiPath` when set, otherwise
54
+ `/api/{name}` with a trailing `-api` stripped, the same rule `flisk up` prints and Traefik serves.
55
+ Frontends that have no discovered backend get None, so callers keep their stack-management fallback.
56
+
18
57
  Args:
19
58
  manifest: Frontend service manifest
20
-
59
+
21
60
  Returns:
22
- str: API path from backend manifest, or None if not found
61
+ str: API path of the frontend's backend, or None if there is no backend
23
62
  """
24
- backend_name = manifest.get("backendName")
25
- if not backend_name:
26
- return None
27
-
28
- # Try to find the backend service
29
- backend_service = get_resource_by_name(backend_name)
30
- if not backend_service:
63
+ backend_name, backend_manifest = _find_frontend_backend(manifest)
64
+ if backend_name == None:
31
65
  return None
32
-
33
- # Get the backend manifest
34
- backend_resources = backend_service.get("resources", [])
35
- for resource in backend_resources:
36
- if resource.get("name") == backend_name:
37
- backend_manifest = resource.get("_manifest", {})
38
- return backend_manifest.get("apiPath")
39
-
40
- return None
66
+ return cli_api_path(backend_name, backend_manifest)
41
67
 
42
68
 
43
69
  def resolve_dependency_api_urls(manifest):