Skip to content

Commit

Permalink
feat(@astrojs/netlify): add build.split support (#7615)
Browse files Browse the repository at this point in the history
Co-authored-by: Sarah Rainsberger <[email protected]>
Co-authored-by: Bjorn Lu <[email protected]>
  • Loading branch information
3 people authored Jul 13, 2023
1 parent b30a1bc commit f21357b
Show file tree
Hide file tree
Showing 20 changed files with 234 additions and 35 deletions.
20 changes: 20 additions & 0 deletions .changeset/happy-frogs-appear.md
Original file line number Diff line number Diff line change
@@ -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,
},
});
```
5 changes: 5 additions & 0 deletions .changeset/nasty-geckos-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/underscore-redirects': minor
---

Refactor how the routes are passed.
8 changes: 6 additions & 2 deletions packages/integrations/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
18 changes: 18 additions & 0 deletions packages/integrations/netlify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions packages/integrations/netlify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
},
};
Expand Down
39 changes: 35 additions & 4 deletions packages/integrations/netlify/src/integration-functions.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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: {
Expand All @@ -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(
Expand All @@ -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);
}
},
},
};
Expand Down
9 changes: 7 additions & 2 deletions packages/integrations/netlify/src/integration-static.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AstroIntegration } from 'astro';
import type { AstroIntegration, RouteData } from 'astro';
import { createRedirects } from './shared.js';

export function netlifyStatic(): AstroIntegration {
Expand All @@ -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);
},
},
};
Expand Down
11 changes: 3 additions & 8 deletions packages/integrations/netlify/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<html>
<head>
<title>Testing</title>
<title>Blog</title>
</head>
<body>
<h1>testing</h1>
<h1>Blog</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>testing</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Blog</h1>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ describe('SSG - Redirects', () => {
'/.netlify/functions/entry',
'200',
]);
expect(redirects).to.matchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -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"
`;
63 changes: 63 additions & 0 deletions packages/integrations/netlify/test/functions/split-support.test.js
Original file line number Diff line number Diff line change
@@ -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;
}
});
});
});
7 changes: 6 additions & 1 deletion packages/integrations/netlify/test/functions/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -24,6 +24,11 @@ export function testIntegration() {
},
});
},
'astro:build:ssr': ({ entryPoints }) => {
if (entryPoints.size) {
setEntryPoints(entryPoints);
}
},
},
};
}
12 changes: 12 additions & 0 deletions packages/integrations/netlify/test/setup.js
Original file line number Diff line number Diff line change
@@ -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);
});
13 changes: 7 additions & 6 deletions packages/underscore-redirects/src/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,29 @@ 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;
}

/**
* Takes a set of routes and creates a Redirects object from them.
*/
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,
Expand Down
Loading

0 comments on commit f21357b

Please sign in to comment.