0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-04-07 23:41:43 -05:00

Server Islands

This commit is contained in:
Matthew Phillips 2024-06-20 16:08:36 -04:00
parent 5da2be3305
commit cbdd13112b
22 changed files with 179 additions and 126 deletions

View file

@ -11,7 +11,7 @@
},
"devDependencies": {
"@astrojs/node": "^8.2.6",
"@astrojs/react": "^3.5.0",
"@astrojs/react": "workspace:*",
"@astrojs/tailwind": "^5.1.0",
"@fortawesome/fontawesome-free": "^6.5.2",
"@tailwindcss/forms": "^0.5.7",

View file

@ -44,8 +44,8 @@ import PersonalBar from '../components/PersonalBar.astro';
<div class="flex items-center space-x-4">
<PersonalBar server:defer>
<PersonalBar slot="fallback" placeholder />
</PersonalBar>
<PersonalBar placeholder slot="fallback" />
</PersonalBar>
</div>
</div>
</header>

View file

@ -124,7 +124,7 @@
"test:node": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
"@astrojs/compiler": "0.0.0-server-islands-20240610194904",
"@astrojs/compiler": "0.0.0-server-islands-20240620184241",
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/markdown-remark": "workspace:*",
"@astrojs/telemetry": "workspace:*",

View file

@ -13,7 +13,7 @@ import type * as rollup from 'rollup';
import type * as vite from 'vite';
import type { Accept, ActionClient, InputSchema } from '../actions/runtime/virtual/server.js';
import type { RemotePattern } from '../assets/utils/remotePattern.js';
import type { AssetsPrefix, SerializedSSRManifest } from '../core/app/types.js';
import type { AssetsPrefix, SSRManifest, SerializedSSRManifest } from '../core/app/types.js';
import type { PageBuildData } from '../core/build/types.js';
import type { AstroConfigType } from '../core/config/index.js';
import type { AstroTimer } from '../core/config/timer.js';
@ -2361,6 +2361,8 @@ export interface AstroSettings {
* - the user is on the latest version already
*/
latestAstroVersion: string | undefined;
serverIslandMap: NonNullable<SSRManifest['serverIslandMap']>;
serverIslandNameMap: NonNullable<SSRManifest['serverIslandNameMap']>;
}
export type AsyncRendererComponentFn<U> = (
@ -3207,6 +3209,7 @@ export interface SSRResult {
props: Record<string, any>,
slots: Record<string, any> | null
): AstroGlobal;
params: Params;
resolve: (s: string) => Promise<string>;
response: AstroGlobal['response'];
request: AstroGlobal['request'];
@ -3223,6 +3226,7 @@ export interface SSRResult {
*/
pathname: string;
cookies: AstroCookies | undefined;
serverIslandNameMap: Map<string, string>;
_metadata: SSRMetadata;
}
@ -3248,6 +3252,7 @@ export interface SSRMetadata {
headInTree: boolean;
extraHead: string[];
propagators: Set<AstroComponentInstance>;
}
/* Preview server stuff */

View file

@ -13,7 +13,6 @@ import {
createModuleScriptElement,
createStylesheetElementSet,
} from '../core/render/ssr-element.js';
import { default404Page } from '../core/routing/astro-designed-error-pages.js';
export class ContainerPipeline extends Pipeline {
/**

View file

@ -1,4 +1,5 @@
import type {
ComponentInstance,
Locales,
MiddlewareHandler,
RouteData,
@ -62,6 +63,8 @@ export type SSRManifest = {
componentMetadata: SSRResult['componentMetadata'];
pageModule?: SinglePageBuiltModule;
pageMap?: Map<ComponentPath, ImportComponentInstance>;
serverIslandMap?: Map<string, () => Promise<ComponentInstance>>;
serverIslandNameMap?: Map<string, string>;
i18n: SSRManifestI18n | undefined;
middleware: MiddlewareHandler;
checkOrigin: boolean;

View file

@ -23,6 +23,8 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
adapter: undefined,
injectedRoutes: [],
resolvedInjectedRoutes: [],
serverIslandMap: new Map(),
serverIslandNameMap: new Map(),
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
contentEntryTypes: [markdownContentEntryType],
dataEntryTypes: [

View file

@ -320,6 +320,7 @@ export class RenderContext {
createAstro: (astroGlobal, props, slots) =>
this.createAstro(result, astroGlobal, props, slots),
links,
params: this.params,
partial,
pathname,
renderers,
@ -329,6 +330,7 @@ export class RenderContext {
scripts,
styles,
actionResult,
serverIslandNameMap: manifest.serverIslandNameMap ?? new Map(),
_metadata: {
hasHydrationScript: false,
rendererSpecificHydrationScripts: new Set(),

View file

@ -1,4 +1,4 @@
import type { ManifestData, RouteData } from '../../@types/astro.js';
import type { ComponentInstance, ManifestData, RouteData } from '../../@types/astro.js';
import notFoundTemplate from '../../template/4xx.js';
import { DEFAULT_404_COMPONENT } from '../constants.js';
@ -23,7 +23,7 @@ export function ensure404Route(manifest: ManifestData) {
return manifest;
}
export async function default404Page({ pathname }: { pathname: string }) {
async function default404Page({ pathname }: { pathname: string }) {
return new Response(
notFoundTemplate({
statusCode: 404,
@ -36,3 +36,7 @@ export async function default404Page({ pathname }: { pathname: string }) {
}
// mark the function as an AstroComponentFactory for the rendering internals
default404Page.isAstroComponentFactory = true;
export const default404Instance: ComponentInstance = {
default: default404Page,
};

View file

@ -1,9 +1,31 @@
import type { ManifestData } from "#astro/@types/astro";
import type { ModuleLoader } from "../module-loader/loader.js";
import { ensureServerIslandRoute } from "../server-islands/endpoint.js";
import { ensure404Route } from './astro-designed-error-pages.js';
import type { ComponentInstance, ManifestData, SSRManifest, } from "../../@types/astro.js";
import { DEFAULT_404_COMPONENT } from "../constants.js";
import { ensureServerIslandRoute, createEndpoint as createServerIslandEndpoint, SERVER_ISLAND_ROUTE, SERVER_ISLAND_COMPONENT } from "../server-islands/endpoint.js";
import { ensure404Route, default404Instance, DEFAULT_404_ROUTE } from './astro-designed-error-pages.js';
export function injectDefaultRoutes(manifest: ManifestData, loader: ModuleLoader) {
export function injectDefaultRoutes(manifest: ManifestData) {
ensure404Route(manifest);
ensureServerIslandRoute(manifest);
return manifest;
}
type DefaultRouteParams = {
instance: ComponentInstance;
matchesComponent(filePath: URL): boolean;
route: string;
}
export function createDefaultRoutes(manifest: SSRManifest, root: URL): DefaultRouteParams[] {
return [
{
instance: default404Instance,
matchesComponent: (filePath) => filePath.href === new URL(DEFAULT_404_COMPONENT, root).href,
route: DEFAULT_404_ROUTE.route,
},
{
instance: createServerIslandEndpoint(manifest),
matchesComponent: (filePath) => filePath.href === new URL(SERVER_ISLAND_COMPONENT, root).href,
route: SERVER_ISLAND_ROUTE,
}
];
}

View file

@ -1,10 +1,10 @@
import type { AstroSettings } from '../../@types/astro.js';
export function injectServerIslandEndpoint(settings: AstroSettings) {
settings.injectedRoutes.push({
/*settings.injectedRoutes.push({
pattern: '/_server-islands/[name]',
entrypoint: 'astro/components/_ServerIslandRenderer.astro',
});
});*/
return settings;
}

View file

@ -1,33 +1,18 @@
import type { ManifestData, RouteData } from '#astro/@types/astro';
import type { APIRoute } from '../../@types/astro.js';
import { renderComponent, renderTemplate, type AstroComponentFactory } from '../../runtime/server/index.js';
import type { APIRoute, ComponentInstance, ManifestData, RouteData, SSRManifest } from '../../@types/astro.js';
import type { ModuleLoader } from '../module-loader/loader.js';
export function createEndpoint(loader: ModuleLoader) {
const POST: APIRoute = async ({ request, params }) => {
let raw = await request.text();
let data = JSON.parse(raw);
console.log("D", data);
return new Response(`<div>Testing1</div>`, {
status: 200,
headers: {
'Content-Type': 'text/html'
}
});
export const SERVER_ISLAND_ROUTE = '/_server-islands/[name]';
export const SERVER_ISLAND_COMPONENT = '_server-islands.astro';
export function ensureServerIslandRoute(manifest: ManifestData) {
if (manifest.routes.some((route) => route.route === '/_server-islands/[name]')) {
return;
}
return {
POST
};
}
export function ensureServerIslandRoute(manifest: ManifestData, loader: ModuleLoader) {
const endpoint = createEndpoint(loader);
const route: RouteData = {
type: 'endpoint',
component: '_server-islands.ts',
type: 'page',
component: SERVER_ISLAND_COMPONENT,
generate: () => '',
params: ['name'],
segments: [
@ -38,39 +23,47 @@ export function ensureServerIslandRoute(manifest: ManifestData, loader: ModuleLo
prerender: false,
isIndex: false,
fallbackRoutes: [],
route: '/_server-islands/[name]',
/*
"type": "endpoint",
"isIndex": false,
"route": "/_server-islands/[name]",
"pattern": {},
"segments": [
[
{
"content": "_server-islands",
"dynamic": false,
"spread": false
}
],
[
{
"content": "name",
"dynamic": true,
"spread": false
}
]
],
"params": [
"name"
],
"component": "../../packages/astro/dist/core/server-islands/endpoint.js",
"prerender": false,
"fallbackRoutes": []
*/
route: SERVER_ISLAND_ROUTE,
}
//manifest.routes.push();
manifest.routes.push(route);
}
type RenderOptions = {
componentExport: string;
props: Record<string, any>;
slots: Record<string, any>;
}
export function createEndpoint(manifest: SSRManifest) {
const page: AstroComponentFactory = async (result) => {
const params = result.params;
const request = result.request;
const raw = await request.text();
const data = JSON.parse(raw) as RenderOptions;
const componentId = params.name! as string;
const imp = manifest.serverIslandMap?.get(componentId);
if(!imp) {
return new Response('Not found', {
status: 404
});
}
const props = data.props;
const slots = data.slots;
const componentModule = await imp();
const Component = (componentModule as any)[data.componentExport];
return renderTemplate`${renderComponent(result, 'Component', Component, props, slots)}`;
}
page.isAstroComponentFactory = true;
const instance: ComponentInstance = {
default: page,
};
return instance;
}

View file

@ -1,36 +1,44 @@
import type { AstroSettings } from '../../@types/astro.js';
import type { Plugin as VitePlugin } from 'vite';
const virtualModuleId = 'astro:internal/server-islands';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
import type { AstroPluginMetadata } from '#astro/vite-plugin-astro/index';
import type { AstroSettings, ComponentInstance } from '../../@types/astro.js';
import type { ViteDevServer, Plugin as VitePlugin } from 'vite';
export function vitePluginServerIslands({ settings }: { settings: AstroSettings }): VitePlugin {
let viteServer: ViteDevServer | null = null;
return {
name: 'astro:server-islands',
resolveId(source) {
if(source === 'astro:internal/server-islands') {
return resolvedVirtualModuleId;
}
enforce: 'post',
configureServer(_server) {
viteServer = _server;
},
load(id) {
if(id === resolvedVirtualModuleId) {
return `
export let islands = null;
transform(code, id, options) {
if(id.endsWith('.astro')) {
const info = this.getModuleInfo(id);
if(info?.meta) {
const astro = info.meta.astro as AstroPluginMetadata['astro'];
if(astro.serverComponents.length) {
if(viteServer) {
for(const comp of astro.serverComponents) {
if(!settings.serverIslandNameMap.has(comp.resolvedPath)) {
let name = comp.localName;
let idx = 1;
if(import.meta.env.DEV) {
islands = new Proxy({}, {
get(target, name) {
return () => {
console.log("IMPORT", name);
return import(/* @vite-ignore */ name);
};
}
});
} else {
// TODO inline all of the known server islands from the build artifacts.
islands = {};
}
`.trim();
while(true) {
// Name not taken, let's use it.
if(!settings.serverIslandMap.has(name)) {
break;
}
// Increment a number onto the name: Avatar -> Avatar1
name += idx++;
}
settings.serverIslandNameMap.set(comp.resolvedPath, name);
settings.serverIslandMap.set(name, () => {
return viteServer?.ssrLoadModule(comp.resolvedPath) as any;
});
}
}
}
}
}
}
}
}

View file

@ -6,7 +6,7 @@ export type AstroFactoryReturnValue = RenderTemplateResult | Response | HeadAndC
// The callback passed to to $$createComponent
export interface AstroComponentFactory {
(result: any, props: any, slots: any): AstroFactoryReturnValue | Promise<AstroFactoryReturnValue>;
(result: SSRResult, props: any, slots: any): AstroFactoryReturnValue | Promise<AstroFactoryReturnValue>;
isAstroComponentFactory?: boolean;
moduleId?: string | undefined;
propagation?: PropagationHint;

View file

@ -475,7 +475,7 @@ function renderAstroComponent(
slots: any = {}
): RenderInstance {
if(containsServerDirective(props)) {
return renderServerIsland(displayName, props, slots);
return renderServerIsland(result, displayName, props, slots);
}
const instance = createAstroComponentInstance(result, displayName, Component, props, slots);

View file

@ -1,5 +1,9 @@
import type {
SSRResult,
} from '../../../@types/astro.js';
import { renderChild } from "./any.js";
import type { RenderInstance } from "./common.js";
import { renderSlot, type ComponentSlotValue } from "./slot.js";
const internalProps = new Set([
'server:component-path',
@ -13,15 +17,21 @@ export function containsServerDirective(props: Record<string | number, any>,) {
}
export function renderServerIsland(
result: SSRResult,
displayName: string,
props: Record<string | number, any>,
slots: any,
slots: Record<string, ComponentSlotValue>,
): RenderInstance {
return {
async render(destination) {
const componentPath = props['server:component-path'];
const componentExport = props['server:component-export'];
const componentId = result.serverIslandNameMap.get(componentPath);
if(!componentId) {
throw new Error(`Could not find server component name`);
}
// Remove internal props
for(const key of Object.keys(props)) {
if(internalProps.has(key)) {
@ -32,26 +42,23 @@ export function renderServerIsland(
destination.write('<!--server-island-start-->')
// Render the slots
const renderedSlots = {};
if(slots.fallback) {
await renderChild(destination, slots.fallback());
await renderChild(destination, slots.fallback(result));
}
const hostId = crypto.randomUUID();
destination.write(`<script async type="module" data-island-id="${hostId}">
let componentPath = ${JSON.stringify(componentPath)};
let componentId = ${JSON.stringify(componentId)};
let componentExport = ${JSON.stringify(componentExport)};
let script = document.querySelector('script[data-island-id="${hostId}"]');
let data = {
componentPath,
componentExport,
props: ${JSON.stringify(props)},
slot: ${JSON.stringify(slots)},
};
let response = await fetch('/_server-islands/${displayName}', {
let response = await fetch('/_server-islands/${componentId}', {
method: 'POST',
body: JSON.stringify(data),
});
@ -68,7 +75,6 @@ if(response.status === 200 && response.headers.get('content-type') === 'text/htm
let frag = document.createRange().createContextualFragment(html);
script.before(frag);
//script.insertAdjacentHTML('beforebegin', html);
}
script.remove();
</script>`)

View file

@ -19,7 +19,8 @@ import { AggregateError, AstroError, CSSError, MarkdownError } from '../core/err
import type { Logger } from '../core/logger/core.js';
import type { ModuleLoader } from '../core/module-loader/index.js';
import { Pipeline, loadRenderer } from '../core/render/index.js';
import { DEFAULT_404_ROUTE, default404Page } from '../core/routing/astro-designed-error-pages.js';
import { DEFAULT_404_ROUTE } from '../core/routing/astro-designed-error-pages.js';
import { createDefaultRoutes } from '../core/routing/default.js';
import { isPage, isServerLikeOutput, resolveIdToUrl, viteID } from '../core/util.js';
import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
import { getStylesForURL } from './css.js';
@ -44,13 +45,16 @@ export class DevPipeline extends Pipeline {
readonly logger: Logger,
readonly manifest: SSRManifest,
readonly settings: AstroSettings,
readonly config = settings.config
readonly config = settings.config,
readonly defaultRoutes = createDefaultRoutes(manifest, config.root)
) {
const mode = 'development';
const resolve = createResolve(loader, config.root);
const serverLike = isServerLikeOutput(config);
const streaming = true;
super(logger, manifest, mode, [], resolve, serverLike, streaming);
manifest.serverIslandMap = settings.serverIslandMap;
manifest.serverIslandNameMap = settings.serverIslandNameMap;
}
static create(
@ -153,8 +157,12 @@ export class DevPipeline extends Pipeline {
async preload(routeData: RouteData, filePath: URL) {
const { loader } = this;
if (filePath.href === new URL(DEFAULT_404_COMPONENT, this.config.root).href) {
return { default: default404Page } as any as ComponentInstance;
// First check built-in routes
for(const route of this.defaultRoutes) {
if(route.matchesComponent(filePath)) {
debugger;
return route.instance;
}
}
// Important: This needs to happen first, in case a renderer provides polyfills.
@ -244,8 +252,10 @@ export class DevPipeline extends Pipeline {
rewriteKnownRoute(route: string, sourceRoute: RouteData): ComponentInstance {
if (isServerLikeOutput(this.config) && sourceRoute.prerender) {
if (route === '/404') {
return { default: default404Page } as any as ComponentInstance;
for(let def of this.defaultRoutes) {
if(route === def.route) {
return def.instance;
}
}
}

View file

@ -37,7 +37,6 @@ export default function createVitePluginAstroServer({
const manifest = createDevelopmentManifest(settings);
let manifestData: ManifestData = injectDefaultRoutes(
createRouteManifest({ settings, fsMod }, logger),
loader
);
const pipeline = DevPipeline.create(manifestData, { loader, logger, manifest, settings });
const controller = createController({ loader });
@ -49,7 +48,6 @@ export default function createVitePluginAstroServer({
if (needsManifestRebuild) {
manifestData = injectDefaultRoutes(
createRouteManifest({ settings }, logger),
loader,
);
pipeline.setManifestData(manifestData);
}

View file

@ -11,7 +11,6 @@ import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
import { RenderContext } from '../core/render-context.js';
import { type SSROptions, getProps } from '../core/render/index.js';
import { createRequest } from '../core/request.js';
import { default404Page } from '../core/routing/astro-designed-error-pages.js';
import { matchAllRoutes } from '../core/routing/index.js';
import { normalizeTheLocale } from '../i18n/index.js';
import { getSortedPreloadedMatches } from '../prerender/routing.js';

View file

@ -212,6 +212,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
const astroMetadata: AstroPluginMetadata['astro'] = {
clientOnlyComponents: transformResult.clientOnlyComponents,
hydratedComponents: transformResult.hydratedComponents,
serverComponents: transformResult.serverComponents,
scripts: transformResult.scripts,
containsHead: transformResult.containsHead,
propagation: transformResult.propagation ? 'self' : 'none',

View file

@ -10,6 +10,7 @@ export interface PluginMetadata {
astro: {
hydratedComponents: TransformResult['hydratedComponents'];
clientOnlyComponents: TransformResult['clientOnlyComponents'];
serverComponents: TransformResult['serverComponents'];
scripts: TransformResult['scripts'];
containsHead: TransformResult['containsHead'];
propagation: PropagationHint;

10
pnpm-lock.yaml generated
View file

@ -379,7 +379,7 @@ importers:
specifier: ^8.2.6
version: link:../../packages/integrations/node
'@astrojs/react':
specifier: ^3.5.0
specifier: workspace:*
version: link:../../packages/integrations/react
'@astrojs/tailwind':
specifier: ^5.1.0
@ -568,8 +568,8 @@ importers:
packages/astro:
dependencies:
'@astrojs/compiler':
specifier: 0.0.0-server-islands-20240610194904
version: 0.0.0-server-islands-20240610194904
specifier: 0.0.0-server-islands-20240620184241
version: 0.0.0-server-islands-20240620184241
'@astrojs/internal-helpers':
specifier: workspace:*
version: link:../internal-helpers
@ -5939,8 +5939,8 @@ packages:
sisteransi: 1.0.5
dev: false
/@astrojs/compiler@0.0.0-server-islands-20240610194904:
resolution: {integrity: sha512-qp5mr/fKIYbNDgwkJDYoLbLGvEd3R4pz0xsja7KwVGTla7PnGl+RTmq2RwzUrpfiPAEBnrUiHinep6decgGmcA==}
/@astrojs/compiler@0.0.0-server-islands-20240620184241:
resolution: {integrity: sha512-vPcWdGvtYceezqcQyVGCHs9nwZZjqAVGrovbytN4S0nsUDqLVYZuDuRFxjvboRSSXFF9+42TBM1Wh8HNNC0r/Q==}
dev: false
/@astrojs/compiler@1.8.2: