Skip to content

Commit

Permalink
feat(netlify): support included/excluded files
Browse files Browse the repository at this point in the history
  • Loading branch information
ascorbic committed Jul 11, 2024
1 parent eef25b9 commit a2037a4
Show file tree
Hide file tree
Showing 16 changed files with 228 additions and 521 deletions.
21 changes: 21 additions & 0 deletions .changeset/itchy-donuts-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@astrojs/netlify': minor
---

Adds "includedFiles" and "excludedFiles" options. These allow extra files to be deployed in the SSR function bundle.

When an Astro site using `server` or `hybrid` rendering is deployed to Netlify, the generated functions trace the server dependencies and include any that may be needed in SSR. However sometimes you may want to include extra files that are not detected as dependencies, such as files that are loaded using `fs` functions. Also, you may sometimes want to specifically exclude dependencies that are bundled automatically. For example, you may have a Node module that includes a large binary. The `"includedFiles"` and `"excludedFiles"` options allow you to do these. Both options support glob patterns, so you can include/exclude multiple files at once.

The paths are relative to the site root. If you are loading them using filesystem funcitons, make sure you resolve paths relative to the site root, and not relative to the cxource file. At runtime the compiled file will be in a different location, so paths that are relative to the file will not work. You should instead resolve the path using `path.resolve()` or `process.cwd()`, which will give you the site root.

```js
import netlify from '@astrojs/netlify';
import { defineConfig } from 'astro/config';

export default defineConfig({
output: 'server',
adapter: netlify({
includedFiles: ['files/**/*.csv', 'files/include-this.txt'],
excludedFiles: ['files/subdirectory/not-this.csv'],
})
});
3 changes: 2 additions & 1 deletion packages/netlify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"files": ["dist"],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test-fn": "astro-scripts test \"test/functions/*.test.js\"",
"test-static": "astro-scripts test \"test/static/*.test.js\"",
"test": "pnpm run test-fn && pnpm run test-static",
Expand All @@ -45,7 +46,7 @@
"@netlify/edge-functions": "^2.8.1",
"@netlify/edge-handler-types": "^0.34.1",
"@types/node": "^20.14.10",
"astro": "^4.11.3",
"astro": "^4.11.5",
"astro-scripts": "workspace:*",
"cheerio": "1.0.0-rc.12",
"execa": "^8.0.1",
Expand Down
39 changes: 35 additions & 4 deletions packages/netlify/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { randomUUID } from 'node:crypto';
import { appendFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import type { IncomingMessage } from 'node:http';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { emptyDir } from '@astrojs/internal-helpers/fs';
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
import type { Context } from '@netlify/functions';
import type { AstroConfig, AstroIntegration, AstroIntegrationLogger, RouteData } from 'astro';
import { build } from 'esbuild';
import glob from 'fast-glob';
import { copyDependenciesToFunction } from './lib/nft.js';
import type { Args } from './ssr-function.js';

const { version: packageVersion } = JSON.parse(
await readFile(new URL('../package.json', import.meta.url), 'utf8')
);
Expand Down Expand Up @@ -173,6 +173,18 @@ export interface NetlifyIntegrationConfig {
* @default {true}
*/
imageCDN?: boolean;

/**
* Extra files to include in the SSR function bundle. Paths are relative to the project root.
* Glob patterns are supported.
*/
includedFiles?: string[];

/**
* Files to exclude from the SSR function bundle. Paths are relative to the project root.
* Glob patterns are supported.
*/
excludedFiles?: string[];
}

export default function netlifyIntegration(
Expand Down Expand Up @@ -225,18 +237,37 @@ export default function netlifyIntegration(
}
}

async function getFilesByGlob(
include: Array<string> = [],
exclude: Array<string> = []
): Promise<Array<URL>> {
const files = await glob(include, {
cwd: fileURLToPath(rootDir),
absolute: true,
ignore: exclude,
});
return files.map((file) => pathToFileURL(file));
}

async function writeSSRFunction({
notFoundContent,
logger,
}: { notFoundContent?: string; logger: AstroIntegrationLogger }) {
const entry = new URL('./entry.mjs', ssrBuildDir());

const includedFiles = await getFilesByGlob(
integrationConfig?.includedFiles,
integrationConfig?.excludedFiles
);

const excludedFiles = await getFilesByGlob(integrationConfig?.excludedFiles);

const { handler } = await copyDependenciesToFunction(
{
entry,
outDir: ssrOutputDir(),
includeFiles: [],
excludeFiles: [],
includeFiles: includedFiles,
excludeFiles: excludedFiles,
logger,
},
TRACE_CACHE
Expand Down
7 changes: 7 additions & 0 deletions packages/netlify/src/ssr-function.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fileURLToPath } from 'url';
import type { Context } from '@netlify/functions';
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
Expand All @@ -24,7 +25,13 @@ export const createExports = (manifest: SSRManifest, { middlewareSecret }: Args)
cacheOnDemandPages: boolean;
notFoundContent?: string;
}) {
// The entrypoint is created in `.netlify/v1/build` so we need to go up two levels to get to the root
const root = fileURLToPath(new URL('../../', import.meta.url));
return async function handler(request: Request, context: Context) {
// This entrypoint will be deep inside the directory structure below cwd
// We chdir to the root so that we can resolve the correct paths at runtime
process.chdir(root);

const routeData = app.match(request);
if (!routeData && typeof integrationConfig.notFoundContent !== 'undefined') {
return new Response(integrationConfig.notFoundContent, {
Expand Down
11 changes: 11 additions & 0 deletions packages/netlify/test/functions/fixtures/includes/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import netlify from '@astrojs/netlify';
import { defineConfig } from 'astro/config';

export default defineConfig({
output: 'server',
adapter: netlify({
includedFiles: ['files/**/*.csv', 'files/include-this.txt'],
excludedFiles: ['files/subdirectory/not-this.csv'],
}),
site: `http://example.com`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1,2,3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1,2,3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1,2,3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello
13 changes: 13 additions & 0 deletions packages/netlify/test/functions/fixtures/includes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@test/netlify-includes",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/netlify": "workspace:",
"astro": "^4.11.5"
},
"scripts": {
"build": "astro build",
"dev": "astro dev"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="astro/client" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
export const prerender = false
const header = Astro.request.headers.get("x-test")
---

<p>This is my custom 404 page</p>
<p>x-test: {header}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import { promises as fs } from 'fs';
const cwd = process.cwd();
const file = await fs.readFile('files/include-this.txt', 'utf-8');
---
<html>
<head><title>Testing</title></head>
<body>
<h1>{file}</h1>
<p>{cwd}</p>
</body>
</html>
54 changes: 54 additions & 0 deletions packages/netlify/test/functions/includes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import { before, describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
import { loadFixture } from '@astrojs/test-utils';
import * as cheerio from 'cheerio';

describe(
'Included files',
() => {
let fixture;
const root = new URL('./fixtures/includes/', import.meta.url);
const expectedCwd = new URL(
'.netlify/v1/functions/ssr/packages/netlify/test/functions/fixtures/includes/',
root
);

before(async () => {
fixture = await loadFixture({ root });
await fixture.build();
});

it('Includes files', async () => {
const filesRoot = new URL('./files/', expectedCwd);
const expectedFiles = ['include-this.txt', 'also-this.csv', 'subdirectory/and-this.csv'];

for (const file of expectedFiles) {
assert.ok(existsSync(new URL(file, filesRoot)), `Expected file ${file} to exist`);
}

const notExpectedFiles = ['subdirectory/not-this.csv', 'subdirectory/or-this.txt'];

for (const file of notExpectedFiles) {
assert.ok(!existsSync(new URL(file, filesRoot)), `Expected file ${file} to not exist`);
}
});

it('Can load included files correctly', async () => {
const entryURL = new URL(
'./fixtures/includes/.netlify/v1/functions/ssr/ssr.mjs',
import.meta.url
);
const { default: handler } = await import(entryURL);
const resp = await handler(new Request('http://example.com/'), {});
const html = await resp.text();
const $ = cheerio.load(html);
assert.equal($('h1').text(), 'hello');
assert.equal($('p').text(), fileURLToPath(expectedCwd).slice(0, -1));
});
},
{
timeout: 120000,
}
);
Loading

0 comments on commit a2037a4

Please sign in to comment.