Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Update netlify adapter to integrate includeFiles and excludeFiles options #515

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/beige-insects-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astrojs/netlify': minor
---

Adds `includedFiles` and `excludedFiles` configuration options to customize SSR function bundle contents.

See the [documentation](https://docs.astro.build/en/guides/integrations-guide/netlify/) for detailed usage instructions and examples.
53 changes: 50 additions & 3 deletions packages/netlify/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { randomUUID } from 'node:crypto';
import { appendFile, mkdir, readFile, 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';
Expand All @@ -13,6 +13,7 @@ import type {
IntegrationResolvedRoute,
} from 'astro';
import { build } from 'esbuild';
import glob from 'fast-glob';
import { copyDependenciesToFunction } from './lib/nft.js';
import type { Args } from './ssr-function.js';

Expand Down Expand Up @@ -139,6 +140,12 @@ async function writeNetlifyFrameworkConfig(config: AstroConfig, logger: AstroInt
}

export interface NetlifyIntegrationConfig {
/** Force files to be bundled with your function. This is helpful when you notice missing files. */
includeFiles?: string[];

/** Exclude any files from the bundling process that would otherwise be included. */
excludeFiles?: string[];

/**
* If enabled, On-Demand-Rendered pages are cached for up to a year.
* This is useful for pages that are not updated often, like a blog post,
Expand Down Expand Up @@ -191,6 +198,8 @@ export default function netlifyIntegration(
let outDir: URL;
let rootDir: URL;
let astroMiddlewareEntryPoint: URL | undefined = undefined;
// Extra files to be merged with `includeFiles` during build
const extraFilesToInclude: URL[] = [];
// Secret used to verify that the caller is the astro-generated edge middleware and not a third-party
const middlewareSecret = randomUUID();

Expand Down Expand Up @@ -246,6 +255,18 @@ 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) => new URL(file, _config.root));
}

async function writeSSRFunction({
notFoundContent,
logger,
Expand All @@ -257,12 +278,38 @@ export default function netlifyIntegration(
}) {
const entry = new URL('./entry.mjs', ssrBuildDir());

const _includeFiles = integrationConfig?.includeFiles || [];
const _excludeFiles = integrationConfig?.excludeFiles || [];

if (finalBuildOutput === 'server') {
// Merge any includes from `vite.assetsInclude
if (_config.vite.assetsInclude) {
const mergeGlobbedIncludes = (globPattern: unknown) => {
if (typeof globPattern === 'string') {
const entries = glob.sync(globPattern).map((p) => pathToFileURL(p));
extraFilesToInclude.push(...entries);
} else if (Array.isArray(globPattern)) {
for (const pattern of globPattern) {
mergeGlobbedIncludes(pattern);
}
}
};

mergeGlobbedIncludes(_config.vite.assetsInclude);
}
}

const includeFiles = (await getFilesByGlob(_includeFiles, _excludeFiles)).concat(
extraFilesToInclude
);
const excludeFiles = await getFilesByGlob(_excludeFiles);

const { handler } = await copyDependenciesToFunction(
{
entry,
outDir: ssrOutputDir(),
includeFiles: [],
excludeFiles: [],
includeFiles: includeFiles,
excludeFiles: excludeFiles,
logger,
root,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import netlify from '@astrojs/netlify';
import { defineConfig } from 'astro/config';

export default defineConfig({
output: 'server',
adapter: netlify(),
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 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
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
14 changes: 14 additions & 0 deletions packages/netlify/test/functions/fixtures/includes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@test/netlify-includes",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/netlify": "workspace:",
"astro": "^4.15.1",
"cowsay": "1.6.0"
},
"scripts": {
"build": "astro build",
"dev": "astro dev"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <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,27 @@
---
import { promises as fs } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const loadFile = Astro.url.searchParams.get('file');

const file = await fs.readFile(join(__dirname, `../../../files/${loadFile}`), 'utf-8');

async function moo() {
const cow = await import('cowsay');
return cow.say({ text: 'Moo!' });
}

if (Astro.url.searchParams.get('moo')) {
await moo();
}
---
<html>
<head><title>Testing</title></head>
<body>
{loadFile && <h1>{file}</h1>}
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
'works'
184 changes: 184 additions & 0 deletions packages/netlify/test/functions/include-files.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import * as assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import { after, before, describe, it } from 'node:test';
import netlify from '@astrojs/netlify';
import { loadFixture } from '@astrojs/test-utils';
import * as cheerio from 'cheerio';
import glob from 'fast-glob';

describe(
'Included vite assets files',
() => {
let fixture;

const root = new URL('./fixtures/includes/', import.meta.url);
const expectedCwd = new URL('.netlify/v1/functions/ssr/packages/netlify/', root);

const expectedAssetsInclude = ['./*.json'];
const excludedAssets = ['./files/exclude-asset.json'];

before(async () => {
fixture = await loadFixture({
root,
vite: {
assetsInclude: expectedAssetsInclude,
},
adapter: netlify({
excludeFiles: excludedAssets,
}),
});
await fixture.build();
});

it('Emits vite assets files', async () => {
for (const pattern of expectedAssetsInclude) {
const files = glob.sync(pattern);
for (const file of files) {
assert.ok(
existsSync(new URL(file, expectedCwd)),
`Expected file ${pattern} to exist in build`
);
}
}
});

it('Does not include vite assets files when excluded', async () => {
for (const file of excludedAssets) {
assert.ok(
!existsSync(new URL(file, expectedCwd)),
`Expected file ${file} to not exist in build`
);
}
});

after(async () => {
await fixture.clean();
});
},
{
timeout: 120000,
}
);

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
);

const expectedFiles = [
'./files/include-this.txt',
'./files/also-this.csv',
'./files/subdirectory/and-this.csv',
];

before(async () => {
fixture = await loadFixture({
root,
adapter: netlify({
includeFiles: expectedFiles,
}),
});
await fixture.build();
});

it('Emits include files', async () => {
for (const file of expectedFiles) {
assert.ok(existsSync(new URL(file, expectedCwd)), `Expected file ${file} to 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/?file=include-this.txt'), {});
const html = await resp.text();
const $ = cheerio.load(html);
assert.equal($('h1').text(), 'hello');
});

it('Includes traced node modules with symlinks', async () => {
const expected = new URL(
'.netlify/v1/functions/ssr/node_modules/.pnpm/[email protected]/node_modules/cowsay/cows/happy-whale.cow',
root
);
assert.ok(existsSync(expected, 'Expected excluded file to exist in default build'));
});

after(async () => {
await fixture.clean();
});
},
{
timeout: 120000,
}
);

describe(
'Excluded 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
);

const includeFiles = ['./files/**/*.txt'];
const excludedTxt = ['./files/subdirectory/not-this.txt', './files/subdirectory/or-this.txt'];
const excludeFiles = [...excludedTxt, '../../../../../../node_modules/.pnpm/cowsay@*/**'];

before(async () => {
fixture = await loadFixture({
root,
adapter: netlify({
includeFiles: includeFiles,
excludeFiles: excludeFiles,
}),
});
await fixture.build();
});

it('Excludes traced node modules', async () => {
const expected = new URL(
'.netlify/v1/functions/ssr/node_modules/.pnpm/[email protected]/node_modules/cowsay/cows/happy-whale.cow',
root
);
assert.ok(!existsSync(expected), 'Expected excluded file to not exist in build');
});

it('Does not include files when excluded', async () => {
for (const pattern of includeFiles) {
const files = glob.sync(pattern, { ignore: excludedTxt });
for (const file of files) {
assert.ok(
existsSync(new URL(file, expectedCwd)),
`Expected file ${pattern} to exist in build`
);
}
}
for (const file of excludedTxt) {
assert.ok(
!existsSync(new URL(file, expectedCwd)),
`Expected file ${file} to not exist in build`
);
}
});

after(async () => {
await fixture.clean();
});
},
{
timeout: 120000,
}
);