diff --git a/.llm/plans/manifest-to-ir-migration.md b/.llm/plans/manifest-to-ir-migration.md new file mode 100644 index 0000000..38bbf50 --- /dev/null +++ b/.llm/plans/manifest-to-ir-migration.md @@ -0,0 +1,260 @@ +# Implementation Plan: Manifest → IR Migration + +## Status (progress) + +Done: + +- Centralized the IR→manifest converter as `compiler.ManifestFromIR` and added + IR-first validation entrypoints `compiler.ValidateProgram` / + `compiler.ValidateBackendBindingPolicyIR`. +- Extracted shared leaf value types into the neutral `internal/source` package + (manifest re-exports them as aliases). `internal/gwdkir`, `internal/gwdkast`, + and `internal/contractscan` are now manifest-free (verified via + `go list -deps`). +- Added IR-native `Page` behavior methods (`CachePolicy`, `RenderMode`, + `HasGoBlock`, `DynamicParams`, `TypedRouteParams`) so render helpers consume IR. +- Migrated `internal/buildgen` render helpers to consume `gwdkir` models; only a + `gotypes` bridge and public-API compat types remain. +- Swapped leaf-type references to `internal/source` across `appgen`, `compiler`, + `parser`, `gwdkanalysis`, `lsp`. + +Pending (largest remaining work): + +- Make `internal/compiler` validation/binding IR-native (it still validates the + manifest model — ~980 references). Tracked in **issue #145**. This is a + redesign, not a mechanical swap: a naive flip to `ValidateProgram(ir)` would + silently drop `validateStandaloneEndpoints` / `validateRouteMethodConflicts` + because IR lowering is lossy for endpoint spans, route params, and the raw + kind/method those validators check. The IR must be enriched first and the flip + guarded by a zero-diagnostic-drift differential corpus. +- Collapse the `AST → manifest → IR` path to `AST → IR` in `gwdkanalysis`. +- Keep public manifest JSON until a release plan deprecates it. + +## Context + +`docs/engineering/architecture.md` ("Compatibility Records" section) tracks +`internal/manifest` compatibility records as known, well-documented debt. The +`gwdkir.Program` IR is meant to be the single stable compiler handoff, but +`internal/manifest` types are still threaded through six packages. Reference +counts of `manifest.` (non-test) today: + +| Package | `manifest.` refs | Role | +| --- | ---: | --- | +| `internal/compiler` | 1058 | validation, backend binding, route metadata — gates everyone | +| `internal/buildgen` | 731 | render helpers + an **IR→manifest reconstruction shim** | +| `internal/appgen` | 214 | route/adapter planning (already IR-first at the boundary) | +| `internal/gwdkanalysis` | 176 | AST→manifest lowering, then manifest→IR build | +| `internal/parser` | 169 | emits manifest records beside typed AST | +| `internal/gwdkir` | 80 | re-exports a few shared manifest value types | +| `internal/lang` | 41 | CLI parse/report glue | +| `internal/gwdkast` | 35 | shares span/value types with manifest | + +The single most important fact from the data-flow investigation: **manifest is +a mandatory intermediate, not a parallel output.** The current pipeline is +`AST → manifest → IR`, and `gwdkir` already models essentially every record +manifest does (Page, Component, Layout, Action, API, Fragment, StateContract, +WASMContract, Prop, Export, Emit, spans, BackendBinding-as-`Endpoint.Binding`). +So this migration is mostly **re-pointing consumers and deleting converters**, +not designing new models. + +## Assumptions + +- `gwdkir.Program` stays the stable handoff; new fields are added there first. +- Public manifest **JSON** output (`internal/manifest/json.go`) is a separate + concern and is explicitly OUT of scope here (architecture doc step 5 keeps it + until a release plan deprecates it). We migrate the in-memory model coupling, + not the wire format. +- Behavior must not change; each step is independently shippable and gated by + the existing test suite. +- No new third-party dependencies. + +## The Two Seams That Matter + +The data flow has exactly two conversion seams to delete, in this order: + +1. **`buildgen.buildModelFromIR(ir)` — `internal/buildgen/ir.go:8`** (the + IR→manifest reconstruction shim). buildgen's public entrypoint + `BuildFromIR(config, ir, outputDir)` **already takes IR**, then immediately + rebuilds a `manifest.Manifest` from it (`buildPageFromIR`, + `buildComponentFromIR`, `buildLayoutFromIR`, `buildBackendBindingFromIR`) + solely to call `compiler.ValidateManifest` and + `compiler.ValidateBackendBindingPolicy` (see `build.go:37,43`). This is the + exact round-trip the architecture doc warns about and the **highest-leverage + first kill** — it is lossy, fragile, and serves only validation. + +2. **`gwdkanalysis.BuildIR(config, app manifest.Manifest)` — + `internal/gwdkanalysis/ir_builder.go:14`** (the manifest→IR build). This is + the backbone of the pipeline. Removing manifest as the *input* to IR is the + final, largest step and depends on parser emitting AST-first. + +## Proposed Changes (sequenced — each step ships independently) + +### Step 0 — Guardrails (no behavior change) + +- Add a focused golden test that runs the full `cmd/gowdk build` orchestration + on a representative fixture and snapshots the resulting `gwdkir.Program` + (JSON-serialized) plus generated output file list. This snapshot is the + safety net every later step must keep green. Land this BEFORE touching code. + +### Step 1 — Make compiler validation IR-native (the keystone) + +`compiler` gates every other package, and validation is what forces the +buildgen IR→manifest shim to exist. Provide IR-typed validation entrypoints +that read `gwdkir.Program`: + +- `compiler.ValidateProgram(config, ir gwdkir.Program) error` — IR twin of + `ValidateManifest` (`validate.go:29`). +- `compiler.ValidateBackendBindingPolicyIR(config, ir gwdkir.Program) error` — + IR twin of `ValidateBackendBindingPolicy` (`backend_binding_policy.go:12`). + +Implement these by reading IR fields directly. Where a sub-validator +(`validate_page.go`, `validate_component_*.go`, etc.) reads a manifest field, +add an IR-reading variant or a tiny accessor; keep the manifest versions intact +for now (parallel, not replaced). Internally the manifest validators can be +re-expressed in terms of the IR ones via a thin local adapter to avoid +duplicating logic, but the **public** new surface is IR-typed. + +### Step 2 — Delete the buildgen IR→manifest shim + +- Switch `buildgen/build.go:37,43` to call the new `ValidateProgram` / + `ValidateBackendBindingPolicyIR`. +- Delete `buildModelFromIR` and its helpers in `internal/buildgen/ir.go` + (`buildPageFromIR`, `buildComponentFromIR`, `buildLayoutFromIR`, + `buildBackendBindingFromIR`, and the ~12 sub-helpers). +- Migrate the remaining buildgen render helpers that still take + `manifest.Page/Component/Layout` to take the `gwdkir` equivalents. These are + mechanical field-rename changes since the IR types mirror the manifest ones. + This is the bulk of buildgen's 731 refs and can be done file-by-file + (`css.go`, `components.go`, `render.go`, `routes.go`, …), each compiling and + testing green on its own. + +**Exit criterion for Step 2:** `internal/buildgen` no longer imports +`internal/manifest`. + +### Step 3 — Make backend binding + endpoint discovery IR-native + +- `BindBackendHandlers(app manifest.Manifest) manifest.Manifest` + (`backend_bindings.go:38`) currently mutates manifest. Move binding discovery + to populate `gwdkir.Endpoint.Binding` directly (the IR already has the field; + `attachBackendBindings` in `gwdkanalysis/ir_bindings.go:10` already copies it + across). Provide `BindBackendHandlersIR(ir *gwdkir.Program)`. +- `DiscoverGoEndpointComments(app) (app, error)` (`go_endpoints.go:18`) — same + treatment: discover into IR endpoints. +- Update `BuildRouteMetadata` (`route_bindings.go:105`) to take IR (it already + builds IR internally and discards the manifest afterward, so this mostly + deletes the manifest plumbing). +- Update the `cmd/gowdk/build.go:89-116` orchestration to call the IR-native + validate/bind path. After this, the CLI build path no longer needs manifest + between BuildIR and codegen. + +**Exit criterion for Step 3:** `internal/compiler` no longer imports +`internal/manifest` (public JSON aside — confirm `json.go` is the only +remaining manifest consumer module-wide besides the parser seam). + +### Step 4 — appgen + lang/lsp cleanup + +- `appgen` is already IR-first at its boundary (`Options.IR`, + `actionEndpointsFromIR`, `apiEndpointsFromIR`). Convert the residual + manifest-typed helpers (`routes.go`, `types.go`, `source_*.go`, + `validate_*.go`) to IR types — mechanical, mirrors Step 2. +- `lang` (`sitemap.go`, `accessibility.go`, `tools.go`) and `lsp` + (`completion_hover.go`, `components.go`, `diagnostics.go`): derive reports and + symbols from the IR snapshot. LSP open-document source spans stay as-is + (architecture doc keeps those); only project-wide symbol/report derivation + moves to IR. + +### Step 5 — Collapse the AST→manifest→IR path to AST→IR (the backbone) + +Only after Steps 1–4 leave manifest used by just the parser seam + public JSON: + +- Add `gwdkanalysis.BuildIRFromAST(config, files []gwdkast.File) gwdkir.Program` + that lowers typed AST directly to IR (reusing the existing `lowerIRPage` / + `lowerIRComponent` logic, but sourcing from AST rather than manifest records). +- Repoint the parser/`lang.ParseBuildFiles` path to feed AST into the new + builder. +- Keep `BuildIR(manifest.Manifest)` and the AST→manifest lowering in + `manifest_lowering.go` ALIVE but used **only** to produce public manifest + JSON, isolated behind `internal/manifest/json.go`. This satisfies architecture + doc step 5 ("keep public manifest JSON compatibility until a release plan + explicitly deprecates it"). + +## Files Expected To Change + +- New: `internal/compiler/validate_program.go`, `.../backend_bindings_ir.go`, + `internal/gwdkanalysis/ir_from_ast.go`, plus the Step 0 golden test. +- Heavy edits: `internal/buildgen/*` (delete `ir.go` shim; field renames across + render helpers), `internal/compiler/route_bindings.go`, + `backend_bindings.go`, `go_endpoints.go`, `validate*.go`, + `internal/appgen/*`, `cmd/gowdk/build.go`, `route_report.go`, `dev_loop.go`. +- Light edits: `internal/lang/*`, `internal/lsp/*`. +- Untouched on purpose: `internal/manifest/json.go` (public wire format), + parser's AST output, `gwdkir/ir.go` types (add fields only if a gap is found). + +## Data And API Impact + +- No change to public manifest JSON, generated HTML, generated Go, route + manifests, or build reports. The golden snapshot in Step 0 enforces this. +- Internal-only API surface change: new IR-typed validation/binding entrypoints; + removal of the buildgen IR→manifest shim. `internal/` packages have no + external API stability guarantee. + +## Tests + +- Unit: IR-twin validators must reproduce the exact `ValidationError` set the + manifest validators produce. Add table tests that run both on the same fixture + IR/manifest and assert identical error slices during the parallel-existence + window (Steps 1–4). +- Integration: the Step 0 build-orchestration golden test (IR snapshot + + output file list) gates every step. +- End-to-end: `scripts/test-go-modules.sh` (root + nested adapter modules). +- Manual: `gowdk build` on `examples/` that have buildable targets; diff + `dist/` output byte-for-byte against pre-migration output. + +## Verification Commands + +```sh +# Per-package, after each step: +go build ./... && go vet ./internal/buildgen ./internal/compiler ./internal/appgen +gofmt -l internal cmd + +# Confirm a package has shed its manifest dependency (run after its step): +grep -rl 'gowdk/internal/manifest' internal/buildgen --include='*.go' | grep -v _test.go # expect empty after Step 2 +grep -rl 'gowdk/internal/manifest' internal/compiler --include='*.go' | grep -v _test.go # expect empty after Step 3 + +# Full gate: +scripts/test-go-modules.sh +``` + +## Rollback Plan + +- Each step is a separate PR that compiles and passes tests on its own. Rolling + back is reverting that PR; later steps depend on earlier ones but no step + leaves the tree in a non-building state. +- The parallel-existence strategy (Steps 1, 3 add IR entrypoints beside the + manifest ones rather than replacing in place) means a regression in an IR + validator can be reverted to the manifest path without unwinding consumer + edits. + +## Risks + +- **Validator drift:** the IR twins must reproduce manifest validation exactly. + Mitigated by the dual-run table tests during the parallel window (Tests §). +- **Lossy shim removal:** `buildModelFromIR` reconstructs manifest from IR; if + any buildgen render helper depended on a manifest field NOT present in IR, + Step 2 surfaces it as a compile error → add the field to `gwdkir` first + (architecture doc rule: "add fields to IR first"). Audit for this in Step 2. +- **Span/value type sharing:** `gwdkir`, `gwdkast`, and `manifest` share span + and value types (80 / 35 refs). Decide ownership early — likely move the + shared value types into a neutral package or `gwdkir` to avoid a residual + manifest import just for `SourceSpan`. +- **Scope creep into public JSON:** explicitly fenced off. If a reviewer pushes + to also migrate `json.go`, that is a separate release-gated decision. + +## Suggested PR Sequence + +1. Step 0 golden test (safety net). +2. Step 1 IR-native compiler validation (parallel, additive). +3. Step 2 delete buildgen shim + buildgen field migration. **Highest payoff.** +4. Step 3 IR-native backend binding/discovery + CLI orchestration. +5. Step 4 appgen + lang/lsp. +6. Step 5 AST→IR backbone; isolate manifest to public JSON only. diff --git a/docs/engineering/architecture.md b/docs/engineering/architecture.md index 86f13e4..92f8171 100644 --- a/docs/engineering/architecture.md +++ b/docs/engineering/architecture.md @@ -107,6 +107,17 @@ code still carries manifest compatibility records while migration continues: compatibility records for older compiler entrypoints. - `internal/manifest` remains the public manifest/site-map compatibility model. +Shared leaf value types (source spans, route params, inline scripts, backend +binding/signature enums) now live in the neutral `internal/source` package. +`internal/manifest` re-exports them as type aliases for backward compatibility. +Because of this, `internal/gwdkir`, `internal/gwdkast`, and `internal/contractscan` +no longer depend on `internal/manifest` at all — the IR is a manifest-independent +handoff. `internal/buildgen` render helpers consume IR page/component/layout +models directly (validating through `compiler.ValidateProgram`), and +`internal/appgen` references shared leaf types from `internal/source`. The +remaining manifest coupling is concentrated in `internal/compiler` (which +validates the manifest model) and the public `manifest.Manifest` entrypoints. + New generated-output work should consume `internal/gwdkir.Program` or add fields there first. Removing the remaining compatibility records is planned after the parser, public manifest JSON, and legacy compiler entrypoints stop depending on @@ -119,8 +130,8 @@ Current manifest compatibility users: | `internal/parser` | Produces `manifest.Page`, `manifest.Component`, and `manifest.Layout` records for existing CLI and compiler entrypoints. | Keep typed AST as the parser source of truth; remove direct manifest output after all callers lower through analyzer/IR. | | `internal/gwdkanalysis` | Lowers typed AST into manifest records, then builds `gwdkir.Program` from those records. | Lower typed AST directly into IR and keep manifest output as a separate compatibility adapter. | | `internal/compiler` | Validators, backend binding discovery, route conflict checks, and policy checks still consume `manifest.Manifest`. | Move validation and binding metadata to IR models; keep adapters only for public manifest and legacy entrypoints. | -| `internal/buildgen` | Public `Build`/`BuildMemory` entrypoints accept `manifest.Manifest`; supported generation paths derive or consume IR but still convert IR back to manifest models for older render helpers. | Make render helpers consume IR-native page/component/layout models, then keep manifest entrypoints as thin adapters. | -| `internal/appgen` | Public route helpers and some backend adapter types still use manifest page, endpoint, and backend-binding records. | Keep app route planning and backend adapter planning IR-first; replace manifest-only helper entrypoints with IR helpers before removing compatibility constructors. | +| `internal/buildgen` | Render helpers now consume IR-native page/component/layout/blocks models directly. Public `Build`/`BuildMemory` entrypoints still accept `manifest.Manifest` for compatibility, and a small `gotypes` bridge plus the public `SSRArtifact.LoadBinding` output type keep manifest references at the package edges. | Keep manifest entrypoints as thin adapters; remove the `gotypes` bridge once `internal/gotypes` accepts IR types. | +| `internal/appgen` | Shared leaf types come from `internal/source`. The `manifest.BackendBinding` struct currency (carrying Kind/PageID/Method/Route) and public `manifest.Manifest` route-helper entrypoints remain. | Keep app route planning and backend adapter planning IR-first; replace manifest-only helper entrypoints with IR helpers before removing compatibility constructors. | | `internal/lang` and `cmd/gowdk` | CLI `check`, `manifest`, `sitemap`, `routes`, and build setup still pass manifest records between parse, validate, report, and generation steps. | Parse/analyze into IR once per command, then derive public JSON and reports from IR adapters. | | `internal/lsp` | Open-document completions and hover build an IR snapshot before indexing page, component, layout, and endpoint symbols; workspace component definition lookup indexes IR components; diagnostics and open-buffer owner context still use language tooling and parser compatibility records. | Keep open-document source spans, but drive remaining project-wide symbols and diagnostics from analyzer/IR snapshots. | | Tests | Many generator and validator tests construct manifest records directly. | Keep fixture-style manifest construction until the tested package exposes IR-native helpers; update tests alongside each migrated package. | @@ -129,12 +140,19 @@ Migration order: 1. Keep `gwdkir.Program` as the only source of truth for new generated-output fields. -2. Move compiler validation and backend binding records to IR or IR-adjacent - structs. -3. Replace buildgen/appgen manifest render helpers with IR-native helpers. -4. Convert CLI reports, sitemap output, and LSP project metadata to IR-derived +2. (done) Extract shared leaf value types into `internal/source` so the IR and + manifest both reference them from a neutral home; `gwdkir` no longer depends + on `manifest`. +3. (done) Replace buildgen render helpers with IR-native helpers and route + buildgen validation through `compiler.ValidateProgram`. +4. Move compiler validation and backend binding records to IR or IR-adjacent + structs (compiler still validates the manifest model today). This is a + redesign — IR lowering is currently lossy for standalone-endpoint validation, + so the IR must be enriched and the flip guarded against diagnostic drift. + Tracked in issue #145. +5. Convert CLI reports, sitemap output, and LSP project metadata to IR-derived adapters. -5. Keep public manifest JSON compatibility until a release plan explicitly +6. Keep public manifest JSON compatibility until a release plan explicitly deprecates or replaces it. ## Components diff --git a/internal/appgen/adapter_ir.go b/internal/appgen/adapter_ir.go index 97412c9..82d90c3 100644 --- a/internal/appgen/adapter_ir.go +++ b/internal/appgen/adapter_ir.go @@ -4,7 +4,7 @@ import ( "sort" "github.com/cssbruno/gowdk/internal/gwdkir" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) type BackendAdapterIR struct { @@ -46,7 +46,7 @@ type BackendHandlerCall struct { Endpoint BackendEndpointRegistration Alias string Function string - Signature manifest.BackendSignatureKind + Signature source.BackendSignatureKind InputType string } @@ -59,7 +59,7 @@ type BackendResponse struct { type BackendFallback struct { Endpoint BackendEndpointRegistration - Status manifest.BackendBindingStatus + Status source.BackendBindingStatus Message string } @@ -72,7 +72,7 @@ type BackendContractExposure struct { Result string Roles []string Guards []string - InputFields []manifest.BackendInputField + InputFields []source.BackendInputField Status gwdkir.ContractBindingStatus Handler string Register string @@ -101,7 +101,7 @@ func backendAdapterIR(options Options) BackendAdapterIR { Input: action.InputType, Fields: append([]string(nil), action.InputFields...), } - if action.Binding.Status == manifest.BackendBindingBound && action.Binding.InputType != "" { + if action.Binding.Status == source.BackendBindingBound && action.Binding.InputType != "" { decoder.Function = boundActionDecoderName(action) decoder.Input = action.Binding.InputType } else if action.InputType != "" { @@ -109,7 +109,7 @@ func backendAdapterIR(options Options) BackendAdapterIR { } ir.Decoders = append(ir.Decoders, decoder) } - if action.Binding.Status == manifest.BackendBindingBound { + if action.Binding.Status == source.BackendBindingBound { ir.Calls = append(ir.Calls, BackendHandlerCall{ Endpoint: endpoint, Alias: action.BackendAlias, @@ -118,7 +118,7 @@ func backendAdapterIR(options Options) BackendAdapterIR { InputType: action.Binding.InputType, }) } - if action.Binding.Status != "" && action.Binding.Status != manifest.BackendBindingBound { + if action.Binding.Status != "" && action.Binding.Status != source.BackendBindingBound { ir.Fallbacks = append(ir.Fallbacks, BackendFallback{ Endpoint: endpoint, Status: action.Binding.Status, @@ -142,7 +142,7 @@ func backendAdapterIR(options Options) BackendAdapterIR { Name: api.APIName, } ir.Registrations = append(ir.Registrations, endpoint) - if api.Binding.Status == manifest.BackendBindingBound { + if api.Binding.Status == source.BackendBindingBound { ir.Calls = append(ir.Calls, BackendHandlerCall{ Endpoint: endpoint, Alias: api.BackendAlias, @@ -150,7 +150,7 @@ func backendAdapterIR(options Options) BackendAdapterIR { Signature: api.Binding.Signature, }) } - if api.Binding.Status != "" && api.Binding.Status != manifest.BackendBindingBound { + if api.Binding.Status != "" && api.Binding.Status != source.BackendBindingBound { ir.Fallbacks = append(ir.Fallbacks, BackendFallback{ Endpoint: endpoint, Status: api.Binding.Status, @@ -194,7 +194,7 @@ func backendAdapterIR(options Options) BackendAdapterIR { Result: ref.Result, Roles: append([]string(nil), ref.Roles...), Guards: append([]string(nil), ref.Guards...), - InputFields: append([]manifest.BackendInputField(nil), ref.InputFields...), + InputFields: append([]source.BackendInputField(nil), ref.InputFields...), Status: ref.Status, Handler: ref.Handler, Register: ref.Register, diff --git a/internal/appgen/auto_routes.go b/internal/appgen/auto_routes.go index c7ab051..1b32682 100644 --- a/internal/appgen/auto_routes.go +++ b/internal/appgen/auto_routes.go @@ -9,7 +9,7 @@ import ( "github.com/cssbruno/gowdk/internal/buildgen" "github.com/cssbruno/gowdk/internal/gwdkir" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func resolveOptions(outputDir string, options Options) (Options, error) { @@ -88,22 +88,22 @@ func optionsIR(options Options) (gwdkir.Program, error) { func assignBackendAliases(options *Options) { paths := map[string]string{} for _, action := range options.Actions { - if action.Binding.Status == manifest.BackendBindingBound && action.Binding.ImportPath != "" { + if action.Binding.Status == source.BackendBindingBound && action.Binding.ImportPath != "" { paths[action.Binding.ImportPath] = action.Binding.PackageName } } for _, api := range options.APIs { - if api.Binding.Status == manifest.BackendBindingBound && api.Binding.ImportPath != "" { + if api.Binding.Status == source.BackendBindingBound && api.Binding.ImportPath != "" { paths[api.Binding.ImportPath] = api.Binding.PackageName } } for _, fragment := range options.Fragments { - if fragment.Binding.Status == manifest.BackendBindingBound && fragment.Binding.ImportPath != "" { + if fragment.Binding.Status == source.BackendBindingBound && fragment.Binding.ImportPath != "" { paths[fragment.Binding.ImportPath] = fragment.Binding.PackageName } } for _, route := range options.SSR { - if route.LoadBinding.Status == manifest.BackendBindingBound && route.LoadBinding.ImportPath != "" { + if route.LoadBinding.Status == source.BackendBindingBound && route.LoadBinding.ImportPath != "" { paths[route.LoadBinding.ImportPath] = route.LoadBinding.PackageName } } @@ -171,7 +171,7 @@ func ssrRoutes(artifacts []buildgen.SSRArtifact) []SSRRoute { Cache: artifact.Cache, ErrorPage: artifact.ErrorPage, DynamicParams: append([]string(nil), artifact.DynamicParams...), - RouteParams: append([]manifest.RouteParam(nil), artifact.RouteParams...), + RouteParams: append([]source.RouteParam(nil), artifact.RouteParams...), Guards: append([]string(nil), artifact.Guards...), HasLoad: artifact.HasLoad, LoadBinding: artifact.LoadBinding, diff --git a/internal/appgen/ir.go b/internal/appgen/ir.go index ebd66cf..fcdc7de 100644 --- a/internal/appgen/ir.go +++ b/internal/appgen/ir.go @@ -6,6 +6,7 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/internal/view" ) @@ -149,8 +150,8 @@ func fragmentEndpointsFromIR(ir gwdkir.Program) ([]FragmentEndpoint, error) { return endpoints, nil } -func renderFragmentHTML(source string, packageName string, uses map[string]string, components map[string]view.Component) (string, error) { - return view.RenderWithOptions(source, componentRegistryForFragment(packageName, uses, components), nil, view.Options{ +func renderFragmentHTML(body string, packageName string, uses map[string]string, components map[string]view.Component) (string, error) { + return view.RenderWithOptions(body, componentRegistryForFragment(packageName, uses, components), nil, view.Options{ Package: packageName, Uses: uses, }) @@ -285,7 +286,7 @@ func irBindingsByEndpoint(endpoints []gwdkir.Endpoint) map[string]manifest.Backe Signature: endpoint.Binding.Signature, InputType: endpoint.Binding.InputType, InputPointer: endpoint.Binding.InputPointer, - InputFields: append([]manifest.BackendInputField(nil), endpoint.Binding.InputFields...), + InputFields: append([]source.BackendInputField(nil), endpoint.Binding.InputFields...), Status: endpoint.Binding.Status, Message: endpoint.Binding.Message, } @@ -312,7 +313,7 @@ func actionFragmentsFromIR(action gwdkir.Action) ([]ActionFragment, error) { return fragments, nil } -func bindingInputFieldNames(fields []manifest.BackendInputField) []string { +func bindingInputFieldNames(fields []source.BackendInputField) []string { out := make([]string, 0, len(fields)) for _, field := range fields { out = append(out, field.FormName) diff --git a/internal/appgen/scripts.go b/internal/appgen/scripts.go index 2e8ab6f..b0ead74 100644 --- a/internal/appgen/scripts.go +++ b/internal/appgen/scripts.go @@ -10,6 +10,7 @@ import ( "github.com/cssbruno/gowdk/internal/goblockgen" "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) type inlineGoBlockGroup struct { @@ -32,15 +33,15 @@ func writeInlineGoBlockFiles(appDir string, options Options) ([]string, error) { } var files []string for packageName, group := range groups { - source, err := goblockgen.Source(packageName, group.imports, group.goBlocks) + generated, err := goblockgen.Source(packageName, group.imports, group.goBlocks) if err != nil { return nil, fmt.Errorf("generate inline go block package %s: %w", packageName, err) } - if len(source) == 0 { + if len(generated) == 0 { continue } relPath := goblockgen.GeneratedRelPath(packageName) - if err := writeFileIfChanged(filepath.Join(appDir, relPath), source); err != nil { + if err := writeFileIfChanged(filepath.Join(appDir, relPath), generated); err != nil { return nil, err } files = append(files, relPath) @@ -162,15 +163,15 @@ func writeAddonGoBlockFiles(appDir string, options Options) ([]string, error) { if err != nil { return nil, err } - source := []byte(file.Source) + contents := []byte(file.Source) if strings.HasSuffix(relPath, ".go") { - formatted, err := format.Source(source) + formatted, err := format.Source(contents) if err != nil { return nil, fmt.Errorf("format addon go block file %s: %w", relPath, err) } - source = formatted + contents = formatted } - if err := writeFileIfChanged(filepath.Join(appDir, relPath), source); err != nil { + if err := writeFileIfChanged(filepath.Join(appDir, relPath), contents); err != nil { return nil, err } files = append(files, relPath) @@ -240,13 +241,13 @@ func addonGoBlockTargets(ir gwdkir.Program, config gowdk.Config) []addonGoBlockT return targets } -func gowdkGoBlockTarget(ownerKind string, ownerID string, packageName string, source string, target string, body string, span manifest.SourceSpan) gowdk.GoBlockTarget { +func gowdkGoBlockTarget(ownerKind string, ownerID string, packageName string, sourcePath string, target string, body string, span source.SourceSpan) gowdk.GoBlockTarget { return gowdk.GoBlockTarget{ Target: target, OwnerKind: ownerKind, OwnerID: ownerID, OwnerPackage: packageName, - SourcePath: source, + SourcePath: sourcePath, Body: body, Span: gowdk.SourceSpan{ Start: gowdk.SourcePosition{Line: span.Start.Line, Column: span.Start.Column}, diff --git a/internal/appgen/source_actions.go b/internal/appgen/source_actions.go index 9f28f62..dfceebb 100644 --- a/internal/appgen/source_actions.go +++ b/internal/appgen/source_actions.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func actionHandlerSource(actions []ActionEndpoint, csrf bool) string { @@ -59,12 +60,12 @@ func actionsUseLengthValidation(actions []ActionEndpoint) bool { } func actionsUseActionValidation(action ActionEndpoint) bool { - return action.Binding.Status != manifest.BackendBindingMissing && action.Binding.Status != manifest.BackendBindingUnsupportedSignature && action.ValidatesInput + return action.Binding.Status != source.BackendBindingMissing && action.Binding.Status != source.BackendBindingUnsupportedSignature && action.ValidatesInput } func actionsUseForm(actions []ActionEndpoint) bool { for _, action := range actions { - if action.Binding.Status != manifest.BackendBindingMissing && action.Binding.Status != manifest.BackendBindingUnsupportedSignature && actionNeedsValues(action) { + if action.Binding.Status != source.BackendBindingMissing && action.Binding.Status != source.BackendBindingUnsupportedSignature && actionNeedsValues(action) { return true } } @@ -73,7 +74,7 @@ func actionsUseForm(actions []ActionEndpoint) bool { func actionsParseForm(actions []ActionEndpoint) bool { for _, action := range actions { - if action.Binding.Status != manifest.BackendBindingMissing && action.Binding.Status != manifest.BackendBindingUnsupportedSignature { + if action.Binding.Status != source.BackendBindingMissing && action.Binding.Status != source.BackendBindingUnsupportedSignature { return true } } @@ -92,13 +93,13 @@ func sortedActionEndpoints(actions []ActionEndpoint) []ActionEndpoint { } func actionNeedsValues(action ActionEndpoint) bool { - if action.Binding.Status != manifest.BackendBindingBound { + if action.Binding.Status != source.BackendBindingBound { return true } if action.ValidatesInput { return true } - return action.Binding.Signature != manifest.BackendSignatureAction0 + return action.Binding.Signature != source.BackendSignatureAction0 } func actionFuncDecl(actions []ActionEndpoint, csrf bool, rateLimit bool) *ast.FuncDecl { @@ -143,7 +144,7 @@ func actionCaseStmts(action ActionEndpoint, csrf bool, rateLimit bool) []ast.Stm } stmts = append(stmts, rateLimitStmts(rateLimit)...) stmts = append(stmts, guardStmts(action.Guards)...) - if action.Binding.Status != "" && action.Binding.Status != manifest.BackendBindingBound { + if action.Binding.Status != "" && action.Binding.Status != source.BackendBindingBound { stmts = append(stmts, backendNotImplementedStmts(action.Binding, "action")...) stmts = append(stmts, returnBool(true)) return stmts @@ -153,7 +154,7 @@ func actionCaseStmts(action ActionEndpoint, csrf bool, rateLimit bool) []ast.Stm stmts = append(stmts, define([]ast.Expr{id("values")}, call(sel("gowdkform", "FromURLValues"), selExpr(id("request"), "PostForm")))) } stmts = append(stmts, actionInputDecodeStmts(action)...) - if action.Binding.Status == manifest.BackendBindingBound { + if action.Binding.Status == source.BackendBindingBound { stmts = append(stmts, boundActionResultStmts(action)...) } else { stmts = append(stmts, actionPartialBranchStmts(action)...) @@ -199,7 +200,7 @@ func actionParseFormStmts(csrf bool) []ast.Stmt { } func actionInputDecodeStmts(action ActionEndpoint) []ast.Stmt { - if action.Binding.Status == manifest.BackendBindingBound { + if action.Binding.Status == source.BackendBindingBound { return boundActionInputDecodeStmts(action) } if action.InputType == "" { @@ -219,18 +220,18 @@ func actionInputDecodeStmts(action ActionEndpoint) []ast.Stmt { func boundActionInputDecodeStmts(action ActionEndpoint) []ast.Stmt { switch action.Binding.Signature { - case manifest.BackendSignatureAction0: + case source.BackendSignatureAction0: if action.ValidatesInput { return actionRequiredValidationStmts(action) } return nil - case manifest.BackendSignatureActionValues: + case source.BackendSignatureActionValues: stmts := expectedValuesStmts(action) if action.ValidatesInput { stmts = append(stmts, actionRequiredValidationStmts(action)...) } return stmts - case manifest.BackendSignatureActionForm, manifest.BackendSignatureActionFormPtr: + case source.BackendSignatureActionForm, source.BackendSignatureActionFormPtr: stmts := expectedValuesStmts(action) stmts = append(stmts, define([]ast.Expr{id("input"), id("err")}, call(sel(boundActionDecoderName(action)), id("values"))), @@ -364,10 +365,10 @@ func actionValidationMessage(custom string, fallback string) string { func boundActionResultStmts(action ActionEndpoint) []ast.Stmt { args := []ast.Expr{id("ctx")} switch action.Binding.Signature { - case manifest.BackendSignatureAction0: - case manifest.BackendSignatureActionForm: + case source.BackendSignatureAction0: + case source.BackendSignatureActionForm: args = append(args, id("input")) - case manifest.BackendSignatureActionFormPtr: + case source.BackendSignatureActionFormPtr: args = append(args, &ast.UnaryExpr{Op: token.AND, X: id("input")}) default: args = append(args, id("values")) @@ -550,7 +551,7 @@ func actionDecoderDecls(actions []ActionEndpoint) []ast.Decl { }) } for _, action := range actions { - if action.Binding.Status == manifest.BackendBindingBound || action.Binding.Status == manifest.BackendBindingMissing || action.Binding.Status == manifest.BackendBindingUnsupportedSignature || action.InputType == "" { + if action.Binding.Status == source.BackendBindingBound || action.Binding.Status == source.BackendBindingMissing || action.Binding.Status == source.BackendBindingUnsupportedSignature || action.InputType == "" { continue } decls = append(decls, valuesActionDecoderDecl(action)) @@ -591,7 +592,7 @@ func uniqueInputTypes(actions []ActionEndpoint) []string { seen := map[string]bool{} var types []string for _, action := range actions { - if action.Binding.Status == manifest.BackendBindingBound || action.Binding.Status == manifest.BackendBindingMissing || action.Binding.Status == manifest.BackendBindingUnsupportedSignature || action.InputType == "" || seen[action.InputType] { + if action.Binding.Status == source.BackendBindingBound || action.Binding.Status == source.BackendBindingMissing || action.Binding.Status == source.BackendBindingUnsupportedSignature || action.InputType == "" || seen[action.InputType] { continue } seen[action.InputType] = true @@ -602,10 +603,10 @@ func uniqueInputTypes(actions []ActionEndpoint) []string { } func actionUsesBoundInputDecoder(action ActionEndpoint) bool { - if action.Binding.Status != manifest.BackendBindingBound { + if action.Binding.Status != source.BackendBindingBound { return false } - return action.Binding.Signature == manifest.BackendSignatureActionForm || action.Binding.Signature == manifest.BackendSignatureActionFormPtr + return action.Binding.Signature == source.BackendSignatureActionForm || action.Binding.Signature == source.BackendSignatureActionFormPtr } func boundActionDecoderDecl(action ActionEndpoint) *ast.FuncDecl { @@ -622,7 +623,7 @@ func boundActionDecoderDecl(action ActionEndpoint) *ast.FuncDecl { }, []*ast.Field{{Type: inputType}, {Type: id("error")}}, stmts) } -func boundActionFieldDecodeStmts(index int, field manifest.BackendInputField) []ast.Stmt { +func boundActionFieldDecodeStmts(index int, field source.BackendInputField) []ast.Stmt { value := id(fmtFieldValueName(index)) switch field.Type { case "string": @@ -646,7 +647,7 @@ func boundActionFieldDecodeStmts(index int, field manifest.BackendInputField) [] } } -func boundActionScalarFieldDecodeStmts(value *ast.Ident, field manifest.BackendInputField, decode ast.Expr, assignment ast.Expr) []ast.Stmt { +func boundActionScalarFieldDecodeStmts(value *ast.Ident, field source.BackendInputField, decode ast.Expr, assignment ast.Expr) []ast.Stmt { return []ast.Stmt{ define([]ast.Expr{value, id("ok"), id("err")}, decode), &ast.IfStmt{ diff --git a/internal/appgen/source_api.go b/internal/appgen/source_api.go index d6578f2..fbe2d33 100644 --- a/internal/appgen/source_api.go +++ b/internal/appgen/source_api.go @@ -4,7 +4,7 @@ import ( "go/ast" "go/token" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func apiHandlerSource(apis []APIEndpoint) string { @@ -62,7 +62,7 @@ func apiCaseStmts(api APIEndpoint, rateLimit bool) []ast.Stmt { } stmts = append(stmts, rateLimitStmts(rateLimit)...) stmts = append(stmts, guardStmts(api.Guards)...) - if api.Binding.Status != manifest.BackendBindingBound { + if api.Binding.Status != source.BackendBindingBound { stmts = append(stmts, backendNotImplementedStmts(api.Binding, "API")...) stmts = append(stmts, returnBool(true)) return stmts diff --git a/internal/appgen/source_contracts.go b/internal/appgen/source_contracts.go index 05bba67..40a804d 100644 --- a/internal/appgen/source_contracts.go +++ b/internal/appgen/source_contracts.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/cssbruno/gowdk/internal/gwdkir" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func contractHandlerDecls(exposures []BackendContractExposure, csrf bool, rateLimit bool) []ast.Decl { @@ -187,7 +187,7 @@ func contractDecoderDecl(exposure BackendContractExposure) *ast.FuncDecl { }, []*ast.Field{{Type: inputType}, {Type: id("error")}}, stmts) } -func contractFormSchemaExpr(fields []manifest.BackendInputField) ast.Expr { +func contractFormSchemaExpr(fields []source.BackendInputField) ast.Expr { names := make([]string, 0, len(fields)) for _, field := range fields { names = append(names, field.FormName) diff --git a/internal/appgen/source_fragments.go b/internal/appgen/source_fragments.go index e592854..118fe62 100644 --- a/internal/appgen/source_fragments.go +++ b/internal/appgen/source_fragments.go @@ -5,7 +5,7 @@ import ( "go/token" "sort" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func fragmentFuncDecl(fragments []FragmentEndpoint, rateLimit bool) *ast.FuncDecl { @@ -50,12 +50,12 @@ func fragmentCaseStmts(fragment FragmentEndpoint, rateLimit bool) []ast.Stmt { stmts := endpointContextStmts("fragment", fragment.PageID, fragment.FragmentName, fragment.Method, fragment.Route, "") stmts = append(stmts, rateLimitStmts(rateLimit)...) stmts = append(stmts, guardStmts(fragment.Guards)...) - if fragment.Binding.Status == manifest.BackendBindingUnsupportedSignature { + if fragment.Binding.Status == source.BackendBindingUnsupportedSignature { stmts = append(stmts, backendNotImplementedStmts(fragment.Binding, "fragment")...) stmts = append(stmts, returnBool(true)) return stmts } - if fragment.Binding.Status == manifest.BackendBindingBound { + if fragment.Binding.Status == source.BackendBindingBound { stmts = append(stmts, define([]ast.Expr{id("result"), id("err")}, call(sel(fragment.BackendAlias, fragment.Binding.FunctionName), id("ctx"))), &ast.IfStmt{ diff --git a/internal/appgen/source_ssr.go b/internal/appgen/source_ssr.go index 93cc745..a5d1485 100644 --- a/internal/appgen/source_ssr.go +++ b/internal/appgen/source_ssr.go @@ -5,7 +5,7 @@ import ( "go/token" "sort" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func ssrHandlerSource(routes []SSRRoute) string { @@ -106,7 +106,7 @@ func ssrLoadStmts(route SSRRoute) []ast.Stmt { if !route.HasLoad { return nil } - if route.LoadBinding.Status != manifest.BackendBindingBound { + if route.LoadBinding.Status != source.BackendBindingBound { stmts := backendNotImplementedStmts(route.LoadBinding, "SSR load") stmts = append(stmts, returnBool(true)) return stmts @@ -116,7 +116,7 @@ func ssrLoadStmts(route SSRRoute) []ast.Stmt { } loadCall := call(sel(route.LoadBackendAlias, route.LoadBinding.FunctionName), id("loadContext")) switch route.LoadBinding.Signature { - case manifest.BackendSignatureLoadError: + case source.BackendSignatureLoadError: stmts = append(stmts, define([]ast.Expr{id("loadData"), id("err")}, loadCall), &ast.IfStmt{ @@ -220,7 +220,7 @@ func ssrRouteContextStmts(route SSRRoute, includeParams bool) []ast.Stmt { return stmts } -func typedRouteParamStmts(params []manifest.RouteParam) []ast.Stmt { +func typedRouteParamStmts(params []source.RouteParam) []ast.Stmt { if len(params) == 0 { return nil } @@ -301,7 +301,7 @@ func routeParamDecodeFunc(paramType string) string { } } -func routeParamMetadataExpr(params []manifest.RouteParam) ast.Expr { +func routeParamMetadataExpr(params []source.RouteParam) ast.Expr { elts := make([]ast.Expr, 0, len(params)) for _, param := range params { paramType := param.Type diff --git a/internal/appgen/types.go b/internal/appgen/types.go index 9a56eec..f222cc2 100644 --- a/internal/appgen/types.go +++ b/internal/appgen/types.go @@ -4,6 +4,7 @@ import ( "github.com/cssbruno/gowdk" "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) // Result describes generated app artifacts. @@ -102,7 +103,7 @@ type SSRRoute struct { Cache string ErrorPage string DynamicParams []string - RouteParams []manifest.RouteParam + RouteParams []source.RouteParam Guards []string HasLoad bool LoadBinding manifest.BackendBinding diff --git a/internal/buildgen/build.go b/internal/buildgen/build.go index aab80f6..b1be725 100644 --- a/internal/buildgen/build.go +++ b/internal/buildgen/build.go @@ -14,15 +14,15 @@ import ( ) func Build(config gowdk.Config, app manifest.Manifest, outputDir string) (Result, error) { - return buildFromIR(config, app, gwdkanalysis.BuildIR(config, app), outputDir) + return buildFromIR(config, gwdkanalysis.BuildIR(config, app), app.BackendBindings, outputDir) } // BuildFromIR writes SPA build artifacts from normalized compiler IR. func BuildFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string) (Result, error) { - return buildFromIR(config, buildModelFromIR(ir), ir, outputDir) + return buildFromIR(config, ir, compiler.BackendBindingsFromIR(ir), outputDir) } -func buildFromIR(config gowdk.Config, app manifest.Manifest, ir gwdkir.Program, outputDir string) (Result, error) { +func buildFromIR(config gowdk.Config, ir gwdkir.Program, backendBindings []manifest.BackendBinding, outputDir string) (Result, error) { reporter := newBuildReporter("build", outputDir) reporter.info("start", "build_started", "SPA build started", BuildEvent{ Data: map[string]string{ @@ -34,13 +34,13 @@ func buildFromIR(config gowdk.Config, app manifest.Manifest, ir gwdkir.Program, if strings.TrimSpace(outputDir) == "" { return Result{}, reporter.fail("validate", fmt.Errorf("build output directory is required")) } - if err := compiler.ValidateManifest(config, app); err != nil { + if err := compiler.ValidateProgram(config, ir); err != nil { return Result{}, reporter.fail("validate", err) } reporter.info("validate", "manifest_valid", "manifest validation completed", BuildEvent{}) - reportBackendBindings(reporter, app.BackendBindings) + reportBackendBindings(reporter, backendBindings) reportContractReferences(reporter, ir.ContractRefs) - if err := compiler.ValidateBackendBindingPolicy(config, app); err != nil { + if err := compiler.ValidateBackendBindingPolicyIR(config, ir); err != nil { return Result{}, reporter.fail("bind", err) } @@ -119,16 +119,16 @@ func buildFromIR(config gowdk.Config, app manifest.Manifest, ir gwdkir.Program, } func BuildMemory(config gowdk.Config, app manifest.Manifest, outputDir string) (MemoryResult, error) { - return buildMemoryFromIR(config, app, gwdkanalysis.BuildIR(config, app), outputDir) + return buildMemoryFromIR(config, gwdkanalysis.BuildIR(config, app), app.BackendBindings, outputDir) } // BuildMemoryFromIR plans SPA build artifacts from normalized compiler IR // without writing them to disk. func BuildMemoryFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string) (MemoryResult, error) { - return buildMemoryFromIR(config, buildModelFromIR(ir), ir, outputDir) + return buildMemoryFromIR(config, ir, compiler.BackendBindingsFromIR(ir), outputDir) } -func buildMemoryFromIR(config gowdk.Config, app manifest.Manifest, ir gwdkir.Program, outputDir string) (MemoryResult, error) { +func buildMemoryFromIR(config gowdk.Config, ir gwdkir.Program, backendBindings []manifest.BackendBinding, outputDir string) (MemoryResult, error) { reporter := newBuildReporter("memory", outputDir) reporter.info("start", "build_started", "in-memory SPA build started", BuildEvent{ Data: map[string]string{ @@ -140,13 +140,13 @@ func buildMemoryFromIR(config gowdk.Config, app manifest.Manifest, ir gwdkir.Pro if strings.TrimSpace(outputDir) == "" { return MemoryResult{}, reporter.fail("validate", fmt.Errorf("build output directory is required")) } - if err := compiler.ValidateManifest(config, app); err != nil { + if err := compiler.ValidateProgram(config, ir); err != nil { return MemoryResult{}, reporter.fail("validate", err) } reporter.info("validate", "manifest_valid", "manifest validation completed", BuildEvent{}) - reportBackendBindings(reporter, app.BackendBindings) + reportBackendBindings(reporter, backendBindings) reportContractReferences(reporter, ir.ContractRefs) - if err := compiler.ValidateBackendBindingPolicy(config, app); err != nil { + if err := compiler.ValidateBackendBindingPolicyIR(config, ir); err != nil { return MemoryResult{}, reporter.fail("bind", err) } @@ -336,16 +336,16 @@ func reportContractReferences(reporter *buildReporter, refs []gwdkir.ContractRef } func BuildIncremental(config gowdk.Config, app manifest.Manifest, outputDir string, changedPageSources []string) (Result, error) { - return buildIncrementalFromIR(config, app, gwdkanalysis.BuildIR(config, app), outputDir, changedPageSources) + return buildIncrementalFromIR(config, gwdkanalysis.BuildIR(config, app), outputDir, changedPageSources) } // BuildIncrementalFromIR incrementally renders changed SPA page outputs from // normalized compiler IR. func BuildIncrementalFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string, changedPageSources []string) (Result, error) { - return buildIncrementalFromIR(config, buildModelFromIR(ir), ir, outputDir, changedPageSources) + return buildIncrementalFromIR(config, ir, outputDir, changedPageSources) } -func buildIncrementalFromIR(config gowdk.Config, app manifest.Manifest, ir gwdkir.Program, outputDir string, changedPageSources []string) (Result, error) { +func buildIncrementalFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string, changedPageSources []string) (Result, error) { reporter := newBuildReporter("incremental", outputDir) reporter.info("start", "build_started", "incremental SPA build started", BuildEvent{ Data: map[string]string{ @@ -356,19 +356,19 @@ func buildIncrementalFromIR(config gowdk.Config, app manifest.Manifest, ir gwdki if strings.TrimSpace(outputDir) == "" { return Result{}, reporter.fail("validate", fmt.Errorf("build output directory is required")) } - if err := compiler.ValidateManifest(config, app); err != nil { + if err := compiler.ValidateProgram(config, ir); err != nil { return Result{}, reporter.fail("validate", err) } reporter.info("validate", "manifest_valid", "manifest validation completed", BuildEvent{}) reportContractReferences(reporter, ir.ContractRefs) - if err := compiler.ValidateBackendBindingPolicy(config, app); err != nil { + if err := compiler.ValidateBackendBindingPolicyIR(config, ir); err != nil { return Result{}, reporter.fail("bind", err) } changedPages := sourcePathSet(changedPageSources) - components, componentFailures := buildComponents(app.Components) - layouts, layoutFailures := buildLayouts(app.Layouts) - css, cssFailures := planCSS(config, app, outputDir) + components, componentFailures := buildComponents(ir.Components) + layouts, layoutFailures := buildLayouts(ir.Layouts) + css, cssFailures := planCSS(config, ir, outputDir) scopedJS, scopedJSFailures := planScopedJSAssets(ir.Assets, outputDir) baseStylesheets := append([]gowdk.Stylesheet{}, config.Build.Stylesheets...) baseStylesheets = append(baseStylesheets, css.stylesheets...) @@ -381,7 +381,7 @@ func buildIncrementalFromIR(config gowdk.Config, app manifest.Manifest, ir gwdki if len(failures) > 0 { return Result{}, reporter.fail("plan", errors.New(strings.Join(failures, "\n"))) } - runtime, err := runtimeArtifacts(config, app, outputDir, layouts, components) + runtime, err := runtimeArtifacts(config, ir, outputDir, layouts, components) if err != nil { return Result{}, reporter.fail("plan", err) } @@ -394,7 +394,7 @@ func buildIncrementalFromIR(config gowdk.Config, app manifest.Manifest, ir gwdki }) result := Result{ - Artifacts: make([]Artifact, 0, len(app.Pages)), + Artifacts: make([]Artifact, 0, len(ir.Pages)), CSSArtifacts: make([]CSSArtifact, 0, len(css.assets)), AssetArtifacts: make([]AssetArtifact, 0, 1), } @@ -422,7 +422,7 @@ func buildIncrementalFromIR(config gowdk.Config, app manifest.Manifest, ir gwdki } seenOutputPaths := map[string]string{} - for _, page := range app.Pages { + for _, page := range ir.Pages { if isRequestTimePage(config, page) { continue } @@ -511,10 +511,9 @@ func plan(config gowdk.Config, app manifest.Manifest, outputDir string) (buildPl } func planFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string) (buildPlan, error) { - app := buildModelFromIR(ir) - components, componentFailures := buildComponents(app.Components) - layouts, layoutFailures := buildLayouts(app.Layouts) - css, cssFailures := planCSS(config, app, outputDir) + components, componentFailures := buildComponents(ir.Components) + layouts, layoutFailures := buildLayouts(ir.Layouts) + css, cssFailures := planCSS(config, ir, outputDir) componentAssets, componentAssetFailures := planComponentFileAssets(ir.Assets, outputDir) scopedJS, scopedJSFailures := planScopedJSAssets(ir.Assets, outputDir) baseStylesheets := append([]gowdk.Stylesheet{}, config.Build.Stylesheets...) @@ -527,7 +526,7 @@ func planFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string) (build failures = append(failures, cssFailures...) failures = append(failures, componentAssetFailures...) failures = append(failures, scopedJSFailures...) - for _, page := range app.Pages { + for _, page := range ir.Pages { if isRequestTimePage(config, page) { continue } @@ -555,7 +554,7 @@ func planFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string) (build if len(failures) > 0 { return buildPlan{}, errors.New(strings.Join(failures, "\n")) } - runtime, err := runtimeArtifacts(config, app, outputDir, layouts, components) + runtime, err := runtimeArtifacts(config, ir, outputDir, layouts, components) if err != nil { return buildPlan{}, err } diff --git a/internal/buildgen/build_data_runner.go b/internal/buildgen/build_data_runner.go index 260d37f..c928a6e 100644 --- a/internal/buildgen/build_data_runner.go +++ b/internal/buildgen/build_data_runner.go @@ -15,10 +15,10 @@ import ( "strconv" "strings" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" ) -func runBuildDataCallRef(ref buildCallRef, imports []manifest.Import, scripts []manifest.GoBlock, source string) (map[string]string, error) { +func runBuildDataCallRef(ref buildCallRef, imports []gwdkir.Import, scripts []gwdkir.GoBlock, source string) (map[string]string, error) { if ref.Alias == "" { if script, ok := packageScriptWithFunction(scripts, ref.Function); ok { return runInlineBuildDataCall(script, imports, source, ref.Function) @@ -36,14 +36,14 @@ func runBuildDataCallRef(ref buildCallRef, imports []manifest.Import, scripts [] return runBuildDataCall(ref.Alias, item.Path, ref.Function) } -func packageScriptWithFunction(scripts []manifest.GoBlock, function string) (manifest.GoBlock, bool) { +func packageScriptWithFunction(scripts []gwdkir.GoBlock, function string) (gwdkir.GoBlock, bool) { for _, script := range scripts { if !isStaticPackageGoBlockTarget(script.Target) { continue } file, err := parseInlineGoBlockFile(script, "gowdkinline") if err != nil { - return manifest.GoBlock{}, false + return gwdkir.GoBlock{}, false } for _, declaration := range file.Decls { functionDeclaration, ok := declaration.(*ast.FuncDecl) @@ -52,7 +52,7 @@ func packageScriptWithFunction(scripts []manifest.GoBlock, function string) (man } } } - return manifest.GoBlock{}, false + return gwdkir.GoBlock{}, false } func isStaticPackageGoBlockTarget(target string) bool { @@ -64,7 +64,7 @@ func isStaticPackageGoBlockTarget(target string) bool { } } -func runInlineBuildDataCall(script manifest.GoBlock, imports []manifest.Import, source string, function string) (map[string]string, error) { +func runInlineBuildDataCall(script gwdkir.GoBlock, imports []gwdkir.Import, source string, function string) (map[string]string, error) { runnerSource, err := inlineBuildDataRunnerSource(script, imports, source, function) if err != nil { return nil, err @@ -91,7 +91,7 @@ func runInlineBuildDataCall(script manifest.GoBlock, imports []manifest.Import, return parseBuildFunctionOutput(output) } -func inlineBuildDataRunnerSource(script manifest.GoBlock, imports []manifest.Import, source string, function string) (string, error) { +func inlineBuildDataRunnerSource(script gwdkir.GoBlock, imports []gwdkir.Import, source string, function string) (string, error) { if !isLiteralName(function) { return "", fmt.Errorf("invalid build function name %q", function) } @@ -121,7 +121,7 @@ func inlineBuildDataRunnerSource(script manifest.GoBlock, imports []manifest.Imp return string(formatted), nil } -func parseInlineGoBlockFile(script manifest.GoBlock, packageName string) (*ast.File, error) { +func parseInlineGoBlockFile(script gwdkir.GoBlock, packageName string) (*ast.File, error) { source := "package " + packageName + "\n" + script.Body file, err := parser.ParseFile(token.NewFileSet(), "inline-script.gwdk.go", source, parser.AllErrors) if err != nil { @@ -142,7 +142,7 @@ func packageNameForInlineScript(source string) string { return "gowdkinline" } -func inlineBuildDataImportDecl(imports []manifest.Import, scriptFile *ast.File) ast.Decl { +func inlineBuildDataImportDecl(imports []gwdkir.Import, scriptFile *ast.File) ast.Decl { specs := []ast.Spec{ &ast.ImportSpec{Name: ast.NewIdent("gowdkjson"), Path: buildDataStringLit("encoding/json")}, &ast.ImportSpec{Name: ast.NewIdent("gowdkos"), Path: buildDataStringLit("os")}, @@ -296,13 +296,13 @@ func runBuildDataCall(alias, importPath, function string) (map[string]string, er return parseBuildFunctionOutput(output) } -func findBuildImport(alias string, imports []manifest.Import) (manifest.Import, bool) { +func findBuildImport(alias string, imports []gwdkir.Import) (gwdkir.Import, bool) { for _, item := range imports { if item.Alias == alias { return item, true } } - return manifest.Import{}, false + return gwdkir.Import{}, false } func buildDataRunnerSource(alias, importPath, function string) (string, error) { diff --git a/internal/buildgen/component_imports.go b/internal/buildgen/component_imports.go index b3e6ca5..c0a0ac1 100644 --- a/internal/buildgen/component_imports.go +++ b/internal/buildgen/component_imports.go @@ -3,7 +3,7 @@ package buildgen import ( "sort" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/view" ) @@ -16,7 +16,7 @@ func componentRegistryKey(packageName, componentName string) string { return internalComponentKeyPrefix + packageName + "." + componentName } -func componentRegistryForPage(page manifest.Page, registry map[string]view.Component) map[string]view.Component { +func componentRegistryForPage(page gwdkir.Page, registry map[string]view.Component) map[string]view.Component { if page.Package == "" && len(page.Uses) == 0 { return registry } @@ -68,7 +68,7 @@ func sortedViewComponents(registry map[string]view.Component) []view.Component { return out } -func manifestComponentIdentity(component manifest.Component) string { +func manifestComponentIdentity(component gwdkir.Component) string { if component.Package == "" { return component.Name } diff --git a/internal/buildgen/components.go b/internal/buildgen/components.go index 3f3f97b..bac7ce4 100644 --- a/internal/buildgen/components.go +++ b/internal/buildgen/components.go @@ -9,11 +9,12 @@ import ( "github.com/cssbruno/gowdk/internal/clientlang" "github.com/cssbruno/gowdk/internal/cssscope" "github.com/cssbruno/gowdk/internal/gotypes" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/internal/view" ) -func buildComponents(components []manifest.Component) (map[string]view.Component, []string) { +func buildComponents(components []gwdkir.Component) (map[string]view.Component, []string) { registry := map[string]view.Component{} var failures []string for _, component := range components { @@ -96,7 +97,7 @@ func buildComponents(components []manifest.Component) (map[string]view.Component return registry, failures } -func viewInlineScripts(scripts []manifest.InlineScript) []view.InlineScript { +func viewInlineScripts(scripts []source.InlineScript) []view.InlineScript { if len(scripts) == 0 { return nil } @@ -107,14 +108,14 @@ func viewInlineScripts(scripts []manifest.InlineScript) []view.InlineScript { return out } -func componentDefaultIsland(component manifest.Component) string { +func componentDefaultIsland(component gwdkir.Component) string { if strings.TrimSpace(component.WASM.Package) != "" { return "wasm" } return "" } -func componentScopeIDs(component manifest.Component) []string { +func componentScopeIDs(component gwdkir.Component) []string { if len(component.CSS) == 0 && strings.TrimSpace(component.Blocks.StyleBody) == "" { return nil } @@ -130,7 +131,7 @@ func componentScopeIDs(component manifest.Component) []string { return scopeIDs } -func componentUses(uses []manifest.Use) map[string]string { +func componentUses(uses []gwdkir.Use) map[string]string { if len(uses) == 0 { return nil } @@ -141,7 +142,7 @@ func componentUses(uses []manifest.Use) map[string]string { return out } -func componentClientComputeds(component manifest.Component) ([]clientlang.Computed, []string) { +func componentClientComputeds(component gwdkir.Component) ([]clientlang.Computed, []string) { if !component.Blocks.Client && strings.TrimSpace(component.Blocks.ClientBody) == "" { return nil, nil } @@ -156,7 +157,7 @@ func componentClientComputeds(component manifest.Component) ([]clientlang.Comput return computeds, nil } -func componentClientRefs(component manifest.Component) (map[string]clientlang.Ref, []string) { +func componentClientRefs(component gwdkir.Component) (map[string]clientlang.Ref, []string) { if !component.Blocks.Client && strings.TrimSpace(component.Blocks.ClientBody) == "" { return nil, nil } @@ -167,7 +168,7 @@ func componentClientRefs(component manifest.Component) (map[string]clientlang.Re return program.RefMap(), nil } -func componentClientHandlers(component manifest.Component) (map[string]clientlang.Handler, string, error) { +func componentClientHandlers(component gwdkir.Component) (map[string]clientlang.Handler, string, error) { emits := componentEmits(component) if !component.Blocks.Client && strings.TrimSpace(component.Blocks.ClientBody) == "" && len(emits) == 0 { return nil, "", nil @@ -213,7 +214,7 @@ func componentClientHandlers(component manifest.Component) (map[string]clientlan return handlers, string(payload), nil } -func componentEmits(component manifest.Component) map[string]clientlang.Emit { +func componentEmits(component gwdkir.Component) map[string]clientlang.Emit { if len(component.Emits) == 0 { return nil } @@ -230,9 +231,9 @@ func componentEmits(component manifest.Component) map[string]clientlang.Emit { return out } -func componentPropNames(component manifest.Component) ([]string, []string) { +func componentPropNames(component gwdkir.Component) ([]string, []string) { if component.PropsType.Name != "" { - resolved, err := gotypes.ResolveStruct(component.Imports, component.PropsType) + resolved, err := gotypes.ResolveStruct(manifestImports(component.Imports), manifestGoTypeRef(component.PropsType)) if err != nil { return nil, []string{fmt.Sprintf("component %s props: %v", component.Name, err)} } @@ -256,15 +257,15 @@ func componentPropNames(component manifest.Component) ([]string, []string) { return props, failures } -func componentInitialState(component manifest.Component) (map[string]string, map[string]clientlang.ValueType, string, error) { +func componentInitialState(component gwdkir.Component) (map[string]string, map[string]clientlang.ValueType, string, error) { if component.State.Type.Name == "" { return nil, nil, "", nil } - resolved, err := gotypes.ResolveStruct(component.Imports, component.State.Type) + resolved, err := gotypes.ResolveStruct(manifestImports(component.Imports), manifestGoTypeRef(component.State.Type)) if err != nil { return nil, nil, "", err } - rawJSON, err := gotypes.RunStateInitJSON(component.Imports, component.State) + rawJSON, err := gotypes.RunStateInitJSON(manifestImports(component.Imports), manifestStateContract(component.State)) if err != nil { return nil, nil, "", err } @@ -313,8 +314,8 @@ func stateValueString(value any) (string, bool) { } } -func buildLayouts(layouts []manifest.Layout) (map[string]manifest.Layout, []string) { - registry := map[string]manifest.Layout{} +func buildLayouts(layouts []gwdkir.Layout) (map[string]gwdkir.Layout, []string) { + registry := map[string]gwdkir.Layout{} var failures []string for _, layout := range layouts { if layout.ID == "" { diff --git a/internal/buildgen/css.go b/internal/buildgen/css.go index 0972484..dacafec 100644 --- a/internal/buildgen/css.go +++ b/internal/buildgen/css.go @@ -10,7 +10,7 @@ import ( "github.com/cssbruno/gowdk" "github.com/cssbruno/gowdk/internal/cssscope" "github.com/cssbruno/gowdk/internal/discover" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/view" ) @@ -26,13 +26,13 @@ type cssInput struct { contents []byte } -func planCSS(config gowdk.Config, app manifest.Manifest, outputDir string) (cssPlan, []string) { +func planCSS(config gowdk.Config, ir gwdkir.Program, outputDir string) (cssPlan, []string) { planned := cssPlan{pageStylesheets: map[string][]gowdk.Stylesheet{}} var failures []string seen := map[string]bool{} - pageIDs := pageIDSet(app.Pages) + pageIDs := pageIDSet(ir.Pages) context := gowdk.CSSContext{ - Sources: cssSources(app), + Sources: cssSources(ir), OutputDir: outputDir, Build: config.Build, CSS: config.CSS, @@ -80,20 +80,20 @@ func planCSS(config gowdk.Config, app manifest.Manifest, outputDir string) (cssP inputs, inputFailures := discoverCSSInputs(config, outputDir) failures = append(failures, inputFailures...) if len(inputFailures) == 0 { - pageCSS, pageStylesheets, pageFailures := planPageCSS(config, app.Pages, outputDir, inputs, seen) + pageCSS, pageStylesheets, pageFailures := planPageCSS(config, ir.Pages, outputDir, inputs, seen) failures = append(failures, pageFailures...) planned.assets = append(planned.assets, pageCSS...) for pageID, stylesheets := range pageStylesheets { planned.pageStylesheets[pageID] = append(planned.pageStylesheets[pageID], stylesheets...) } } - layoutCSS, layoutStylesheets, layoutFailures := planLayoutStyleCSS(app.Pages, app.Layouts, outputDir, seen) + layoutCSS, layoutStylesheets, layoutFailures := planLayoutStyleCSS(ir.Pages, ir.Layouts, outputDir, seen) failures = append(failures, layoutFailures...) planned.assets = append(planned.assets, layoutCSS...) for pageID, stylesheets := range layoutStylesheets { planned.pageStylesheets[pageID] = append(planned.pageStylesheets[pageID], stylesheets...) } - componentCSS, componentStylesheets, componentFailures := planComponentCSS(app.Components, outputDir, seen) + componentCSS, componentStylesheets, componentFailures := planComponentCSS(ir.Components, outputDir, seen) failures = append(failures, componentFailures...) planned.assets = append(planned.assets, componentCSS...) planned.stylesheets = append(planned.stylesheets, componentStylesheets...) @@ -101,7 +101,7 @@ func planCSS(config gowdk.Config, app manifest.Manifest, outputDir string) (cssP return planned, failures } -func pageIDSet(pages []manifest.Page) map[string]bool { +func pageIDSet(pages []gwdkir.Page) map[string]bool { out := map[string]bool{} for _, page := range pages { out[page.ID] = true @@ -160,7 +160,7 @@ func cssInputName(filePath string) string { return strings.TrimSuffix(base, filepath.Ext(base)) } -func planPageCSS(config gowdk.Config, pages []manifest.Page, outputDir string, inputs map[string]cssInput, seenAssets map[string]bool) ([]plannedCSSArtifact, map[string][]gowdk.Stylesheet, []string) { +func planPageCSS(config gowdk.Config, pages []gwdkir.Page, outputDir string, inputs map[string]cssInput, seenAssets map[string]bool) ([]plannedCSSArtifact, map[string][]gowdk.Stylesheet, []string) { var assets []plannedCSSArtifact stylesheets := map[string][]gowdk.Stylesheet{} var failures []string @@ -199,11 +199,11 @@ func planPageCSS(config gowdk.Config, pages []manifest.Page, outputDir string, i return assets, stylesheets, failures } -func planLayoutStyleCSS(pages []manifest.Page, layouts []manifest.Layout, outputDir string, seenAssets map[string]bool) ([]plannedCSSArtifact, map[string][]gowdk.Stylesheet, []string) { +func planLayoutStyleCSS(pages []gwdkir.Page, layouts []gwdkir.Layout, outputDir string, seenAssets map[string]bool) ([]plannedCSSArtifact, map[string][]gowdk.Stylesheet, []string) { var assets []plannedCSSArtifact stylesheets := map[string][]gowdk.Stylesheet{} var failures []string - layoutRegistry := map[string]manifest.Layout{} + layoutRegistry := map[string]gwdkir.Layout{} for _, layout := range layouts { layoutRegistry[layoutRegistryKey(layout.Package, layout.ID)] = layout } @@ -249,7 +249,7 @@ func planLayoutStyleCSS(pages []manifest.Page, layouts []manifest.Layout, output return assets, stylesheets, failures } -func planComponentCSS(components []manifest.Component, outputDir string, seenAssets map[string]bool) ([]plannedCSSArtifact, []gowdk.Stylesheet, []string) { +func planComponentCSS(components []gwdkir.Component, outputDir string, seenAssets map[string]bool) ([]plannedCSSArtifact, []gowdk.Stylesheet, []string) { var assets []plannedCSSArtifact var stylesheets []gowdk.Stylesheet var failures []string @@ -326,7 +326,7 @@ func componentCSSSourcePath(componentSource string, cssPath string) (string, err return filepath.Clean(filepath.Join(baseDir, filepath.FromSlash(cssPath))), nil } -func componentCSSLogicalPath(component manifest.Component, scopeID string) string { +func componentCSSLogicalPath(component gwdkir.Component, scopeID string) string { packagePart := safeCSSPathPart(component.Package) if packagePart == "" { packagePart = "_" @@ -338,7 +338,7 @@ func componentCSSLogicalPath(component manifest.Component, scopeID string) strin return path.Join(defaultPageCSSDir, "components", packagePart, componentPart, scopeID+".css") } -func layoutStyleLogicalPath(layout manifest.Layout, scopeID string) string { +func layoutStyleLogicalPath(layout gwdkir.Layout, scopeID string) string { packagePart := safeCSSPathPart(layout.Package) if packagePart == "" { packagePart = "_" @@ -441,7 +441,7 @@ func rewriteStylesheets(stylesheets []gowdk.Stylesheet, hrefs map[string]string) return out } -func pageCSSInputNames(config gowdk.Config, page manifest.Page, inputs map[string]cssInput) ([]string, error) { +func pageCSSInputNames(config gowdk.Config, page gwdkir.Page, inputs map[string]cssInput) ([]string, error) { references := page.CSS if len(references) == 0 { references = []string{"default", "page"} @@ -578,9 +578,9 @@ func appendNonEmpty(values []string, patterns []string) []string { return values } -func cssSources(app manifest.Manifest) []gowdk.CSSSource { - sources := make([]gowdk.CSSSource, 0, len(app.Pages)+len(app.Components)) - for _, page := range app.Pages { +func cssSources(ir gwdkir.Program) []gowdk.CSSSource { + sources := make([]gowdk.CSSSource, 0, len(ir.Pages)+len(ir.Components)) + for _, page := range ir.Pages { sources = append(sources, gowdk.CSSSource{ Path: page.Source, Kind: "page", @@ -588,7 +588,7 @@ func cssSources(app manifest.Manifest) []gowdk.CSSSource { CSSClasses: cssClassesFromViewBody(page.Blocks.ViewBody), }) } - for _, component := range app.Components { + for _, component := range ir.Components { sources = append(sources, gowdk.CSSSource{ Path: component.Source, Kind: "component", diff --git a/internal/buildgen/css_test.go b/internal/buildgen/css_test.go index 287dd39..ece2548 100644 --- a/internal/buildgen/css_test.go +++ b/internal/buildgen/css_test.go @@ -467,7 +467,7 @@ func TestBuildEmitsScopedComponentCSSWithManifestAndCacheHeaders(t *testing.T) { } hashKey := cssscope.HashKey("component", component.Package, component.Name, component.Source, "./hero.css") scopeID := cssscope.ScopeID(hashKey) - logicalPath := componentCSSLogicalPath(component, scopeID) + logicalPath := componentCSSLogicalPath(irComponent(component), scopeID) artifact := cssArtifactByLogicalPath(t, result.CSSArtifacts, logicalPath) emittedRel := filepath.ToSlash(mustRelativePath(t, outputDir, artifact.Path)) if !strings.Contains(emittedRel, "/"+scopeID+".") || !strings.HasSuffix(emittedRel, ".css") { @@ -656,7 +656,7 @@ func TestBuildEmitsScopedComponentStyleBlock(t *testing.T) { } hashKey := cssscope.HashKey("component", component.Package, component.Name, component.Source, inlineStyleAssetPath) scopeID := cssscope.ScopeID(hashKey) - artifact := cssArtifactByLogicalPath(t, result.CSSArtifacts, componentCSSLogicalPath(component, scopeID)) + artifact := cssArtifactByLogicalPath(t, result.CSSArtifacts, componentCSSLogicalPath(irComponent(component), scopeID)) html := readFile(t, filepath.Join(outputDir, "index.html")) emittedRel := filepath.ToSlash(mustRelativePath(t, outputDir, artifact.Path)) if !strings.Contains(html, ``) { diff --git a/internal/buildgen/data.go b/internal/buildgen/data.go index b7376cb..0f5f650 100644 --- a/internal/buildgen/data.go +++ b/internal/buildgen/data.go @@ -3,7 +3,7 @@ package buildgen import ( "fmt" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" ) func parsePathDeclarations(body string) ([]map[string]string, error) { @@ -14,7 +14,7 @@ func parsePathParams(source string) (map[string]string, error) { return parseLiteralStringMap(source, "path param") } -func parseBuildData(body string, routeParams map[string]string, imports []manifest.Import, scripts []manifest.GoBlock, source string) (map[string]string, error) { +func parseBuildData(body string, routeParams map[string]string, imports []gwdkir.Import, scripts []gwdkir.GoBlock, source string) (map[string]string, error) { lines := significantBuildLines(body) if len(lines) == 1 { call, ok, err := parseBuildDataCallLine(lines[0]) diff --git a/internal/buildgen/gotypes_convert.go b/internal/buildgen/gotypes_convert.go new file mode 100644 index 0000000..9ce9c3f --- /dev/null +++ b/internal/buildgen/gotypes_convert.go @@ -0,0 +1,71 @@ +package buildgen + +import ( + "github.com/cssbruno/gowdk/internal/gwdkir" + "github.com/cssbruno/gowdk/internal/manifest" +) + +// The gotypes package still consumes manifest model types. buildgen now works +// against the compiler IR (gwdkir), so these helpers translate the few IR types +// gotypes needs (Import, GoRef/GoTypeRef, StateContract) into their manifest +// equivalents. The conversions are field-for-field; gwdkir and manifest share +// the same leaf types via internal/source. + +func manifestImports(imports []gwdkir.Import) []manifest.Import { + if imports == nil { + return nil + } + out := make([]manifest.Import, len(imports)) + for i, item := range imports { + out[i] = manifest.Import{ + Alias: item.Alias, + Path: item.Path, + Span: item.Span, + } + } + return out +} + +func manifestGoTypeRef(ref gwdkir.GoRef) manifest.GoTypeRef { + return manifest.GoTypeRef{ + Alias: ref.Alias, + Name: ref.Name, + Span: ref.Span, + } +} + +func manifestGoFuncRef(ref gwdkir.GoRef) manifest.GoFuncRef { + return manifest.GoFuncRef{ + Alias: ref.Alias, + Name: ref.Name, + Span: ref.Span, + } +} + +func manifestStateContract(state gwdkir.StateContract) manifest.StateContract { + return manifest.StateContract{ + Type: manifestGoTypeRef(state.Type), + Init: manifestGoFuncRef(state.Init), + Span: state.Span, + } +} + +// manifestBackendBinding converts an IR page load binding into the +// manifest.BackendBinding shape that SSRArtifact.LoadBinding exposes to appgen. +// Only the fields appgen reads (Status, ImportPath, PackageName, FunctionName, +// Signature) plus the binding-local metadata are populated; the kind/page/route +// fields of manifest.BackendBinding describe a binding site that is not part of +// a page load binding and are left empty (matching ManifestFromIR). +func manifestBackendBinding(binding gwdkir.Binding) manifest.BackendBinding { + return manifest.BackendBinding{ + ImportPath: binding.ImportPath, + PackageName: binding.PackageName, + FunctionName: binding.FunctionName, + Signature: binding.Signature, + InputType: binding.InputType, + InputPointer: binding.InputPointer, + InputFields: binding.InputFields, + Status: binding.Status, + Message: binding.Message, + } +} diff --git a/internal/buildgen/ir_test.go b/internal/buildgen/ir_test.go index 6312e35..d47387c 100644 --- a/internal/buildgen/ir_test.go +++ b/internal/buildgen/ir_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/cssbruno/gowdk" + "github.com/cssbruno/gowdk/internal/compiler" "github.com/cssbruno/gowdk/internal/gwdkanalysis" "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" @@ -105,7 +106,7 @@ func TestBuildMemoryFromIRCollectsArtifacts(t *testing.T) { } func TestBuildModelFromIRPreservesFragmentEndpoints(t *testing.T) { - app := buildModelFromIR(gwdkir.Program{ + app := compiler.ManifestFromIR(gwdkir.Program{ Version: gwdkir.Version, Pages: []gwdkir.Page{{ ID: "patients", diff --git a/internal/buildgen/island_source_map.go b/internal/buildgen/island_source_map.go index 563106f..023b75d 100644 --- a/internal/buildgen/island_source_map.go +++ b/internal/buildgen/island_source_map.go @@ -5,7 +5,8 @@ import ( "path" "strings" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" + "github.com/cssbruno/gowdk/internal/source" ) type jsSourceMap struct { @@ -17,7 +18,7 @@ type jsSourceMap struct { Mappings string `json:"mappings"` } -func islandJSSourceMap(component manifest.Component, generatedSource string) []byte { +func islandJSSourceMap(component gwdkir.Component, generatedSource string) []byte { source := component.Source if source == "" { source = "components/" + component.Name + ".cmp.gwdk" @@ -43,7 +44,7 @@ type sourceMapAnchor struct { sourceColumn int } -func sourceMapMappings(component manifest.Component, generatedSource string) string { +func sourceMapMappings(component gwdkir.Component, generatedSource string) string { anchors := sourceMapAnchors(component, generatedSource) if len(anchors) == 0 { return "" @@ -94,7 +95,7 @@ func sourceMapMappings(component manifest.Component, generatedSource string) str return strings.Join(mappings, ";") } -func sourceMapAnchors(component manifest.Component, generatedSource string) []sourceMapAnchor { +func sourceMapAnchors(component gwdkir.Component, generatedSource string) []sourceMapAnchor { name := componentAssetName(component.Name) componentSpan := firstSourceSpan(component.Span, component.Blocks.Spans.Client, component.Blocks.Spans.View) clientSpan := firstSourceSpan(component.Blocks.Spans.Client, componentSpan) @@ -136,7 +137,7 @@ func sourceMapLineBelongsToView(line string) bool { strings.Contains(line, "function render(root, state, helpers, bindings)") } -func appendSourceMapAnchor(anchors []sourceMapAnchor, generatedLine int, span manifest.SourceSpan) []sourceMapAnchor { +func appendSourceMapAnchor(anchors []sourceMapAnchor, generatedLine int, span source.SourceSpan) []sourceMapAnchor { if span.Start.Line <= 0 || span.Start.Column <= 0 { return anchors } @@ -147,13 +148,13 @@ func appendSourceMapAnchor(anchors []sourceMapAnchor, generatedLine int, span ma }) } -func firstSourceSpan(spans ...manifest.SourceSpan) manifest.SourceSpan { +func firstSourceSpan(spans ...source.SourceSpan) source.SourceSpan { for _, span := range spans { if span.Start.Line > 0 && span.Start.Column > 0 { return span } } - return manifest.SourceSpan{} + return source.SourceSpan{} } const sourceMapBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" @@ -178,7 +179,7 @@ func sourceMapVLQ(value int) string { return string(out) } -func componentSourceMapContent(component manifest.Component) string { +func componentSourceMapContent(component gwdkir.Component) string { if component.Blocks.ClientBody == "" { return "view {\n" + component.Blocks.ViewBody + "\n}\n" } diff --git a/internal/buildgen/islands_test.go b/internal/buildgen/islands_test.go index f217307..49f126d 100644 --- a/internal/buildgen/islands_test.go +++ b/internal/buildgen/islands_test.go @@ -358,7 +358,7 @@ func TestIslandJSSourceMapMappingsUseComponentSpans(t *testing.T) { var sourceMap struct { Mappings string `json:"mappings"` } - if err := json.Unmarshal(islandJSSourceMap(component, source), &sourceMap); err != nil { + if err := json.Unmarshal(islandJSSourceMap(irComponent(component), source), &sourceMap); err != nil { t.Fatal(err) } if sourceMap.Mappings == "" { diff --git a/internal/buildgen/render.go b/internal/buildgen/render.go index 1953501..03ec23d 100644 --- a/internal/buildgen/render.go +++ b/internal/buildgen/render.go @@ -6,6 +6,7 @@ import ( "github.com/cssbruno/gowdk" "github.com/cssbruno/gowdk/internal/gotypes" + "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" "github.com/cssbruno/gowdk/internal/view" gowhtml "github.com/cssbruno/gowdk/runtime/html" @@ -18,7 +19,7 @@ const ( renderModeRequestTime renderModePolicy = "request-time" ) -func renderPage(config gowdk.Config, page manifest.Page, components map[string]view.Component, layouts map[string]manifest.Layout, stylesheets []gowdk.Stylesheet, data map[string]string, policy renderModePolicy) (string, error) { +func renderPage(config gowdk.Config, page gwdkir.Page, components map[string]view.Component, layouts map[string]gwdkir.Layout, stylesheets []gowdk.Stylesheet, data map[string]string, policy renderModePolicy) (string, error) { mode := page.RenderMode(config.Render.DefaultMode()) if policy == renderModeSPA && mode != gowdk.SPA && mode != gowdk.Action { return "", fmt.Errorf("%s: SPA build cannot emit request-time %s pages yet", page.ID, mode) @@ -55,7 +56,7 @@ func renderPage(config gowdk.Config, page manifest.Page, components map[string]v return document(config, page, body, stylesheets, storeSeeds, pageScripts(config, page, viewSource, pageComponents, policy)), nil } -func composePageViewSource(page manifest.Page, layouts map[string]manifest.Layout) (string, error) { +func composePageViewSource(page gwdkir.Page, layouts map[string]gwdkir.Layout) (string, error) { source := page.Blocks.ViewBody if len(layouts) == 0 { return source, nil @@ -75,7 +76,7 @@ func composePageViewSource(page manifest.Page, layouts map[string]manifest.Layou return source, nil } -func resolvePageLayout(page manifest.Page, layouts map[string]manifest.Layout, layoutRef string) (manifest.Layout, bool) { +func resolvePageLayout(page gwdkir.Page, layouts map[string]gwdkir.Layout, layoutRef string) (gwdkir.Layout, bool) { if alias, layoutID, ok := strings.Cut(layoutRef, "."); ok { for _, use := range page.Uses { if use.Alias == alias { @@ -83,7 +84,7 @@ func resolvePageLayout(page manifest.Page, layouts map[string]manifest.Layout, l return layout, exists } } - return manifest.Layout{}, false + return gwdkir.Layout{}, false } if page.Package != "" { if layout, ok := layouts[layoutRegistryKey(page.Package, layoutRef)]; ok { @@ -94,7 +95,7 @@ func resolvePageLayout(page manifest.Page, layouts map[string]manifest.Layout, l return layout, ok } -func composeLayoutSource(layout manifest.Layout, child string) (string, error) { +func composeLayoutSource(layout gwdkir.Layout, child string) (string, error) { matches := layoutSlotIndexes(layout.Blocks.ViewBody) if len(matches) != 1 { return "", fmt.Errorf("layout %s must contain exactly one placeholder", layout.ID) @@ -103,7 +104,7 @@ func composeLayoutSource(layout manifest.Layout, child string) (string, error) { return layout.Blocks.ViewBody[:match[0]] + child + layout.Blocks.ViewBody[match[1]:], nil } -func validateViewParamReferences(page manifest.Page, source string) error { +func validateViewParamReferences(page gwdkir.Page, source string) error { refs, err := view.ParamReferences(source) if err != nil { return err @@ -123,7 +124,7 @@ func validateViewParamReferences(page manifest.Page, source string) error { return nil } -func actionRoutes(page manifest.Page, data map[string]string) map[string]string { +func actionRoutes(page gwdkir.Page, data map[string]string) map[string]string { routes := map[string]string{} for _, action := range page.Blocks.Actions { route := action.Route @@ -138,7 +139,7 @@ func actionRoutes(page manifest.Page, data map[string]string) map[string]string return routes } -func pageScripts(config gowdk.Config, page manifest.Page, viewSource string, components map[string]view.Component, policy renderModePolicy) []gowdk.Script { +func pageScripts(config gowdk.Config, page gwdkir.Page, viewSource string, components map[string]view.Component, policy renderModePolicy) []gowdk.Script { scripts := append([]gowdk.Script{}, nonEmptyScripts(config.Build.Scripts)...) for _, href := range scopedScriptHrefs(page, viewSource, components) { scripts = append(scripts, gowdk.Script{Src: href, Type: "module"}) @@ -161,14 +162,14 @@ func pageScripts(config gowdk.Config, page manifest.Page, viewSource string, com return scripts } -func pageUsesPartialRuntime(page manifest.Page, viewSource string) bool { +func pageUsesPartialRuntime(page gwdkir.Page, viewSource string) bool { if !strings.Contains(viewSource, "g:target") { return false } return len(page.Blocks.Actions) > 0 } -func pageUsesSPANavigationRuntime(config gowdk.Config, page manifest.Page, viewSource string, components map[string]view.Component) bool { +func pageUsesSPANavigationRuntime(config gowdk.Config, page gwdkir.Page, viewSource string, components map[string]view.Component) bool { mode := page.RenderMode(config.Render.DefaultMode()) if mode != gowdk.SPA && mode != gowdk.Action { return false @@ -232,15 +233,15 @@ type pageStoreSeed struct { JSON string } -func pageStoreSeeds(page manifest.Page) ([]pageStoreSeed, error) { +func pageStoreSeeds(page gwdkir.Page) ([]pageStoreSeed, error) { if len(page.Stores) == 0 { return nil, nil } seeds := make([]pageStoreSeed, 0, len(page.Stores)) for _, store := range page.Stores { - payload, err := gotypes.RunStateInitJSON(page.Imports, manifest.StateContract{ - Type: store.Type, - Init: store.Init, + payload, err := gotypes.RunStateInitJSON(manifestImports(page.Imports), manifest.StateContract{ + Type: manifestGoTypeRef(store.Type), + Init: manifestGoFuncRef(store.Init), Span: store.Span, }) if err != nil { @@ -251,7 +252,7 @@ func pageStoreSeeds(page manifest.Page) ([]pageStoreSeed, error) { return seeds, nil } -func document(config gowdk.Config, page manifest.Page, body string, stylesheets []gowdk.Stylesheet, storeSeeds []pageStoreSeed, scripts []gowdk.Script) string { +func document(config gowdk.Config, page gwdkir.Page, body string, stylesheets []gowdk.Stylesheet, storeSeeds []pageStoreSeed, scripts []gowdk.Script) string { title := page.ID if page.Metadata.Title != "" { title = page.Metadata.Title @@ -346,7 +347,7 @@ func nonEmptyScripts(scripts []gowdk.Script) []gowdk.Script { return out } -func socialHeadEnabled(head gowdk.HeadConfig, metadata manifest.PageMetadata) bool { +func socialHeadEnabled(head gowdk.HeadConfig, metadata gwdkir.PageMetadata) bool { return head.SiteName != "" || head.Image != "" || head.TwitterCard != "" || metadata.Image != "" } diff --git a/internal/buildgen/report.go b/internal/buildgen/report.go index 21725f6..1171b13 100644 --- a/internal/buildgen/report.go +++ b/internal/buildgen/report.go @@ -5,7 +5,7 @@ import ( "path/filepath" "strings" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) type BuildEventLevel string @@ -37,11 +37,11 @@ type BuildReport struct { // BuildDiagnostic is a structured diagnostic produced during SPA planning // or output generation after parser/compiler validation has already completed. type BuildDiagnostic struct { - Code string `json:"code"` - ComponentName string `json:"componentName,omitempty"` - Source string `json:"source,omitempty"` - Span manifest.SourceSpan `json:"span,omitempty"` - Message string `json:"message"` + Code string `json:"code"` + ComponentName string `json:"componentName,omitempty"` + Source string `json:"source,omitempty"` + Span source.SourceSpan `json:"span,omitempty"` + Message string `json:"message"` } type BuildError struct { diff --git a/internal/buildgen/routes.go b/internal/buildgen/routes.go index f755a8b..c74bf82 100644 --- a/internal/buildgen/routes.go +++ b/internal/buildgen/routes.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/cssbruno/gowdk" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/view" ) @@ -16,7 +16,7 @@ type pageOutput struct { data map[string]string } -func pageRouteArtifacts(outputDir string, page manifest.Page) ([]Artifact, error) { +func pageRouteArtifacts(outputDir string, page gwdkir.Page) ([]Artifact, error) { outputs, err := pageOutputs(page) if err != nil { return nil, fmt.Errorf("%s: %w", page.ID, err) @@ -32,7 +32,7 @@ func pageRouteArtifacts(outputDir string, page manifest.Page) ([]Artifact, error return artifacts, nil } -func pageOutputArtifacts(config gowdk.Config, outputDir string, page manifest.Page, components map[string]view.Component, layouts map[string]manifest.Layout, stylesheets []gowdk.Stylesheet) ([]plannedArtifact, error) { +func pageOutputArtifacts(config gowdk.Config, outputDir string, page gwdkir.Page, components map[string]view.Component, layouts map[string]gwdkir.Layout, stylesheets []gowdk.Stylesheet) ([]plannedArtifact, error) { outputs, err := pageOutputs(page) if err != nil { return nil, fmt.Errorf("%s: %w", page.ID, err) @@ -63,7 +63,7 @@ func pageOutputArtifacts(config gowdk.Config, outputDir string, page manifest.Pa return artifacts, nil } -func pageOutputs(page manifest.Page) ([]pageOutput, error) { +func pageOutputs(page gwdkir.Page) ([]pageOutput, error) { params := page.DynamicParams() if len(params) == 0 { return []pageOutput{{route: page.Route}}, nil diff --git a/internal/buildgen/runtime_asset_paths.go b/internal/buildgen/runtime_asset_paths.go index fb9f3d9..2023e4d 100644 --- a/internal/buildgen/runtime_asset_paths.go +++ b/internal/buildgen/runtime_asset_paths.go @@ -7,7 +7,7 @@ import ( "path/filepath" "runtime" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" ) func islandWASMLoaderArtifact(outputDir, componentName string) plannedAssetArtifact { @@ -18,7 +18,7 @@ func islandWASMLoaderArtifact(outputDir, componentName string) plannedAssetArtif } } -func clientGoBlockWASMLoaderArtifact(outputDir string, page manifest.Page) plannedAssetArtifact { +func clientGoBlockWASMLoaderArtifact(outputDir string, page gwdkir.Page) plannedAssetArtifact { assetPath := clientGoBlockWASMLoaderAssetPath(page) return plannedAssetArtifact{ AssetArtifact: AssetArtifact{Path: filepath.Join(outputDir, filepath.FromSlash(assetPath))}, @@ -50,11 +50,11 @@ func islandWASMLoaderAssetPath(componentName string) string { return path.Join(islandRuntimeDir, componentAssetName(componentName)+".wasm.js") } -func clientGoBlockWASMAssetPath(page manifest.Page) string { +func clientGoBlockWASMAssetPath(page gwdkir.Page) string { return path.Join(islandRuntimeDir, "pages", clientGoBlockAssetName(page)+".wasm") } -func clientGoBlockWASMLoaderAssetPath(page manifest.Page) string { +func clientGoBlockWASMLoaderAssetPath(page gwdkir.Page) string { return path.Join(islandRuntimeDir, "pages", clientGoBlockAssetName(page)+".wasm.js") } @@ -74,7 +74,7 @@ func componentAssetName(componentName string) string { return name } -func clientGoBlockAssetName(page manifest.Page) string { +func clientGoBlockAssetName(page gwdkir.Page) string { name := exportedPascalSafe(page.ID) if name == "" { return "Page" @@ -82,7 +82,7 @@ func clientGoBlockAssetName(page manifest.Page) string { return name } -func clientGoBlockMountExportName(page manifest.Page) string { +func clientGoBlockMountExportName(page gwdkir.Page) string { return "GOWDKMount" + clientGoBlockAssetName(page) } diff --git a/internal/buildgen/runtime_assets.go b/internal/buildgen/runtime_assets.go index b2c909c..d4c6f39 100644 --- a/internal/buildgen/runtime_assets.go +++ b/internal/buildgen/runtime_assets.go @@ -5,11 +5,11 @@ import ( "github.com/cssbruno/gowdk" "github.com/cssbruno/gowdk/internal/clientrt" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/view" ) -func clientRuntimeArtifacts(config gowdk.Config, pages []manifest.Page, outputDir string, layouts map[string]manifest.Layout, components map[string]view.Component) []plannedAssetArtifact { +func clientRuntimeArtifacts(config gowdk.Config, pages []gwdkir.Page, outputDir string, layouts map[string]gwdkir.Layout, components map[string]view.Component) []plannedAssetArtifact { for _, page := range pages { viewSource := page.Blocks.ViewBody if source, err := composePageViewSource(page, layouts); err == nil { @@ -24,16 +24,16 @@ func clientRuntimeArtifacts(config gowdk.Config, pages []manifest.Page, outputDi } return nil } -func runtimeArtifacts(config gowdk.Config, app manifest.Manifest, outputDir string, layouts map[string]manifest.Layout, components map[string]view.Component) ([]plannedAssetArtifact, error) { +func runtimeArtifacts(config gowdk.Config, ir gwdkir.Program, outputDir string, layouts map[string]gwdkir.Layout, components map[string]view.Component) ([]plannedAssetArtifact, error) { var artifacts []plannedAssetArtifact - artifacts = append(artifacts, clientRuntimeArtifacts(config, app.Pages, outputDir, layouts, components)...) - artifacts = append(artifacts, storeRuntimeArtifacts(app.Pages, outputDir)...) - islands, err := islandRuntimeArtifacts(config, app, outputDir, layouts) + artifacts = append(artifacts, clientRuntimeArtifacts(config, ir.Pages, outputDir, layouts, components)...) + artifacts = append(artifacts, storeRuntimeArtifacts(ir.Pages, outputDir)...) + islands, err := islandRuntimeArtifacts(config, ir.Pages, ir.Components, outputDir, layouts) if err != nil { return nil, err } artifacts = append(artifacts, islands...) - clientGoBlocks, err := clientGoBlockRuntimeArtifacts(app.Pages, outputDir) + clientGoBlocks, err := clientGoBlockRuntimeArtifacts(ir.Pages, outputDir) if err != nil { return nil, err } @@ -41,7 +41,7 @@ func runtimeArtifacts(config gowdk.Config, app manifest.Manifest, outputDir stri return dedupeAssetArtifacts(artifacts), nil } -func storeRuntimeArtifacts(pages []manifest.Page, outputDir string) []plannedAssetArtifact { +func storeRuntimeArtifacts(pages []gwdkir.Page, outputDir string) []plannedAssetArtifact { for _, page := range pages { if len(page.Stores) > 0 { return []plannedAssetArtifact{{ diff --git a/internal/buildgen/runtime_assets_test.go b/internal/buildgen/runtime_assets_test.go index c4ee519..bfe9667 100644 --- a/internal/buildgen/runtime_assets_test.go +++ b/internal/buildgen/runtime_assets_test.go @@ -6,19 +6,19 @@ import ( "strings" "testing" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" ) func TestClientGoBlockWASMSourceMergesImportsWithAST(t *testing.T) { - page := manifest.Page{ + page := gwdkir.Page{ ID: "counter", - Imports: []manifest.Import{ + Imports: []gwdkir.Import{ {Alias: "dom", Path: "syscall/js"}, {Path: "fmt"}, {Path: "example.com/unused"}, }, } - block := manifest.GoBlock{Body: `import "strings" + block := gwdkir.GoBlock{Body: `import "strings" func GOWDKMountCounter(value string) { _ = dom.Global() diff --git a/internal/buildgen/runtime_islands.go b/internal/buildgen/runtime_islands.go index 5bb18f6..9403af3 100644 --- a/internal/buildgen/runtime_islands.go +++ b/internal/buildgen/runtime_islands.go @@ -6,15 +6,15 @@ import ( "strings" "github.com/cssbruno/gowdk" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/view" ) -func islandRuntimeArtifacts(config gowdk.Config, app manifest.Manifest, outputDir string, layouts map[string]manifest.Layout) ([]plannedAssetArtifact, error) { - components := componentsByName(app.Components) +func islandRuntimeArtifacts(config gowdk.Config, pages []gwdkir.Page, allComponents []gwdkir.Component, outputDir string, layouts map[string]gwdkir.Layout) ([]plannedAssetArtifact, error) { + components := componentsByName(allComponents) includeSourceMaps := config.Build.DebugAssets() planned := map[string]plannedAssetArtifact{} - for _, page := range app.Pages { + for _, page := range pages { source, err := composePageViewSource(page, layouts) if err != nil { source = page.Blocks.ViewBody @@ -95,7 +95,7 @@ func islandScriptHrefs(source string, components map[string]view.Component, owne return scripts } -func manifestComponentRuntimeMode(explicit string, component manifest.Component) string { +func manifestComponentRuntimeMode(explicit string, component gwdkir.Component) string { if explicit != "" { return explicit } @@ -173,10 +173,10 @@ func lookupViewComponent(components map[string]view.Component, name string, owne type resolvedManifestComponentCallUsage struct { call view.ComponentCallUsage - component manifest.Component + component gwdkir.Component } -func recursiveManifestComponentCallUsages(source string, components map[string]manifest.Component, ownerPackage string, uses map[string]string) ([]resolvedManifestComponentCallUsage, error) { +func recursiveManifestComponentCallUsages(source string, components map[string]gwdkir.Component, ownerPackage string, uses map[string]string) ([]resolvedManifestComponentCallUsage, error) { var usages []resolvedManifestComponentCallUsage visiting := map[string]bool{} var walk func(string, string, map[string]string) error @@ -209,7 +209,7 @@ func recursiveManifestComponentCallUsages(source string, components map[string]m return usages, nil } -func lookupManifestComponent(components map[string]manifest.Component, name string, ownerPackage string, uses map[string]string) (manifest.Component, bool) { +func lookupManifestComponent(components map[string]gwdkir.Component, name string, ownerPackage string, uses map[string]string) (gwdkir.Component, bool) { if strings.Contains(name, ".") { if component, ok := components[name]; ok { return component, true @@ -217,7 +217,7 @@ func lookupManifestComponent(components map[string]manifest.Component, name stri alias, componentName, _ := strings.Cut(name, ".") packageName := uses[alias] if packageName == "" { - return manifest.Component{}, false + return gwdkir.Component{}, false } component, ok := components[componentRegistryKey(packageName, componentName)] return component, ok @@ -230,7 +230,7 @@ func lookupManifestComponent(components map[string]manifest.Component, name stri return component, ok } -func statefulComponentNames(components []manifest.Component) map[string]bool { +func statefulComponentNames(components []gwdkir.Component) map[string]bool { out := map[string]bool{} for _, component := range components { if componentNeedsJSIsland(component) { @@ -243,12 +243,12 @@ func statefulComponentNames(components []manifest.Component) map[string]bool { return out } -func componentNeedsJSIsland(component manifest.Component) bool { +func componentNeedsJSIsland(component gwdkir.Component) bool { return component.State.Type.Name != "" || component.Blocks.Client || len(component.Emits) > 0 } -func componentsByName(components []manifest.Component) map[string]manifest.Component { - out := map[string]manifest.Component{} +func componentsByName(components []gwdkir.Component) map[string]gwdkir.Component { + out := map[string]gwdkir.Component{} for _, component := range components { key := componentRegistryKey(component.Package, component.Name) out[key] = component @@ -307,7 +307,7 @@ func compactGeneratedJSSource(source string) string { return strings.Join(lines, "\n") + "\n" } -func islandJSSourceMapArtifact(outputDir string, component manifest.Component) plannedAssetArtifact { +func islandJSSourceMapArtifact(outputDir string, component gwdkir.Component) plannedAssetArtifact { assetPath := islandJSSourceMapAssetPath(component.Name) source := islandJSSource(component.Name, true) return plannedAssetArtifact{ diff --git a/internal/buildgen/runtime_wasm_assets.go b/internal/buildgen/runtime_wasm_assets.go index 88cac3c..2c65a55 100644 --- a/internal/buildgen/runtime_wasm_assets.go +++ b/internal/buildgen/runtime_wasm_assets.go @@ -15,12 +15,13 @@ import ( "strconv" "strings" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" + "github.com/cssbruno/gowdk/internal/source" ) var wasmMagic = []byte{0x00, 0x61, 0x73, 0x6d} -func clientGoBlockRuntimeArtifacts(pages []manifest.Page, outputDir string) ([]plannedAssetArtifact, error) { +func clientGoBlockRuntimeArtifacts(pages []gwdkir.Page, outputDir string) ([]plannedAssetArtifact, error) { planned := map[string]plannedAssetArtifact{} for _, page := range pages { script, ok := clientGoBlock(page) @@ -54,14 +55,14 @@ func clientGoBlockRuntimeArtifacts(pages []manifest.Page, outputDir string) ([]p return artifacts, nil } -func clientGoBlockHrefs(page manifest.Page) []string { +func clientGoBlockHrefs(page gwdkir.Page) []string { if _, ok := clientGoBlock(page); !ok { return nil } return []string{"/" + clientGoBlockWASMLoaderAssetPath(page)} } -func clientGoBlock(page manifest.Page) (manifest.GoBlock, bool) { +func clientGoBlock(page gwdkir.Page) (gwdkir.GoBlock, bool) { required := clientGoBlockMountExportName(page) for _, script := range page.Blocks.GoBlocks { if script.Target != "client" { @@ -71,10 +72,10 @@ func clientGoBlock(page manifest.Page) (manifest.GoBlock, bool) { return script, true } } - return manifest.GoBlock{}, false + return gwdkir.GoBlock{}, false } -func islandWASMArtifact(outputDir string, component manifest.Component) (plannedAssetArtifact, error) { +func islandWASMArtifact(outputDir string, component gwdkir.Component) (plannedAssetArtifact, error) { assetPath := islandWASMAssetPath(component.Name) contents := []byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00} if strings.TrimSpace(component.WASM.Package) != "" { @@ -90,7 +91,7 @@ func islandWASMArtifact(outputDir string, component manifest.Component) (planned }, nil } -func buildWASMIslandPackage(component manifest.Component) ([]byte, error) { +func buildWASMIslandPackage(component gwdkir.Component) ([]byte, error) { packagePath := strings.TrimSpace(component.WASM.Package) temp, err := os.CreateTemp("", "gowdk-"+componentAssetName(component.Name)+"-*.wasm") if err != nil { @@ -132,7 +133,7 @@ func buildWASMIslandPackage(component manifest.Component) ([]byte, error) { return contents, nil } -func clientGoBlockWASMArtifact(outputDir string, page manifest.Page, script manifest.GoBlock) (plannedAssetArtifact, error) { +func clientGoBlockWASMArtifact(outputDir string, page gwdkir.Page, script gwdkir.GoBlock) (plannedAssetArtifact, error) { assetPath := clientGoBlockWASMAssetPath(page) contents, err := buildClientGoBlockWASM(page, script) if err != nil { @@ -144,7 +145,7 @@ func clientGoBlockWASMArtifact(outputDir string, page manifest.Page, script mani }, nil } -func buildClientGoBlockWASM(page manifest.Page, script manifest.GoBlock) ([]byte, error) { +func buildClientGoBlockWASM(page gwdkir.Page, script gwdkir.GoBlock) ([]byte, error) { temp, err := os.CreateTemp(sourceDir(page.Source), ".gowdk-"+clientGoBlockAssetName(page)+"-*.wasm") if err != nil { return nil, clientGoBlockDiagnosticError(page, "client_go_block_wasm_build_error", fmt.Errorf("create temp output: %w", err)) @@ -198,7 +199,7 @@ func buildClientGoBlockWASM(page manifest.Page, script manifest.GoBlock) ([]byte return contents, nil } -func clientGoBlockWASMSource(page manifest.Page, script manifest.GoBlock) (string, error) { +func clientGoBlockWASMSource(page gwdkir.Page, script gwdkir.GoBlock) (string, error) { body := strings.TrimSpace(script.Body) sourceWithoutImports := "package main\n" + body + "\n" fileSet := token.NewFileSet() @@ -232,7 +233,7 @@ func clientGoBlockWASMSource(page manifest.Page, script manifest.GoBlock) (strin return buffer.String(), nil } -func clientGoBlockGOWDKImportDecl(imports []manifest.Import, file *ast.File) ast.Decl { +func clientGoBlockGOWDKImportDecl(imports []gwdkir.Import, file *ast.File) ast.Decl { used := usedIdentifiers(file) localImports := map[string]bool{} for _, spec := range importSpecs(file) { @@ -280,7 +281,7 @@ func importSpec(alias string, importPath string) ast.Spec { return spec } -func validateClientGoBlockWASMImports(page manifest.Page, sourcePath string) error { +func validateClientGoBlockWASMImports(page gwdkir.Page, sourcePath string) error { file, err := parser.ParseFile(token.NewFileSet(), sourcePath, nil, parser.ImportsOnly) if err != nil { return clientGoBlockDiagnosticError(page, "client_go_block_wasm_import_error", fmt.Errorf("parse imports: %w", err)) @@ -299,7 +300,7 @@ func validateClientGoBlockWASMImports(page manifest.Page, sourcePath string) err return nil } -func validateClientGoBlockWASMExports(page manifest.Page, contents []byte) error { +func validateClientGoBlockWASMExports(page gwdkir.Page, contents []byte) error { exports, err := wasmExportNames(contents) if err != nil { return clientGoBlockDiagnosticError(page, "client_go_block_wasm_export_error", err) @@ -311,7 +312,7 @@ func validateClientGoBlockWASMExports(page manifest.Page, contents []byte) error return nil } -func validateWASMIslandExports(component manifest.Component, packagePath string, contents []byte) error { +func validateWASMIslandExports(component gwdkir.Component, packagePath string, contents []byte) error { exports, err := wasmExportNames(contents) if err != nil { return wasmIslandDiagnosticError(component, "wasm_package_export_error", packagePath, err) @@ -428,7 +429,7 @@ func (err *wasmIslandBuildDiagnosticError) BuildDiagnostics() []BuildDiagnostic return []BuildDiagnostic{err.diagnostic} } -func wasmIslandDiagnosticError(component manifest.Component, code, packagePath string, cause error) error { +func wasmIslandDiagnosticError(component gwdkir.Component, code, packagePath string, cause error) error { message := fmt.Sprintf("component %s wasm package %q %v", component.Name, packagePath, cause) return &wasmIslandBuildDiagnosticError{ err: fmt.Errorf("%s", message), @@ -468,7 +469,7 @@ func (err *clientGoBlockBuildDiagnosticError) BuildDiagnostics() []BuildDiagnost return []BuildDiagnostic{err.diagnostic} } -func clientGoBlockDiagnosticError(page manifest.Page, code string, cause error) error { +func clientGoBlockDiagnosticError(page gwdkir.Page, code string, cause error) error { message := fmt.Sprintf("page %s go client WASM: %v", page.ID, cause) return &clientGoBlockBuildDiagnosticError{ err: fmt.Errorf("%s", message), @@ -481,13 +482,13 @@ func clientGoBlockDiagnosticError(page manifest.Page, code string, cause error) } } -func goBlockTargetSpan(spans []manifest.NamedSpan, target string) manifest.SourceSpan { +func goBlockTargetSpan(spans []source.NamedSpan, target string) source.SourceSpan { for _, span := range spans { if span.Name == target { return span.Span } } - return manifest.SourceSpan{} + return source.SourceSpan{} } var forbiddenWASMIslandImports = map[string]string{ @@ -558,7 +559,7 @@ func fileDeclaresMain(file *ast.File) bool { return false } -func clientGoBlockImportAlias(item manifest.Import) string { +func clientGoBlockImportAlias(item gwdkir.Import) string { if strings.TrimSpace(item.Alias) != "" { return item.Alias } @@ -569,7 +570,7 @@ func clientGoBlockImportAlias(item manifest.Import) string { return path.Base(importPath) } -func validateWASMIslandPackageImports(component manifest.Component, dir, buildPackage, packagePath string) error { +func validateWASMIslandPackageImports(component gwdkir.Component, dir, buildPackage, packagePath string) error { sourceDir, ok := wasmIslandLocalSourceDir(dir, buildPackage, packagePath) if !ok { return nil diff --git a/internal/buildgen/scoped_scripts.go b/internal/buildgen/scoped_scripts.go index 180c183..43ffb76 100644 --- a/internal/buildgen/scoped_scripts.go +++ b/internal/buildgen/scoped_scripts.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/cssbruno/gowdk/internal/gwdkir" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/internal/view" "github.com/evanw/esbuild/pkg/api" ) @@ -186,7 +186,7 @@ func safeScriptAssetName(scriptPath string) string { return name } -func scopedScriptHrefs(page manifest.Page, viewSource string, components map[string]view.Component) []string { +func scopedScriptHrefs(page gwdkir.Page, viewSource string, components map[string]view.Component) []string { seen := map[string]bool{} var scripts []string add := func(href string) { @@ -202,7 +202,7 @@ func scopedScriptHrefs(page manifest.Page, viewSource string, components map[str for index, script := range page.InlineJS { name := script.Name if name == "" { - name = manifest.InlineScriptName(index) + name = source.InlineScriptName(index) } add("/" + pageScopedJSLogicalPath(page.ID, name)) } @@ -213,7 +213,7 @@ func scopedScriptHrefs(page manifest.Page, viewSource string, components map[str return scripts } -func scopedComponentScriptHrefs(page manifest.Page, viewSource string, components map[string]view.Component) []string { +func scopedComponentScriptHrefs(page gwdkir.Page, viewSource string, components map[string]view.Component) []string { usages, err := recursiveViewComponentCallUsages(viewSource, components, page.Package, componentUses(page.Uses)) if err != nil { return nil @@ -233,7 +233,7 @@ func scopedComponentScriptHrefs(page manifest.Page, viewSource string, component for index, script := range component.InlineJS { name := script.Name if name == "" { - name = manifest.InlineScriptName(index) + name = source.InlineScriptName(index) } href := "/" + componentScopedJSLogicalPath(component.Package, component.Name, name) if seen[href] { diff --git a/internal/buildgen/ssr.go b/internal/buildgen/ssr.go index f68d7d0..68996a3 100644 --- a/internal/buildgen/ssr.go +++ b/internal/buildgen/ssr.go @@ -12,6 +12,7 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkanalysis" "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/internal/view" ) @@ -22,7 +23,7 @@ type SSRArtifact struct { Cache string ErrorPage string DynamicParams []string - RouteParams []manifest.RouteParam + RouteParams []source.RouteParam Guards []string HasLoad bool LoadBinding manifest.BackendBinding @@ -48,14 +49,13 @@ func SSRArtifacts(config gowdk.Config, app manifest.Manifest, outputDir string) // SSRArtifactsFromIR renders request-time page artifacts from normalized // compiler IR. func SSRArtifactsFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string) ([]SSRArtifact, error) { - app := buildModelFromIR(ir) - if err := compiler.ValidateManifest(config, app); err != nil { + if err := compiler.ValidateProgram(config, ir); err != nil { return nil, err } - components, componentFailures := buildComponents(app.Components) - layouts, layoutFailures := buildLayouts(app.Layouts) - css, cssFailures := planCSS(config, app, outputDir) + components, componentFailures := buildComponents(ir.Components) + layouts, layoutFailures := buildLayouts(ir.Layouts) + css, cssFailures := planCSS(config, ir, outputDir) baseStylesheets := append([]gowdk.Stylesheet{}, config.Build.Stylesheets...) baseStylesheets = append(baseStylesheets, css.stylesheets...) @@ -64,7 +64,7 @@ func SSRArtifactsFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string failures = append(failures, componentFailures...) failures = append(failures, layoutFailures...) failures = append(failures, cssFailures...) - for _, page := range app.Pages { + for _, page := range ir.Pages { if !isRequestTimePage(config, page) { continue } @@ -81,7 +81,7 @@ func SSRArtifactsFromIR(config gowdk.Config, ir gwdkir.Program, outputDir string return artifacts, nil } -func ssrArtifact(config gowdk.Config, page manifest.Page, components map[string]view.Component, layouts map[string]manifest.Layout, stylesheets []gowdk.Stylesheet) (SSRArtifact, error) { +func ssrArtifact(config gowdk.Config, page gwdkir.Page, components map[string]view.Component, layouts map[string]gwdkir.Layout, stylesheets []gowdk.Stylesheet) (SSRArtifact, error) { routeData, replacements := ssrRouteData(page) buildData, err := parseBuildData(page.Blocks.BuildBody, routeData, page.Imports, page.Blocks.GoBlocks, page.Source) if err != nil { @@ -109,17 +109,17 @@ func ssrArtifact(config gowdk.Config, page manifest.Page, components map[string] Cache: page.CachePolicy(), ErrorPage: page.ErrorPage, DynamicParams: page.DynamicParams(), - RouteParams: append([]manifest.RouteParam(nil), page.TypedRouteParams()...), - Guards: append([]string(nil), page.Guard...), + RouteParams: append([]source.RouteParam(nil), page.TypedRouteParams()...), + Guards: append([]string(nil), page.Guards...), HasLoad: page.Blocks.Load, - LoadBinding: page.LoadBinding, + LoadBinding: manifestBackendBinding(page.LoadBinding), HTML: html, Replacements: replacements, LoadReplacements: loadReplacements, }, nil } -func ssrRouteData(page manifest.Page) (map[string]string, []SSRReplacement) { +func ssrRouteData(page gwdkir.Page) (map[string]string, []SSRReplacement) { params := page.DynamicParams() if len(params) == 0 { return nil, nil @@ -134,7 +134,7 @@ func ssrRouteData(page manifest.Page) (map[string]string, []SSRReplacement) { return data, replacements } -func ssrLoadData(page manifest.Page, existing map[string]string) (map[string]string, []SSRLoadReplacement, error) { +func ssrLoadData(page gwdkir.Page, existing map[string]string) (map[string]string, []SSRLoadReplacement, error) { if !page.Blocks.Load { return nil, nil, nil } @@ -243,7 +243,7 @@ func exportedSafe(value string) string { return string(out) } -func isRequestTimePage(config gowdk.Config, page manifest.Page) bool { +func isRequestTimePage(config gowdk.Config, page gwdkir.Page) bool { switch page.RenderMode(config.Render.DefaultMode()) { case gowdk.SSR: return true diff --git a/internal/buildgen/test_helpers_test.go b/internal/buildgen/test_helpers_test.go index 9b14a2c..47904b2 100644 --- a/internal/buildgen/test_helpers_test.go +++ b/internal/buildgen/test_helpers_test.go @@ -6,9 +6,23 @@ import ( "path/filepath" "testing" + "github.com/cssbruno/gowdk" + "github.com/cssbruno/gowdk/internal/gwdkanalysis" + "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" ) +// irComponent converts a manifest.Component fixture into the IR component the +// migrated render helpers now consume. It routes through the production +// IR builder so the test exercises the same conversion as the real pipeline. +func irComponent(component manifest.Component) gwdkir.Component { + ir := gwdkanalysis.BuildIR(gowdk.Config{}, manifest.Manifest{Components: []manifest.Component{component}}) + if len(ir.Components) == 0 { + return gwdkir.Component{} + } + return ir.Components[0] +} + type testRouteManifest struct { Version int `json:"version"` Routes []struct { diff --git a/internal/buildgen/wasm_loader_source.go b/internal/buildgen/wasm_loader_source.go index 39cfa99..5d672f5 100644 --- a/internal/buildgen/wasm_loader_source.go +++ b/internal/buildgen/wasm_loader_source.go @@ -4,10 +4,10 @@ import ( "fmt" "strconv" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/gwdkir" ) -func clientGoBlockWASMLoaderSource(page manifest.Page) string { +func clientGoBlockWASMLoaderSource(page gwdkir.Page) string { pageID := strconv.Quote(page.ID) loaderPath := strconv.Quote("/" + clientGoBlockWASMLoaderAssetPath(page)) wasmPath := strconv.Quote("/" + clientGoBlockWASMAssetPath(page)) diff --git a/internal/compiler/backend_binding_policy.go b/internal/compiler/backend_binding_policy.go index 865e6d3..7e05fd0 100644 --- a/internal/compiler/backend_binding_policy.go +++ b/internal/compiler/backend_binding_policy.go @@ -4,9 +4,19 @@ import ( "fmt" "github.com/cssbruno/gowdk" + "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) +// ValidateBackendBindingPolicyIR enforces the same build-mode rules as +// ValidateBackendBindingPolicy against an IR-first build path. IR already +// carries per-endpoint binding metadata, so the reconstructed manifest has its +// BackendBindings populated and the on-disk rebinding branch does not refire. +func ValidateBackendBindingPolicyIR(config gowdk.Config, ir gwdkir.Program) error { + return ValidateBackendBindingPolicy(config, ManifestFromIR(ir)) +} + // ValidateBackendBindingPolicy enforces build-mode rules for declared backend // endpoints after same-package Go handler binding metadata has been produced. func ValidateBackendBindingPolicy(config gowdk.Config, app manifest.Manifest) error { @@ -20,7 +30,7 @@ func ValidateBackendBindingPolicy(config gowdk.Config, app manifest.Manifest) er var diagnostics []ValidationError for _, binding := range app.BackendBindings { switch binding.Status { - case manifest.BackendBindingMissing, manifest.BackendBindingUnsupportedSignature: + case source.BackendBindingMissing, source.BackendBindingUnsupportedSignature: diagnostics = append(diagnostics, backendBindingRequiredDiagnostic(binding)) } } diff --git a/internal/compiler/backend_bindings.go b/internal/compiler/backend_bindings.go index 34d0063..99d9e02 100644 --- a/internal/compiler/backend_bindings.go +++ b/internal/compiler/backend_bindings.go @@ -16,6 +16,7 @@ import ( "github.com/cssbruno/gowdk/internal/goblockgen" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) const ( @@ -62,9 +63,9 @@ func BindBackendHandlers(app manifest.Manifest) manifest.Manifest { } for _, action := range page.Blocks.Actions { binding := bindAction(page, action, pkg) - if binding.Status == manifest.BackendBindingMissing { + if binding.Status == source.BackendBindingMissing { inlineBinding := bindAction(page, action, defaultInlinePkg()) - if inlineBinding.Status != manifest.BackendBindingMissing { + if inlineBinding.Status != source.BackendBindingMissing { binding = inlineBinding } } @@ -72,9 +73,9 @@ func BindBackendHandlers(app manifest.Manifest) manifest.Manifest { } for _, api := range page.Blocks.APIs { binding := bindAPI(page, api, pkg) - if binding.Status == manifest.BackendBindingMissing { + if binding.Status == source.BackendBindingMissing { inlineBinding := bindAPI(page, api, defaultInlinePkg()) - if inlineBinding.Status != manifest.BackendBindingMissing { + if inlineBinding.Status != source.BackendBindingMissing { binding = inlineBinding } } @@ -131,28 +132,28 @@ func bindLoad(page manifest.Page, pkg featurePackage) manifest.BackendBinding { if function, ok := pkg.Functions[functionName]; ok { binding := baseBackendBinding(page, loadHandlerKind, functionName, "GET", page.Route, pkg) if !function.Load() { - binding.Status = manifest.BackendBindingUnsupportedSignature + binding.Status = source.BackendBindingUnsupportedSignature binding.Message = fmt.Sprintf("GOWDK SSR load handler %s.%s must have signature func(ssr.LoadContext) map[string]any or func(ssr.LoadContext) (map[string]any, error)", packageLabel(pkg), functionName) return binding } binding.Signature = function.Signature - binding.Status = manifest.BackendBindingBound + binding.Status = source.BackendBindingBound return binding } inlinePkg := inspectInlineScriptFeaturePackage(page, "ssr") if function, ok := inlinePkg.Functions[functionName]; ok { binding := baseBackendBinding(page, loadHandlerKind, functionName, "GET", page.Route, inlinePkg) if !function.Load() { - binding.Status = manifest.BackendBindingUnsupportedSignature + binding.Status = source.BackendBindingUnsupportedSignature binding.Message = fmt.Sprintf("GOWDK SSR load handler %s.%s must have signature func(ssr.LoadContext) map[string]any or func(ssr.LoadContext) (map[string]any, error)", packageLabel(inlinePkg), functionName) return binding } binding.Signature = function.Signature - binding.Status = manifest.BackendBindingBound + binding.Status = source.BackendBindingBound return binding } binding := baseBackendBinding(page, loadHandlerKind, functionName, "GET", page.Route, pkg) - binding.Status = manifest.BackendBindingMissing + binding.Status = source.BackendBindingMissing binding.Message = fmt.Sprintf("GOWDK SSR load handler %s.%s is not implemented", packageLabel(pkg), functionName) return binding } @@ -165,12 +166,12 @@ func bindStandaloneAction(endpoint manifest.EndpointDeclaration, pkg featurePack binding := baseStandaloneBackendBinding(endpoint, actionHandlerKind, method, pkg) function, ok := pkg.Functions[binding.FunctionName] if !ok { - binding.Status = manifest.BackendBindingMissing + binding.Status = source.BackendBindingMissing binding.Message = fmt.Sprintf("GOWDK action handler %s.%s is not implemented", packageLabel(pkg), binding.FunctionName) return binding } if !function.Action() { - binding.Status = manifest.BackendBindingUnsupportedSignature + binding.Status = source.BackendBindingUnsupportedSignature if function.SupportMessage != "" { binding.Message = fmt.Sprintf("GOWDK action handler %s.%s is unsupported: %s", packageLabel(pkg), binding.FunctionName, function.SupportMessage) } else { @@ -182,7 +183,7 @@ func bindStandaloneAction(endpoint manifest.EndpointDeclaration, pkg featurePack binding.InputType = function.InputType binding.InputPointer = function.InputPointer binding.InputFields = function.InputFields - binding.Status = manifest.BackendBindingBound + binding.Status = source.BackendBindingBound return binding } @@ -194,17 +195,17 @@ func bindStandaloneAPI(endpoint manifest.EndpointDeclaration, pkg featurePackage binding := baseStandaloneBackendBinding(endpoint, apiHandlerKind, method, pkg) function, ok := pkg.Functions[binding.FunctionName] if !ok { - binding.Status = manifest.BackendBindingMissing + binding.Status = source.BackendBindingMissing binding.Message = fmt.Sprintf("GOWDK API handler %s.%s is not implemented", packageLabel(pkg), binding.FunctionName) return binding } if !function.API() { - binding.Status = manifest.BackendBindingUnsupportedSignature + binding.Status = source.BackendBindingUnsupportedSignature binding.Message = fmt.Sprintf("GOWDK API handler %s.%s must have signature func(context.Context, *http.Request) (response.Response, error)", packageLabel(pkg), binding.FunctionName) return binding } binding.Signature = function.Signature - binding.Status = manifest.BackendBindingBound + binding.Status = source.BackendBindingBound return binding } @@ -220,12 +221,12 @@ func bindAction(page manifest.Page, action manifest.Action, pkg featurePackage) binding := baseBackendBinding(page, actionHandlerKind, action.Name, method, route, pkg) function, ok := pkg.Functions[binding.FunctionName] if !ok { - binding.Status = manifest.BackendBindingMissing + binding.Status = source.BackendBindingMissing binding.Message = fmt.Sprintf("GOWDK action handler %s.%s is not implemented", packageLabel(pkg), binding.FunctionName) return binding } if !function.Action() { - binding.Status = manifest.BackendBindingUnsupportedSignature + binding.Status = source.BackendBindingUnsupportedSignature if function.SupportMessage != "" { binding.Message = fmt.Sprintf("GOWDK action handler %s.%s is unsupported: %s", packageLabel(pkg), binding.FunctionName, function.SupportMessage) } else { @@ -237,7 +238,7 @@ func bindAction(page manifest.Page, action manifest.Action, pkg featurePackage) binding.InputType = function.InputType binding.InputPointer = function.InputPointer binding.InputFields = function.InputFields - binding.Status = manifest.BackendBindingBound + binding.Status = source.BackendBindingBound return binding } @@ -253,17 +254,17 @@ func bindAPI(page manifest.Page, api manifest.API, pkg featurePackage) manifest. binding := baseBackendBinding(page, apiHandlerKind, api.Name, method, route, pkg) function, ok := pkg.Functions[binding.FunctionName] if !ok { - binding.Status = manifest.BackendBindingMissing + binding.Status = source.BackendBindingMissing binding.Message = fmt.Sprintf("GOWDK API handler %s.%s is not implemented", packageLabel(pkg), binding.FunctionName) return binding } if !function.API() { - binding.Status = manifest.BackendBindingUnsupportedSignature + binding.Status = source.BackendBindingUnsupportedSignature binding.Message = fmt.Sprintf("GOWDK API handler %s.%s must have signature func(context.Context, *http.Request) (response.Response, error)", packageLabel(pkg), binding.FunctionName) return binding } binding.Signature = function.Signature - binding.Status = manifest.BackendBindingBound + binding.Status = source.BackendBindingBound return binding } @@ -278,12 +279,12 @@ func bindFragment(page manifest.Page, fragment manifest.FragmentEndpoint, pkg fe return manifest.BackendBinding{}, false } if !function.Fragment() { - binding.Status = manifest.BackendBindingUnsupportedSignature + binding.Status = source.BackendBindingUnsupportedSignature binding.Message = fmt.Sprintf("GOWDK fragment handler %s.%s must have signature func(context.Context) (response.Response, error)", packageLabel(pkg), binding.FunctionName) return binding, true } - binding.Signature = manifest.BackendSignatureFragment - binding.Status = manifest.BackendBindingBound + binding.Signature = source.BackendSignatureFragment + binding.Status = source.BackendBindingBound return binding, true } @@ -298,7 +299,7 @@ func baseBackendBinding(page manifest.Page, kind, blockName, method, route strin ImportPath: pkg.ImportPath, PackageName: pkg.Name, FunctionName: blockName, - Status: manifest.BackendBindingMissing, + Status: source.BackendBindingMissing, } } @@ -313,7 +314,7 @@ func baseStandaloneBackendBinding(endpoint manifest.EndpointDeclaration, kind, m ImportPath: pkg.ImportPath, PackageName: pkg.Name, FunctionName: endpoint.Name, - Status: manifest.BackendBindingMissing, + Status: source.BackendBindingMissing, } } @@ -327,11 +328,11 @@ func packageLabel(pkg featurePackage) string { return "feature" } -func sourceDir(source string) string { - if strings.TrimSpace(source) == "" { +func sourceDir(sourcePath string) string { + if strings.TrimSpace(sourcePath) == "" { return "." } - return filepath.Dir(source) + return filepath.Dir(sourcePath) } type featurePackage struct { @@ -342,22 +343,22 @@ type featurePackage struct { } type inputStruct struct { - Fields []manifest.BackendInputField + Fields []source.BackendInputField Message string } type featureFunction struct { Name string - Signature manifest.BackendSignatureKind + Signature source.BackendSignatureKind InputType string InputPointer bool - InputFields []manifest.BackendInputField + InputFields []source.BackendInputField SupportMessage string } func (function featureFunction) Action() bool { switch function.Signature { - case manifest.BackendSignatureAction0, manifest.BackendSignatureActionValues, manifest.BackendSignatureActionForm, manifest.BackendSignatureActionFormPtr: + case source.BackendSignatureAction0, source.BackendSignatureActionValues, source.BackendSignatureActionForm, source.BackendSignatureActionFormPtr: return true default: return false @@ -365,15 +366,15 @@ func (function featureFunction) Action() bool { } func (function featureFunction) API() bool { - return function.Signature == manifest.BackendSignatureAPI + return function.Signature == source.BackendSignatureAPI } func (function featureFunction) Fragment() bool { - return function.Signature == manifest.BackendSignatureAction0 || function.Signature == manifest.BackendSignatureFragment + return function.Signature == source.BackendSignatureAction0 || function.Signature == source.BackendSignatureFragment } func (function featureFunction) Load() bool { - return function.Signature == manifest.BackendSignatureLoad || function.Signature == manifest.BackendSignatureLoadError + return function.Signature == source.BackendSignatureLoad || function.Signature == source.BackendSignatureLoadError } func inspectFeaturePackage(dir string) featurePackage { @@ -416,9 +417,9 @@ func inspectFeaturePackage(dir string) featurePackage { continue } signature, inputType, inputPointer := backendSignature(fn.Type, imports) - var inputFields []manifest.BackendInputField + var inputFields []source.BackendInputField var supportMessage string - if signature == manifest.BackendSignatureActionForm || signature == manifest.BackendSignatureActionFormPtr { + if signature == source.BackendSignatureActionForm || signature == source.BackendSignatureActionFormPtr { inputStruct, ok := inputStructs[inputType] if !ok { supportMessage = fmt.Sprintf("typed action input %s must be an exported struct in the same package", inputType) @@ -427,7 +428,7 @@ func inspectFeaturePackage(dir string) featurePackage { supportMessage = inputStruct.Message signature = "" } else { - inputFields = append([]manifest.BackendInputField(nil), inputStruct.Fields...) + inputFields = append([]source.BackendInputField(nil), inputStruct.Fields...) } } pkg.Functions[fn.Name.Name] = featureFunction{ @@ -474,9 +475,9 @@ func inspectInlineScriptFeaturePackage(page manifest.Page, target string) featur continue } signature, inputType, inputPointer := backendSignature(fn.Type, imports) - var inputFields []manifest.BackendInputField + var inputFields []source.BackendInputField var supportMessage string - if signature == manifest.BackendSignatureActionForm || signature == manifest.BackendSignatureActionFormPtr { + if signature == source.BackendSignatureActionForm || signature == source.BackendSignatureActionFormPtr { inputStruct, ok := inputStructs[inputType] if !ok { supportMessage = fmt.Sprintf("typed action input %s must be an exported struct in the same package", inputType) @@ -485,7 +486,7 @@ func inspectInlineScriptFeaturePackage(page manifest.Page, target string) featur supportMessage = inputStruct.Message signature = "" } else { - inputFields = append([]manifest.BackendInputField(nil), inputStruct.Fields...) + inputFields = append([]source.BackendInputField(nil), inputStruct.Fields...) } } pkg.Functions[fn.Name.Name] = featureFunction{ @@ -530,7 +531,7 @@ func backendInputStruct(typeName string, structType *ast.StructType) inputStruct return inputStruct{} } seen := map[string]bool{} - var fields []manifest.BackendInputField + var fields []source.BackendInputField for _, field := range structType.Fields.List { if len(field.Names) == 0 { return inputStruct{Message: fmt.Sprintf("typed action input %s cannot use embedded fields", typeName)} @@ -564,7 +565,7 @@ func backendInputStruct(typeName string, structType *ast.StructType) inputStruct return inputStruct{Message: fmt.Sprintf("typed action input %s maps multiple fields to form field %q", typeName, nameFormName)} } seen[nameFormName] = true - fields = append(fields, manifest.BackendInputField{ + fields = append(fields, source.BackendInputField{ FieldName: name.Name, FormName: nameFormName, Type: fieldType, @@ -690,12 +691,12 @@ func astImportAliases(file *ast.File) map[string]string { return imports } -func backendSignature(function *ast.FuncType, imports map[string]string) (manifest.BackendSignatureKind, string, bool) { +func backendSignature(function *ast.FuncType, imports map[string]string) (source.BackendSignatureKind, string, bool) { if kind, inputType, inputPointer, ok := actionSignature(function, imports); ok { return kind, inputType, inputPointer } if isAPISignature(function, imports) { - return manifest.BackendSignatureAPI, "", false + return source.BackendSignatureAPI, "", false } if signature, ok := loadSignature(function, imports); ok { return signature, "", false @@ -703,7 +704,7 @@ func backendSignature(function *ast.FuncType, imports map[string]string) (manife return "", "", false } -func actionSignature(function *ast.FuncType, imports map[string]string) (manifest.BackendSignatureKind, string, bool, bool) { +func actionSignature(function *ast.FuncType, imports map[string]string) (source.BackendSignatureKind, string, bool, bool) { if function == nil || function.Params == nil || function.Results == nil { return "", "", false, false } @@ -721,18 +722,18 @@ func actionSignature(function *ast.FuncType, imports map[string]string) (manifes return "", "", false, false } if len(function.Params.List) == 1 { - return manifest.BackendSignatureAction0, "", false, true + return source.BackendSignatureAction0, "", false, true } second := function.Params.List[1].Type if isSelector(second, imports, formImportPath, "Values") { - return manifest.BackendSignatureActionValues, "", false, true + return source.BackendSignatureActionValues, "", false, true } if ident, ok := second.(*ast.Ident); ok && ident.IsExported() { - return manifest.BackendSignatureActionForm, ident.Name, false, true + return source.BackendSignatureActionForm, ident.Name, false, true } if pointer, ok := second.(*ast.StarExpr); ok { if ident, ok := pointer.X.(*ast.Ident); ok && ident.IsExported() { - return manifest.BackendSignatureActionFormPtr, ident.Name, true, true + return source.BackendSignatureActionFormPtr, ident.Name, true, true } } return "", "", false, false @@ -753,7 +754,7 @@ func isAPISignature(function *ast.FuncType, imports map[string]string) bool { isError(function.Results.List[1].Type) } -func loadSignature(function *ast.FuncType, imports map[string]string) (manifest.BackendSignatureKind, bool) { +func loadSignature(function *ast.FuncType, imports map[string]string) (source.BackendSignatureKind, bool) { if function == nil || function.Params == nil || function.Results == nil { return "", false } @@ -761,10 +762,10 @@ func loadSignature(function *ast.FuncType, imports map[string]string) (manifest. return "", false } if len(function.Results.List) == 1 && isMapStringAny(function.Results.List[0].Type) { - return manifest.BackendSignatureLoad, true + return source.BackendSignatureLoad, true } if len(function.Results.List) == 2 && isMapStringAny(function.Results.List[0].Type) && isError(function.Results.List[1].Type) { - return manifest.BackendSignatureLoadError, true + return source.BackendSignatureLoadError, true } return "", false } diff --git a/internal/compiler/backend_bindings_test.go b/internal/compiler/backend_bindings_test.go index 39bdb4b..37a5040 100644 --- a/internal/compiler/backend_bindings_test.go +++ b/internal/compiler/backend_bindings_test.go @@ -9,6 +9,7 @@ import ( "github.com/cssbruno/gowdk" "github.com/cssbruno/gowdk/internal/goblockgen" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func TestBindBackendHandlersClassifiesSupportedActionSignatures(t *testing.T) { @@ -103,26 +104,26 @@ func BrokenFragment(context.Context, *http.Request) (response.Response, error) { }}}) bindings := compilerBindingsByBlock(app.BackendBindings) - assertBinding(t, bindings["Ping"], manifest.BackendBindingBound, manifest.BackendSignatureAction0, "", false) - assertBinding(t, bindings["Login"], manifest.BackendBindingBound, manifest.BackendSignatureActionForm, "LoginInput", false) - assertBinding(t, bindings["LoginPtr"], manifest.BackendBindingBound, manifest.BackendSignatureActionFormPtr, "LoginInput", true) - assertBinding(t, bindings["Raw"], manifest.BackendBindingBound, manifest.BackendSignatureActionValues, "", false) - assertBinding(t, bindings["Session"], manifest.BackendBindingBound, manifest.BackendSignatureAPI, "", false) + assertBinding(t, bindings["Ping"], source.BackendBindingBound, source.BackendSignatureAction0, "", false) + assertBinding(t, bindings["Login"], source.BackendBindingBound, source.BackendSignatureActionForm, "LoginInput", false) + assertBinding(t, bindings["LoginPtr"], source.BackendBindingBound, source.BackendSignatureActionFormPtr, "LoginInput", true) + assertBinding(t, bindings["Raw"], source.BackendBindingBound, source.BackendSignatureActionValues, "", false) + assertBinding(t, bindings["Session"], source.BackendBindingBound, source.BackendSignatureAPI, "", false) assertInputFields(t, bindings["Login"].InputFields, "Email:email:string,Tags:tag:[]string,Remember:remember:bool,Age:age:int,Score:score:uint64") - if got := bindings["Broken"]; got.Status != manifest.BackendBindingUnsupportedSignature { + if got := bindings["Broken"]; got.Status != source.BackendBindingUnsupportedSignature { t.Fatalf("expected Broken unsupported signature, got %#v", got) } if !strings.Contains(bindings["Broken"].Message, "unsupported field type") { t.Fatalf("expected Broken message to explain unsupported field type, got %q", bindings["Broken"].Message) } - if got := bindings["Bad"]; got.Status != manifest.BackendBindingUnsupportedSignature { + if got := bindings["Bad"]; got.Status != source.BackendBindingUnsupportedSignature { t.Fatalf("expected Bad unsupported signature, got %#v", got) } - if got := bindings["Missing"]; got.Status != manifest.BackendBindingMissing { + if got := bindings["Missing"]; got.Status != source.BackendBindingMissing { t.Fatalf("expected Missing binding, got %#v", got) } - assertBinding(t, bindings["List"], manifest.BackendBindingBound, manifest.BackendSignatureFragment, "", false) - if got := bindings["BrokenFragment"]; got.Status != manifest.BackendBindingUnsupportedSignature { + assertBinding(t, bindings["List"], source.BackendBindingBound, source.BackendSignatureFragment, "", false) + if got := bindings["BrokenFragment"]; got.Status != source.BackendBindingUnsupportedSignature { t.Fatalf("expected BrokenFragment unsupported signature, got %#v", got) } if _, ok := bindings["MissingFragment"]; ok { @@ -190,15 +191,15 @@ func LoadBroken() map[string]any { }}) bindings := compilerBindingsByBlock(app.BackendBindings) - assertBinding(t, bindings["LoadDashboard"], manifest.BackendBindingBound, manifest.BackendSignatureLoadError, "", false) - assertBinding(t, bindings["LoadProfile"], manifest.BackendBindingBound, manifest.BackendSignatureLoad, "", false) - if got := bindings["LoadBroken"]; got.Status != manifest.BackendBindingUnsupportedSignature { + assertBinding(t, bindings["LoadDashboard"], source.BackendBindingBound, source.BackendSignatureLoadError, "", false) + assertBinding(t, bindings["LoadProfile"], source.BackendBindingBound, source.BackendSignatureLoad, "", false) + if got := bindings["LoadBroken"]; got.Status != source.BackendBindingUnsupportedSignature { t.Fatalf("expected LoadBroken unsupported signature, got %#v", got) } - if got := bindings["LoadMissing"]; got.Status != manifest.BackendBindingMissing { + if got := bindings["LoadMissing"]; got.Status != source.BackendBindingMissing { t.Fatalf("expected LoadMissing missing binding, got %#v", got) } - if app.Pages[0].LoadBinding.FunctionName != "LoadDashboard" || app.Pages[0].LoadBinding.Status != manifest.BackendBindingBound { + if app.Pages[0].LoadBinding.FunctionName != "LoadDashboard" || app.Pages[0].LoadBinding.Status != source.BackendBindingBound { t.Fatalf("expected page load binding to be attached, got %#v", app.Pages[0].LoadBinding) } } @@ -229,7 +230,7 @@ func TestBindBackendHandlersBindsInlineSSRScriptLoad(t *testing.T) { app := BindBackendHandlers(manifest.Manifest{Pages: []manifest.Page{page}}) bindings := compilerBindingsByBlock(app.BackendBindings) binding := bindings["LoadDashboard"] - if binding.Status != manifest.BackendBindingBound || binding.Signature != manifest.BackendSignatureLoadError { + if binding.Status != source.BackendBindingBound || binding.Signature != source.BackendSignatureLoadError { t.Fatalf("expected inline SSR load binding, got %#v", binding) } if binding.ImportPath != goblockgen.GeneratedImportPath("pages") || binding.PackageName != "pages" { @@ -294,9 +295,9 @@ func List(context.Context) (response.Response, error) { t.Fatalf("expected %s to bind generated inline go package, got %#v", name, bindings[name]) } } - assertBinding(t, bindings["Subscribe"], manifest.BackendBindingBound, manifest.BackendSignatureAction0, "", false) - assertBinding(t, bindings["Session"], manifest.BackendBindingBound, manifest.BackendSignatureAPI, "", false) - assertBinding(t, bindings["List"], manifest.BackendBindingBound, manifest.BackendSignatureFragment, "", false) + assertBinding(t, bindings["Subscribe"], source.BackendBindingBound, source.BackendSignatureAction0, "", false) + assertBinding(t, bindings["Session"], source.BackendBindingBound, source.BackendSignatureAPI, "", false) + assertBinding(t, bindings["List"], source.BackendBindingBound, source.BackendSignatureFragment, "", false) } func TestDiscoverGoEndpointCommentsBindsStandaloneEndpoints(t *testing.T) { @@ -341,17 +342,17 @@ func Session(context.Context, *http.Request) (response.Response, error) { } app = BindBackendHandlers(app) bindings := compilerBindingsByBlock(app.BackendBindings) - assertBinding(t, bindings["Login"], manifest.BackendBindingBound, manifest.BackendSignatureAction0, "", false) - assertBinding(t, bindings["Session"], manifest.BackendBindingBound, manifest.BackendSignatureAPI, "", false) + assertBinding(t, bindings["Login"], source.BackendBindingBound, source.BackendSignatureAction0, "", false) + assertBinding(t, bindings["Session"], source.BackendBindingBound, source.BackendSignatureAPI, "", false) } func TestValidateManifestRejectsGoEndpointConflictWithGOWDKEndpoint(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") + sourcePath := filepath.Join(root, "home.page.gwdk") app := manifest.Manifest{ Pages: []manifest.Page{{ ID: "home", - Source: source, + Source: sourcePath, Route: "/", Blocks: manifest.Blocks{ View: true, @@ -410,7 +411,7 @@ func TestValidateBackendBindingPolicyAllowsDevelopmentMissingHandler(t *testing. Method: "POST", Route: "/login", FunctionName: "Login", - Status: manifest.BackendBindingMissing, + Status: source.BackendBindingMissing, }}} if err := ValidateBackendBindingPolicy(gowdk.Config{}, app); err != nil { @@ -426,7 +427,7 @@ func TestValidateBackendBindingPolicyAllowsExplicitProductionStubMode(t *testing Method: "GET", Route: "/api/session", FunctionName: "Session", - Status: manifest.BackendBindingUnsupportedSignature, + Status: source.BackendBindingUnsupportedSignature, }}} config := gowdk.Config{Build: gowdk.BuildConfig{ @@ -438,14 +439,14 @@ func TestValidateBackendBindingPolicyAllowsExplicitProductionStubMode(t *testing } } -func assertBinding(t *testing.T, binding manifest.BackendBinding, status manifest.BackendBindingStatus, signature manifest.BackendSignatureKind, inputType string, inputPointer bool) { +func assertBinding(t *testing.T, binding manifest.BackendBinding, status source.BackendBindingStatus, signature source.BackendSignatureKind, inputType string, inputPointer bool) { t.Helper() if binding.Status != status || binding.Signature != signature || binding.InputType != inputType || binding.InputPointer != inputPointer { t.Fatalf("unexpected binding: %#v", binding) } } -func assertInputFields(t *testing.T, fields []manifest.BackendInputField, expected string) { +func assertInputFields(t *testing.T, fields []source.BackendInputField, expected string) { t.Helper() parts := make([]string, 0, len(fields)) for _, field := range fields { diff --git a/internal/compiler/go_endpoints.go b/internal/compiler/go_endpoints.go index d53dc6c..7759111 100644 --- a/internal/compiler/go_endpoints.go +++ b/internal/compiler/go_endpoints.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) // DiscoverGoEndpointComments merges optional //gowdk:act and //gowdk:api @@ -43,11 +44,11 @@ func DiscoverGoEndpointComments(app manifest.Manifest) (manifest.Manifest, error func endpointSourceDirs(app manifest.Manifest) []string { seen := map[string]bool{} var dirs []string - add := func(source string) { - if strings.TrimSpace(source) == "" { + add := func(sourcePath string) { + if strings.TrimSpace(sourcePath) == "" { return } - dir := sourceDir(source) + dir := sourceDir(sourcePath) abs, err := filepath.Abs(dir) if err == nil { dir = abs @@ -181,30 +182,30 @@ func isASCIILetters(value string) bool { } func goEndpointDiagnostic(fileSet *token.FileSet, path string, node ast.Node, code string, message string) ValidationError { - var span manifest.SourceSpan + var span source.SourceSpan if node != nil { span = goTokenSpan(fileSet, node.Pos(), node.End()) } return ValidationError{Code: code, Source: path, Span: span, Message: message} } -func goTokenSpan(fileSet *token.FileSet, start token.Pos, end token.Pos) manifest.SourceSpan { +func goTokenSpan(fileSet *token.FileSet, start token.Pos, end token.Pos) source.SourceSpan { startPos := fileSet.Position(start) endPos := fileSet.Position(end) - return manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: startPos.Line, Column: startPos.Column}, - End: manifest.SourcePosition{Line: endPos.Line, Column: endPos.Column}, + return source.SourceSpan{ + Start: source.SourcePosition{Line: startPos.Line, Column: startPos.Column}, + End: source.SourcePosition{Line: endPos.Line, Column: endPos.Column}, } } -func routeParamSpansFallback(route string, fallback manifest.SourceSpan) []manifest.NamedSpan { +func routeParamSpansFallback(route string, fallback source.SourceSpan) []source.NamedSpan { info, issues := parseRoute(route) if len(issues) > 0 { return nil } - out := make([]manifest.NamedSpan, 0, len(info.Params)) + out := make([]source.NamedSpan, 0, len(info.Params)) for _, param := range info.Params { - out = append(out, manifest.NamedSpan{Name: param, Span: fallback}) + out = append(out, source.NamedSpan{Name: param, Span: fallback}) } return out } diff --git a/internal/buildgen/ir.go b/internal/compiler/manifest_from_ir.go similarity index 58% rename from internal/buildgen/ir.go rename to internal/compiler/manifest_from_ir.go index 1ab58f7..254ed55 100644 --- a/internal/buildgen/ir.go +++ b/internal/compiler/manifest_from_ir.go @@ -1,36 +1,55 @@ -package buildgen +package compiler import ( "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) -func buildModelFromIR(ir gwdkir.Program) manifest.Manifest { +// ManifestFromIR reconstructs a manifest.Manifest from compiler IR so the +// existing manifest-typed validators can run against an IR-first build path. +// +// This is the single IR->manifest conversion seam in the codebase. It lives in +// compiler (the package that owns validation) rather than being duplicated in +// each generated-output package. As individual validators move to read IR +// directly, this converter shrinks and is eventually removed; until then it +// keeps the build path validating identical data whether it starts from a +// parsed manifest or from IR. +func ManifestFromIR(ir gwdkir.Program) manifest.Manifest { app := manifest.Manifest{ Pages: make([]manifest.Page, 0, len(ir.Pages)), Components: make([]manifest.Component, 0, len(ir.Components)), Layouts: make([]manifest.Layout, 0, len(ir.Layouts)), - BackendBindings: make([]manifest.BackendBinding, 0, len(ir.Endpoints)), + BackendBindings: BackendBindingsFromIR(ir), } for _, page := range ir.Pages { - app.Pages = append(app.Pages, buildPageFromIR(page)) + app.Pages = append(app.Pages, pageFromIR(page)) } for _, component := range ir.Components { - app.Components = append(app.Components, buildComponentFromIR(component)) + app.Components = append(app.Components, componentFromIR(component)) } for _, layout := range ir.Layouts { - app.Layouts = append(app.Layouts, buildLayoutFromIR(layout)) + app.Layouts = append(app.Layouts, layoutFromIR(layout)) } + return app +} + +// BackendBindingsFromIR derives just the backend binding records from IR +// endpoints, without reconstructing the full page/component/layout manifest. +// Callers that only need bindings (e.g. build reporting) should use this instead +// of ManifestFromIR(ir).BackendBindings, which would allocate the whole model. +func BackendBindingsFromIR(ir gwdkir.Program) []manifest.BackendBinding { + out := make([]manifest.BackendBinding, 0, len(ir.Endpoints)) for _, endpoint := range ir.Endpoints { - binding := buildBackendBindingFromIR(endpoint) + binding := backendBindingFromIR(endpoint) if binding.Status != "" || binding.ImportPath != "" || binding.FunctionName != "" { - app.BackendBindings = append(app.BackendBindings, binding) + out = append(out, binding) } } - return app + return out } -func buildBackendBindingFromIR(endpoint gwdkir.Endpoint) manifest.BackendBinding { +func backendBindingFromIR(endpoint gwdkir.Endpoint) manifest.BackendBinding { kind := "action" if endpoint.Kind == gwdkir.EndpointAPI { kind = "api" @@ -48,19 +67,19 @@ func buildBackendBindingFromIR(endpoint gwdkir.Endpoint) manifest.BackendBinding Signature: endpoint.Binding.Signature, InputType: endpoint.Binding.InputType, InputPointer: endpoint.Binding.InputPointer, - InputFields: append([]manifest.BackendInputField(nil), endpoint.Binding.InputFields...), + InputFields: append([]source.BackendInputField(nil), endpoint.Binding.InputFields...), Status: endpoint.Binding.Status, Message: endpoint.Binding.Message, } } -func buildPageFromIR(page gwdkir.Page) manifest.Page { +func pageFromIR(page gwdkir.Page) manifest.Page { return manifest.Page{ Source: page.Source, Package: page.Package, ID: page.ID, Route: page.Route, - RouteParams: append([]manifest.RouteParam(nil), page.RouteParams...), + RouteParams: append([]source.RouteParam(nil), page.RouteParams...), Render: page.Render, Cache: page.Cache, Revalidate: page.Revalidate, @@ -70,13 +89,13 @@ func buildPageFromIR(page gwdkir.Page) manifest.Page { Guard: append([]string(nil), page.Guards...), CSS: append([]string(nil), page.CSS...), JS: append([]string(nil), page.JS...), - InlineJS: copyInlineScripts(page.InlineJS), - Imports: buildImportsFromIR(page.Imports), - Uses: buildUsesFromIR(page.Uses), - Stores: buildStoresFromIR(page.Stores), + InlineJS: copyInlineScriptsFromIR(page.InlineJS), + Imports: importsFromIR(page.Imports), + Uses: usesFromIR(page.Uses), + Stores: storesFromIR(page.Stores), Paths: page.Blocks.PathsBody != "", - Blocks: buildBlocksFromIR(page.Blocks), - LoadBinding: buildLoadBindingFromIR(page), + Blocks: blocksFromIR(page.Blocks), + LoadBinding: loadBindingFromIR(page), Spans: manifest.PageSpans{ Package: page.Spans.Package, Page: page.Spans.Page, @@ -89,17 +108,17 @@ func buildPageFromIR(page gwdkir.Page) manifest.Page { Description: page.Spans.Description, Canonical: page.Spans.Canonical, Image: page.Spans.Image, - Layouts: append([]manifest.NamedSpan(nil), page.Spans.Layouts...), - Guard: append([]manifest.NamedSpan(nil), page.Spans.Guard...), - CSS: append([]manifest.NamedSpan(nil), page.Spans.CSS...), - JS: append([]manifest.NamedSpan(nil), page.Spans.JS...), - InlineJS: append([]manifest.NamedSpan(nil), page.Spans.InlineJS...), - RouteParams: append([]manifest.NamedSpan(nil), page.Spans.RouteParams...), + Layouts: append([]source.NamedSpan(nil), page.Spans.Layouts...), + Guard: append([]source.NamedSpan(nil), page.Spans.Guard...), + CSS: append([]source.NamedSpan(nil), page.Spans.CSS...), + JS: append([]source.NamedSpan(nil), page.Spans.JS...), + InlineJS: append([]source.NamedSpan(nil), page.Spans.InlineJS...), + RouteParams: append([]source.NamedSpan(nil), page.Spans.RouteParams...), }, } } -func buildLoadBindingFromIR(page gwdkir.Page) manifest.BackendBinding { +func loadBindingFromIR(page gwdkir.Page) manifest.BackendBinding { binding := page.LoadBinding if binding.Status == "" && binding.ImportPath == "" && binding.FunctionName == "" { return manifest.BackendBinding{} @@ -120,57 +139,57 @@ func buildLoadBindingFromIR(page gwdkir.Page) manifest.BackendBinding { } } -func buildComponentFromIR(component gwdkir.Component) manifest.Component { +func componentFromIR(component gwdkir.Component) manifest.Component { return manifest.Component{ Source: component.Source, Package: component.Package, Name: component.Name, - Imports: buildImportsFromIR(component.Imports), - Uses: buildUsesFromIR(component.Uses), + Imports: importsFromIR(component.Imports), + Uses: usesFromIR(component.Uses), CSS: append([]string(nil), component.CSS...), JS: append([]string(nil), component.JS...), - InlineJS: copyInlineScripts(component.InlineJS), + InlineJS: copyInlineScriptsFromIR(component.InlineJS), Assets: append([]string(nil), component.Assets...), - Props: buildPropsFromIR(component.Props), - PropsType: buildGoTypeRefFromIR(component.PropsType), - State: buildStateContractFromIR(component.State), + Props: propsFromIR(component.Props), + PropsType: goTypeRefFromIR(component.PropsType), + State: stateContractFromIR(component.State), WASM: manifest.WASMContract(component.WASM), - Exports: buildExportsFromIR(component.Exports), - Emits: buildEmitsFromIR(component.Emits), - Blocks: buildBlocksFromIR(component.Blocks), + Exports: exportsFromIR(component.Exports), + Emits: emitsFromIR(component.Emits), + Blocks: blocksFromIR(component.Blocks), Span: component.Span, PackageSpan: component.PackageSpan, Spans: manifest.ComponentSpans{ - CSS: append([]manifest.NamedSpan(nil), component.Spans.CSS...), - JS: append([]manifest.NamedSpan(nil), component.Spans.JS...), - InlineJS: append([]manifest.NamedSpan(nil), component.Spans.InlineJS...), - Assets: append([]manifest.NamedSpan(nil), component.Spans.Assets...), + CSS: append([]source.NamedSpan(nil), component.Spans.CSS...), + JS: append([]source.NamedSpan(nil), component.Spans.JS...), + InlineJS: append([]source.NamedSpan(nil), component.Spans.InlineJS...), + Assets: append([]source.NamedSpan(nil), component.Spans.Assets...), }, } } -func copyInlineScripts(scripts []manifest.InlineScript) []manifest.InlineScript { +func copyInlineScriptsFromIR(scripts []source.InlineScript) []source.InlineScript { if len(scripts) == 0 { return nil } - out := make([]manifest.InlineScript, len(scripts)) + out := make([]source.InlineScript, len(scripts)) copy(out, scripts) return out } -func buildLayoutFromIR(layout gwdkir.Layout) manifest.Layout { +func layoutFromIR(layout gwdkir.Layout) manifest.Layout { return manifest.Layout{ Source: layout.Source, Package: layout.Package, ID: layout.ID, - Uses: buildUsesFromIR(layout.Uses), - Blocks: buildBlocksFromIR(layout.Blocks), + Uses: usesFromIR(layout.Uses), + Blocks: blocksFromIR(layout.Blocks), Span: layout.Span, PackageSpan: layout.PackageSpan, } } -func buildBlocksFromIR(blocks gwdkir.Blocks) manifest.Blocks { +func blocksFromIR(blocks gwdkir.Blocks) manifest.Blocks { return manifest.Blocks{ PathsBody: blocks.PathsBody, Build: blocks.Build, @@ -179,32 +198,32 @@ func buildBlocksFromIR(blocks gwdkir.Blocks) manifest.Blocks { LoadBody: blocks.LoadBody, Client: blocks.Client, ClientBody: blocks.ClientBody, - GoBlocks: buildScriptsFromIR(blocks.GoBlocks), + GoBlocks: scriptsFromIR(blocks.GoBlocks), View: blocks.View, ViewBody: blocks.ViewBody, Style: blocks.Style, StyleBody: blocks.StyleBody, - Actions: buildActionsFromIR(blocks.Actions), - APIs: buildAPIsFromIR(blocks.APIs), - Fragments: buildFragmentEndpointsFromIR(blocks.Fragments), + Actions: actionsFromIR(blocks.Actions), + APIs: apisFromIR(blocks.APIs), + Fragments: fragmentEndpointsFromIR(blocks.Fragments), Spans: manifest.BlockSpans{ Paths: blocks.Spans.Paths, Build: blocks.Spans.Build, Load: blocks.Spans.Load, Client: blocks.Spans.Client, - GoBlocks: append([]manifest.NamedSpan(nil), blocks.Spans.GoBlocks...), + GoBlocks: append([]source.NamedSpan(nil), blocks.Spans.GoBlocks...), View: blocks.Spans.View, ViewBodyStart: blocks.Spans.ViewBodyStart, - Actions: append([]manifest.NamedSpan(nil), blocks.Spans.Actions...), - APIs: append([]manifest.NamedSpan(nil), blocks.Spans.APIs...), - Fragments: append([]manifest.NamedSpan(nil), blocks.Spans.Fragments...), + Actions: append([]source.NamedSpan(nil), blocks.Spans.Actions...), + APIs: append([]source.NamedSpan(nil), blocks.Spans.APIs...), + Fragments: append([]source.NamedSpan(nil), blocks.Spans.Fragments...), Exports: blocks.Spans.Exports, Emits: blocks.Spans.Emits, }, } } -func buildScriptsFromIR(scripts []gwdkir.GoBlock) []manifest.GoBlock { +func scriptsFromIR(scripts []gwdkir.GoBlock) []manifest.GoBlock { out := make([]manifest.GoBlock, 0, len(scripts)) for _, script := range scripts { out = append(out, manifest.GoBlock{ @@ -216,7 +235,7 @@ func buildScriptsFromIR(scripts []gwdkir.GoBlock) []manifest.GoBlock { return out } -func buildActionsFromIR(actions []gwdkir.Action) []manifest.Action { +func actionsFromIR(actions []gwdkir.Action) []manifest.Action { out := make([]manifest.Action, 0, len(actions)) for _, action := range actions { out = append(out, manifest.Action{ @@ -228,11 +247,11 @@ func buildActionsFromIR(actions []gwdkir.Action) []manifest.Action { InputType: action.InputType, ValidatesInput: action.ValidatesInput, Redirect: action.Redirect, - Fragments: buildFragmentsFromIR(action.Fragments), + Fragments: fragmentsFromIR(action.Fragments), ErrorPage: action.ErrorPage, Span: action.Span, RouteSpan: action.RouteSpan, - RouteParams: append([]manifest.NamedSpan(nil), action.RouteParams...), + RouteParams: append([]source.NamedSpan(nil), action.RouteParams...), InputSpan: action.InputSpan, ValidationSpan: action.ValidationSpan, RedirectSpan: action.RedirectSpan, @@ -242,7 +261,7 @@ func buildActionsFromIR(actions []gwdkir.Action) []manifest.Action { return out } -func buildAPIsFromIR(apis []gwdkir.API) []manifest.API { +func apisFromIR(apis []gwdkir.API) []manifest.API { out := make([]manifest.API, 0, len(apis)) for _, api := range apis { out = append(out, manifest.API{ @@ -252,14 +271,14 @@ func buildAPIsFromIR(apis []gwdkir.API) []manifest.API { ErrorPage: api.ErrorPage, Span: api.Span, RouteSpan: api.RouteSpan, - RouteParams: append([]manifest.NamedSpan(nil), api.RouteParams...), + RouteParams: append([]source.NamedSpan(nil), api.RouteParams...), ErrorPageSpan: api.ErrorPageSpan, }) } return out } -func buildFragmentsFromIR(fragments []gwdkir.Fragment) []manifest.Fragment { +func fragmentsFromIR(fragments []gwdkir.Fragment) []manifest.Fragment { out := make([]manifest.Fragment, 0, len(fragments)) for _, fragment := range fragments { out = append(out, manifest.Fragment{Target: fragment.Target, Body: fragment.Body, Span: fragment.Span}) @@ -267,7 +286,7 @@ func buildFragmentsFromIR(fragments []gwdkir.Fragment) []manifest.Fragment { return out } -func buildFragmentEndpointsFromIR(fragments []gwdkir.FragmentEndpoint) []manifest.FragmentEndpoint { +func fragmentEndpointsFromIR(fragments []gwdkir.FragmentEndpoint) []manifest.FragmentEndpoint { out := make([]manifest.FragmentEndpoint, 0, len(fragments)) for _, fragment := range fragments { out = append(out, manifest.FragmentEndpoint{ @@ -279,13 +298,13 @@ func buildFragmentEndpointsFromIR(fragments []gwdkir.FragmentEndpoint) []manifes Span: fragment.Span, RouteSpan: fragment.RouteSpan, TargetSpan: fragment.TargetSpan, - RouteParams: append([]manifest.NamedSpan(nil), fragment.RouteParams...), + RouteParams: append([]source.NamedSpan(nil), fragment.RouteParams...), }) } return out } -func buildImportsFromIR(imports []gwdkir.Import) []manifest.Import { +func importsFromIR(imports []gwdkir.Import) []manifest.Import { out := make([]manifest.Import, 0, len(imports)) for _, item := range imports { out = append(out, manifest.Import{Alias: item.Alias, Path: item.Path, Span: item.Span}) @@ -293,7 +312,7 @@ func buildImportsFromIR(imports []gwdkir.Import) []manifest.Import { return out } -func buildUsesFromIR(uses []gwdkir.Use) []manifest.Use { +func usesFromIR(uses []gwdkir.Use) []manifest.Use { out := make([]manifest.Use, 0, len(uses)) for _, item := range uses { out = append(out, manifest.Use{Alias: item.Alias, Package: item.Package, Span: item.Span}) @@ -301,20 +320,20 @@ func buildUsesFromIR(uses []gwdkir.Use) []manifest.Use { return out } -func buildStoresFromIR(stores []gwdkir.Store) []manifest.Store { +func storesFromIR(stores []gwdkir.Store) []manifest.Store { out := make([]manifest.Store, 0, len(stores)) for _, store := range stores { out = append(out, manifest.Store{ Name: store.Name, - Type: buildGoTypeRefFromIR(store.Type), - Init: buildGoFuncRefFromIR(store.Init), + Type: goTypeRefFromIR(store.Type), + Init: goFuncRefFromIR(store.Init), Span: store.Span, }) } return out } -func buildPropsFromIR(props []gwdkir.Prop) []manifest.Prop { +func propsFromIR(props []gwdkir.Prop) []manifest.Prop { out := make([]manifest.Prop, 0, len(props)) for _, prop := range props { out = append(out, manifest.Prop{Name: prop.Name, Type: prop.Type, Span: prop.Span}) @@ -322,7 +341,7 @@ func buildPropsFromIR(props []gwdkir.Prop) []manifest.Prop { return out } -func buildExportsFromIR(exports []gwdkir.Export) []manifest.Export { +func exportsFromIR(exports []gwdkir.Export) []manifest.Export { out := make([]manifest.Export, 0, len(exports)) for _, export := range exports { out = append(out, manifest.Export{Name: export.Name, Type: export.Type, Span: export.Span}) @@ -330,7 +349,7 @@ func buildExportsFromIR(exports []gwdkir.Export) []manifest.Export { return out } -func buildEmitsFromIR(emits []gwdkir.Emit) []manifest.Emit { +func emitsFromIR(emits []gwdkir.Emit) []manifest.Emit { out := make([]manifest.Emit, 0, len(emits)) for _, emit := range emits { params := make([]manifest.EmitParam, 0, len(emit.Params)) @@ -342,18 +361,18 @@ func buildEmitsFromIR(emits []gwdkir.Emit) []manifest.Emit { return out } -func buildStateContractFromIR(state gwdkir.StateContract) manifest.StateContract { +func stateContractFromIR(state gwdkir.StateContract) manifest.StateContract { return manifest.StateContract{ - Type: buildGoTypeRefFromIR(state.Type), - Init: buildGoFuncRefFromIR(state.Init), + Type: goTypeRefFromIR(state.Type), + Init: goFuncRefFromIR(state.Init), Span: state.Span, } } -func buildGoTypeRefFromIR(ref gwdkir.GoRef) manifest.GoTypeRef { +func goTypeRefFromIR(ref gwdkir.GoRef) manifest.GoTypeRef { return manifest.GoTypeRef{Alias: ref.Alias, Name: ref.Name, Span: ref.Span} } -func buildGoFuncRefFromIR(ref gwdkir.GoRef) manifest.GoFuncRef { +func goFuncRefFromIR(ref gwdkir.GoRef) manifest.GoFuncRef { return manifest.GoFuncRef{Alias: ref.Alias, Name: ref.Name, Span: ref.Span} } diff --git a/internal/compiler/manifest_from_ir_test.go b/internal/compiler/manifest_from_ir_test.go new file mode 100644 index 0000000..add27ca --- /dev/null +++ b/internal/compiler/manifest_from_ir_test.go @@ -0,0 +1,155 @@ +package compiler + +import ( + "testing" + + "github.com/cssbruno/gowdk" + "github.com/cssbruno/gowdk/internal/gwdkir" + "github.com/cssbruno/gowdk/internal/source" +) + +// sampleProgram returns an IR program that exercises the converter across pages, +// components, layouts, blocks, endpoints, and bindings. +func sampleProgram() gwdkir.Program { + return gwdkir.Program{ + Version: gwdkir.Version, + Pages: []gwdkir.Page{{ + Source: "pages/home.page.gwdk", + Package: "pages", + ID: "home", + Route: "/", + Render: gowdk.SPA, + Layouts: []string{"root"}, + Guards: []string{"public"}, + Blocks: gwdkir.Blocks{ + Build: true, + BuildBody: `=> { title: "Home" }`, + View: true, + ViewBody: `
{title}
`, + Fragments: []gwdkir.FragmentEndpoint{{ + Name: "List", + Method: "GET", + Route: "/home/list", + Target: "#list", + Body: "
List
", + }}, + Spans: gwdkir.BlockSpans{ + Fragments: []source.NamedSpan{{Name: "List"}}, + }, + }, + }}, + Components: []gwdkir.Component{{ + Source: "components/counter.cmp.gwdk", + Package: "components", + Name: "Counter", + Props: []gwdkir.Prop{{Name: "label", Type: "string"}}, + }}, + Layouts: []gwdkir.Layout{{ + Source: "pages/root.layout.gwdk", + Package: "pages", + ID: "root", + Blocks: gwdkir.Blocks{ + View: true, + ViewBody: ``, + }, + }}, + Endpoints: []gwdkir.Endpoint{{ + Kind: gwdkir.EndpointAction, + PageID: "home", + SourceFile: "pages/home.page.gwdk", + Symbol: "Subscribe", + Method: "POST", + Path: "/subscribe", + Binding: gwdkir.Binding{ + ImportPath: "example.com/app/handlers", + PackageName: "handlers", + FunctionName: "Subscribe", + Status: source.BackendBindingBound, + }, + }}, + } +} + +// TestManifestFromIRReconstructsCoreRecords pins the converter's output shape so +// later validator-migration steps can rely on it as the IR->manifest seam. +func TestManifestFromIRReconstructsCoreRecords(t *testing.T) { + app := ManifestFromIR(sampleProgram()) + + if len(app.Pages) != 1 || app.Pages[0].ID != "home" || app.Pages[0].Route != "/" { + t.Fatalf("unexpected pages: %#v", app.Pages) + } + if len(app.Pages[0].Blocks.Fragments) != 1 || app.Pages[0].Blocks.Fragments[0].Name != "List" { + t.Fatalf("fragment endpoints not preserved: %#v", app.Pages[0].Blocks.Fragments) + } + if len(app.Components) != 1 || app.Components[0].Name != "Counter" { + t.Fatalf("unexpected components: %#v", app.Components) + } + if len(app.Layouts) != 1 || app.Layouts[0].ID != "root" { + t.Fatalf("unexpected layouts: %#v", app.Layouts) + } + if len(app.BackendBindings) != 1 || app.BackendBindings[0].FunctionName != "Subscribe" { + t.Fatalf("unexpected backend bindings: %#v", app.BackendBindings) + } + if app.BackendBindings[0].Kind != "action" || app.BackendBindings[0].Status != source.BackendBindingBound { + t.Fatalf("unexpected binding kind/status: %#v", app.BackendBindings[0]) + } +} + +// TestValidateProgramMatchesManifestValidation is the equivalence guard for the +// IR-first validation path: ValidateProgram(ir) must produce the exact same +// result as running the manifest validator on the reconstructed manifest. While +// individual validators still read manifest, this is a tautology by +// construction; the test exists to catch a future change that makes +// ValidateProgram diverge from "validate the manifest produced from this IR". +func TestValidateProgramMatchesManifestValidation(t *testing.T) { + config := gowdk.Config{} + ir := sampleProgram() + + viaProgram := ValidateProgram(config, ir) + viaManifest := ValidateManifest(config, ManifestFromIR(ir)) + + if errString(viaProgram) != errString(viaManifest) { + t.Fatalf("ValidateProgram diverged from manifest validation:\nprogram: %v\nmanifest: %v", viaProgram, viaManifest) + } + if viaProgram != nil { + t.Fatalf("expected sample program to validate cleanly, got: %v", viaProgram) + } +} + +// TestValidateProgramReportsInvalidRoutes confirms the IR-first path still +// surfaces validation failures (here, a duplicate page identity). +func TestValidateProgramReportsInvalidRoutes(t *testing.T) { + config := gowdk.Config{} + ir := sampleProgram() + ir.Pages = append(ir.Pages, ir.Pages[0]) // duplicate page id "home" + + if err := ValidateProgram(config, ir); err == nil { + t.Fatal("expected duplicate page identity to fail IR-first validation") + } +} + +// TestValidateBackendBindingPolicyIRMatchesManifest is the equivalence guard for +// the production backend-binding policy on the IR-first path. +func TestValidateBackendBindingPolicyIRMatchesManifest(t *testing.T) { + config := gowdk.Config{Build: gowdk.BuildConfig{Mode: gowdk.Production}} + ir := sampleProgram() + // Force an unbound endpoint so the production policy has something to reject. + ir.Endpoints[0].Binding.Status = source.BackendBindingMissing + + viaIR := ValidateBackendBindingPolicyIR(config, ir) + viaManifest := ValidateBackendBindingPolicy(config, ManifestFromIR(ir)) + + if errString(viaIR) != errString(viaManifest) { + t.Fatalf("policy IR path diverged:\nir: %v\nmanifest: %v", viaIR, viaManifest) + } + if viaIR == nil { + t.Fatal("expected production policy to reject a missing backend binding") + } +} + +func errString(err error) string { + if err == nil { + return "" + } + return err.Error() +} diff --git a/internal/compiler/route_bindings.go b/internal/compiler/route_bindings.go index 467b77e..7347020 100644 --- a/internal/compiler/route_bindings.go +++ b/internal/compiler/route_bindings.go @@ -9,6 +9,7 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkanalysis" "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) // RouteKind describes route behavior in the CLI routes report. @@ -57,7 +58,7 @@ type EndpointBinding struct { Kind EndpointKind EndpointSource string Source string - SourceSpan manifest.SourceSpan + SourceSpan source.SourceSpan Package string PackagePath string PackageName string @@ -66,12 +67,12 @@ type EndpointBinding struct { Route string PageID string Handler string - BindingStatus manifest.BackendBindingStatus + BindingStatus source.BackendBindingStatus BindingMessage string BindingImportPath string BindingPackage string BindingFunction string - BindingSignature manifest.BackendSignatureKind + BindingSignature source.BackendSignatureKind BindingInputType string Contract ContractEndpointBinding } diff --git a/internal/compiler/route_bindings_test.go b/internal/compiler/route_bindings_test.go index 0cc8006..eb1d4e3 100644 --- a/internal/compiler/route_bindings_test.go +++ b/internal/compiler/route_bindings_test.go @@ -7,6 +7,7 @@ import ( "github.com/cssbruno/gowdk/addons/ssr" "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func TestBuildRouteMetadataSeparatesRoutesFromEndpoints(t *testing.T) { @@ -143,11 +144,11 @@ func TestBuildRouteMetadataFromIR(t *testing.T) { Path: "/newsletter", SourceFile: "newsletter.page.gwdk", Binding: gwdkir.Binding{ - Status: manifest.BackendBindingBound, + Status: source.BackendBindingBound, ImportPath: "example.com/app/newsletter", PackageName: "newsletter", FunctionName: "Subscribe", - Signature: manifest.BackendSignatureAction0, + Signature: source.BackendSignatureAction0, }, }, { @@ -184,7 +185,7 @@ func TestBuildRouteMetadataFromIR(t *testing.T) { assertEndpoint(t, metadata.Endpoints, EndpointAction, "POST", "/newsletter", "actions.NewsletterSubscribe") assertEndpoint(t, metadata.Endpoints, EndpointFragment, "GET", "/newsletter/list", "fragments.NewsletterList") assertEndpoint(t, metadata.Endpoints, EndpointCommand, "POST", "/patients", "contracts.command.patients.CreatePatient") - if metadata.Endpoints[0].BindingStatus != manifest.BackendBindingBound { + if metadata.Endpoints[0].BindingStatus != source.BackendBindingBound { t.Fatalf("expected binding status from IR, got %#v", metadata.Endpoints[0]) } command := findEndpoint(t, metadata.Endpoints, EndpointCommand, "POST", "/patients") diff --git a/internal/compiler/routes.go b/internal/compiler/routes.go index 2a00c37..b7d0132 100644 --- a/internal/compiler/routes.go +++ b/internal/compiler/routes.go @@ -6,6 +6,7 @@ import ( "unicode" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func validateUniquePageRoutes(pages []manifest.Page) []ValidationError { @@ -162,7 +163,7 @@ type routeRegistration struct { Pattern string PageID string Source string - Span manifest.SourceSpan + Span source.SourceSpan } func routeRegistrations(pages []manifest.Page, endpoints []manifest.EndpointDeclaration) []routeRegistration { @@ -325,7 +326,7 @@ func standaloneEndpointPageID(endpoint manifest.EndpointDeclaration) string { return endpoint.Package + "." + endpoint.Name } -func routeDiagnostics(page manifest.Page, label string, issues []routeIssue, routeSpan manifest.SourceSpan, paramSpans []manifest.NamedSpan) []ValidationError { +func routeDiagnostics(page manifest.Page, label string, issues []routeIssue, routeSpan source.SourceSpan, paramSpans []source.NamedSpan) []ValidationError { if len(issues) == 0 { return nil } @@ -359,7 +360,7 @@ type routeIssue struct { ParamOccurrence int } -func routeIssueSpan(issue routeIssue, routeSpan manifest.SourceSpan, paramSpans []manifest.NamedSpan) manifest.SourceSpan { +func routeIssueSpan(issue routeIssue, routeSpan source.SourceSpan, paramSpans []source.NamedSpan) source.SourceSpan { if issue.Param != "" { if issue.ParamOccurrence > 1 { return spanForNameOccurrence(paramSpans, issue.Param, issue.ParamOccurrence, routeSpan) diff --git a/internal/compiler/validate.go b/internal/compiler/validate.go index 0b8e30f..a7bbecb 100644 --- a/internal/compiler/validate.go +++ b/internal/compiler/validate.go @@ -3,7 +3,9 @@ package compiler import ( "fmt" "github.com/cssbruno/gowdk" + "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) type ValidationError struct { @@ -11,7 +13,7 @@ type ValidationError struct { PageID string ComponentName string Source string - Span manifest.SourceSpan + Span source.SourceSpan Message string } @@ -52,3 +54,12 @@ func ValidateManifest(config gowdk.Config, app manifest.Manifest) error { } return ValidationErrors(diagnostics) } + +// ValidateProgram checks the same render-mode invariants as ValidateManifest +// against an IR-first build path. It reconstructs the manifest from IR via +// ManifestFromIR so generated-output packages no longer need to carry their own +// IR->manifest converter. As validators move to read IR directly, the converter +// shrinks until this entrypoint reads IR with no manifest intermediary. +func ValidateProgram(config gowdk.Config, ir gwdkir.Program) error { + return ValidateManifest(config, ManifestFromIR(ir)) +} diff --git a/internal/compiler/validate_component_client.go b/internal/compiler/validate_component_client.go index de7bc6d..999dcb5 100644 --- a/internal/compiler/validate_component_client.go +++ b/internal/compiler/validate_component_client.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/cssbruno/gowdk/internal/clientlang" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/internal/view" "strings" ) @@ -246,7 +247,7 @@ func componentEmitMap(component manifest.Component) map[string]clientlang.Emit { return out } -func clientStatementErrorSpan(component manifest.Component, statements []string, spans []clientlang.Span, err error) manifest.SourceSpan { +func clientStatementErrorSpan(component manifest.Component, statements []string, spans []clientlang.Span, err error) source.SourceSpan { var statementErr view.StatementValidationError if errors.As(err, &statementErr) && statementErr.Index >= 0 && statementErr.Index < len(spans) { if statementErr.Index < len(statements) { @@ -257,7 +258,7 @@ func clientStatementErrorSpan(component manifest.Component, statements []string, return firstSpan(component.Blocks.Spans.Client, component.Span) } -func clientParseErrorSpan(component manifest.Component, err error) manifest.SourceSpan { +func clientParseErrorSpan(component manifest.Component, err error) source.SourceSpan { var parseErr *clientlang.ParseError if errors.As(err, &parseErr) && parseErr.Line > 0 { return clientSpan(component, clientlang.Span{StartLine: parseErr.Line, EndLine: parseErr.Line}) @@ -265,7 +266,7 @@ func clientParseErrorSpan(component manifest.Component, err error) manifest.Sour return firstSpan(component.Blocks.Spans.Client, component.Span) } -func clientExpressionErrorSpan(component manifest.Component, statement string, span clientlang.Span, err error) manifest.SourceSpan { +func clientExpressionErrorSpan(component manifest.Component, statement string, span clientlang.Span, err error) source.SourceSpan { var exprErr clientlang.ExprValidationError if !errors.As(err, &exprErr) || exprErr.Span.StartColumn <= 0 { return clientSpan(component, span) @@ -299,11 +300,11 @@ func functionReturnSpan(function clientlang.Function) clientlang.Span { return function.StatementSpans[len(function.StatementSpans)-1] } -func clientSpan(component manifest.Component, span clientlang.Span) manifest.SourceSpan { +func clientSpan(component manifest.Component, span clientlang.Span) source.SourceSpan { return clientSpanColumns(component, span, 1, 2) } -func clientSpanColumns(component manifest.Component, span clientlang.Span, startColumn, endColumn int) manifest.SourceSpan { +func clientSpanColumns(component manifest.Component, span clientlang.Span, startColumn, endColumn int) source.SourceSpan { if span.StartLine <= 0 { return firstSpan(component.Blocks.Spans.Client, component.Span) } @@ -322,9 +323,9 @@ func clientSpanColumns(component manifest.Component, span clientlang.Span, start if endColumn <= startColumn { endColumn = startColumn + 1 } - return manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: startLine, Column: startColumn}, - End: manifest.SourcePosition{Line: endLine, Column: endColumn}, + return source.SourceSpan{ + Start: source.SourcePosition{Line: startLine, Column: startColumn}, + End: source.SourcePosition{Line: endLine, Column: endColumn}, } } diff --git a/internal/compiler/validate_component_contracts.go b/internal/compiler/validate_component_contracts.go index 4e7cc21..5c1cbe3 100644 --- a/internal/compiler/validate_component_contracts.go +++ b/internal/compiler/validate_component_contracts.go @@ -6,6 +6,7 @@ import ( "github.com/cssbruno/gowdk/internal/clientlang" "github.com/cssbruno/gowdk/internal/gotypes" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "strings" ) @@ -166,7 +167,7 @@ func validateComponentImports(component manifest.Component) []ValidationError { return diagnostics } -func componentContractDiagnostic(component manifest.Component, code string, span manifest.SourceSpan, err error) ValidationError { +func componentContractDiagnostic(component manifest.Component, code string, span source.SourceSpan, err error) ValidationError { return ValidationError{ Code: code, ComponentName: component.Name, @@ -176,13 +177,13 @@ func componentContractDiagnostic(component manifest.Component, code string, span } } -func importSource(source string, item manifest.Import) string { - if source == "" { +func importSource(sourcePath string, item manifest.Import) string { + if sourcePath == "" { return "" } name := item.Alias if strings.TrimSpace(name) == "" { name = item.Path } - return fmt.Sprintf("%s import %s", source, name) + return fmt.Sprintf("%s import %s", sourcePath, name) } diff --git a/internal/compiler/validate_component_lists.go b/internal/compiler/validate_component_lists.go index 4d5e9d5..0e6ac0d 100644 --- a/internal/compiler/validate_component_lists.go +++ b/internal/compiler/validate_component_lists.go @@ -6,6 +6,7 @@ import ( "github.com/cssbruno/gowdk/internal/clientlang" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/internal/view" ) @@ -34,7 +35,7 @@ func validateComponentListDirectives(component manifest.Component, symbols map[s type spannedMessage struct { Message string - Span manifest.SourceSpan + Span source.SourceSpan } func validateListNodes(nodes []view.Node, component manifest.Component, symbols map[string]clientlang.ValueType, stateTypes map[string]clientlang.ValueType, handlers map[string]clientlang.Handler, helpers map[string]clientlang.ExprFunction, messages *[]spannedMessage) { diff --git a/internal/compiler/validate_contract_refs_test.go b/internal/compiler/validate_contract_refs_test.go index edd06e8..89b402b 100644 --- a/internal/compiler/validate_contract_refs_test.go +++ b/internal/compiler/validate_contract_refs_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/cssbruno/gowdk/internal/gwdkir" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func TestValidateContractReferencesRejectsBoundNonWebRole(t *testing.T) { @@ -17,8 +17,8 @@ func TestValidateContractReferencesRejectsBoundNonWebRole(t *testing.T) { OwnerKind: gwdkir.SourcePage, OwnerID: "patients", Source: "patients.page.gwdk", - Span: manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: 8, Column: 42}, + Span: source.SourceSpan{ + Start: source.SourcePosition{Line: 8, Column: 42}, }, }}) if err == nil { diff --git a/internal/compiler/validate_packages.go b/internal/compiler/validate_packages.go index a25d850..339cf68 100644 --- a/internal/compiler/validate_packages.go +++ b/internal/compiler/validate_packages.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) type packageDeclaration struct { @@ -29,7 +30,7 @@ type packageDeclaration struct { Package string Imports []manifest.Import GoBlocks []manifest.GoBlock - Span manifest.SourceSpan + Span source.SourceSpan } type goPackageInfo struct { @@ -91,12 +92,12 @@ func validatePackages(app manifest.Manifest) []ValidationError { return diagnostics } -func shouldValidatePackageSource(source string) bool { - source = strings.TrimSpace(source) - if source == "" { +func shouldValidatePackageSource(sourcePath string) bool { + sourcePath = strings.TrimSpace(sourcePath) + if sourcePath == "" { return false } - if _, err := os.Stat(source); err == nil { + if _, err := os.Stat(sourcePath); err == nil { return true } return false @@ -256,11 +257,11 @@ func inspectGoPackageForValidation(dir string, group []packageDeclaration) goPac } func parseGoBlockPackageFileForValidation(fileSet *token.FileSet, declaration packageDeclaration, block manifest.GoBlock) (*ast.File, *ValidationError) { - source, err := goBlockPackageSourceForValidation(declaration, block) + src, err := goBlockPackageSourceForValidation(declaration, block) if err != nil { return nil, nil } - file, parseErr := parser.ParseFile(fileSet, declaration.Source, source, parser.AllErrors|parser.ParseComments) + file, parseErr := parser.ParseFile(fileSet, declaration.Source, src, parser.AllErrors|parser.ParseComments) if parseErr != nil { diagnostic := ValidationError{ Code: "invalid_go_block", @@ -355,13 +356,13 @@ func goBlockImportSpecSortKey(spec ast.Spec) string { return alias + "\x00" + path } -func addGoBlockLineDirectiveForValidation(fileSet *token.FileSet, file *ast.File, source string, line int) *ast.CommentGroup { +func addGoBlockLineDirectiveForValidation(fileSet *token.FileSet, file *ast.File, sourcePath string, line int) *ast.CommentGroup { if len(file.Decls) == 0 { return nil } directive := &ast.Comment{ Slash: goBlockLineDirectivePosition(fileSet, file.Decls[0].Pos()), - Text: "//line " + source + ":" + strconv.Itoa(line), + Text: "//line " + sourcePath + ":" + strconv.Itoa(line), } group := &ast.CommentGroup{List: []*ast.Comment{directive}} switch decl := file.Decls[0].(type) { @@ -555,9 +556,9 @@ func goTypeCheckDiagnostic(fileSet *token.FileSet, err error) ValidationError { position := fileSet.PositionFor(typeError.Pos, true) if position.IsValid() { diagnostic.Source = position.Filename - diagnostic.Span = manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: position.Line, Column: position.Column}, - End: manifest.SourcePosition{Line: position.Line, Column: position.Column + 1}, + diagnostic.Span = source.SourceSpan{ + Start: source.SourcePosition{Line: position.Line, Column: position.Column}, + End: source.SourcePosition{Line: position.Line, Column: position.Column + 1}, } } return diagnostic @@ -570,8 +571,8 @@ func isGoPackageSource(name string) bool { return strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") } -func packageSourceDir(source string) string { - dir := filepath.Dir(source) +func packageSourceDir(sourcePath string) string { + dir := filepath.Dir(sourcePath) abs, err := filepath.Abs(dir) if err != nil { return dir @@ -579,9 +580,9 @@ func packageSourceDir(source string) string { return abs } -func sourceLabel(source string, fallback string) string { - if strings.TrimSpace(source) == "" { +func sourceLabel(sourcePath string, fallback string) string { + if strings.TrimSpace(sourcePath) == "" { return fallback } - return filepath.Base(source) + return filepath.Base(sourcePath) } diff --git a/internal/compiler/validate_page.go b/internal/compiler/validate_page.go index a64eb87..8fe921e 100644 --- a/internal/compiler/validate_page.go +++ b/internal/compiler/validate_page.go @@ -8,6 +8,7 @@ import ( "github.com/cssbruno/gowdk" "github.com/cssbruno/gowdk/internal/gotypes" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/runtime/auth" ) @@ -240,13 +241,13 @@ func hasProtectedPageGuard(page manifest.Page) bool { return false } -func firstGoBlockSpan(page manifest.Page, target string) manifest.SourceSpan { +func firstGoBlockSpan(page manifest.Page, target string) source.SourceSpan { for _, block := range page.Blocks.GoBlocks { if block.Target == target { return block.Span } } - return manifest.SourceSpan{} + return source.SourceSpan{} } func requiresSSRFeature(mode gowdk.RenderMode, page manifest.Page) bool { diff --git a/internal/compiler/validate_scripts.go b/internal/compiler/validate_scripts.go index 1745657..b926fa4 100644 --- a/internal/compiler/validate_scripts.go +++ b/internal/compiler/validate_scripts.go @@ -8,6 +8,7 @@ import ( "github.com/cssbruno/gowdk" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func validateGoBlocks(config gowdk.Config, app manifest.Manifest) []ValidationError { @@ -35,7 +36,7 @@ func validateGoBlocks(config gowdk.Config, app manifest.Manifest) []ValidationEr return diagnostics } -func validateGoBlockSyntax(packageName string, source string, pageID string, componentName string, block manifest.GoBlock) []ValidationError { +func validateGoBlockSyntax(packageName string, sourcePath string, pageID string, componentName string, block manifest.GoBlock) []ValidationError { if strings.TrimSpace(block.Body) == "" { return nil } @@ -50,13 +51,13 @@ func validateGoBlockSyntax(packageName string, source string, pageID string, com Code: "invalid_go_block", PageID: pageID, ComponentName: componentName, - Source: source, + Source: sourcePath, Span: block.Span, Message: fmt.Sprintf("go %s contains invalid Go: %v", goBlockLabel(block.Target), err), }} } -func validateGoBlockTarget(config gowdk.Config, enabledAddons map[string]gowdk.Addon, pageID string, componentName string, source string, packageName string, mode gowdk.RenderMode, hasLoad bool, block manifest.GoBlock) []ValidationError { +func validateGoBlockTarget(config gowdk.Config, enabledAddons map[string]gowdk.Addon, pageID string, componentName string, sourcePath string, packageName string, mode gowdk.RenderMode, hasLoad bool, block manifest.GoBlock) []ValidationError { target := strings.TrimSpace(block.Target) switch { case target == "" || target == "client": @@ -69,25 +70,25 @@ func validateGoBlockTarget(config gowdk.Config, enabledAddons map[string]gowdk.A Code: "go_ssr_requires_request_render", PageID: pageID, ComponentName: componentName, - Source: source, + Source: sourcePath, Span: block.Span, Message: fmt.Sprintf("%s declares go ssr {}, but ssr Go code is request-time behavior and requires the SSR addon", pageID), }} case strings.HasPrefix(target, "addon."): - return validateAddonGoBlockTarget(enabledAddons, pageID, componentName, source, packageName, mode, block) + return validateAddonGoBlockTarget(enabledAddons, pageID, componentName, sourcePath, packageName, mode, block) default: return []ValidationError{{ Code: "unknown_go_block_target", PageID: pageID, ComponentName: componentName, - Source: source, + Source: sourcePath, Span: block.Span, Message: fmt.Sprintf("unknown go block target %q; use go {}, go client {}, go ssr {}, or go addon. {}", target), }} } } -func validateNonPageGoBlockTarget(config gowdk.Config, enabledAddons map[string]gowdk.Addon, pageID string, componentName string, source string, packageName string, block manifest.GoBlock) []ValidationError { +func validateNonPageGoBlockTarget(config gowdk.Config, enabledAddons map[string]gowdk.Addon, pageID string, componentName string, sourcePath string, packageName string, block manifest.GoBlock) []ValidationError { target := strings.TrimSpace(block.Target) switch { case target == "": @@ -97,7 +98,7 @@ func validateNonPageGoBlockTarget(config gowdk.Config, enabledAddons map[string] Code: "go_client_requires_page", PageID: pageID, ComponentName: componentName, - Source: source, + Source: sourcePath, Span: block.Span, Message: "go client {} is page-level client-side behavior; use a page go client {} block or a component @wasm package", }} @@ -109,25 +110,25 @@ func validateNonPageGoBlockTarget(config gowdk.Config, enabledAddons map[string] Code: "missing_ssr_addon", PageID: pageID, ComponentName: componentName, - Source: source, + Source: sourcePath, Span: block.Span, Message: "go ssr {} requires the SSR addon before request-time Go code can be validated or generated", }} case strings.HasPrefix(target, "addon."): - return validateAddonGoBlockTarget(enabledAddons, pageID, componentName, source, packageName, gowdk.SPA, block) + return validateAddonGoBlockTarget(enabledAddons, pageID, componentName, sourcePath, packageName, gowdk.SPA, block) default: return []ValidationError{{ Code: "unknown_go_block_target", PageID: pageID, ComponentName: componentName, - Source: source, + Source: sourcePath, Span: block.Span, Message: fmt.Sprintf("unknown go block target %q; use go {}, go client {}, go ssr {}, or go addon. {}", target), }} } } -func validateAddonGoBlockTarget(enabledAddons map[string]gowdk.Addon, pageID string, componentName string, source string, packageName string, render gowdk.RenderMode, block manifest.GoBlock) []ValidationError { +func validateAddonGoBlockTarget(enabledAddons map[string]gowdk.Addon, pageID string, componentName string, sourcePath string, packageName string, render gowdk.RenderMode, block manifest.GoBlock) []ValidationError { name := strings.TrimPrefix(strings.TrimSpace(block.Target), "addon.") addon, ok := enabledAddons[name] if name != "" && ok { @@ -140,18 +141,18 @@ func validateAddonGoBlockTarget(enabledAddons map[string]gowdk.Addon, pageID str Code: "unsupported_addon_go_block_target", PageID: pageID, ComponentName: componentName, - Source: source, + Source: sourcePath, Span: block.Span, Message: fmt.Sprintf("addon %q does not consume go block target %q", name, block.Target), }} } - return addonGoBlockDiagnostics(consumer, pageID, componentName, source, packageName, render, block) + return addonGoBlockDiagnostics(consumer, pageID, componentName, sourcePath, packageName, render, block) } return []ValidationError{{ Code: "unknown_addon_go_block_target", PageID: pageID, ComponentName: componentName, - Source: source, + Source: sourcePath, Span: block.Span, Message: fmt.Sprintf("go addon.%s {} requires an enabled addon named %q", name, name), }} @@ -166,8 +167,8 @@ func goBlockConsumerSupportsTarget(consumer gowdk.GoBlockConsumer, target string return false } -func addonGoBlockDiagnostics(consumer gowdk.GoBlockConsumer, pageID string, componentName string, source string, packageName string, render gowdk.RenderMode, block manifest.GoBlock) []ValidationError { - target := gowdkGoBlockTarget(pageID, componentName, source, packageName, block) +func addonGoBlockDiagnostics(consumer gowdk.GoBlockConsumer, pageID string, componentName string, sourcePath string, packageName string, render gowdk.RenderMode, block manifest.GoBlock) []ValidationError { + target := gowdkGoBlockTarget(pageID, componentName, sourcePath, packageName, block) context := gowdk.GoBlockContext{Render: render} var diagnostics []ValidationError for _, diagnostic := range consumer.ValidateGoBlock(target, context) { @@ -183,7 +184,7 @@ func addonGoBlockDiagnostics(consumer gowdk.GoBlockConsumer, pageID string, comp Code: code, PageID: pageID, ComponentName: componentName, - Source: source, + Source: sourcePath, Span: span, Message: diagnostic.Message, }) @@ -199,7 +200,7 @@ func addonsByName(config gowdk.Config) map[string]gowdk.Addon { return names } -func gowdkGoBlockTarget(pageID string, componentName string, source string, packageName string, block manifest.GoBlock) gowdk.GoBlockTarget { +func gowdkGoBlockTarget(pageID string, componentName string, sourcePath string, packageName string, block manifest.GoBlock) gowdk.GoBlockTarget { ownerKind := "layout" ownerID := "" if pageID != "" { @@ -214,23 +215,23 @@ func gowdkGoBlockTarget(pageID string, componentName string, source string, pack OwnerKind: ownerKind, OwnerID: ownerID, OwnerPackage: packageName, - SourcePath: source, + SourcePath: sourcePath, Body: block.Body, Span: gowdkSpan(block.Span), } } -func gowdkSpan(span manifest.SourceSpan) gowdk.SourceSpan { +func gowdkSpan(span source.SourceSpan) gowdk.SourceSpan { return gowdk.SourceSpan{ Start: gowdk.SourcePosition{Line: span.Start.Line, Column: span.Start.Column}, End: gowdk.SourcePosition{Line: span.End.Line, Column: span.End.Column}, } } -func manifestSpan(span gowdk.SourceSpan) manifest.SourceSpan { - return manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: span.Start.Line, Column: span.Start.Column}, - End: manifest.SourcePosition{Line: span.End.Line, Column: span.End.Column}, +func manifestSpan(span gowdk.SourceSpan) source.SourceSpan { + return source.SourceSpan{ + Start: source.SourcePosition{Line: span.Start.Line, Column: span.Start.Column}, + End: source.SourcePosition{Line: span.End.Line, Column: span.End.Column}, } } diff --git a/internal/compiler/validate_spans.go b/internal/compiler/validate_spans.go index c3cdda7..db31c75 100644 --- a/internal/compiler/validate_spans.go +++ b/internal/compiler/validate_spans.go @@ -2,19 +2,20 @@ package compiler import ( "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "strings" ) -func firstSpan(spans ...manifest.SourceSpan) manifest.SourceSpan { +func firstSpan(spans ...source.SourceSpan) source.SourceSpan { for _, span := range spans { if hasSpan(span) { return span } } - return manifest.SourceSpan{} + return source.SourceSpan{} } -func viewBodyNeedleSpan(component manifest.Component, needle string) manifest.SourceSpan { +func viewBodyNeedleSpan(component manifest.Component, needle string) source.SourceSpan { needle = strings.TrimSpace(needle) if needle == "" || component.Blocks.ViewBody == "" || !hasSpan(component.Blocks.Spans.View) { return firstSpan(component.Blocks.Spans.View, component.Span) @@ -32,19 +33,19 @@ func viewBodyNeedleSpan(component manifest.Component, needle string) manifest.So } startColumn := len([]rune(before[lineStart:])) + 1 endColumn := startColumn + len([]rune(needle)) - return manifest.SourceSpan{ - Start: manifest.SourcePosition{ + return source.SourceSpan{ + Start: source.SourcePosition{ Line: component.Blocks.Spans.View.Start.Line + 1 + lineOffset, Column: startColumn, }, - End: manifest.SourcePosition{ + End: source.SourcePosition{ Line: component.Blocks.Spans.View.Start.Line + 1 + lineOffset, Column: endColumn, }, } } -func firstNamedSpan(spans []manifest.NamedSpan, fallback manifest.SourceSpan) manifest.SourceSpan { +func firstNamedSpan(spans []source.NamedSpan, fallback source.SourceSpan) source.SourceSpan { for _, item := range spans { if hasSpan(item.Span) { return item.Span @@ -53,7 +54,7 @@ func firstNamedSpan(spans []manifest.NamedSpan, fallback manifest.SourceSpan) ma return fallback } -func spanForName(spans []manifest.NamedSpan, name string, fallback manifest.SourceSpan) manifest.SourceSpan { +func spanForName(spans []source.NamedSpan, name string, fallback source.SourceSpan) source.SourceSpan { for _, item := range spans { if item.Name == name && hasSpan(item.Span) { return item.Span @@ -62,7 +63,7 @@ func spanForName(spans []manifest.NamedSpan, name string, fallback manifest.Sour return fallback } -func spanForNameOccurrence(spans []manifest.NamedSpan, name string, occurrence int, fallback manifest.SourceSpan) manifest.SourceSpan { +func spanForNameOccurrence(spans []source.NamedSpan, name string, occurrence int, fallback source.SourceSpan) source.SourceSpan { if occurrence <= 1 { return spanForName(spans, name, fallback) } @@ -82,6 +83,6 @@ func spanForNameOccurrence(spans []manifest.NamedSpan, name string, occurrence i return spanForName(spans, name, fallback) } -func hasSpan(span manifest.SourceSpan) bool { +func hasSpan(span source.SourceSpan) bool { return span.Start.Line > 0 && span.Start.Column > 0 && span.End.Line > 0 && span.End.Column > 0 } diff --git a/internal/compiler/validate_test.go b/internal/compiler/validate_test.go index 063e925..963e523 100644 --- a/internal/compiler/validate_test.go +++ b/internal/compiler/validate_test.go @@ -13,16 +13,17 @@ import ( "github.com/cssbruno/gowdk" "github.com/cssbruno/gowdk/addons/ssr" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func TestValidateManifestRejectsMissingPackageDeclaration(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") - if err := os.WriteFile(source, []byte(""), 0o644); err != nil { + sourcePath := filepath.Join(root, "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte(""), 0o644); err != nil { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, ID: "home", Route: "/", Blocks: manifest.Blocks{View: true}, @@ -40,8 +41,8 @@ func TestValidateManifestRejectsMissingPackageDeclaration(t *testing.T) { func TestValidateManifestRejectsPackageMismatchWithSiblingGoFile(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") - if err := os.WriteFile(source, []byte("package views\n"), 0o644); err != nil { + sourcePath := filepath.Join(root, "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package views\n"), 0o644); err != nil { t.Fatal(err) } goFile := filepath.Join(root, "handlers.go") @@ -49,7 +50,7 @@ func TestValidateManifestRejectsPackageMismatchWithSiblingGoFile(t *testing.T) { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, Package: "views", ID: "home", Route: "/", @@ -68,8 +69,8 @@ func TestValidateManifestRejectsPackageMismatchWithSiblingGoFile(t *testing.T) { func TestValidateManifestAcceptsPackageMatchingSiblingGoFile(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(root, "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } goFile := filepath.Join(root, "handlers.go") @@ -77,7 +78,7 @@ func TestValidateManifestAcceptsPackageMatchingSiblingGoFile(t *testing.T) { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, Package: "app", ID: "home", Route: "/", @@ -92,8 +93,8 @@ func TestValidateManifestAcceptsPackageMatchingSiblingGoFile(t *testing.T) { func TestValidateManifestIgnoresProjectConfigGoPackage(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "styled.page.gwdk") - if err := os.WriteFile(source, []byte("package css\n"), 0o644); err != nil { + sourcePath := filepath.Join(root, "styled.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package css\n"), 0o644); err != nil { t.Fatal(err) } configFile := filepath.Join(root, "gowdk.config.go") @@ -101,7 +102,7 @@ func TestValidateManifestIgnoresProjectConfigGoPackage(t *testing.T) { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, Package: "css", ID: "styled", Route: "/styled", @@ -116,8 +117,8 @@ func TestValidateManifestIgnoresProjectConfigGoPackage(t *testing.T) { func TestValidateManifestReportsGoPackageParseErrors(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(root, "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } goFile := filepath.Join(root, "handlers.go") @@ -125,7 +126,7 @@ func TestValidateManifestReportsGoPackageParseErrors(t *testing.T) { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, Package: "app", ID: "home", Route: "/", @@ -144,8 +145,8 @@ func TestValidateManifestReportsGoPackageParseErrors(t *testing.T) { func TestValidateManifestReportsGoPackageTypeErrors(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(root, "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } goFile := filepath.Join(root, "handlers.go") @@ -153,7 +154,7 @@ func TestValidateManifestReportsGoPackageTypeErrors(t *testing.T) { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, Package: "app", ID: "home", Route: "/", @@ -182,8 +183,8 @@ func TestValidateManifestReportsGoPackageTypeErrors(t *testing.T) { func TestValidateManifestTypeChecksDefaultScriptWithSiblingGoFiles(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(root, "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } goFile := filepath.Join(root, "copy.go") @@ -196,7 +197,7 @@ type PageCopy struct { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, Package: "app", ID: "home", Route: "/", @@ -218,12 +219,12 @@ type PageCopy struct { func TestValidateManifestTypeChecksDefaultScriptWithGOWDKImports(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(root, "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, Package: "app", ID: "home", Route: "/", @@ -246,19 +247,19 @@ func TestValidateManifestTypeChecksDefaultScriptWithGOWDKImports(t *testing.T) { func TestValidateManifestReportsDefaultScriptTypeErrors(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(root, "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, Package: "app", ID: "home", Route: "/", Blocks: manifest.Blocks{ View: true, GoBlocks: []manifest.GoBlock{{ - Span: manifest.SourceSpan{Start: manifest.SourcePosition{Line: 8, Column: 1}}, + Span: source.SourceSpan{Start: source.SourcePosition{Line: 8, Column: 1}}, Body: `func BrokenCopy() string { return MissingTitle }`, @@ -275,8 +276,8 @@ func TestValidateManifestReportsDefaultScriptTypeErrors(t *testing.T) { if diagnostic == nil { t.Fatalf("missing Go package error diagnostic: %#v", diagnostics) } - if diagnostic.Source != source { - t.Fatalf("expected diagnostic source %s, got %#v", source, diagnostic) + if diagnostic.Source != sourcePath { + t.Fatalf("expected diagnostic source %s, got %#v", sourcePath, diagnostic) } if !strings.Contains(diagnostic.Message, "undefined: MissingTitle") { t.Fatalf("expected undefined inline go block symbol in diagnostic, got %q", diagnostic.Message) @@ -287,12 +288,12 @@ func TestValidateManifestReportsDefaultScriptTypeErrors(t *testing.T) { } func TestGoBlockPackageSourceForValidationPreservesLineDirective(t *testing.T) { - source := filepath.Join(t.TempDir(), "home.page.gwdk") + sourcePath := filepath.Join(t.TempDir(), "home.page.gwdk") payload, err := goBlockPackageSourceForValidation(packageDeclaration{ - Source: source, + Source: sourcePath, Package: "app", }, manifest.GoBlock{ - Span: manifest.SourceSpan{Start: manifest.SourcePosition{Line: 8, Column: 1}}, + Span: source.SourceSpan{Start: source.SourcePosition{Line: 8, Column: 1}}, Body: `func BrokenCopy() string { return MissingTitle }`, @@ -300,11 +301,11 @@ func TestGoBlockPackageSourceForValidationPreservesLineDirective(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.Contains(payload, "//line "+filepath.ToSlash(source)+":8") { + if !strings.Contains(payload, "//line "+filepath.ToSlash(sourcePath)+":8") { t.Fatalf("missing line directive in validation source:\n%s", payload) } fileSet := token.NewFileSet() - file, err := parser.ParseFile(fileSet, source, payload, parser.ParseComments|parser.AllErrors) + file, err := parser.ParseFile(fileSet, sourcePath, payload, parser.ParseComments|parser.AllErrors) if err != nil { t.Fatal(err) } @@ -317,12 +318,12 @@ func TestGoBlockPackageSourceForValidationPreservesLineDirective(t *testing.T) { func TestValidateManifestTypeChecksDefaultGoBlockAsStaticPackageGo(t *testing.T) { root := t.TempDir() - source := filepath.Join(root, "home.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(root, "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } page := manifest.Page{ - Source: source, + Source: sourcePath, Package: "app", ID: "home", Route: "/", @@ -633,14 +634,14 @@ func TestValidatePageRejectsSSRWithoutAddon(t *testing.T) { } func TestValidatePageRequiresExplicitGuard(t *testing.T) { - source := filepath.Join(t.TempDir(), "home.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(t.TempDir(), "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } page := manifest.Page{ ID: "home", Route: "/", - Source: source, + Source: sourcePath, Blocks: manifest.Blocks{View: true}, } @@ -651,14 +652,14 @@ func TestValidatePageRequiresExplicitGuard(t *testing.T) { } func TestValidatePageRequiresPublicGuardToBeExclusive(t *testing.T) { - source := filepath.Join(t.TempDir(), "home.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(t.TempDir(), "home.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } page := manifest.Page{ ID: "home", Route: "/", - Source: source, + Source: sourcePath, Guard: []string{"public", "auth.required"}, Blocks: manifest.Blocks{View: true}, } @@ -670,14 +671,14 @@ func TestValidatePageRequiresPublicGuardToBeExclusive(t *testing.T) { } func TestValidatePageRejectsProtectedGuardOnBuildTimePage(t *testing.T) { - source := filepath.Join(t.TempDir(), "dashboard.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(t.TempDir(), "dashboard.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } page := manifest.Page{ ID: "dashboard", Route: "/dashboard", - Source: source, + Source: sourcePath, Guard: []string{"auth.required"}, Render: gowdk.SPA, Blocks: manifest.Blocks{View: true}, @@ -690,14 +691,14 @@ func TestValidatePageRejectsProtectedGuardOnBuildTimePage(t *testing.T) { } func TestValidatePageAllowsProtectedGuardOnRequestTimePage(t *testing.T) { - source := filepath.Join(t.TempDir(), "dashboard.page.gwdk") - if err := os.WriteFile(source, []byte("package app\n"), 0o644); err != nil { + sourcePath := filepath.Join(t.TempDir(), "dashboard.page.gwdk") + if err := os.WriteFile(sourcePath, []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } page := manifest.Page{ ID: "dashboard", Route: "/dashboard", - Source: source, + Source: sourcePath, Guard: []string{"auth.required"}, Render: gowdk.SSR, Blocks: manifest.Blocks{View: true}, @@ -793,11 +794,11 @@ func TestValidateManifestRejectsDuplicatePageStore(t *testing.T) { Stores: []manifest.Store{ { Name: "cart", - Span: manifest.SourceSpan{Start: manifest.SourcePosition{Line: 5, Column: 1}, End: manifest.SourcePosition{Line: 5, Column: 40}}, + Span: source.SourceSpan{Start: source.SourcePosition{Line: 5, Column: 1}, End: source.SourcePosition{Line: 5, Column: 40}}, }, { Name: "cart", - Span: manifest.SourceSpan{Start: manifest.SourcePosition{Line: 6, Column: 1}, End: manifest.SourcePosition{Line: 6, Column: 40}}, + Span: source.SourceSpan{Start: source.SourcePosition{Line: 6, Column: 1}, End: source.SourcePosition{Line: 6, Column: 40}}, }, }, Blocks: manifest.Blocks{View: true, ViewBody: `
Cart
`}, @@ -828,7 +829,7 @@ func TestValidateManifestRejectsUnknownComponentStoreUse(t *testing.T) { Client: true, ClientBody: "use cart", Spans: manifest.BlockSpans{ - Client: manifest.SourceSpan{Start: manifest.SourcePosition{Line: 4, Column: 1}, End: manifest.SourcePosition{Line: 4, Column: 9}}, + Client: source.SourceSpan{Start: source.SourcePosition{Line: 4, Column: 1}, End: source.SourcePosition{Line: 4, Column: 9}}, }, View: true, ViewBody: ``, @@ -1372,16 +1373,16 @@ func TestValidateManifestRejectsDuplicateComponentEmitNames(t *testing.T) { Emits: []manifest.Emit{ { Name: "select", - Span: manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: 4, Column: 3}, - End: manifest.SourcePosition{Line: 4, Column: 16}, + Span: source.SourceSpan{ + Start: source.SourcePosition{Line: 4, Column: 3}, + End: source.SourcePosition{Line: 4, Column: 16}, }, }, { Name: "select", - Span: manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: 5, Column: 3}, - End: manifest.SourcePosition{Line: 5, Column: 20}, + Span: source.SourceSpan{ + Start: source.SourcePosition{Line: 5, Column: 3}, + End: source.SourcePosition{Line: 5, Column: 20}, }, }, }, @@ -1438,9 +1439,9 @@ func TestValidateManifestClientParseErrorPointsToClientLine(t *testing.T) { Client: true, ClientBody: "fn Bad() {\n if Count {\n }\n}", Spans: manifest.BlockSpans{ - Client: manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: 10, Column: 1}, - End: manifest.SourcePosition{Line: 14, Column: 1}, + Client: source.SourceSpan{ + Start: source.SourcePosition{Line: 10, Column: 1}, + End: source.SourcePosition{Line: 14, Column: 1}, }, }, View: true, @@ -2383,7 +2384,7 @@ func TestValidateManifestViewEventDiagnosticPointsToExpression(t *testing.T) { View: true, ViewBody: ``, Spans: manifest.BlockSpans{ - View: manifest.SourceSpan{Start: manifest.SourcePosition{Line: 9, Column: 1}, End: manifest.SourcePosition{Line: 9, Column: 7}}, + View: source.SourceSpan{Start: source.SourcePosition{Line: 9, Column: 1}, End: source.SourcePosition{Line: 9, Column: 7}}, }, }, }}} @@ -2407,7 +2408,7 @@ func TestValidateManifestUnknownViewFieldDiagnosticPointsToIdentifier(t *testing View: true, ViewBody: ``, Spans: manifest.BlockSpans{ - View: manifest.SourceSpan{Start: manifest.SourcePosition{Line: 4, Column: 1}, End: manifest.SourcePosition{Line: 4, Column: 7}}, + View: source.SourceSpan{Start: source.SourcePosition{Line: 4, Column: 1}, End: source.SourcePosition{Line: 4, Column: 7}}, }, }, }}} @@ -2431,7 +2432,7 @@ func TestValidateManifestBadGForDiagnosticPointsToDirectiveValue(t *testing.T) { View: true, ViewBody: `
  • {item.Name}
`, Spans: manifest.BlockSpans{ - View: manifest.SourceSpan{Start: manifest.SourcePosition{Line: 12, Column: 1}, End: manifest.SourcePosition{Line: 12, Column: 7}}, + View: source.SourceSpan{Start: source.SourcePosition{Line: 12, Column: 1}, End: source.SourcePosition{Line: 12, Column: 7}}, }, }, }}} @@ -3431,7 +3432,7 @@ func TestValidatePageRejectsDuplicateRouteParams(t *testing.T) { Blocks: manifest.Blocks{View: true}, Spans: manifest.PageSpans{ Route: testSourceSpan(3, 8, 3, 28), - RouteParams: []manifest.NamedSpan{ + RouteParams: []source.NamedSpan{ {Name: "slug", Span: testSourceSpan(3, 14, 3, 20)}, {Name: "slug", Span: testSourceSpan(3, 21, 3, 27)}, }, @@ -3461,7 +3462,7 @@ func TestValidatePageRouteDiagnosticsUseExactSpans(t *testing.T) { Route: "/save/{id:uuid}", Span: testSourceSpan(6, 1, 6, 32), RouteSpan: testSourceSpan(6, 17, 6, 34), - RouteParams: []manifest.NamedSpan{{Name: "id", Span: testSourceSpan(6, 24, 6, 33)}}, + RouteParams: []source.NamedSpan{{Name: "id", Span: testSourceSpan(6, 24, 6, 33)}}, }} diagnostics := ValidatePage(gowdk.Config{}, page) @@ -3480,7 +3481,7 @@ func TestValidatePageRouteDiagnosticsUseExactSpans(t *testing.T) { Route: "/api/{id:uuid}", Span: testSourceSpan(9, 1, 9, 31), RouteSpan: testSourceSpan(9, 16, 9, 31), - RouteParams: []manifest.NamedSpan{{Name: "id", Span: testSourceSpan(9, 21, 9, 30)}}, + RouteParams: []source.NamedSpan{{Name: "id", Span: testSourceSpan(9, 21, 9, 30)}}, }} diagnostics := ValidatePage(gowdk.Config{}, page) @@ -3499,7 +3500,7 @@ func TestValidatePageRouteDiagnosticsUseExactSpans(t *testing.T) { Route: "/preview/{id}", Span: testSourceSpan(12, 1, 12, 34), RouteSpan: testSourceSpan(12, 20, 12, 35), - RouteParams: []manifest.NamedSpan{{Name: "id", Span: testSourceSpan(12, 29, 12, 33)}}, + RouteParams: []source.NamedSpan{{Name: "id", Span: testSourceSpan(12, 29, 12, 33)}}, }} diagnostics := ValidatePage(gowdk.Config{}, page) @@ -3904,17 +3905,17 @@ func firstDiagnostic(diagnostics []ValidationError, code string) *ValidationErro return nil } -func assertSourceSpan(t *testing.T, span manifest.SourceSpan, startLine, startColumn, endLine, endColumn int) { +func assertSourceSpan(t *testing.T, span source.SourceSpan, startLine, startColumn, endLine, endColumn int) { t.Helper() if span.Start.Line != startLine || span.Start.Column != startColumn || span.End.Line != endLine || span.End.Column != endColumn { t.Fatalf("unexpected source span: got %#v, want %d:%d-%d:%d", span, startLine, startColumn, endLine, endColumn) } } -func testSourceSpan(startLine, startColumn, endLine, endColumn int) manifest.SourceSpan { - return manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: startLine, Column: startColumn}, - End: manifest.SourcePosition{Line: endLine, Column: endColumn}, +func testSourceSpan(startLine, startColumn, endLine, endColumn int) source.SourceSpan { + return source.SourceSpan{ + Start: source.SourcePosition{Line: startLine, Column: startColumn}, + End: source.SourcePosition{Line: endLine, Column: endColumn}, } } diff --git a/internal/contractscan/input_structs.go b/internal/contractscan/input_structs.go index 601d394..92029fc 100644 --- a/internal/contractscan/input_structs.go +++ b/internal/contractscan/input_structs.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func contractInputStruct(typeName string, structType *ast.StructType) inputStruct { @@ -14,7 +14,7 @@ func contractInputStruct(typeName string, structType *ast.StructType) inputStruc return inputStruct{} } seen := map[string]bool{} - var fields []manifest.BackendInputField + var fields []source.BackendInputField for _, field := range structType.Fields.List { if len(field.Names) == 0 { return inputStruct{Message: fmt.Sprintf("contract input %s cannot use embedded fields", typeName)} @@ -48,7 +48,7 @@ func contractInputStruct(typeName string, structType *ast.StructType) inputStruc return inputStruct{Message: fmt.Sprintf("contract input %s maps multiple fields to form field %q", typeName, nameFormName)} } seen[nameFormName] = true - fields = append(fields, manifest.BackendInputField{ + fields = append(fields, source.BackendInputField{ FieldName: name.Name, FormName: nameFormName, Type: fieldType, diff --git a/internal/contractscan/packages.go b/internal/contractscan/packages.go index 99329d9..5a25db9 100644 --- a/internal/contractscan/packages.go +++ b/internal/contractscan/packages.go @@ -11,7 +11,7 @@ import ( "strconv" "strings" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" runtimecontracts "github.com/cssbruno/gowdk/runtime/contracts" ) @@ -22,7 +22,7 @@ type fileScan struct { } type inputStruct struct { - Fields []manifest.BackendInputField + Fields []source.BackendInputField Message string } @@ -63,11 +63,11 @@ func parseScanPackages(fset *token.FileSet, root string, files []string) ([][]pa } func parseScanFile(fset *token.FileSet, root string, path string) (parsedGoFile, error) { - source, err := os.ReadFile(path) + src, err := os.ReadFile(path) if err != nil { return parsedGoFile{}, err } - file, err := parser.ParseFile(fset, path, source, 0) + file, err := parser.ParseFile(fset, path, src, 0) if err != nil { return parsedGoFile{}, err } @@ -183,7 +183,7 @@ func applyContractInputFields(contracts []Contract, structs map[string]inputStru if !ok || inputStruct.Message != "" { continue } - contracts[index].InputFields = append([]manifest.BackendInputField(nil), inputStruct.Fields...) + contracts[index].InputFields = append([]source.BackendInputField(nil), inputStruct.Fields...) } } diff --git a/internal/contractscan/references.go b/internal/contractscan/references.go index 2584ea6..38e8dea 100644 --- a/internal/contractscan/references.go +++ b/internal/contractscan/references.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/cssbruno/gowdk/internal/gwdkir" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" runtimecontracts "github.com/cssbruno/gowdk/runtime/contracts" ) @@ -62,7 +62,7 @@ func LinkReferences(refs []gwdkir.ContractReference, report Report) []gwdkir.Con } linked[index].Result = contract.Result linked[index].Roles = append([]string(nil), contract.Roles...) - linked[index].InputFields = append([]manifest.BackendInputField(nil), contract.InputFields...) + linked[index].InputFields = append([]source.BackendInputField(nil), contract.InputFields...) if diagnostic, bad := lookupContractDiagnostic(invalid[ref.Kind], ref); bad { linked[index].Status = gwdkir.ContractBindingInvalid linked[index].Message = diagnostic.Message diff --git a/internal/contractscan/scan.go b/internal/contractscan/scan.go index 6073ef6..9fb05e5 100644 --- a/internal/contractscan/scan.go +++ b/internal/contractscan/scan.go @@ -10,7 +10,7 @@ import ( "sort" "strings" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" runtimecontracts "github.com/cssbruno/gowdk/runtime/contracts" ) @@ -28,7 +28,7 @@ type Contract struct { ResultImportPath string `json:"resultImportPath,omitempty"` Handler string `json:"handler,omitempty"` Register string `json:"register,omitempty"` - InputFields []manifest.BackendInputField `json:"inputFields,omitempty"` + InputFields []source.BackendInputField `json:"inputFields,omitempty"` Emits []EventRef `json:"emits,omitempty"` Roles []string `json:"roles,omitempty"` Source string `json:"source"` diff --git a/internal/contractscan/scan_test.go b/internal/contractscan/scan_test.go index dc678d5..e2fe71b 100644 --- a/internal/contractscan/scan_test.go +++ b/internal/contractscan/scan_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/cssbruno/gowdk/internal/gwdkir" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" runtimecontracts "github.com/cssbruno/gowdk/runtime/contracts" ) @@ -766,7 +766,7 @@ func findDiagnostic(t *testing.T, diagnostics []Diagnostic, code string) Diagnos return Diagnostic{} } -func inputFieldsString(fields []manifest.BackendInputField) string { +func inputFieldsString(fields []source.BackendInputField) string { parts := make([]string, 0, len(fields)) for _, field := range fields { parts = append(parts, field.FieldName+":"+field.FormName+":"+field.Type) diff --git a/internal/gwdkanalysis/analyzer_test.go b/internal/gwdkanalysis/analyzer_test.go index 1030717..d31419e 100644 --- a/internal/gwdkanalysis/analyzer_test.go +++ b/internal/gwdkanalysis/analyzer_test.go @@ -8,6 +8,7 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" "github.com/cssbruno/gowdk/internal/parser" + "github.com/cssbruno/gowdk/internal/source" ) func TestAnalyzeLowersASTIntoManifestAndIR(t *testing.T) { @@ -217,7 +218,7 @@ view { } func TestAnalyzeAddsCommandReferencesToIR(t *testing.T) { - source := `package pages + src := `package pages @page patients @route "/patients" @guard auth.required @@ -230,7 +231,7 @@ view { } ` - pageAST := mustParse(t, source) + pageAST := mustParse(t, src) result, err := Analyze(gowdk.Config{}, []SourceFile{{Path: "pages/patients.page.gwdk", Kind: SourcePage, AST: pageAST}}) if err != nil { t.Fatal(err) @@ -251,14 +252,14 @@ view { if ref.ImportAlias != "patients" || ref.ImportPath != "example.com/app/patients" || ref.Type != "CreatePatient" { t.Fatalf("unexpected command import/type metadata: %#v", ref) } - wantColumn := strings.Index(sourceLine(source, 9), "g:command") + 1 + wantColumn := strings.Index(sourceLine(src, 9), "g:command") + 1 if ref.Span.Start.Line != 9 || ref.Span.Start.Column != wantColumn { t.Fatalf("expected g:command span at 9:%d, got %#v", wantColumn, ref.Span) } } func TestAnalyzeAddsQueryReferencesToIR(t *testing.T) { - source := `package pages + src := `package pages @page patients @route "/patients" @@ -270,7 +271,7 @@ view { } ` - pageAST := mustParse(t, source) + pageAST := mustParse(t, src) result, err := Analyze(gowdk.Config{}, []SourceFile{{Path: "pages/patients.page.gwdk", Kind: SourcePage, AST: pageAST}}) if err != nil { t.Fatal(err) @@ -288,7 +289,7 @@ view { if ref.ImportAlias != "patients" || ref.ImportPath != "example.com/app/patients" || ref.Type != "GetPatientPage" { t.Fatalf("unexpected query import/type metadata: %#v", ref) } - wantColumn := strings.Index(sourceLine(source, 8), "g:query") + 1 + wantColumn := strings.Index(sourceLine(src, 8), "g:query") + 1 if ref.Span.Start.Line != 8 || ref.Span.Start.Column != wantColumn { t.Fatalf("expected g:query span at 8:%d, got %#v", wantColumn, ref.Span) } @@ -310,11 +311,11 @@ func TestBuildIRAttachesBackendBindings(t *testing.T) { BlockName: "Subscribe", Method: "POST", Route: "/newsletter", - Status: manifest.BackendBindingBound, + Status: source.BackendBindingBound, ImportPath: "example.com/app/newsletter", PackageName: "newsletter", FunctionName: "Subscribe", - Signature: manifest.BackendSignatureAction0, + Signature: source.BackendSignatureAction0, }}, }) @@ -322,7 +323,7 @@ func TestBuildIRAttachesBackendBindings(t *testing.T) { t.Fatalf("expected one endpoint, got %#v", ir.Endpoints) } binding := ir.Endpoints[0].Binding - if binding.Status != manifest.BackendBindingBound || binding.FunctionName != "Subscribe" { + if binding.Status != source.BackendBindingBound || binding.FunctionName != "Subscribe" { t.Fatalf("expected backend binding on IR endpoint, got %#v", binding) } } @@ -344,18 +345,18 @@ func TestBuildIRIncludesStandaloneGoEndpointSource(t *testing.T) { BlockName: "Session", Method: "GET", Route: "/api/session", - Status: manifest.BackendBindingBound, + Status: source.BackendBindingBound, ImportPath: "example.com/app/api", PackageName: "api", FunctionName: "Session", - Signature: manifest.BackendSignatureAPI, + Signature: source.BackendSignatureAPI, }}, }) if len(ir.Endpoints) != 1 { t.Fatalf("expected one endpoint, got %#v", ir.Endpoints) } endpoint := ir.Endpoints[0] - if endpoint.Source != gwdkir.EndpointSourceGo || endpoint.PageID != "api.Session" || endpoint.Binding.Signature != manifest.BackendSignatureAPI { + if endpoint.Source != gwdkir.EndpointSourceGo || endpoint.PageID != "api.Session" || endpoint.Binding.Signature != source.BackendSignatureAPI { t.Fatalf("unexpected endpoint IR: %#v", endpoint) } } @@ -423,17 +424,17 @@ view { } } -func mustParse(t *testing.T, source string) parser.SyntaxFile { +func mustParse(t *testing.T, src string) parser.SyntaxFile { t.Helper() - file, err := parser.ParseSyntax([]byte(source)) + file, err := parser.ParseSyntax([]byte(src)) if err != nil { t.Fatal(err) } return file } -func sourceLine(source string, line int) string { - lines := strings.Split(source, "\n") +func sourceLine(src string, line int) string { + lines := strings.Split(src, "\n") if line <= 0 || line > len(lines) { return "" } diff --git a/internal/gwdkanalysis/ir_bindings.go b/internal/gwdkanalysis/ir_bindings.go index bdbf1c0..1543cc7 100644 --- a/internal/gwdkanalysis/ir_bindings.go +++ b/internal/gwdkanalysis/ir_bindings.go @@ -5,6 +5,7 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func attachBackendBindings(program *gwdkir.Program, bindings []manifest.BackendBinding) { @@ -35,7 +36,7 @@ func attachBackendBindings(program *gwdkir.Program, bindings []manifest.BackendB Signature: binding.Signature, InputType: binding.InputType, InputPointer: binding.InputPointer, - InputFields: append([]manifest.BackendInputField(nil), binding.InputFields...), + InputFields: append([]source.BackendInputField(nil), binding.InputFields...), } } for index := range program.Pages { diff --git a/internal/gwdkanalysis/ir_builder.go b/internal/gwdkanalysis/ir_builder.go index 2a2cdc8..97a32d3 100644 --- a/internal/gwdkanalysis/ir_builder.go +++ b/internal/gwdkanalysis/ir_builder.go @@ -8,6 +8,7 @@ import ( "github.com/cssbruno/gowdk/internal/cssscope" "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) // BuildIR converts a normalized manifest into the stable compiler IR. @@ -43,14 +44,14 @@ type irBuilder struct { packages map[string]*gwdkir.Package } -func (builder *irBuilder) ensurePackage(name string, source string) *gwdkir.Package { +func (builder *irBuilder) ensurePackage(name string, src string) *gwdkir.Package { pkg := builder.packages[name] if pkg == nil { pkg = &gwdkir.Package{Name: name} builder.packages[name] = pkg } - dir := filepath.Dir(source) - if source != "" && !contains(pkg.SourceDirs, dir) { + dir := filepath.Dir(src) + if src != "" && !contains(pkg.SourceDirs, dir) { pkg.SourceDirs = append(pkg.SourceDirs, dir) } return pkg @@ -132,7 +133,7 @@ func (builder *irBuilder) addPageAssets(page manifest.Page) { for index, script := range page.InlineJS { name := script.Name if name == "" { - name = manifest.InlineScriptName(index) + name = source.InlineScriptName(index) } builder.program.Assets = append(builder.program.Assets, gwdkir.Asset{ Kind: gwdkir.AssetJS, @@ -262,7 +263,7 @@ func (builder *irBuilder) addComponentAssets(component manifest.Component) { for index, script := range component.InlineJS { name := script.Name if name == "" { - name = manifest.InlineScriptName(index) + name = source.InlineScriptName(index) } builder.program.Assets = append(builder.program.Assets, gwdkir.Asset{ Kind: gwdkir.AssetJS, diff --git a/internal/gwdkanalysis/ir_contracts.go b/internal/gwdkanalysis/ir_contracts.go index 016ef2d..29999d6 100644 --- a/internal/gwdkanalysis/ir_contracts.go +++ b/internal/gwdkanalysis/ir_contracts.go @@ -4,7 +4,7 @@ import ( "strings" "github.com/cssbruno/gowdk/internal/gwdkir" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/internal/view" ) @@ -69,7 +69,7 @@ func irContractReferenceKind(kind view.ContractReferenceKind) gwdkir.ContractKin } } -func templateOffsetSpan(template gwdkir.Template, start int, end int) manifest.SourceSpan { +func templateOffsetSpan(template gwdkir.Template, start int, end int) source.SourceSpan { if start < 0 || end <= start || start >= len([]rune(template.Body)) { return template.Span } @@ -78,10 +78,10 @@ func templateOffsetSpan(template gwdkir.Template, start int, end int) manifest.S if startPos.Line == 0 || endPos.Line == 0 { return template.Span } - return manifest.SourceSpan{Start: startPos, End: endPos} + return source.SourceSpan{Start: startPos, End: endPos} } -func templateOffsetPosition(template gwdkir.Template, offset int) manifest.SourcePosition { +func templateOffsetPosition(template gwdkir.Template, offset int) source.SourcePosition { line := template.BodyStart.Line column := template.BodyStart.Column if line == 0 { @@ -90,7 +90,7 @@ func templateOffsetPosition(template gwdkir.Template, offset int) manifest.Sourc } for index, char := range []rune(template.Body) { if index == offset { - return manifest.SourcePosition{Line: line, Column: column} + return source.SourcePosition{Line: line, Column: column} } if char == '\n' { line++ @@ -100,7 +100,7 @@ func templateOffsetPosition(template gwdkir.Template, offset int) manifest.Sourc column++ } if offset == len([]rune(template.Body)) { - return manifest.SourcePosition{Line: line, Column: column} + return source.SourcePosition{Line: line, Column: column} } - return manifest.SourcePosition{} + return source.SourcePosition{} } diff --git a/internal/gwdkanalysis/ir_lowering.go b/internal/gwdkanalysis/ir_lowering.go index 2c9501f..f61eca5 100644 --- a/internal/gwdkanalysis/ir_lowering.go +++ b/internal/gwdkanalysis/ir_lowering.go @@ -4,6 +4,7 @@ import ( "github.com/cssbruno/gowdk" "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func routeKind(mode gowdk.RenderMode, page manifest.Page) gwdkir.RouteKind { @@ -50,21 +51,21 @@ func lowerIRPage(page manifest.Page) gwdkir.Page { Description: page.Spans.Description, Canonical: page.Spans.Canonical, Image: page.Spans.Image, - Layouts: append([]manifest.NamedSpan(nil), page.Spans.Layouts...), - Guard: append([]manifest.NamedSpan(nil), page.Spans.Guard...), - CSS: append([]manifest.NamedSpan(nil), page.Spans.CSS...), - JS: append([]manifest.NamedSpan(nil), page.Spans.JS...), - InlineJS: append([]manifest.NamedSpan(nil), page.Spans.InlineJS...), - RouteParams: append([]manifest.NamedSpan(nil), page.Spans.RouteParams...), + Layouts: append([]source.NamedSpan(nil), page.Spans.Layouts...), + Guard: append([]source.NamedSpan(nil), page.Spans.Guard...), + CSS: append([]source.NamedSpan(nil), page.Spans.CSS...), + JS: append([]source.NamedSpan(nil), page.Spans.JS...), + InlineJS: append([]source.NamedSpan(nil), page.Spans.InlineJS...), + RouteParams: append([]source.NamedSpan(nil), page.Spans.RouteParams...), }, } } -func copyRouteParams(params []manifest.RouteParam) []manifest.RouteParam { +func copyRouteParams(params []source.RouteParam) []source.RouteParam { if len(params) == 0 { return nil } - out := make([]manifest.RouteParam, len(params)) + out := make([]source.RouteParam, len(params)) copy(out, params) return out } @@ -90,19 +91,19 @@ func lowerIRComponent(component manifest.Component) gwdkir.Component { Span: component.Span, PackageSpan: component.PackageSpan, Spans: gwdkir.ComponentSpans{ - CSS: append([]manifest.NamedSpan(nil), component.Spans.CSS...), - JS: append([]manifest.NamedSpan(nil), component.Spans.JS...), - InlineJS: append([]manifest.NamedSpan(nil), component.Spans.InlineJS...), - Assets: append([]manifest.NamedSpan(nil), component.Spans.Assets...), + CSS: append([]source.NamedSpan(nil), component.Spans.CSS...), + JS: append([]source.NamedSpan(nil), component.Spans.JS...), + InlineJS: append([]source.NamedSpan(nil), component.Spans.InlineJS...), + Assets: append([]source.NamedSpan(nil), component.Spans.Assets...), }, } } -func copyInlineScripts(scripts []manifest.InlineScript) []manifest.InlineScript { +func copyInlineScripts(scripts []source.InlineScript) []source.InlineScript { if len(scripts) == 0 { return nil } - out := make([]manifest.InlineScript, len(scripts)) + out := make([]source.InlineScript, len(scripts)) copy(out, scripts) return out } @@ -141,12 +142,12 @@ func lowerIRBlocks(blocks manifest.Blocks) gwdkir.Blocks { Build: blocks.Spans.Build, Load: blocks.Spans.Load, Client: blocks.Spans.Client, - GoBlocks: append([]manifest.NamedSpan(nil), blocks.Spans.GoBlocks...), + GoBlocks: append([]source.NamedSpan(nil), blocks.Spans.GoBlocks...), View: blocks.Spans.View, ViewBodyStart: blocks.Spans.ViewBodyStart, - Actions: append([]manifest.NamedSpan(nil), blocks.Spans.Actions...), - APIs: append([]manifest.NamedSpan(nil), blocks.Spans.APIs...), - Fragments: append([]manifest.NamedSpan(nil), blocks.Spans.Fragments...), + Actions: append([]source.NamedSpan(nil), blocks.Spans.Actions...), + APIs: append([]source.NamedSpan(nil), blocks.Spans.APIs...), + Fragments: append([]source.NamedSpan(nil), blocks.Spans.Fragments...), Exports: blocks.Spans.Exports, Emits: blocks.Spans.Emits, }, @@ -169,7 +170,7 @@ func lowerIRActions(actions []manifest.Action) []gwdkir.Action { ErrorPage: action.ErrorPage, Span: action.Span, RouteSpan: action.RouteSpan, - RouteParams: append([]manifest.NamedSpan(nil), action.RouteParams...), + RouteParams: append([]source.NamedSpan(nil), action.RouteParams...), InputSpan: action.InputSpan, ValidationSpan: action.ValidationSpan, RedirectSpan: action.RedirectSpan, @@ -189,7 +190,7 @@ func lowerIRAPIs(apis []manifest.API) []gwdkir.API { ErrorPage: api.ErrorPage, Span: api.Span, RouteSpan: api.RouteSpan, - RouteParams: append([]manifest.NamedSpan(nil), api.RouteParams...), + RouteParams: append([]source.NamedSpan(nil), api.RouteParams...), ErrorPageSpan: api.ErrorPageSpan, }) } @@ -228,7 +229,7 @@ func lowerIRFragmentEndpoints(fragments []manifest.FragmentEndpoint) []gwdkir.Fr Span: fragment.Span, RouteSpan: fragment.RouteSpan, TargetSpan: fragment.TargetSpan, - RouteParams: append([]manifest.NamedSpan(nil), fragment.RouteParams...), + RouteParams: append([]source.NamedSpan(nil), fragment.RouteParams...), }) } return out diff --git a/internal/gwdkanalysis/manifest_lowering.go b/internal/gwdkanalysis/manifest_lowering.go index d4b4980..d555f95 100644 --- a/internal/gwdkanalysis/manifest_lowering.go +++ b/internal/gwdkanalysis/manifest_lowering.go @@ -7,11 +7,12 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkast" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) // LowerPage lowers one page AST into manifest compatibility records. -func LowerPage(source string, ast gwdkast.File) (manifest.Page, error) { - page := manifest.Page{Source: source} +func LowerPage(src string, ast gwdkast.File) (manifest.Page, error) { + page := manifest.Page{Source: src} if ast.Package != nil { page.Package = ast.Package.Name page.Spans.Package = ast.Package.Span @@ -43,25 +44,25 @@ func LowerPage(source string, ast gwdkast.File) (manifest.Page, error) { } for _, layout := range ast.Layouts { page.Layouts = append(page.Layouts, layout.ID) - page.Spans.Layouts = append(page.Spans.Layouts, manifest.NamedSpan{Name: layout.ID, Span: layout.Span}) + page.Spans.Layouts = append(page.Spans.Layouts, source.NamedSpan{Name: layout.ID, Span: layout.Span}) } for _, guard := range ast.Guards { page.Guard = append(page.Guard, guard.Name) - page.Spans.Guard = append(page.Spans.Guard, manifest.NamedSpan{Name: guard.Name, Span: guard.Span}) + page.Spans.Guard = append(page.Spans.Guard, source.NamedSpan{Name: guard.Name, Span: guard.Span}) } for _, asset := range ast.CSS { page.CSS = append(page.CSS, asset.Path) - page.Spans.CSS = append(page.Spans.CSS, manifest.NamedSpan{Name: asset.Path, Span: asset.Span}) + page.Spans.CSS = append(page.Spans.CSS, source.NamedSpan{Name: asset.Path, Span: asset.Span}) } for _, asset := range ast.JS { if strings.TrimSpace(asset.Path) != "" { page.JS = append(page.JS, asset.Path) - page.Spans.JS = append(page.Spans.JS, manifest.NamedSpan{Name: asset.Path, Span: asset.Span}) + page.Spans.JS = append(page.Spans.JS, source.NamedSpan{Name: asset.Path, Span: asset.Span}) continue } - name := manifest.InlineScriptName(len(page.InlineJS)) - page.InlineJS = append(page.InlineJS, manifest.InlineScript{Name: name, Body: asset.Inline, Span: asset.Span}) - page.Spans.InlineJS = append(page.Spans.InlineJS, manifest.NamedSpan{Name: name, Span: asset.Span}) + name := source.InlineScriptName(len(page.InlineJS)) + page.InlineJS = append(page.InlineJS, source.InlineScript{Name: name, Body: asset.Inline, Span: asset.Span}) + page.Spans.InlineJS = append(page.Spans.InlineJS, source.NamedSpan{Name: name, Span: asset.Span}) } for _, annotation := range ast.Annotations { @@ -86,7 +87,7 @@ func LowerPage(source string, ast gwdkast.File) (manifest.Page, error) { RouteParams: routeParamSpans(endpoint.Route, endpoint.Span), ErrorPageSpan: endpoint.ErrorPageSpan, }) - page.Blocks.Spans.Actions = append(page.Blocks.Spans.Actions, manifest.NamedSpan{Name: endpoint.Name, Span: endpoint.Span}) + page.Blocks.Spans.Actions = append(page.Blocks.Spans.Actions, source.NamedSpan{Name: endpoint.Name, Span: endpoint.Span}) } for _, endpoint := range ast.APIs { page.Blocks.APIs = append(page.Blocks.APIs, manifest.API{ @@ -99,7 +100,7 @@ func LowerPage(source string, ast gwdkast.File) (manifest.Page, error) { RouteParams: routeParamSpans(endpoint.Route, endpoint.Span), ErrorPageSpan: endpoint.ErrorPageSpan, }) - page.Blocks.Spans.APIs = append(page.Blocks.Spans.APIs, manifest.NamedSpan{Name: endpoint.Name, Span: endpoint.Span}) + page.Blocks.Spans.APIs = append(page.Blocks.Spans.APIs, source.NamedSpan{Name: endpoint.Name, Span: endpoint.Span}) } for _, fragment := range ast.Fragments { page.Blocks.Fragments = append(page.Blocks.Fragments, manifest.FragmentEndpoint{ @@ -113,25 +114,25 @@ func LowerPage(source string, ast gwdkast.File) (manifest.Page, error) { TargetSpan: fragment.TargetSpan, RouteParams: routeParamSpans(fragment.Route, fragment.RouteSpan), }) - page.Blocks.Spans.Fragments = append(page.Blocks.Spans.Fragments, manifest.NamedSpan{Name: fragment.Name, Span: fragment.Span}) + page.Blocks.Spans.Fragments = append(page.Blocks.Spans.Fragments, source.NamedSpan{Name: fragment.Name, Span: fragment.Span}) } if page.ID == "" { - page.ID = derivedPageID(source) + page.ID = derivedPageID(src) } if page.ID == "" { - return manifest.Page{}, fmt.Errorf("%s: missing @page", source) + return manifest.Page{}, fmt.Errorf("%s: missing @page", src) } if page.Route == "" { - return manifest.Page{}, fmt.Errorf("%s: missing @route", source) + return manifest.Page{}, fmt.Errorf("%s: missing @route", src) } return page, nil } -func derivedPageID(source string) string { - if strings.TrimSpace(source) == "" { +func derivedPageID(src string) string { + if strings.TrimSpace(src) == "" { return "" } - base := filepath.Base(source) + base := filepath.Base(src) if base == "." || base == string(filepath.Separator) { return "" } @@ -145,8 +146,8 @@ func derivedPageID(source string) string { } // LowerComponent lowers one component AST into manifest compatibility records. -func LowerComponent(source string, ast gwdkast.File) (manifest.Component, error) { - component := manifest.Component{Source: source} +func LowerComponent(src string, ast gwdkast.File) (manifest.Component, error) { + component := manifest.Component{Source: src} if ast.Package != nil { component.Package = ast.Package.Name component.PackageSpan = ast.Package.Span @@ -155,21 +156,21 @@ func LowerComponent(source string, ast gwdkast.File) (manifest.Component, error) component.Uses = lowerUses(ast.Uses) for _, asset := range ast.CSS { component.CSS = append(component.CSS, asset.Path) - component.Spans.CSS = append(component.Spans.CSS, manifest.NamedSpan{Name: asset.Path, Span: asset.Span}) + component.Spans.CSS = append(component.Spans.CSS, source.NamedSpan{Name: asset.Path, Span: asset.Span}) } for _, asset := range ast.JS { if strings.TrimSpace(asset.Path) != "" { component.JS = append(component.JS, asset.Path) - component.Spans.JS = append(component.Spans.JS, manifest.NamedSpan{Name: asset.Path, Span: asset.Span}) + component.Spans.JS = append(component.Spans.JS, source.NamedSpan{Name: asset.Path, Span: asset.Span}) continue } - name := manifest.InlineScriptName(len(component.InlineJS)) - component.InlineJS = append(component.InlineJS, manifest.InlineScript{Name: name, Body: asset.Inline, Span: asset.Span}) - component.Spans.InlineJS = append(component.Spans.InlineJS, manifest.NamedSpan{Name: name, Span: asset.Span}) + name := source.InlineScriptName(len(component.InlineJS)) + component.InlineJS = append(component.InlineJS, source.InlineScript{Name: name, Body: asset.Inline, Span: asset.Span}) + component.Spans.InlineJS = append(component.Spans.InlineJS, source.NamedSpan{Name: name, Span: asset.Span}) } for _, asset := range ast.Assets { component.Assets = append(component.Assets, asset.Path) - component.Spans.Assets = append(component.Spans.Assets, manifest.NamedSpan{Name: asset.Path, Span: asset.Span}) + component.Spans.Assets = append(component.Spans.Assets, source.NamedSpan{Name: asset.Path, Span: asset.Span}) } if ast.PropsType != nil { component.PropsType = lowerGoTypeRef(*ast.PropsType) @@ -217,7 +218,7 @@ func LowerComponent(source string, ast gwdkast.File) (manifest.Component, error) component.Spans.Assets = namedSpans(component.Assets, annotation.Span) } default: - return manifest.Component{}, fmt.Errorf("%s: unsupported component annotation @%s", source, annotation.Name) + return manifest.Component{}, fmt.Errorf("%s: unsupported component annotation @%s", src, annotation.Name) } } for _, block := range ast.Blocks { @@ -240,7 +241,7 @@ func LowerComponent(source string, ast gwdkast.File) (manifest.Component, error) Body: block.Body, Span: block.Span, }) - component.Blocks.Spans.GoBlocks = append(component.Blocks.Spans.GoBlocks, manifest.NamedSpan{Name: block.Name, Span: block.Span}) + component.Blocks.Spans.GoBlocks = append(component.Blocks.Spans.GoBlocks, source.NamedSpan{Name: block.Name, Span: block.Span}) case "view": component.Blocks.View = true component.Blocks.ViewBody = block.Body @@ -250,18 +251,18 @@ func LowerComponent(source string, ast gwdkast.File) (manifest.Component, error) component.Blocks.Style = strings.TrimSpace(block.StyleBody) != "" component.Blocks.StyleBody = block.StyleBody default: - return manifest.Component{}, fmt.Errorf("%s: unsupported component block %q", source, block.Kind) + return manifest.Component{}, fmt.Errorf("%s: unsupported component block %q", src, block.Kind) } } if component.Name == "" { - return manifest.Component{}, fmt.Errorf("%s: missing @component", source) + return manifest.Component{}, fmt.Errorf("%s: missing @component", src) } return component, nil } // LowerLayout lowers one layout AST into manifest compatibility records. -func LowerLayout(source string, ast gwdkast.File) (manifest.Layout, error) { - layout := manifest.Layout{Source: source} +func LowerLayout(src string, ast gwdkast.File) (manifest.Layout, error) { + layout := manifest.Layout{Source: src} if ast.Package != nil { layout.Package = ast.Package.Name layout.PackageSpan = ast.Package.Span @@ -280,7 +281,7 @@ func LowerLayout(source string, ast gwdkast.File) (manifest.Layout, error) { layout.ID = trimQuotes(annotation.Value) layout.Span = annotation.Span default: - return manifest.Layout{}, fmt.Errorf("%s: unsupported layout annotation @%s", source, annotation.Name) + return manifest.Layout{}, fmt.Errorf("%s: unsupported layout annotation @%s", src, annotation.Name) } } for _, block := range ast.Blocks { @@ -290,7 +291,7 @@ func LowerLayout(source string, ast gwdkast.File) (manifest.Layout, error) { Body: block.Body, Span: block.Span, }) - layout.Blocks.Spans.GoBlocks = append(layout.Blocks.Spans.GoBlocks, manifest.NamedSpan{Name: block.Name, Span: block.Span}) + layout.Blocks.Spans.GoBlocks = append(layout.Blocks.Spans.GoBlocks, source.NamedSpan{Name: block.Name, Span: block.Span}) continue } if block.Kind == "style" { @@ -299,7 +300,7 @@ func LowerLayout(source string, ast gwdkast.File) (manifest.Layout, error) { continue } if block.Kind != "view" { - return manifest.Layout{}, fmt.Errorf("%s: unsupported layout block %q", source, block.Kind) + return manifest.Layout{}, fmt.Errorf("%s: unsupported layout block %q", src, block.Kind) } layout.Blocks.View = true layout.Blocks.ViewBody = block.Body @@ -307,7 +308,7 @@ func LowerLayout(source string, ast gwdkast.File) (manifest.Layout, error) { layout.Blocks.Spans.ViewBodyStart = block.BodyStart } if layout.ID == "" { - return manifest.Layout{}, fmt.Errorf("%s: missing @layout", source) + return manifest.Layout{}, fmt.Errorf("%s: missing @layout", src) } return layout, nil } @@ -413,7 +414,7 @@ func applyPageBlock(page *manifest.Page, block gwdkast.Block) { Body: block.Body, Span: block.Span, }) - page.Blocks.Spans.GoBlocks = append(page.Blocks.Spans.GoBlocks, manifest.NamedSpan{Name: block.Name, Span: block.Span}) + page.Blocks.Spans.GoBlocks = append(page.Blocks.Spans.GoBlocks, source.NamedSpan{Name: block.Name, Span: block.Span}) case "view": page.Blocks.View = true page.Blocks.ViewBody = block.Body @@ -454,22 +455,22 @@ func lowerStores(in []gwdkast.Store) []manifest.Store { return out } -func lowerRouteParamSpans(in []gwdkast.RouteParam) []manifest.NamedSpan { - out := make([]manifest.NamedSpan, 0, len(in)) +func lowerRouteParamSpans(in []gwdkast.RouteParam) []source.NamedSpan { + out := make([]source.NamedSpan, 0, len(in)) for _, param := range in { - out = append(out, manifest.NamedSpan{Name: param.Name, Span: param.Span}) + out = append(out, source.NamedSpan{Name: param.Name, Span: param.Span}) } return out } -func lowerRouteParams(in []gwdkast.RouteParam) []manifest.RouteParam { - out := make([]manifest.RouteParam, 0, len(in)) +func lowerRouteParams(in []gwdkast.RouteParam) []source.RouteParam { + out := make([]source.RouteParam, 0, len(in)) for _, param := range in { paramType := param.Type if paramType == "" { paramType = "string" } - out = append(out, manifest.RouteParam{Name: param.Name, Type: paramType, Span: param.Span}) + out = append(out, source.RouteParam{Name: param.Name, Type: paramType, Span: param.Span}) } return out } diff --git a/internal/gwdkanalysis/routes.go b/internal/gwdkanalysis/routes.go index 1ef09af..68abf3c 100644 --- a/internal/gwdkanalysis/routes.go +++ b/internal/gwdkanalysis/routes.go @@ -4,6 +4,7 @@ import ( "sort" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func routeParams(route string) []string { @@ -20,9 +21,9 @@ func routeParams(route string) []string { return out } -func typedRouteParams(route string) []manifest.RouteParam { +func typedRouteParams(route string) []source.RouteParam { params := manifest.RouteParamsFromPath(route) - out := make([]manifest.RouteParam, 0, len(params)) + out := make([]source.RouteParam, 0, len(params)) seen := map[string]bool{} for _, param := range params { if seen[param.Name] { @@ -34,19 +35,19 @@ func typedRouteParams(route string) []manifest.RouteParam { return out } -func routeParamSpans(route string, fallback manifest.SourceSpan) []manifest.NamedSpan { +func routeParamSpans(route string, fallback source.SourceSpan) []source.NamedSpan { params := routeParams(route) - out := make([]manifest.NamedSpan, 0, len(params)) + out := make([]source.NamedSpan, 0, len(params)) for _, param := range params { - out = append(out, manifest.NamedSpan{Name: param, Span: fallback}) + out = append(out, source.NamedSpan{Name: param, Span: fallback}) } return out } -func namedSpans(values []string, fallback manifest.SourceSpan) []manifest.NamedSpan { - out := make([]manifest.NamedSpan, 0, len(values)) +func namedSpans(values []string, fallback source.SourceSpan) []source.NamedSpan { + out := make([]source.NamedSpan, 0, len(values)) for _, value := range values { - out = append(out, manifest.NamedSpan{Name: value, Span: fallback}) + out = append(out, source.NamedSpan{Name: value, Span: fallback}) } return out } diff --git a/internal/gwdkanalysis/util.go b/internal/gwdkanalysis/util.go index fd3d434..2d62c1b 100644 --- a/internal/gwdkanalysis/util.go +++ b/internal/gwdkanalysis/util.go @@ -8,10 +8,11 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkir" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) -func endpointSource(source manifest.EndpointSource) gwdkir.EndpointSource { - if source == manifest.EndpointSourceGo { +func endpointSource(src manifest.EndpointSource) gwdkir.EndpointSource { + if src == manifest.EndpointSourceGo { return gwdkir.EndpointSourceGo } return gwdkir.EndpointSourceGOWDK @@ -97,7 +98,7 @@ func revalidateSecondsValue(value string) (string, error) { return strconv.FormatInt(int64(duration/time.Second), 10), nil } -func spanForName(spans []manifest.NamedSpan, name string, fallback manifest.SourceSpan) manifest.SourceSpan { +func spanForName(spans []source.NamedSpan, name string, fallback source.SourceSpan) source.SourceSpan { for _, span := range spans { if span.Name == name { return span.Span diff --git a/internal/gwdkast/ast.go b/internal/gwdkast/ast.go index 7b78cd3..862a0d5 100644 --- a/internal/gwdkast/ast.go +++ b/internal/gwdkast/ast.go @@ -2,7 +2,7 @@ package gwdkast import ( - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/internal/view" ) @@ -37,76 +37,76 @@ type File struct { // Package is the top-level Go package declaration. type Package struct { Name string - Span manifest.SourceSpan + Span source.SourceSpan } // Annotation is one top-level @annotation. type Annotation struct { Name string Value string - Span manifest.SourceSpan + Span source.SourceSpan } // PageDecl is an @page declaration. type PageDecl struct { ID string - Span manifest.SourceSpan + Span source.SourceSpan } // ComponentDecl is an @component declaration. type ComponentDecl struct { Name string - Span manifest.SourceSpan + Span source.SourceSpan } // LayoutDecl is an @layout declaration in a layout file. type LayoutDecl struct { ID string - Span manifest.SourceSpan + Span source.SourceSpan } // RouteDecl is an @route declaration. type RouteDecl struct { Path string Params []RouteParam - Span manifest.SourceSpan + Span source.SourceSpan } // RouteParam is one dynamic route segment declared by @route. type RouteParam struct { Name string Type string - Span manifest.SourceSpan + Span source.SourceSpan } // CacheDecl is an @cache route response policy declaration. type CacheDecl struct { Policy string - Span manifest.SourceSpan + Span source.SourceSpan } // RevalidateDecl is an @revalidate stale-while-revalidate declaration. type RevalidateDecl struct { Seconds string - Span manifest.SourceSpan + Span source.SourceSpan } // ErrorPageDecl is a route-local generated error page path. type ErrorPageDecl struct { Path string - Span manifest.SourceSpan + Span source.SourceSpan } // LayoutRef is one @layout reference on a page. type LayoutRef struct { ID string - Span manifest.SourceSpan + Span source.SourceSpan } // GuardRef is one @guard reference on a page. type GuardRef struct { Name string - Span manifest.SourceSpan + Span source.SourceSpan } // AssetRef is one source-selected asset reference. @@ -115,7 +115,7 @@ type AssetRef struct { Path string Inline string Scope AssetScope - Span manifest.SourceSpan + Span source.SourceSpan } // AssetScope records deterministic owner metadata for scoped CSS and future @@ -132,28 +132,28 @@ type AssetScope struct { type Import struct { Alias string Path string - Span manifest.SourceSpan + Span source.SourceSpan } // Use is one top-level GOWDK package import declaration. type Use struct { Alias string Package string - Span manifest.SourceSpan + Span source.SourceSpan } // GoTypeRef references a Go type through a .gwdk import alias. type GoTypeRef struct { Alias string Name string - Span manifest.SourceSpan + Span source.SourceSpan } // GoFuncRef references a Go function through a .gwdk import alias. type GoFuncRef struct { Alias string Name string - Span manifest.SourceSpan + Span source.SourceSpan } // Store is one top-level page-scoped store declaration. @@ -161,20 +161,20 @@ type Store struct { Name string Type GoTypeRef Init GoFuncRef - Span manifest.SourceSpan + Span source.SourceSpan } // StateContract describes a component state type and initializer. type StateContract struct { Type GoTypeRef Init GoFuncRef - Span manifest.SourceSpan + Span source.SourceSpan } // WASMContract points an explicit browser-side Go package at a component. type WASMContract struct { Package string - Span manifest.SourceSpan + Span source.SourceSpan } // Block is one parsed top-level block. @@ -182,8 +182,8 @@ type Block struct { Kind string Name string Body string - Span manifest.SourceSpan - BodyStart manifest.SourcePosition + Span source.SourceSpan + BodyStart source.SourcePosition View []view.Node StyleBody string Records []LiteralRecord @@ -202,8 +202,8 @@ type Endpoint struct { Method string Route string ErrorPage string - Span manifest.SourceSpan - ErrorPageSpan manifest.SourceSpan + Span source.SourceSpan + ErrorPageSpan source.SourceSpan } // FragmentEndpoint is one generated server fragment route declaration. @@ -213,50 +213,50 @@ type FragmentEndpoint struct { Route string Target string Body string - Span manifest.SourceSpan - RouteSpan manifest.SourceSpan - TargetSpan manifest.SourceSpan + Span source.SourceSpan + RouteSpan source.SourceSpan + TargetSpan source.SourceSpan } // LiteralRecord is a first-slice paths/build return record. type LiteralRecord struct { Fields map[string]string - Span manifest.SourceSpan + Span source.SourceSpan } // BuildCall is a first-slice imported build data function call. type BuildCall struct { Alias string Function string - Span manifest.SourceSpan + Span source.SourceSpan } // Prop is one scalar prop declaration inside props {}. type Prop struct { Name string Type string - Span manifest.SourceSpan + Span source.SourceSpan } // Export is one typed public component export inside exports {}. type Export struct { Name string Type string - Span manifest.SourceSpan + Span source.SourceSpan } // Emit is one component event declaration inside emits {}. type Emit struct { Name string Params []EmitParam - Span manifest.SourceSpan + Span source.SourceSpan } // EmitParam is one typed event payload field. type EmitParam struct { Name string Type string - Span manifest.SourceSpan + Span source.SourceSpan } // ActionStatement is one supported statement inside legacy act {} parsing. @@ -268,12 +268,12 @@ type ActionStatement struct { Target string Redirect string Body string - Span manifest.SourceSpan + Span source.SourceSpan } // APIStatement is one supported statement inside legacy api {} parsing. type APIStatement struct { Method string Route string - Span manifest.SourceSpan + Span source.SourceSpan } diff --git a/internal/gwdkir/ir.go b/internal/gwdkir/ir.go index db87708..5de2219 100644 --- a/internal/gwdkir/ir.go +++ b/internal/gwdkir/ir.go @@ -4,7 +4,7 @@ package gwdkir import ( "github.com/cssbruno/gowdk" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) const Version = 1 @@ -41,7 +41,7 @@ type SourceFile struct { Kind SourceKind Package string Name string - Span manifest.SourceSpan + Span source.SourceSpan } type SourceKind string @@ -56,14 +56,14 @@ const ( type Import struct { Alias string Path string - Span manifest.SourceSpan + Span source.SourceSpan } // Use records an explicit GOWDK package import. type Use struct { Alias string Package string - Span manifest.SourceSpan + Span source.SourceSpan } // Store records one shared state declaration. @@ -71,7 +71,7 @@ type Store struct { Name string Type GoRef Init GoRef - Span manifest.SourceSpan + Span source.SourceSpan } // Page is the normalized IR for one page source. @@ -80,7 +80,7 @@ type Page struct { Package string ID string Route string - RouteParams []manifest.RouteParam + RouteParams []source.RouteParam Render gowdk.RenderMode Cache string Revalidate string @@ -90,7 +90,7 @@ type Page struct { Guards []string CSS []string JS []string - InlineJS []manifest.InlineScript + InlineJS []source.InlineScript Imports []Import Uses []Use Stores []Store @@ -130,42 +130,42 @@ type Blocks struct { type GoBlock struct { Target string Body string - Span manifest.SourceSpan + Span source.SourceSpan } type PageSpans struct { - Package manifest.SourceSpan - Page manifest.SourceSpan - Route manifest.SourceSpan - Render manifest.SourceSpan - Cache manifest.SourceSpan - Revalidate manifest.SourceSpan - ErrorPage manifest.SourceSpan - Title manifest.SourceSpan - Description manifest.SourceSpan - Canonical manifest.SourceSpan - Image manifest.SourceSpan - Layouts []manifest.NamedSpan - Guard []manifest.NamedSpan - CSS []manifest.NamedSpan - JS []manifest.NamedSpan - InlineJS []manifest.NamedSpan - RouteParams []manifest.NamedSpan + Package source.SourceSpan + Page source.SourceSpan + Route source.SourceSpan + Render source.SourceSpan + Cache source.SourceSpan + Revalidate source.SourceSpan + ErrorPage source.SourceSpan + Title source.SourceSpan + Description source.SourceSpan + Canonical source.SourceSpan + Image source.SourceSpan + Layouts []source.NamedSpan + Guard []source.NamedSpan + CSS []source.NamedSpan + JS []source.NamedSpan + InlineJS []source.NamedSpan + RouteParams []source.NamedSpan } type BlockSpans struct { - Paths manifest.SourceSpan - Build manifest.SourceSpan - Load manifest.SourceSpan - Client manifest.SourceSpan - GoBlocks []manifest.NamedSpan - View manifest.SourceSpan - ViewBodyStart manifest.SourcePosition - Actions []manifest.NamedSpan - APIs []manifest.NamedSpan - Fragments []manifest.NamedSpan - Exports manifest.SourceSpan - Emits manifest.SourceSpan + Paths source.SourceSpan + Build source.SourceSpan + Load source.SourceSpan + Client source.SourceSpan + GoBlocks []source.NamedSpan + View source.SourceSpan + ViewBodyStart source.SourcePosition + Actions []source.NamedSpan + APIs []source.NamedSpan + Fragments []source.NamedSpan + Exports source.SourceSpan + Emits source.SourceSpan } type Action struct { @@ -179,19 +179,19 @@ type Action struct { Redirect string Fragments []Fragment ErrorPage string - Span manifest.SourceSpan - RouteSpan manifest.SourceSpan - RouteParams []manifest.NamedSpan - InputSpan manifest.SourceSpan - ValidationSpan manifest.SourceSpan - RedirectSpan manifest.SourceSpan - ErrorPageSpan manifest.SourceSpan + Span source.SourceSpan + RouteSpan source.SourceSpan + RouteParams []source.NamedSpan + InputSpan source.SourceSpan + ValidationSpan source.SourceSpan + RedirectSpan source.SourceSpan + ErrorPageSpan source.SourceSpan } type Fragment struct { Target string Body string - Span manifest.SourceSpan + Span source.SourceSpan } type FragmentEndpoint struct { @@ -200,10 +200,10 @@ type FragmentEndpoint struct { Route string Target string Body string - Span manifest.SourceSpan - RouteSpan manifest.SourceSpan - TargetSpan manifest.SourceSpan - RouteParams []manifest.NamedSpan + Span source.SourceSpan + RouteSpan source.SourceSpan + TargetSpan source.SourceSpan + RouteParams []source.NamedSpan } type API struct { @@ -211,10 +211,10 @@ type API struct { Method string Route string ErrorPage string - Span manifest.SourceSpan - RouteSpan manifest.SourceSpan - RouteParams []manifest.NamedSpan - ErrorPageSpan manifest.SourceSpan + Span source.SourceSpan + RouteSpan source.SourceSpan + RouteParams []source.NamedSpan + ErrorPageSpan source.SourceSpan } // Component is the normalized IR for one component source. @@ -226,7 +226,7 @@ type Component struct { Uses []Use CSS []string JS []string - InlineJS []manifest.InlineScript + InlineJS []source.InlineScript Assets []string Props []Prop PropsType GoRef @@ -235,51 +235,51 @@ type Component struct { Exports []Export Emits []Emit Blocks Blocks - Span manifest.SourceSpan - PackageSpan manifest.SourceSpan + Span source.SourceSpan + PackageSpan source.SourceSpan Spans ComponentSpans } type ComponentSpans struct { - CSS []manifest.NamedSpan - JS []manifest.NamedSpan - InlineJS []manifest.NamedSpan - Assets []manifest.NamedSpan + CSS []source.NamedSpan + JS []source.NamedSpan + InlineJS []source.NamedSpan + Assets []source.NamedSpan } type StateContract struct { Type GoRef Init GoRef - Span manifest.SourceSpan + Span source.SourceSpan } type WASMContract struct { Package string - Span manifest.SourceSpan + Span source.SourceSpan } type Prop struct { Name string Type string - Span manifest.SourceSpan + Span source.SourceSpan } type Export struct { Name string Type string - Span manifest.SourceSpan + Span source.SourceSpan } type Emit struct { Name string Params []EmitParam - Span manifest.SourceSpan + Span source.SourceSpan } type EmitParam struct { Name string Type string - Span manifest.SourceSpan + Span source.SourceSpan } // Layout is the normalized IR for one layout source. @@ -289,15 +289,15 @@ type Layout struct { ID string Uses []Use Blocks Blocks - Span manifest.SourceSpan - PackageSpan manifest.SourceSpan + Span source.SourceSpan + PackageSpan source.SourceSpan } // GoRef points at an imported Go package symbol. type GoRef struct { Alias string Name string - Span manifest.SourceSpan + Span source.SourceSpan } // Route is page/file route metadata. Endpoint behavior is represented by @@ -311,11 +311,11 @@ type Route struct { Render gowdk.RenderMode Cache string DynamicParams []string - RouteParams []manifest.RouteParam + RouteParams []source.RouteParam Layouts []string Guards []string Source string - Span manifest.SourceSpan + Span source.SourceSpan } type RouteKind string @@ -339,7 +339,7 @@ type Endpoint struct { ErrorPage string DynamicParams []string SourceFile string - Span manifest.SourceSpan + Span source.SourceSpan Binding Binding } @@ -360,15 +360,15 @@ const ( // Binding describes the selected Go backend handler when one is known. type Binding struct { - Status manifest.BackendBindingStatus + Status source.BackendBindingStatus Message string ImportPath string PackageName string FunctionName string - Signature manifest.BackendSignatureKind + Signature source.BackendSignatureKind InputType string InputPointer bool - InputFields []manifest.BackendInputField + InputFields []source.BackendInputField } // Template records a renderable view block. @@ -381,8 +381,8 @@ type Template struct { Guards []string Imports []Import Body string - Span manifest.SourceSpan - BodyStart manifest.SourcePosition + Span source.SourceSpan + BodyStart source.SourcePosition } // ContractReference records a source-level reference to a backend contract. @@ -396,7 +396,7 @@ type ContractReference struct { Result string Roles []string Guards []string - InputFields []manifest.BackendInputField + InputFields []source.BackendInputField Method string Path string Status ContractBindingStatus @@ -407,7 +407,7 @@ type ContractReference struct { OwnerID string Package string Source string - Span manifest.SourceSpan + Span source.SourceSpan } type ContractKind string @@ -433,7 +433,7 @@ type ClientBehavior struct { Package string Source string Body string - Span manifest.SourceSpan + Span source.SourceSpan } // Asset records source-selected assets and future generated assets. @@ -449,7 +449,7 @@ type Asset struct { UsePackage string ScopeID string HashKey string - Span manifest.SourceSpan + Span source.SourceSpan } type AssetKind string diff --git a/internal/gwdkir/page_methods.go b/internal/gwdkir/page_methods.go new file mode 100644 index 0000000..ca755d2 --- /dev/null +++ b/internal/gwdkir/page_methods.go @@ -0,0 +1,173 @@ +package gwdkir + +import ( + "sort" + "strings" + + "github.com/cssbruno/gowdk" + "github.com/cssbruno/gowdk/internal/source" +) + +// These methods mirror the behavior previously provided by manifest.Page so +// generated-output packages can consume the IR page model directly instead of a +// reconstructed manifest. They read IR fields only and depend on no other model. + +// CachePolicy returns the concrete Cache-Control policy generated for the page. +func (page Page) CachePolicy() string { + return CachePolicyWithRevalidate(page.Cache, page.Revalidate) +} + +// CachePolicyWithRevalidate appends the page revalidation directive to an +// explicit Cache-Control policy. +func CachePolicyWithRevalidate(cache string, revalidate string) string { + if cache == "" || revalidate == "" { + return cache + } + return cache + ", stale-while-revalidate=" + revalidate +} + +// RenderMode resolves the effective render mode for the page, defaulting to SSR +// when the page declares request-time behavior and otherwise to defaultMode +// (SPA when unset). +func (page Page) RenderMode(defaultMode gowdk.RenderMode) gowdk.RenderMode { + if page.Render != "" { + return page.Render + } + if page.Blocks.Load || page.HasGoBlock("ssr") { + return gowdk.SSR + } + if defaultMode == "" { + return gowdk.SPA + } + return defaultMode +} + +// HasGoBlock reports whether the page declares a go block for target. +func (page Page) HasGoBlock(target string) bool { + for _, block := range page.Blocks.GoBlocks { + if block.Target == target { + return true + } + } + return false +} + +// DynamicParams returns route parameters declared with /path/{param} syntax. +func (page Page) DynamicParams() []string { + if len(page.RouteParams) > 0 { + params := make([]string, 0, len(page.RouteParams)) + seen := map[string]bool{} + for _, param := range page.RouteParams { + if param.Name == "" || seen[param.Name] { + continue + } + seen[param.Name] = true + params = append(params, param.Name) + } + sort.Strings(params) + return params + } + params := RouteParamsFromPath(page.Route) + if len(params) == 0 { + return nil + } + names := make([]string, 0, len(params)) + seen := map[string]bool{} + for _, param := range params { + if !seen[param.Name] { + seen[param.Name] = true + names = append(names, param.Name) + } + } + sort.Strings(names) + return names +} + +// TypedRouteParams returns route params with explicit type metadata. Untyped +// params are reported as string. +func (page Page) TypedRouteParams() []source.RouteParam { + if len(page.RouteParams) > 0 { + out := make([]source.RouteParam, 0, len(page.RouteParams)) + for _, param := range page.RouteParams { + if param.Name == "" { + continue + } + if param.Type == "" { + param.Type = "string" + } + out = append(out, param) + } + return out + } + params := RouteParamsFromPath(page.Route) + if len(params) == 0 { + return nil + } + out := make([]source.RouteParam, 0, len(params)) + seen := map[string]bool{} + for _, param := range params { + if seen[param.Name] { + continue + } + seen[param.Name] = true + if param.Type == "" { + param.Type = "string" + } + out = append(out, param) + } + return out +} + +// RouteParamsFromPath parses dynamic route parameters from a route pattern of +// the form /path/{name} or /path/{name:type}. +func RouteParamsFromPath(route string) []source.RouteParam { + var params []source.RouteParam + for index := 0; index < len(route); index++ { + if route[index] != '{' { + continue + } + end := strings.IndexByte(route[index:], '}') + if end < 0 { + continue + } + end += index + body := route[index+1 : end] + name, paramType, ok := splitRouteParamBody(body) + if ok { + params = append(params, source.RouteParam{Name: name, Type: paramType}) + } + index = end + } + return params +} + +func splitRouteParamBody(body string) (string, string, bool) { + name := body + paramType := "string" + if before, after, ok := strings.Cut(body, ":"); ok { + name = before + paramType = after + } + if !isRouteIdent(name) || !isRouteIdent(paramType) { + return "", "", false + } + return name, paramType, true +} + +func isRouteIdent(value string) bool { + if value == "" { + return false + } + for index, r := range value { + if index == 0 { + if r != '_' && (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') { + return false + } + continue + } + if r != '_' && (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && (r < '0' || r > '9') { + return false + } + } + return true +} diff --git a/internal/gwdkir/page_methods_test.go b/internal/gwdkir/page_methods_test.go new file mode 100644 index 0000000..c234a24 --- /dev/null +++ b/internal/gwdkir/page_methods_test.go @@ -0,0 +1,63 @@ +package gwdkir + +import ( + "reflect" + "testing" + + "github.com/cssbruno/gowdk" + "github.com/cssbruno/gowdk/internal/source" +) + +func TestPageRenderModeResolvesRequestTime(t *testing.T) { + cases := []struct { + name string + page Page + def gowdk.RenderMode + want gowdk.RenderMode + }{ + {"explicit", Page{Render: gowdk.SSR}, gowdk.SPA, gowdk.SSR}, + {"load_block", Page{Blocks: Blocks{Load: true}}, gowdk.SPA, gowdk.SSR}, + {"go_ssr_block", Page{Blocks: Blocks{GoBlocks: []GoBlock{{Target: "ssr"}}}}, gowdk.SPA, gowdk.SSR}, + {"default_spa", Page{}, "", gowdk.SPA}, + {"default_passthrough", Page{}, gowdk.Action, gowdk.Action}, + } + for _, tc := range cases { + if got := tc.page.RenderMode(tc.def); got != tc.want { + t.Fatalf("%s: RenderMode = %q, want %q", tc.name, got, tc.want) + } + } +} + +func TestPageDynamicParamsFromExplicitAndPath(t *testing.T) { + explicit := Page{RouteParams: []source.RouteParam{{Name: "slug"}, {Name: "id"}, {Name: "slug"}}} + if got := explicit.DynamicParams(); !reflect.DeepEqual(got, []string{"id", "slug"}) { + t.Fatalf("explicit DynamicParams = %v, want [id slug]", got) + } + + fromPath := Page{Route: "/blog/{slug}/{id:int}"} + if got := fromPath.DynamicParams(); !reflect.DeepEqual(got, []string{"id", "slug"}) { + t.Fatalf("path DynamicParams = %v, want [id slug]", got) + } + + if got := (Page{Route: "/"}).DynamicParams(); got != nil { + t.Fatalf("static route DynamicParams = %v, want nil", got) + } +} + +func TestPageTypedRouteParamsDefaultsToString(t *testing.T) { + page := Page{Route: "/blog/{slug}/{id:int}"} + got := page.TypedRouteParams() + want := []source.RouteParam{{Name: "slug", Type: "string"}, {Name: "id", Type: "int"}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("TypedRouteParams = %#v, want %#v", got, want) + } +} + +func TestPageCachePolicy(t *testing.T) { + if got := (Page{Cache: "public"}).CachePolicy(); got != "public" { + t.Fatalf("CachePolicy = %q", got) + } + if got := (Page{Cache: "public", Revalidate: "60"}).CachePolicy(); got != "public, stale-while-revalidate=60" { + t.Fatalf("CachePolicy with revalidate = %q", got) + } +} diff --git a/internal/lsp/components.go b/internal/lsp/components.go index 4bb5d39..9b1cd03 100644 --- a/internal/lsp/components.go +++ b/internal/lsp/components.go @@ -9,6 +9,7 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkanalysis" "github.com/cssbruno/gowdk/internal/lang" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) type componentDefinition struct { @@ -16,7 +17,7 @@ type componentDefinition struct { Text string Package string Name string - Span manifest.SourceSpan + Span source.SourceSpan } func (server *Server) resolveComponentDefinition(doc document, name string) (componentDefinition, bool) { diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 1caaf3f..e38c5e2 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -5,16 +5,16 @@ import ( "unicode/utf16" "github.com/cssbruno/gowdk/internal/lang" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) -func diagnosticFromLang(item lang.Diagnostic, source string) diagnostic { +func diagnosticFromLang(item lang.Diagnostic, body string) diagnostic { severity := diagnosticSeverityError if item.Severity == "warning" { severity = diagnosticSeverityWarning } return diagnostic{ - Range: rangeFromLangDiagnostic(item, source), + Range: rangeFromLangDiagnostic(item, body), Severity: severity, Code: item.Code, Source: "gowdk", @@ -22,30 +22,30 @@ func diagnosticFromLang(item lang.Diagnostic, source string) diagnostic { } } -func rangeFromLangDiagnostic(item lang.Diagnostic, source string) lspRange { +func rangeFromLangDiagnostic(item lang.Diagnostic, body string) lspRange { if item.Range != nil { - return rangeFromLangRange(*item.Range, source) + return rangeFromLangRange(*item.Range, body) } - return rangeFromPosition(item.Pos, source) + return rangeFromPosition(item.Pos, body) } -func rangeFromLangRange(item lang.Range, source string) lspRange { - start := positionFromLangPosition(item.Start, source) - end := positionFromLangPosition(item.End, source) +func rangeFromLangRange(item lang.Range, body string) lspRange { + start := positionFromLangPosition(item.Start, body) + end := positionFromLangPosition(item.End, body) if end.Line < start.Line || (end.Line == start.Line && end.Character <= start.Character) { end = position{Line: start.Line, Character: start.Character + 1} } return lspRange{Start: start, End: end} } -func lspRangeFromSourceSpan(span manifest.SourceSpan, source string) lspRange { +func lspRangeFromSourceSpan(span source.SourceSpan, body string) lspRange { return rangeFromLangRange(lang.Range{ Start: lang.Position{Line: span.Start.Line, Column: span.Start.Column}, End: lang.Position{Line: span.End.Line, Column: span.End.Column}, - }, source) + }, body) } -func rangeFromPosition(pos lang.Position, source string) lspRange { +func rangeFromPosition(pos lang.Position, body string) lspRange { if pos.Line <= 0 { return lspRange{ Start: position{Line: 0, Character: 0}, @@ -53,7 +53,7 @@ func rangeFromPosition(pos lang.Position, source string) lspRange { } } - lines := strings.Split(source, "\n") + lines := strings.Split(body, "\n") lineIndex := clamp(pos.Line-1, 0, len(lines)-1) character := 0 if pos.Column > 1 && len(lines) > 0 { @@ -73,11 +73,11 @@ func rangeFromPosition(pos lang.Position, source string) lspRange { } } -func positionFromLangPosition(pos lang.Position, source string) position { +func positionFromLangPosition(pos lang.Position, body string) position { if pos.Line <= 0 { return position{Line: 0, Character: 0} } - lines := strings.Split(source, "\n") + lines := strings.Split(body, "\n") lineIndex := clamp(pos.Line-1, 0, len(lines)-1) character := 0 if pos.Column > 1 && len(lines) > 0 { diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 18039ef..defe926 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/cssbruno/gowdk" + "github.com/cssbruno/gowdk/internal/source" ) // Manifest is the compiler's normalized view of discovered .gwdk files. @@ -18,23 +19,17 @@ type Manifest struct { BackendBindings []BackendBinding } +// The shared leaf value types now live in internal/source. These aliases keep +// existing manifest.* references compiling while packages migrate to source.*. + // SourcePosition is a 1-based source location in a parsed .gwdk file. -type SourcePosition struct { - Line int - Column int -} +type SourcePosition = source.SourcePosition // SourceSpan is a 1-based source range. End is exclusive. -type SourceSpan struct { - Start SourcePosition - End SourcePosition -} +type SourceSpan = source.SourceSpan // NamedSpan records the source range for a named declaration or reference. -type NamedSpan struct { - Name string - Span SourceSpan -} +type NamedSpan = source.NamedSpan // Import records a Go import declared by a .gwdk page. type Import struct { @@ -163,19 +158,12 @@ type Page struct { // InlineScript records browser module code declared directly inside a .gwdk // source file. Path-based script declarations should remain preferred. -type InlineScript struct { - Name string - Body string - Span SourceSpan -} +type InlineScript = source.InlineScript // InlineScriptName returns the deterministic generated filename for the // zero-based inline browser script declaration index in one source owner. func InlineScriptName(index int) string { - if index <= 0 { - return "inline-gowdk.js" - } - return fmt.Sprintf("inline-%d-gowdk.js", index+1) + return source.InlineScriptName(index) } // CachePolicy returns the concrete Cache-Control policy generated for the page. @@ -219,11 +207,7 @@ func ErrorPagePath(value string) (string, error) { // RouteParam describes one dynamic route parameter and its declared scalar // type. Empty Type means string for compatibility with legacy {name} syntax. -type RouteParam struct { - Name string - Type string - Span SourceSpan -} +type RouteParam = source.RouteParam // PageMetadata describes HTML document metadata declared by a page. type PageMetadata struct { @@ -405,35 +389,31 @@ type EndpointDeclaration struct { // BackendBindingStatus describes whether a .gwdk backend block has a matching // same-package Go handler. -type BackendBindingStatus string +type BackendBindingStatus = source.BackendBindingStatus const ( - BackendBindingBound BackendBindingStatus = "bound" - BackendBindingMissing BackendBindingStatus = "missing" - BackendBindingUnsupportedSignature BackendBindingStatus = "unsupported_signature" + BackendBindingBound = source.BackendBindingBound + BackendBindingMissing = source.BackendBindingMissing + BackendBindingUnsupportedSignature = source.BackendBindingUnsupportedSignature ) // BackendSignatureKind describes the supported Go handler shape. -type BackendSignatureKind string +type BackendSignatureKind = source.BackendSignatureKind const ( - BackendSignatureAction0 BackendSignatureKind = "action0" - BackendSignatureActionValues BackendSignatureKind = "action_values" - BackendSignatureActionForm BackendSignatureKind = "action_form" - BackendSignatureActionFormPtr BackendSignatureKind = "action_form_ptr" - BackendSignatureAPI BackendSignatureKind = "api" - BackendSignatureFragment BackendSignatureKind = "fragment" - BackendSignatureLoad BackendSignatureKind = "load" - BackendSignatureLoadError BackendSignatureKind = "load_error" + BackendSignatureAction0 = source.BackendSignatureAction0 + BackendSignatureActionValues = source.BackendSignatureActionValues + BackendSignatureActionForm = source.BackendSignatureActionForm + BackendSignatureActionFormPtr = source.BackendSignatureActionFormPtr + BackendSignatureAPI = source.BackendSignatureAPI + BackendSignatureFragment = source.BackendSignatureFragment + BackendSignatureLoad = source.BackendSignatureLoad + BackendSignatureLoadError = source.BackendSignatureLoadError ) // BackendInputField describes one form field decoded into a Go action input // struct from compile-time Go AST metadata. -type BackendInputField struct { - FieldName string - FormName string - Type string -} +type BackendInputField = source.BackendInputField // BackendBinding describes the Go handler selected for an act or api block. type BackendBinding struct { diff --git a/internal/parser/annotations.go b/internal/parser/annotations.go index 2cf15d9..a44d0da 100644 --- a/internal/parser/annotations.go +++ b/internal/parser/annotations.go @@ -7,6 +7,7 @@ import ( "time" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func applyAnnotation(page *manifest.Page, name, rawValue string, lineNumber int, rawLine string) error { @@ -126,9 +127,9 @@ func endpointErrorPage(match []string, lineNumber int) (string, error) { return errorPage, nil } -func endpointErrorPageSpan(match []string, fallback manifest.SourceSpan) manifest.SourceSpan { +func endpointErrorPageSpan(match []string, fallback source.SourceSpan) source.SourceSpan { if len(match) < 5 || strings.TrimSpace(match[4]) == "" { - return manifest.SourceSpan{} + return source.SourceSpan{} } return fallback } diff --git a/internal/parser/component.go b/internal/parser/component.go index 26b9931..562c9eb 100644 --- a/internal/parser/component.go +++ b/internal/parser/component.go @@ -7,10 +7,11 @@ import ( "strings" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) // ParseComponent extracts component metadata and top-level block declarations. -func ParseComponent(source []byte) (manifest.Component, error) { +func ParseComponent(src []byte) (manifest.Component, error) { var component manifest.Component var viewBody []string inView := false @@ -30,10 +31,10 @@ func ParseComponent(source []byte) (manifest.Component, error) { inGoBlock := false goBlockDepth := 0 goBlockTarget := "" - seenGoBlocks := map[string]manifest.SourceSpan{} + seenGoBlocks := map[string]source.SourceSpan{} seenDeclaration := false - scanner := bufio.NewScanner(bytes.NewReader(source)) + scanner := bufio.NewScanner(bytes.NewReader(src)) for lineNumber := 1; scanner.Scan(); lineNumber++ { rawLine := scanner.Text() line := strings.TrimSpace(rawLine) @@ -46,7 +47,7 @@ func ParseComponent(source []byte) (manifest.Component, error) { Body: strings.TrimSpace(strings.Join(goBlockBody, "\n")), Span: seenGoBlocks[goBlockTarget], }) - component.Blocks.Spans.GoBlocks = append(component.Blocks.Spans.GoBlocks, manifest.NamedSpan{Name: goBlockTarget, Span: seenGoBlocks[goBlockTarget]}) + component.Blocks.Spans.GoBlocks = append(component.Blocks.Spans.GoBlocks, source.NamedSpan{Name: goBlockTarget, Span: seenGoBlocks[goBlockTarget]}) inGoBlock = false goBlockBody = nil goBlockDepth = 0 @@ -98,8 +99,8 @@ func ParseComponent(source []byte) (manifest.Component, error) { if line == "}" { jsDepth-- if jsDepth == 0 { - name := manifest.InlineScriptName(len(component.InlineJS)) - component.InlineJS = append(component.InlineJS, manifest.InlineScript{ + name := source.InlineScriptName(len(component.InlineJS)) + component.InlineJS = append(component.InlineJS, source.InlineScript{ Name: name, Body: strings.TrimSpace(strings.Join(jsBody, "\n")), Span: component.Spans.InlineJS[len(component.Spans.InlineJS)-1].Span, @@ -227,13 +228,13 @@ func ParseComponent(source []byte) (manifest.Component, error) { } if match := jsPattern.FindStringSubmatch(line); match != nil { component.JS = append(component.JS, match[1]) - component.Spans.JS = append(component.Spans.JS, manifest.NamedSpan{Name: match[1], Span: sourceLineSpan(lineNumber, rawLine)}) + component.Spans.JS = append(component.Spans.JS, source.NamedSpan{Name: match[1], Span: sourceLineSpan(lineNumber, rawLine)}) continue } if jsBlockPattern.MatchString(line) { span := sourceLineSpan(lineNumber, rawLine) - name := manifest.InlineScriptName(len(component.InlineJS)) - component.Spans.InlineJS = append(component.Spans.InlineJS, manifest.NamedSpan{Name: name, Span: span}) + name := source.InlineScriptName(len(component.InlineJS)) + component.Spans.InlineJS = append(component.Spans.InlineJS, source.NamedSpan{Name: name, Span: span}) inJS = true jsDepth = 1 continue @@ -403,12 +404,12 @@ func parseEmitDeclaration(line string, lineNumber int, rawLine string) (manifest return manifest.Emit{Name: match[1], Params: params, Span: sourceLineSpan(lineNumber, rawLine)}, nil } -func parseEmitParams(source string, lineNumber int, rawLine string) ([]manifest.EmitParam, error) { - source = strings.TrimSpace(source) - if source == "" { +func parseEmitParams(src string, lineNumber int, rawLine string) ([]manifest.EmitParam, error) { + src = strings.TrimSpace(src) + if src == "" { return nil, nil } - parts := strings.Split(source, ",") + parts := strings.Split(src, ",") params := make([]manifest.EmitParam, 0, len(parts)) seen := map[string]bool{} for _, part := range parts { diff --git a/internal/parser/layout.go b/internal/parser/layout.go index 152ab02..f4c337d 100644 --- a/internal/parser/layout.go +++ b/internal/parser/layout.go @@ -7,10 +7,11 @@ import ( "strings" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) // ParseLayout extracts layout metadata and top-level block declarations. -func ParseLayout(source []byte) (manifest.Layout, error) { +func ParseLayout(src []byte) (manifest.Layout, error) { var layout manifest.Layout var viewBody []string inView := false @@ -21,10 +22,10 @@ func ParseLayout(source []byte) (manifest.Layout, error) { inGoBlock := false goBlockDepth := 0 goBlockTarget := "" - seenGoBlocks := map[string]manifest.SourceSpan{} + seenGoBlocks := map[string]source.SourceSpan{} seenDeclaration := false - scanner := bufio.NewScanner(bytes.NewReader(source)) + scanner := bufio.NewScanner(bytes.NewReader(src)) for lineNumber := 1; scanner.Scan(); lineNumber++ { rawLine := scanner.Text() line := strings.TrimSpace(rawLine) @@ -37,7 +38,7 @@ func ParseLayout(source []byte) (manifest.Layout, error) { Body: strings.TrimSpace(strings.Join(goBlockBody, "\n")), Span: seenGoBlocks[goBlockTarget], }) - layout.Blocks.Spans.GoBlocks = append(layout.Blocks.Spans.GoBlocks, manifest.NamedSpan{Name: goBlockTarget, Span: seenGoBlocks[goBlockTarget]}) + layout.Blocks.Spans.GoBlocks = append(layout.Blocks.Spans.GoBlocks, source.NamedSpan{Name: goBlockTarget, Span: seenGoBlocks[goBlockTarget]}) inGoBlock = false goBlockBody = nil goBlockDepth = 0 diff --git a/internal/parser/page_lower.go b/internal/parser/page_lower.go index 55cc1a9..8cf4f39 100644 --- a/internal/parser/page_lower.go +++ b/internal/parser/page_lower.go @@ -6,9 +6,10 @@ import ( "github.com/cssbruno/gowdk/internal/gwdkast" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) -func lowerPageSyntax(source []byte, ast gwdkast.File, defaultID string) (manifest.Page, error) { +func lowerPageSyntax(src []byte, ast gwdkast.File, defaultID string) (manifest.Page, error) { var page manifest.Page if ast.Package != nil { page.Package = ast.Package.Name @@ -17,14 +18,14 @@ func lowerPageSyntax(source []byte, ast gwdkast.File, defaultID string) (manifes page.Imports = lowerSyntaxImports(ast.Imports) page.Uses = lowerSyntaxUses(ast.Uses) page.Stores = lowerSyntaxStores(ast.Stores) - if err := lowerPageSyntaxAnnotations(source, ast, &page); err != nil { + if err := lowerPageSyntaxAnnotations(src, ast, &page); err != nil { return manifest.Page{}, err } for _, block := range ast.Blocks { applyPageSyntaxBlock(&page, block) } for _, endpoint := range ast.Actions { - rawLine := sourceLineText(source, endpoint.Span.Start.Line) + rawLine := sourceLineText(src, endpoint.Span.Start.Line) page.Blocks.Actions = append(page.Blocks.Actions, manifest.Action{ Name: endpoint.Name, Method: endpoint.Method, @@ -35,10 +36,10 @@ func lowerPageSyntax(source []byte, ast gwdkast.File, defaultID string) (manifes RouteParams: routeParamSpans(endpoint.Route, endpoint.Span.Start.Line, rawLine), ErrorPageSpan: endpoint.ErrorPageSpan, }) - page.Blocks.Spans.Actions = append(page.Blocks.Spans.Actions, manifest.NamedSpan{Name: endpoint.Name, Span: endpoint.Span}) + page.Blocks.Spans.Actions = append(page.Blocks.Spans.Actions, source.NamedSpan{Name: endpoint.Name, Span: endpoint.Span}) } for _, endpoint := range ast.APIs { - rawLine := sourceLineText(source, endpoint.Span.Start.Line) + rawLine := sourceLineText(src, endpoint.Span.Start.Line) page.Blocks.APIs = append(page.Blocks.APIs, manifest.API{ Name: endpoint.Name, Method: endpoint.Method, @@ -49,10 +50,10 @@ func lowerPageSyntax(source []byte, ast gwdkast.File, defaultID string) (manifes RouteParams: routeParamSpans(endpoint.Route, endpoint.Span.Start.Line, rawLine), ErrorPageSpan: endpoint.ErrorPageSpan, }) - page.Blocks.Spans.APIs = append(page.Blocks.Spans.APIs, manifest.NamedSpan{Name: endpoint.Name, Span: endpoint.Span}) + page.Blocks.Spans.APIs = append(page.Blocks.Spans.APIs, source.NamedSpan{Name: endpoint.Name, Span: endpoint.Span}) } for _, fragment := range ast.Fragments { - rawLine := sourceLineText(source, fragment.RouteSpan.Start.Line) + rawLine := sourceLineText(src, fragment.RouteSpan.Start.Line) page.Blocks.Fragments = append(page.Blocks.Fragments, manifest.FragmentEndpoint{ Name: fragment.Name, Method: fragment.Method, @@ -64,7 +65,7 @@ func lowerPageSyntax(source []byte, ast gwdkast.File, defaultID string) (manifes TargetSpan: fragment.TargetSpan, RouteParams: routeParamSpans(fragment.Route, fragment.RouteSpan.Start.Line, rawLine), }) - page.Blocks.Spans.Fragments = append(page.Blocks.Spans.Fragments, manifest.NamedSpan{Name: fragment.Name, Span: fragment.Span}) + page.Blocks.Spans.Fragments = append(page.Blocks.Spans.Fragments, source.NamedSpan{Name: fragment.Name, Span: fragment.Span}) } if page.ID == "" { @@ -79,7 +80,7 @@ func lowerPageSyntax(source []byte, ast gwdkast.File, defaultID string) (manifes return page, nil } -func lowerPageSyntaxAnnotations(source []byte, ast gwdkast.File, page *manifest.Page) error { +func lowerPageSyntaxAnnotations(src []byte, ast gwdkast.File, page *manifest.Page) error { if ast.Page != nil { if ast.Page.ID == "" { return fmt.Errorf("line %d: @page requires a value", ast.Page.Span.Start.Line) @@ -110,32 +111,32 @@ func lowerPageSyntaxAnnotations(source []byte, ast gwdkast.File, page *manifest. } for _, layout := range ast.Layouts { page.Layouts = append(page.Layouts, layout.ID) - page.Spans.Layouts = append(page.Spans.Layouts, manifest.NamedSpan{Name: layout.ID, Span: layout.Span}) + page.Spans.Layouts = append(page.Spans.Layouts, source.NamedSpan{Name: layout.ID, Span: layout.Span}) } for _, guard := range ast.Guards { page.Guard = append(page.Guard, guard.Name) - page.Spans.Guard = append(page.Spans.Guard, manifest.NamedSpan{Name: guard.Name, Span: guard.Span}) + page.Spans.Guard = append(page.Spans.Guard, source.NamedSpan{Name: guard.Name, Span: guard.Span}) } for _, css := range ast.CSS { page.CSS = append(page.CSS, css.Path) - page.Spans.CSS = append(page.Spans.CSS, manifest.NamedSpan{Name: css.Path, Span: css.Span}) + page.Spans.CSS = append(page.Spans.CSS, source.NamedSpan{Name: css.Path, Span: css.Span}) } for _, script := range ast.JS { if strings.TrimSpace(script.Path) != "" { page.JS = append(page.JS, script.Path) - page.Spans.JS = append(page.Spans.JS, manifest.NamedSpan{Name: script.Path, Span: script.Span}) + page.Spans.JS = append(page.Spans.JS, source.NamedSpan{Name: script.Path, Span: script.Span}) continue } - name := manifest.InlineScriptName(len(page.InlineJS)) - page.InlineJS = append(page.InlineJS, manifest.InlineScript{Name: name, Body: script.Inline, Span: script.Span}) - page.Spans.InlineJS = append(page.Spans.InlineJS, manifest.NamedSpan{Name: name, Span: script.Span}) + name := source.InlineScriptName(len(page.InlineJS)) + page.InlineJS = append(page.InlineJS, source.InlineScript{Name: name, Body: script.Inline, Span: script.Span}) + page.Spans.InlineJS = append(page.Spans.InlineJS, source.NamedSpan{Name: name, Span: script.Span}) } for _, annotation := range ast.Annotations { if pageAnnotationLoweredFromAST(ast, annotation.Name) { continue } lineNumber := annotation.Span.Start.Line - rawLine := sourceLineText(source, lineNumber) + rawLine := sourceLineText(src, lineNumber) if err := applyAnnotation(page, annotation.Name, annotation.Value, lineNumber, rawLine); err != nil { return fmt.Errorf("line %d: %w", lineNumber, err) } @@ -195,22 +196,22 @@ func lowerSyntaxStores(in []gwdkast.Store) []manifest.Store { return out } -func lowerSyntaxRouteParams(in []gwdkast.RouteParam) []manifest.RouteParam { - out := make([]manifest.RouteParam, 0, len(in)) +func lowerSyntaxRouteParams(in []gwdkast.RouteParam) []source.RouteParam { + out := make([]source.RouteParam, 0, len(in)) for _, param := range in { paramType := param.Type if paramType == "" { paramType = "string" } - out = append(out, manifest.RouteParam{Name: param.Name, Type: paramType, Span: param.Span}) + out = append(out, source.RouteParam{Name: param.Name, Type: paramType, Span: param.Span}) } return out } -func lowerSyntaxRouteParamSpans(in []gwdkast.RouteParam) []manifest.NamedSpan { - out := make([]manifest.NamedSpan, 0, len(in)) +func lowerSyntaxRouteParamSpans(in []gwdkast.RouteParam) []source.NamedSpan { + out := make([]source.NamedSpan, 0, len(in)) for _, param := range in { - out = append(out, manifest.NamedSpan{Name: param.Name, Span: param.Span}) + out = append(out, source.NamedSpan{Name: param.Name, Span: param.Span}) } return out } @@ -239,7 +240,7 @@ func applyPageSyntaxBlock(page *manifest.Page, block gwdkast.Block) { Body: block.Body, Span: block.Span, }) - page.Blocks.Spans.GoBlocks = append(page.Blocks.Spans.GoBlocks, manifest.NamedSpan{Name: block.Name, Span: block.Span}) + page.Blocks.Spans.GoBlocks = append(page.Blocks.Spans.GoBlocks, source.NamedSpan{Name: block.Name, Span: block.Span}) case "view": page.Blocks.View = true page.Blocks.ViewBody = block.Body @@ -251,11 +252,11 @@ func applyPageSyntaxBlock(page *manifest.Page, block gwdkast.Block) { } } -func sourceLineText(source []byte, lineNumber int) string { +func sourceLineText(src []byte, lineNumber int) string { if lineNumber <= 0 { return "" } - lines := strings.Split(string(source), "\n") + lines := strings.Split(string(src), "\n") if lineNumber > len(lines) { return "" } diff --git a/internal/parser/route_helpers.go b/internal/parser/route_helpers.go index a603bb6..93289c5 100644 --- a/internal/parser/route_helpers.go +++ b/internal/parser/route_helpers.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" ) func splitList(value string) []string { @@ -32,7 +32,7 @@ func splitCSSList(value string) []string { return out } -func sourceLineSpan(lineNumber int, rawLine string) manifest.SourceSpan { +func sourceLineSpan(lineNumber int, rawLine string) source.SourceSpan { startColumn := 1 for _, r := range rawLine { if r != ' ' && r != '\t' { @@ -44,29 +44,29 @@ func sourceLineSpan(lineNumber int, rawLine string) manifest.SourceSpan { if endColumn <= startColumn { endColumn = startColumn + 1 } - return manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: lineNumber, Column: startColumn}, - End: manifest.SourcePosition{Line: lineNumber, Column: endColumn}, + return source.SourceSpan{ + Start: source.SourcePosition{Line: lineNumber, Column: startColumn}, + End: source.SourcePosition{Line: lineNumber, Column: endColumn}, } } -func sourceBodyStart(lines []string, firstLineNumber int) manifest.SourcePosition { +func sourceBodyStart(lines []string, firstLineNumber int) source.SourcePosition { for offset, rawLine := range lines { for index, char := range []rune(rawLine) { if strings.TrimSpace(string(char)) == "" { continue } - return manifest.SourcePosition{Line: firstLineNumber + offset, Column: index + 1} + return source.SourcePosition{Line: firstLineNumber + offset, Column: index + 1} } } - return manifest.SourcePosition{} + return source.SourcePosition{} } -func namedValueSpans(values []string, lineNumber int, rawLine string) []manifest.NamedSpan { +func namedValueSpans(values []string, lineNumber int, rawLine string) []source.NamedSpan { if len(values) == 0 { return nil } - spans := make([]manifest.NamedSpan, 0, len(values)) + spans := make([]source.NamedSpan, 0, len(values)) searchStart := 0 for _, value := range values { if value == "" { @@ -74,16 +74,16 @@ func namedValueSpans(values []string, lineNumber int, rawLine string) []manifest } index := strings.Index(rawLine[searchStart:], value) if index < 0 { - spans = append(spans, manifest.NamedSpan{Name: value, Span: sourceLineSpan(lineNumber, rawLine)}) + spans = append(spans, source.NamedSpan{Name: value, Span: sourceLineSpan(lineNumber, rawLine)}) continue } start := searchStart + index end := start + len([]rune(value)) - spans = append(spans, manifest.NamedSpan{ + spans = append(spans, source.NamedSpan{ Name: value, - Span: manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: lineNumber, Column: start + 1}, - End: manifest.SourcePosition{Line: lineNumber, Column: end + 1}, + Span: source.SourceSpan{ + Start: source.SourcePosition{Line: lineNumber, Column: start + 1}, + End: source.SourcePosition{Line: lineNumber, Column: end + 1}, }, }) searchStart = end @@ -91,7 +91,7 @@ func namedValueSpans(values []string, lineNumber int, rawLine string) []manifest return spans } -func parseRouteDeclaration(route string, lineNumber int, rawLine string) (string, []manifest.RouteParam, []manifest.NamedSpan, error) { +func parseRouteDeclaration(route string, lineNumber int, rawLine string) (string, []source.RouteParam, []source.NamedSpan, error) { matches := routeParamPattern.FindAllStringSubmatchIndex(route, -1) if len(matches) == 0 { return route, nil, nil, nil @@ -102,8 +102,8 @@ func parseRouteDeclaration(route string, lineNumber int, rawLine string) (string } normalizedParts := make([]string, 0, len(matches)*3+1) last := 0 - params := make([]manifest.RouteParam, 0, len(matches)) - spans := make([]manifest.NamedSpan, 0, len(matches)) + params := make([]source.RouteParam, 0, len(matches)) + spans := make([]source.NamedSpan, 0, len(matches)) for _, match := range matches { name := route[match[2]:match[3]] paramType := "string" @@ -115,12 +115,12 @@ func parseRouteDeclaration(route string, lineNumber int, rawLine string) (string } start := routeStart + match[0] end := routeStart + match[1] - span := manifest.SourceSpan{ - Start: manifest.SourcePosition{Line: lineNumber, Column: start + 1}, - End: manifest.SourcePosition{Line: lineNumber, Column: end + 1}, + span := source.SourceSpan{ + Start: source.SourcePosition{Line: lineNumber, Column: start + 1}, + End: source.SourcePosition{Line: lineNumber, Column: end + 1}, } - params = append(params, manifest.RouteParam{Name: name, Type: paramType, Span: span}) - spans = append(spans, manifest.NamedSpan{ + params = append(params, source.RouteParam{Name: name, Type: paramType, Span: span}) + spans = append(spans, source.NamedSpan{ Name: name, Span: span, }) @@ -131,7 +131,7 @@ func parseRouteDeclaration(route string, lineNumber int, rawLine string) (string return strings.Join(normalizedParts, ""), params, spans, nil } -func routeParamSpans(route string, lineNumber int, rawLine string) []manifest.NamedSpan { +func routeParamSpans(route string, lineNumber int, rawLine string) []source.NamedSpan { _, _, spans, _ := parseRouteDeclaration(route, lineNumber, rawLine) return spans } diff --git a/internal/parser/syntax.go b/internal/parser/syntax.go index 0624905..8ae8580 100644 --- a/internal/parser/syntax.go +++ b/internal/parser/syntax.go @@ -9,6 +9,7 @@ import ( "github.com/cssbruno/gowdk/internal/cssscope" "github.com/cssbruno/gowdk/internal/gwdkast" "github.com/cssbruno/gowdk/internal/manifest" + "github.com/cssbruno/gowdk/internal/source" "github.com/cssbruno/gowdk/internal/view" ) @@ -35,16 +36,16 @@ type APIStatement = gwdkast.APIStatement // ParseSyntax parses a .gwdk source file into a typed syntax AST for the // current compiler subset. -func ParseSyntax(source []byte) (SyntaxFile, error) { +func ParseSyntax(src []byte) (SyntaxFile, error) { var file SyntaxFile var body []syntaxBodyLine var captured SyntaxBlock var capturedFragment *SyntaxFragmentEndpoint depth := 0 seenDeclaration := false - seenGoBlocks := map[string]manifest.SourceSpan{} + seenGoBlocks := map[string]source.SourceSpan{} - scanner := bufio.NewScanner(bytes.NewReader(source)) + scanner := bufio.NewScanner(bytes.NewReader(src)) for lineNumber := 1; scanner.Scan(); lineNumber++ { rawLine := scanner.Text() line := strings.TrimSpace(rawLine) @@ -478,16 +479,16 @@ func finishSyntaxBlock(block SyntaxBlock, body []syntaxBodyLine) (SyntaxBlock, e return block, nil } -func syntaxBodyStart(body []syntaxBodyLine) manifest.SourcePosition { +func syntaxBodyStart(body []syntaxBodyLine) source.SourcePosition { for _, raw := range body { for index, char := range []rune(raw.Text) { if strings.TrimSpace(string(char)) == "" { continue } - return manifest.SourcePosition{Line: raw.Line, Column: index + 1} + return source.SourcePosition{Line: raw.Line, Column: index + 1} } } - return manifest.SourcePosition{} + return source.SourcePosition{} } func parseBuildCall(body []syntaxBodyLine) (BuildCall, bool, error) { diff --git a/internal/source/source.go b/internal/source/source.go new file mode 100644 index 0000000..c2a3494 --- /dev/null +++ b/internal/source/source.go @@ -0,0 +1,90 @@ +// Package source holds the neutral leaf value types shared across the GOWDK +// compiler packages: source spans, route params, inline scripts, and backend +// binding metadata. These types carry no behavior and depend on nothing else in +// the module, so every layer (parser, AST, IR, manifest, generated output) can +// reference them without creating import cycles or coupling to the manifest +// page/component model. +// +// Historically these lived in internal/manifest, which forced packages that +// only needed a SourceSpan to depend on the whole manifest model (and made +// internal/gwdkir depend on manifest). They were extracted here so the +// manifest model and the IR can both reference shared leaf types from a neutral +// home. manifest re-exports them as aliases for backward compatibility. +package source + +import "fmt" + +// SourcePosition is a 1-based source location in a parsed .gwdk file. +type SourcePosition struct { + Line int + Column int +} + +// SourceSpan is a 1-based source range. End is exclusive. +type SourceSpan struct { + Start SourcePosition + End SourcePosition +} + +// NamedSpan records the source range for a named declaration or reference. +type NamedSpan struct { + Name string + Span SourceSpan +} + +// RouteParam describes one dynamic route parameter and its declared scalar +// type. Empty Type means string for compatibility with legacy {name} syntax. +type RouteParam struct { + Name string + Type string + Span SourceSpan +} + +// InlineScript records browser module code declared directly inside a .gwdk +// source file. Path-based script declarations should remain preferred. +type InlineScript struct { + Name string + Body string + Span SourceSpan +} + +// InlineScriptName returns the deterministic generated filename for the +// zero-based inline browser script declaration index in one source owner. +func InlineScriptName(index int) string { + if index <= 0 { + return "inline-gowdk.js" + } + return fmt.Sprintf("inline-%d-gowdk.js", index+1) +} + +// BackendBindingStatus describes whether a .gwdk backend block has a matching +// same-package Go handler. +type BackendBindingStatus string + +const ( + BackendBindingBound BackendBindingStatus = "bound" + BackendBindingMissing BackendBindingStatus = "missing" + BackendBindingUnsupportedSignature BackendBindingStatus = "unsupported_signature" +) + +// BackendSignatureKind describes the supported Go handler shape. +type BackendSignatureKind string + +const ( + BackendSignatureAction0 BackendSignatureKind = "action0" + BackendSignatureActionValues BackendSignatureKind = "action_values" + BackendSignatureActionForm BackendSignatureKind = "action_form" + BackendSignatureActionFormPtr BackendSignatureKind = "action_form_ptr" + BackendSignatureAPI BackendSignatureKind = "api" + BackendSignatureFragment BackendSignatureKind = "fragment" + BackendSignatureLoad BackendSignatureKind = "load" + BackendSignatureLoadError BackendSignatureKind = "load_error" +) + +// BackendInputField describes one form field decoded into a Go action input +// struct from compile-time Go AST metadata. +type BackendInputField struct { + FieldName string + FormName string + Type string +}