From 996cd565de5d48accaafea9dc0e5e53280a6fcc0 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 26 May 2023 11:36:52 +0100 Subject: [PATCH 01/20] feat: `serverless` mode --- benchmark/packages/timer/src/server.ts | 4 +- packages/astro/client-base.d.ts | 2 +- packages/astro/src/@types/astro.ts | 23 +- packages/astro/src/core/app/common.ts | 12 +- packages/astro/src/core/app/index.ts | 23 +- packages/astro/src/core/app/node.ts | 4 +- packages/astro/src/core/app/types.ts | 32 +- packages/astro/src/core/build/internal.ts | 6 +- .../astro/src/core/build/plugins/index.ts | 5 +- .../src/core/build/plugins/plugin-pages.ts | 34 +-- .../src/core/build/plugins/plugin-ssr.ts | 279 +++++++++++++++--- packages/astro/src/core/build/plugins/util.ts | 31 ++ packages/astro/src/core/build/static-build.ts | 47 ++- packages/astro/src/core/config/schema.ts | 12 + .../fixtures/ssr-request/astro.config.mjs | 8 + .../ssr-serverless-manifest/astro.config.mjs | 7 + .../ssr-serverless-manifest/package.json | 8 + .../src/pages/index.astro | 17 ++ .../fixtures/ssr-serverless/astro.config.mjs | 7 + .../test/fixtures/ssr-serverless/package.json | 8 + .../src/pages/blog/[slug].astro | 0 .../ssr-serverless/src/pages/blog/about.astro | 0 .../ssr-serverless/src/pages/index.astro | 12 + .../test/ssr-serverless-manifest.test.js | 29 ++ packages/astro/test/ssr-serverless.test.js | 18 ++ packages/astro/test/test-utils.js | 18 ++ .../cloudflare/src/server.advanced.ts | 4 +- .../cloudflare/src/server.directory.ts | 4 +- packages/integrations/deno/src/server.ts | 6 +- packages/integrations/mdx/package.json | 1 + .../netlify/src/netlify-edge-functions.ts | 4 +- .../netlify/src/netlify-functions.ts | 4 +- packages/integrations/node/src/server.ts | 6 +- .../vercel/src/edge/entrypoint.ts | 4 +- .../vercel/src/serverless/entrypoint.ts | 4 +- 35 files changed, 572 insertions(+), 111 deletions(-) create mode 100644 packages/astro/test/fixtures/ssr-request/astro.config.mjs create mode 100644 packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs create mode 100644 packages/astro/test/fixtures/ssr-serverless-manifest/package.json create mode 100644 packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/ssr-serverless/astro.config.mjs create mode 100644 packages/astro/test/fixtures/ssr-serverless/package.json create mode 100644 packages/astro/test/fixtures/ssr-serverless/src/pages/blog/[slug].astro create mode 100644 packages/astro/test/fixtures/ssr-serverless/src/pages/blog/about.astro create mode 100644 packages/astro/test/fixtures/ssr-serverless/src/pages/index.astro create mode 100644 packages/astro/test/ssr-serverless-manifest.test.js create mode 100644 packages/astro/test/ssr-serverless.test.js diff --git a/benchmark/packages/timer/src/server.ts b/benchmark/packages/timer/src/server.ts index 5cfa4ad76822..245f95b28d74 100644 --- a/benchmark/packages/timer/src/server.ts +++ b/benchmark/packages/timer/src/server.ts @@ -1,5 +1,5 @@ import { polyfill } from '@astrojs/webapi'; -import type { SSRManifest } from 'astro'; +import type { SSRBaseManifest } from 'astro'; import { NodeApp } from 'astro/app/node'; import type { IncomingMessage, ServerResponse } from 'http'; @@ -7,7 +7,7 @@ polyfill(globalThis, { exclude: 'window document', }); -export function createExports(manifest: SSRManifest) { +export function createExports(manifest: SSRBaseManifest) { const app = new NodeApp(manifest); return { handler: async (req: IncomingMessage, res: ServerResponse) => { diff --git a/packages/astro/client-base.d.ts b/packages/astro/client-base.d.ts index 37bae7b1c6ae..504b0880cd51 100644 --- a/packages/astro/client-base.d.ts +++ b/packages/astro/client-base.d.ts @@ -188,7 +188,7 @@ declare module '*.mdx' { } declare module 'astro:ssr-manifest' { - export const manifest: import('./dist/@types/astro').SSRManifest; + export const manifest: import('./dist/@types/astro').SSRBaseManifest; } // Everything below are Vite's types (apart from image types, which are in `client.d.ts`) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index fea28d092a87..62540d5523b8 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -44,7 +44,7 @@ export type { ImageQualityPreset, ImageTransform, } from '../assets/types'; -export type { SSRManifest } from '../core/app/types'; +export type { SSRBaseManifest } from '../core/app/types'; export type { AstroCookies } from '../core/cookies'; export interface AstroBuiltinProps { @@ -838,6 +838,27 @@ export interface AstroUserConfig { * ``` */ inlineStylesheets?: 'always' | 'auto' | 'never'; + + /** + * @docs + * @name build.mode + * @type {string} + * @default `'server'` + * @description + * Defines how the SSR should be bundled. SSR code for "server" + * will be built in one single file. + * + * + * + * ```js + * { + * build: { + * mode: 'server' + * } + * } + * ``` + */ + mode?: 'server' | 'serverless'; }; /** diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index 58898b2fe51f..fd0b792f6598 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -1,7 +1,13 @@ import { deserializeRouteData } from '../routing/manifest/serialization.js'; -import type { RouteInfo, SerializedSSRManifest, SSRManifest } from './types'; +import type { + RouteInfo, + SerializedSSRManifest, + SSRBaseManifest, + SSRServerlessManifest, + SSRServerManifest, +} from './types'; -export function deserializeManifest(serializedManifest: SerializedSSRManifest): SSRManifest { +export function deserializeManifest(serializedManifest: SerializedSSRManifest): SSRBaseManifest { const routes: RouteInfo[] = []; for (const serializedRoute of serializedManifest.routes) { routes.push({ @@ -17,7 +23,7 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): const componentMetadata = new Map(serializedManifest.componentMetadata); const clientDirectives = new Map(serializedManifest.clientDirectives); - return { + return { ...serializedManifest, assets, componentMetadata, diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index ae83b301623a..fd77f0340b48 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -4,8 +4,9 @@ import type { MiddlewareResponseHandler, RouteData, SSRElement, + SSRBaseManifest, } from '../../@types/astro'; -import type { RouteInfo, SSRManifest as Manifest } from './types'; +import type { RouteInfo, SSRServerlessManifest, SSRServerManifest } from './types'; import mime from 'mime'; import type { SinglePageBuiltModule } from '../build/types'; @@ -41,7 +42,7 @@ export interface MatchOptions { export class App { #env: Environment; - #manifest: Manifest; + #manifest: SSRBaseManifest; #manifestData: ManifestData; #routeDataToRouteInfo: Map; #encoder = new TextEncoder(); @@ -52,7 +53,17 @@ export class App { #base: string; #baseWithoutTrailingSlash: string; - constructor(manifest: Manifest, streaming = true) { + async #retrievePage(routeData: RouteData) { + if (isSsrServerManifest(this.#manifest)) { + const pageModule = await this.#manifest.pageMap.get(routeData.component)!(); + return await pageModule.page(); + } else { + const pageModule = await this.#manifest.pageModule; + return await pageModule.page(); + } + } + + constructor(manifest: SSRBaseManifest, streaming = true) { this.#manifest = manifest; this.#manifestData = { routes: manifest.routes.map((route) => route.routeData), @@ -139,6 +150,7 @@ export class App { } let mod = await this.#getModuleForRoute(routeData); + let mod = await this.#retrievePage(routeData); if (routeData.type === 'page' || routeData.type === 'redirect') { let response = await this.#renderPage(request, routeData, mod, defaultStatus); @@ -147,6 +159,7 @@ export class App { if (response.status === 500 || response.status === 404) { const errorRouteData = matchRoute('/' + response.status, this.#manifestData); if (errorRouteData && errorRouteData.route !== routeData.route) { + mod = await this.#retrievePage(errorPageData); mod = await this.#getModuleForRoute(errorRouteData); try { let errorResponse = await this.#renderPage( @@ -319,3 +332,7 @@ export class App { } } } + +function isSsrServerManifest(manifest: any): manifest is SSRServerManifest { + return typeof manifest.pageMap !== 'undefined'; +} diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index 40b7b4e7ce6e..ef4c2d308983 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -1,5 +1,5 @@ import type { RouteData } from '../../@types/astro'; -import type { SerializedSSRManifest, SSRManifest } from './types'; +import type { SerializedSSRManifest, SSRBaseManifest } from './types'; import * as fs from 'fs'; import { IncomingMessage } from 'http'; @@ -90,7 +90,7 @@ export class NodeApp extends App { } } -export async function loadManifest(rootFolder: URL): Promise { +export async function loadManifest(rootFolder: URL): Promise { const manifestFile = new URL('./manifest.json', rootFolder); const rawManifest = await fs.promises.readFile(manifestFile, 'utf-8'); const serializedManifest: SerializedSSRManifest = JSON.parse(rawManifest); diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 1283f1a10f26..f0c2e183f4c5 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -30,16 +30,38 @@ export interface RouteInfo { export type SerializedRouteInfo = Omit & { routeData: SerializedRouteData; }; + type ImportComponentInstance = () => Promise; -export interface SSRManifest { +export type SSRBaseManifest = SSRServerManifest | SSRServerlessManifest; + +export type SSRServerManifest = { adapterName: string; routes: RouteInfo[]; site?: string; base?: string; assetsPrefix?: string; markdown: MarkdownRenderingOptions; + renderers: SSRLoadedRenderer[]; + /** + * Map of directive name (e.g. `load`) to the directive script code + */ + clientDirectives: Map; + entryModules: Record; + assets: Set; + componentMetadata: SSRResult['componentMetadata']; + middleware?: AstroMiddlewareInstance; + pageModule?: undefined; pageMap: Map; +}; + +export type SSRServerlessManifest = { + adapterName: string; + routes: RouteInfo[]; + site?: string; + base?: string; + assetsPrefix?: string; + markdown: MarkdownRenderingOptions; renderers: SSRLoadedRenderer[]; /** * Map of directive name (e.g. `load`) to the directive script code @@ -48,10 +70,12 @@ export interface SSRManifest { entryModules: Record; assets: Set; componentMetadata: SSRResult['componentMetadata']; -} + pageModule: SinglePageBuiltModule; + pageMap?: undefined; +}; export type SerializedSSRManifest = Omit< - SSRManifest, + SSRBaseManifest, 'routes' | 'assets' | 'componentMetadata' | 'clientDirectives' > & { routes: SerializedRouteInfo[]; @@ -61,6 +85,6 @@ export type SerializedSSRManifest = Omit< }; export type AdapterCreateExports = ( - manifest: SSRManifest, + manifest: SSRBaseManifest, args?: T ) => Record; diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index 4cf40cb9ad83..cc0e3faaad5f 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -4,11 +4,11 @@ import type { PageOptions } from '../../vite-plugin-astro/types'; import { prependForwardSlash, removeFileExtension } from '../path.js'; import { viteID } from '../util.js'; import { - ASTRO_PAGE_EXTENSION_POST_PATTERN, - ASTRO_PAGE_MODULE_ID, + ASTRO_PAGE_MODULE_ID , getVirtualModulePageIdFromPath, } from './plugins/plugin-pages.js'; import type { PageBuildData, StylesheetAsset, ViteID } from './types'; +import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; export interface BuildInternals { /** @@ -84,6 +84,7 @@ export interface BuildInternals { staticFiles: Set; // The SSR entry chunk. Kept in internals to share between ssr/client build steps ssrEntryChunk?: Rollup.OutputChunk; + ssrServerlessEntryChunks: Map; componentMetadata: SSRResult['componentMetadata']; } @@ -114,6 +115,7 @@ export function createBuildInternals(): BuildInternals { discoveredScripts: new Set(), staticFiles: new Set(), componentMetadata: new Map(), + ssrServerlessEntryChunks: new Map(), }; } diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts index f6fcacfb1b7d..c64fe66f5430 100644 --- a/packages/astro/src/core/build/plugins/index.ts +++ b/packages/astro/src/core/build/plugins/index.ts @@ -11,7 +11,7 @@ import { pluginMiddleware } from './plugin-middleware.js'; import { pluginPages } from './plugin-pages.js'; import { pluginPrerender } from './plugin-prerender.js'; import { pluginRenderers } from './plugin-renderers.js'; -import { pluginSSR } from './plugin-ssr.js'; +import { pluginSSRServer, pluginSSRServerless } from './plugin-ssr.js'; export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) { register(pluginComponentEntry(internals)); @@ -26,5 +26,6 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP register(pluginPrerender(options, internals)); register(astroConfigBuildPlugin(options, internals)); register(pluginHoistedScripts(options, internals)); - register(pluginSSR(options, internals)); + register(pluginSSRServer(options, internals)); + register(pluginSSRServerless(options, internals)); } diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index c6f89a558bee..b9cfb8ee6724 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -1,4 +1,4 @@ -import { extname } from 'node:path'; +import { getVirtualModulePageNameFromPath, getPathFromVirtualModulePageName } from './util.js'; import type { Plugin as VitePlugin } from 'vite'; import { routeIsRedirect } from '../../redirects/index.js'; import { addRollupInput } from '../add-rollup-input.js'; @@ -9,26 +9,7 @@ import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; export const ASTRO_PAGE_MODULE_ID = '@astro-page:'; -export const ASTRO_PAGE_RESOLVED_MODULE_ID = '\0@astro-page:'; - -// This is an arbitrary string that we are going to replace the dot of the extension -export const ASTRO_PAGE_EXTENSION_POST_PATTERN = '@_@'; - -/** - * 1. We add a fixed prefix, which is used as virtual module naming convention; - * 2. We replace the dot that belongs extension with an arbitrary string. - * - * @param path - */ -export function getVirtualModulePageNameFromPath(path: string) { - // we mask the extension, so this virtual file - // so rollup won't trigger other plugins in the process - const extension = extname(path); - return `${ASTRO_PAGE_MODULE_ID}${path.replace( - extension, - extension.replace('.', ASTRO_PAGE_EXTENSION_POST_PATTERN) - )}`; -} +export const ASTRO_PAGE_RESOLVED_MODULE_ID = '\0' + ASTRO_PAGE_MODULE_ID; export function getVirtualModulePageIdFromPath(path: string) { const name = getVirtualModulePageNameFromPath(path); @@ -47,7 +28,7 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V if (routeIsRedirect(pageData.route)) { continue; } - inputs.add(getVirtualModulePageNameFromPath(path)); + inputs.add(getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path)); } return addRollupInput(options, Array.from(inputs)); @@ -64,13 +45,8 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V if (id.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) { const imports: string[] = []; const exports: string[] = []; - - // we remove the module name prefix from id, this will result into a string that will start with "src/..." - const pageName = id.slice(ASTRO_PAGE_RESOLVED_MODULE_ID.length); - // We replaced the `.` of the extension with ASTRO_PAGE_EXTENSION_POST_PATTERN, let's replace it back - const pageData = internals.pagesByComponent.get( - `${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}` - ); + const pageName = getPathFromVirtualModulePageName(ASTRO_PAGE_RESOLVED_MODULE_ID, id); + const pageData = internals.pagesByComponent.get(pageName); if (pageData) { const resolvedPage = await this.resolve(pageData.moduleSpecifier); if (resolvedPage) { diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 3c2825e4c63c..8eab1e4e861c 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -1,7 +1,7 @@ import glob from 'fast-glob'; import { fileURLToPath } from 'url'; import type { Plugin as VitePlugin } from 'vite'; -import type { AstroAdapter } from '../../../@types/astro'; +import type { AstroAdapter, AstroConfig } from '../../../@types/astro'; import { runHookBuildSsr } from '../../../integrations/index.js'; import { isServerLikeOutput } from '../../../prerender/utils.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js'; @@ -15,20 +15,24 @@ import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin'; import type { StaticBuildOptions } from '../types'; import { getVirtualModulePageNameFromPath } from './plugin-pages.js'; +import type { OutputChunk, StaticBuildOptions } from '../types'; +import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; +import { getPathFromVirtualModulePageName, getVirtualModulePageNameFromPath } from './util.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; +import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js'; export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry'; const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID; const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g'); -function vitePluginSSR( +function vitePluginSSRServer( internals: BuildInternals, adapter: AstroAdapter, options: StaticBuildOptions ): VitePlugin { return { - name: '@astrojs/vite-plugin-astro-ssr', + name: '@astrojs/vite-plugin-astro-ssr-server', enforce: 'post', options(opts) { return addRollupInput(opts, [SSR_VIRTUAL_MODULE_ID]); @@ -54,7 +58,7 @@ function vitePluginSSR( if (routeIsRedirect(pageData.route)) { continue; } - const virtualModuleName = getVirtualModulePageNameFromPath(path); + const virtualModuleName = getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path); let module = await this.resolve(virtualModuleName); if (module) { const variable = `_page${i}`; @@ -71,12 +75,224 @@ function vitePluginSSR( contents.push(`const pageMap = new Map([${pageMap.join(',')}]);`); exports.push(`export { pageMap }`); - const content = `import * as adapter from '${adapter.serverEntrypoint}'; + const ssrCode = generateSSRCode(options.settings.config, adapter); + imports.push(...ssrCode.imports); + contents.push(...ssrCode.contents); + return `${imports.join('\n')}${contents.join('\n')}${exports.join('\n')}`; + } + return void 0; + }, + async generateBundle(_opts, bundle) { + // Add assets from this SSR chunk as well. + for (const [_chunkName, chunk] of Object.entries(bundle)) { + if (chunk.type === 'asset') { + internals.staticFiles.add(chunk.fileName); + } + } + + for (const [chunkName, chunk] of Object.entries(bundle)) { + if (chunk.type === 'asset') { + continue; + } + if (chunk.modules[RESOLVED_SSR_VIRTUAL_MODULE_ID]) { + internals.ssrEntryChunk = chunk; + delete bundle[chunkName]; + } + } + }, + }; +} + +export function pluginSSRServer( + options: StaticBuildOptions, + internals: BuildInternals +): AstroBuildPlugin { + const ssr = + options.settings.config.output === 'server' || isHybridOutput(options.settings.config); + return { + build: 'ssr', + hooks: { + 'build:before': () => { + let vitePlugin = + // config.build object is optional, so we check NOT EQUAL against "serverless" instead + ssr && options.settings.config.build?.mode !== 'serverless' + ? vitePluginSSRServer(internals, options.settings.adapter!, options) + : undefined; + + return { + enforce: 'after-user-plugins', + vitePlugin, + }; + }, + 'build:post': async ({ mutate }) => { + if (!ssr) { + return; + } + + if (options.settings.config.build?.mode === 'serverless') { + return; + } + + if (!internals.ssrEntryChunk) { + throw new Error(`Did not generate an entry chunk for SSR`); + } + // Mutate the filename + internals.ssrEntryChunk.fileName = options.settings.config.build.serverEntry; + + const code = await injectManifest(options, internals, internals.ssrEntryChunk); + mutate(internals.ssrEntryChunk, 'server', code); + }, + }, + }; +} + +export const SERVERLESS_MODULE_ID = '@astro-page-serverless:'; +export const RESOLVED_SERVERLESS_MODULE_ID = '\0@astro-page-serverless:'; + +function vitePluginSSRServerless( + internals: BuildInternals, + adapter: AstroAdapter, + options: StaticBuildOptions +): VitePlugin { + return { + name: '@astrojs/vite-plugin-astro-ssr-serverless', + enforce: 'post', + options(opts) { + if (options.settings.config.build?.mode === 'serverless') { + const inputs: Set = new Set(); + + for (const path of Object.keys(options.allPages)) { + inputs.add(getVirtualModulePageNameFromPath(SERVERLESS_MODULE_ID, path)); + } + + return addRollupInput(opts, Array.from(inputs)); + } + }, + resolveId(id) { + if (id.startsWith(SERVERLESS_MODULE_ID)) { + return '\0' + id; + } + }, + async load(id) { + if (id.startsWith(RESOLVED_SERVERLESS_MODULE_ID)) { + const { + settings: { config }, + allPages, + } = options; + const imports: string[] = []; + const contents: string[] = []; + const exports: string[] = []; + + const path = getPathFromVirtualModulePageName(RESOLVED_SERVERLESS_MODULE_ID, id); + const virtualModuleName = getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path); + let module = await this.resolve(virtualModuleName); + if (module) { + // we need to use the non-resolved ID in order to resolve correctly the virtual module + imports.push(`import * as pageModule from "${virtualModuleName}";`); + } + + const ssrCode = generateSSRCode(options.settings.config, adapter); + imports.push(...ssrCode.imports); + contents.push(...ssrCode.contents); + + return `${imports.join('\n')}${contents.join('\n')}${exports.join('\n')}`; + } + return void 0; + }, + async generateBundle(_opts, bundle) { + // Add assets from this SSR chunk as well. + for (const [_chunkName, chunk] of Object.entries(bundle)) { + if (chunk.type === 'asset') { + internals.staticFiles.add(chunk.fileName); + } + } + + for (const [chunkName, chunk] of Object.entries(bundle)) { + if (chunk.type === 'asset') { + continue; + } + let shouldDeleteBundle = false; + for (const moduleKey of Object.keys(chunk.modules)) { + if (moduleKey.startsWith(RESOLVED_SERVERLESS_MODULE_ID)) { + internals.ssrServerlessEntryChunks.set(moduleKey, chunk); + shouldDeleteBundle = true; + } + } + if (shouldDeleteBundle) { + delete bundle[chunkName]; + } + // if (chunk.modules[RESOLVED_SSR_VIRTUAL_MODULE_ID]) { + // } + } + }, + }; +} + +export function pluginSSRServerless( + options: StaticBuildOptions, + internals: BuildInternals +): AstroBuildPlugin { + const ssr = + options.settings.config.output === 'server' || isHybridOutput(options.settings.config); + return { + build: 'ssr', + hooks: { + 'build:before': () => { + let vitePlugin = + ssr && options.settings.config.build.mode === 'serverless' + ? vitePluginSSRServerless(internals, options.settings.adapter!, options) + : undefined; + + return { + enforce: 'after-user-plugins', + vitePlugin, + }; + }, + 'build:post': async ({ mutate }) => { + if (!ssr) { + return; + } + if (options.settings.config.build?.mode === 'server') { + return; + } + + if (internals.ssrServerlessEntryChunks.size === 0) { + throw new Error(`Did not generate an entry chunk for SSR serverless`); + } + // TODO: check this commented code + // Mutate the filename + // internals.ssrEntryChunk.fileName = options.settings.config.build.serverEntry; + + for (const [moduleName, chunk] of internals.ssrServerlessEntryChunks) { + const code = await injectManifest(options, internals, chunk); + mutate(chunk, 'server', code); + } + }, + }, + }; +} + +function generateSSRCode(config: AstroConfig, adapter: AstroAdapter) { + const imports: string[] = []; + const contents: string[] = []; + let middleware; + if (config.experimental?.middleware === true) { + imports.push(`import * as _middleware from "${MIDDLEWARE_MODULE_ID}";`); + middleware = 'middleware: _middleware'; + } + let pageMap; + if (config.build.mode === 'serverless') { + pageMap = 'pageModule'; + } else { + pageMap = 'pageMap'; + } + + contents.push(`import * as adapter from '${adapter.serverEntrypoint}'; import { renderers } from '${RENDERERS_MODULE_ID}'; import { deserializeManifest as _deserializeManifest } from 'astro/app'; import { _privateSetManifestDontUseThis } from 'astro:ssr-manifest'; const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), { - pageMap, + ${pageMap}, renderers, }); _privateSetManifestDontUseThis(_manifest); @@ -101,35 +317,32 @@ export { _default as default };`; const _start = 'start'; if(_start in adapter) { adapter[_start](_manifest, _args); -}`; - return `${imports.join('\n')}${contents.join('\n')}${content}${exports.join('\n')}`; - } - return void 0; - }, - async generateBundle(_opts, bundle) { - // Add assets from this SSR chunk as well. - for (const [_chunkName, chunk] of Object.entries(bundle)) { - if (chunk.type === 'asset') { - internals.staticFiles.add(chunk.fileName); - } - } - - for (const [chunkName, chunk] of Object.entries(bundle)) { - if (chunk.type === 'asset') { - continue; - } - if (chunk.modules[RESOLVED_SSR_VIRTUAL_MODULE_ID]) { - internals.ssrEntryChunk = chunk; - delete bundle[chunkName]; - } - } - }, +}`); + return { + imports, + contents, }; } -export async function injectManifest(buildOpts: StaticBuildOptions, internals: BuildInternals) { - if (!internals.ssrEntryChunk) { - throw new Error(`Did not generate an entry chunk for SSR`); +/** + * It injects the manifest in the given output rollup chunk. It returns the new emitted code + * @param buildOpts + * @param internals + * @param chunk + */ +export async function injectManifest( + buildOpts: StaticBuildOptions, + internals: BuildInternals, + chunk: Readonly +) { + if (buildOpts.settings.config.build.mode === 'serverless') { + if (internals.ssrServerlessEntryChunks.size === 0) { + throw new Error(`Did not generate an entry chunk for SSR in serverless mode`); + } + } else { + if (!internals.ssrEntryChunk) { + throw new Error(`Did not generate an entry chunk for SSR`); + } } // Add assets from the client build. @@ -150,7 +363,6 @@ export async function injectManifest(buildOpts: StaticBuildOptions, internals: B logging: buildOpts.logging, }); - const chunk = internals.ssrEntryChunk; const code = chunk.code; return code.replace(replaceExp, () => { @@ -254,7 +466,6 @@ function buildManifest( base: settings.config.base, assetsPrefix: settings.config.build.assetsPrefix, markdown: settings.config.markdown, - pageMap: null as any, componentMetadata: Array.from(internals.componentMetadata), renderers: [], clientDirectives: Array.from(settings.clientDirectives), diff --git a/packages/astro/src/core/build/plugins/util.ts b/packages/astro/src/core/build/plugins/util.ts index 50f5e07059b8..1fbf0fcdc133 100644 --- a/packages/astro/src/core/build/plugins/util.ts +++ b/packages/astro/src/core/build/plugins/util.ts @@ -1,4 +1,5 @@ import type { Plugin as VitePlugin } from 'vite'; +import { extname } from 'node:path'; // eslint-disable-next-line @typescript-eslint/ban-types type OutputOptionsHook = Extract; @@ -38,3 +39,33 @@ export function extendManualChunks(outputOptions: OutputOptions, hooks: ExtendMa return null; }; } + +// This is an arbitrary string that we are going to replace the dot of the extension +export const ASTRO_PAGE_EXTENSION_POST_PATTERN = '@_@'; + +/** + * 1. We add a fixed prefix, which is used as virtual module naming convention; + * 2. We replace the dot that belongs extension with an arbitrary string. + * + * @param virtualModulePrefix + * @param path + */ +export function getVirtualModulePageNameFromPath(virtualModulePrefix: string, path: string) { + // we mask the extension, so this virtual file + // so rollup won't trigger other plugins in the process + const extension = extname(path); + return `${virtualModulePrefix}${path.replace( + extension, + extension.replace('.', ASTRO_PAGE_EXTENSION_POST_PATTERN) + )}`; +} + +/** + * + * @param virtualModulePrefix + * @param id + */ +export function getPathFromVirtualModulePageName(virtualModulePrefix: string, id: string) { + const pageName = id.slice(virtualModulePrefix.length); + return pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.'); +} diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 327ef5f16035..1c0575ac464c 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -25,14 +25,13 @@ import { trackPageData } from './internal.js'; import { createPluginContainer, type AstroBuildPluginContainer } from './plugin.js'; import { registerAllPlugins } from './plugins/index.js'; import { MIDDLEWARE_MODULE_ID } from './plugins/plugin-middleware.js'; -import { - ASTRO_PAGE_EXTENSION_POST_PATTERN, - ASTRO_PAGE_RESOLVED_MODULE_ID, -} from './plugins/plugin-pages.js'; +import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js'; import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js'; -import { SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js'; +import { RESOLVED_SERVERLESS_MODULE_ID, SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js'; import type { AllPagesData, PageBuildData, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; +import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; +import { extname } from 'node:path'; export async function viteBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; @@ -122,14 +121,14 @@ export async function staticBuild(opts: StaticBuildOptions, internals: BuildInte case settings.config.output === 'static': { settings.timer.start('Static generate'); await generatePages(opts, internals); - await cleanServerOutput(opts); + // await cleanServerOutput(opts); settings.timer.end('Static generate'); return; } case isServerLikeOutput(settings.config): { settings.timer.start('Server generate'); await generatePages(opts, internals); - await cleanStaticOutput(opts, internals); + // await cleanStaticOutput(opts, internals); info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`); await ssrMoveAssets(opts); settings.timer.end('Server generate'); @@ -176,7 +175,12 @@ async function ssrBuild( ...viteConfig.build?.rollupOptions?.output, entryFileNames(chunkInfo) { if (chunkInfo.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) { - return makeAstroPageEntryPointFileName(chunkInfo.facadeModuleId, allPages); + return makeAstroPageEntryPointFileName( + ASTRO_PAGE_RESOLVED_MODULE_ID, + chunkInfo.facadeModuleId + ); + } else if (chunkInfo.facadeModuleId?.startsWith(RESOLVED_SERVERLESS_MODULE_ID)) { + return makeServerlessEntryPointFileName(chunkInfo.facadeModuleId, opts); } else if (chunkInfo.facadeModuleId === MIDDLEWARE_MODULE_ID) { return 'middleware.mjs'; } else if (chunkInfo.facadeModuleId === SSR_VIRTUAL_MODULE_ID) { @@ -430,11 +434,34 @@ async function ssrMoveAssets(opts: StaticBuildOptions) { * @param facadeModuleId string * @param pages AllPagesData */ -function makeAstroPageEntryPointFileName(facadeModuleId: string, pages: AllPagesData) { +function makeAstroPageEntryPointFileName(prefix: string, facadeModuleId: string, pages: AllPagesData) { const pageModuleId = facadeModuleId - .replace(ASTRO_PAGE_RESOLVED_MODULE_ID, '') + .replace(prefix, '') .replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.'); let name = pages[pageModuleId]?.route?.route ?? pageModuleId; if (name.endsWith('/')) name += 'index'; return `pages${name.replaceAll('[', '_').replaceAll(']', '_').replaceAll('...', '---')}.mjs`; } + +/** + * This function attempts + * @param facadeModuleId + * @param opts + */ +function makeServerlessEntryPointFileName(facadeModuleId: string, opts: StaticBuildOptions) { + const filePath = `${makeAstroPageEntryPointFileName( + RESOLVED_SERVERLESS_MODULE_ID, + facadeModuleId + )}`; + + const pathComponents = filePath.split(path.sep); + const lastPathComponent = pathComponents.pop(); + if (lastPathComponent) { + const extension = extname(lastPathComponent); + if (extension.length > 0) { + const newFileName = `${opts.settings.config.build.serverlessEntryPrefix}.${lastPathComponent}`; + return [...pathComponents, newFileName].join(path.sep); + } + } + return filePath; +} diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 3edabc5d54e3..e03878c1d611 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -24,6 +24,8 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { serverEntry: 'entry.mjs', redirects: true, inlineStylesheets: 'never', + serverlessEntryPrefix: 'entry', + mode: 'server', }, compressHTML: false, server: { @@ -120,6 +122,11 @@ export const AstroConfigSchema = z.object({ .enum(['always', 'auto', 'never']) .optional() .default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets), + serverlessEntryPrefix: z + .string() + .optional() + .default(ASTRO_CONFIG_DEFAULTS.build.serverlessEntryPrefix), + mode: z.enum(['server', 'serverless']).optional().default(ASTRO_CONFIG_DEFAULTS.build.server), }) .optional() .default({}), @@ -279,6 +286,11 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) { .enum(['always', 'auto', 'never']) .optional() .default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets), + serverlessEntryPrefix: z + .string() + .optional() + .default(ASTRO_CONFIG_DEFAULTS.build.serverlessEntryPrefix), + mode: z.enum(['server', 'serverless']).optional().default(ASTRO_CONFIG_DEFAULTS.build.mode), }) .optional() .default({}), diff --git a/packages/astro/test/fixtures/ssr-request/astro.config.mjs b/packages/astro/test/fixtures/ssr-request/astro.config.mjs new file mode 100644 index 000000000000..d5d304da91be --- /dev/null +++ b/packages/astro/test/fixtures/ssr-request/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + build: { + mode: "serverless" + } +}); diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs b/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs new file mode 100644 index 000000000000..59ffc57a69a2 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from 'astro/config'; +export default defineConfig({ + build: { + mode: "serverless" + }, + output: "server" +}) \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/package.json b/packages/astro/test/fixtures/ssr-serverless-manifest/package.json new file mode 100644 index 000000000000..bd1a5dd6bf37 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-serverless-manifest/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/ssr-serverless-manifest", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/index.astro b/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/index.astro new file mode 100644 index 000000000000..f189e711c19a --- /dev/null +++ b/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/index.astro @@ -0,0 +1,17 @@ +--- +import { manifest } from 'astro:ssr-manifest'; +--- + + + Testing + + + +

Testing

+
+ + diff --git a/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs b/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs new file mode 100644 index 000000000000..59ffc57a69a2 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from 'astro/config'; +export default defineConfig({ + build: { + mode: "serverless" + }, + output: "server" +}) \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-serverless/package.json b/packages/astro/test/fixtures/ssr-serverless/package.json new file mode 100644 index 000000000000..f4e328bd35f6 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-serverless/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/ssr-serverless", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/ssr-serverless/src/pages/blog/[slug].astro b/packages/astro/test/fixtures/ssr-serverless/src/pages/blog/[slug].astro new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/astro/test/fixtures/ssr-serverless/src/pages/blog/about.astro b/packages/astro/test/fixtures/ssr-serverless/src/pages/blog/about.astro new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/astro/test/fixtures/ssr-serverless/src/pages/index.astro b/packages/astro/test/fixtures/ssr-serverless/src/pages/index.astro new file mode 100644 index 000000000000..ff484b4bb438 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-serverless/src/pages/index.astro @@ -0,0 +1,12 @@ + + + Testing + + + + + diff --git a/packages/astro/test/ssr-serverless-manifest.test.js b/packages/astro/test/ssr-serverless-manifest.test.js new file mode 100644 index 000000000000..98588ba5aee8 --- /dev/null +++ b/packages/astro/test/ssr-serverless-manifest.test.js @@ -0,0 +1,29 @@ +import { expect } from 'chai'; +import { loadFixture } from './test-utils.js'; +import testAdapter from './test-adapter.js'; +import * as cheerio from 'cheerio'; + +describe('astro:ssr-manifest', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/ssr-serverless-manifest/', + output: 'server', + adapter: testAdapter(), + }); + await fixture.build(); + }); + + it('works', async () => { + const pagePath = 'pages/index.astro'; + const app = await fixture.loadServerlessEntrypointApp(pagePath); + const request = new Request('http://example.com/'); + const response = await app.render(request); + const html = await response.text(); + + const $ = cheerio.load(html); + expect($('#assets').text()).to.equal('["/_astro/index.a8a337e4.css"]'); + }); +}); diff --git a/packages/astro/test/ssr-serverless.test.js b/packages/astro/test/ssr-serverless.test.js new file mode 100644 index 000000000000..de14d784c69c --- /dev/null +++ b/packages/astro/test/ssr-serverless.test.js @@ -0,0 +1,18 @@ +import { loadFixture } from './test-utils.js'; +import { expect } from 'chai'; +import testAdapter from './test-adapter.js'; + +describe('SSR serverless support', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/ssr-serverless/', + adapter: testAdapter(), + }); + await fixture.build(); + }); + + it('SSR pages require zero config', async () => {}); +}); diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index b618f8593a42..532776159055 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -13,6 +13,8 @@ import dev from '../dist/core/dev/index.js'; import { nodeLogDestination } from '../dist/core/logger/node.js'; import preview from '../dist/core/preview/index.js'; import { check } from '../dist/cli/check/index.js'; +import path from 'path'; +import { extname } from 'node:path'; // polyfill WebAPIs to globalThis for Node v12, Node v14, and Node v16 polyfill(globalThis, { @@ -245,6 +247,22 @@ export async function loadFixture(inlineConfig) { app.manifest = manifest; return app; }, + loadServerlessEntrypointApp: async (pagePath, streaming) => { + const pathComponents = pagePath.split(path.sep); + const lastPathComponent = pathComponents.pop(); + if (lastPathComponent) { + const extension = extname(lastPathComponent); + if (extension.length > 0) { + const newFileName = `entry.${lastPathComponent}`; + pagePath = `${[...pathComponents, newFileName].join(path.sep)}.mjs`; + } + } + const url = new URL(`./server/${pagePath}?id=${fixtureId}`, config.outDir); + const { createApp, manifest, middleware } = await import(url); + const app = createApp(streaming); + app.manifest = manifest; + return app; + }, editFile: async (filePath, newContentsOrCallback) => { const fileUrl = new URL(filePath.replace(/^\//, ''), config.root); const contents = await fs.promises.readFile(fileUrl, 'utf-8'); diff --git a/packages/integrations/cloudflare/src/server.advanced.ts b/packages/integrations/cloudflare/src/server.advanced.ts index 5335db8dc51a..15c359f69422 100644 --- a/packages/integrations/cloudflare/src/server.advanced.ts +++ b/packages/integrations/cloudflare/src/server.advanced.ts @@ -1,5 +1,5 @@ import type { ExecutionContext, Request as CFRequest } from '@cloudflare/workers-types'; -import type { SSRManifest } from 'astro'; +import type { SSRBaseManifest } from 'astro'; import { App } from 'astro/app'; import { getProcessEnvProxy, isNode } from './util.js'; @@ -12,7 +12,7 @@ type Env = { name: string; }; -export function createExports(manifest: SSRManifest) { +export function createExports(manifest: SSRBaseManifest) { const app = new App(manifest); const fetch = async (request: Request & CFRequest, env: Env, context: ExecutionContext) => { diff --git a/packages/integrations/cloudflare/src/server.directory.ts b/packages/integrations/cloudflare/src/server.directory.ts index da23605573e3..22497bd37ce2 100644 --- a/packages/integrations/cloudflare/src/server.directory.ts +++ b/packages/integrations/cloudflare/src/server.directory.ts @@ -1,5 +1,5 @@ import type { EventContext, Request as CFRequest } from '@cloudflare/workers-types'; -import type { SSRManifest } from 'astro'; +import type { SSRBaseManifest } from 'astro'; import { App } from 'astro/app'; import { getProcessEnvProxy, isNode } from './util.js'; @@ -7,7 +7,7 @@ if (!isNode) { process.env = getProcessEnvProxy(); } -export function createExports(manifest: SSRManifest) { +export function createExports(manifest: SSRBaseManifest) { const app = new App(manifest); const onRequest = async ({ diff --git a/packages/integrations/deno/src/server.ts b/packages/integrations/deno/src/server.ts index 08d967065296..904113fb62e9 100644 --- a/packages/integrations/deno/src/server.ts +++ b/packages/integrations/deno/src/server.ts @@ -1,5 +1,5 @@ // Normal Imports -import type { SSRManifest } from 'astro'; +import type { SSRBaseManifest } from 'astro'; import { App } from 'astro/app'; // @ts-expect-error @@ -25,7 +25,7 @@ async function* getPrerenderedFiles(clientRoot: URL): AsyncGenerator { } } -export function start(manifest: SSRManifest, options: Options) { +export function start(manifest: SSRBaseManifest, options: Options) { if (options.start === false) { return; } @@ -97,7 +97,7 @@ export function start(manifest: SSRManifest, options: Options) { console.error(`Server running on port ${port}`); } -export function createExports(manifest: SSRManifest, options: Options) { +export function createExports(manifest: SSRBaseManifest, options: Options) { const app = new App(manifest); return { async stop() { diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index 41682a6bd0fe..e636ca20eb44 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -37,6 +37,7 @@ "@astrojs/markdown-remark": "^2.2.1", "@astrojs/prism": "^2.1.2", "@mdx-js/mdx": "^2.3.0", + "@mdx-js/rollup": "^2.3.0", "acorn": "^8.8.0", "es-module-lexer": "^1.1.1", "estree-util-visit": "^1.2.0", diff --git a/packages/integrations/netlify/src/netlify-edge-functions.ts b/packages/integrations/netlify/src/netlify-edge-functions.ts index 4a6d3674c0e4..b26b870f8a77 100644 --- a/packages/integrations/netlify/src/netlify-edge-functions.ts +++ b/packages/integrations/netlify/src/netlify-edge-functions.ts @@ -1,10 +1,10 @@ import type { Context } from '@netlify/edge-functions'; -import type { SSRManifest } from 'astro'; +import type { SSRBaseManifest } from 'astro'; import { App } from 'astro/app'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); -export function createExports(manifest: SSRManifest) { +export function createExports(manifest: SSRBaseManifest) { const app = new App(manifest); const handler = async (request: Request, context: Context): Promise => { diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts index 915f72955105..09d5baa36bc2 100644 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ b/packages/integrations/netlify/src/netlify-functions.ts @@ -1,6 +1,6 @@ import { polyfill } from '@astrojs/webapi'; import { builder, type Handler } from '@netlify/functions'; -import type { SSRManifest } from 'astro'; +import type { SSRBaseManifest } from 'astro'; import { App } from 'astro/app'; polyfill(globalThis, { @@ -18,7 +18,7 @@ function parseContentType(header?: string) { const clientAddressSymbol = Symbol.for('astro.clientAddress'); -export const createExports = (manifest: SSRManifest, args: Args) => { +export const createExports = (manifest: SSRBaseManifest, args: Args) => { const app = new App(manifest); const builders = args.builders ?? false; diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts index 98f5cd14bd4f..98c12ee6ea8e 100644 --- a/packages/integrations/node/src/server.ts +++ b/packages/integrations/node/src/server.ts @@ -1,5 +1,5 @@ import { polyfill } from '@astrojs/webapi'; -import type { SSRManifest } from 'astro'; +import type { SSRBaseManifest } from 'astro'; import { NodeApp } from 'astro/app/node'; import middleware from './nodeMiddleware.js'; import startServer from './standalone.js'; @@ -9,7 +9,7 @@ polyfill(globalThis, { exclude: 'window document', }); -export function createExports(manifest: SSRManifest, options: Options) { +export function createExports(manifest: SSRBaseManifest, options: Options) { const app = new NodeApp(manifest); return { handler: middleware(app, options.mode), @@ -17,7 +17,7 @@ export function createExports(manifest: SSRManifest, options: Options) { }; } -export function start(manifest: SSRManifest, options: Options) { +export function start(manifest: SSRBaseManifest, options: Options) { if (options.mode !== 'standalone' || process.env.ASTRO_NODE_AUTOSTART === 'disabled') { return; } diff --git a/packages/integrations/vercel/src/edge/entrypoint.ts b/packages/integrations/vercel/src/edge/entrypoint.ts index a9870ef2bfc2..e49dee483dd6 100644 --- a/packages/integrations/vercel/src/edge/entrypoint.ts +++ b/packages/integrations/vercel/src/edge/entrypoint.ts @@ -4,12 +4,12 @@ import './shim.js'; // Normal Imports -import type { SSRManifest } from 'astro'; +import type { SSRBaseManifest } from 'astro'; import { App } from 'astro/app'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); -export function createExports(manifest: SSRManifest) { +export function createExports(manifest: SSRBaseManifest) { const app = new App(manifest); const handler = async (request: Request): Promise => { diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts index 71ad2bfaef7f..9b4fc39fb280 100644 --- a/packages/integrations/vercel/src/serverless/entrypoint.ts +++ b/packages/integrations/vercel/src/serverless/entrypoint.ts @@ -1,5 +1,5 @@ import { polyfill } from '@astrojs/webapi'; -import type { SSRManifest } from 'astro'; +import type { SSRBaseManifest } from 'astro'; import { App } from 'astro/app'; import type { IncomingMessage, ServerResponse } from 'node:http'; @@ -9,7 +9,7 @@ polyfill(globalThis, { exclude: 'window document', }); -export const createExports = (manifest: SSRManifest) => { +export const createExports = (manifest: SSRBaseManifest) => { const app = new App(manifest); const handler = async (req: IncomingMessage, res: ServerResponse) => { From 48a32b86939887f3b75803b435082b38a74f4d30 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 26 May 2023 13:43:27 +0100 Subject: [PATCH 02/20] some doc --- packages/astro/src/core/build/static-build.ts | 11 ++++++++++- packages/integrations/mdx/package.json | 1 - 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 1c0575ac464c..3cdcbcec5073 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -444,7 +444,16 @@ function makeAstroPageEntryPointFileName(prefix: string, facadeModuleId: string, } /** - * This function attempts + * This function attempts to prepend the `serverlessEntryPrefix` prefix to each entry point emitted. + * + * The `facadeModuleId` has a shape like: \0@astro-serverless-page:src/pages/index@_@astro. + * + * 1. We call `makeAstroPageEntryPointFileName` which normalise its name, making it like a file path + * 2. We split the file path using the file system separator and attempt to retrieve the last entry + * 3. The last entry should be the file + * 4. We prepend the file name with `serverlessEntryPrefix` + * 5. We built the file path again, using the new entry built in the previous step + * * @param facadeModuleId * @param opts */ diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index e636ca20eb44..41682a6bd0fe 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -37,7 +37,6 @@ "@astrojs/markdown-remark": "^2.2.1", "@astrojs/prism": "^2.1.2", "@mdx-js/mdx": "^2.3.0", - "@mdx-js/rollup": "^2.3.0", "acorn": "^8.8.0", "es-module-lexer": "^1.1.1", "estree-util-visit": "^1.2.0", From b0f7998514327ff96c05aedb5d4d88899db36fa4 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 29 May 2023 15:49:17 +0100 Subject: [PATCH 03/20] documentation --- packages/astro/src/@types/astro.ts | 36 +++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 62540d5523b8..20f7f32aeadc 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -839,16 +839,50 @@ export interface AstroUserConfig { */ inlineStylesheets?: 'always' | 'auto' | 'never'; + /** + * @docs + * @name build.serverlessEntryPrefix + * @type {string} + * @default `'entry'` + * @description + * Option used when `build.mode` is set to `"serverless"`. Astro will prepend + * the emitted files using this option. + * + * ```js + * { + * build: { + * serverlessEntryPrefix: 'main' + * } + * } + * ``` + */ + serverlessEntryPrefix?: string; + /** * @docs * @name build.mode * @type {string} - * @default `'server'` + * @default {'server' | 'serverless'} * @description * Defines how the SSR should be bundled. SSR code for "server" * will be built in one single file. * + * When "serverless" is specified, Astro will emit a file for each page. + * Each file emitted will render only one page. The pages will be emitted + * inside a `pages/` directory, and emitted file will keep the same file paths + * of the `src/pages` directory. + * + * Each emitted file will be prefixed with `entry`. You can use {@link build.serverlessEntryPrefix} + * to change the prefix. * + * Inside the `dist/` directory, the pages + * ```plaintext + * ├── pages + * │ ├── blog + * │ │ ├── entry._slug_.astro.mjs + * │ │ └── entry.about.astro.mjs + * │ └── entry.index.astro.mjs + * ``` * * ```js * { From b78be4a878ba2a870a803f05abe621f2af1ad8bb Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 12 Jun 2023 16:20:47 +0300 Subject: [PATCH 04/20] rebase --- packages/astro/src/@types/astro.ts | 16 ++--- packages/astro/src/core/app/index.ts | 23 ++++--- packages/astro/src/core/app/types.ts | 3 +- .../src/core/build/plugins/plugin-pages.ts | 21 +++++- .../src/core/build/plugins/plugin-ssr.ts | 64 +++---------------- packages/astro/src/core/config/schema.ts | 12 +++- .../fixtures/ssr-request/astro.config.mjs | 2 +- .../ssr-serverless-manifest/astro.config.mjs | 2 +- .../fixtures/ssr-serverless/astro.config.mjs | 2 +- .../test/ssr-serverless-manifest.test.js | 2 +- 10 files changed, 63 insertions(+), 84 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 20f7f32aeadc..155cc0681c8a 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -860,22 +860,22 @@ export interface AstroUserConfig { /** * @docs - * @name build.mode - * @type {string} - * @default {'server' | 'serverless'} + * @name build.ssrMode + * @type {'server' | 'serverless'} + * @default {'server'} * @description - * Defines how the SSR should be bundled. SSR code for "server" + * Defines how the SSR code should be bundled. SSR code for "server" * will be built in one single file. * - * When "serverless" is specified, Astro will emit a file for each page. + * When "serverless" is passed, Astro will emit a file for each page. * Each file emitted will render only one page. The pages will be emitted - * inside a `pages/` directory, and emitted file will keep the same file paths + * inside a `dist/pages/` directory, and the emitted files will keep the same file paths * of the `src/pages` directory. * * Each emitted file will be prefixed with `entry`. You can use {@link build.serverlessEntryPrefix} * to change the prefix. * - * Inside the `dist/` directory, the pages + * Inside the `dist/` directory, the pages will look like this: * ```plaintext * ├── pages * │ ├── blog @@ -892,7 +892,7 @@ export interface AstroUserConfig { * } * ``` */ - mode?: 'server' | 'serverless'; + ssrMode?: 'server' | 'serverless'; }; /** diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index fd77f0340b48..82a762f68e39 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -6,7 +6,7 @@ import type { SSRElement, SSRBaseManifest, } from '../../@types/astro'; -import type { RouteInfo, SSRServerlessManifest, SSRServerManifest } from './types'; +import type { RouteInfo, SSRServerManifest } from './types'; import mime from 'mime'; import type { SinglePageBuiltModule } from '../build/types'; @@ -150,7 +150,6 @@ export class App { } let mod = await this.#getModuleForRoute(routeData); - let mod = await this.#retrievePage(routeData); if (routeData.type === 'page' || routeData.type === 'redirect') { let response = await this.#renderPage(request, routeData, mod, defaultStatus); @@ -159,7 +158,6 @@ export class App { if (response.status === 500 || response.status === 404) { const errorRouteData = matchRoute('/' + response.status, this.#manifestData); if (errorRouteData && errorRouteData.route !== routeData.route) { - mod = await this.#retrievePage(errorPageData); mod = await this.#getModuleForRoute(errorRouteData); try { let errorResponse = await this.#renderPage( @@ -188,14 +186,19 @@ export class App { if (route.type === 'redirect') { return RedirectSinglePageBuiltModule; } else { - const importComponentInstance = this.#manifest.pageMap.get(route.component); - if (!importComponentInstance) { - throw new Error( - `Unexpectedly unable to find a component instance for route ${route.route}` - ); + if (isSsrServerManifest(this.#manifest)) { + const importComponentInstance = this.#manifest.pageMap.get(route.component); + if (!importComponentInstance) { + throw new Error( + `Unexpectedly unable to find a component instance for route ${route.route}` + ); + } + const pageModule = await importComponentInstance(); + return pageModule; + } else { + const importComponentInstance = this.#manifest.pageModule; + return importComponentInstance; } - const built = await importComponentInstance(); - return built; } } diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index f0c2e183f4c5..582549e77a5e 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -31,7 +31,7 @@ export type SerializedRouteInfo = Omit & { routeData: SerializedRouteData; }; -type ImportComponentInstance = () => Promise; +export type ImportComponentInstance = () => Promise; export type SSRBaseManifest = SSRServerManifest | SSRServerlessManifest; @@ -50,7 +50,6 @@ export type SSRServerManifest = { entryModules: Record; assets: Set; componentMetadata: SSRResult['componentMetadata']; - middleware?: AstroMiddlewareInstance; pageModule?: undefined; pageMap: Map; }; diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index b9cfb8ee6724..ca224d851d25 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -1,4 +1,4 @@ -import { getVirtualModulePageNameFromPath, getPathFromVirtualModulePageName } from './util.js'; +import { getPathFromVirtualModulePageName, ASTRO_PAGE_EXTENSION_POST_PATTERN } from './util.js'; import type { Plugin as VitePlugin } from 'vite'; import { routeIsRedirect } from '../../redirects/index.js'; import { addRollupInput } from '../add-rollup-input.js'; @@ -7,10 +7,27 @@ import type { AstroBuildPlugin } from '../plugin'; import type { StaticBuildOptions } from '../types'; import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; +import { extname } from 'node:path'; export const ASTRO_PAGE_MODULE_ID = '@astro-page:'; export const ASTRO_PAGE_RESOLVED_MODULE_ID = '\0' + ASTRO_PAGE_MODULE_ID; +/** + * 1. We add a fixed prefix, which is used as virtual module naming convention; + * 2. We replace the dot that belongs extension with an arbitrary string. + * + * @param path + */ +export function getVirtualModulePageNameFromPath(path: string) { + // we mask the extension, so this virtual file + // so rollup won't trigger other plugins in the process + const extension = extname(path); + return `${ASTRO_PAGE_MODULE_ID}${path.replace( + extension, + extension.replace('.', ASTRO_PAGE_EXTENSION_POST_PATTERN) + )}`; +} + export function getVirtualModulePageIdFromPath(path: string) { const name = getVirtualModulePageNameFromPath(path); return '\x00' + name; @@ -28,7 +45,7 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V if (routeIsRedirect(pageData.route)) { continue; } - inputs.add(getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path)); + inputs.add(getVirtualModulePageNameFromPath(path)); } return addRollupInput(options, Array.from(inputs)); diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 8eab1e4e861c..2b7c99d7eee6 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -13,8 +13,6 @@ import { addRollupInput } from '../add-rollup-input.js'; import { getOutFile, getOutFolder } from '../common.js'; import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin'; -import type { StaticBuildOptions } from '../types'; -import { getVirtualModulePageNameFromPath } from './plugin-pages.js'; import type { OutputChunk, StaticBuildOptions } from '../types'; import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; import { getPathFromVirtualModulePageName, getVirtualModulePageNameFromPath } from './util.js'; @@ -107,15 +105,13 @@ export function pluginSSRServer( options: StaticBuildOptions, internals: BuildInternals ): AstroBuildPlugin { - const ssr = - options.settings.config.output === 'server' || isHybridOutput(options.settings.config); + const ssr = isServerLikeOutput(options.settings.config); return { build: 'ssr', hooks: { 'build:before': () => { let vitePlugin = - // config.build object is optional, so we check NOT EQUAL against "serverless" instead - ssr && options.settings.config.build?.mode !== 'serverless' + ssr && options.settings.config.build.ssrMode === 'server' ? vitePluginSSRServer(internals, options.settings.adapter!, options) : undefined; @@ -129,7 +125,7 @@ export function pluginSSRServer( return; } - if (options.settings.config.build?.mode === 'serverless') { + if (options.settings.config.build.ssrMode === 'serverless') { return; } @@ -158,7 +154,7 @@ function vitePluginSSRServerless( name: '@astrojs/vite-plugin-astro-ssr-serverless', enforce: 'post', options(opts) { - if (options.settings.config.build?.mode === 'serverless') { + if (options.settings.config.build.ssrMode === 'serverless') { const inputs: Set = new Set(); for (const path of Object.keys(options.allPages)) { @@ -232,14 +228,13 @@ export function pluginSSRServerless( options: StaticBuildOptions, internals: BuildInternals ): AstroBuildPlugin { - const ssr = - options.settings.config.output === 'server' || isHybridOutput(options.settings.config); + const ssr = isServerLikeOutput(options.settings.config); return { build: 'ssr', hooks: { 'build:before': () => { let vitePlugin = - ssr && options.settings.config.build.mode === 'serverless' + ssr && options.settings.config.build.ssrMode === 'serverless' ? vitePluginSSRServerless(internals, options.settings.adapter!, options) : undefined; @@ -252,7 +247,7 @@ export function pluginSSRServerless( if (!ssr) { return; } - if (options.settings.config.build?.mode === 'server') { + if (options.settings.config.build.ssrMode === 'server') { return; } @@ -275,13 +270,8 @@ export function pluginSSRServerless( function generateSSRCode(config: AstroConfig, adapter: AstroAdapter) { const imports: string[] = []; const contents: string[] = []; - let middleware; - if (config.experimental?.middleware === true) { - imports.push(`import * as _middleware from "${MIDDLEWARE_MODULE_ID}";`); - middleware = 'middleware: _middleware'; - } let pageMap; - if (config.build.mode === 'serverless') { + if (config.build.ssrMode === 'serverless') { pageMap = 'pageModule'; } else { pageMap = 'pageMap'; @@ -335,7 +325,7 @@ export async function injectManifest( internals: BuildInternals, chunk: Readonly ) { - if (buildOpts.settings.config.build.mode === 'serverless') { + if (buildOpts.settings.config.build.ssrMode === 'serverless') { if (internals.ssrServerlessEntryChunks.size === 0) { throw new Error(`Did not generate an entry chunk for SSR in serverless mode`); } @@ -475,39 +465,3 @@ function buildManifest( return ssrManifest; } - -export function pluginSSR( - options: StaticBuildOptions, - internals: BuildInternals -): AstroBuildPlugin { - const ssr = isServerLikeOutput(options.settings.config); - return { - build: 'ssr', - hooks: { - 'build:before': () => { - let vitePlugin = ssr - ? vitePluginSSR(internals, options.settings.adapter!, options) - : undefined; - - return { - enforce: 'after-user-plugins', - vitePlugin, - }; - }, - 'build:post': async ({ mutate }) => { - if (!ssr) { - return; - } - - if (!internals.ssrEntryChunk) { - throw new Error(`Did not generate an entry chunk for SSR`); - } - // Mutate the filename - internals.ssrEntryChunk.fileName = options.settings.config.build.serverEntry; - - const code = await injectManifest(options, internals); - mutate(internals.ssrEntryChunk, 'server', code); - }, - }, - }; -} diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index e03878c1d611..381e7b8eed2e 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -25,7 +25,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { redirects: true, inlineStylesheets: 'never', serverlessEntryPrefix: 'entry', - mode: 'server', + ssrMode: 'server', }, compressHTML: false, server: { @@ -126,7 +126,10 @@ export const AstroConfigSchema = z.object({ .string() .optional() .default(ASTRO_CONFIG_DEFAULTS.build.serverlessEntryPrefix), - mode: z.enum(['server', 'serverless']).optional().default(ASTRO_CONFIG_DEFAULTS.build.server), + ssrMode: z + .enum(['server', 'serverless']) + .optional() + .default(ASTRO_CONFIG_DEFAULTS.build.ssrMode), }) .optional() .default({}), @@ -290,7 +293,10 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) { .string() .optional() .default(ASTRO_CONFIG_DEFAULTS.build.serverlessEntryPrefix), - mode: z.enum(['server', 'serverless']).optional().default(ASTRO_CONFIG_DEFAULTS.build.mode), + ssrMode: z + .enum(['server', 'serverless']) + .optional() + .default(ASTRO_CONFIG_DEFAULTS.build.ssrMode), }) .optional() .default({}), diff --git a/packages/astro/test/fixtures/ssr-request/astro.config.mjs b/packages/astro/test/fixtures/ssr-request/astro.config.mjs index d5d304da91be..ec813582afb7 100644 --- a/packages/astro/test/fixtures/ssr-request/astro.config.mjs +++ b/packages/astro/test/fixtures/ssr-request/astro.config.mjs @@ -3,6 +3,6 @@ import { defineConfig } from 'astro/config'; // https://astro.build/config export default defineConfig({ build: { - mode: "serverless" + ssrMode: "server" } }); diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs b/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs index 59ffc57a69a2..6429a1c4039a 100644 --- a/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs +++ b/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs @@ -1,7 +1,7 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ build: { - mode: "serverless" + ssrMode: "serverless" }, output: "server" }) \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs b/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs index 59ffc57a69a2..6429a1c4039a 100644 --- a/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs +++ b/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs @@ -1,7 +1,7 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ build: { - mode: "serverless" + ssrMode: "serverless" }, output: "server" }) \ No newline at end of file diff --git a/packages/astro/test/ssr-serverless-manifest.test.js b/packages/astro/test/ssr-serverless-manifest.test.js index 98588ba5aee8..ed5488df4554 100644 --- a/packages/astro/test/ssr-serverless-manifest.test.js +++ b/packages/astro/test/ssr-serverless-manifest.test.js @@ -3,7 +3,7 @@ import { loadFixture } from './test-utils.js'; import testAdapter from './test-adapter.js'; import * as cheerio from 'cheerio'; -describe('astro:ssr-manifest', () => { +describe('astro:ssr-manifest, serverless', () => { /** @type {import('./test-utils').Fixture} */ let fixture; From a36c0d7904832188c6ab0582d83a1c25a41baf6b Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 12 Jun 2023 17:03:26 +0300 Subject: [PATCH 05/20] changeset --- .changeset/wet-readers-join.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .changeset/wet-readers-join.md diff --git a/.changeset/wet-readers-join.md b/.changeset/wet-readers-join.md new file mode 100644 index 000000000000..208fa981d7a4 --- /dev/null +++ b/.changeset/wet-readers-join.md @@ -0,0 +1,34 @@ +--- +'astro': minor +--- + +Shipped a new SSR mode, called `serverless`. +When enabled, Astro will emit a file for each page, which will render one single page. + +These files will be emitted inside `dist/pages`, and they will look like this: + +``` +├── pages +│ ├── blog +│ │ ├── entry._slug_.astro.mjs +│ │ └── entry.about.astro.mjs +│ └── entry.index.astro.mjs +``` + +To enable and customise this mode, new options are now available: + +```js +export default defineConfig({ + output: "server", + adapter: node({ + mode: "standalone" + }), + build: { + ssrMode: "serverless", + serverlessEntryPrefix: "main" + } +}) +``` + +- `ssrMode` accepts two values, `"server"` or `"serverless"`. Default value, `"server"`. +- `serverlessEntryPrefix` allows to change the prefix of a serverless From 7a1aa40577acfb6d18387e2429667ab29a335e24 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 14 Jun 2023 17:27:39 +0300 Subject: [PATCH 06/20] address code suggestions --- .changeset/wet-readers-join.md | 2 +- packages/astro/src/@types/astro.ts | 39 +++----- packages/astro/src/core/app/common.ts | 4 +- packages/astro/src/core/app/types.ts | 4 +- packages/astro/src/core/build/internal.ts | 4 +- .../src/core/build/plugins/plugin-ssr.ts | 94 +++++++++++++------ packages/astro/src/core/build/static-build.ts | 2 +- packages/astro/src/core/config/schema.ts | 23 +---- packages/astro/src/integrations/index.ts | 4 +- .../fixtures/ssr-request/astro.config.mjs | 2 +- .../ssr-serverless-manifest/astro.config.mjs | 2 +- .../src/pages/[...post].astro | 18 ++++ .../src/pages/lorem.md | 1 + .../src/pages/zod.astro | 17 ++++ .../fixtures/ssr-serverless/astro.config.mjs | 2 +- .../test/ssr-serverless-manifest.test.js | 24 ++++- packages/astro/test/test-adapter.js | 11 ++- packages/astro/test/test-utils.js | 2 +- 18 files changed, 162 insertions(+), 93 deletions(-) create mode 100644 packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/[...post].astro create mode 100644 packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/lorem.md create mode 100644 packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/zod.astro diff --git a/.changeset/wet-readers-join.md b/.changeset/wet-readers-join.md index 208fa981d7a4..37deef640fa9 100644 --- a/.changeset/wet-readers-join.md +++ b/.changeset/wet-readers-join.md @@ -24,7 +24,7 @@ export default defineConfig({ mode: "standalone" }), build: { - ssrMode: "serverless", + split: "serverless", serverlessEntryPrefix: "main" } }) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 155cc0681c8a..510412de65dd 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -839,30 +839,11 @@ export interface AstroUserConfig { */ inlineStylesheets?: 'always' | 'auto' | 'never'; - /** - * @docs - * @name build.serverlessEntryPrefix - * @type {string} - * @default `'entry'` - * @description - * Option used when `build.mode` is set to `"serverless"`. Astro will prepend - * the emitted files using this option. - * - * ```js - * { - * build: { - * serverlessEntryPrefix: 'main' - * } - * } - * ``` - */ - serverlessEntryPrefix?: string; - /** * @docs * @name build.ssrMode - * @type {'server' | 'serverless'} - * @default {'server'} + * @type {boolean} + * @default {false} * @description * Defines how the SSR code should be bundled. SSR code for "server" * will be built in one single file. @@ -872,9 +853,6 @@ export interface AstroUserConfig { * inside a `dist/pages/` directory, and the emitted files will keep the same file paths * of the `src/pages` directory. * - * Each emitted file will be prefixed with `entry`. You can use {@link build.serverlessEntryPrefix} - * to change the prefix. - * * Inside the `dist/` directory, the pages will look like this: * ```plaintext * ├── pages @@ -887,12 +865,12 @@ export interface AstroUserConfig { * ```js * { * build: { - * mode: 'server' + * split: true * } * } * ``` */ - ssrMode?: 'server' | 'serverless'; + split?: boolean; }; /** @@ -1879,7 +1857,14 @@ export interface AstroIntegration { 'astro:server:setup'?: (options: { server: vite.ViteDevServer }) => void | Promise; 'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise; 'astro:server:done'?: () => void | Promise; - 'astro:build:ssr'?: (options: { manifest: SerializedSSRManifest }) => void | Promise; + 'astro:build:ssr'?: (options: { + manifest: SerializedSSRManifest; + /** + * This maps a {@link RouteData} to an {@link URL}, this URL represents + * the physical file you should import. + */ + entryPoints: Map; + }) => void | Promise; 'astro:build:start'?: () => void | Promise; 'astro:build:setup'?: (options: { vite: vite.InlineConfig; diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index fd0b792f6598..b2eb7718a090 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -3,7 +3,7 @@ import type { RouteInfo, SerializedSSRManifest, SSRBaseManifest, - SSRServerlessManifest, + SSRSplitManifest, SSRServerManifest, } from './types'; @@ -23,7 +23,7 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): const componentMetadata = new Map(serializedManifest.componentMetadata); const clientDirectives = new Map(serializedManifest.clientDirectives); - return { + return { ...serializedManifest, assets, componentMetadata, diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 582549e77a5e..a812f63ebff3 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -33,7 +33,7 @@ export type SerializedRouteInfo = Omit & { export type ImportComponentInstance = () => Promise; -export type SSRBaseManifest = SSRServerManifest | SSRServerlessManifest; +export type SSRBaseManifest = SSRServerManifest | SSRSplitManifest; export type SSRServerManifest = { adapterName: string; @@ -54,7 +54,7 @@ export type SSRServerManifest = { pageMap: Map; }; -export type SSRServerlessManifest = { +export type SSRSplitManifest = { adapterName: string; routes: RouteInfo[]; site?: string; diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index cc0e3faaad5f..c6677966ee7b 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -1,5 +1,5 @@ import type { Rollup } from 'vite'; -import type { SSRResult } from '../../@types/astro'; +import type { RouteData, SSRResult } from '../../@types/astro'; import type { PageOptions } from '../../vite-plugin-astro/types'; import { prependForwardSlash, removeFileExtension } from '../path.js'; import { viteID } from '../util.js'; @@ -84,6 +84,7 @@ export interface BuildInternals { staticFiles: Set; // The SSR entry chunk. Kept in internals to share between ssr/client build steps ssrEntryChunk?: Rollup.OutputChunk; + entryPoints: Map; ssrServerlessEntryChunks: Map; componentMetadata: SSRResult['componentMetadata']; } @@ -116,6 +117,7 @@ export function createBuildInternals(): BuildInternals { staticFiles: new Set(), componentMetadata: new Map(), ssrServerlessEntryChunks: new Map(), + entryPoints: new Map(), }; } diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 2b7c99d7eee6..c5bf76c05138 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -1,5 +1,5 @@ import glob from 'fast-glob'; -import { fileURLToPath } from 'url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Plugin as VitePlugin } from 'vite'; import type { AstroAdapter, AstroConfig } from '../../../@types/astro'; import { runHookBuildSsr } from '../../../integrations/index.js'; @@ -14,10 +14,10 @@ import { getOutFile, getOutFolder } from '../common.js'; import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin'; import type { OutputChunk, StaticBuildOptions } from '../types'; -import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js'; import { getPathFromVirtualModulePageName, getVirtualModulePageNameFromPath } from './util.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js'; +import { join } from 'node:path'; export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry'; const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID; @@ -98,6 +98,10 @@ function vitePluginSSRServer( } } }, + async writeBundle(outputOptions, bundle) { + console.log(outputOptions); + console.log(bundle); + }, }; } @@ -111,7 +115,7 @@ export function pluginSSRServer( hooks: { 'build:before': () => { let vitePlugin = - ssr && options.settings.config.build.ssrMode === 'server' + ssr && !options.settings.config.build.split ? vitePluginSSRServer(internals, options.settings.adapter!, options) : undefined; @@ -125,7 +129,7 @@ export function pluginSSRServer( return; } - if (options.settings.config.build.ssrMode === 'serverless') { + if (options.settings.config.build.split) { return; } @@ -135,8 +139,15 @@ export function pluginSSRServer( // Mutate the filename internals.ssrEntryChunk.fileName = options.settings.config.build.serverEntry; - const code = await injectManifest(options, internals, internals.ssrEntryChunk); + const manifest = await createManifest(options, internals); + const code = await injectManifest(manifest, internals.ssrEntryChunk); mutate(internals.ssrEntryChunk, 'server', code); + await runHookBuildSsr({ + config: options.settings.config, + manifest, + logging: options.logging, + entryPoints: internals.entryPoints, + }); }, }, }; @@ -154,7 +165,7 @@ function vitePluginSSRServerless( name: '@astrojs/vite-plugin-astro-ssr-serverless', enforce: 'post', options(opts) { - if (options.settings.config.build.ssrMode === 'serverless') { + if (options.settings.config.build.split) { const inputs: Set = new Set(); for (const path of Object.keys(options.allPages)) { @@ -211,14 +222,13 @@ function vitePluginSSRServerless( for (const moduleKey of Object.keys(chunk.modules)) { if (moduleKey.startsWith(RESOLVED_SERVERLESS_MODULE_ID)) { internals.ssrServerlessEntryChunks.set(moduleKey, chunk); + storeEntryPoint(moduleKey, options, internals, chunk.fileName); shouldDeleteBundle = true; } } if (shouldDeleteBundle) { delete bundle[chunkName]; } - // if (chunk.modules[RESOLVED_SSR_VIRTUAL_MODULE_ID]) { - // } } }, }; @@ -234,7 +244,7 @@ export function pluginSSRServerless( hooks: { 'build:before': () => { let vitePlugin = - ssr && options.settings.config.build.ssrMode === 'serverless' + ssr && options.settings.config.build.split ? vitePluginSSRServerless(internals, options.settings.adapter!, options) : undefined; @@ -247,21 +257,25 @@ export function pluginSSRServerless( if (!ssr) { return; } - if (options.settings.config.build.ssrMode === 'server') { + if (!options.settings.config.build.split) { return; } if (internals.ssrServerlessEntryChunks.size === 0) { throw new Error(`Did not generate an entry chunk for SSR serverless`); } - // TODO: check this commented code - // Mutate the filename - // internals.ssrEntryChunk.fileName = options.settings.config.build.serverEntry; + const manifest = await createManifest(options, internals); for (const [moduleName, chunk] of internals.ssrServerlessEntryChunks) { - const code = await injectManifest(options, internals, chunk); + const code = injectManifest(manifest, chunk); mutate(chunk, 'server', code); } + await runHookBuildSsr({ + config: options.settings.config, + manifest, + logging: options.logging, + entryPoints: internals.entryPoints, + }); }, }, }; @@ -271,7 +285,7 @@ function generateSSRCode(config: AstroConfig, adapter: AstroAdapter) { const imports: string[] = []; const contents: string[] = []; let pageMap; - if (config.build.ssrMode === 'serverless') { + if (config.build.split) { pageMap = 'pageModule'; } else { pageMap = 'pageMap'; @@ -320,12 +334,19 @@ if(_start in adapter) { * @param internals * @param chunk */ -export async function injectManifest( +export function injectManifest(manifest: SerializedSSRManifest, chunk: Readonly) { + const code = chunk.code; + + return code.replace(replaceExp, () => { + return JSON.stringify(manifest); + }); +} + +export async function createManifest( buildOpts: StaticBuildOptions, - internals: BuildInternals, - chunk: Readonly -) { - if (buildOpts.settings.config.build.ssrMode === 'serverless') { + internals: BuildInternals +): Promise { + if (buildOpts.settings.config.build.split) { if (internals.ssrServerlessEntryChunks.size === 0) { throw new Error(`Did not generate an entry chunk for SSR in serverless mode`); } @@ -346,18 +367,29 @@ export async function injectManifest( } const staticFiles = internals.staticFiles; - const manifest = buildManifest(buildOpts, internals, Array.from(staticFiles)); - await runHookBuildSsr({ - config: buildOpts.settings.config, - manifest, - logging: buildOpts.logging, - }); - - const code = chunk.code; + return buildManifest(buildOpts, internals, Array.from(staticFiles)); +} - return code.replace(replaceExp, () => { - return JSON.stringify(manifest); - }); +/** + * Because we delete the bundle from rollup at the end of this function, + * we can't use `writeBundle` hook to get the final file name of the entry point written on disk. + * We use this hook instead. + * + * We retrieve the {@link RouteData} that belongs the current moduleKey + */ +function storeEntryPoint( + moduleKey: string, + options: StaticBuildOptions, + internals: BuildInternals, + fileName: string +) { + const componentPath = getPathFromVirtualModulePageName(RESOLVED_SERVERLESS_MODULE_ID, moduleKey); + for (const [page, pageData] of Object.entries(options.allPages)) { + if (componentPath == page) { + const publicPath = fileURLToPath(options.settings.config.outDir); + internals.entryPoints.set(pageData.route, pathToFileURL(join(publicPath, fileName))); + } + } } function buildManifest( diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 3cdcbcec5073..7b30984fe918 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -468,7 +468,7 @@ function makeServerlessEntryPointFileName(facadeModuleId: string, opts: StaticBu if (lastPathComponent) { const extension = extname(lastPathComponent); if (extension.length > 0) { - const newFileName = `${opts.settings.config.build.serverlessEntryPrefix}.${lastPathComponent}`; + const newFileName = `entry.${lastPathComponent}`; return [...pathComponents, newFileName].join(path.sep); } } diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 381e7b8eed2e..99ef653d0788 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -24,8 +24,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { serverEntry: 'entry.mjs', redirects: true, inlineStylesheets: 'never', - serverlessEntryPrefix: 'entry', - ssrMode: 'server', + split: false, }, compressHTML: false, server: { @@ -122,14 +121,8 @@ export const AstroConfigSchema = z.object({ .enum(['always', 'auto', 'never']) .optional() .default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets), - serverlessEntryPrefix: z - .string() - .optional() - .default(ASTRO_CONFIG_DEFAULTS.build.serverlessEntryPrefix), - ssrMode: z - .enum(['server', 'serverless']) - .optional() - .default(ASTRO_CONFIG_DEFAULTS.build.ssrMode), + + split: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.split), }) .optional() .default({}), @@ -289,14 +282,8 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) { .enum(['always', 'auto', 'never']) .optional() .default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets), - serverlessEntryPrefix: z - .string() - .optional() - .default(ASTRO_CONFIG_DEFAULTS.build.serverlessEntryPrefix), - ssrMode: z - .enum(['server', 'serverless']) - .optional() - .default(ASTRO_CONFIG_DEFAULTS.build.ssrMode), + + split: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.split), }) .optional() .default({}), diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index 53a75fbea1ad..ae1ff97618d1 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -309,16 +309,18 @@ export async function runHookBuildSsr({ config, manifest, logging, + entryPoints, }: { config: AstroConfig; manifest: SerializedSSRManifest; logging: LogOptions; + entryPoints: Map; }) { for (const integration of config.integrations) { if (integration?.hooks?.['astro:build:ssr']) { await withTakingALongTimeMsg({ name: integration.name, - hookResult: integration.hooks['astro:build:ssr']({ manifest }), + hookResult: integration.hooks['astro:build:ssr']({ manifest, entryPoints }), logging, }); } diff --git a/packages/astro/test/fixtures/ssr-request/astro.config.mjs b/packages/astro/test/fixtures/ssr-request/astro.config.mjs index ec813582afb7..59b57a5eeccf 100644 --- a/packages/astro/test/fixtures/ssr-request/astro.config.mjs +++ b/packages/astro/test/fixtures/ssr-request/astro.config.mjs @@ -3,6 +3,6 @@ import { defineConfig } from 'astro/config'; // https://astro.build/config export default defineConfig({ build: { - ssrMode: "server" + split: "server" } }); diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs b/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs index 6429a1c4039a..171de39d9e24 100644 --- a/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs +++ b/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs @@ -1,7 +1,7 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ build: { - ssrMode: "serverless" + split: true }, output: "server" }) \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/[...post].astro b/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/[...post].astro new file mode 100644 index 000000000000..8bac75eb9404 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/[...post].astro @@ -0,0 +1,18 @@ +--- +export async function getStaticPaths() { + return [ + { + params: { page: 1 }, + }, + { + params: { page: 2 }, + }, + { + params: { page: 3 } + } + ] +}; +--- + + + \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/lorem.md b/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/lorem.md new file mode 100644 index 000000000000..8a38d58c1963 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/lorem.md @@ -0,0 +1 @@ +# Title \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/zod.astro b/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/zod.astro new file mode 100644 index 000000000000..06d949d47f6c --- /dev/null +++ b/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/zod.astro @@ -0,0 +1,17 @@ +--- +import { manifest } from 'astro:ssr-manifest'; +--- + + + Testing + + + +

Testing

+
+ + \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs b/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs index 6429a1c4039a..635b1af0f00b 100644 --- a/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs +++ b/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs @@ -1,7 +1,7 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ build: { - ssrMode: "serverless" + split: "serverless" }, output: "server" }) \ No newline at end of file diff --git a/packages/astro/test/ssr-serverless-manifest.test.js b/packages/astro/test/ssr-serverless-manifest.test.js index ed5488df4554..de4297b4381f 100644 --- a/packages/astro/test/ssr-serverless-manifest.test.js +++ b/packages/astro/test/ssr-serverless-manifest.test.js @@ -2,23 +2,30 @@ import { expect } from 'chai'; import { loadFixture } from './test-utils.js'; import testAdapter from './test-adapter.js'; import * as cheerio from 'cheerio'; +import { fileURLToPath } from 'node:url'; +import { existsSync } from 'node:fs'; -describe('astro:ssr-manifest, serverless', () => { +describe('astro:ssr-manifest, split', () => { /** @type {import('./test-utils').Fixture} */ let fixture; + let entryPoints; before(async () => { fixture = await loadFixture({ root: './fixtures/ssr-serverless-manifest/', output: 'server', - adapter: testAdapter(), + adapter: testAdapter({ + setEntries(entries) { + entryPoints = entries; + }, + }), }); await fixture.build(); }); - it('works', async () => { + it('should be able to render a specific entry point', async () => { const pagePath = 'pages/index.astro'; - const app = await fixture.loadServerlessEntrypointApp(pagePath); + const app = await fixture.loadEntryPoint(pagePath); const request = new Request('http://example.com/'); const response = await app.render(request); const html = await response.text(); @@ -26,4 +33,13 @@ describe('astro:ssr-manifest, serverless', () => { const $ = cheerio.load(html); expect($('#assets').text()).to.equal('["/_astro/index.a8a337e4.css"]'); }); + + it('should give access to entry points that exists on file system', async () => { + // number of the pages inside src/ + expect(entryPoints.size).to.equal(4); + for (const fileUrl in entryPoints.values()) { + let filePath = fileURLToPath(fileUrl); + expect(existsSync(filePath)).to.be.true; + } + }); }); diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js index d74cfaf81a71..723943dcbd49 100644 --- a/packages/astro/test/test-adapter.js +++ b/packages/astro/test/test-adapter.js @@ -4,7 +4,11 @@ import { viteID } from '../dist/core/util.js'; * * @returns {import('../src/@types/astro').AstroIntegration} */ -export default function ({ provideAddress = true, extendAdapter } = { provideAddress: true }) { +export default function ( + { provideAddress = true, extendAdapter, setEntries: setEntryPoints = undefined } = { + provideAddress: true, + } +) { return { name: 'my-ssr-adapter', hooks: { @@ -70,6 +74,11 @@ export default function ({ provideAddress = true, extendAdapter } = { provideAdd ...extendAdapter, }); }, + 'astro:build:ssr': ({ entryPoints }) => { + if (setEntryPoints) { + setEntryPoints(entryPoints); + } + }, }, }; } diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 532776159055..30fdf008b438 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -247,7 +247,7 @@ export async function loadFixture(inlineConfig) { app.manifest = manifest; return app; }, - loadServerlessEntrypointApp: async (pagePath, streaming) => { + loadEntryPoint: async (pagePath, streaming) => { const pathComponents = pagePath.split(path.sep); const lastPathComponent = pathComponents.pop(); if (lastPathComponent) { From 756b6830747ea40d26a75c7911c912f83adef8fd Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 15 Jun 2023 11:17:12 +0300 Subject: [PATCH 07/20] chore: revert SSRManifest change, apply suggestion --- benchmark/packages/timer/src/server.ts | 4 +-- packages/astro/client-base.d.ts | 2 +- packages/astro/src/@types/astro.ts | 2 +- packages/astro/src/core/app/common.ts | 12 ++----- packages/astro/src/core/app/index.ts | 26 ++++++++-------- packages/astro/src/core/app/node.ts | 4 +-- packages/astro/src/core/app/types.ts | 31 +++---------------- packages/astro/src/core/errors/errors-data.ts | 11 +++++++ .../cloudflare/src/server.advanced.ts | 4 +-- .../cloudflare/src/server.directory.ts | 4 +-- packages/integrations/deno/src/server.ts | 6 ++-- .../netlify/src/netlify-edge-functions.ts | 4 +-- .../netlify/src/netlify-functions.ts | 4 +-- packages/integrations/node/src/server.ts | 6 ++-- .../vercel/src/edge/entrypoint.ts | 4 +-- .../vercel/src/serverless/entrypoint.ts | 4 +-- 16 files changed, 56 insertions(+), 72 deletions(-) diff --git a/benchmark/packages/timer/src/server.ts b/benchmark/packages/timer/src/server.ts index 245f95b28d74..5cfa4ad76822 100644 --- a/benchmark/packages/timer/src/server.ts +++ b/benchmark/packages/timer/src/server.ts @@ -1,5 +1,5 @@ import { polyfill } from '@astrojs/webapi'; -import type { SSRBaseManifest } from 'astro'; +import type { SSRManifest } from 'astro'; import { NodeApp } from 'astro/app/node'; import type { IncomingMessage, ServerResponse } from 'http'; @@ -7,7 +7,7 @@ polyfill(globalThis, { exclude: 'window document', }); -export function createExports(manifest: SSRBaseManifest) { +export function createExports(manifest: SSRManifest) { const app = new NodeApp(manifest); return { handler: async (req: IncomingMessage, res: ServerResponse) => { diff --git a/packages/astro/client-base.d.ts b/packages/astro/client-base.d.ts index 504b0880cd51..37bae7b1c6ae 100644 --- a/packages/astro/client-base.d.ts +++ b/packages/astro/client-base.d.ts @@ -188,7 +188,7 @@ declare module '*.mdx' { } declare module 'astro:ssr-manifest' { - export const manifest: import('./dist/@types/astro').SSRBaseManifest; + export const manifest: import('./dist/@types/astro').SSRManifest; } // Everything below are Vite's types (apart from image types, which are in `client.d.ts`) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 510412de65dd..b2967fc8797f 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -44,7 +44,7 @@ export type { ImageQualityPreset, ImageTransform, } from '../assets/types'; -export type { SSRBaseManifest } from '../core/app/types'; +export type { SSRManifest } from '../core/app/types'; export type { AstroCookies } from '../core/cookies'; export interface AstroBuiltinProps { diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index b2eb7718a090..58898b2fe51f 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -1,13 +1,7 @@ import { deserializeRouteData } from '../routing/manifest/serialization.js'; -import type { - RouteInfo, - SerializedSSRManifest, - SSRBaseManifest, - SSRSplitManifest, - SSRServerManifest, -} from './types'; +import type { RouteInfo, SerializedSSRManifest, SSRManifest } from './types'; -export function deserializeManifest(serializedManifest: SerializedSSRManifest): SSRBaseManifest { +export function deserializeManifest(serializedManifest: SerializedSSRManifest): SSRManifest { const routes: RouteInfo[] = []; for (const serializedRoute of serializedManifest.routes) { routes.push({ @@ -23,7 +17,7 @@ export function deserializeManifest(serializedManifest: SerializedSSRManifest): const componentMetadata = new Map(serializedManifest.componentMetadata); const clientDirectives = new Map(serializedManifest.clientDirectives); - return { + return { ...serializedManifest, assets, componentMetadata, diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 82a762f68e39..eb9cf9c46659 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -4,10 +4,9 @@ import type { MiddlewareResponseHandler, RouteData, SSRElement, - SSRBaseManifest, + SSRManifest, } from '../../@types/astro'; -import type { RouteInfo, SSRServerManifest } from './types'; - +import type { RouteInfo } from './types'; import mime from 'mime'; import type { SinglePageBuiltModule } from '../build/types'; import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js'; @@ -30,6 +29,7 @@ import { createStylesheetElementSet, } from '../render/ssr-element.js'; import { matchRoute } from '../routing/match.js'; +import { AstroError, AstroErrorData } from '../errors/index.js'; export { deserializeManifest } from './common.js'; const clientLocalsSymbol = Symbol.for('astro.locals'); @@ -42,7 +42,7 @@ export interface MatchOptions { export class App { #env: Environment; - #manifest: SSRBaseManifest; + #manifest: SSRManifest; #manifestData: ManifestData; #routeDataToRouteInfo: Map; #encoder = new TextEncoder(); @@ -54,16 +54,18 @@ export class App { #baseWithoutTrailingSlash: string; async #retrievePage(routeData: RouteData) { - if (isSsrServerManifest(this.#manifest)) { + if (this.#manifest.pageMap) { const pageModule = await this.#manifest.pageMap.get(routeData.component)!(); return await pageModule.page(); - } else { + } else if (this.#manifest.pageModule) { const pageModule = await this.#manifest.pageModule; return await pageModule.page(); + } else { + throw new AstroError(AstroErrorData.FailedToFindPageMapSSR); } } - constructor(manifest: SSRBaseManifest, streaming = true) { + constructor(manifest: SSRManifest, streaming = true) { this.#manifest = manifest; this.#manifestData = { routes: manifest.routes.map((route) => route.routeData), @@ -186,7 +188,7 @@ export class App { if (route.type === 'redirect') { return RedirectSinglePageBuiltModule; } else { - if (isSsrServerManifest(this.#manifest)) { + if (this.#manifest.pageMap) { const importComponentInstance = this.#manifest.pageMap.get(route.component); if (!importComponentInstance) { throw new Error( @@ -195,9 +197,11 @@ export class App { } const pageModule = await importComponentInstance(); return pageModule; - } else { + } else if (this.#manifest.pageModule) { const importComponentInstance = this.#manifest.pageModule; return importComponentInstance; + } else { + throw new AstroError(AstroErrorData.FailedToFindPageMapSSR); } } } @@ -335,7 +339,3 @@ export class App { } } } - -function isSsrServerManifest(manifest: any): manifest is SSRServerManifest { - return typeof manifest.pageMap !== 'undefined'; -} diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index ef4c2d308983..40b7b4e7ce6e 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -1,5 +1,5 @@ import type { RouteData } from '../../@types/astro'; -import type { SerializedSSRManifest, SSRBaseManifest } from './types'; +import type { SerializedSSRManifest, SSRManifest } from './types'; import * as fs from 'fs'; import { IncomingMessage } from 'http'; @@ -90,7 +90,7 @@ export class NodeApp extends App { } } -export async function loadManifest(rootFolder: URL): Promise { +export async function loadManifest(rootFolder: URL): Promise { const manifestFile = new URL('./manifest.json', rootFolder); const rawManifest = await fs.promises.readFile(manifestFile, 'utf-8'); const serializedManifest: SerializedSSRManifest = JSON.parse(rawManifest); diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index a812f63ebff3..9af15bf50252 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -33,28 +33,7 @@ export type SerializedRouteInfo = Omit & { export type ImportComponentInstance = () => Promise; -export type SSRBaseManifest = SSRServerManifest | SSRSplitManifest; - -export type SSRServerManifest = { - adapterName: string; - routes: RouteInfo[]; - site?: string; - base?: string; - assetsPrefix?: string; - markdown: MarkdownRenderingOptions; - renderers: SSRLoadedRenderer[]; - /** - * Map of directive name (e.g. `load`) to the directive script code - */ - clientDirectives: Map; - entryModules: Record; - assets: Set; - componentMetadata: SSRResult['componentMetadata']; - pageModule?: undefined; - pageMap: Map; -}; - -export type SSRSplitManifest = { +export type SSRManifest = { adapterName: string; routes: RouteInfo[]; site?: string; @@ -69,12 +48,12 @@ export type SSRSplitManifest = { entryModules: Record; assets: Set; componentMetadata: SSRResult['componentMetadata']; - pageModule: SinglePageBuiltModule; - pageMap?: undefined; + pageModule?: SinglePageBuiltModule; + pageMap?: Map; }; export type SerializedSSRManifest = Omit< - SSRBaseManifest, + SSRManifest, 'routes' | 'assets' | 'componentMetadata' | 'clientDirectives' > & { routes: SerializedRouteInfo[]; @@ -84,6 +63,6 @@ export type SerializedSSRManifest = Omit< }; export type AdapterCreateExports = ( - manifest: SSRBaseManifest, + manifest: SSRManifest, args?: T ) => Record; diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index a47087336073..be0ab96725ba 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -817,6 +817,17 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati `Invalid glob pattern: \`${globPattern}\`. Glob patterns must start with './', '../' or '/'.`, hint: 'See https://docs.astro.build/en/guides/imports/#glob-patterns for more information on supported glob patterns.', }, + /** + * @docs + * @description + * Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error. + */ + FailedToFindPageMapSSR: { + title: "Astro couldn't find the correct page to render", + code: 4003, + message: + "Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue.", + }, /** * @docs * @kind heading diff --git a/packages/integrations/cloudflare/src/server.advanced.ts b/packages/integrations/cloudflare/src/server.advanced.ts index 15c359f69422..5335db8dc51a 100644 --- a/packages/integrations/cloudflare/src/server.advanced.ts +++ b/packages/integrations/cloudflare/src/server.advanced.ts @@ -1,5 +1,5 @@ import type { ExecutionContext, Request as CFRequest } from '@cloudflare/workers-types'; -import type { SSRBaseManifest } from 'astro'; +import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; import { getProcessEnvProxy, isNode } from './util.js'; @@ -12,7 +12,7 @@ type Env = { name: string; }; -export function createExports(manifest: SSRBaseManifest) { +export function createExports(manifest: SSRManifest) { const app = new App(manifest); const fetch = async (request: Request & CFRequest, env: Env, context: ExecutionContext) => { diff --git a/packages/integrations/cloudflare/src/server.directory.ts b/packages/integrations/cloudflare/src/server.directory.ts index 22497bd37ce2..da23605573e3 100644 --- a/packages/integrations/cloudflare/src/server.directory.ts +++ b/packages/integrations/cloudflare/src/server.directory.ts @@ -1,5 +1,5 @@ import type { EventContext, Request as CFRequest } from '@cloudflare/workers-types'; -import type { SSRBaseManifest } from 'astro'; +import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; import { getProcessEnvProxy, isNode } from './util.js'; @@ -7,7 +7,7 @@ if (!isNode) { process.env = getProcessEnvProxy(); } -export function createExports(manifest: SSRBaseManifest) { +export function createExports(manifest: SSRManifest) { const app = new App(manifest); const onRequest = async ({ diff --git a/packages/integrations/deno/src/server.ts b/packages/integrations/deno/src/server.ts index 904113fb62e9..08d967065296 100644 --- a/packages/integrations/deno/src/server.ts +++ b/packages/integrations/deno/src/server.ts @@ -1,5 +1,5 @@ // Normal Imports -import type { SSRBaseManifest } from 'astro'; +import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; // @ts-expect-error @@ -25,7 +25,7 @@ async function* getPrerenderedFiles(clientRoot: URL): AsyncGenerator { } } -export function start(manifest: SSRBaseManifest, options: Options) { +export function start(manifest: SSRManifest, options: Options) { if (options.start === false) { return; } @@ -97,7 +97,7 @@ export function start(manifest: SSRBaseManifest, options: Options) { console.error(`Server running on port ${port}`); } -export function createExports(manifest: SSRBaseManifest, options: Options) { +export function createExports(manifest: SSRManifest, options: Options) { const app = new App(manifest); return { async stop() { diff --git a/packages/integrations/netlify/src/netlify-edge-functions.ts b/packages/integrations/netlify/src/netlify-edge-functions.ts index b26b870f8a77..4a6d3674c0e4 100644 --- a/packages/integrations/netlify/src/netlify-edge-functions.ts +++ b/packages/integrations/netlify/src/netlify-edge-functions.ts @@ -1,10 +1,10 @@ import type { Context } from '@netlify/edge-functions'; -import type { SSRBaseManifest } from 'astro'; +import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); -export function createExports(manifest: SSRBaseManifest) { +export function createExports(manifest: SSRManifest) { const app = new App(manifest); const handler = async (request: Request, context: Context): Promise => { diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts index 09d5baa36bc2..915f72955105 100644 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ b/packages/integrations/netlify/src/netlify-functions.ts @@ -1,6 +1,6 @@ import { polyfill } from '@astrojs/webapi'; import { builder, type Handler } from '@netlify/functions'; -import type { SSRBaseManifest } from 'astro'; +import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; polyfill(globalThis, { @@ -18,7 +18,7 @@ function parseContentType(header?: string) { const clientAddressSymbol = Symbol.for('astro.clientAddress'); -export const createExports = (manifest: SSRBaseManifest, args: Args) => { +export const createExports = (manifest: SSRManifest, args: Args) => { const app = new App(manifest); const builders = args.builders ?? false; diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts index 98c12ee6ea8e..98f5cd14bd4f 100644 --- a/packages/integrations/node/src/server.ts +++ b/packages/integrations/node/src/server.ts @@ -1,5 +1,5 @@ import { polyfill } from '@astrojs/webapi'; -import type { SSRBaseManifest } from 'astro'; +import type { SSRManifest } from 'astro'; import { NodeApp } from 'astro/app/node'; import middleware from './nodeMiddleware.js'; import startServer from './standalone.js'; @@ -9,7 +9,7 @@ polyfill(globalThis, { exclude: 'window document', }); -export function createExports(manifest: SSRBaseManifest, options: Options) { +export function createExports(manifest: SSRManifest, options: Options) { const app = new NodeApp(manifest); return { handler: middleware(app, options.mode), @@ -17,7 +17,7 @@ export function createExports(manifest: SSRBaseManifest, options: Options) { }; } -export function start(manifest: SSRBaseManifest, options: Options) { +export function start(manifest: SSRManifest, options: Options) { if (options.mode !== 'standalone' || process.env.ASTRO_NODE_AUTOSTART === 'disabled') { return; } diff --git a/packages/integrations/vercel/src/edge/entrypoint.ts b/packages/integrations/vercel/src/edge/entrypoint.ts index e49dee483dd6..a9870ef2bfc2 100644 --- a/packages/integrations/vercel/src/edge/entrypoint.ts +++ b/packages/integrations/vercel/src/edge/entrypoint.ts @@ -4,12 +4,12 @@ import './shim.js'; // Normal Imports -import type { SSRBaseManifest } from 'astro'; +import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); -export function createExports(manifest: SSRBaseManifest) { +export function createExports(manifest: SSRManifest) { const app = new App(manifest); const handler = async (request: Request): Promise => { diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts index 9b4fc39fb280..71ad2bfaef7f 100644 --- a/packages/integrations/vercel/src/serverless/entrypoint.ts +++ b/packages/integrations/vercel/src/serverless/entrypoint.ts @@ -1,5 +1,5 @@ import { polyfill } from '@astrojs/webapi'; -import type { SSRBaseManifest } from 'astro'; +import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; import type { IncomingMessage, ServerResponse } from 'node:http'; @@ -9,7 +9,7 @@ polyfill(globalThis, { exclude: 'window document', }); -export const createExports = (manifest: SSRBaseManifest) => { +export const createExports = (manifest: SSRManifest) => { const app = new App(manifest); const handler = async (req: IncomingMessage, res: ServerResponse) => { From a6ab22e146470d55d5ffa92291d147db6134c46c Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 15 Jun 2023 11:52:29 +0300 Subject: [PATCH 08/20] fix: incorrect config --- .changeset/wet-readers-join.md | 3 +-- packages/astro/test/fixtures/ssr-request/astro.config.mjs | 2 +- packages/astro/test/fixtures/ssr-serverless/astro.config.mjs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.changeset/wet-readers-join.md b/.changeset/wet-readers-join.md index 37deef640fa9..157d2d4c5e4d 100644 --- a/.changeset/wet-readers-join.md +++ b/.changeset/wet-readers-join.md @@ -24,8 +24,7 @@ export default defineConfig({ mode: "standalone" }), build: { - split: "serverless", - serverlessEntryPrefix: "main" + split: true } }) ``` diff --git a/packages/astro/test/fixtures/ssr-request/astro.config.mjs b/packages/astro/test/fixtures/ssr-request/astro.config.mjs index 59b57a5eeccf..3bd2a19a386e 100644 --- a/packages/astro/test/fixtures/ssr-request/astro.config.mjs +++ b/packages/astro/test/fixtures/ssr-request/astro.config.mjs @@ -3,6 +3,6 @@ import { defineConfig } from 'astro/config'; // https://astro.build/config export default defineConfig({ build: { - split: "server" + split: false } }); diff --git a/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs b/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs index 635b1af0f00b..171de39d9e24 100644 --- a/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs +++ b/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs @@ -1,7 +1,7 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ build: { - split: "serverless" + split: true }, output: "server" }) \ No newline at end of file From b121d63dbbacc58601643ceaa993800a4e80debc Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 15 Jun 2023 12:16:48 +0300 Subject: [PATCH 09/20] chore: remove console.log --- packages/astro/src/core/build/plugins/plugin-ssr.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index c5bf76c05138..871aa09ec4d9 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -98,10 +98,6 @@ function vitePluginSSRServer( } } }, - async writeBundle(outputOptions, bundle) { - console.log(outputOptions); - console.log(bundle); - }, }; } From 9c5a015eca93bece8b51457dd97de77454f00080 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 16 Jun 2023 15:18:33 +0300 Subject: [PATCH 10/20] chore: address code review --- .changeset/wet-readers-join.md | 7 ++----- packages/astro/src/@types/astro.ts | 8 ++++---- packages/astro/src/core/app/index.ts | 17 +++-------------- packages/astro/src/core/build/internal.ts | 9 +++------ .../astro/src/core/build/plugins/plugin-ssr.ts | 8 ++++---- packages/astro/src/core/build/static-build.ts | 4 ++-- .../astro.config.mjs | 0 .../package.json | 2 +- .../src/pages/[...post].astro | 0 .../src/pages/index.astro | 0 .../src/pages/lorem.md | 0 .../src/pages/zod.astro | 0 .../astro.config.mjs | 0 .../{ssr-serverless => ssr-split}/package.json | 2 +- .../src/pages/blog/[slug].astro | 0 .../src/pages/blog/about.astro | 0 .../src/pages/index.astro | 0 .../astro/test/ssr-serverless-manifest.test.js | 2 +- packages/astro/test/ssr-serverless.test.js | 18 ------------------ packages/astro/test/test-utils.js | 5 ++--- 20 files changed, 23 insertions(+), 59 deletions(-) rename packages/astro/test/fixtures/{ssr-serverless-manifest => ssr-split-manifest}/astro.config.mjs (100%) rename packages/astro/test/fixtures/{ssr-serverless-manifest => ssr-split-manifest}/package.json (69%) rename packages/astro/test/fixtures/{ssr-serverless-manifest => ssr-split-manifest}/src/pages/[...post].astro (100%) rename packages/astro/test/fixtures/{ssr-serverless-manifest => ssr-split-manifest}/src/pages/index.astro (100%) rename packages/astro/test/fixtures/{ssr-serverless-manifest => ssr-split-manifest}/src/pages/lorem.md (100%) rename packages/astro/test/fixtures/{ssr-serverless-manifest => ssr-split-manifest}/src/pages/zod.astro (100%) rename packages/astro/test/fixtures/{ssr-serverless => ssr-split}/astro.config.mjs (100%) rename packages/astro/test/fixtures/{ssr-serverless => ssr-split}/package.json (73%) rename packages/astro/test/fixtures/{ssr-serverless => ssr-split}/src/pages/blog/[slug].astro (100%) rename packages/astro/test/fixtures/{ssr-serverless => ssr-split}/src/pages/blog/about.astro (100%) rename packages/astro/test/fixtures/{ssr-serverless => ssr-split}/src/pages/index.astro (100%) delete mode 100644 packages/astro/test/ssr-serverless.test.js diff --git a/.changeset/wet-readers-join.md b/.changeset/wet-readers-join.md index 157d2d4c5e4d..1314283aeae9 100644 --- a/.changeset/wet-readers-join.md +++ b/.changeset/wet-readers-join.md @@ -2,7 +2,7 @@ 'astro': minor --- -Shipped a new SSR mode, called `serverless`. +Shipped a new SSR mode, called `split`. When enabled, Astro will emit a file for each page, which will render one single page. These files will be emitted inside `dist/pages`, and they will look like this: @@ -27,7 +27,4 @@ export default defineConfig({ split: true } }) -``` - -- `ssrMode` accepts two values, `"server"` or `"serverless"`. Default value, `"server"`. -- `serverlessEntryPrefix` allows to change the prefix of a serverless +``` \ No newline at end of file diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index b2967fc8797f..ee6098a67a10 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -841,14 +841,14 @@ export interface AstroUserConfig { /** * @docs - * @name build.ssrMode + * @name build.split * @type {boolean} * @default {false} + * @version 2.7.0 * @description - * Defines how the SSR code should be bundled. SSR code for "server" - * will be built in one single file. + * Defines how the SSR code should be bundled. * - * When "serverless" is passed, Astro will emit a file for each page. + * When `split` is `true`, Astro will emit a file for each page. * Each file emitted will render only one page. The pages will be emitted * inside a `dist/pages/` directory, and the emitted files will keep the same file paths * of the `src/pages` directory. diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index eb9cf9c46659..00b93cd09cce 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -29,7 +29,6 @@ import { createStylesheetElementSet, } from '../render/ssr-element.js'; import { matchRoute } from '../routing/match.js'; -import { AstroError, AstroErrorData } from '../errors/index.js'; export { deserializeManifest } from './common.js'; const clientLocalsSymbol = Symbol.for('astro.locals'); @@ -53,18 +52,6 @@ export class App { #base: string; #baseWithoutTrailingSlash: string; - async #retrievePage(routeData: RouteData) { - if (this.#manifest.pageMap) { - const pageModule = await this.#manifest.pageMap.get(routeData.component)!(); - return await pageModule.page(); - } else if (this.#manifest.pageModule) { - const pageModule = await this.#manifest.pageModule; - return await pageModule.page(); - } else { - throw new AstroError(AstroErrorData.FailedToFindPageMapSSR); - } - } - constructor(manifest: SSRManifest, streaming = true) { this.#manifest = manifest; this.#manifestData = { @@ -201,7 +188,9 @@ export class App { const importComponentInstance = this.#manifest.pageModule; return importComponentInstance; } else { - throw new AstroError(AstroErrorData.FailedToFindPageMapSSR); + throw new Error( + "Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue." + ); } } } diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index c6677966ee7b..7a13e4d08a73 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -3,10 +3,7 @@ import type { RouteData, SSRResult } from '../../@types/astro'; import type { PageOptions } from '../../vite-plugin-astro/types'; import { prependForwardSlash, removeFileExtension } from '../path.js'; import { viteID } from '../util.js'; -import { - ASTRO_PAGE_MODULE_ID , - getVirtualModulePageIdFromPath, -} from './plugins/plugin-pages.js'; +import { ASTRO_PAGE_MODULE_ID, getVirtualModulePageIdFromPath } from './plugins/plugin-pages.js'; import type { PageBuildData, StylesheetAsset, ViteID } from './types'; import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; @@ -85,7 +82,7 @@ export interface BuildInternals { // The SSR entry chunk. Kept in internals to share between ssr/client build steps ssrEntryChunk?: Rollup.OutputChunk; entryPoints: Map; - ssrServerlessEntryChunks: Map; + ssrSplitEntryChunks: Map; componentMetadata: SSRResult['componentMetadata']; } @@ -116,7 +113,7 @@ export function createBuildInternals(): BuildInternals { discoveredScripts: new Set(), staticFiles: new Set(), componentMetadata: new Map(), - ssrServerlessEntryChunks: new Map(), + ssrSplitEntryChunks: new Map(), entryPoints: new Map(), }; } diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 871aa09ec4d9..65b995134465 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -217,7 +217,7 @@ function vitePluginSSRServerless( let shouldDeleteBundle = false; for (const moduleKey of Object.keys(chunk.modules)) { if (moduleKey.startsWith(RESOLVED_SERVERLESS_MODULE_ID)) { - internals.ssrServerlessEntryChunks.set(moduleKey, chunk); + internals.ssrSplitEntryChunks.set(moduleKey, chunk); storeEntryPoint(moduleKey, options, internals, chunk.fileName); shouldDeleteBundle = true; } @@ -257,12 +257,12 @@ export function pluginSSRServerless( return; } - if (internals.ssrServerlessEntryChunks.size === 0) { + if (internals.ssrSplitEntryChunks.size === 0) { throw new Error(`Did not generate an entry chunk for SSR serverless`); } const manifest = await createManifest(options, internals); - for (const [moduleName, chunk] of internals.ssrServerlessEntryChunks) { + for (const [moduleName, chunk] of internals.ssrSplitEntryChunks) { const code = injectManifest(manifest, chunk); mutate(chunk, 'server', code); } @@ -343,7 +343,7 @@ export async function createManifest( internals: BuildInternals ): Promise { if (buildOpts.settings.config.build.split) { - if (internals.ssrServerlessEntryChunks.size === 0) { + if (internals.ssrSplitEntryChunks.size === 0) { throw new Error(`Did not generate an entry chunk for SSR in serverless mode`); } } else { diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 7b30984fe918..6c00d1c0d701 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -121,14 +121,14 @@ export async function staticBuild(opts: StaticBuildOptions, internals: BuildInte case settings.config.output === 'static': { settings.timer.start('Static generate'); await generatePages(opts, internals); - // await cleanServerOutput(opts); + await cleanServerOutput(opts); settings.timer.end('Static generate'); return; } case isServerLikeOutput(settings.config): { settings.timer.start('Server generate'); await generatePages(opts, internals); - // await cleanStaticOutput(opts, internals); + await cleanStaticOutput(opts, internals); info(opts.logging, null, `\n${bgMagenta(black(' finalizing server assets '))}\n`); await ssrMoveAssets(opts); settings.timer.end('Server generate'); diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs b/packages/astro/test/fixtures/ssr-split-manifest/astro.config.mjs similarity index 100% rename from packages/astro/test/fixtures/ssr-serverless-manifest/astro.config.mjs rename to packages/astro/test/fixtures/ssr-split-manifest/astro.config.mjs diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/package.json b/packages/astro/test/fixtures/ssr-split-manifest/package.json similarity index 69% rename from packages/astro/test/fixtures/ssr-serverless-manifest/package.json rename to packages/astro/test/fixtures/ssr-split-manifest/package.json index bd1a5dd6bf37..b980cc8a7b2e 100644 --- a/packages/astro/test/fixtures/ssr-serverless-manifest/package.json +++ b/packages/astro/test/fixtures/ssr-split-manifest/package.json @@ -1,5 +1,5 @@ { - "name": "@test/ssr-serverless-manifest", + "name": "@test/ssr-split-manifest", "version": "0.0.0", "private": true, "dependencies": { diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/[...post].astro b/packages/astro/test/fixtures/ssr-split-manifest/src/pages/[...post].astro similarity index 100% rename from packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/[...post].astro rename to packages/astro/test/fixtures/ssr-split-manifest/src/pages/[...post].astro diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/index.astro b/packages/astro/test/fixtures/ssr-split-manifest/src/pages/index.astro similarity index 100% rename from packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/index.astro rename to packages/astro/test/fixtures/ssr-split-manifest/src/pages/index.astro diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/lorem.md b/packages/astro/test/fixtures/ssr-split-manifest/src/pages/lorem.md similarity index 100% rename from packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/lorem.md rename to packages/astro/test/fixtures/ssr-split-manifest/src/pages/lorem.md diff --git a/packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/zod.astro b/packages/astro/test/fixtures/ssr-split-manifest/src/pages/zod.astro similarity index 100% rename from packages/astro/test/fixtures/ssr-serverless-manifest/src/pages/zod.astro rename to packages/astro/test/fixtures/ssr-split-manifest/src/pages/zod.astro diff --git a/packages/astro/test/fixtures/ssr-serverless/astro.config.mjs b/packages/astro/test/fixtures/ssr-split/astro.config.mjs similarity index 100% rename from packages/astro/test/fixtures/ssr-serverless/astro.config.mjs rename to packages/astro/test/fixtures/ssr-split/astro.config.mjs diff --git a/packages/astro/test/fixtures/ssr-serverless/package.json b/packages/astro/test/fixtures/ssr-split/package.json similarity index 73% rename from packages/astro/test/fixtures/ssr-serverless/package.json rename to packages/astro/test/fixtures/ssr-split/package.json index f4e328bd35f6..d3a32c09c38a 100644 --- a/packages/astro/test/fixtures/ssr-serverless/package.json +++ b/packages/astro/test/fixtures/ssr-split/package.json @@ -1,5 +1,5 @@ { - "name": "@test/ssr-serverless", + "name": "@test/ssr-split", "version": "0.0.0", "private": true, "dependencies": { diff --git a/packages/astro/test/fixtures/ssr-serverless/src/pages/blog/[slug].astro b/packages/astro/test/fixtures/ssr-split/src/pages/blog/[slug].astro similarity index 100% rename from packages/astro/test/fixtures/ssr-serverless/src/pages/blog/[slug].astro rename to packages/astro/test/fixtures/ssr-split/src/pages/blog/[slug].astro diff --git a/packages/astro/test/fixtures/ssr-serverless/src/pages/blog/about.astro b/packages/astro/test/fixtures/ssr-split/src/pages/blog/about.astro similarity index 100% rename from packages/astro/test/fixtures/ssr-serverless/src/pages/blog/about.astro rename to packages/astro/test/fixtures/ssr-split/src/pages/blog/about.astro diff --git a/packages/astro/test/fixtures/ssr-serverless/src/pages/index.astro b/packages/astro/test/fixtures/ssr-split/src/pages/index.astro similarity index 100% rename from packages/astro/test/fixtures/ssr-serverless/src/pages/index.astro rename to packages/astro/test/fixtures/ssr-split/src/pages/index.astro diff --git a/packages/astro/test/ssr-serverless-manifest.test.js b/packages/astro/test/ssr-serverless-manifest.test.js index de4297b4381f..3ffffc26710f 100644 --- a/packages/astro/test/ssr-serverless-manifest.test.js +++ b/packages/astro/test/ssr-serverless-manifest.test.js @@ -12,7 +12,7 @@ describe('astro:ssr-manifest, split', () => { before(async () => { fixture = await loadFixture({ - root: './fixtures/ssr-serverless-manifest/', + root: './fixtures/ssr-split-manifest/', output: 'server', adapter: testAdapter({ setEntries(entries) { diff --git a/packages/astro/test/ssr-serverless.test.js b/packages/astro/test/ssr-serverless.test.js deleted file mode 100644 index de14d784c69c..000000000000 --- a/packages/astro/test/ssr-serverless.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import { loadFixture } from './test-utils.js'; -import { expect } from 'chai'; -import testAdapter from './test-adapter.js'; - -describe('SSR serverless support', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/ssr-serverless/', - adapter: testAdapter(), - }); - await fixture.build(); - }); - - it('SSR pages require zero config', async () => {}); -}); diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 30fdf008b438..ef1c18adadf9 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -13,8 +13,7 @@ import dev from '../dist/core/dev/index.js'; import { nodeLogDestination } from '../dist/core/logger/node.js'; import preview from '../dist/core/preview/index.js'; import { check } from '../dist/cli/check/index.js'; -import path from 'path'; -import { extname } from 'node:path'; +import path from 'node:path'; // polyfill WebAPIs to globalThis for Node v12, Node v14, and Node v16 polyfill(globalThis, { @@ -251,7 +250,7 @@ export async function loadFixture(inlineConfig) { const pathComponents = pagePath.split(path.sep); const lastPathComponent = pathComponents.pop(); if (lastPathComponent) { - const extension = extname(lastPathComponent); + const extension = path.extname(lastPathComponent); if (extension.length > 0) { const newFileName = `entry.${lastPathComponent}`; pagePath = `${[...pathComponents, newFileName].join(path.sep)}.mjs`; From c0e8713f10c415b6fbed19276c4e3daf60c941bb Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 19 Jun 2023 09:36:05 +0100 Subject: [PATCH 11/20] Apply suggestions from code review Co-authored-by: Sarah Rainsberger --- .changeset/wet-readers-join.md | 7 ++++--- packages/astro/src/@types/astro.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.changeset/wet-readers-join.md b/.changeset/wet-readers-join.md index 1314283aeae9..48606920bf9d 100644 --- a/.changeset/wet-readers-join.md +++ b/.changeset/wet-readers-join.md @@ -2,8 +2,8 @@ 'astro': minor --- -Shipped a new SSR mode, called `split`. -When enabled, Astro will emit a file for each page, which will render one single page. +Shipped a new SSR build configuration mode: `split`. +When enabled, Astro will "split" the single `entry.mjs` file and instead emit a separate file to render each individual page during the build process. These files will be emitted inside `dist/pages`, and they will look like this: @@ -15,9 +15,10 @@ These files will be emitted inside `dist/pages`, and they will look like this: │ └── entry.index.astro.mjs ``` -To enable and customise this mode, new options are now available: +To enable, set `build.split: true` in your Astro config: ```js +// src/astro.config.mjs export default defineConfig({ output: "server", adapter: node({ diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index ee6098a67a10..8a2df2501d76 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -846,7 +846,7 @@ export interface AstroUserConfig { * @default {false} * @version 2.7.0 * @description - * Defines how the SSR code should be bundled. + * Defines how the SSR code should be bundled when built. * * When `split` is `true`, Astro will emit a file for each page. * Each file emitted will render only one page. The pages will be emitted From 9f68bc5e09e7d6f752151b09e3078d7692f38ccc Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 19 Jun 2023 09:36:48 +0100 Subject: [PATCH 12/20] chore: remove unused fixture --- .../astro/test/fixtures/ssr-split/astro.config.mjs | 7 ------- packages/astro/test/fixtures/ssr-split/package.json | 8 -------- .../fixtures/ssr-split/src/pages/blog/[slug].astro | 0 .../fixtures/ssr-split/src/pages/blog/about.astro | 0 .../test/fixtures/ssr-split/src/pages/index.astro | 12 ------------ ...s-manifest.test.js => ssr-split-manifest.test.js} | 0 6 files changed, 27 deletions(-) delete mode 100644 packages/astro/test/fixtures/ssr-split/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/ssr-split/package.json delete mode 100644 packages/astro/test/fixtures/ssr-split/src/pages/blog/[slug].astro delete mode 100644 packages/astro/test/fixtures/ssr-split/src/pages/blog/about.astro delete mode 100644 packages/astro/test/fixtures/ssr-split/src/pages/index.astro rename packages/astro/test/{ssr-serverless-manifest.test.js => ssr-split-manifest.test.js} (100%) diff --git a/packages/astro/test/fixtures/ssr-split/astro.config.mjs b/packages/astro/test/fixtures/ssr-split/astro.config.mjs deleted file mode 100644 index 171de39d9e24..000000000000 --- a/packages/astro/test/fixtures/ssr-split/astro.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'astro/config'; -export default defineConfig({ - build: { - split: true - }, - output: "server" -}) \ No newline at end of file diff --git a/packages/astro/test/fixtures/ssr-split/package.json b/packages/astro/test/fixtures/ssr-split/package.json deleted file mode 100644 index d3a32c09c38a..000000000000 --- a/packages/astro/test/fixtures/ssr-split/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/ssr-split", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/fixtures/ssr-split/src/pages/blog/[slug].astro b/packages/astro/test/fixtures/ssr-split/src/pages/blog/[slug].astro deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/astro/test/fixtures/ssr-split/src/pages/blog/about.astro b/packages/astro/test/fixtures/ssr-split/src/pages/blog/about.astro deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/astro/test/fixtures/ssr-split/src/pages/index.astro b/packages/astro/test/fixtures/ssr-split/src/pages/index.astro deleted file mode 100644 index ff484b4bb438..000000000000 --- a/packages/astro/test/fixtures/ssr-split/src/pages/index.astro +++ /dev/null @@ -1,12 +0,0 @@ - - - Testing - - - - - diff --git a/packages/astro/test/ssr-serverless-manifest.test.js b/packages/astro/test/ssr-split-manifest.test.js similarity index 100% rename from packages/astro/test/ssr-serverless-manifest.test.js rename to packages/astro/test/ssr-split-manifest.test.js From 4274f9de626634ad454d646674fa31d7c7bbae15 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 19 Jun 2023 09:44:48 +0100 Subject: [PATCH 13/20] rebase --- packages/astro/src/core/build/static-build.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 6c00d1c0d701..ce858c27c61e 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -177,10 +177,11 @@ async function ssrBuild( if (chunkInfo.facadeModuleId?.startsWith(ASTRO_PAGE_RESOLVED_MODULE_ID)) { return makeAstroPageEntryPointFileName( ASTRO_PAGE_RESOLVED_MODULE_ID, - chunkInfo.facadeModuleId + chunkInfo.facadeModuleId, + allPages ); } else if (chunkInfo.facadeModuleId?.startsWith(RESOLVED_SERVERLESS_MODULE_ID)) { - return makeServerlessEntryPointFileName(chunkInfo.facadeModuleId, opts); + return makeSplitEntryPointFileName(chunkInfo.facadeModuleId, opts); } else if (chunkInfo.facadeModuleId === MIDDLEWARE_MODULE_ID) { return 'middleware.mjs'; } else if (chunkInfo.facadeModuleId === SSR_VIRTUAL_MODULE_ID) { @@ -434,7 +435,11 @@ async function ssrMoveAssets(opts: StaticBuildOptions) { * @param facadeModuleId string * @param pages AllPagesData */ -function makeAstroPageEntryPointFileName(prefix: string, facadeModuleId: string, pages: AllPagesData) { +function makeAstroPageEntryPointFileName( + prefix: string, + facadeModuleId: string, + pages: AllPagesData +) { const pageModuleId = facadeModuleId .replace(prefix, '') .replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.'); @@ -457,10 +462,11 @@ function makeAstroPageEntryPointFileName(prefix: string, facadeModuleId: string, * @param facadeModuleId * @param opts */ -function makeServerlessEntryPointFileName(facadeModuleId: string, opts: StaticBuildOptions) { +function makeSplitEntryPointFileName(facadeModuleId: string, opts: StaticBuildOptions) { const filePath = `${makeAstroPageEntryPointFileName( RESOLVED_SERVERLESS_MODULE_ID, - facadeModuleId + facadeModuleId, + opts.allPages )}`; const pathComponents = filePath.split(path.sep); From 36602c6ea687dae54d17e30af0ee29c5ba726df6 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 19 Jun 2023 10:07:01 +0100 Subject: [PATCH 14/20] fix: run ssr hook in the correct order --- .../astro/src/core/build/plugins/plugin-ssr.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 65b995134465..011049c7fb45 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -136,14 +136,14 @@ export function pluginSSRServer( internals.ssrEntryChunk.fileName = options.settings.config.build.serverEntry; const manifest = await createManifest(options, internals); - const code = await injectManifest(manifest, internals.ssrEntryChunk); - mutate(internals.ssrEntryChunk, 'server', code); await runHookBuildSsr({ config: options.settings.config, manifest, logging: options.logging, entryPoints: internals.entryPoints, }); + const code = injectManifest(manifest, internals.ssrEntryChunk); + mutate(internals.ssrEntryChunk, 'server', code); }, }, }; @@ -262,16 +262,16 @@ export function pluginSSRServerless( } const manifest = await createManifest(options, internals); - for (const [moduleName, chunk] of internals.ssrSplitEntryChunks) { - const code = injectManifest(manifest, chunk); - mutate(chunk, 'server', code); - } await runHookBuildSsr({ config: options.settings.config, manifest, logging: options.logging, entryPoints: internals.entryPoints, }); + for (const [moduleName, chunk] of internals.ssrSplitEntryChunks) { + const code = injectManifest(manifest, chunk); + mutate(chunk, 'server', code); + } }, }, }; @@ -355,7 +355,7 @@ export async function createManifest( // Add assets from the client build. const clientStatics = new Set( await glob('**/*', { - cwd: fileURLToPath(buildOpts.buildConfig.client), + cwd: fileURLToPath(buildOpts.settings.config.build.client), }) ); for (const file of clientStatics) { From de3eb4c3399cfc61ee76d03fcd440f1fbc054267 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 19 Jun 2023 11:20:30 +0100 Subject: [PATCH 15/20] fix: changed way of how the entry points are computed, passing `RouteData[]` --- .../astro/src/core/build/plugins/index.ts | 6 +-- .../src/core/build/plugins/plugin-ssr.ts | 30 +++++++-------- packages/astro/src/core/build/static-build.ts | 38 +++++++++++-------- .../astro/test/ssr-split-manifest.test.js | 10 +++-- packages/astro/test/test-adapter.js | 7 +++- packages/astro/test/test-utils.js | 17 +++------ 6 files changed, 58 insertions(+), 50 deletions(-) diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts index c64fe66f5430..4563bb6964d7 100644 --- a/packages/astro/src/core/build/plugins/index.ts +++ b/packages/astro/src/core/build/plugins/index.ts @@ -11,7 +11,7 @@ import { pluginMiddleware } from './plugin-middleware.js'; import { pluginPages } from './plugin-pages.js'; import { pluginPrerender } from './plugin-prerender.js'; import { pluginRenderers } from './plugin-renderers.js'; -import { pluginSSRServer, pluginSSRServerless } from './plugin-ssr.js'; +import { pluginSSR, pluginSSRSplit } from './plugin-ssr.js'; export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) { register(pluginComponentEntry(internals)); @@ -26,6 +26,6 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP register(pluginPrerender(options, internals)); register(astroConfigBuildPlugin(options, internals)); register(pluginHoistedScripts(options, internals)); - register(pluginSSRServer(options, internals)); - register(pluginSSRServerless(options, internals)); + register(pluginSSR(options, internals)); + register(pluginSSRSplit(options, internals)); } diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts index 011049c7fb45..f9ba9190922f 100644 --- a/packages/astro/src/core/build/plugins/plugin-ssr.ts +++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts @@ -24,7 +24,7 @@ const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID; const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g'); -function vitePluginSSRServer( +function vitePluginSSR( internals: BuildInternals, adapter: AstroAdapter, options: StaticBuildOptions @@ -101,7 +101,7 @@ function vitePluginSSRServer( }; } -export function pluginSSRServer( +export function pluginSSR( options: StaticBuildOptions, internals: BuildInternals ): AstroBuildPlugin { @@ -112,7 +112,7 @@ export function pluginSSRServer( 'build:before': () => { let vitePlugin = ssr && !options.settings.config.build.split - ? vitePluginSSRServer(internals, options.settings.adapter!, options) + ? vitePluginSSR(internals, options.settings.adapter!, options) : undefined; return { @@ -149,35 +149,35 @@ export function pluginSSRServer( }; } -export const SERVERLESS_MODULE_ID = '@astro-page-serverless:'; -export const RESOLVED_SERVERLESS_MODULE_ID = '\0@astro-page-serverless:'; +export const SPLIT_MODULE_ID = '@astro-page-split:'; +export const RESOLVED_SPLIT_MODULE_ID = '\0@astro-page-split:'; -function vitePluginSSRServerless( +function vitePluginSSRSplit( internals: BuildInternals, adapter: AstroAdapter, options: StaticBuildOptions ): VitePlugin { return { - name: '@astrojs/vite-plugin-astro-ssr-serverless', + name: '@astrojs/vite-plugin-astro-ssr-split', enforce: 'post', options(opts) { if (options.settings.config.build.split) { const inputs: Set = new Set(); for (const path of Object.keys(options.allPages)) { - inputs.add(getVirtualModulePageNameFromPath(SERVERLESS_MODULE_ID, path)); + inputs.add(getVirtualModulePageNameFromPath(SPLIT_MODULE_ID, path)); } return addRollupInput(opts, Array.from(inputs)); } }, resolveId(id) { - if (id.startsWith(SERVERLESS_MODULE_ID)) { + if (id.startsWith(SPLIT_MODULE_ID)) { return '\0' + id; } }, async load(id) { - if (id.startsWith(RESOLVED_SERVERLESS_MODULE_ID)) { + if (id.startsWith(RESOLVED_SPLIT_MODULE_ID)) { const { settings: { config }, allPages, @@ -186,7 +186,7 @@ function vitePluginSSRServerless( const contents: string[] = []; const exports: string[] = []; - const path = getPathFromVirtualModulePageName(RESOLVED_SERVERLESS_MODULE_ID, id); + const path = getPathFromVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, id); const virtualModuleName = getVirtualModulePageNameFromPath(ASTRO_PAGE_MODULE_ID, path); let module = await this.resolve(virtualModuleName); if (module) { @@ -216,7 +216,7 @@ function vitePluginSSRServerless( } let shouldDeleteBundle = false; for (const moduleKey of Object.keys(chunk.modules)) { - if (moduleKey.startsWith(RESOLVED_SERVERLESS_MODULE_ID)) { + if (moduleKey.startsWith(RESOLVED_SPLIT_MODULE_ID)) { internals.ssrSplitEntryChunks.set(moduleKey, chunk); storeEntryPoint(moduleKey, options, internals, chunk.fileName); shouldDeleteBundle = true; @@ -230,7 +230,7 @@ function vitePluginSSRServerless( }; } -export function pluginSSRServerless( +export function pluginSSRSplit( options: StaticBuildOptions, internals: BuildInternals ): AstroBuildPlugin { @@ -241,7 +241,7 @@ export function pluginSSRServerless( 'build:before': () => { let vitePlugin = ssr && options.settings.config.build.split - ? vitePluginSSRServerless(internals, options.settings.adapter!, options) + ? vitePluginSSRSplit(internals, options.settings.adapter!, options) : undefined; return { @@ -379,7 +379,7 @@ function storeEntryPoint( internals: BuildInternals, fileName: string ) { - const componentPath = getPathFromVirtualModulePageName(RESOLVED_SERVERLESS_MODULE_ID, moduleKey); + const componentPath = getPathFromVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, moduleKey); for (const [page, pageData] of Object.entries(options.allPages)) { if (componentPath == page) { const publicPath = fileURLToPath(options.settings.config.outDir); diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index ce858c27c61e..9e08bcc4ee90 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -27,11 +27,12 @@ import { registerAllPlugins } from './plugins/index.js'; import { MIDDLEWARE_MODULE_ID } from './plugins/plugin-middleware.js'; import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js'; import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js'; -import { RESOLVED_SERVERLESS_MODULE_ID, SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js'; +import { RESOLVED_SPLIT_MODULE_ID, SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js'; import type { AllPagesData, PageBuildData, StaticBuildOptions } from './types'; import { getTimeStat } from './util.js'; import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; import { extname } from 'node:path'; +import type { RouteData } from '../../@types/astro'; export async function viteBuild(opts: StaticBuildOptions) { const { allPages, settings } = opts; @@ -146,7 +147,7 @@ async function ssrBuild( const { allPages, settings, viteConfig } = opts; const ssr = isServerLikeOutput(settings.config); const out = ssr ? opts.buildConfig.server : getOutDirWithinCwd(settings.config.outDir); - + const routes = Object.values(allPages).map((pd) => pd.route); const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input); const viteBuildConfig: vite.InlineConfig = { @@ -178,10 +179,10 @@ async function ssrBuild( return makeAstroPageEntryPointFileName( ASTRO_PAGE_RESOLVED_MODULE_ID, chunkInfo.facadeModuleId, - allPages + routes ); - } else if (chunkInfo.facadeModuleId?.startsWith(RESOLVED_SERVERLESS_MODULE_ID)) { - return makeSplitEntryPointFileName(chunkInfo.facadeModuleId, opts); + } else if (chunkInfo.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) { + return makeSplitEntryPointFileName(chunkInfo.facadeModuleId, routes); } else if (chunkInfo.facadeModuleId === MIDDLEWARE_MODULE_ID) { return 'middleware.mjs'; } else if (chunkInfo.facadeModuleId === SSR_VIRTUAL_MODULE_ID) { @@ -427,46 +428,51 @@ async function ssrMoveAssets(opts: StaticBuildOptions) { * Input: `@astro-page:../node_modules/my-dep/injected@_@astro` * Output: `pages/injected.mjs` * - * 1. We clean the `facadeModuleId` by removing the `@astro-page:` prefix and `@_@` suffix + * 1. We clean the `facadeModuleId` by removing the `ASTRO_PAGE_MODULE_ID` prefix and `ASTRO_PAGE_EXTENSION_POST_PATTERN`. * 2. We find the matching route pattern in the manifest (or fallback to the cleaned module id) * 3. We replace square brackets with underscore (`[slug]` => `_slug_`) and `...` with `` (`[...slug]` => `_---slug_`). * 4. We append the `.mjs` extension, so the file will always be an ESM module * + * @param prefix string * @param facadeModuleId string * @param pages AllPagesData */ -function makeAstroPageEntryPointFileName( +export function makeAstroPageEntryPointFileName( prefix: string, facadeModuleId: string, - pages: AllPagesData + routes: RouteData[] ) { const pageModuleId = facadeModuleId .replace(prefix, '') .replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.'); - let name = pages[pageModuleId]?.route?.route ?? pageModuleId; + let route = routes.find((routeData) => { + return routeData.route === pageModuleId; + }); + let name = pageModuleId; + if (route) { + name = route.route; + } if (name.endsWith('/')) name += 'index'; - return `pages${name.replaceAll('[', '_').replaceAll(']', '_').replaceAll('...', '---')}.mjs`; + return `${name.replaceAll('[', '_').replaceAll(']', '_').replaceAll('...', '---')}.mjs`; } /** - * This function attempts to prepend the `serverlessEntryPrefix` prefix to each entry point emitted. - * * The `facadeModuleId` has a shape like: \0@astro-serverless-page:src/pages/index@_@astro. * * 1. We call `makeAstroPageEntryPointFileName` which normalise its name, making it like a file path * 2. We split the file path using the file system separator and attempt to retrieve the last entry * 3. The last entry should be the file - * 4. We prepend the file name with `serverlessEntryPrefix` + * 4. We prepend the file name with `entry.` * 5. We built the file path again, using the new entry built in the previous step * * @param facadeModuleId * @param opts */ -function makeSplitEntryPointFileName(facadeModuleId: string, opts: StaticBuildOptions) { +export function makeSplitEntryPointFileName(facadeModuleId: string, routes: RouteData[]) { const filePath = `${makeAstroPageEntryPointFileName( - RESOLVED_SERVERLESS_MODULE_ID, + RESOLVED_SPLIT_MODULE_ID, facadeModuleId, - opts.allPages + routes )}`; const pathComponents = filePath.split(path.sep); diff --git a/packages/astro/test/ssr-split-manifest.test.js b/packages/astro/test/ssr-split-manifest.test.js index 3ffffc26710f..5005f6279953 100644 --- a/packages/astro/test/ssr-split-manifest.test.js +++ b/packages/astro/test/ssr-split-manifest.test.js @@ -9,23 +9,27 @@ describe('astro:ssr-manifest, split', () => { /** @type {import('./test-utils').Fixture} */ let fixture; let entryPoints; + let currentRoutes; before(async () => { fixture = await loadFixture({ root: './fixtures/ssr-split-manifest/', output: 'server', adapter: testAdapter({ - setEntries(entries) { + setEntryPoints(entries) { entryPoints = entries; }, + setRoutes(routes) { + currentRoutes = routes; + }, }), }); await fixture.build(); }); it('should be able to render a specific entry point', async () => { - const pagePath = 'pages/index.astro'; - const app = await fixture.loadEntryPoint(pagePath); + const pagePath = 'src/pages/index.astro'; + const app = await fixture.loadEntryPoint(pagePath, currentRoutes); const request = new Request('http://example.com/'); const response = await app.render(request); const html = await response.text(); diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js index 723943dcbd49..af5a7777b8c1 100644 --- a/packages/astro/test/test-adapter.js +++ b/packages/astro/test/test-adapter.js @@ -5,7 +5,7 @@ import { viteID } from '../dist/core/util.js'; * @returns {import('../src/@types/astro').AstroIntegration} */ export default function ( - { provideAddress = true, extendAdapter, setEntries: setEntryPoints = undefined } = { + { provideAddress = true, extendAdapter, setEntryPoints = undefined, setRoutes = undefined } = { provideAddress: true, } ) { @@ -79,6 +79,11 @@ export default function ( setEntryPoints(entryPoints); } }, + 'astro:build:done': ({ routes }) => { + if (setRoutes) { + setRoutes(routes); + } + }, }, }; } diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index ef1c18adadf9..4d63cb438fa6 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -13,7 +13,7 @@ import dev from '../dist/core/dev/index.js'; import { nodeLogDestination } from '../dist/core/logger/node.js'; import preview from '../dist/core/preview/index.js'; import { check } from '../dist/cli/check/index.js'; -import path from 'node:path'; +import { getVirtualModulePageNameFromPath } from '../dist/core/build/plugins/util.js'; // polyfill WebAPIs to globalThis for Node v12, Node v14, and Node v16 polyfill(globalThis, { @@ -246,17 +246,10 @@ export async function loadFixture(inlineConfig) { app.manifest = manifest; return app; }, - loadEntryPoint: async (pagePath, streaming) => { - const pathComponents = pagePath.split(path.sep); - const lastPathComponent = pathComponents.pop(); - if (lastPathComponent) { - const extension = path.extname(lastPathComponent); - if (extension.length > 0) { - const newFileName = `entry.${lastPathComponent}`; - pagePath = `${[...pathComponents, newFileName].join(path.sep)}.mjs`; - } - } - const url = new URL(`./server/${pagePath}?id=${fixtureId}`, config.outDir); + loadEntryPoint: async (pagePath, routes, streaming) => { + const virtualModule = getVirtualModulePageNameFromPath(RESOLVED_SPLIT_MODULE_ID, pagePath); + const filePath = makeSplitEntryPointFileName(virtualModule, routes); + const url = new URL(`./server/${filePath}?id=${fixtureId}`, config.outDir); const { createApp, manifest, middleware } = await import(url); const app = createApp(streaming); app.manifest = manifest; From 958703c3fbb80bea378ccb9ed2e78821cc49db7e Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Mon, 19 Jun 2023 13:06:57 +0100 Subject: [PATCH 16/20] chore: correctly handle injected routes --- packages/astro/src/core/build/static-build.ts | 6 +++++- packages/astro/test/test-utils.js | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 9e08bcc4ee90..e783c28b11cb 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -453,7 +453,11 @@ export function makeAstroPageEntryPointFileName( name = route.route; } if (name.endsWith('/')) name += 'index'; - return `${name.replaceAll('[', '_').replaceAll(']', '_').replaceAll('...', '---')}.mjs`; + const fileName = `${name.replaceAll('[', '_').replaceAll(']', '_').replaceAll('...', '---')}.mjs`; + if (name.startsWith('..')) { + return `pages${fileName}`; + } + return fileName; } /** diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 4d63cb438fa6..11b181779bbc 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -14,6 +14,8 @@ import { nodeLogDestination } from '../dist/core/logger/node.js'; import preview from '../dist/core/preview/index.js'; import { check } from '../dist/cli/check/index.js'; import { getVirtualModulePageNameFromPath } from '../dist/core/build/plugins/util.js'; +import { RESOLVED_SPLIT_MODULE_ID } from '../dist/core/build/plugins/plugin-ssr.js'; +import { makeSplitEntryPointFileName } from '../dist/core/build/static-build.js'; // polyfill WebAPIs to globalThis for Node v12, Node v14, and Node v16 polyfill(globalThis, { From c655370b81f6faa03d2e21ebdedcb89a0393acbd Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 21 Jun 2023 12:46:41 +0100 Subject: [PATCH 17/20] Update .changeset/wet-readers-join.md Co-authored-by: Sarah Rainsberger --- .changeset/wet-readers-join.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/wet-readers-join.md b/.changeset/wet-readers-join.md index 48606920bf9d..abf77e6c97ca 100644 --- a/.changeset/wet-readers-join.md +++ b/.changeset/wet-readers-join.md @@ -5,7 +5,7 @@ Shipped a new SSR build configuration mode: `split`. When enabled, Astro will "split" the single `entry.mjs` file and instead emit a separate file to render each individual page during the build process. -These files will be emitted inside `dist/pages`, and they will look like this: +These files will be emitted inside `dist/pages`, mirroring the directory structure of your page files in `src/pages/`, for example: ``` ├── pages From 47d5a23451b3bd7b906e1517a8e8f33aa6ba1c7a Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 21 Jun 2023 12:49:27 +0100 Subject: [PATCH 18/20] remove plain text --- packages/astro/src/@types/astro.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 8a2df2501d76..1a5679f03e9c 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -853,15 +853,6 @@ export interface AstroUserConfig { * inside a `dist/pages/` directory, and the emitted files will keep the same file paths * of the `src/pages` directory. * - * Inside the `dist/` directory, the pages will look like this: - * ```plaintext - * ├── pages - * │ ├── blog - * │ │ ├── entry._slug_.astro.mjs - * │ │ └── entry.about.astro.mjs - * │ └── entry.index.astro.mjs - * ``` - * * ```js * { * build: { From 114eec496b787248e9966acea4026f4c3d9971df Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 21 Jun 2023 13:10:54 +0100 Subject: [PATCH 19/20] Update packages/astro/src/core/errors/errors-data.ts Co-authored-by: Sarah Rainsberger --- packages/astro/src/core/errors/errors-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index be0ab96725ba..b8babd91a06d 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -826,7 +826,7 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati title: "Astro couldn't find the correct page to render", code: 4003, message: - "Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue.", + "Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error. Please file an issue.", }, /** * @docs From cd062960e87192868559c17a9ef1c4a062ffa2af Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 21 Jun 2023 13:13:20 +0100 Subject: [PATCH 20/20] rebase --- pnpm-lock.yaml | 60 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29d581936ece..45c034404385 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3339,6 +3339,12 @@ importers: specifier: ^10.11.0 version: 10.13.2 + packages/astro/test/fixtures/ssr-split-manifest: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/static-build: dependencies: '@astrojs/preact': @@ -4421,7 +4427,7 @@ importers: version: 9.2.2 vite: specifier: ^4.3.1 - version: 4.3.1(@types/node@18.16.3)(sass@1.52.2) + version: 4.3.1(@types/node@14.18.21) packages/integrations/netlify/test/edge-functions/fixtures/dynimport: dependencies: @@ -4945,7 +4951,7 @@ importers: version: 3.0.0(vite@4.3.1)(vue@3.2.47) '@vue/babel-plugin-jsx': specifier: ^1.1.1 - version: 1.1.1(@babel/core@7.21.8) + version: 1.1.1 '@vue/compiler-sfc': specifier: ^3.2.39 version: 3.2.39 @@ -9332,6 +9338,23 @@ packages: resolution: {integrity: sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==} dev: false + /@vue/babel-plugin-jsx@1.1.1: + resolution: {integrity: sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==} + dependencies: + '@babel/helper-module-imports': 7.21.4 + '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.18.2) + '@babel/template': 7.20.7 + '@babel/traverse': 7.18.2 + '@babel/types': 7.21.5 + '@vue/babel-helper-vue-transform-on': 1.0.2 + camelcase: 6.3.0 + html-tags: 3.3.1 + svg-tags: 1.0.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: false + /@vue/babel-plugin-jsx@1.1.1(@babel/core@7.21.8): resolution: {integrity: sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==} dependencies: @@ -17663,6 +17686,39 @@ packages: - supports-color dev: false + /vite@4.3.1(@types/node@14.18.21): + resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 14.18.21 + esbuild: 0.17.18 + postcss: 8.4.23 + rollup: 3.21.8 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /vite@4.3.1(@types/node@18.16.3)(sass@1.52.2): resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==} engines: {node: ^14.18.0 || >=16.0.0}