diff --git a/examples/server-islands/package.json b/examples/server-islands/package.json index 4c2b0591b6..b80361b5e6 100644 --- a/examples/server-islands/package.json +++ b/examples/server-islands/package.json @@ -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", diff --git a/examples/server-islands/src/components/Cart.astro b/examples/server-islands/src/components/Cart.astro deleted file mode 100644 index a1b40b1641..0000000000 --- a/examples/server-islands/src/components/Cart.astro +++ /dev/null @@ -1,17 +0,0 @@ ---- -import Cart from './Cart.js'; - -// Delay for fun -await new Promise(resolve => setTimeout(resolve, 3000)); ---- - -
- I'm a shopping cart - - -
diff --git a/examples/server-islands/src/components/Cart.tsx b/examples/server-islands/src/components/Cart.tsx deleted file mode 100644 index 5a64bb6c77..0000000000 --- a/examples/server-islands/src/components/Cart.tsx +++ /dev/null @@ -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 ( -
Count: {count}
- ) -} diff --git a/examples/server-islands/src/components/Header.astro b/examples/server-islands/src/components/Header.astro deleted file mode 100644 index 2dfc99107b..0000000000 --- a/examples/server-islands/src/components/Header.astro +++ /dev/null @@ -1,24 +0,0 @@ ---- -import Cart from '../components/Cart.astro'; ---- - -
-

My App

-
- -
Loading
-
-
-
diff --git a/examples/server-islands/src/pages/index.astro b/examples/server-islands/src/pages/index.astro index 0fbf0b122c..a36d5df05f 100644 --- a/examples/server-islands/src/pages/index.astro +++ b/examples/server-islands/src/pages/index.astro @@ -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'; --- @@ -19,8 +20,6 @@ import PersonalBar from '../components/PersonalBar.astro'; - - diff --git a/examples/server-islands/src/pages/old-index.astro b/examples/server-islands/src/pages/old-index.astro deleted file mode 100644 index 2d68d4d8b7..0000000000 --- a/examples/server-islands/src/pages/old-index.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -import Header from '../components/Header.astro'; ---- - - - Testing - - -
- - diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index e1385ea687..19bbee1954 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -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, }; } diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 0ef48a9265..b9de9a97a6 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -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); diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index a0fc1eadd6..cc53e4a8aa 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -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 { - 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 { diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 248ab01c38..0c4e671160 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -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][]; }; diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index a23c3ce271..05a7a83e02 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -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 diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 791b33deae..0134aff0f5 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -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', diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 880a4d6a8e..572069cb43 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -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`, `});`, diff --git a/packages/astro/src/core/server-islands/endpoint.ts b/packages/astro/src/core/server-islands/endpoint.ts index 4599b8cf36..c9d3f5866d 100644 --- a/packages/astro/src/core/server-islands/endpoint.ts +++ b/packages/astro/src/core/server-islands/endpoint.ts @@ -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' }); } diff --git a/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts b/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts index d2455d6be3..cad0c77523 100644 --- a/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts +++ b/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts @@ -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(); 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); + } + } } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32c81edf35..6b3d759ba8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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