Skip to content

Commit

Permalink
refactor: emit pages as physical entry points (#7193)
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico authored May 25, 2023
1 parent f5a8cff commit 8b041bf
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 92 deletions.
6 changes: 6 additions & 0 deletions .changeset/dry-taxis-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'astro': patch
---

Refactor how pages are emitted during the internal bundling. Now each
page is emitted as a separate entry point.
2 changes: 1 addition & 1 deletion packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1691,7 +1691,7 @@ export interface APIContext<Props extends Record<string, any> = Record<string, a
*
* export const onRequest = defineMiddleware((context, next) => {
* context.locals.greeting = "Hello!";
* next();
* return next();
* });
* ```
* Inside a `.astro` file:
Expand Down
8 changes: 4 additions & 4 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ export { deserializeManifest } from './common.js';

const clientLocalsSymbol = Symbol.for('astro.locals');

export const pagesVirtualModuleId = '@astrojs-pages-virtual-entry';
export const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId;
const responseSentSymbol = Symbol.for('astro.responseSent');

export interface MatchOptions {
Expand Down Expand Up @@ -139,7 +137,8 @@ export class App {
defaultStatus = 404;
}

let mod = await this.#manifest.pageMap.get(routeData.component)!();
let page = await this.#manifest.pageMap.get(routeData.component)!();
let mod = await page.page();

if (routeData.type === 'page') {
let response = await this.#renderPage(request, routeData, mod, defaultStatus);
Expand All @@ -148,7 +147,8 @@ export class App {
if (response.status === 500 || response.status === 404) {
const errorPageData = matchRoute('/' + response.status, this.#manifestData);
if (errorPageData && errorPageData.route !== routeData.route) {
mod = await this.#manifest.pageMap.get(errorPageData.component)!();
page = await this.#manifest.pageMap.get(errorPageData.component)!();
mod = await page.page();
try {
let errorResponse = await this.#renderPage(
request,
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
SSRLoadedRenderer,
SSRResult,
} from '../../@types/astro';
import type { SinglePageBuiltModule } from '../build/types';

export type ComponentPath = string;

Expand All @@ -31,7 +32,7 @@ export interface RouteInfo {
export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
routeData: SerializedRouteData;
};
type ImportComponentInstance = () => Promise<ComponentInstance>;
type ImportComponentInstance = () => Promise<SinglePageBuiltModule>;

export interface SSRManifest {
adapterName: string;
Expand Down
32 changes: 21 additions & 11 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {
generateImage as generateImageInternal,
getStaticImageList,
} from '../../assets/generate.js';
import { hasPrerenderedPages, type BuildInternals } from '../../core/build/internal.js';
import {
hasPrerenderedPages,
type BuildInternals,
eachPageDataFromEntryPoint,
} from '../../core/build/internal.js';
import {
prependForwardSlash,
removeLeadingForwardSlash,
Expand All @@ -47,11 +51,12 @@ import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
import { cssOrder, eachPageData, getPageDataByComponent, mergeInlineCss } from './internal.js';
import type {
PageBuildData,
SingleFileBuiltModule,
SinglePageBuiltModule,
StaticBuildOptions,
StylesheetAsset,
} from './types';
import { getTimeStat } from './util.js';
import { ASTRO_PAGE_MODULE_ID } from './plugins/plugin-pages';

function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
return (
Expand Down Expand Up @@ -99,18 +104,23 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
const verb = ssr ? 'prerendering' : 'generating';
info(opts.logging, null, `\n${bgGreen(black(` ${verb} static routes `))}`);

const ssrEntryURL = new URL('./' + serverEntry + `?time=${Date.now()}`, outFolder);
const ssrEntry = await import(ssrEntryURL.toString());
const builtPaths = new Set<string>();

if (ssr) {
for (const pageData of eachPageData(internals)) {
if (pageData.route.prerender)
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
if (pageData.route.prerender) {
const ssrEntryURLPage = new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
const ssrEntryPage = await import(ssrEntryURLPage.toString());

await generatePage(opts, internals, pageData, ssrEntryPage, builtPaths);
}
}
} else {
for (const pageData of eachPageData(internals)) {
await generatePage(opts, internals, pageData, ssrEntry, builtPaths);
for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
const ssrEntryURLPage = new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
const ssrEntryPage = await import(ssrEntryURLPage.toString());

await generatePage(opts, internals, pageData, ssrEntryPage, builtPaths);
}
}

Expand Down Expand Up @@ -153,7 +163,7 @@ async function generatePage(
opts: StaticBuildOptions,
internals: BuildInternals,
pageData: PageBuildData,
ssrEntry: SingleFileBuiltModule,
ssrEntry: SinglePageBuiltModule,
builtPaths: Set<string>
) {
let timeStart = performance.now();
Expand All @@ -169,7 +179,7 @@ async function generatePage(
.map(({ sheet }) => sheet)
.reduce(mergeInlineCss, []);

const pageModulePromise = ssrEntry.pageMap?.get(pageData.component);
const pageModulePromise = ssrEntry.page;
const middleware = ssrEntry.middleware;

if (!pageModulePromise) {
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/core/build/graph.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { GetModuleInfo, ModuleInfo } from 'rollup';

import { resolvedPagesVirtualModuleId } from '../app/index.js';
import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';

// This walks up the dependency graph and yields out each ModuleInfo object.
export function* walkParentInfos(
Expand Down Expand Up @@ -43,8 +43,8 @@ export function* walkParentInfos(
// it is imported by the top-level virtual module.
export function moduleIsTopLevelPage(info: ModuleInfo): boolean {
return (
info.importers[0] === resolvedPagesVirtualModuleId ||
info.dynamicImporters[0] == resolvedPagesVirtualModuleId
info.importers[0]?.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) ||
info.dynamicImporters[0]?.includes(ASTRO_PAGE_RESOLVED_MODULE_ID)
);
}

Expand Down
23 changes: 21 additions & 2 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Rollup } from 'vite';
import type { PageBuildData, StylesheetAsset, ViteID } from './types';

import type { 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 } from './plugins/plugin-pages.js';

export interface BuildInternals {
/**
Expand Down Expand Up @@ -97,7 +97,6 @@ export function createBuildInternals(): BuildInternals {
hoistedScriptIdToPagesMap,
entrySpecifierToBundleMap: new Map<string, string>(),
pageToBundleMap: new Map<string, string>(),

pagesByComponent: new Map(),
pageOptionsByPage: new Map(),
pagesByViteID: new Map(),
Expand Down Expand Up @@ -215,6 +214,26 @@ export function* eachPageData(internals: BuildInternals) {
yield* internals.pagesByComponent.values();
}

export function* eachPageDataFromEntryPoint(
internals: BuildInternals
): Generator<[PageBuildData, string]> {
for (const [entryPoint, filePath] of internals.entrySpecifierToBundleMap) {
if (entryPoint.includes(ASTRO_PAGE_MODULE_ID)) {
const [, pageName] = entryPoint.split(':');
const pageData = internals.pagesByComponent.get(
`${pageName.replace(ASTRO_PAGE_EXTENSION_POST_PATTERN, '.')}`
);
if (!pageData) {
throw new Error(
"Build failed. Astro couldn't find the emitted page from " + pageName + ' pattern'
);
}

yield [pageData, filePath];
}
}
}

export function hasPrerenderedPages(internals: BuildInternals) {
for (const pageData of eachPageData(internals)) {
if (pageData.route.prerender) {
Expand Down
144 changes: 144 additions & 0 deletions packages/astro/src/core/build/plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Plugin directory (WIP)

This file serves as developer documentation to explain how the internal plugins work


## `plugin-middleware`

This plugin is responsible to retrieve the `src/middleware.{ts.js}` file and emit an entry point during the SSR build.

The final file is emitted only if the user has the middleware file. The final name of the file is `middleware.mjs`.

This is **not** a virtual module. The plugin will try to resolve the physical file.

## `plugin-renderers`

This plugin is responsible to collect all the renderers inside an Astro application and emit them in a single file.

The emitted file is called `renderers.mjs`.

The emitted file has content similar to:

```js
const renderers = [Object.assign({"name":"astro:jsx","serverEntrypoint":"astro/jsx/server.js","jsxImportSource":"astro"}, { ssr: server_default }),];

export { renderers };
```

## `plugin-pages`

This plugin is responsible to collect all pages inside an Astro application, and emit a single entry point file for each page.

This plugin **will emit code** only when building a static site.

In order to achieve that, the plugin emits these pages as **virtual modules**. Doing so allows us to bypass:
- rollup resolution of the files
- possible plugins that get triggered when the name of the module has an extension e.g. `.astro`

The plugin does the following operations:
- loop through all the pages and collects their paths;
- with each path, we create a new [string](#plugin-pages-mapping-resolution) that will serve and virtual module for that particular page
- when resolving the page, we check if the `id` of the module starts with `@astro-page`
- once the module is resolved, we emit [the code of the module](#plugin-pages-code-generation)


### `plugin pages` mapping resolution

The mapping is as follows:

```
src/pages/index.astro => @astro-page:src/pages/index@_@astro
```

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.

This kind of patterns will then allow us to retrieve the path physical path of the
file back from that string. This is important for the [code generation](#plugin-pages-code-generation)



### `plugin pages` code generation

When generating the code of the page, we will import and export the following modules:
- the `renderers.mjs`
- the `middleware.mjs`
- the page, via dynamic import

The emitted code of each entry point will look like this:

```js
export { renderers } from '../renderers.mjs';
import { _ as _middleware } from '../middleware.mjs';
import '../chunks/astro.540fbe4e.mjs';

const page = () => import('../chunks/pages/index.astro.8aad0438.mjs');
const middleware = _middleware;

export { middleware, page };
```

If we have a `pages/` folder that looks like this:
```
├── blog
│ ├── first.astro
│ └── post.astro
├── first.astro
├── index.astro
├── issue.md
└── second.astro
```

The emitted entry points will be stored inside a `pages/` folder, and they
will look like this:
```
├── _astro
│ ├── first.132e69e0.css
│ ├── first.49cbf029.css
│ ├── post.a3e86c58.css
│ └── second.d178d0b2.css
├── chunks
│ ├── astro.540fbe4e.mjs
│ └── pages
│ ├── first.astro.493fa853.mjs
│ ├── index.astro.8aad0438.mjs
│ ├── issue.md.535b7d3b.mjs
│ ├── post.astro.26e892d9.mjs
│ └── second.astro.76540694.mjs
├── middleware.mjs
├── pages
│ ├── blog
│ │ ├── first.astro.mjs
│ │ └── post.astro.mjs
│ ├── first.astro.mjs
│ ├── index.astro.mjs
│ ├── issue.md.mjs
│ └── second.astro.mjs
└── renderers.mjs
```

Of course, all these files will be deleted by Astro at the end build.

## `plugin-ssr` (WIP)

This plugin is responsible to create a single `entry.mjs` file that will be used
in SSR.

This plugin **will emit code** only when building an **SSR** site.

The plugin will collect all the [virtual pages](#plugin-pages) and create
a JavaScript `Map`. These map will look like this:

```js
const _page$0 = () => import("../chunks/<INDEX.ASTRO_CHUNK>.mjs")
const _page$1 = () => import("../chunks/<ABOUT.ASTRO_CHUNK>.mjs")

const pageMap = new Map([
["src/pages/index.astro", _page$0],
["src/pages/about.astro", _page$1],
])
```

It will also import the [`renderers`](#plugin-renderers) virtual module
and the [`middleware`](#plugin-middleware) virtual module.

20 changes: 3 additions & 17 deletions packages/astro/src/core/build/plugins/plugin-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import type { AstroBuildPlugin } from '../plugin';
import type { StaticBuildOptions } from '../types';

export const MIDDLEWARE_MODULE_ID = '@astro-middleware';
export const RESOLVED_MIDDLEWARE_MODULE_ID = '\0@astro-middleware';

let inputs: Set<string> = new Set();
export function vitePluginMiddleware(
opts: StaticBuildOptions,
_internals: BuildInternals
Expand All @@ -21,26 +19,14 @@ export function vitePluginMiddleware(
}
},

resolveId(id) {
async resolveId(id) {
if (id === MIDDLEWARE_MODULE_ID && opts.settings.config.experimental.middleware) {
return RESOLVED_MIDDLEWARE_MODULE_ID;
}
},

async load(id) {
if (id === RESOLVED_MIDDLEWARE_MODULE_ID && opts.settings.config.experimental.middleware) {
const imports: string[] = [];
const exports: string[] = [];
let middlewareId = await this.resolve(
const middlewareId = await this.resolve(
`${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}`
);
if (middlewareId) {
imports.push(`import { onRequest } from "${middlewareId.id}"`);
exports.push(`export { onRequest }`);
return middlewareId.id;
}
const result = [imports.join('\n'), exports.join('\n')];

return result.join('\n');
}
},
};
Expand Down
Loading

0 comments on commit 8b041bf

Please sign in to comment.