diff --git a/packages/zip-it-and-ship-it/src/manifest.ts b/packages/zip-it-and-ship-it/src/manifest.ts index 41a4481235..d57a519bf5 100644 --- a/packages/zip-it-and-ship-it/src/manifest.ts +++ b/packages/zip-it-and-ship-it/src/manifest.ts @@ -18,6 +18,7 @@ interface ManifestFunction { name: string path: string priority?: number + region?: string routes?: ExtendedRoute[] runtime: string runtimeVersion?: string @@ -62,6 +63,7 @@ const formatFunctionForManifest = ({ path, priority, trafficRules, + region, routes, runtime, runtimeVersion, @@ -79,6 +81,7 @@ const formatFunctionForManifest = ({ mainFile, name, priority, + region, trafficRules, runtimeVersion, path: resolve(path), diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/in_source_config/index.ts b/packages/zip-it-and-ship-it/src/runtimes/node/in_source_config/index.ts index 484689785b..0bfff359bf 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/in_source_config/index.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/in_source_config/index.ts @@ -17,6 +17,7 @@ import { getImports } from '../parser/imports.js' import { safelyParseSource, safelyReadSource } from '../parser/index.js' import type { ModuleFormat } from '../utils/module_format.js' +import { REGIONS } from './properties/region.js' import { parse as parseSchedule } from './properties/schedule.js' export const IN_SOURCE_CONFIG_MODULE = '@netlify/functions' @@ -26,6 +27,7 @@ export interface StaticAnalysisResult { excludedRoutes?: Route[] inputModuleFormat?: ModuleFormat invocationMode?: InvocationMode + region?: string routes?: ExtendedRoute[] runtimeAPIVersion?: number } @@ -37,6 +39,11 @@ interface FindISCDeclarationsOptions { const httpMethod = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE', 'HEAD']) const httpMethods = z.preprocess((input) => (typeof input === 'string' ? input.toUpperCase() : input), httpMethod) const path = z.string().startsWith('/', { message: "Must start with a '/'" }) +const region = z.enum(REGIONS, { + errorMap: () => ({ + message: `Must be one of the supported regions (${REGIONS.join(', ')})`, + }), +}) export type HttpMethod = z.infer export type HttpMethods = z.infer @@ -70,6 +77,7 @@ export const inSourceConfig = functionConfig .optional(), preferStatic: z.boolean().optional().catch(undefined), rateLimit: rateLimit.optional().catch(undefined), + region: region.optional(), }) export type InSourceConfig = z.infer @@ -149,6 +157,10 @@ export const parseSource = (source: string, { functionName }: FindISCDeclaration methods: data.method ?? [], prefer_static: data.preferStatic || undefined, })) + + if (data.region) { + result.region = data.region + } } else { // TODO: Handle multiple errors. const [issue] = error.issues diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/in_source_config/properties/region.ts b/packages/zip-it-and-ship-it/src/runtimes/node/in_source_config/properties/region.ts new file mode 100644 index 0000000000..61cd75047a --- /dev/null +++ b/packages/zip-it-and-ship-it/src/runtimes/node/in_source_config/properties/region.ts @@ -0,0 +1,14 @@ +export const REGIONS = [ + 'ap-northeast-1', + 'ap-southeast-1', + 'ap-southeast-2', + 'ca-central-1', + 'eu-central-1', + 'eu-west-1', + 'eu-west-2', + 'sa-east-1', + 'us-east-1', + 'us-east-2', + 'us-west-1', + 'us-west-2', +] as const diff --git a/packages/zip-it-and-ship-it/src/utils/format_result.ts b/packages/zip-it-and-ship-it/src/utils/format_result.ts index 3e2cbb3564..cb52380737 100644 --- a/packages/zip-it-and-ship-it/src/utils/format_result.ts +++ b/packages/zip-it-and-ship-it/src/utils/format_result.ts @@ -8,6 +8,7 @@ export type FunctionResult = Omit & { bootstrapVersion?: string routes?: ExtendedRoute[] excludedRoutes?: Route[] + region?: string runtime: RuntimeName schedule?: string runtimeAPIVersion?: number @@ -20,6 +21,7 @@ export const formatZipResult = (archive: FunctionArchive) => { staticAnalysisResult: undefined, routes: archive.staticAnalysisResult?.routes, excludedRoutes: archive.staticAnalysisResult?.excludedRoutes, + region: archive.staticAnalysisResult?.region, runtime: archive.runtime.name, schedule: archive.staticAnalysisResult?.config?.schedule ?? archive?.config?.schedule, runtimeAPIVersion: archive.staticAnalysisResult?.runtimeAPIVersion, diff --git a/packages/zip-it-and-ship-it/tests/fixtures-esm/v2-region/function.js b/packages/zip-it-and-ship-it/tests/fixtures-esm/v2-region/function.js new file mode 100644 index 0000000000..75b7eab026 --- /dev/null +++ b/packages/zip-it-and-ship-it/tests/fixtures-esm/v2-region/function.js @@ -0,0 +1,10 @@ +export default async () => + new Response('

Hello world

', { + headers: { + 'content-type': 'text/html', + }, + }) + +export const config = { + region: 'eu-west-1', +} diff --git a/packages/zip-it-and-ship-it/tests/fixtures-esm/v2-region/package.json b/packages/zip-it-and-ship-it/tests/fixtures-esm/v2-region/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/packages/zip-it-and-ship-it/tests/fixtures-esm/v2-region/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/zip-it-and-ship-it/tests/unit/runtimes/node/in_source_config.test.ts b/packages/zip-it-and-ship-it/tests/unit/runtimes/node/in_source_config.test.ts index 57b6c683dc..5f27246000 100644 --- a/packages/zip-it-and-ship-it/tests/unit/runtimes/node/in_source_config.test.ts +++ b/packages/zip-it-and-ship-it/tests/unit/runtimes/node/in_source_config.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'vitest' import { parseSource } from '../../../../src/runtimes/node/in_source_config/index.js' +import { REGIONS } from '../../../../src/runtimes/node/in_source_config/properties/region.js' import { getLogger } from '../../../../src/utils/logger.js' describe('`schedule` helper', () => { @@ -838,4 +839,60 @@ describe('V2 API', () => { runtimeAPIVersion: 2, }) }) + + describe('`region` property', () => { + test('With no region', () => { + const source = ` + export default async () => new Response("Hello!") + export const config = {}` + + const isc = parseSource(source, options) + expect(isc).toEqual({ + config: {}, + excludedRoutes: [], + inputModuleFormat: 'esm', + routes: [], + runtimeAPIVersion: 2, + }) + }) + + test('With a supported region', () => { + const source = ` + export default async () => new Response("Hello!") + export const config = { region: 'eu-west-1' }` + + const isc = parseSource(source, options) + expect(isc).toEqual({ + config: { region: 'eu-west-1' }, + excludedRoutes: [], + inputModuleFormat: 'esm', + region: 'eu-west-1', + routes: [], + runtimeAPIVersion: 2, + }) + }) + + test('With an unsupported region', () => { + expect.assertions(4) + + const source = ` + export default async () => new Response("Hello!") + export const config = { region: 'mars-west-1' }` + + try { + parseSource(source, options) + } catch (error) { + const { customErrorInfo, message } = error + + expect(message).toBe( + `Function func1 has a configuration error on 'region': Must be one of the supported regions (${REGIONS.join( + ', ', + )})`, + ) + expect(customErrorInfo.type).toBe('functionsBundling') + expect(customErrorInfo.location.functionName).toBe('func1') + expect(customErrorInfo.location.runtime).toBe('js') + } + }) + }) }) diff --git a/packages/zip-it-and-ship-it/tests/v2api.test.ts b/packages/zip-it-and-ship-it/tests/v2api.test.ts index a79c6348ac..612847d242 100644 --- a/packages/zip-it-and-ship-it/tests/v2api.test.ts +++ b/packages/zip-it-and-ship-it/tests/v2api.test.ts @@ -784,4 +784,35 @@ describe.runIf(semver.gte(nodeVersion, '18.13.0'))('V2 functions API', () => { expect(manifest.functions[0].name).toBe('function') expect(manifest.functions[0].buildData).toEqual({ bootstrapVersion, runtimeAPIVersion: 2 }) }) + + test('Adds the selected region to the manifest file', async () => { + const bootstrapPath = getBootstrapPath() + const bootstrapPackageJson = await readFile(resolve(bootstrapPath, '..', '..', 'package.json'), 'utf8') + const { version: bootstrapVersion } = JSON.parse(bootstrapPackageJson) + + const { path: tmpDir } = await getTmpDir({ prefix: 'zip-it-test' }) + const manifestPath = join(tmpDir, 'manifest.json') + + const { files } = await zipFixture('v2-region', { + fixtureDir: FIXTURES_ESM_DIR, + opts: { + featureFlags: { + zisi_add_metadata_file: true, + }, + manifest: manifestPath, + }, + }) + + expect(files.length).toBe(1) + expect(files[0].name).toBe('function') + expect(files[0].bootstrapVersion).toBe(bootstrapVersion) + expect(files[0].runtimeAPIVersion).toBe(2) + + const manifestString = await readFile(manifestPath, { encoding: 'utf8' }) + const manifest = JSON.parse(manifestString) + + expect(manifest.functions.length).toBe(1) + expect(manifest.functions[0].name).toBe('function') + expect(manifest.functions[0].buildData).toEqual({ bootstrapVersion, runtimeAPIVersion: 2 }) + }) })