summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Emanuele Stoppa <my.burning@gmail.com> 2023-07-05 16:45:58 +0100
committerGravatar GitHub <noreply@github.com> 2023-07-05 16:45:58 +0100
commit9e5fafa2b25b5128084c7072aa282642fcfbb14b (patch)
tree80e21475f93da004a5eae87e3a0d2d6a4f22cce6
parentcfd5b2b785ad277b82c380fdf68ead0475ddb42f (diff)
downloadastro-9e5fafa2b25b5128084c7072aa282642fcfbb14b.tar.gz
astro-9e5fafa2b25b5128084c7072aa282642fcfbb14b.tar.zst
astro-9e5fafa2b25b5128084c7072aa282642fcfbb14b.zip
feat: vercel edge middleware support (#7532)
Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
-rw-r--r--.changeset/brown-shrimps-hug.md11
-rw-r--r--.changeset/chilly-pants-fix.md24
-rw-r--r--.changeset/cool-kids-grin.md5
-rw-r--r--.changeset/good-pigs-fetch.md11
-rw-r--r--.changeset/long-geckos-battle.md7
-rw-r--r--.changeset/strong-years-travel.md20
-rw-r--r--packages/astro/src/@types/astro.ts25
-rw-r--r--packages/astro/src/core/build/index.ts22
-rw-r--r--packages/astro/src/core/build/internal.ts1
-rw-r--r--packages/astro/src/core/build/plugins/index.ts2
-rw-r--r--packages/astro/src/core/build/plugins/plugin-middleware.ts33
-rw-r--r--packages/astro/src/core/build/plugins/plugin-pages.ts11
-rw-r--r--packages/astro/src/core/build/plugins/plugin-ssr.ts2
-rw-r--r--packages/astro/src/core/build/static-build.ts3
-rw-r--r--packages/astro/src/core/config/schema.ts9
-rw-r--r--packages/astro/src/core/endpoint/index.ts21
-rw-r--r--packages/astro/src/core/middleware/index.ts102
-rw-r--r--packages/astro/src/integrations/index.ts33
-rw-r--r--packages/astro/test/middleware.test.js49
-rw-r--r--packages/astro/test/ssr-split-manifest.test.js4
-rw-r--r--packages/astro/test/test-adapter.js3
-rw-r--r--packages/integrations/vercel/README.md74
-rw-r--r--packages/integrations/vercel/package.json5
-rw-r--r--packages/integrations/vercel/src/lib/fs.ts4
-rw-r--r--packages/integrations/vercel/src/lib/nft.ts7
-rw-r--r--packages/integrations/vercel/src/serverless/adapter.ts35
-rw-r--r--packages/integrations/vercel/src/serverless/entrypoint.ts10
-rw-r--r--packages/integrations/vercel/src/serverless/middleware.ts81
-rw-r--r--packages/integrations/vercel/test/edge-middleware.test.js30
-rw-r--r--packages/integrations/vercel/test/edge-middleware.test.js.snap40
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs10
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/package.json9
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/src/middleware.js8
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro0
-rw-r--r--packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js5
-rw-r--r--pnpm-lock.yaml92
36 files changed, 758 insertions, 50 deletions
diff --git a/.changeset/brown-shrimps-hug.md b/.changeset/brown-shrimps-hug.md
new file mode 100644
index 000000000..1c40fc380
--- /dev/null
+++ b/.changeset/brown-shrimps-hug.md
@@ -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.
diff --git a/.changeset/chilly-pants-fix.md b/.changeset/chilly-pants-fix.md
new file mode 100644
index 000000000..c862a15dc
--- /dev/null
+++ b/.changeset/chilly-pants-fix.md
@@ -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.
diff --git a/.changeset/cool-kids-grin.md b/.changeset/cool-kids-grin.md
new file mode 100644
index 000000000..190e5eee9
--- /dev/null
+++ b/.changeset/cool-kids-grin.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Correctly track the middleware during the SSR build.
diff --git a/.changeset/good-pigs-fetch.md b/.changeset/good-pigs-fetch.md
new file mode 100644
index 000000000..4a463044e
--- /dev/null
+++ b/.changeset/good-pigs-fetch.md
@@ -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.
diff --git a/.changeset/long-geckos-battle.md b/.changeset/long-geckos-battle.md
new file mode 100644
index 000000000..3c1a993be
--- /dev/null
+++ b/.changeset/long-geckos-battle.md
@@ -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.
diff --git a/.changeset/strong-years-travel.md b/.changeset/strong-years-travel.md
new file mode 100644
index 000000000..3067e01b4
--- /dev/null
+++ b/.changeset/strong-years-travel.md
@@ -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.
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 185401a89..12f309f1a 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -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;
};
/**
@@ -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: {
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index 037c462fd..11e2b1fa9 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -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';
@@ -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';
@@ -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 */
diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts
index 28d15d874..5dff6f3dd 100644
--- a/packages/astro/src/core/build/internal.ts
+++ b/packages/astro/src/core/build/internal.ts
@@ -88,6 +88,7 @@ export interface BuildInternals {
entryPoints: Map<RouteData, URL>;
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
componentMetadata: SSRResult['componentMetadata'];
+ middlewareEntryPoint?: URL;
}
/**
diff --git a/packages/astro/src/core/build/plugins/index.ts b/packages/astro/src/core/build/plugins/index.ts
index 160e18fdd..3a44824d6 100644
--- a/packages/astro/src/core/build/plugins/index.ts
+++ b/packages/astro/src/core/build/plugins/index.ts
@@ -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));
diff --git a/packages/astro/src/core/build/plugins/plugin-middleware.ts b/packages/astro/src/core/build/plugins/plugin-middleware.ts
index dee73d2f8..6db39733e 100644
--- a/packages/astro/src/core/build/plugins/plugin-middleware.ts
+++ b/packages/astro/src/core/build/plugins/plugin-middleware.ts
@@ -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',
@@ -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;
@@ -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),
};
},
},
diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts
index cf078f0b5..2ee438a6a 100644
--- a/packages/astro/src/core/build/plugins/plugin-pages.ts
+++ b/packages/astro/src/core/build/plugins/plugin-pages.ts
@@ -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')}`;
diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts
index 41f38a8b2..514fe2409 100644
--- a/packages/astro/src/core/build/plugins/plugin-ssr.ts
+++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts
@@ -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);
@@ -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);
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index 59a42db34..9bef0d681 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -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';
@@ -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) {
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 7410df470..ae681a543 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -25,6 +25,7 @@ const ASTRO_CONFIG_DEFAULTS = {
redirects: true,
inlineStylesheets: 'never',
split: false,
+ excludeMiddleware: false,
},
compressHTML: false,
server: {
@@ -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({}),
@@ -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({}),
diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts
index dde07cd9c..33cb113a2 100644
--- a/packages/astro/src/core/endpoint/index.ts
+++ b/packages/astro/src/core/endpoint/index.ts
@@ -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,
diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts
index f9fb07bd4..47127c674 100644
--- a/packages/astro/src/core/middleware/index.ts
+++ b/packages/astro/src/core/middleware/index.ts
@@ -1,9 +1,107 @@
-import type { MiddlewareResponseHandler } from '../../@types/astro';
+import type { MiddlewareResponseHandler, Params } from '../../@types/astro';
import { sequence } from './sequence.js';
+import { createAPIContext } from '../endpoint/index.js';
function defineMiddleware(fn: MiddlewareResponseHandler) {
return fn;
}
+/**
+ * Payload for creating a context to be passed to Astro middleware
+ */
+export type CreateContext = {
+ /**
+ * The incoming request
+ */
+ request: Request;
+ /**
+ * Optional parameters
+ */
+ params?: Params;
+};
+
+/**
+ * Creates a context to be passed to Astro middleware `onRequest` function.
+ */
+function createContext({ request, params }: CreateContext) {
+ return createAPIContext({
+ request,
+ params: params ?? {},
+ props: {},
+ site: undefined,
+ });
+}
+
+/**
+ * Checks whether the passed `value` is serializable.
+ *
+ * A serializable value contains plain values. For example, `Proxy`, `Set`, `Map`, functions, etc.
+ * are not accepted because they can't be serialized.
+ */
+function isLocalsSerializable(value: unknown): boolean {
+ let type = typeof value;
+ let plainObject = true;
+ if (type === 'object' && isPlainObject(value)) {
+ for (const [, nestedValue] of Object.entries(value)) {
+ if (!isLocalsSerializable(nestedValue)) {
+ plainObject = false;
+ break;
+ }
+ }
+ } else {
+ plainObject = false;
+ }
+ let result =
+ value === null ||
+ type === 'string' ||
+ type === 'number' ||
+ type === 'boolean' ||
+ Array.isArray(value) ||
+ plainObject;
+
+ return result;
+}
+
+/**
+ *
+ * From [redux-toolkit](https://github.com/reduxjs/redux-toolkit/blob/master/packages/toolkit/src/isPlainObject.ts)
+ *
+ * Returns true if the passed value is "plain" object, i.e. an object whose
+ * prototype is the root `Object.prototype`. This includes objects created
+ * using object literals, but not for instance for class instances.
+ */
+function isPlainObject(value: unknown): value is object {
+ if (typeof value !== 'object' || value === null) return false;
+
+ let proto = Object.getPrototypeOf(value);
+ if (proto === null) return true;
+
+ let baseProto = proto;
+ while (Object.getPrototypeOf(baseProto) !== null) {
+ baseProto = Object.getPrototypeOf(baseProto);
+ }
+
+ return proto === baseProto;
+}
+
+/**
+ * It attempts to serialize `value` and return it as a string.
+ *
+ * ## Errors
+ * If the `value` is not serializable if the function will throw a runtime error.
+ *
+ * Something is **not serializable** when it contains properties/values like functions, `Map`, `Set`, `Date`,
+ * and other types that can't be made a string.
+ *
+ * @param value
+ */
+function trySerializeLocals(value: unknown) {
+ if (isLocalsSerializable(value)) {
+ return JSON.stringify(value);
+ } else {
+ throw new Error("The passed value can't be serialized.");
+ }
+}
+
// NOTE: this export must export only the functions that will be exposed to user-land as officials APIs
-export { sequence, defineMiddleware };
+export { sequence, defineMiddleware, createContext, trySerializeLocals };
diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts
index eaf4b21d1..b243ba979 100644
--- a/packages/astro/src/integrations/index.ts
+++ b/packages/astro/src/integrations/index.ts
@@ -298,22 +298,30 @@ export async function runHookBuildSetup({
return updatedConfig;
}
+type RunHookBuildSsr = {
+ config: AstroConfig;
+ manifest: SerializedSSRManifest;
+ logging: LogOptions;
+ entryPoints: Map<RouteData, URL>;
+ middlewareEntryPoint: URL | undefined;
+};
+
export async function runHookBuildSsr({
config,
manifest,
logging,
entryPoints,
-}: {
- config: AstroConfig;
- manifest: SerializedSSRManifest;
- logging: LogOptions;
- entryPoints: Map<RouteData, URL>;
-}) {
+ middlewareEntryPoint,
+}: RunHookBuildSsr) {
for (const integration of config.integrations) {
if (integration?.hooks?.['astro:build:ssr']) {
await withTakingALongTimeMsg({
name: integration.name,
- hookResult: integration.hooks['astro:build:ssr']({ manifest, entryPoints }),
+ hookResult: integration.hooks['astro:build:ssr']({
+ manifest,
+ entryPoints,
+ middlewareEntryPoint,
+ }),
logging,
});
}
@@ -340,17 +348,14 @@ export async function runHookBuildGenerated({
}
}
-export async function runHookBuildDone({
- config,
- pages,
- routes,
- logging,
-}: {
+type RunHookBuildDone = {
config: AstroConfig;
pages: string[];
routes: RouteData[];
logging: LogOptions;
-}) {
+};
+
+export async function runHookBuildDone({ config, pages, routes, logging }: RunHookBuildDone) {
const dir = isServerLikeOutput(config) ? config.build.client : config.outDir;
await fs.promises.mkdir(dir, { recursive: true });
diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js
index e2c57bafb..9e2213146 100644
--- a/packages/astro/test/middleware.test.js
+++ b/packages/astro/test/middleware.test.js
@@ -2,6 +2,8 @@ import { loadFixture } from './test-utils.js';
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import testAdapter from './test-adapter.js';
+import { fileURLToPath } from 'node:url';
+import { readFileSync, existsSync } from 'node:fs';
describe('Middleware in DEV mode', () => {
/** @type {import('./test-utils').Fixture} */
@@ -104,12 +106,19 @@ describe('Middleware in PROD mode, SSG', () => {
describe('Middleware API in PROD mode, SSR', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
+ let middlewarePath;
before(async () => {
fixture = await loadFixture({
root: './fixtures/middleware-dev/',
output: 'server',
- adapter: testAdapter({}),
+ adapter: testAdapter({
+ setEntryPoints(entryPointsOrMiddleware) {
+ if (entryPointsOrMiddleware instanceof URL) {
+ middlewarePath = entryPointsOrMiddleware;
+ }
+ },
+ }),
});
await fixture.build();
});
@@ -201,6 +210,18 @@ describe('Middleware API in PROD mode, SSR', () => {
const text = await response.text();
expect(text.includes('REDACTED')).to.be.true;
});
+
+ it('the integration should receive the path to the middleware', async () => {
+ expect(middlewarePath).to.not.be.undefined;
+ try {
+ const path = fileURLToPath(middlewarePath);
+ expect(existsSync(path)).to.be.true;
+ const content = readFileSync(fileURLToPath(middlewarePath), 'utf-8');
+ expect(content.length).to.be.greaterThan(0);
+ } catch (e) {
+ throw e;
+ }
+ });
});
describe('Middleware with tailwind', () => {
@@ -224,3 +245,29 @@ describe('Middleware with tailwind', () => {
expect(bundledCSS.includes('--tw-content')).to.be.true;
});
});
+
+describe('Middleware, split middleware option', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/middleware-dev/',
+ output: 'server',
+ build: {
+ excludeMiddleware: true,
+ },
+ adapter: testAdapter({}),
+ });
+ await fixture.build();
+ });
+
+ it('should not render locals data because the page does not export it', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+ expect($('p').html()).to.not.equal('bar');
+ });
+});
diff --git a/packages/astro/test/ssr-split-manifest.test.js b/packages/astro/test/ssr-split-manifest.test.js
index 9e8a0981e..394740395 100644
--- a/packages/astro/test/ssr-split-manifest.test.js
+++ b/packages/astro/test/ssr-split-manifest.test.js
@@ -18,7 +18,9 @@ describe('astro:ssr-manifest, split', () => {
output: 'server',
adapter: testAdapter({
setEntryPoints(entries) {
- entryPoints = entries;
+ if (entries) {
+ entryPoints = entries;
+ }
},
setRoutes(routes) {
currentRoutes = routes;
diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js
index af5a7777b..ed79e5f21 100644
--- a/packages/astro/test/test-adapter.js
+++ b/packages/astro/test/test-adapter.js
@@ -74,9 +74,10 @@ export default function (
...extendAdapter,
});
},
- 'astro:build:ssr': ({ entryPoints }) => {
+ 'astro:build:ssr': ({ entryPoints, middlewareEntryPoint }) => {
if (setEntryPoints) {
setEntryPoints(entryPoints);
+ setEntryPoints(middlewareEntryPoint);
}
},
'astro:build:done': ({ routes }) => {
diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md
index 41a5591dc..19d841a2f 100644
--- a/packages/integrations/vercel/README.md
+++ b/packages/integrations/vercel/README.md
@@ -233,9 +233,9 @@ export default defineConfig({
});
```
-### Vercel Middleware
+### Vercel Edge Middleware
-You can use Vercel middleware to intercept a request and redirect before sending a response. Vercel middleware can run for Edge, SSR, and Static deployments. You don't need to install `@vercel/edge` to write middleware, but you do need to install it to use features such as geolocation. For more information see [Vercel’s middleware documentation](https://vercel.com/docs/concepts/functions/edge-middleware).
+You can use Vercel Edge middleware to intercept a request and redirect before sending a response. Vercel middleware can run for Edge, SSR, and Static deployments. You may not need to install this package for your middleware. `@vercel/edge` is only required to use some middleware features such as geolocation. For more information see [Vercel’s middleware documentation](https://vercel.com/docs/concepts/functions/edge-middleware).
1. Add a `middleware.js` file to the root of your project:
@@ -262,6 +262,76 @@ You can use Vercel middleware to intercept a request and redirect before sending
> **Warning**
> **Trying to rewrite?** Currently rewriting a request with middleware only works for static files.
+### Vercel Edge Middleware with Astro middleware
+
+The `@astrojs/vercel/serverless` adapter can automatically create the Vercel Edge middleware from an Astro middleware in your code base.
+
+This is an opt-in feature, and the `build.excludeMiddleware` option needs to be set to `true`:
+
+```js
+// astro.config.mjs
+import {defineConfig} from "astro/config";
+import vercel from "@astrojs/vercel";
+export default defineConfig({
+ output: "server",
+ adapter: vercel(),
+ build: {
+ excludeMiddleware: true
+ }
+})
+```
+
+Optionally, you can create a file recognized by the adapter named `vercel-edge-middleware.(js|ts)` in the [`srcDir`](https://docs.astro.build/en/reference/configuration-reference/#srcdir) folder to create [`Astro.locals`](https://docs.astro.build/en/reference/api-reference/#astrolocals).
+
+Typings requires the [`@vercel/edge`](https://www.npmjs.com/package/@vercel/edge) package.
+
+```js
+// src/vercel-edge-middleware.js
+/**
+ *
+ * @param options.request {Request}
+ * @param options.context {import("@vercel/edge").RequestContext}
+ * @returns {object}
+ */
+export default function({ request, context }) {
+ // do something with request and context
+ return {
+ title: "Spider-man's blog"
+ }
+}
+```
+
+If you use TypeScript, you can type the function as follows:
+
+```ts
+// src/vercel-edge-middleware.ts
+import type {RequestContext} from "@vercel/edge";
+
+export default function ({request, context}: { request: Request, context: RequestContext }) {
+ // do something with request and context
+ return {
+ title: "Spider-man's blog"
+ }
+}
+```
+
+The data returned by this function will be passed to Astro middleware.
+
+The function:
+- must export a **default** function;
+- must **return** an `object`;
+- accepts an object with a `request` and `context` as properties;
+- `request` is typed as [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request);
+- `context` is typed as [`RequestContext`](https://vercel.com/docs/concepts/functions/edge-functions/vercel-edge-package#requestcontext);
+
+#### Limitations and constraints
+
+When you opt in to this feature, there are few constraints to note:
+- The Vercel Edge middleware will always be the **first** function to receive the `Request` and the last function to receive `Response`. This an architectural constraint that follows the [boundaries set by Vercel](https://vercel.com/docs/concepts/functions/edge-middleware).
+- Only `request` and `context` may be used to produce an `Astro.locals` object. Operations like redirects, etc. should be delegated to Astro middleware.
+- `Astro.locals` **must be serializable**. Failing to do so will result in a **runtime error**. This means that you **cannot** store complex types like `Map`, `function`, `Set`, etc.
+
+
## Troubleshooting
**A few known complex packages (example: [puppeteer](https://github.com/puppeteer/puppeteer)) do not support bundling and therefore will not work properly with this adapter.** By default, Vercel doesn't include npm installed files & packages from your project's `./node_modules` folder. To address this, the `@astrojs/vercel` adapter automatically bundles your final build output using `esbuild`.
diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json
index 3899b9069..a039ee5a8 100644
--- a/packages/integrations/vercel/package.json
+++ b/packages/integrations/vercel/package.json
@@ -64,10 +64,13 @@
},
"devDependencies": {
"@types/set-cookie-parser": "^2.4.2",
+ "@vercel/edge": "^0.3.4",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
+ "chai-jest-snapshot": "^2.0.0",
"cheerio": "1.0.0-rc.12",
- "mocha": "^9.2.2"
+ "mocha": "^9.2.2",
+ "rollup": "^3.20.1"
}
}
diff --git a/packages/integrations/vercel/src/lib/fs.ts b/packages/integrations/vercel/src/lib/fs.ts
index 18fbe85d2..51b12d52f 100644
--- a/packages/integrations/vercel/src/lib/fs.ts
+++ b/packages/integrations/vercel/src/lib/fs.ts
@@ -86,3 +86,7 @@ export async function copyFilesToFunction(
return commonAncestor;
}
+
+export async function writeFile(path: PathLike, content: string) {
+ await fs.writeFile(path, content, { encoding: 'utf-8' });
+}
diff --git a/packages/integrations/vercel/src/lib/nft.ts b/packages/integrations/vercel/src/lib/nft.ts
index 46604db90..752f87251 100644
--- a/packages/integrations/vercel/src/lib/nft.ts
+++ b/packages/integrations/vercel/src/lib/nft.ts
@@ -1,7 +1,5 @@
-import { nodeFileTrace } from '@vercel/nft';
import { relative as relativePath } from 'node:path';
import { fileURLToPath } from 'node:url';
-
import { copyFilesToFunction } from './fs.js';
export async function copyDependenciesToFunction({
@@ -23,6 +21,11 @@ export async function copyDependenciesToFunction({
base = new URL('../', base);
}
+ // The Vite bundle includes an import to `@vercel/nft` for some reason,
+ // and that trips up `@vercel/nft` itself during the adapter build. Using a
+ // dynamic import helps prevent the issue.
+ // TODO: investigate why
+ const { nodeFileTrace } = await import('@vercel/nft');
const result = await nodeFileTrace([entryPath], {
base: fileURLToPath(base),
});
diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts
index 007fb8537..9d799a7bf 100644
--- a/packages/integrations/vercel/src/serverless/adapter.ts
+++ b/packages/integrations/vercel/src/serverless/adapter.ts
@@ -13,8 +13,12 @@ import { exposeEnv } from '../lib/env.js';
import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js';
import { copyDependenciesToFunction } from '../lib/nft.js';
import { getRedirects } from '../lib/redirects.js';
+import { generateEdgeMiddleware } from './middleware.js';
+import { fileURLToPath } from 'node:url';
const PACKAGE_NAME = '@astrojs/vercel/serverless';
+export const ASTRO_LOCALS_HEADER = 'x-astro-locals';
+export const VERCEL_EDGE_MIDDLEWARE_FILE = 'vercel-edge-middleware';
function getAdapter(): AstroAdapter {
return {
@@ -70,6 +74,8 @@ export default function vercelServerless({
});
}
+ const filesToInclude = includeFiles?.map((file) => new URL(file, _config.root)) || [];
+
return {
name: PACKAGE_NAME,
hooks: {
@@ -106,17 +112,32 @@ export default function vercelServerless({
`);
}
},
- 'astro:build:ssr': async ({ entryPoints }) => {
+
+ 'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => {
_entryPoints = entryPoints;
+ if (middlewareEntryPoint) {
+ const outPath = fileURLToPath(buildTempFolder);
+ const vercelEdgeMiddlewareHandlerPath = new URL(
+ VERCEL_EDGE_MIDDLEWARE_FILE,
+ _config.srcDir
+ );
+ const bundledMiddlewarePath = await generateEdgeMiddleware(
+ middlewareEntryPoint,
+ outPath,
+ vercelEdgeMiddlewareHandlerPath
+ );
+ // let's tell the adapter that we need to save this file
+ filesToInclude.push(bundledMiddlewarePath);
+ }
},
+
'astro:build:done': async ({ routes }) => {
// Merge any includes from `vite.assetsInclude
- const inc = includeFiles?.map((file) => new URL(file, _config.root)) || [];
if (_config.vite.assetsInclude) {
const mergeGlobbedIncludes = (globPattern: unknown) => {
if (typeof globPattern === 'string') {
const entries = glob.sync(globPattern).map((p) => pathToFileURL(p));
- inc.push(...entries);
+ filesToInclude.push(...entries);
} else if (Array.isArray(globPattern)) {
for (const pattern of globPattern) {
mergeGlobbedIncludes(pattern);
@@ -133,14 +154,18 @@ export default function vercelServerless({
if (_entryPoints.size) {
for (const [route, entryFile] of _entryPoints) {
const func = basename(entryFile.toString()).replace(/\.mjs$/, '');
- await createFunctionFolder(func, entryFile, inc);
+ await createFunctionFolder(func, entryFile, filesToInclude);
routeDefinitions.push({
src: route.pattern.source,
dest: func,
});
}
} else {
- await createFunctionFolder('render', new URL(serverEntry, buildTempFolder), inc);
+ await createFunctionFolder(
+ 'render',
+ new URL(serverEntry, buildTempFolder),
+ filesToInclude
+ );
routeDefinitions.push({ src: '/.*', dest: 'render' });
}
diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts
index 71ad2bfae..3c0e22a28 100644
--- a/packages/integrations/vercel/src/serverless/entrypoint.ts
+++ b/packages/integrations/vercel/src/serverless/entrypoint.ts
@@ -4,6 +4,7 @@ import { App } from 'astro/app';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { getRequest, setResponse } from './request-transform';
+import { ASTRO_LOCALS_HEADER } from './adapter';
polyfill(globalThis, {
exclude: 'window document',
@@ -28,7 +29,14 @@ export const createExports = (manifest: SSRManifest) => {
return res.end('Not found');
}
- await setResponse(app, res, await app.render(request, routeData));
+ let locals = {};
+ if (request.headers.has(ASTRO_LOCALS_HEADER)) {
+ let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER);
+ if (localsAsString) {
+ locals = JSON.parse(localsAsString);
+ }
+ }
+ await setResponse(app, res, await app.render(request, routeData, locals));
};
return { default: handler };
diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts
new file mode 100644
index 000000000..2f05756c6
--- /dev/null
+++ b/packages/integrations/vercel/src/serverless/middleware.ts
@@ -0,0 +1,81 @@
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { join } from 'node:path';
+import { ASTRO_LOCALS_HEADER } from './adapter.js';
+import { existsSync } from 'fs';
+
+/**
+ * It generates the Vercel Edge Middleware file.
+ *
+ * It creates a temporary file, the edge middleware, with some dynamic info.
+ *
+ * Then this file gets bundled with esbuild. The bundle phase will inline the Astro middleware code.
+ *
+ * @param astroMiddlewareEntryPoint
+ * @param outPath
+ * @returns {Promise<URL>} The path to the bundled file
+ */
+export async function generateEdgeMiddleware(
+ astroMiddlewareEntryPointPath: URL,
+ outPath: string,
+ vercelEdgeMiddlewareHandlerPath: URL
+): Promise<URL> {
+ const entryPointPathURLAsString = JSON.stringify(
+ fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/')
+ );
+
+ const code = edgeMiddlewareTemplate(entryPointPathURLAsString, vercelEdgeMiddlewareHandlerPath);
+ // https://vercel.com/docs/concepts/functions/edge-middleware#create-edge-middleware
+ const bundledFilePath = join(outPath, 'middleware.mjs');
+ const esbuild = await import('esbuild');
+ await esbuild.build({
+ stdin: {
+ contents: code,
+ resolveDir: process.cwd(),
+ },
+ target: 'es2020',
+ platform: 'browser',
+ // https://runtime-keys.proposal.wintercg.org/#edge-light
+ conditions: ['edge-light', 'worker', 'browser'],
+ external: ['astro/middleware'],
+ outfile: bundledFilePath,
+ allowOverwrite: true,
+ format: 'esm',
+ bundle: true,
+ minify: false,
+ });
+ return pathToFileURL(bundledFilePath);
+}
+
+function edgeMiddlewareTemplate(middlewarePath: string, vercelEdgeMiddlewareHandlerPath: URL) {
+ const filePathEdgeMiddleware = fileURLToPath(vercelEdgeMiddlewareHandlerPath);
+ let handlerTemplateImport = '';
+ let handlerTemplateCall = '{}';
+ if (existsSync(filePathEdgeMiddleware) + '.js' || existsSync(filePathEdgeMiddleware) + '.ts') {
+ const stringified = JSON.stringify(filePathEdgeMiddleware.replace(/\\/g, '/'));
+ handlerTemplateImport = `import handler from ${stringified}`;
+ handlerTemplateCall = `handler({ request, context })`;
+ } else {
+ }
+ return `
+ ${handlerTemplateImport}
+import { onRequest } from ${middlewarePath};
+import { createContext, trySerializeLocals } from 'astro/middleware';
+export default async function middleware(request, context) {
+ const url = new URL(request.url);
+ const ctx = createContext({
+ request,
+ params: {}
+ });
+ ctx.locals = ${handlerTemplateCall};
+ const next = async () => {
+ const response = await fetch(url, {
+ headers: {
+ ${JSON.stringify(ASTRO_LOCALS_HEADER)}: trySerializeLocals(ctx.locals)
+ }
+ });
+ return response;
+ };
+
+ return onRequest(ctx, next);
+}`;
+}
diff --git a/packages/integrations/vercel/test/edge-middleware.test.js b/packages/integrations/vercel/test/edge-middleware.test.js
new file mode 100644
index 000000000..dd4b25b67
--- /dev/null
+++ b/packages/integrations/vercel/test/edge-middleware.test.js
@@ -0,0 +1,30 @@
+import { loadFixture } from './test-utils.js';
+import { expect, use } from 'chai';
+import chaiJestSnapshot from 'chai-jest-snapshot';
+
+use(chaiJestSnapshot);
+
+describe('Serverless prerender', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ beforeEach(function () {
+ chaiJestSnapshot.configureUsingMochaContext(this);
+ });
+
+ before(async () => {
+ chaiJestSnapshot.resetSnapshotRegistry();
+ fixture = await loadFixture({
+ root: './fixtures/middleware/',
+ });
+ });
+
+ it('build successfully the middleware edge file', async () => {
+ await fixture.build();
+ const contents = await fixture.readFile(
+ // this is abysmal...
+ '../.vercel/output/functions/render.func/packages/integrations/vercel/test/fixtures/middleware/dist/middleware.mjs'
+ );
+ expect(contents).to.matchSnapshot();
+ });
+});
diff --git a/packages/integrations/vercel/test/edge-middleware.test.js.snap b/packages/integrations/vercel/test/edge-middleware.test.js.snap
new file mode 100644
index 000000000..fe82ccff9
--- /dev/null
+++ b/packages/integrations/vercel/test/edge-middleware.test.js.snap
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Serverless prerender build successfully the middleware edge file 1`] = `
+"// test/fixtures/middleware/src/vercel-edge-middleware.js
+function vercel_edge_middleware_default({ request, context }) {
+ return {
+ title: \\"Hello world\\"
+ };
+}
+
+// test/fixtures/middleware/dist/middleware2.mjs
+var onRequest = async (context, next) => {
+ const response = await next();
+ return response;
+};
+
+// <stdin>
+import { createContext, trySerializeLocals } from \\"astro/middleware\\";
+async function middleware(request, context) {
+ const url = new URL(request.url);
+ const ctx = createContext({
+ request,
+ params: {}
+ });
+ ctx.locals = vercel_edge_middleware_default({ request, context });
+ const next = async () => {
+ const response = await fetch(url, {
+ headers: {
+ \\"x-astro-locals\\": trySerializeLocals(ctx.locals)
+ }
+ });
+ return response;
+ };
+ return onRequest(ctx, next);
+}
+export {
+ middleware as default
+};
+"
+`;
diff --git a/packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs b/packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs
new file mode 100644
index 000000000..321a8bde3
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs
@@ -0,0 +1,10 @@
+import {defineConfig} from "astro/config";
+import vercel from "@astrojs/vercel/serverless";
+
+export default defineConfig({
+ adapter: vercel(),
+ build: {
+ excludeMiddleware: true
+ },
+ output: 'server'
+}); \ No newline at end of file
diff --git a/packages/integrations/vercel/test/fixtures/middleware/package.json b/packages/integrations/vercel/test/fixtures/middleware/package.json
new file mode 100644
index 000000000..9ba60852d
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/vercel-edge-middleware",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/vercel": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/integrations/vercel/test/fixtures/middleware/src/middleware.js b/packages/integrations/vercel/test/fixtures/middleware/src/middleware.js
new file mode 100644
index 000000000..349a0aa79
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/src/middleware.js
@@ -0,0 +1,8 @@
+/**
+ * @type {import("astro").MiddlewareResponseHandler}
+ */
+export const onRequest = async (context, next) => {
+ const test = 'something';
+ const response = await next();
+ return response;
+};
diff --git a/packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro b/packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro
diff --git a/packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js b/packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js
new file mode 100644
index 000000000..bf69edb3e
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js
@@ -0,0 +1,5 @@
+export default function ({ request, context }) {
+ return {
+ title: 'Hello world',
+ };
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c4922d35c..0ee9cf674 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4898,6 +4898,9 @@ importers:
'@types/set-cookie-parser':
specifier: ^2.4.2
version: 2.4.2
+ '@vercel/edge':
+ specifier: ^0.3.4
+ version: 0.3.4
astro:
specifier: workspace:*
version: link:../../astro
@@ -4907,12 +4910,18 @@ importers:
chai:
specifier: ^4.3.7
version: 4.3.7
+ chai-jest-snapshot:
+ specifier: ^2.0.0
+ version: 2.0.0(chai@4.3.7)
cheerio:
specifier: 1.0.0-rc.12
version: 1.0.0-rc.12
mocha:
specifier: ^9.2.2
version: 9.2.2
+ rollup:
+ specifier: ^3.20.1
+ version: 3.25.1
packages/integrations/vercel/test/fixtures/basic:
dependencies:
@@ -4932,6 +4941,15 @@ importers:
specifier: workspace:*
version: link:../../../../../astro
+ packages/integrations/vercel/test/fixtures/middleware:
+ dependencies:
+ '@astrojs/vercel':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: workspace:*
+ version: link:../../../../../astro
+
packages/integrations/vercel/test/fixtures/no-output:
dependencies:
'@astrojs/vercel':
@@ -9015,6 +9033,10 @@ packages:
optional: true
dev: false
+ /@vercel/edge@0.3.4:
+ resolution: {integrity: sha512-dFU+yAUDQRwpuRGxRDlEO1LMq0y1LGsBgkyryQWe4w15/Fy2/lCnpvdIoAhHl3QvIGAxCLHzwRHsqfLRdpxgJQ==}
+ dev: true
+
/@vercel/nft@0.22.6:
resolution: {integrity: sha512-gTsFnnT4mGxodr4AUlW3/urY+8JKKB452LwF3m477RFUJTAaDmcz2JqFuInzvdybYIeyIv1sSONEJxsxnbQ5JQ==}
engines: {node: '>=14'}
@@ -9355,6 +9377,11 @@ packages:
type-fest: 1.4.0
dev: false
+ /ansi-regex@3.0.1:
+ resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==}
+ engines: {node: '>=4'}
+ dev: true
+
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -9885,6 +9912,16 @@ packages:
check-error: 1.0.2
dev: true
+ /chai-jest-snapshot@2.0.0(chai@4.3.7):
+ resolution: {integrity: sha512-u8jZZjw/0G1t5A8wDfH6K7DAVfMg3g0dsw9wKQURNUyrZX96VojHNrFMmLirq1m0kOvC5icgL/Qh/fu1MZyvUw==}
+ peerDependencies:
+ chai: '>=1.9.0'
+ dependencies:
+ chai: 4.3.7
+ jest-snapshot: 21.2.1
+ lodash.values: 4.3.0
+ dev: true
+
/chai-xml@0.4.1(chai@4.3.7):
resolution: {integrity: sha512-VUf5Ol4ifOAsgz+lN4tfWENgQtrKxHPWsmpL5wdbqQdkpblZkcDlaT2aFvsPQH219Yvl8vc4064yFErgBIn9bw==}
engines: {node: '>= 0.8.0'}
@@ -10594,6 +10631,11 @@ packages:
/didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+ /diff@3.5.0:
+ resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==}
+ engines: {node: '>=0.3.1'}
+ dev: true
+
/diff@5.0.0:
resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==}
engines: {node: '>=0.3.1'}
@@ -12837,6 +12879,38 @@ packages:
minimatch: 3.1.2
dev: false
+ /jest-diff@21.2.1:
+ resolution: {integrity: sha512-E5fu6r7PvvPr5qAWE1RaUwIh/k6Zx/3OOkZ4rk5dBJkEWRrUuSgbMt2EO8IUTPTd6DOqU3LW6uTIwX5FRvXoFA==}
+ dependencies:
+ chalk: 2.4.2
+ diff: 3.5.0
+ jest-get-type: 21.2.0
+ pretty-format: 21.2.1
+ dev: true
+
+ /jest-get-type@21.2.0:
+ resolution: {integrity: sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q==}
+ dev: true
+
+ /jest-matcher-utils@21.2.1:
+ resolution: {integrity: sha512-kn56My+sekD43dwQPrXBl9Zn9tAqwoy25xxe7/iY4u+mG8P3ALj5IK7MLHZ4Mi3xW7uWVCjGY8cm4PqgbsqMCg==}
+ dependencies:
+ chalk: 2.4.2
+ jest-get-type: 21.2.0
+ pretty-format: 21.2.1
+ dev: true
+
+ /jest-snapshot@21.2.1:
+ resolution: {integrity: sha512-bpaeBnDpdqaRTzN8tWg0DqOTo2DvD3StOemxn67CUd1p1Po+BUpvePAp44jdJ7Pxcjfg+42o4NHw1SxdCA2rvg==}
+ dependencies:
+ chalk: 2.4.2
+ jest-diff: 21.2.1
+ jest-matcher-utils: 21.2.1
+ mkdirp: 0.5.6
+ natural-compare: 1.4.0
+ pretty-format: 21.2.1
+ dev: true
+
/jest-worker@26.6.2:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'}
@@ -13134,6 +13208,10 @@ packages:
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
dev: true
+ /lodash.values@4.3.0:
+ resolution: {integrity: sha512-r0RwvdCv8id9TUblb/O7rYPwVy6lerCbcawrfdo9iC/1t1wsNMJknO79WNBgwkH0hIeJ08jmvvESbFpNb4jH0Q==}
+ dev: true
+
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
@@ -14018,6 +14096,13 @@ packages:
/mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+ /mkdirp@0.5.6:
+ resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+ hasBin: true
+ dependencies:
+ minimist: 1.2.8
+ dev: true
+
/mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
@@ -15221,6 +15306,13 @@ packages:
engines: {node: ^14.13.1 || >=16.0.0}
dev: false
+ /pretty-format@21.2.1:
+ resolution: {integrity: sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A==}
+ dependencies:
+ ansi-regex: 3.0.1
+ ansi-styles: 3.2.1
+ dev: true
+
/pretty-format@27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}