Skip to content

Commit

Permalink
Remove functionPerRoute option (#11714)
Browse files Browse the repository at this point in the history
* Remove functionPerRoute option

* Remove more code

* Remove unused test util

* Linting

* Update tests to reflect new structure

* Add a changeset

* Update plugin

* Remove unused import
  • Loading branch information
matthewp authored Aug 19, 2024
1 parent 3822e57 commit 8a53517
Show file tree
Hide file tree
Showing 24 changed files with 40 additions and 543 deletions.
14 changes: 14 additions & 0 deletions .changeset/ten-students-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@astrojs/vercel': major
'astro': major
---

Remove support for functionPerRoute

This change removes support for the `functionPerRoute` option both in Astro and `@astrojs/vercel`.

This option made it so that each route got built as separate entrypoints so that they could be loaded as separate functions. The hope was that by doing this it would decrease the size of each function. However in practice routes use most of the same code, and increases in function size limitations made the potential upsides less important.

Additionally there are downsides to functionPerRoute, such as hitting limits on the number of functions per project. The feature also never worked with some Astro features like i18n domains and request rewriting.

Given this, the feature has been removed from Astro.
29 changes: 4 additions & 25 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { createRequest } from '../request.js';
import { matchRoute } from '../routing/match.js';
import { stringifyParams } from '../routing/params.js';
import { getOutputFilename, isServerLikeOutput } from '../util.js';
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
import { getOutFile, getOutFolder } from './common.js';
import { cssOrder, mergeInlineCss } from './internal.js';
import { BuildPipeline } from './pipeline.js';
import type {
Expand All @@ -47,10 +47,6 @@ import type {
} from './types.js';
import { getTimeStat, shouldAppendForwardSlash } from './util.js';

function createEntryURL(filePath: string, outFolder: URL) {
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
}

export async function generatePages(options: StaticBuildOptions, internals: BuildInternals) {
const generatePagesTimer = performance.now();
const ssr = isServerLikeOutput(options.settings.config);
Expand Down Expand Up @@ -80,10 +76,6 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
const pipeline = BuildPipeline.create({ internals, manifest, options });
const { config, logger } = pipeline;

const outFolder = ssr
? options.settings.config.build.server
: getOutDirWithinCwd(options.settings.config.outDir);

// HACK! `astro:assets` relies on a global to know if its running in dev, prod, ssr, ssg, full moon
// If we don't delete it here, it's technically not impossible (albeit improbable) for it to leak
if (ssr && !hasPrerenderedPages(internals)) {
Expand All @@ -107,22 +99,9 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
}

const ssrEntryPage = await pipeline.retrieveSsrEntry(pageData.route, filePath);
if (options.settings.adapter?.adapterFeatures?.functionPerRoute) {
// forcing to use undefined, so we fail in an expected way if the module is not even there.
// @ts-expect-error When building for `functionPerRoute`, the module exports a `pageModule` function instead
const ssrEntry = ssrEntryPage?.pageModule;
if (ssrEntry) {
await generatePage(pageData, ssrEntry, builtPaths, pipeline);
} else {
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
throw new Error(
`Unable to find the manifest for the module ${ssrEntryURLPage.toString()}. This is unexpected and likely a bug in Astro, please report.`,
);
}
} else {
const ssrEntry = ssrEntryPage as SinglePageBuiltModule;
await generatePage(pageData, ssrEntry, builtPaths, pipeline);
}

const ssrEntry = ssrEntryPage as SinglePageBuiltModule;
await generatePage(pageData, ssrEntry, builtPaths, pipeline);
}
}
} else {
Expand Down
39 changes: 11 additions & 28 deletions packages/astro/src/core/build/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { isServerLikeOutput } from '../util.js';
import { getOutDirWithinCwd } from './common.js';
import { type BuildInternals, cssOrder, getPageData, mergeInlineCss } from './internal.js';
import { ASTRO_PAGE_MODULE_ID, ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
import { getPagesFromVirtualModulePageName, getVirtualModulePageName } from './plugins/util.js';
import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js';
import { i18nHasFallback } from './util.js';
Expand Down Expand Up @@ -199,32 +198,18 @@ export class BuildPipeline extends Pipeline {
const pages = new Map<PageBuildData, string>();

for (const [virtualModulePageName, filePath] of this.internals.entrySpecifierToBundleMap) {
// virtual pages can be emitted with different prefixes:
// - the classic way are pages emitted with prefix ASTRO_PAGE_RESOLVED_MODULE_ID -> plugin-pages
// - pages emitted using `functionPerRoute`, in this case pages are emitted with prefix RESOLVED_SPLIT_MODULE_ID
// virtual pages are emitted with the 'plugin-pages' prefix
if (
virtualModulePageName.includes(ASTRO_PAGE_RESOLVED_MODULE_ID) ||
virtualModulePageName.includes(RESOLVED_SPLIT_MODULE_ID)
virtualModulePageName.includes(ASTRO_PAGE_RESOLVED_MODULE_ID)
) {
let pageDatas: PageBuildData[] = [];
if (virtualModulePageName.includes(ASTRO_PAGE_RESOLVED_MODULE_ID)) {
pageDatas.push(
...getPagesFromVirtualModulePageName(
this.internals,
ASTRO_PAGE_RESOLVED_MODULE_ID,
virtualModulePageName,
),
);
}
if (virtualModulePageName.includes(RESOLVED_SPLIT_MODULE_ID)) {
pageDatas.push(
...getPagesFromVirtualModulePageName(
this.internals,
RESOLVED_SPLIT_MODULE_ID,
virtualModulePageName,
),
);
}
pageDatas.push(
...getPagesFromVirtualModulePageName(
this.internals,
ASTRO_PAGE_RESOLVED_MODULE_ID,
virtualModulePageName,
),
);
for (const pageData of pageDatas) {
pages.set(pageData, filePath);
}
Expand Down Expand Up @@ -306,9 +291,9 @@ export class BuildPipeline extends Pipeline {
}
let entry;
if (routeIsRedirect(route)) {
entry = await this.#getEntryForRedirectRoute(route, this.internals, this.outFolder);
entry = await this.#getEntryForRedirectRoute(route, this.outFolder);
} else if (routeIsFallback(route)) {
entry = await this.#getEntryForFallbackRoute(route, this.internals, this.outFolder);
entry = await this.#getEntryForFallbackRoute(route, this.outFolder);
} else {
const ssrEntryURLPage = createEntryURL(filePath, this.outFolder);
entry = await import(ssrEntryURLPage.toString());
Expand All @@ -319,7 +304,6 @@ export class BuildPipeline extends Pipeline {

async #getEntryForFallbackRoute(
route: RouteData,
internals: BuildInternals,
outFolder: URL,
): Promise<SinglePageBuiltModule> {
if (route.type !== 'fallback') {
Expand All @@ -339,7 +323,6 @@ export class BuildPipeline extends Pipeline {

async #getEntryForRedirectRoute(
route: RouteData,
internals: BuildInternals,
outFolder: URL,
): Promise<SinglePageBuiltModule> {
if (route.type !== 'redirect') {
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/core/build/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { pluginPages } from './plugin-pages.js';
import { pluginPrerender } from './plugin-prerender.js';
import { pluginRenderers } from './plugin-renderers.js';
import { pluginScripts } from './plugin-scripts.js';
import { pluginSSR, pluginSSRSplit } from './plugin-ssr.js';
import { pluginSSR } from './plugin-ssr.js';

export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
register(pluginComponentEntry(internals));
Expand All @@ -35,6 +35,5 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
register(pluginHoistedScripts(options, internals));
}
register(pluginSSR(options, internals));
register(pluginSSRSplit(options, internals));
register(pluginChunks());
}
15 changes: 0 additions & 15 deletions packages/astro/src/core/build/plugins/plugin-prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,6 @@ function getNonPrerenderOnlyChunks(bundle: Rollup.OutputBundle, internals: Build
continue;
}
}
// Ideally we should record entries when `functionPerRoute` is enabled, but this breaks some tests
// that expect the entrypoint to still exist even if it should be unused.
// TODO: Revisit this so we can delete additional unused chunks
// else if (chunk.facadeModuleId?.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
// const pageDatas = getPagesFromVirtualModulePageName(
// internals,
// RESOLVED_SPLIT_MODULE_ID,
// chunk.facadeModuleId
// );
// const prerender = pageDatas.every((pageData) => pageData.route.prerender);
// if (prerender) {
// prerenderOnlyEntryChunks.add(chunk);
// continue;
// }
// }

nonPrerenderOnlyEntryChunks.add(chunk);
}
Expand Down
144 changes: 4 additions & 140 deletions packages/astro/src/core/build/plugins/plugin-ssr.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { Plugin as VitePlugin } from 'vite';
import { isFunctionPerRouteEnabled } from '../../../integrations/hooks.js';
import type { AstroSettings } from '../../../types/astro.js';
import type { AstroAdapter } from '../../../types/public/integrations.js';
import { routeIsRedirect } from '../../redirects/index.js';
Expand All @@ -15,7 +11,8 @@ import { SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugin-manifest.js';
import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js';
import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js';
import { RENDERERS_MODULE_ID } from './plugin-renderers.js';
import { getComponentFromVirtualModulePageName, getVirtualModulePageName } from './util.js';
import { getVirtualModulePageName } from './util.js';
import type { Plugin as VitePlugin } from 'vite';

export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry';
export const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
Expand Down Expand Up @@ -135,16 +132,12 @@ export function pluginSSR(
internals: BuildInternals,
): AstroBuildPlugin {
const ssr = isServerLikeOutput(options.settings.config);
const functionPerRouteEnabled = isFunctionPerRouteEnabled(options.settings.adapter);
return {
targets: ['server'],
hooks: {
'build:before': () => {
const adapter = options.settings.adapter!;
let ssrPlugin =
ssr && functionPerRouteEnabled === false
? vitePluginSSR(internals, adapter, options)
: undefined;
const ssrPlugin = ssr && vitePluginSSR(internals, adapter, options)
const vitePlugin = [vitePluginAdapter(adapter)];
if (ssrPlugin) {
vitePlugin.unshift(ssrPlugin);
Expand All @@ -160,10 +153,6 @@ export function pluginSSR(
return;
}

if (functionPerRouteEnabled) {
return;
}

if (!internals.ssrEntryChunk) {
throw new Error(`Did not generate an entry chunk for SSR`);
}
Expand All @@ -174,111 +163,8 @@ export function pluginSSR(
};
}

export const SPLIT_MODULE_ID = '@astro-page-split:';
export const RESOLVED_SPLIT_MODULE_ID = '\0@astro-page-split:';

function vitePluginSSRSplit(
internals: BuildInternals,
adapter: AstroAdapter,
options: StaticBuildOptions,
): VitePlugin {
return {
name: '@astrojs/vite-plugin-astro-ssr-split',
enforce: 'post',
options(opts) {
const inputs = new Set<string>();

for (const pageData of Object.values(options.allPages)) {
if (routeIsRedirect(pageData.route)) {
continue;
}
inputs.add(getVirtualModulePageName(SPLIT_MODULE_ID, pageData.component));
}

return addRollupInput(opts, Array.from(inputs));
},
resolveId(id) {
if (id.startsWith(SPLIT_MODULE_ID)) {
return '\0' + id;
}
},
async load(id) {
if (id.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
const imports: string[] = [];
const contents: string[] = [];
const exports: string[] = [];
const componentPath = getComponentFromVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, id);
const virtualModuleName = getVirtualModulePageName(ASTRO_PAGE_MODULE_ID, componentPath);
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 middleware = await this.resolve(MIDDLEWARE_MODULE_ID);
const ssrCode = generateSSRCode(options.settings, adapter, middleware!.id);
imports.push(...ssrCode.imports);
contents.push(...ssrCode.contents);

exports.push('export { pageModule }');

return [...imports, ...contents, ...exports].join('\n');
}
},
async generateBundle(_opts, bundle) {
// Add assets from this SSR chunk as well.
for (const [, chunk] of Object.entries(bundle)) {
if (chunk.type === 'asset') {
internals.staticFiles.add(chunk.fileName);
}
}

for (const [, chunk] of Object.entries(bundle)) {
if (chunk.type === 'asset') {
continue;
}
for (const moduleKey of Object.keys(chunk.modules)) {
if (moduleKey.startsWith(RESOLVED_SPLIT_MODULE_ID)) {
storeEntryPoint(moduleKey, options, internals, chunk.fileName);
}
}
}
},
};
}

export function pluginSSRSplit(
options: StaticBuildOptions,
internals: BuildInternals,
): AstroBuildPlugin {
const ssr = isServerLikeOutput(options.settings.config);
const functionPerRouteEnabled = isFunctionPerRouteEnabled(options.settings.adapter);

return {
targets: ['server'],
hooks: {
'build:before': () => {
const adapter = options.settings.adapter!;
let ssrPlugin =
ssr && functionPerRouteEnabled
? vitePluginSSRSplit(internals, adapter, options)
: undefined;
const vitePlugin = [vitePluginAdapter(adapter)];
if (ssrPlugin) {
vitePlugin.unshift(ssrPlugin);
}

return {
enforce: 'after-user-plugins',
vitePlugin,
};
},
},
};
}

function generateSSRCode(settings: AstroSettings, adapter: AstroAdapter, middlewareId: string) {
const edgeMiddleware = adapter?.adapterFeatures?.edgeMiddleware ?? false;
const pageMap = isFunctionPerRouteEnabled(adapter) ? 'pageModule' : 'pageMap';

const imports = [
`import { renderers } from '${RENDERERS_MODULE_ID}';`,
Expand All @@ -294,7 +180,7 @@ function generateSSRCode(settings: AstroSettings, adapter: AstroAdapter, middlew
settings.config.experimental.serverIslands ? '' : `const serverIslandMap = new Map()`,
edgeMiddleware ? `const middleware = (_, next) => next()` : '',
`const _manifest = Object.assign(defaultManifest, {`,
` ${pageMap},`,
` pageMap,`,
` serverIslandMap,`,
` renderers,`,
` middleware`,
Expand Down Expand Up @@ -325,25 +211,3 @@ if (_start in serverEntrypointModule) {
contents,
};
}

/**
* 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 all the {@link RouteData} that have the same component as the one we are processing.
*/
function storeEntryPoint(
moduleKey: string,
options: StaticBuildOptions,
internals: BuildInternals,
fileName: string,
) {
const componentPath = getComponentFromVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, moduleKey);
for (const pageData of Object.values(options.allPages)) {
if (componentPath == pageData.component) {
const publicPath = fileURLToPath(options.settings.config.build.server);
internals.entryPoints.set(pageData.route, pathToFileURL(join(publicPath, fileName)));
}
}
}
Loading

0 comments on commit 8a53517

Please sign in to comment.