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:
parent
324f74e55d
commit
df8d5f20c0
16 changed files with 98 additions and 103 deletions
|
@ -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",
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
import Header from '../components/Header.astro';
|
||||
---
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
</body>
|
||||
</html>
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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][];
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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`,
|
||||
`});`,
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue