summaryrefslogtreecommitdiff
path: root/packages/integrations/netlify/src
diff options
context:
space:
mode:
authorGravatar Matt Kane <matt.kane@netlify.com> 2024-03-19 14:36:30 +0000
committerGravatar GitHub <noreply@github.com> 2024-03-19 14:36:30 +0000
commitd81596d04c5d9e09014eacde111425a82a027491 (patch)
treec89eb9a921220b4b5fa638537e59865e5654fe69 /packages/integrations/netlify/src
parent47482906310282e1c00c2379fd62e6267c98855e (diff)
downloadastro-d81596d04c5d9e09014eacde111425a82a027491.tar.gz
astro-d81596d04c5d9e09014eacde111425a82a027491.tar.zst
astro-d81596d04c5d9e09014eacde111425a82a027491.zip
feat(netlify): add support for image.remotePatterns and images.domains with Netlify Image CDN (#187)
* feat(netlify): support for image.remotePatterns and images.domains * chore: add sample remote image * chore: lint * chore: add changeset * chore: better comments * chore: format * fix: handle dots in domains * Update .changeset/nine-lamps-cry.md Co-authored-by: Alexander Niebuhr <alexander@nbhr.io> * fix: warn if invalid regex is generated --------- Co-authored-by: Alexander Niebuhr <alexander@nbhr.io>
Diffstat (limited to 'packages/integrations/netlify/src')
-rw-r--r--packages/integrations/netlify/src/index.ts113
1 files changed, 105 insertions, 8 deletions
diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts
index 765f40dd9..598e0d2c1 100644
--- a/packages/integrations/netlify/src/index.ts
+++ b/packages/integrations/netlify/src/index.ts
@@ -4,7 +4,7 @@ import type { IncomingMessage } from 'node:http';
import { fileURLToPath } from 'node:url';
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects';
import type { Context } from '@netlify/functions';
-import type { AstroConfig, AstroIntegration, RouteData } from 'astro';
+import type { AstroConfig, AstroIntegration, AstroIntegrationLogger, RouteData } from 'astro';
import { AstroError } from 'astro/errors';
import { build } from 'esbuild';
import type { Args } from './ssr-function.js';
@@ -24,6 +24,106 @@ const isStaticRedirect = (route: RouteData) =>
const clearDirectory = (dir: URL) => rm(dir, { recursive: true }).catch(() => {});
+type RemotePattern = AstroConfig['image']['remotePatterns'][number];
+
+/**
+ * Convert a remote pattern object to a regex string
+ */
+export function remotePatternToRegex(
+ pattern: RemotePattern,
+ logger: AstroIntegrationLogger
+): string | undefined {
+ let { protocol, hostname, port, pathname } = pattern;
+
+ let regexStr = '';
+
+ if (protocol) {
+ regexStr += `${protocol}://`;
+ } else {
+ // Default to matching any protocol
+ regexStr += '[a-z]+://';
+ }
+
+ if (hostname) {
+ if (hostname.startsWith('**.')) {
+ // match any number of subdomains
+ regexStr += '([a-z0-9]+\\.)*';
+ hostname = hostname.substring(3);
+ } else if (hostname.startsWith('*.')) {
+ // match one subdomain
+ regexStr += '([a-z0-9]+\\.)?';
+ hostname = hostname.substring(2); // Remove '*.' from the beginning
+ }
+ // Escape dots in the hostname
+ regexStr += hostname.replace(/\./g, '\\.');
+ } else {
+ regexStr += '[a-z0-9.-]+';
+ }
+
+ if (port) {
+ regexStr += `:${port}`;
+ } else {
+ // Default to matching any port
+ regexStr += '(:[0-9]+)?';
+ }
+
+ if (pathname) {
+ if (pathname.endsWith('/**')) {
+ // Match any path.
+ regexStr += `(\\${pathname.replace('/**', '')}.*)`;
+ }
+ if (pathname.endsWith('/*')) {
+ // Match one level of path
+ regexStr += `(\\${pathname.replace('/*', '')}\/[^/?#]+)\/?`;
+ } else {
+ // Exact match
+ regexStr += `(\\${pathname})`;
+ }
+ } else {
+ // Default to matching any path
+ regexStr += '(\\/[^?#]*)?';
+ }
+ if (!regexStr.endsWith('.*)')) {
+ // Match query, but only if it's not already matched by the pathname
+ regexStr += '([?][^#]*)?';
+ }
+ try {
+ new RegExp(regexStr);
+ } catch (e) {
+ logger.warn(
+ `Could not generate a valid regex from the remotePattern "${JSON.stringify(
+ pattern
+ )}". Please check the syntax.`
+ );
+ return undefined;
+ }
+ return regexStr;
+}
+
+async function writeNetlifyDeployConfig(config: AstroConfig, logger: AstroIntegrationLogger) {
+ const remoteImages: Array<string> = [];
+ // Domains get a simple regex match
+ remoteImages.push(
+ ...config.image.domains.map((domain) => `https?:\/\/${domain.replaceAll('.', '\\.')}\/.*`)
+ );
+ // Remote patterns need to be converted to regexes
+ remoteImages.push(
+ ...config.image.remotePatterns
+ .map((pattern) => remotePatternToRegex(pattern, logger))
+ .filter(Boolean as unknown as (pattern?: string) => pattern is string)
+ );
+
+ // See https://docs.netlify.com/image-cdn/create-integration/
+ const deployConfigDir = new URL('.netlify/deploy/v1/', config.root);
+ await mkdir(deployConfigDir, { recursive: true });
+ await writeFile(
+ new URL('./config.json', deployConfigDir),
+ JSON.stringify({
+ images: { remote_images: remoteImages },
+ })
+ );
+}
+
export interface NetlifyIntegrationConfig {
/**
* If enabled, On-Demand-Rendered pages are cached for up to a year.
@@ -127,7 +227,7 @@ export default function netlifyIntegration(
await mkdir(middlewareOutputDir(), { recursive: true });
await writeFile(
new URL('./entry.mjs', middlewareOutputDir()),
- `
+ /* ts */ `
import { onRequest } from "${fileURLToPath(entrypoint).replaceAll('\\', '/')}";
import { createContext, trySerializeLocals } from 'astro/middleware';
@@ -266,15 +366,12 @@ export default function netlifyIntegration(
},
});
},
- 'astro:config:done': ({ config, setAdapter }) => {
+ 'astro:config:done': async ({ config, setAdapter, logger }) => {
rootDir = config.root;
_config = config;
- if (config.image.domains.length || config.image.remotePatterns.length) {
- throw new AstroError(
- "config.image.domains and config.image.remotePatterns aren't supported by the Netlify adapter.",
- 'See https://github.com/withastro/adapters/tree/main/packages/netlify#image-cdn for more.'
- );
+ if (config.image?.domains?.length || config.image?.remotePatterns?.length) {
+ await writeNetlifyDeployConfig(config, logger);
}
const edgeMiddleware = integrationConfig?.edgeMiddleware ?? false;