import fs, { existsSync } from 'fs' import { relative } from 'pathe' // I have no idea what eslint is up to here but it gives an error // eslint-disable-next-line no-shadow export const enum ApiRouteType { SCHEDULED = 'experimental-scheduled', BACKGROUND = 'experimental-background', } export interface ApiStandardConfig { type?: never runtime?: 'nodejs' | 'experimental-edge' schedule?: never } export interface ApiScheduledConfig { type: ApiRouteType.SCHEDULED runtime?: 'nodejs' schedule: string } export interface ApiBackgroundConfig { type: ApiRouteType.BACKGROUND runtime?: 'nodejs' schedule?: never } export type ApiConfig = ApiStandardConfig | ApiScheduledConfig | ApiBackgroundConfig export const validateConfigValue = (config: ApiConfig, apiFilePath: string): config is ApiConfig => { if (config.type === ApiRouteType.SCHEDULED) { if (!config.schedule) { console.error( `Invalid config value in ${relative(process.cwd(), apiFilePath)}: schedule is required when type is "${ ApiRouteType.SCHEDULED }"`, ) return false } if ((config as ApiConfig).runtime === 'experimental-edge') { console.error( `Invalid config value in ${relative( process.cwd(), apiFilePath, )}: edge runtime is not supported for scheduled functions`, ) return false } return true } if (!config.type || config.type === ApiRouteType.BACKGROUND) { if (config.schedule) { console.error( `Invalid config value in ${relative(process.cwd(), apiFilePath)}: schedule is not allowed unless type is "${ ApiRouteType.SCHEDULED }"`, ) return false } if (config.type && (config as ApiConfig).runtime === 'experimental-edge') { console.error( `Invalid config value in ${relative( process.cwd(), apiFilePath, )}: edge runtime is not supported for background functions`, ) return false } return true } console.error( `Invalid config value in ${relative(process.cwd(), apiFilePath)}: type ${ (config as ApiConfig).type } is not supported`, ) return false } let extractConstValue let parseModule let hasWarnedAboutNextVersion = false /** * Uses Next's swc static analysis to extract the config values from a file. */ export const extractConfigFromFile = async (apiFilePath: string): Promise => { if (!apiFilePath || !existsSync(apiFilePath)) { return {} } try { if (!extractConstValue) { extractConstValue = require('next/dist/build/analysis/extract-const-value') } if (!parseModule) { // eslint-disable-next-line prefer-destructuring, @typescript-eslint/no-var-requires parseModule = require('next/dist/build/analysis/parse-module').parseModule } } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { if (!hasWarnedAboutNextVersion) { console.log("This version of Next.js doesn't support advanced API routes. Skipping...") hasWarnedAboutNextVersion = true } // Old Next.js version return {} } throw error } const { extractExportedConstValue, UnsupportedValueError } = extractConstValue const fileContent = await fs.promises.readFile(apiFilePath, 'utf8') // No need to parse if there's no "config" if (!fileContent.includes('config')) { return {} } const ast = await parseModule(apiFilePath, fileContent) let config: ApiConfig try { config = extractExportedConstValue(ast, 'config') } catch (error) { if (UnsupportedValueError && error instanceof UnsupportedValueError) { console.warn(`Unsupported config value in ${relative(process.cwd(), apiFilePath)}`) } return {} } if (validateConfigValue(config, apiFilePath)) { return config } throw new Error(`Unsupported config value in ${relative(process.cwd(), apiFilePath)}`) }