0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-30 22:03:56 -05:00

Add support for the build to Server Islands

This commit is contained in:
Matthew Phillips 2024-06-28 09:01:58 -04:00
parent 324f74e55d
commit df8d5f20c0
16 changed files with 98 additions and 103 deletions

View file

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

View file

@ -1,17 +0,0 @@
---
import Cart from './Cart.js';
// Delay for fun
await new Promise(resolve => setTimeout(resolve, 3000));
---
<style>
.shopping-cart {
display: inline-block;
background-color: tomato;
}
</style>
<div class="shopping-cart">
I'm a shopping cart
<Cart client:load />
</div>

View file

@ -1,15 +0,0 @@
import { useEffect, useState } from 'react';
export default function() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
if(count < 10) {
setCount(count + 1);
}
}, 2000);
}, [count]);
return (
<div>Count: {count}</div>
)
}

View file

@ -1,24 +0,0 @@
---
import Cart from '../components/Cart.astro';
---
<style>
header {
display: flex;
justify-content: space-between;
}
header > * {
flex: 1;
}
h1 {
margin: 0;
}
</style>
<header>
<h1>My App</h1>
<div class="items-left">
<Cart server:defer>
<div class="shopping-fallback" slot="fallback">Loading</div>
</Cart>
</div>
</header>

View file

@ -2,6 +2,7 @@
import '../base.css';
import AddToCart from '../components/AddToCart';
import PersonalBar from '../components/PersonalBar.astro';
import '@fortawesome/fontawesome-free/css/all.min.css';
---
<!DOCTYPE html>
<html lang="en">
@ -19,8 +20,6 @@ import PersonalBar from '../components/PersonalBar.astro';
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="../../node_modules/@fortawesome/fontawesome-free/css/all.min.css">
</head>
<body>

View file

@ -1,11 +0,0 @@
---
import Header from '../components/Header.astro';
---
<html lang="en">
<head>
<title>Testing</title>
</head>
<body>
<Header />
</body>
</html>

View file

@ -17,6 +17,7 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest):
const componentMetadata = new Map(serializedManifest.componentMetadata);
const inlinedScripts = new Map(serializedManifest.inlinedScripts);
const clientDirectives = new Map(serializedManifest.clientDirectives);
const serverIslandNameMap = new Map(serializedManifest.serverIslandNameMap);
return {
// in case user middleware exists, this no-op middleware will be reassigned (see plugin-ssr.ts)
@ -29,5 +30,6 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest):
inlinedScripts,
clientDirectives,
routes,
serverIslandNameMap,
};
}

View file

@ -20,7 +20,7 @@ import {
} from '../path.js';
import { RenderContext } from '../render-context.js';
import { createAssetLink } from '../render/ssr-element.js';
import { ensure404Route } from '../routing/astro-designed-error-pages.js';
import { injectDefaultRoutes } from '../routing/default.js';
import { matchRoute } from '../routing/match.js';
import { createOriginCheckMiddleware } from './middlewares.js';
import { AppPipeline } from './pipeline.js';
@ -87,7 +87,7 @@ export class App {
constructor(manifest: SSRManifest, streaming = true) {
this.#manifest = manifest;
this.#manifestData = ensure404Route({
this.#manifestData = injectDefaultRoutes({
routes: manifest.routes.map((route) => route.routeData),
});
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);

View file

@ -8,12 +8,8 @@ import type {
} from '../../@types/astro.js';
import { Pipeline } from '../base-pipeline.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import { DEFAULT_404_COMPONENT } from '../constants.js';
import { RewriteEncounteredAnError } from '../errors/errors-data.js';
import { AstroError } from '../errors/index.js';
import { RedirectSinglePageBuiltModule } from '../redirects/component.js';
import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js';
import { DEFAULT_404_ROUTE } from '../routing/astro-designed-error-pages.js';
import { findRouteToRewrite } from '../routing/rewrite.js';
export class AppPipeline extends Pipeline {
@ -103,13 +99,16 @@ export class AppPipeline extends Pipeline {
}
async getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
if (route.component === DEFAULT_404_COMPONENT) {
return {
page: async () =>
({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance,
renderers: [],
};
for(const defaultRoute of this.defaultRoutes) {
if(route.component === defaultRoute.component) {
//return defaultRoute.instance;
return {
page: () => Promise.resolve(defaultRoute.instance),
renderers: []
};
}
}
if (route.type === 'redirect') {
return RedirectSinglePageBuiltModule;
} else {

View file

@ -84,11 +84,12 @@ export type SSRManifestI18n = {
export type SerializedSSRManifest = Omit<
SSRManifest,
'middleware' | 'routes' | 'assets' | 'componentMetadata' | 'inlinedScripts' | 'clientDirectives'
'middleware' | 'routes' | 'assets' | 'componentMetadata' | 'inlinedScripts' | 'clientDirectives' | 'serverIslandNameMap'
> & {
routes: SerializedRouteInfo[];
assets: string[];
componentMetadata: [string, SSRComponentMetadata][];
inlinedScripts: [string, string][];
clientDirectives: [string, string][];
serverIslandNameMap: [string, string][];
};

View file

@ -14,6 +14,7 @@ import { AstroError } from './errors/errors.js';
import { AstroErrorData } from './errors/index.js';
import type { Logger } from './logger/core.js';
import { RouteCache } from './render/route-cache.js';
import { createDefaultRoutes } from './routing/default.js';
/**
* The `Pipeline` represents the static parts of rendering that do not change between requests.
@ -52,7 +53,8 @@ export abstract class Pipeline {
* Used for `Astro.site`.
*/
readonly site = manifest.site ? new URL(manifest.site) : undefined,
readonly callSetGetEnv = true
readonly callSetGetEnv = true,
readonly defaultRoutes = createDefaultRoutes(manifest, new URL(import.meta.url))
) {
this.internalMiddleware = [];
// We do use our middleware only if the user isn't using the manual setup

View file

@ -279,6 +279,7 @@ function buildManifest(
buildFormat: settings.config.build.format,
checkOrigin: settings.config.security?.checkOrigin ?? false,
rewritingEnabled: settings.config.experimental.rewriting,
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
experimentalEnvGetSecretEnabled:
settings.config.experimental.env !== undefined &&
(settings.adapter?.supportedAstroFeatures.envGetSecret ?? 'unsupported') !== 'unsupported',

View file

@ -13,6 +13,7 @@ import { SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugin-manifest.js';
import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js';
import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js';
import { RENDERERS_MODULE_ID } from './plugin-renderers.js';
import { VIRTUAL_ISLAND_MAP_ID } from '../../server-islands/vite-plugin-server-islands.js';
import { getComponentFromVirtualModulePageName, getVirtualModulePageName } from './util.js';
export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry';
@ -249,12 +250,14 @@ function generateSSRCode(adapter: AstroAdapter, middlewareId: string) {
`import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`,
`import * as serverEntrypointModule from '${adapter.serverEntrypoint}';`,
edgeMiddleware ? `` : `import { onRequest as middleware } from '${middlewareId}';`,
`import { serverIslandMap } from '${VIRTUAL_ISLAND_MAP_ID}';`
];
const contents = [
edgeMiddleware ? `const middleware = (_, next) => next()` : '',
`const _manifest = Object.assign(defaultManifest, {`,
` ${pageMap},`,
` serverIslandMap,`,
` renderers,`,
` middleware`,
`});`,

View file

@ -41,12 +41,19 @@ export function createEndpoint(manifest: SSRManifest) {
const request = result.request;
const raw = await request.text();
const data = JSON.parse(raw) as RenderOptions;
const componentId = params.name! as string;
if(!params.name) {
return new Response(null, {
status: 400,
statusText: 'Bad request'
});
}
const componentId = params.name;
const imp = manifest.serverIslandMap?.get(componentId);
if(!imp) {
return new Response('Not found', {
status: 404
return new Response(null, {
status: 404,
statusText: 'Not found'
});
}

View file

@ -2,44 +2,92 @@ import type { AstroPluginMetadata } from '../../vite-plugin-astro/index.js';
import type { AstroSettings, ComponentInstance } from '../../@types/astro.js';
import type { ViteDevServer, Plugin as VitePlugin } from 'vite';
export const VIRTUAL_ISLAND_MAP_ID = '@astro-server-islands';
export const RESOLVED_VIRTUAL_ISLAND_MAP_ID = '\0' + VIRTUAL_ISLAND_MAP_ID;
const serverIslandPlaceholder = '\'$$server-islands$$\'';
export function vitePluginServerIslands({ settings }: { settings: AstroSettings }): VitePlugin {
let viteServer: ViteDevServer | null = null;
const referenceIdMap = new Map<string, string>();
return {
name: 'astro:server-islands',
enforce: 'post',
configureServer(_server) {
viteServer = _server;
},
resolveId(name) {
if(name === VIRTUAL_ISLAND_MAP_ID) {
return RESOLVED_VIRTUAL_ISLAND_MAP_ID;
}
},
load(id) {
if(id === RESOLVED_VIRTUAL_ISLAND_MAP_ID) {
return `export const serverIslandMap = ${serverIslandPlaceholder};`;
}
},
transform(code, id, options) {
if(id.endsWith('.astro')) {
const info = this.getModuleInfo(id);
if(info?.meta) {
const astro = info.meta.astro as AstroPluginMetadata['astro'] | undefined;
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;
for(const comp of astro.serverComponents) {
if(!settings.serverIslandNameMap.has(comp.resolvedPath)) {
let name = comp.localName;
let idx = 1;
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++;
while(true) {
// Name not taken, let's use it.
if(!settings.serverIslandMap.has(name)) {
break;
}
settings.serverIslandNameMap.set(comp.resolvedPath, name);
settings.serverIslandMap.set(name, () => {
return viteServer?.ssrLoadModule(comp.resolvedPath) as any;
// Increment a number onto the name: Avatar -> Avatar1
name += idx++;
}
// Append the name map, for prod
settings.serverIslandNameMap.set(comp.resolvedPath, name);
settings.serverIslandMap.set(name, () => {
return viteServer?.ssrLoadModule(comp.resolvedPath) as any;
});
// Build mode
if(!viteServer) {
let referenceId = this.emitFile({
type: 'chunk',
id: comp.specifier,
importer: id,
name: comp.localName
});
referenceIdMap.set(comp.resolvedPath, referenceId);
}
}
}
}
}
}
},
generateBundle(options, bundles) {
let mapSource = 'new Map([';
for(let [resolvedPath, referenceId] of referenceIdMap) {
const fileName = this.getFileName(referenceId);
const islandName = settings.serverIslandNameMap.get(resolvedPath)!;
mapSource += `\n\t['${islandName}', () => import('./${fileName}')],`
}
mapSource += '\n]);';
referenceIdMap.clear();
for (const [fileName, output] of Object.entries(bundles)) {
if(output.type !== 'chunk') continue;
//console.log("OUTPUT", output.code);
if(output.code.includes(serverIslandPlaceholder)) {
output.code = output.code.replace(serverIslandPlaceholder, mapSource);
}
}
}
}
}

View file

@ -379,7 +379,7 @@ importers:
specifier: ^8.2.6
version: link:../../packages/integrations/node
'@astrojs/react':
specifier: workspace:*
specifier: ^3.6.0
version: link:../../packages/integrations/react
'@astrojs/tailwind':
specifier: ^5.1.0