Skip to content

Commit

Permalink
feat: vercel edge middleware support (#7532)
Browse files Browse the repository at this point in the history
Co-authored-by: Bjorn Lu <[email protected]>
Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
3 people authored Jul 5, 2023
1 parent cfd5b2b commit 9e5fafa
Show file tree
Hide file tree
Showing 36 changed files with 758 additions and 50 deletions.
11 changes: 11 additions & 0 deletions .changeset/brown-shrimps-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'astro': minor
---

The `astro/middleware` module exports a new utility called `trySerializeLocals`.

This utility can be used by adapters to validate their `locals` before sending it
to the Astro middleware.

This function will throw a runtime error if the value passed is not serializable, so
consumers will need to handle that error.
24 changes: 24 additions & 0 deletions .changeset/chilly-pants-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'astro': minor
---

Astro exposes the middleware file path to the integrations in the hook `astro:build:ssr`

```ts
// myIntegration.js
import type { AstroIntegration } from 'astro';
function integration(): AstroIntegration {
return {
name: "fancy-astro-integration",
hooks: {
'astro:build:ssr': ({ middlewareEntryPoint }) => {
if (middlewareEntryPoint) {
// do some operations
}
}
}
}
}
```

The `middlewareEntryPoint` is only defined if the user has created an Astro middleware.
5 changes: 5 additions & 0 deletions .changeset/cool-kids-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Correctly track the middleware during the SSR build.
11 changes: 11 additions & 0 deletions .changeset/good-pigs-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@astrojs/vercel': minor
---

Support for Vercel Edge Middleware via Astro middleware.

When a project uses the new option Astro `build.excludeMiddleware`, the
`@astrojs/vercel/serverless` adapter will automatically create a Vercel Edge Middleware
that will automatically communicate with the Astro Middleware.

Check the [documentation](https://github.com/withastro/astro/blob/main/packages/integrations/vercel/README.md##vercel-edge-middleware-with-astro-middleware) for more details.
7 changes: 7 additions & 0 deletions .changeset/long-geckos-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'astro': minor
---

The `astro/middleware` module exports a new API called `createContext`.

This a low-level API that adapters can use to create a context that can be consumed by middleware functions.
20 changes: 20 additions & 0 deletions .changeset/strong-years-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'astro': minor
---

Introduced a new build option for SSR, called `build.excludeMiddleware`.

```js
// astro.config.mjs
import {defineConfig} from "astro/config";

export default defineConfig({
build: {
excludeMiddleware: true
}
})
```

When enabled, the code that belongs to be middleware **won't** be imported
by the final pages/entry points. The user is responsible for importing it and
calling it manually.
25 changes: 25 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,27 @@ export interface AstroUserConfig {
* ```
*/
split?: boolean;

/**
* @docs
* @name build.excludeMiddleware
* @type {boolean}
* @default {false}
* @version 2.8.0
* @description
* Defines whether or not any SSR middleware code will be bundled when built.
*
* When enabled, middleware code is not bundled and imported by all pages during the build. To instead execute and import middleware code manually, set `build.excludeMiddleware: true`:
*
* ```js
* {
* build: {
* excludeMiddleware: true
* }
* }
* ```
*/
excludeMiddleware?: boolean;
};

/**
Expand Down Expand Up @@ -1842,6 +1863,10 @@ export interface AstroIntegration {
* the physical file you should import.
*/
entryPoints: Map<RouteData, URL>;
/**
* File path of the emitted middleware
*/
middlewareEntryPoint: URL | undefined;
}) => void | Promise<void>;
'astro:build:start'?: () => void | Promise<void>;
'astro:build:setup'?: (options: {
Expand Down
22 changes: 20 additions & 2 deletions packages/astro/src/core/build/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { AstroConfig, AstroSettings, ManifestData, RuntimeMode } from '../../@types/astro';

import fs from 'fs';
import * as colors from 'kleur/colors';
import { performance } from 'perf_hooks';
Expand All @@ -12,7 +11,7 @@ import {
runHookConfigSetup,
} from '../../integrations/index.js';
import { createVite } from '../create-vite.js';
import { debug, info, levels, timerMessage, type LogOptions } from '../logger/core.js';
import { debug, info, warn, levels, timerMessage, type LogOptions } from '../logger/core.js';
import { printHelp } from '../messages.js';
import { apply as applyPolyfill } from '../polyfill.js';
import { RouteCache } from '../render/route-cache.js';
Expand Down Expand Up @@ -211,6 +210,25 @@ class AstroBuilder {
`the outDir cannot be the root folder. Please build to a folder such as dist.`
);
}

if (config.build.split === true) {
if (config.output === 'static') {
warn(
this.logging,
'configuration',
'The option `build.split` won\'t take effect, because `output` is not `"server"` or `"hybrid"`.'
);
}
}
if (config.build.excludeMiddleware === true) {
if (config.output === 'static') {
warn(
this.logging,
'configuration',
'The option `build.excludeMiddleware` won\'t take effect, because `output` is not `"server"` or `"hybrid"`.'
);
}
}
}

/** Stats */
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export interface BuildInternals {
entryPoints: Map<RouteData, URL>;
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
componentMetadata: SSRResult['componentMetadata'];
middlewareEntryPoint?: URL;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
register(pluginAnalyzer(internals));
register(pluginInternals(internals));
register(pluginRenderers(options));
register(pluginMiddleware(options));
register(pluginMiddleware(options, internals));
register(pluginPages(options, internals));
register(pluginCSS(options, internals));
register(astroHeadBuildPlugin(internals));
Expand Down
33 changes: 30 additions & 3 deletions packages/astro/src/core/build/plugins/plugin-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../../constants.js';
import { addRollupInput } from '../add-rollup-input.js';
import type { AstroBuildPlugin } from '../plugin';
import type { StaticBuildOptions } from '../types';
import type { BuildInternals } from '../internal';

export const MIDDLEWARE_MODULE_ID = '@astro-middleware';

const EMPTY_MIDDLEWARE = '\0empty-middleware';

export function vitePluginMiddleware(opts: StaticBuildOptions): VitePlugin {
export function vitePluginMiddleware(
opts: StaticBuildOptions,
internals: BuildInternals
): VitePlugin {
let resolvedMiddlewareId: string;
return {
name: '@astro/plugin-middleware',

Expand All @@ -22,6 +27,7 @@ export function vitePluginMiddleware(opts: StaticBuildOptions): VitePlugin {
`${opts.settings.config.srcDir.pathname}/${MIDDLEWARE_PATH_SEGMENT_NAME}`
);
if (middlewareId) {
resolvedMiddlewareId = middlewareId.id;
return middlewareId.id;
} else {
return EMPTY_MIDDLEWARE;
Expand All @@ -35,18 +41,39 @@ export function vitePluginMiddleware(opts: StaticBuildOptions): VitePlugin {
load(id) {
if (id === EMPTY_MIDDLEWARE) {
return 'export const onRequest = undefined';
} else if (id === resolvedMiddlewareId) {
this.emitFile({
type: 'chunk',
preserveSignature: 'strict',
fileName: 'middleware.mjs',
id,
});
}
},

writeBundle(_, bundle) {
for (const [chunkName, chunk] of Object.entries(bundle)) {
if (chunk.type === 'asset') {
continue;
}
if (chunk.fileName === 'middleware.mjs') {
internals.middlewareEntryPoint = new URL(chunkName, opts.settings.config.build.server);
}
}
},
};
}

export function pluginMiddleware(opts: StaticBuildOptions): AstroBuildPlugin {
export function pluginMiddleware(
opts: StaticBuildOptions,
internals: BuildInternals
): AstroBuildPlugin {
return {
build: 'ssr',
hooks: {
'build:before': () => {
return {
vitePlugin: vitePluginMiddleware(opts),
vitePlugin: vitePluginMiddleware(opts, internals),
};
},
},
Expand Down
11 changes: 7 additions & 4 deletions packages/astro/src/core/build/plugins/plugin-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,13 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V
imports.push(`import { renderers } from "${RENDERERS_MODULE_ID}";`);
exports.push(`export { renderers };`);

const middlewareModule = await this.resolve(MIDDLEWARE_MODULE_ID);
if (middlewareModule) {
imports.push(`import { onRequest } from "${middlewareModule.id}";`);
exports.push(`export { onRequest };`);
// The middleware should not be imported by the pages
if (!opts.settings.config.build.excludeMiddleware) {
const middlewareModule = await this.resolve(MIDDLEWARE_MODULE_ID);
if (middlewareModule) {
imports.push(`import { onRequest } from "${middlewareModule.id}";`);
exports.push(`export { onRequest };`);
}
}

return `${imports.join('\n')}${exports.join('\n')}`;
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/build/plugins/plugin-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export function pluginSSR(
manifest,
logging: options.logging,
entryPoints: internals.entryPoints,
middlewareEntryPoint: internals.middlewareEntryPoint,
});
const code = injectManifest(manifest, internals.ssrEntryChunk);
mutate(internals.ssrEntryChunk, 'server', code);
Expand Down Expand Up @@ -260,6 +261,7 @@ export function pluginSSRSplit(
manifest,
logging: options.logging,
entryPoints: internals.entryPoints,
middlewareEntryPoint: internals.middlewareEntryPoint,
});
for (const [, chunk] of internals.ssrSplitEntryChunks) {
const code = injectManifest(manifest, chunk);
Expand Down
3 changes: 0 additions & 3 deletions packages/astro/src/core/build/static-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { generatePages } from './generate.js';
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_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js';
import { RESOLVED_SPLIT_MODULE_ID, SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js';
Expand Down Expand Up @@ -183,8 +182,6 @@ async function ssrBuild(
);
} 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) {
return opts.settings.config.build.serverEntry;
} else if (chunkInfo.facadeModuleId === RESOLVED_RENDERERS_MODULE_ID) {
Expand Down
9 changes: 9 additions & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const ASTRO_CONFIG_DEFAULTS = {
redirects: true,
inlineStylesheets: 'never',
split: false,
excludeMiddleware: false,
},
compressHTML: false,
server: {
Expand Down Expand Up @@ -122,6 +123,10 @@ export const AstroConfigSchema = z.object({
.default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets),

split: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.split),
excludeMiddleware: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.build.excludeMiddleware),
})
.optional()
.default({}),
Expand Down Expand Up @@ -283,6 +288,10 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
.default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets),

split: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.split),
excludeMiddleware: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.build.excludeMiddleware),
})
.optional()
.default({}),
Expand Down
21 changes: 14 additions & 7 deletions packages/astro/src/core/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,26 @@ type EndpointCallResult =
response: Response;
};

type CreateAPIContext = {
request: Request;
params: Params;
site?: string;
props: Record<string, any>;
adapterName?: string;
};

/**
* Creates a context that holds all the information needed to handle an Astro endpoint.
*
* @param {CreateAPIContext} payload
*/
export function createAPIContext({
request,
params,
site,
props,
adapterName,
}: {
request: Request;
params: Params;
site?: string;
props: Record<string, any>;
adapterName?: string;
}): APIContext {
}: CreateAPIContext): APIContext {
const context = {
cookies: new AstroCookies(request),
request,
Expand Down
Loading

0 comments on commit 9e5fafa

Please sign in to comment.