Skip to content

Commit 736216b

Browse files
feat: convert netlify and vercel adapters to new API (#15413)
* feat: convert adapters to new API * vercel * fix: netlify tests * fix: netlify tests * changeset * changeset * Discard changes to packages/integrations/netlify/test/functions/redirects.test.js * Discard changes to packages/integrations/netlify/test/functions/cookies.test.js * Discard changes to packages/integrations/netlify/test/functions/include-files.test.js * Discard changes to packages/integrations/netlify/test/functions/sessions.test.js * fix: prerendered 404
1 parent db988d6 commit 736216b

File tree

20 files changed

+271
-185
lines changed

20 files changed

+271
-185
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/vercel': major
3+
---
4+
5+
Removes the deprecated `@astrojs/vercel/serverless` and `@astrojs/vercel/static` exports. Use the `@astrojs/vercel` export instead

.changeset/sweet-buttons-jam.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@astrojs/netlify': minor
3+
'@astrojs/vercel': minor
4+
---
5+
6+
Updates the implementation to use the new Adapter API

packages/integrations/cloudflare/src/utils/handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ declare global {
2424

2525
type CfResponse = Awaited<ReturnType<Required<ExportedHandler<Env>>['fetch']>>;
2626

27+
const app = createApp();
28+
2729
export async function handle(
2830
request: Request,
2931
env: Env,
3032
context: ExecutionContext,
3133
): Promise<CfResponse> {
32-
const app = createApp();
33-
3434
// Handle prerender endpoints (only active during build prerender phase)
3535
if (isPrerender) {
3636
if (isStaticPathsRequest(request)) {

packages/integrations/cloudflare/src/vite-plugin-config.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import type { PluginOption } from 'vite';
33
const VIRTUAL_CONFIG_ID = 'virtual:astro-cloudflare:config';
44
const RESOLVED_VIRTUAL_CONFIG_ID = '\0' + VIRTUAL_CONFIG_ID;
55

6-
interface CloudflareConfig {
6+
export interface Config {
77
sessionKVBindingName: string;
8+
isPrerender: boolean;
89
}
910

10-
export function createConfigPlugin(config: CloudflareConfig): PluginOption {
11+
export function createConfigPlugin(config: Omit<Config, 'isPrerender'>): PluginOption {
1112
return {
12-
name: 'vite:astro-cloudflare-config',
13+
name: VIRTUAL_CONFIG_ID,
1314
resolveId: {
1415
filter: {
1516
id: new RegExp(`^${VIRTUAL_CONFIG_ID}$`),
@@ -23,9 +24,10 @@ export function createConfigPlugin(config: CloudflareConfig): PluginOption {
2324
id: new RegExp(`^${RESOLVED_VIRTUAL_CONFIG_ID}$`),
2425
},
2526
handler() {
26-
const isPrerender = this.environment?.name === 'prerender';
27-
return `export const sessionKVBindingName = ${JSON.stringify(config.sessionKVBindingName)};
28-
export const isPrerender = ${isPrerender};`;
27+
return [
28+
...Object.entries(config).map(([k, v]) => `export const ${k} = ${JSON.stringify(v)};`),
29+
`export const isPrerender = ${this.environment?.name === 'prerender'};`,
30+
].join('\n');
2931
},
3032
},
3133
};

packages/integrations/cloudflare/virtual.d.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
/// <reference types="@cloudflare/workers-types" />
33

44
declare module 'virtual:astro-cloudflare:config' {
5-
export const sessionKVBindingName: string;
6-
/** True when running in the prerender environment during build */
7-
export const isPrerender: boolean;
5+
const config: import('./src/vite-plugin-config.js').Config;
6+
export = config;
87
}
98

109
declare namespace Cloudflare {

packages/integrations/netlify/src/index.ts

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import type {
1717
import { build } from 'esbuild';
1818
import { glob, globSync } from 'tinyglobby';
1919
import { copyDependenciesToFunction } from './lib/nft.js';
20-
import type { Args } from './ssr-function.js';
2120
import { sessionDrivers } from 'astro/config';
21+
import { createConfigPlugin } from './vite-plugin-config.js';
2222

2323
const { version: packageVersion } = JSON.parse(
2424
await readFile(new URL('../package.json', import.meta.url), 'utf8'),
@@ -361,15 +361,17 @@ export default function netlifyIntegration(
361361
}
362362

363363
async function writeSSRFunction({
364-
notFoundContent,
365364
logger,
366365
root,
366+
serverEntry,
367+
notFoundContent,
367368
}: {
368-
notFoundContent?: string;
369369
logger: AstroIntegrationLogger;
370370
root: URL;
371+
serverEntry: string;
372+
notFoundContent: string | undefined;
371373
}) {
372-
const entry = new URL('./entry.mjs', ssrBuildDir());
374+
const entry = new URL(`./${serverEntry}`, ssrBuildDir());
373375

374376
const _includeFiles = integrationConfig?.includeFiles || [];
375377
const _excludeFiles = integrationConfig?.excludeFiles || [];
@@ -412,27 +414,25 @@ export default function netlifyIntegration(
412414
await writeFile(
413415
new URL('./ssr.mjs', ssrOutputDir()),
414416
`
415-
import createSSRHandler from './${handler}';
416-
export default createSSRHandler(${JSON.stringify({
417-
cacheOnDemandPages: Boolean(integrationConfig?.cacheOnDemandPages),
418-
notFoundContent,
419-
})});
420-
export const config = {
421-
includedFiles: ['**/*'],
422-
name: 'Astro SSR',
423-
nodeBundler: 'none',
424-
generator: '@astrojs/netlify@${packageVersion}',
425-
path: '/*',
426-
preferStatic: true,
427-
};
428-
`,
417+
import { config, createHandler } from './${handler}';
418+
419+
export default createHandler(${JSON.stringify({ notFoundContent })});
420+
421+
export { config };
422+
`,
429423
);
430424
}
431425

432-
async function writeMiddleware(entrypoint: URL) {
426+
async function writeMiddleware({
427+
entrypoint,
428+
serverEntry,
429+
}: {
430+
entrypoint: URL;
431+
serverEntry: string;
432+
}) {
433433
await mkdir(middlewareOutputDir(), { recursive: true });
434434
await writeFile(
435-
new URL('./entry.mjs', middlewareOutputDir()),
435+
new URL(`./${serverEntry}`, middlewareOutputDir()),
436436
/* ts */ `
437437
import { onRequest } from "${fileURLToPath(entrypoint).replaceAll('\\', '/')}";
438438
import { createContext, trySerializeLocals } from 'astro/middleware';
@@ -482,7 +482,7 @@ export default function netlifyIntegration(
482482

483483
// taking over bundling, because Netlify bundling trips over NPM modules
484484
await build({
485-
entryPoints: [fileURLToPath(new URL('./entry.mjs', middlewareOutputDir()))],
485+
entryPoints: [fileURLToPath(new URL(`./${serverEntry}`, middlewareOutputDir()))],
486486
// allow `node:` prefixed imports, which are valid in netlify's deno edge runtime
487487
plugins: [
488488
{
@@ -638,15 +638,28 @@ export default function netlifyIntegration(
638638
redirects: false,
639639
client: outDir,
640640
server: ssrBuildDir(),
641+
serverEntry: 'ssr-function.mjs',
641642
},
642643
session,
643644
vite: {
644-
plugins: [netlifyVitePlugin(vitePluginOptions)],
645+
plugins: [
646+
netlifyVitePlugin(vitePluginOptions),
647+
createConfigPlugin({
648+
middlewareSecret,
649+
cacheOnDemandPages: !!integrationConfig?.cacheOnDemandPages,
650+
packageVersion,
651+
}),
652+
],
645653
server: {
646654
watch: {
647655
ignored: [fileURLToPath(new URL('./.netlify/**', rootDir))],
648656
},
649657
},
658+
build: {
659+
rollupOptions: {
660+
input: '@astrojs/netlify/ssr-function.js',
661+
},
662+
},
650663
},
651664
image: {
652665
service: {
@@ -675,13 +688,11 @@ export default function netlifyIntegration(
675688

676689
setAdapter({
677690
name: '@astrojs/netlify',
678-
serverEntrypoint: '@astrojs/netlify/ssr-function.js',
679-
exports: ['default'],
691+
entryType: 'self',
680692
adapterFeatures: {
681693
edgeMiddleware: useEdgeMiddleware,
682694
staticHeaders: useStaticHeaders,
683695
},
684-
args: { middlewareSecret } satisfies Args,
685696
supportedAstroFeatures: {
686697
hybridOutput: 'stable',
687698
staticOutput: 'stable',
@@ -718,11 +729,19 @@ export default function netlifyIntegration(
718729
try {
719730
notFoundContent = await readFile(new URL('./404.html', dir), 'utf8');
720731
} catch {}
721-
await writeSSRFunction({ notFoundContent, logger, root: _config.root });
732+
await writeSSRFunction({
733+
logger,
734+
root: _config.root,
735+
serverEntry: _config.build.serverEntry,
736+
notFoundContent,
737+
});
722738
logger.info('Generated SSR Function');
723739
}
724740
if (astroMiddlewareEntryPoint) {
725-
await writeMiddleware(astroMiddlewareEntryPoint);
741+
await writeMiddleware({
742+
entrypoint: astroMiddlewareEntryPoint,
743+
serverEntry: _config.build.serverEntry,
744+
});
726745
logger.info('Generated Middleware Edge Function');
727746
}
728747

Lines changed: 54 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,75 @@
11
import type { Context } from '@netlify/functions';
2-
import type { SSRManifest } from 'astro';
3-
import { App } from 'astro/app';
42
import { setGetEnv } from 'astro/env/setup';
3+
import { middlewareSecret, cacheOnDemandPages, packageVersion } from 'virtual:astro-netlify:config';
4+
import { createApp } from 'astro/app/entrypoint';
55

66
setGetEnv((key) => process.env[key]);
77

8-
export interface Args {
9-
middlewareSecret: string;
10-
}
8+
const app = createApp();
119

12-
export const createExports = (manifest: SSRManifest, { middlewareSecret }: Args) => {
13-
const app = new App(manifest);
10+
export function createHandler({ notFoundContent }: { notFoundContent: string | undefined }) {
11+
return async function handler(request: Request, context: Context): Promise<Response> {
12+
const routeData = app.match(request);
1413

15-
function createHandler(integrationConfig: {
16-
cacheOnDemandPages: boolean;
17-
notFoundContent?: string;
18-
}) {
19-
return async function handler(request: Request, context: Context) {
20-
const routeData = app.match(request);
21-
if (!routeData && typeof integrationConfig.notFoundContent !== 'undefined') {
22-
return new Response(integrationConfig.notFoundContent, {
23-
status: 404,
24-
headers: { 'Content-Type': 'text/html; charset=utf-8' },
25-
});
26-
}
14+
if (!routeData && typeof notFoundContent !== 'undefined') {
15+
return new Response(notFoundContent, {
16+
status: 404,
17+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
18+
});
19+
}
2720

28-
let locals: Record<string, unknown> = {};
21+
let locals: Record<string, unknown> = {};
2922

30-
const astroLocalsHeader = request.headers.get('x-astro-locals');
31-
const middlewareSecretHeader = request.headers.get('x-astro-middleware-secret');
32-
if (astroLocalsHeader) {
33-
if (middlewareSecretHeader !== middlewareSecret) {
34-
return new Response('Forbidden', { status: 403 });
35-
}
36-
// hide the secret from the rest of user and library code
37-
request.headers.delete('x-astro-middleware-secret');
38-
locals = JSON.parse(astroLocalsHeader);
23+
const astroLocalsHeader = request.headers.get('x-astro-locals');
24+
const middlewareSecretHeader = request.headers.get('x-astro-middleware-secret');
25+
if (astroLocalsHeader) {
26+
if (middlewareSecretHeader !== middlewareSecret) {
27+
return new Response('Forbidden', { status: 403 });
3928
}
29+
// hide the secret from the rest of user and library code
30+
request.headers.delete('x-astro-middleware-secret');
31+
locals = JSON.parse(astroLocalsHeader);
32+
}
4033

41-
locals.netlify = { context };
34+
locals.netlify = { context };
4235

43-
const response = await app.render(request, {
44-
routeData,
45-
locals,
46-
clientAddress: context.ip,
47-
});
36+
const response = await app.render(request, {
37+
routeData,
38+
locals,
39+
clientAddress: context.ip,
40+
});
4841

49-
if (app.setCookieHeaders) {
50-
for (const setCookieHeader of app.setCookieHeaders(response)) {
51-
response.headers.append('Set-Cookie', setCookieHeader);
52-
}
42+
if (app.setCookieHeaders) {
43+
for (const setCookieHeader of app.setCookieHeaders(response)) {
44+
response.headers.append('Set-Cookie', setCookieHeader);
5345
}
46+
}
5447

55-
if (integrationConfig.cacheOnDemandPages) {
56-
const isCacheableMethod = ['GET', 'HEAD'].includes(request.method);
48+
if (cacheOnDemandPages) {
49+
const isCacheableMethod = ['GET', 'HEAD'].includes(request.method);
5750

58-
// any user-provided Cache-Control headers take precedence
59-
const hasCacheControl = [
60-
'Cache-Control',
61-
'CDN-Cache-Control',
62-
'Netlify-CDN-Cache-Control',
63-
].some((header) => response.headers.has(header));
51+
// any user-provided Cache-Control headers take precedence
52+
const hasCacheControl = [
53+
'Cache-Control',
54+
'CDN-Cache-Control',
55+
'Netlify-CDN-Cache-Control',
56+
].some((header) => response.headers.has(header));
6457

65-
if (isCacheableMethod && !hasCacheControl) {
66-
// caches this page for up to a year
67-
response.headers.append('CDN-Cache-Control', 'public, max-age=31536000, must-revalidate');
68-
}
58+
if (isCacheableMethod && !hasCacheControl) {
59+
// caches this page for up to a year
60+
response.headers.append('CDN-Cache-Control', 'public, max-age=31536000, must-revalidate');
6961
}
62+
}
7063

71-
return response;
72-
};
73-
}
64+
return response;
65+
};
66+
}
7467

75-
return { default: createHandler };
68+
export const config = {
69+
includedFiles: ['**/*'],
70+
name: 'Astro SSR',
71+
nodeBundler: 'none',
72+
generator: `@astrojs/netlify@${packageVersion}`,
73+
path: '/*',
74+
preferStatic: true,
7675
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { PluginOption } from 'vite';
2+
3+
const VIRTUAL_CONFIG_ID = 'virtual:astro-netlify:config';
4+
const RESOLVED_VIRTUAL_CONFIG_ID = '\0' + VIRTUAL_CONFIG_ID;
5+
6+
export interface Config {
7+
middlewareSecret: string;
8+
cacheOnDemandPages: boolean;
9+
packageVersion: string;
10+
}
11+
12+
export function createConfigPlugin(config: Config): PluginOption {
13+
return {
14+
name: VIRTUAL_CONFIG_ID,
15+
resolveId: {
16+
filter: {
17+
id: new RegExp(`^${VIRTUAL_CONFIG_ID}$`),
18+
},
19+
handler() {
20+
return RESOLVED_VIRTUAL_CONFIG_ID;
21+
},
22+
},
23+
load: {
24+
filter: {
25+
id: new RegExp(`^${RESOLVED_VIRTUAL_CONFIG_ID}$`),
26+
},
27+
handler() {
28+
return Object.entries(config)
29+
.map(([k, v]) => `export const ${k} = ${JSON.stringify(v)};`)
30+
.join('\n');
31+
},
32+
},
33+
};
34+
}

0 commit comments

Comments
 (0)