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();