feat: vercel edge middleware support (#7532)
Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
cfd5b2b785
commit
9e5fafa2b2
36 changed files with 758 additions and 50 deletions
11
.changeset/brown-shrimps-hug.md
Normal file
11
.changeset/brown-shrimps-hug.md
Normal file
|
@ -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
.changeset/chilly-pants-fix.md
Normal file
24
.changeset/chilly-pants-fix.md
Normal file
|
@ -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
.changeset/cool-kids-grin.md
Normal file
5
.changeset/cool-kids-grin.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Correctly track the middleware during the SSR build.
|
11
.changeset/good-pigs-fetch.md
Normal file
11
.changeset/good-pigs-fetch.md
Normal file
|
@ -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
.changeset/long-geckos-battle.md
Normal file
7
.changeset/long-geckos-battle.md
Normal file
|
@ -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
.changeset/strong-years-travel.md
Normal file
20
.changeset/strong-years-travel.md
Normal file
|
@ -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.
|
|
@ -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: {
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -88,6 +88,7 @@ export interface BuildInternals {
|
|||
entryPoints: Map<RouteData, URL>;
|
||||
ssrSplitEntryChunks: Map<string, Rollup.OutputChunk>;
|
||||
componentMetadata: SSRResult['componentMetadata'];
|
||||
middlewareEntryPoint?: URL;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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')}`;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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({}),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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' });
|
||||
}
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
81
packages/integrations/vercel/src/serverless/middleware.ts
Normal file
81
packages/integrations/vercel/src/serverless/middleware.ts
Normal file
|
@ -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);
|
||||
}`;
|
||||
}
|
30
packages/integrations/vercel/test/edge-middleware.test.js
Normal file
30
packages/integrations/vercel/test/edge-middleware.test.js
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
};
|
||||
"
|
||||
`;
|
10
packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs
vendored
Normal file
10
packages/integrations/vercel/test/fixtures/middleware/astro.config.mjs
vendored
Normal file
|
@ -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'
|
||||
});
|
9
packages/integrations/vercel/test/fixtures/middleware/package.json
vendored
Normal file
9
packages/integrations/vercel/test/fixtures/middleware/package.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@test/vercel-edge-middleware",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/vercel": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
8
packages/integrations/vercel/test/fixtures/middleware/src/middleware.js
vendored
Normal file
8
packages/integrations/vercel/test/fixtures/middleware/src/middleware.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* @type {import("astro").MiddlewareResponseHandler}
|
||||
*/
|
||||
export const onRequest = async (context, next) => {
|
||||
const test = 'something';
|
||||
const response = await next();
|
||||
return response;
|
||||
};
|
0
packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro
vendored
Normal file
0
packages/integrations/vercel/test/fixtures/middleware/src/pages/index.astro
vendored
Normal file
5
packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js
vendored
Normal file
5
packages/integrations/vercel/test/fixtures/middleware/src/vercel-edge-middleware.js
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default function ({ request, context }) {
|
||||
return {
|
||||
title: 'Hello world',
|
||||
};
|
||||
}
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue