diff --git a/.changeset/yellow-cooks-deliver.md b/.changeset/yellow-cooks-deliver.md new file mode 100644 index 00000000..f6328344 --- /dev/null +++ b/.changeset/yellow-cooks-deliver.md @@ -0,0 +1,7 @@ +--- +'@astrojs/compiler': minor +--- + +Adds `serverComponents` metadata + +This adds a change necessary to support server islands. During transformation the compiler discovers `server:defer` directives and appends them to the `serverComponents` array. This is exported along with the other metadata so that it can be used inside of Astro. diff --git a/cmd/astro-wasm/astro-wasm.go b/cmd/astro-wasm/astro-wasm.go index b8346a1a..9f3ae132 100644 --- a/cmd/astro-wasm/astro-wasm.go +++ b/cmd/astro-wasm/astro-wasm.go @@ -183,6 +183,7 @@ type HoistedScript struct { type HydratedComponent struct { ExportName string `js:"exportName"` + LocalName string `js:"localName"` Specifier string `js:"specifier"` ResolvedPath string `js:"resolvedPath"` } @@ -208,6 +209,7 @@ type TransformResult struct { Scripts []HoistedScript `js:"scripts"` HydratedComponents []HydratedComponent `js:"hydratedComponents"` ClientOnlyComponents []HydratedComponent `js:"clientOnlyComponents"` + ServerComponents []HydratedComponent `js:"serverComponents"` ContainsHead bool `js:"containsHead"` StyleError []string `js:"styleError"` Propagation bool `js:"propagation"` @@ -358,6 +360,7 @@ func Transform() any { scripts := []HoistedScript{} hydratedComponents := []HydratedComponent{} clientOnlyComponents := []HydratedComponent{} + serverComponents := []HydratedComponent{} css_result := printer.PrintCSS(source, doc, transformOptions) for _, bytes := range css_result.Output { css = append(css, string(bytes)) @@ -438,6 +441,15 @@ func Transform() any { }) } + for _, c := range doc.ServerComponents { + serverComponents = append(serverComponents, HydratedComponent{ + ExportName: c.ExportName, + LocalName: c.LocalName, + Specifier: c.Specifier, + ResolvedPath: c.ResolvedPath, + }) + } + var value vert.Value result := printer.PrintToJS(source, doc, len(css), transformOptions, h) transformResult := &TransformResult{ @@ -446,6 +458,7 @@ func Transform() any { Scripts: scripts, HydratedComponents: hydratedComponents, ClientOnlyComponents: clientOnlyComponents, + ServerComponents: serverComponents, ContainsHead: doc.ContainsHead, StyleError: styleError, Propagation: doc.HeadPropagation, diff --git a/internal/node.go b/internal/node.go index d0a4c59d..39d06301 100644 --- a/internal/node.go +++ b/internal/node.go @@ -65,6 +65,7 @@ var scopeMarker = Node{Type: scopeMarkerNode} type HydratedComponentMetadata struct { ExportName string + LocalName string Specifier string ResolvedPath string } @@ -98,6 +99,7 @@ type Node struct { ClientOnlyComponentNodes []*Node ClientOnlyComponents []*HydratedComponentMetadata HydrationDirectives map[string]bool + ServerComponents []*HydratedComponentMetadata ContainsHead bool HeadPropagation bool diff --git a/internal/transform/transform.go b/internal/transform/transform.go index 457baad6..4a099282 100644 --- a/internal/transform/transform.go +++ b/internal/transform/transform.go @@ -523,6 +523,39 @@ func AddComponentProps(doc *astro.Node, n *astro.Node, opts *TransformOptions) { } break + } else if strings.HasPrefix(attr.Key, "server:") { + parts := strings.Split(attr.Key, ":") + directive := parts[1] + + hydrationAttr := astro.Attribute{ + Key: "server:component-directive", + Val: directive, + } + n.Attr = append(n.Attr, hydrationAttr) + + match := matchNodeToImportStatement(doc, n) + if match != nil { + doc.ServerComponents = append(doc.ServerComponents, &astro.HydratedComponentMetadata{ + ExportName: match.ExportName, + LocalName: n.Data, + Specifier: match.Specifier, + ResolvedPath: ResolveIdForMatch(match.Specifier, opts), + }) + + pathAttr := astro.Attribute{ + Key: "server:component-path", + Val: fmt.Sprintf(`"%s"`, ResolveIdForMatch(match.Specifier, opts)), + Type: astro.ExpressionAttribute, + } + n.Attr = append(n.Attr, pathAttr) + + exportAttr := astro.Attribute{ + Key: "server:component-export", + Val: fmt.Sprintf(`"%s"`, match.ExportName), + Type: astro.ExpressionAttribute, + } + n.Attr = append(n.Attr, exportAttr) + } } } } diff --git a/packages/compiler/src/shared/types.ts b/packages/compiler/src/shared/types.ts index 505063e8..8e642395 100644 --- a/packages/compiler/src/shared/types.ts +++ b/packages/compiler/src/shared/types.ts @@ -95,6 +95,7 @@ export type HoistedScript = { type: string } & ( export interface HydratedComponent { exportName: string; + localName: string; specifier: string; resolvedPath: string; } @@ -109,6 +110,7 @@ export interface TransformResult { scripts: HoistedScript[]; hydratedComponents: HydratedComponent[]; clientOnlyComponents: HydratedComponent[]; + serverComponents: HydratedComponent[]; containsHead: boolean; propagation: boolean; } diff --git a/packages/compiler/test/server-islands/meta.ts b/packages/compiler/test/server-islands/meta.ts new file mode 100644 index 00000000..94402f02 --- /dev/null +++ b/packages/compiler/test/server-islands/meta.ts @@ -0,0 +1,45 @@ +import { fileURLToPath } from 'node:url'; +import { transform } from '@astrojs/compiler'; +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; + +const FIXTURE = ` +--- +import Avatar from './Avatar.astro'; +import {Other} from './Other.astro'; +--- + + + +`; + +let result: Awaited>; +test.before(async () => { + result = await transform(FIXTURE, { + resolvePath: async (s: string) => { + const out = new URL(s, import.meta.url); + return fileURLToPath(out); + }, + }); +}); + +test('component metadata added', () => { + assert.equal(result.serverComponents.length, 2); +}); + +test('path resolved to the filename', () => { + const m = result.serverComponents[0]; + assert.ok(m.specifier !== m.resolvedPath); +}); + +test('localName is the name used in the template', () => { + assert.equal(result.serverComponents[0].localName, 'Avatar'); + assert.equal(result.serverComponents[1].localName, 'Other'); +}); + +test('exportName is the export name of the imported module', () => { + assert.equal(result.serverComponents[0].exportName, 'default'); + assert.equal(result.serverComponents[1].exportName, 'Other'); +}); + +test.run();