From f21357b69d94fe8d81f267efddb182d1a3cc678a Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 13 Jul 2023 09:21:33 +0100 Subject: [PATCH] feat(@astrojs/netlify): add `build.split` support (#7615) Co-authored-by: Sarah Rainsberger Co-authored-by: Bjorn Lu --- .changeset/happy-frogs-appear.md | 20 ++++++ .changeset/nasty-geckos-know.md | 5 ++ packages/integrations/cloudflare/src/index.ts | 8 ++- packages/integrations/netlify/README.md | 18 ++++++ packages/integrations/netlify/package.json | 5 +- .../netlify/src/integration-edge-functions.ts | 7 ++- .../netlify/src/integration-functions.ts | 39 ++++++++++-- .../netlify/src/integration-static.ts | 9 ++- packages/integrations/netlify/src/shared.ts | 11 +--- .../fixtures/prerender/src/pages/index.astro | 4 +- .../split-support/src/pages/blog.astro | 8 +++ .../split-support/src/pages/index.astro | 8 +++ .../netlify/test/functions/redirects.test.js | 1 + .../test/functions/redirects.test.js.snap | 8 +++ .../test/functions/split-support.test.js | 63 +++++++++++++++++++ .../netlify/test/functions/test-utils.js | 7 ++- packages/integrations/netlify/test/setup.js | 12 ++++ packages/underscore-redirects/src/astro.ts | 13 ++-- .../underscore-redirects/test/astro.test.js | 20 +++--- pnpm-lock.yaml | 3 + 20 files changed, 234 insertions(+), 35 deletions(-) create mode 100644 .changeset/happy-frogs-appear.md create mode 100644 .changeset/nasty-geckos-know.md create mode 100644 packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/blog.astro create mode 100644 packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/index.astro create mode 100644 packages/integrations/netlify/test/functions/redirects.test.js.snap create mode 100644 packages/integrations/netlify/test/functions/split-support.test.js create mode 100644 packages/integrations/netlify/test/setup.js diff --git a/.changeset/happy-frogs-appear.md b/.changeset/happy-frogs-appear.md new file mode 100644 index 0000000000..6bbc25f24a --- /dev/null +++ b/.changeset/happy-frogs-appear.md @@ -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, + }, + }); + ``` \ No newline at end of file diff --git a/.changeset/nasty-geckos-know.md b/.changeset/nasty-geckos-know.md new file mode 100644 index 0000000000..fcdfed8064 --- /dev/null +++ b/.changeset/nasty-geckos-know.md @@ -0,0 +1,5 @@ +--- +'@astrojs/underscore-redirects': minor +--- + +Refactor how the routes are passed. diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index 3dc237b727..fa064e0ac2 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -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()) { diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md index 86fff4fd23..5e78952176 100644 --- a/packages/integrations/netlify/README.md +++ b/packages/integrations/netlify/README.md @@ -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. diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index d6fe379cad..fdba25b25a 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -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 diff --git a/packages/integrations/netlify/src/integration-edge-functions.ts b/packages/integrations/netlify/src/integration-edge-functions.ts index 2f65bccda9..72721bd592 100644 --- a/packages/integrations/netlify/src/integration-edge-functions.ts +++ b/packages/integrations/netlify/src/integration-edge-functions.ts @@ -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); }, }, }; diff --git a/packages/integrations/netlify/src/integration-functions.ts b/packages/integrations/netlify/src/integration-functions.ts index 348b007f5d..5442079d09 100644 --- a/packages/integrations/netlify/src/integration-functions.ts +++ b/packages/integrations/netlify/src/integration-functions.ts @@ -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; + 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); + } }, }, }; diff --git a/packages/integrations/netlify/src/integration-static.ts b/packages/integrations/netlify/src/integration-static.ts index 78d0bb4b03..af28498674 100644 --- a/packages/integrations/netlify/src/integration-static.ts +++ b/packages/integrations/netlify/src/integration-static.ts @@ -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); }, }, }; diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index e4aabd8240..ca45dc752c 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -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, + 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(); diff --git a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro index ad5d44aa21..852d00b7b5 100644 --- a/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro +++ b/packages/integrations/netlify/test/functions/fixtures/prerender/src/pages/index.astro @@ -1,8 +1,8 @@ - Testing + Blog -

testing

+

Blog

diff --git a/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/blog.astro b/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/blog.astro new file mode 100644 index 0000000000..248c2218be --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/blog.astro @@ -0,0 +1,8 @@ + + + Testing + + +

testing

+ + diff --git a/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/index.astro new file mode 100644 index 0000000000..852d00b7b5 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/split-support/src/pages/index.astro @@ -0,0 +1,8 @@ + + + Blog + + +

Blog

+ + diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js index 1e20d41a09..566b88f4d7 100644 --- a/packages/integrations/netlify/test/functions/redirects.test.js +++ b/packages/integrations/netlify/test/functions/redirects.test.js @@ -46,5 +46,6 @@ describe('SSG - Redirects', () => { '/.netlify/functions/entry', '200', ]); + expect(redirects).to.matchSnapshot(); }); }); diff --git a/packages/integrations/netlify/test/functions/redirects.test.js.snap b/packages/integrations/netlify/test/functions/redirects.test.js.snap new file mode 100644 index 0000000000..322b4ee852 --- /dev/null +++ b/packages/integrations/netlify/test/functions/redirects.test.js.snap @@ -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" +`; diff --git a/packages/integrations/netlify/test/functions/split-support.test.js b/packages/integrations/netlify/test/functions/split-support.test.js new file mode 100644 index 0000000000..217b3c0d3f --- /dev/null +++ b/packages/integrations/netlify/test/functions/split-support.test.js @@ -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; + } + }); + }); +}); diff --git a/packages/integrations/netlify/test/functions/test-utils.js b/packages/integrations/netlify/test/functions/test-utils.js index 02b5d2ad90..eff6c2782d 100644 --- a/packages/integrations/netlify/test/functions/test-utils.js +++ b/packages/integrations/netlify/test/functions/test-utils.js @@ -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); + } + }, }, }; } diff --git a/packages/integrations/netlify/test/setup.js b/packages/integrations/netlify/test/setup.js new file mode 100644 index 0000000000..c53aa9894f --- /dev/null +++ b/packages/integrations/netlify/test/setup.js @@ -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); +}); diff --git a/packages/underscore-redirects/src/astro.ts b/packages/underscore-redirects/src/astro.ts index 1464cb4924..b378eb9559 100644 --- a/packages/underscore-redirects/src/astro.ts +++ b/packages/underscore-redirects/src/astro.ts @@ -13,9 +13,11 @@ function getRedirectStatus(route: RouteData): ValidRedirectStatus { interface CreateRedirectsFromAstroRoutesParams { config: Pick; - routes: RouteData[]; + /** + * Maps a `RouteData` to a dynamic target + */ + routeToDynamicTargetMap: Map; 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, diff --git a/packages/underscore-redirects/test/astro.test.js b/packages/underscore-redirects/test/astro.test.js index 4ddd5c8e1c..6b6bbbd8e0 100644 --- a/packages/underscore-redirects/test/astro.test.js +++ b/packages/underscore-redirects/test/astro.test.js @@ -8,16 +8,22 @@ describe('Astro', () => { }; it('Creates a Redirects object from routes', () => { - const routes = [ - { pathname: '/', distURL: new URL('./index.html', import.meta.url), segments: [] }, - { pathname: '/one', distURL: new URL('./one/index.html', import.meta.url), segments: [] }, - ]; - const dynamicTarget = './.adapter/dist/entry.mjs'; + 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: [] }, + './.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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9cbdd43da4..0b1e651b46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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