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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ Core GOWDK renders at build time by default. SSR is an optional addon and a per-
Render modes:

- `spa`: build-time HTML.
- `action`: SPA page plus backend actions/API behavior.
- `hybrid`: SPA by default with selected request-time behavior.
- `ssr`: request-time full-page rendering through the SSR addon.

Expand All @@ -34,7 +33,7 @@ Compiler rules:
- Dynamic SPA routes require `paths {}`; action endpoints inherit generated
concrete page paths.
- `server {}` and `go server {}` require `ssr.Addon()`.
- `server {}` is rejected on SPA/action pages.
- `server {}` is rejected on SPA pages.
- Actions can exist without SSR.
- Partial updates are server fragments, not full-page SSR.

Expand Down
2 changes: 1 addition & 1 deletion docs/engineering/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ must pass `--config <file>`.
| Runtime race detector | `scripts/test-runtime-race.sh` | Shared-state runtime packages, lifecycle, contracts, SSE, rate limiting, trace, or testkit changes. |
| VS Code extension syntax | `node --check editors/vscode/extension.js` | Editor extension changes and broad verification. |
| VS Code extension behavior | `node --test editors/vscode/*.test.js` | Editor extension pure helper changes and broad verification. |
| SPA/action examples | `go run ./cmd/gowdk check examples/pages/home.page.gwdk examples/actions/newsletter.page.gwdk` | Language/tooling changes. |
| SPA and action endpoint examples | `go run ./cmd/gowdk check examples/pages/home.page.gwdk examples/actions/newsletter.page.gwdk` | Language/tooling changes. |
| Init project smoke | `go build -o /tmp/gowdk-cli ./cmd/gowdk && rm -rf /tmp/gowdk-init && /tmp/gowdk-cli init /tmp/gowdk-init && (cd /tmp/gowdk-init && /tmp/gowdk-cli build)` | CLI scaffold changes. |
| Init project tests | `go build -o /tmp/gowdk-cli ./cmd/gowdk && rm -rf /tmp/gowdk-init-tested && /tmp/gowdk-cli init --tests /tmp/gowdk-init-tested && (cd /tmp/gowdk-init-tested && /tmp/gowdk-cli test --count=1 --timeout=2m)` | CLI scaffold or `gowdk test` changes. |
| SSR example | `go run ./cmd/gowdk check --ssr examples/ssr/dashboard.page.gwdk` | SSR validation or example changes. |
Expand Down
2 changes: 1 addition & 1 deletion docs/language/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,4 @@ Future API behavior must define:
`func(context.Context, *http.Request) (response.Response, error)`.
- Per-route body/query/result contracts and route-param accessors.
- Per-endpoint CORS policy syntax and richer content negotiation.
- Interaction with SPA/action pages without full-page SSR.
- Interaction with SPA pages that declare backend endpoints without full-page SSR.
4 changes: 2 additions & 2 deletions docs/language/blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The parser records whether these top-level blocks are present:
statement forms are rejected with a `parse_error` diagnostic; arbitrary
build-time statements remain planned.
- `server {}`: request-time data block. Presence and raw body text are recorded,
then rejected on SPA/action pages.
then rejected on SPA pages.
- `go {}` and `go target {}`: optional inline Go authoring blocks.
Presence, target, raw body text, and source span are recorded. Default
`go {}` can provide build-data functions called by
Expand Down Expand Up @@ -68,7 +68,7 @@ api Health GET "/api/health"
- `act` and `api` endpoint declarations describe request handlers that should
work without full-page SSR. Normal Go handlers own behavior and return
`runtime/response.Response`.
- `view {}` renders markup for spa, action, partial, and SSR output.
- `view {}` renders markup for SPA, partial, and SSR output.

## Style Blocks

Expand Down
4 changes: 2 additions & 2 deletions docs/language/partials.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Partials

Partial updates use server fragments, not full-page SSR. The generated slice
supports action-driven fragment responses for SPA/action pages and standalone
concrete or dynamic fragment routes.
supports action-driven fragment responses for SPA pages and standalone concrete
or dynamic fragment routes.

Current support:

Expand Down
2 changes: 1 addition & 1 deletion docs/product/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ language references, compiler docs, and examples.
| --- | --- | --- | --- | --- |
| 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-002 | Default render mode must be `spa`. | High | Implemented | Root `RenderConfig.DefaultMode()` defaults to `gowdk.SPA`. |
| PRD-003 | Support render modes `spa`, `action`, `hybrid`, and `ssr`. | High | Implemented | Root `RenderMode` constants exist. |
| 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`. |
| PRD-005 | Require `paths {}` for dynamic SPA routes. | High | Implemented | Dynamic SPA routes without paths are rejected; action endpoints on those pages inherit generated concrete page paths. Malformed routes, duplicate route params, duplicate page route patterns, and route-method conflicts are rejected; the first literal string `paths {}` subset can prerender dynamic SPA routes. |
| PRD-006 | Keep typed actions available without SSR. | High | Partial | SPA pages with exported `act Name POST "/path"` endpoint declarations validate without SSR. Generated apps can serve POST action handlers with generated typed decoders, unexpected-field rejection, generated validation for direct literal `required`, `minlength`, `maxlength`, and supported anchored `pattern` form controls, bounded multipart action forms with explicit file count/size/MIME policy, generated validation fragments for partial requests, partial fragment responses, same-package action handlers using no-input, typed value, typed pointer, `form.Values`, or `form.Data` signatures returning `response.Response`, and generated CSRF token injection/validation by default unless `Build.CSRF.Disabled` is set. Upload storage/scanning/persistence and user-defined domain validation patterns remain in normal Go handlers. |
Expand Down
4 changes: 3 additions & 1 deletion docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,9 @@ artifacts from another module selection cannot be copied into the next binary.

## Render

`RenderConfig.Default` controls the default render mode. When omitted, default mode is `spa`.
`RenderConfig.Default` controls the default render mode. When omitted, default
mode is `spa`. Supported render modes are `spa`, `hybrid`, and `ssr`; actions
are endpoint declarations, not render modes.

## Env

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ contract):
`fragment` endpoints, where it is a build error.
- `guard public` marks intentional public access and cannot be combined with
protected guard IDs.
- Non-public page guards on build-time SPA/action page routes fail validation;
- Non-public page guards on build-time SPA page routes fail validation;
add `server {}` or `go server {}` with the SSR addon when the page itself is
protected.
- Guards run in declaration order.
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Current JSON shape:
"source": "examples/actions/signup.page.gwdk",
"kind": "page",
"route": "/signup",
"render": "action",
"render": "spa",
"artifacts": [
{"kind": "html", "path": "signup/index.html"}
],
Expand Down Expand Up @@ -137,8 +137,8 @@ Fields:
- `cssClasses`: optional sorted class names directly visible in literal `class`
attributes.
- `styleAttributes`: optional sorted literal inline `style` attribute values.
- `artifacts`: optional generated artifact path metadata. SPA and action
pages list the generated HTML path pattern relative to the build output
- `artifacts`: optional generated artifact path metadata. SPA pages list the
generated HTML path pattern relative to the build output
directory, such as `index.html`, `newsletter/index.html`, or
`blog/{slug}/index.html`. SSR-only pages omit app-shell HTML artifacts.
- `components`: component declarations known to the manifest.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/seo.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ are joined onto it. If `BaseURL` includes a path such as

`sitemap.xml` includes build-time-enumerable page routes:

- public static SPA/action pages;
- public static SPA pages;
- public dynamic SPA routes expanded from literal `paths {}` declarations;
- optional `ExtraURLs`, which may be absolute URLs or root-relative paths.

Expand Down
2 changes: 1 addition & 1 deletion editors/vscode/extension-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -1746,7 +1746,7 @@ function semanticTokens(source) {
collectPatternTokens(tokens, line, text, /\b(package|import|use|paths|build|load|act|api|fragment|view|script|go|style|props|state|exports|client|emits)\b/g, 'keyword');
collectPatternTokens(tokens, line, text, /\b(async|fn|computed|on|mount|destroy|effect|when|ref|let|return|await|if|else|in|emit)\b/g, 'keyword');
collectPatternTokens(tokens, line, text, /\b(GET|POST|PUT|PATCH|DELETE)\b/g, 'enumMember');
collectPatternTokens(tokens, line, text, /\b(spa|action|hybrid|ssr)\b/g, 'enumMember');
collectPatternTokens(tokens, line, text, /\b(spa|hybrid|ssr)\b/g, 'enumMember');
collectPatternTokens(tokens, line, text, /\b(string|int|float|bool)\b/g, 'enumMember');
collectPatternTokens(tokens, line, text, /\bg:(post|target|swap|ref|if|else-if|else|for|key|bind:(?:value|checked)|island)\b/g, 'property');
collectPatternTokens(tokens, line, text, /\bg:on:[A-Za-z][A-Za-z0-9_-]*(?:\.(?:prevent|stop|once|capture|debounce\([^)]+\)|throttle\([^)]+\)))*/g, 'property');
Expand Down
2 changes: 1 addition & 1 deletion editors/vscode/syntaxes/gwdk.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
},
{
"name": "support.type.render-mode.gwdk",
"match": "\\b(?:spa|action|hybrid|ssr)\\b"
"match": "\\b(?:spa|hybrid|ssr)\\b"
},
{
"name": "constant.language.http-method.gwdk",
Expand Down
6 changes: 2 additions & 4 deletions gowdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -704,8 +704,6 @@ type RenderMode string
const (
// SPA emits a non-SSR app shell and client-side route experience.
SPA RenderMode = "spa"
// Action emits a non-SSR app shell while allowing backend actions.
Action RenderMode = "action"
// Hybrid allows a route to combine app output and request-time behavior.
Hybrid RenderMode = "hybrid"
// SSR renders full pages at request time through the SSR addon.
Expand All @@ -716,7 +714,7 @@ const (
func ParseRenderMode(value string) (RenderMode, error) {
mode := RenderMode(value)
switch mode {
case SPA, Action, Hybrid, SSR:
case SPA, Hybrid, SSR:
return mode, nil
default:
return "", fmt.Errorf("unknown render mode %q", value)
Expand All @@ -732,7 +730,7 @@ func (mode RenderMode) RequiresSSR() bool {
// IsBuildTime reports whether this mode is always build-time. Hybrid defaults
// to build-time unless explicit request-time capabilities are declared.
func (mode RenderMode) IsBuildTime() bool {
return mode == SPA || mode == Action
return mode == SPA
}

// Feature names the capabilities that addons make available to the compiler.
Expand Down
12 changes: 6 additions & 6 deletions internal/buildgen/actions_partials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import (
"github.com/cssbruno/gowdk/internal/source"
)

func TestBuildLowersGPostDirectiveForActionPage(t *testing.T) {
func TestBuildLowersGPostDirectiveForSPAPage(t *testing.T) {
outputDir := t.TempDir()
app := gwdkanalysis.Sources{Pages: []gwdkir.Page{{
ID: "signup",
Route: "/signup",
Render: gowdk.Action,
Render: gowdk.SPA,
Guards: []string{"public"},
Blocks: gwdkir.Blocks{
View: true,
Expand Down Expand Up @@ -48,7 +48,7 @@ func TestBuildSynthesizesActionInputAttrsFromBindingFields(t *testing.T) {
ir := gwdkir.Program{Pages: []gwdkir.Page{{
ID: "signup",
Route: "/signup",
Render: gowdk.Action,
Render: gowdk.SPA,
Guards: []string{"public"},
Blocks: gwdkir.Blocks{
View: true,
Expand Down Expand Up @@ -94,7 +94,7 @@ func TestBuildProductionRequiresBoundBackendHandlers(t *testing.T) {
Package: "app",
Source: filepath.Join(t.TempDir(), "signup.page.gwdk"),
Route: "/signup",
Render: gowdk.Action,
Render: gowdk.SPA,
Guards: []string{"public"},
Blocks: gwdkir.Blocks{
View: true,
Expand All @@ -119,7 +119,7 @@ func TestBuildProductionAllowsExplicitMissingBackendStubs(t *testing.T) {
Package: "app",
Source: filepath.Join(t.TempDir(), "signup.page.gwdk"),
Route: "/signup",
Render: gowdk.Action,
Render: gowdk.SPA,
Guards: []string{"public"},
Blocks: gwdkir.Blocks{
View: true,
Expand All @@ -145,7 +145,7 @@ func TestBuildAllowsGPostWithLocalValueBinding(t *testing.T) {
Pages: []gwdkir.Page{{
ID: "search",
Route: "/search",
Render: gowdk.Action,
Render: gowdk.SPA,
Guards: []string{"public"},
Blocks: gwdkir.Blocks{
View: true,
Expand Down
4 changes: 2 additions & 2 deletions internal/buildgen/build_data_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -987,7 +987,7 @@ func TestBuildExpandsTypedDynamicSPAPathsAndInheritedActionRoutes(t *testing.T)
app := gwdkanalysis.Sources{Pages: []gwdkir.Page{{
ID: "patients.show",
Route: "/patients/{id:int}",
Render: gowdk.Action,
Render: gowdk.SPA,
Guards: []string{"public"},
Blocks: gwdkir.Blocks{
Paths: true,
Expand Down Expand Up @@ -1028,7 +1028,7 @@ func TestBuildLocalizesInheritedActionRoutes(t *testing.T) {
app := gwdkanalysis.Sources{Pages: []gwdkir.Page{{
ID: "contact",
Route: "/contact",
Render: gowdk.Action,
Render: gowdk.SPA,
Guards: []string{"public"},
Blocks: gwdkir.Blocks{
View: true,
Expand Down
4 changes: 2 additions & 2 deletions internal/buildgen/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const (

func renderPage(config gowdk.Config, page gwdkir.Page, route string, components map[string]view.Component, layouts map[string]gwdkir.Layout, stylesheets []gowdk.Stylesheet, actionFields map[string][]view.ActionInputField, data map[string]string, locale string, realtimeEventTypeNames map[string]string, queryTypeNames map[string]string, policy renderModePolicy) (string, ssrRegions, error) {
mode := page.RenderMode(config.Render.DefaultMode())
if policy == renderModeSPA && mode != gowdk.SPA && mode != gowdk.Action {
if policy == renderModeSPA && mode != gowdk.SPA {
return "", ssrRegions{}, fmt.Errorf("%s: SPA build cannot emit request-time %s pages yet", page.ID, mode)
}
if policy == renderModeRequestTime && mode != gowdk.SSR && mode != gowdk.Hybrid {
Expand Down Expand Up @@ -466,7 +466,7 @@ func viewHasInvalidatedQuery(source string, nodes []view.Node, queryTypeNames ma

func pageUsesSPANavigationRuntime(config gowdk.Config, page gwdkir.Page, viewSource string, viewNodes []view.Node, components map[string]view.Component) (bool, error) {
mode := page.RenderMode(config.Render.DefaultMode())
if mode != gowdk.SPA && mode != gowdk.Action {
if mode != gowdk.SPA {
return false, nil
}
if viewHasInternalLink(viewSource, viewNodes) {
Expand Down
2 changes: 1 addition & 1 deletion internal/compiler/validate_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ func requiresSSRFeature(mode gowdk.RenderMode, page gwdkir.Page) bool {

func isBuildTimeRoute(mode gowdk.RenderMode, page gwdkir.Page) bool {
switch mode {
case gowdk.SPA, gowdk.Action:
case gowdk.SPA:
return true
default:
return false
Expand Down
2 changes: 1 addition & 1 deletion internal/gwdkir/page_methods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestPageRenderModeResolvesRequestTime(t *testing.T) {
{"load_block", Page{Blocks: Blocks{Server: true}}, gowdk.SPA, gowdk.SSR},
{"go_ssr_block", Page{Blocks: Blocks{GoBlocks: []GoBlock{{Target: "server"}}}}, gowdk.SPA, gowdk.SSR},
{"default_spa", Page{}, "", gowdk.SPA},
{"default_passthrough", Page{}, gowdk.Action, gowdk.Action},
{"default_passthrough", Page{}, gowdk.Hybrid, gowdk.Hybrid},
}
for _, tc := range cases {
if got := tc.page.RenderMode(tc.def); got != tc.want {
Expand Down
2 changes: 1 addition & 1 deletion internal/lang/manifest_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ func fragmentEndpointsJSON(fragments []gwdkir.FragmentEndpoint) []fragmentEndpoi

func artifactsJSON(page gwdkir.Page) []artifactJSON {
switch page.RenderMode(gowdk.SPA) {
case gowdk.SPA, gowdk.Action:
case gowdk.SPA:
default:
return nil
}
Expand Down
9 changes: 6 additions & 3 deletions internal/lang/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,12 +341,15 @@ view {
}
`)

payload, diagnostics := ManifestJSON(gowdk.Config{Render: gowdk.RenderConfig{Default: gowdk.Action}}, []string{path})
payload, diagnostics := ManifestJSON(gowdk.Config{
Render: gowdk.RenderConfig{Default: gowdk.SSR},
Addons: []gowdk.Addon{ssr.Addon()},
}, []string{path})
if diagnostics.HasErrors() {
t.Fatal(diagnostics)
}
if !strings.Contains(string(payload), `"render": "action"`) {
t.Fatalf("expected action render mode in manifest JSON: %s", payload)
if !strings.Contains(string(payload), `"render": "ssr"`) {
t.Fatalf("expected ssr render mode in manifest JSON: %s", payload)
}
}

Expand Down
38 changes: 22 additions & 16 deletions internal/project/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ func parseConfigLiteral(expression ast.Expr, imports map[string]string) (gowdk.C
needsExecutableLoad = true
continue
}
config.Render = parseRenderConfig(field.Value)
render, err := parseRenderConfig(field.Value)
if err != nil {
return gowdk.Config{}, false, false, err
}
config.Render = render
Comment on lines +168 to +172

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate executable render defaults too

This new rejection only runs for the AST-only config path; when Render.Default contains an expression such as Default: gowdk.RenderMode("action") or a variable initialized to that value, needsConfigExpressionEvaluation switches to loadExecutableConfig, and validateLoadedConfig never revalidates config.Render.Default. Those configs still load with the removed "action" mode and then fail later or produce inconsistent route metadata, despite this change intending removed render modes to fail during config loading.

Useful? React with 👍 / 👎.

case "I18N":
if needsConfigExpressionEvaluation(field.Value) {
needsExecutableLoad = true
Expand Down Expand Up @@ -352,19 +356,23 @@ func parseModuleConfig(expression ast.Expr) gowdk.ModuleConfig {
return module
}

func parseRenderConfig(expression ast.Expr) gowdk.RenderConfig {
func parseRenderConfig(expression ast.Expr) (gowdk.RenderConfig, error) {
fields, ok := configLiteralFields(expression)
if !ok {
return gowdk.RenderConfig{}
return gowdk.RenderConfig{}, nil
}

var render gowdk.RenderConfig
for _, field := range fields {
if field.Name == "Default" {
render.Default = parseRenderMode(field.Value)
mode, err := parseRenderMode(field.Value)
if err != nil {
return gowdk.RenderConfig{}, err
}
render.Default = mode
}
}
return render
return render, nil
}

func parseI18NConfig(expression ast.Expr) gowdk.I18NConfig {
Expand Down Expand Up @@ -425,36 +433,34 @@ func parseLocaleConfig(expression ast.Expr) (gowdk.LocaleConfig, bool) {
return locale, true
}

func parseRenderMode(expression ast.Expr) gowdk.RenderMode {
func parseRenderMode(expression ast.Expr) (gowdk.RenderMode, error) {
if value := parseString(expression); value != "" {
mode, err := gowdk.ParseRenderMode(value)
if err == nil {
return mode
return mode, nil
}
return ""
return "", err
}
switch typed := expression.(type) {
case *ast.SelectorExpr:
return renderModeByName(typed.Sel.Name)
case *ast.Ident:
return renderModeByName(typed.Name)
default:
return ""
return "", fmt.Errorf("unsupported render mode expression")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicit zero render defaults

A literal config that explicitly leaves the render default at its zero value, e.g. Render: gowdk.RenderConfig{Default: ""}, now falls through to this new unsupported-expression error because parseString returns an empty string for both an empty literal and non-string expressions. The zero value is still the documented/default SPA behavior via RenderConfig.DefaultMode(), so this rejects an otherwise equivalent config during loading; allow an empty string literal before treating the expression as unsupported.

Useful? React with 👍 / 👎.

}
}

func renderModeByName(name string) gowdk.RenderMode {
func renderModeByName(name string) (gowdk.RenderMode, error) {
switch name {
case "SPA":
return gowdk.SPA
case "Action":
return gowdk.Action
return gowdk.SPA, nil
case "Hybrid":
return gowdk.Hybrid
return gowdk.Hybrid, nil
case "SSR":
return gowdk.SSR
return gowdk.SSR, nil
default:
return ""
return "", fmt.Errorf("unknown render mode %q", name)
}
}

Expand Down
Loading
Loading