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:
parent
b30a1bc2b8
commit
f21357b69d
20 changed files with 234 additions and 35 deletions
20
.changeset/happy-frogs-appear.md
Normal file
20
.changeset/happy-frogs-appear.md
Normal 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,
|
||||
},
|
||||
});
|
||||
```
|
5
.changeset/nasty-geckos-know.md
Normal file
5
.changeset/nasty-geckos-know.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/underscore-redirects': minor
|
||||
---
|
||||
|
||||
Refactor how the routes are passed.
|
|
@ -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()) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
<title>Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>testing</h1>
|
||||
<h1>Blog</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>testing</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Blog</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -46,5 +46,6 @@ describe('SSG - Redirects', () => {
|
|||
'/.netlify/functions/entry',
|
||||
'200',
|
||||
]);
|
||||
expect(redirects).to.matchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
`;
|
|
@ -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,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);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
12
packages/integrations/netlify/test/setup.js
Normal file
12
packages/integrations/netlify/test/setup.js
Normal 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);
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue