Skip to content

Commit

Permalink
feat: SSR split mode (#7220)
Browse files Browse the repository at this point in the history
Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
ematipico and sarah11918 authored Jun 21, 2023
1 parent c8bccb4 commit 459b5bd
Show file tree
Hide file tree
Showing 24 changed files with 702 additions and 133 deletions.
31 changes: 31 additions & 0 deletions .changeset/wet-readers-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'astro': minor
---

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`, mirroring the directory structure of your page files in `src/pages/`, for example:

```
├── pages
│ ├── blog
│ │ ├── entry._slug_.astro.mjs
│ │ └── entry.about.astro.mjs
│ └── entry.index.astro.mjs
```

To enable, set `build.split: true` in your Astro config:

```js
// src/astro.config.mjs
export default defineConfig({
output: "server",
adapter: node({
mode: "standalone"
}),
build: {
split: true
}
})
```
33 changes: 32 additions & 1 deletion packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,30 @@ export interface AstroUserConfig {
* ```
*/
inlineStylesheets?: 'always' | 'auto' | 'never';

/**
* @docs
* @name build.split
* @type {boolean}
* @default {false}
* @version 2.7.0
* @description
* 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
* inside a `dist/pages/` directory, and the emitted files will keep the same file paths
* of the `src/pages` directory.
*
* ```js
* {
* build: {
* split: true
* }
* }
* ```
*/
split?: boolean;
};

/**
Expand Down Expand Up @@ -1824,7 +1848,14 @@ export interface AstroIntegration {
'astro:server:setup'?: (options: { server: vite.ViteDevServer }) => void | Promise<void>;
'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise<void>;
'astro:server:done'?: () => void | Promise<void>;
'astro:build:ssr'?: (options: { manifest: SerializedSSRManifest }) => void | Promise<void>;
'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<RouteData, URL>;
}) => void | Promise<void>;
'astro:build:start'?: () => void | Promise<void>;
'astro:build:setup'?: (options: {
vite: vite.InlineConfig;
Expand Down
27 changes: 18 additions & 9 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type {
MiddlewareResponseHandler,
RouteData,
SSRElement,
SSRManifest,
} from '../../@types/astro';
import type { RouteInfo, SSRManifest as Manifest } from './types';

import type { RouteInfo } from './types';
import mime from 'mime';
import type { SinglePageBuiltModule } from '../build/types';
import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js';
Expand Down Expand Up @@ -41,7 +41,7 @@ export interface MatchOptions {

export class App {
#env: Environment;
#manifest: Manifest;
#manifest: SSRManifest;
#manifestData: ManifestData;
#routeDataToRouteInfo: Map<RouteData, RouteInfo>;
#encoder = new TextEncoder();
Expand All @@ -52,7 +52,7 @@ export class App {
#base: string;
#baseWithoutTrailingSlash: string;

constructor(manifest: Manifest, streaming = true) {
constructor(manifest: SSRManifest, streaming = true) {
this.#manifest = manifest;
this.#manifestData = {
routes: manifest.routes.map((route) => route.routeData),
Expand Down Expand Up @@ -175,14 +175,23 @@ export class App {
if (route.type === 'redirect') {
return RedirectSinglePageBuiltModule;
} else {
const importComponentInstance = this.#manifest.pageMap.get(route.component);
if (!importComponentInstance) {
if (this.#manifest.pageMap) {
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 if (this.#manifest.pageModule) {
const importComponentInstance = this.#manifest.pageModule;
return importComponentInstance;
} else {
throw new Error(
`Unexpectedly unable to find a component instance for route ${route.route}`
"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."
);
}
const built = await importComponentInstance();
return built;
}
}

Expand Down
10 changes: 6 additions & 4 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ export interface RouteInfo {
export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
routeData: SerializedRouteData;
};
type ImportComponentInstance = () => Promise<SinglePageBuiltModule>;

export interface SSRManifest {
export type ImportComponentInstance = () => Promise<SinglePageBuiltModule>;

export type SSRManifest = {
adapterName: string;
routes: RouteInfo[];
site?: string;
base?: string;
assetsPrefix?: string;
markdown: MarkdownRenderingOptions;
pageMap: Map<ComponentPath, ImportComponentInstance>;
renderers: SSRLoadedRenderer[];
/**
* Map of directive name (e.g. `load`) to the directive script code
Expand All @@ -48,7 +48,9 @@ export interface SSRManifest {
entryModules: Record<string, string>;
assets: Set<string>;
componentMetadata: SSRResult['componentMetadata'];
}
pageModule?: SinglePageBuiltModule;
pageMap?: Map<ComponentPath, ImportComponentInstance>;
};

export type SerializedSSRManifest = Omit<
SSRManifest,
Expand Down
13 changes: 7 additions & 6 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
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';
import {
ASTRO_PAGE_EXTENSION_POST_PATTERN,
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';

export interface BuildInternals {
/**
Expand Down Expand Up @@ -84,6 +81,8 @@ export interface BuildInternals {
staticFiles: Set<string>;
// The SSR entry chunk. Kept in internals to share between ssr/client build steps
ssrEntryChunk?: Rollup.OutputChunk;
entryPoints: Map<RouteData, URL>;
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
componentMetadata: SSRResult['componentMetadata'];
}

Expand Down Expand Up @@ -114,6 +113,8 @@ export function createBuildInternals(): BuildInternals {
discoveredScripts: new Set(),
staticFiles: new Set(),
componentMetadata: new Map(),
ssrSplitEntryChunks: new Map(),
entryPoints: new Map(),
};
}

Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/build/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { pluginSSR, pluginSSRSplit } from './plugin-ssr.js';

export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
register(pluginComponentEntry(internals));
Expand All @@ -27,4 +27,5 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
register(astroConfigBuildPlugin(options, internals));
register(pluginHoistedScripts(options, internals));
register(pluginSSR(options, internals));
register(pluginSSRSplit(options, internals));
}
17 changes: 5 additions & 12 deletions packages/astro/src/core/build/plugins/plugin-pages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { extname } from 'node:path';
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';
Expand All @@ -7,12 +7,10 @@ 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:';

// This is an arbitrary string that we are going to replace the dot of the extension
export const ASTRO_PAGE_EXTENSION_POST_PATTERN = '@_@';
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;
Expand Down Expand Up @@ -64,13 +62,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) {
Expand Down
Loading

0 comments on commit 459b5bd

Please sign in to comment.