Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/compiler/generated-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ The generated server uses `GOWDK_ADDR`, applies bounded HTTP server timeouts,
serves `/_gowdk/health`, loads optional generated error pages, and emits
configured security and identity headers.

When the generated frontend or backend app directory is under `.gowdk/` inside
the application module, GOWDK emits it as normal package source in that module
and does not write a nested `go.mod`. Its imports use the application module
path, so app-owned `internal/` packages, `replace` directives, vendoring, and
workspace settings resolve the same way as the rest of the app. Explicit legacy
app directories outside `.gowdk/`, plus generated worker and cron role apps,
keep a nested generated module.

Generated backend routes are registered through `runtime/app.BackendRouter`.
Action, API, fragment, command, query, SSR, hybrid, realtime, guard, rate-limit,
CSRF, CORS, and tracing behavior is included only when declared, enabled, and
Expand Down
6 changes: 3 additions & 3 deletions docs/product/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ language references, compiler docs, and examples.

| ID | Requirement | Priority | Status | Notes |
| --- | --- | --- | --- | --- |
| 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. |
| 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. |
| PRD-002 | Default render mode must be `spa`. | High | Implemented | Root `RenderConfig.DefaultMode()` defaults to `gowdk.SPA`. |
| PRD-003 | Support render modes `spa`, `hybrid`, and `ssr`. | High | Implemented | Root `RenderMode` constants exist. Actions are endpoint capability, not a render mode. |
| 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`. |
Expand All @@ -45,7 +45,7 @@ language references, compiler docs, and examples.
| 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. |
| 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`. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
Expand Down Expand Up @@ -74,7 +74,7 @@ implemented.
| 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). |
| 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. |
| 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. |
| 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). |
| 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). |
| 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. |
| 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`. |
| 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. |
Expand Down
11 changes: 9 additions & 2 deletions docs/reference/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,22 @@ swaps those island roots, remounts islands, rehydrates page stores, and emits
`gowdk:component-hmr` plus `gowdk:dev-update`. Payloads with an unsupported
protocol version reload the page.

Each generated JavaScript island carries a deterministic state-shape marker.
During component remount HMR, the dev bridge carries the current island state
into the fresh root only when the old and new markers match. A missing or
changed marker remounts from the fresh document seed instead of preserving
potentially incompatible local state.

Layout-only incremental SPA rebuilds send a route-scoped `reload` payload for
the pages that use the changed layout. Browser tabs outside those routes do not
reload.

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

Generated-app rebuild and runtime 5xx overlay delivery use the dev-only proxy
bridge. Unsupported HMR cases continue to use the full-page reload fallback
Expand Down
7 changes: 5 additions & 2 deletions docs/reference/framework-integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ uses the same handler that other Go routers can mount or wrap.
Generated apps include an importable package:

```go
import "gowdk-generated-app/gowdkapp"
import "example.com/site/.gowdk/site/gowdkapp"

handler, err := gowdkapp.Handler()
mux, err := gowdkapp.ServeMux()
```

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

Route-aware framework adapters can also consume the generated `openapi.json`
report:
Expand Down
38 changes: 26 additions & 12 deletions internal/appgen/appgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func GenerateWithPlan(outputDir, appDir string, plan ApplicationPlan) (result Re
if err := os.MkdirAll(absApp, 0o755); err != nil {
return Result{}, err
}
moduleContext := resolveGeneratedModuleContext(absApp)
if err := os.MkdirAll(targetOutput, 0o755); err != nil {
return Result{}, err
}
Expand All @@ -141,13 +142,10 @@ func GenerateWithPlan(outputDir, appDir string, plan ApplicationPlan) (result Re
if err := removeStaleOutputFiles(targetOutput, files); err != nil {
return Result{}, err
}
modulePayload, err := moduleSource(options)
modulePath, err := writeGeneratedModuleFile(absApp, moduleContext, options)
if err != nil {
return Result{}, err
}
if err := writeFileIfChanged(filepath.Join(absApp, modFileName), []byte(modulePayload)); err != nil {
return Result{}, err
}
packageSource, err := appPackageSource(options)
if err != nil {
return Result{}, err
Expand Down Expand Up @@ -184,7 +182,7 @@ func GenerateWithPlan(outputDir, appDir string, plan ApplicationPlan) (result Re
}
files = append(files, scriptFiles...)
files = append(files, addonGoBlockFiles...)
mainSource, err := serverMainSource()
mainSource, err := serverMainSource(moduleContext.ImportBase + "/" + appPackageDirName)
if err != nil {
return Result{}, err
}
Expand All @@ -196,7 +194,7 @@ func GenerateWithPlan(outputDir, appDir string, plan ApplicationPlan) (result Re
AppDir: absApp,
MainPath: filepath.Join(absApp, mainFileName),
PackagePath: filepath.Join(absApp, appFileName),
ModulePath: filepath.Join(absApp, modFileName),
ModulePath: modulePath,
OutputDir: targetOutput,
Files: files,
}, nil
Expand Down Expand Up @@ -234,13 +232,11 @@ func GenerateBackendWithPlan(appDir string, plan ApplicationPlan) (result Result
if err := os.MkdirAll(absApp, 0o755); err != nil {
return Result{}, err
}
modulePayload, err := moduleSource(options)
moduleContext := resolveGeneratedModuleContext(absApp)
modulePath, err := writeGeneratedModuleFile(absApp, moduleContext, options)
if err != nil {
return Result{}, err
}
if err := writeFileIfChanged(filepath.Join(absApp, modFileName), []byte(modulePayload)); err != nil {
return Result{}, err
}
packageSource, err := backendAppPackageSource(options)
if err != nil {
return Result{}, err
Expand All @@ -261,7 +257,7 @@ func GenerateBackendWithPlan(appDir string, plan ApplicationPlan) (result Result
if _, err := writeAddonGoBlockFiles(absApp, options); err != nil {
return Result{}, err
}
mainSource, err := serverMainSource()
mainSource, err := serverMainSource(moduleContext.ImportBase + "/" + appPackageDirName)
if err != nil {
return Result{}, err
}
Expand All @@ -272,10 +268,28 @@ func GenerateBackendWithPlan(appDir string, plan ApplicationPlan) (result Result
AppDir: absApp,
MainPath: filepath.Join(absApp, mainFileName),
PackagePath: filepath.Join(absApp, appFileName),
ModulePath: filepath.Join(absApp, modFileName),
ModulePath: modulePath,
}, nil
}

func writeGeneratedModuleFile(absApp string, context generatedModuleContext, options Options) (string, error) {
nestedPath := filepath.Join(absApp, modFileName)
if !context.Nested {
if err := os.Remove(nestedPath); err != nil && !os.IsNotExist(err) {
return "", err
}
return "", nil
}
modulePayload, err := moduleSource(options)
if err != nil {
return "", err
}
if err := writeFileIfChanged(nestedPath, []byte(modulePayload)); err != nil {
return "", err
}
return nestedPath, nil
}

func writeLifecycleServiceFiles(absApp string, options Options) error {
sources, err := lifecycleServiceFileSources(options)
if err != nil {
Expand Down
Loading