diff options
| author | 2024-03-19 14:36:30 +0000 | |
|---|---|---|
| committer | 2024-03-19 14:36:30 +0000 | |
| commit | d81596d04c5d9e09014eacde111425a82a027491 (patch) | |
| tree | c89eb9a921220b4b5fa638537e59865e5654fe69 /packages/integrations/netlify/src | |
| parent | 47482906310282e1c00c2379fd62e6267c98855e (diff) | |
| download | astro-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.ts | 113 |
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; |
