diff --git a/src/app-engine.js b/src/app-engine.js index 132bea1d..5196f935 100644 --- a/src/app-engine.js +++ b/src/app-engine.js @@ -1,54 +1,142 @@ import { env } from 'node:process' +/** + * Generate `allowedHosts` config for `AngularAppEngine` from `@angular/ssr` + * @returns {string[]} + */ export function getAllowedHosts() { - const allowedHosts = [] + const defaultAllowedHosts = [] + + let deployId + let deployPrimeUrlHostname + let siteId + let siteName + const environmentVariables = ['DEPLOY_ID', 'DEPLOY_PRIME_URL', 'DEPLOY_URL', 'SITE_ID', 'SITE_NAME', 'URL'] for (const environmentVariable of environmentVariables) { - if (!env[environmentVariable] || env[environmentVariable] === 'undefined') { - console.warn( - `Missing Netlify-specific environment variable ${environmentVariable}. \`allowedHosts\` config might be incomplete.`, - ) - return allowedHosts + switch (environmentVariable) { + case 'DEPLOY_ID': + // not setting this directly above so that validation + // and warnings remain centralized in the helper + deployId = getEnvironmentVariable(environmentVariable) + break + case 'DEPLOY_PRIME_URL': + // DEPLOY_PRIME_URL: + // --.netlify.app or + // --.netlify.app (supports ADS) + deployPrimeUrlHostname = getHostnameFromEnvironmentVariable(environmentVariable) + if (deployPrimeUrlHostname) { + defaultAllowedHosts.push(deployPrimeUrlHostname) + } + break + case 'DEPLOY_URL': + case 'URL': { + // DEPLOY_URL: --.netlify.app + // URL: .netlify.app OR + // www handling is not required as Netlify auto-redirects + const hostname = getHostnameFromEnvironmentVariable(environmentVariable) + if (hostname) { + defaultAllowedHosts.push(hostname) + } + break + } + case 'SITE_ID': + // SITE_ID: + // this makes it .netlify.app + // separate handling because we need it later + siteId = getEnvironmentVariable(environmentVariable) + if (siteId) { + defaultAllowedHosts.push(`${siteId}.netlify.app`) + } + break + case 'SITE_NAME': + // SITE_NAME: + // this makes it .netlify.app + // this may duplicate URL for sites without a custom domain + // but duplicates are removed later and this is still useful + // if a custom domain is removed after deployment + siteName = getEnvironmentVariable(environmentVariable) + if (siteName) { + defaultAllowedHosts.push(`${siteName}.netlify.app`) + } + break + default: + break } } - const deployPrimeUrlHostname = new URL(env.DEPLOY_PRIME_URL).hostname - - // .netlify.app OR - // www handling is not required as Netlify will auto-redirect - allowedHosts.push(new URL(env.URL).hostname) - // --.netlify.app - allowedHosts.push(new URL(env.DEPLOY_URL).hostname) - // --.netlify.app or --.netlify.app (supports ADS) - allowedHosts.push(deployPrimeUrlHostname) - // .netlify.app - // this will be duplicated for sites without custom domain - // but it's important to have in case a site's custom domain is removed after a deploy - allowedHosts.push(`${env.SITE_NAME}.netlify.app`) - // .netlify.app - allowedHosts.push(`${env.SITE_ID}.netlify.app`) - // --.netlify.app - allowedHosts.push(`${env.DEPLOY_ID}--${env.SITE_ID}.netlify.app`) - - // we need to extract the branch name or the deploy preview number - // so we can add the subdomain as well as site-id specific URLs - // this would be required for sites using ADS so that - // we can add netlify.app URLs as well - if (deployPrimeUrlHostname.includes('--')) { + // extract the branch name or deploy preview number + // so we can generate ADS-compatible URLs + if (deployPrimeUrlHostname?.includes('--') && siteId && siteName) { const [branchNameOrDpNumber] = deployPrimeUrlHostname.split('--') - allowedHosts.push(`${branchNameOrDpNumber}--${env.SITE_NAME}.netlify.app`) - allowedHosts.push(`${branchNameOrDpNumber}--${env.SITE_ID}.netlify.app`) + defaultAllowedHosts.push(`${branchNameOrDpNumber}--${siteName}.netlify.app`) + defaultAllowedHosts.push(`${branchNameOrDpNumber}--${siteId}.netlify.app`) + } + + if (deployId && siteId) { + // --.netlify.app + defaultAllowedHosts.push(`${deployId}--${siteId}.netlify.app`) } - return allowedHosts + return [...new Set(defaultAllowedHosts)] } +/** + * Return Netlify-specific context + * @returns {import('@netlify/edge-functions').Context | undefined} + */ export function getContext() { // eslint-disable-next-line no-undef return typeof Netlify !== 'undefined' ? Netlify?.context : undefined } +/** + * Return the value of an environment variable + * @param {string} environmentVariable + * + * @returns {string | undefined} + */ +function getEnvironmentVariable(environmentVariable) { + const value = env[environmentVariable] + + // we inject the literal string "undefined" for variables that don't have a value + if (value == null || value === '' || value === 'undefined') { + console.warn( + `Missing Netlify-specific environment variable ${environmentVariable}. ` + + '`allowedHosts` config might be incomplete.', + ) + + return + } + + return value +} + +/** + * Return hostname from the value of an environment variable + * @param {string} environmentVariable + * + * @returns {string | undefined} + */ +function getHostnameFromEnvironmentVariable(environmentVariable) { + const value = getEnvironmentVariable(environmentVariable) + + if (value == null) { + return + } + + try { + return new URL(value).hostname + } catch { + console.warn(`Netlify-specific environment variable ${environmentVariable} does not contain a valid URL`) + } +} + +/** + * Generate `trustProxyHeaders` config for `AngularAppEngine` from `@angular/ssr` + * @returns {string[]} + */ export function getTrustProxyHeaders() { return ['x-forwarded-for'] }