Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: SSR split mode #7220

Merged
merged 20 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
ematipico marked this conversation as resolved.
Show resolved Hide resolved
// 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.
sarah11918 marked this conversation as resolved.
Show resolved Hide resolved
*
* ```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