0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

feat(@astrojs/netlify): add build.split support (#7615)

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
This commit is contained in:
Emanuele Stoppa 2023-07-13 09:21:33 +01:00 committed by GitHub
parent b30a1bc2b8
commit f21357b69d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 234 additions and 35 deletions

View file

@ -0,0 +1,20 @@
---
'@astrojs/netlify': minor
---
The Netlify adapter builds to a single function by default. Astro 2.7 added support for splitting your build into separate entry points per page. If you use this configuration, the Netlify adapter will generate a separate function for each page. This can help reduce the size of each function so they are only bundling code used on that page.
```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';
export default defineConfig({
output: 'server',
adapter: netlify(),
build: {
split: true,
},
});
```

View file

@ -0,0 +1,5 @@
---
'@astrojs/underscore-redirects': minor
---
Refactor how the routes are passed.

View file

@ -264,10 +264,14 @@ export default function createIntegration(args?: Options): AstroIntegration {
}
}
const redirectRoutes = routes.filter((r) => r.type === 'redirect');
const redirectRoutes: [RouteData, string][] = routes
.filter((r) => r.type === 'redirect')
.map((r) => {
return [r, ''];
});
const trueRedirects = createRedirectsFromAstroRoutes({
config: _config,
routes: redirectRoutes,
routeToDynamicTargetMap: new Map(Array.from(redirectRoutes)),
dir,
});
if (!trueRedirects.empty()) {

View file

@ -72,6 +72,24 @@ export default defineConfig({
});
```
### Per-page functions
The Netlify adapter builds to a single function by default. Astro 2.7 added support for splitting your build into separate entry points per page. If you use this configuration, the Netlify adapter will generate a separate function for each page. This can help reduce the size of each function so they are only bundling code used on that page.
```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';
export default defineConfig({
output: 'server',
adapter: netlify(),
build: {
split: true,
},
});
```
### Static sites
For static sites you usually don't need an adapter. However, if you use `redirects` configuration (experimental) in your Astro config, the Netlify adapter can be used to translate this to the proper `_redirects` format.

View file

@ -32,7 +32,7 @@
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test-fn": "mocha --exit --timeout 20000 test/functions/",
"test-fn": "mocha --exit --timeout 20000 --file \"./test/setup.js\" test/functions/",
"test-edge": "deno test --allow-run --allow-read --allow-net --allow-env ./test/edge-functions/",
"test": "npm run test-fn"
},
@ -54,7 +54,8 @@
"chai": "^4.3.7",
"cheerio": "1.0.0-rc.12",
"mocha": "^9.2.2",
"vite": "^4.3.9"
"vite": "^4.3.9",
"chai-jest-snapshot": "^2.0.0"
},
"astro": {
"external": true

View file

@ -166,7 +166,12 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
'astro:build:done': async ({ routes, dir }) => {
await bundleServerEntry(_buildConfig, _vite);
await createEdgeManifest(routes, entryFile, _config.root);
await createRedirects(_config, routes, dir, entryFile, 'edge-functions');
const dynamicTarget = `/.netlify/edge-functions/${entryFile}`;
const map: [RouteData, string][] = routes.map((route) => {
return [route, dynamicTarget];
});
const routeToDynamicTargetMap = new Map(Array.from(map));
await createRedirects(_config, routeToDynamicTargetMap, dir);
},
},
};

View file

@ -1,6 +1,8 @@
import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
import type { Args } from './netlify-functions.js';
import { createRedirects } from './shared.js';
import { fileURLToPath } from 'node:url';
import { extname } from 'node:path';
export function getAdapter(args: Args = {}): AstroAdapter {
return {
@ -23,7 +25,8 @@ function netlifyFunctions({
binaryMediaTypes,
}: NetlifyFunctionsOptions = {}): AstroIntegration {
let _config: AstroConfig;
let entryFile: string;
let _entryPoints: Map<RouteData, URL>;
let ssrEntryFile: string;
return {
name: '@astrojs/netlify',
hooks: {
@ -37,10 +40,13 @@ function netlifyFunctions({
},
});
},
'astro:build:ssr': ({ entryPoints }) => {
_entryPoints = entryPoints;
},
'astro:config:done': ({ config, setAdapter }) => {
setAdapter(getAdapter({ binaryMediaTypes, builders }));
_config = config;
entryFile = config.build.serverEntry.replace(/\.m?js/, '');
ssrEntryFile = config.build.serverEntry.replace(/\.m?js/, '');
if (config.output === 'static') {
console.warn(
@ -53,7 +59,32 @@ function netlifyFunctions({
},
'astro:build:done': async ({ routes, dir }) => {
const type = builders ? 'builders' : 'functions';
await createRedirects(_config, routes, dir, entryFile, type);
const kind = type ?? 'functions';
if (_entryPoints.size) {
const routeToDynamicTargetMap = new Map();
for (const [route, entryFile] of _entryPoints) {
const wholeFileUrl = fileURLToPath(entryFile);
const extension = extname(wholeFileUrl);
const relative = wholeFileUrl
.replace(fileURLToPath(_config.build.server), '')
.replace(extension, '')
.replaceAll('\\', '/');
const dynamicTarget = `/.netlify/${kind}/${relative}`;
routeToDynamicTargetMap.set(route, dynamicTarget);
}
await createRedirects(_config, routeToDynamicTargetMap, dir);
} else {
const dynamicTarget = `/.netlify/${kind}/${ssrEntryFile}`;
const map: [RouteData, string][] = routes.map((route) => {
return [route, dynamicTarget];
});
const routeToDynamicTargetMap = new Map(Array.from(map));
await createRedirects(_config, routeToDynamicTargetMap, dir);
}
},
},
};

View file

@ -1,4 +1,4 @@
import type { AstroIntegration } from 'astro';
import type { AstroIntegration, RouteData } from 'astro';
import { createRedirects } from './shared.js';
export function netlifyStatic(): AstroIntegration {
@ -18,7 +18,12 @@ export function netlifyStatic(): AstroIntegration {
_config = config;
},
'astro:build:done': async ({ dir, routes }) => {
await createRedirects(_config, routes, dir, '', 'static');
const mappedRoutes: [RouteData, string][] = routes.map((route) => [
route,
`/.netlify/static/`,
]);
const routesToDynamicTargetMap = new Map(Array.from(mappedRoutes));
await createRedirects(_config, routesToDynamicTargetMap, dir);
},
},
};

View file

@ -4,20 +4,15 @@ import fs from 'node:fs';
export async function createRedirects(
config: AstroConfig,
routes: RouteData[],
dir: URL,
entryFile: string,
type: 'functions' | 'edge-functions' | 'builders' | 'static'
routeToDynamicTargetMap: Map<RouteData, string>,
dir: URL
) {
const kind = type ?? 'functions';
const dynamicTarget = `/.netlify/${kind}/${entryFile}`;
const _redirectsURL = new URL('./_redirects', dir);
const _redirects = createRedirectsFromAstroRoutes({
config,
routes,
routeToDynamicTargetMap,
dir,
dynamicTarget,
});
const content = _redirects.print();

View file

@ -1,8 +1,8 @@
<html>
<head>
<title>Testing</title>
<title>Blog</title>
</head>
<body>
<h1>testing</h1>
<h1>Blog</h1>
</body>
</html>

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>testing</h1>
</body>
</html>

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Blog</h1>
</body>
</html>

View file

@ -46,5 +46,6 @@ describe('SSG - Redirects', () => {
'/.netlify/functions/entry',
'200',
]);
expect(redirects).to.matchSnapshot();
});
});

View file

@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SSG - Redirects Creates a redirects file 1`] = `
"/other / 301
/nope /.netlify/functions/entry 200
/ /.netlify/functions/entry 200
/team/articles/* /.netlify/functions/entry 200"
`;

View file

@ -0,0 +1,63 @@
import { expect } from 'chai';
import netlifyAdapter from '../../dist/index.js';
import { loadFixture, testIntegration } from './test-utils.js';
describe('Split support', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let _entryPoints;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/split-support/', import.meta.url).toString(),
output: 'server',
adapter: netlifyAdapter({
dist: new URL('./fixtures/split-support/dist/', import.meta.url),
}),
site: `http://example.com`,
integrations: [
testIntegration({
setEntryPoints(ep) {
_entryPoints = ep;
},
}),
],
build: {
split: true,
},
});
await fixture.build();
});
it('outputs a correct redirect file', async () => {
const redir = await fixture.readFile('/_redirects');
const lines = redir.split(/[\r\n]+/);
expect(lines.length).to.equal(2);
expect(lines[0].includes('/blog')).to.be.true;
expect(lines[0].includes('blog.astro')).to.be.true;
expect(lines[0].includes('200')).to.be.true;
expect(lines[1].includes('/')).to.be.true;
expect(lines[1].includes('index.astro')).to.be.true;
expect(lines[1].includes('200')).to.be.true;
});
describe('Should create multiple functions', () => {
it('and hit 200', async () => {
if (_entryPoints) {
for (const [, filePath] of _entryPoints) {
const { handler } = await import(filePath.toString());
const resp = await handler({
httpMethod: 'POST',
headers: {},
rawUrl: 'http://example.com/',
body: '{}',
});
expect(resp.statusCode).to.equal(200);
}
} else {
expect(false).to.be.true;
}
});
});
});

View file

@ -7,7 +7,7 @@ export * from '../../../../astro/test/test-utils.js';
*
* @returns {import('../../../../astro/dist/types/@types/astro').AstroIntegration}
*/
export function testIntegration() {
export function testIntegration({ setEntryPoints } = {}) {
return {
name: '@astrojs/netlify/test-integration',
hooks: {
@ -24,6 +24,11 @@ export function testIntegration() {
},
});
},
'astro:build:ssr': ({ entryPoints }) => {
if (entryPoints.size) {
setEntryPoints(entryPoints);
}
},
},
};
}

View file

@ -0,0 +1,12 @@
import { use } from 'chai';
import chaiJestSnapshot from 'chai-jest-snapshot';
use(chaiJestSnapshot);
before(function () {
chaiJestSnapshot.resetSnapshotRegistry();
});
beforeEach(function () {
chaiJestSnapshot.configureUsingMochaContext(this);
});

View file

@ -13,9 +13,11 @@ function getRedirectStatus(route: RouteData): ValidRedirectStatus {
interface CreateRedirectsFromAstroRoutesParams {
config: Pick<AstroConfig, 'build' | 'output'>;
routes: RouteData[];
/**
* Maps a `RouteData` to a dynamic target
*/
routeToDynamicTargetMap: Map<RouteData, string>;
dir: URL;
dynamicTarget?: string;
}
/**
@ -23,18 +25,17 @@ interface CreateRedirectsFromAstroRoutesParams {
*/
export function createRedirectsFromAstroRoutes({
config,
routes,
routeToDynamicTargetMap,
dir,
dynamicTarget = '',
}: CreateRedirectsFromAstroRoutesParams) {
const output = config.output;
const _redirects = new Redirects();
for (const route of routes) {
for (const [route, dynamicTarget = ''] of routeToDynamicTargetMap) {
// A route with a `pathname` is as static route.
if (route.pathname) {
if (route.redirect) {
// A redirect route without dynamic parts. Get the redirect status
// A redirect route without dynami§c parts. Get the redirect status
// from the user if provided.
_redirects.add({
dynamic: false,

View file

@ -8,16 +8,22 @@ describe('Astro', () => {
};
it('Creates a Redirects object from routes', () => {
const routes = [
const routeToDynamicTargetMap = new Map(
Array.from([
[
{ pathname: '/', distURL: new URL('./index.html', import.meta.url), segments: [] },
'./.adapter/dist/entry.mjs',
],
[
{ pathname: '/one', distURL: new URL('./one/index.html', import.meta.url), segments: [] },
];
const dynamicTarget = './.adapter/dist/entry.mjs';
'./.adapter/dist/entry.mjs',
],
])
);
const _redirects = createRedirectsFromAstroRoutes({
config: serverConfig,
routes,
routeToDynamicTargetMap,
dir: new URL(import.meta.url),
dynamicTarget,
});
expect(_redirects.definitions).to.have.a.lengthOf(2);

View file

@ -4431,6 +4431,9 @@ 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