Skip to content

Commit c2cf66c

Browse files
authored
fix(compiler): align generated apps and typed IR (#787)
* fix(compiler): align generated apps and typed IR * fix(compiler): address generated app review issues
1 parent 419ed3f commit c2cf66c

28 files changed

Lines changed: 673 additions & 165 deletions

docs/compiler/generated-output.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ The generated server uses `GOWDK_ADDR`, applies bounded HTTP server timeouts,
9898
serves `/_gowdk/health`, loads optional generated error pages, and emits
9999
configured security and identity headers.
100100

101+
When the generated frontend or backend app directory is under `.gowdk/` inside
102+
the application module, GOWDK emits it as normal package source in that module
103+
and does not write a nested `go.mod`. Its imports use the application module
104+
path, so app-owned `internal/` packages, `replace` directives, vendoring, and
105+
workspace settings resolve the same way as the rest of the app. Explicit legacy
106+
app directories outside `.gowdk/`, plus generated worker and cron role apps,
107+
keep a nested generated module.
108+
101109
Generated backend routes are registered through `runtime/app.BackendRouter`.
102110
Action, API, fragment, command, query, SSR, hybrid, realtime, guard, rate-limit,
103111
CSRF, CORS, and tracing behavior is included only when declared, enabled, and

docs/product/requirements.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ language references, compiler docs, and examples.
2626

2727
| ID | Requirement | Priority | Status | Notes |
2828
| --- | --- | --- | --- | --- |
29-
| PRD-001 | Compile portable package-peer `.gwdk` files that declare `package`, optional `page`, `route`, `guard`, `layout`, blocks, and endpoints. | High | Partial | Discovery, package parsing, metadata parsing, parser syntax validation, filename-derived page IDs, default build discovery, route shape/conflict validation, required page-view and page-guard validation, explicit component-file build input, typed GOWDK AST, AST analyzer, versioned compiler IR, endpoint comment discovery, and endpoint conflict diagnostics are implemented; full downstream migration to the IR remains planned. |
29+
| PRD-001 | Compile portable package-peer `.gwdk` files that declare `package`, optional `page`, `route`, `guard`, `layout`, blocks, and endpoints. | High | Partial | Discovery, package parsing, metadata parsing, parser syntax validation, filename-derived page IDs, default build discovery, route shape/conflict validation, required page-view and page-guard validation, explicit component-file build input, typed GOWDK AST, AST analyzer, versioned compiler IR, endpoint comment discovery, endpoint conflict diagnostics, and mandatory typed `view {}` IR nodes for supported view bodies are implemented; full downstream migration to typed IR for every semantic block remains planned. |
3030
| PRD-002 | Default render mode must be `spa`. | High | Implemented | Root `RenderConfig.DefaultMode()` defaults to `gowdk.SPA`. |
3131
| PRD-003 | Support render modes `spa`, `hybrid`, and `ssr`. | High | Implemented | Root `RenderMode` constants exist. Actions are endpoint capability, not a render mode. |
3232
| PRD-004 | Reject request-time page behavior unless the SSR feature is enabled in config or CLI options. | High | Implemented | `internal/compiler.ValidatePage` emits `missing_ssr_addon`. |
@@ -45,7 +45,7 @@ language references, compiler docs, and examples.
4545
| PRD-017 | Define cache and revalidation behavior for static files, SPA routes, backend endpoints, partial responses, SSR routes, and hybrid pages. | Medium | Implemented | Generated binaries apply asset-manifest cache policies for generated assets, default SPA HTML to `no-cache`, default request-time handlers to `no-store`, and apply explicit page `cache` policies to successful generated static SPA, SSR, and hybrid HTML responses. `revalidate` accepts positive second or duration values, requires `cache`, and compiles into a `stale-while-revalidate=<seconds>` Cache-Control directive for successful generated static SPA, SSR, and hybrid HTML. Safety responses for actions, APIs, fragments, guard failures, load redirects, generated errors, and CSRF-mutated HTML remain no-store. |
4646
| PRD-018 | Escape generated HTML by default and require any raw HTML escape hatch to be explicit. | High | Implemented | Rendering escapes text and attributes by default. `g:unsafe-html={Expr}` is the single explicit raw HTML escape hatch: attributes stay escaped, markup children are rejected, and it is refused on void elements, in stateful/island/loop contexts, and for route-param-tainted values. Foreign raw HTML syntax such as `{@html}` fails loudly and points at `g:unsafe-html`. URL-bearing attributes reject active-content schemes, protocol-relative URLs, and control characters; raw `on*` handlers, `srcdoc`, and literal `<script>` tags are rejected in `view {}`. See `docs/language/markup.md`. |
4747
| PRD-019 | Provide optional rate limiting for request-time handlers without making it core. | Medium | Implemented | `FeatureRateLimit` and `addons/ratelimit` enable generated rate-limit hooks; `runtime/ratelimit` exposes HTTP middleware, fixed-window decisions, an in-memory store, and a Redis-backed store adapter. Generated action, API, fragment, SSR, and split-backend proxy handlers expose `RegisterRateLimiter(*ratelimit.Limiter)` when the addon is enabled and call the registered limiter before guards and user logic. Docs include an in-memory registration example and a concrete go-redis adapter. |
48-
| PRD-020 | Allow generated apps and binaries to package selected configured modules. | High | Implemented | `Build.Targets` statically declares module sets, output dirs, generated app dirs, and binaries. `gowdk build` runs all configured targets, `--target` selects named targets, and ad hoc repeated or comma-separated `--module` flags remain supported. |
48+
| PRD-020 | Allow generated apps and binaries to package selected configured modules. | High | Implemented | `Build.Targets` statically declares module sets, output dirs, generated app dirs, and binaries. `gowdk build` runs all configured targets, `--target` selects named targets, and ad hoc repeated or comma-separated `--module` flags remain supported. Generated frontend/backend apps under `.gowdk/` build as packages inside the application module, while explicit legacy app dirs and worker/cron role apps keep nested generated modules. |
4949
| PRD-021 | Provide a dependency-free fast local development loop. | High | Implemented | `gowdk dev` polls discovered inputs without production dependencies, compares content hashes, caches watched input snapshots between ticks, rebuilds only on real input changes, incrementally renders page/component/layout affected SPA output when possible, falls back to full builds for config/CSS/source-set/app/binary/WASM changes, serves the generated output, live reloads browsers after successful rebuilds, keeps the last successful output after failed rebuilds, and can build/restart generated app targets. SPA/app generation skips identical file writes. |
5050
| PRD-022 | Allow generated app output to compile to a WASM deploy artifact. | Medium | Partial | `gowdk build --wasm <file>` and `Build.Targets[].WASM` compile the generated app with `GOOS=js GOARCH=wasm`. CI verifies the emitted artifact is a real WASM module by checking the WebAssembly magic header. This remains separate from component-level browser island assets emitted for `wasm` components; host runtime/loader integration is deploy-platform owned. |
5151
| PRD-023 | Keep current documentation aligned with implemented CLI, config, compiler, language, routing, deployment, and examples. | High | Implemented | `README.md`, `docs/getting-started.md`, `docs/cookbook/README.md`, reference docs, language docs, compiler docs, and `examples/README.md` describe current support, link to the right source of truth, and call out planned behavior. |
@@ -74,7 +74,7 @@ implemented.
7474
| Hybrid | Keep hybrid request-time page behavior explicit through config-selected effective render mode and the integrated SSR-gated request-time lane; keep streaming, browser-owned server-data refresh, non-HTTP revalidation, and implicit action invalidation unsupported until a future source contract exists. | Implemented for the current bounded contract — see [Hybrid Lifecycle Contract](hybrid-lifecycle-spec.md). |
7575
| Hooks | Compose app-wide hooks as `net/http` middleware plus explicit generated registration points and generated-binary lifecycle services; defer route rewriting and fetch interception. | Partial — generated embedded and backend-only apps expose `RegisterMiddleware(runtime/app.Middleware)` for ordered app-wide middleware, `gowdkapp.App()` for generated-binary startup, and `runtime/app.Service` lifecycle hooks for app-owned workers or extra servers. Route rewriting, response transformation, fetch/navigation interception, and protocol-specific built-ins such as MCP remain out of core. |
7676
| Errors | Keep `error` for route-local SSR and action/API boundaries; define expected error types and layout boundaries without leaking internals. | Partial — typed expected errors map to 404, 403, 422, and 500 for generated boundaries, SSR load errors honor `response.HandlerStatus` instead of forcing 500, and layout-level `error` pages compose with SSR route boundaries for HTTP 500 handling. Templated error regions and component/fragment-specific boundaries remain planned. |
77-
| Dev server | Keep dependency-free live reload as baseline; add browser error overlay before component-aware HMR. | Partial — `gowdk dev` polls without production dependencies, skips no-op rebuilds, supports incremental SPA rebuilds, runs generated app targets, prints stable change/rebuild/runtime-proxy log lines, shows browser overlays for SPA/static serving, generated-app rebuild failures, and generic generated-app runtime 5xx failures through a dev-only proxy bridge, skips live-reload/initial-overlay injection for proxied HTML over the documented dev-only size limit without full buffering, sends successful rebuilds through versioned `dev-update` v1 payloads, route-scopes layout-only reloads to affected pages, and hot-swaps changed JS island component roots when the dependency graph maps the change to the current page. Page/source-set/runtime changes, unmatched component roots, WASM islands, unsupported dev-update protocol versions, and broader state-preserving HMR fall back to full reload or remain deferred to [#424](https://github.com/cssbruno/GoWDK/issues/424). |
77+
| Dev server | Keep dependency-free live reload as baseline; add browser error overlay before component-aware HMR. | Partial — `gowdk dev` polls without production dependencies, skips no-op rebuilds, supports incremental SPA rebuilds, runs generated app targets, prints stable change/rebuild/runtime-proxy log lines, shows browser overlays for SPA/static serving, generated-app rebuild failures, and generic generated-app runtime 5xx failures through a dev-only proxy bridge, skips live-reload/initial-overlay injection for proxied HTML over the documented dev-only size limit without full buffering, sends successful rebuilds through versioned `dev-update` v1 payloads, route-scopes layout-only reloads to affected pages, hot-swaps changed JS island component roots when the dependency graph maps the change to the current page, and preserves island state only when deterministic state-shape markers match. Page/source-set/runtime changes, unmatched component roots, incompatible state shapes, WASM islands, unsupported dev-update protocol versions, and broader page/layout DOM patching fall back to full reload or remain deferred to [#424](https://github.com/cssbruno/GoWDK/issues/424). |
7878
| Playground | Own playground onboarding in website/docs first with local preview commands and static examples; keep hosted code execution optional, sandboxed, and exportable as a normal GOWDK app. | Partial — [playground.md](playground.md) defines the onboarding path, sandbox rules, export contract, and non-goals. `gowdk playground policy/export/run` implements local policy inspection, normal source archive export, and an opt-in staged build bridge for future hosted runners; production hosted execution remains app-owned infrastructure. |
7979
| Routing | Add rest params and trailing-slash policy first while keeping explicit route declarations; defer optional params, route groups, and same-path page/API negotiation. | Partial — rest params `{name...}` are supported as the final segment of SSR page routes (string-only, one or more segments joined with `/`) with duplicate/ambiguity validation, and the trailing-slash policy is explicit (canonical declarations; GET/HEAD trailing-slash requests 308-redirect to the canonical path). Optional params, route groups, and same-path negotiation remain deferred with explicit diagnostics; see `docs/reference/routing.md`. |
8080
| Typed generated APIs | Generate typed route-param accessors first; add typed SSR load data before broader action result contracts. | Partial — generated SSR and fragment request-time handlers attach raw route params through `app.Params(ctx)` and decoded typed params through `app.TypedParams(ctx)`; SSR load handlers can return exported typed result structs whose declared `server {}` fields are compiler-checked and lowered into generated adapter glue. Per-route param structs and typed action-result accessors remain planned. |

docs/reference/dev.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,22 @@ swaps those island roots, remounts islands, rehydrates page stores, and emits
9696
`gowdk:component-hmr` plus `gowdk:dev-update`. Payloads with an unsupported
9797
protocol version reload the page.
9898

99+
Each generated JavaScript island carries a deterministic state-shape marker.
100+
During component remount HMR, the dev bridge carries the current island state
101+
into the fresh root only when the old and new markers match. A missing or
102+
changed marker remounts from the fresh document seed instead of preserving
103+
potentially incompatible local state.
104+
99105
Layout-only incremental SPA rebuilds send a route-scoped `reload` payload for
100106
the pages that use the changed layout. Browser tabs outside those routes do not
101107
reload.
102108

103109
The dev bridge falls back to one full-page reload for page changes, source-set
104110
changes, added or removed inputs, generated app/runtime mode, WASM output or
105111
component WASM islands, and component changes that cannot be mapped to matching
106-
island boundaries on the current page. Local island state preservation is not a
107-
current contract.
112+
island boundaries on the current page. Broader cross-component state transfer,
113+
WASM state transfer, and page/layout DOM patching remain outside the current
114+
HMR contract.
108115

109116
Generated-app rebuild and runtime 5xx overlay delivery use the dev-only proxy
110117
bridge. Unsupported HMR cases continue to use the full-page reload fallback

docs/reference/framework-integrations.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ uses the same handler that other Go routers can mount or wrap.
88
Generated apps include an importable package:
99

1010
```go
11-
import "gowdk-generated-app/gowdkapp"
11+
import "example.com/site/.gowdk/site/gowdkapp"
1212

1313
handler, err := gowdkapp.Handler()
1414
mux, err := gowdkapp.ServeMux()
1515
```
1616

1717
`Handler()` returns `http.Handler`. `ServeMux()` returns the concrete
18-
`*http.ServeMux`.
18+
`*http.ServeMux`. Replace `example.com/site` and `.gowdk/site` with the
19+
application module path and generated app directory. Explicit legacy app
20+
directories outside `.gowdk/` still use their generated module-local import
21+
inside that generated module.
1922

2023
Route-aware framework adapters can also consume the generated `openapi.json`
2124
report:

internal/appgen/appgen.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func GenerateWithPlan(outputDir, appDir string, plan ApplicationPlan) (result Re
130130
if err := os.MkdirAll(absApp, 0o755); err != nil {
131131
return Result{}, err
132132
}
133+
moduleContext := resolveGeneratedModuleContext(absApp)
133134
if err := os.MkdirAll(targetOutput, 0o755); err != nil {
134135
return Result{}, err
135136
}
@@ -141,13 +142,10 @@ func GenerateWithPlan(outputDir, appDir string, plan ApplicationPlan) (result Re
141142
if err := removeStaleOutputFiles(targetOutput, files); err != nil {
142143
return Result{}, err
143144
}
144-
modulePayload, err := moduleSource(options)
145+
modulePath, err := writeGeneratedModuleFile(absApp, moduleContext, options)
145146
if err != nil {
146147
return Result{}, err
147148
}
148-
if err := writeFileIfChanged(filepath.Join(absApp, modFileName), []byte(modulePayload)); err != nil {
149-
return Result{}, err
150-
}
151149
packageSource, err := appPackageSource(options)
152150
if err != nil {
153151
return Result{}, err
@@ -184,7 +182,7 @@ func GenerateWithPlan(outputDir, appDir string, plan ApplicationPlan) (result Re
184182
}
185183
files = append(files, scriptFiles...)
186184
files = append(files, addonGoBlockFiles...)
187-
mainSource, err := serverMainSource()
185+
mainSource, err := serverMainSource(moduleContext.ImportBase + "/" + appPackageDirName)
188186
if err != nil {
189187
return Result{}, err
190188
}
@@ -196,7 +194,7 @@ func GenerateWithPlan(outputDir, appDir string, plan ApplicationPlan) (result Re
196194
AppDir: absApp,
197195
MainPath: filepath.Join(absApp, mainFileName),
198196
PackagePath: filepath.Join(absApp, appFileName),
199-
ModulePath: filepath.Join(absApp, modFileName),
197+
ModulePath: modulePath,
200198
OutputDir: targetOutput,
201199
Files: files,
202200
}, nil
@@ -234,13 +232,11 @@ func GenerateBackendWithPlan(appDir string, plan ApplicationPlan) (result Result
234232
if err := os.MkdirAll(absApp, 0o755); err != nil {
235233
return Result{}, err
236234
}
237-
modulePayload, err := moduleSource(options)
235+
moduleContext := resolveGeneratedModuleContext(absApp)
236+
modulePath, err := writeGeneratedModuleFile(absApp, moduleContext, options)
238237
if err != nil {
239238
return Result{}, err
240239
}
241-
if err := writeFileIfChanged(filepath.Join(absApp, modFileName), []byte(modulePayload)); err != nil {
242-
return Result{}, err
243-
}
244240
packageSource, err := backendAppPackageSource(options)
245241
if err != nil {
246242
return Result{}, err
@@ -261,7 +257,7 @@ func GenerateBackendWithPlan(appDir string, plan ApplicationPlan) (result Result
261257
if _, err := writeAddonGoBlockFiles(absApp, options); err != nil {
262258
return Result{}, err
263259
}
264-
mainSource, err := serverMainSource()
260+
mainSource, err := serverMainSource(moduleContext.ImportBase + "/" + appPackageDirName)
265261
if err != nil {
266262
return Result{}, err
267263
}
@@ -272,10 +268,28 @@ func GenerateBackendWithPlan(appDir string, plan ApplicationPlan) (result Result
272268
AppDir: absApp,
273269
MainPath: filepath.Join(absApp, mainFileName),
274270
PackagePath: filepath.Join(absApp, appFileName),
275-
ModulePath: filepath.Join(absApp, modFileName),
271+
ModulePath: modulePath,
276272
}, nil
277273
}
278274

275+
func writeGeneratedModuleFile(absApp string, context generatedModuleContext, options Options) (string, error) {
276+
nestedPath := filepath.Join(absApp, modFileName)
277+
if !context.Nested {
278+
if err := os.Remove(nestedPath); err != nil && !os.IsNotExist(err) {
279+
return "", err
280+
}
281+
return "", nil
282+
}
283+
modulePayload, err := moduleSource(options)
284+
if err != nil {
285+
return "", err
286+
}
287+
if err := writeFileIfChanged(nestedPath, []byte(modulePayload)); err != nil {
288+
return "", err
289+
}
290+
return nestedPath, nil
291+
}
292+
279293
func writeLifecycleServiceFiles(absApp string, options Options) error {
280294
sources, err := lifecycleServiceFileSources(options)
281295
if err != nil {

0 commit comments

Comments
 (0)