diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 4da26852ef..d97d614fb8 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -18,7 +18,6 @@ import { Rewrites, RoutesManifest } from './types' import { findModuleFromBase } from './utils' const TEST_ROUTE = /(|\/)\[[^/]+?](\/|\.html|$)/ -const SOURCE_FILE_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx'] export const isDynamicRoute = (route) => TEST_ROUTE.test(route) @@ -341,12 +340,16 @@ const getServerFile = (root: string, includeBase = true) => { return findModuleFromBase({ candidates, paths: [root] }) } +// Next.js already defines a default `pageExtensions` array in its `required-server-files.json` file +// In case it gets `undefined`, this is a fallback +const SOURCE_FILE_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx'] + /** * Find the source file for a given page route */ -export const getSourceFileForPage = (page: string, roots: string[]) => { +export const getSourceFileForPage = (page: string, roots: string[], pageExtensions = SOURCE_FILE_EXTENSIONS) => { for (const root of roots) { - for (const extension of SOURCE_FILE_EXTENSIONS) { + for (const extension of pageExtensions) { const file = join(root, `${page}.${extension}`) if (existsSync(file)) { return file diff --git a/packages/runtime/src/helpers/functions.ts b/packages/runtime/src/helpers/functions.ts index 5d0f8032b7..aa2423922e 100644 --- a/packages/runtime/src/helpers/functions.ts +++ b/packages/runtime/src/helpers/functions.ts @@ -199,7 +199,11 @@ export const setupImageFunction = async ({ /** * Look for API routes, and extract the config from the source file. */ -export const getApiRouteConfigs = async (publish: string, baseDir: string): Promise> => { +export const getApiRouteConfigs = async ( + publish: string, + baseDir: string, + pageExtensions: string[], +): Promise> => { const pages = await readJSON(join(publish, 'server', 'pages-manifest.json')) const apiRoutes = Object.keys(pages).filter((page) => page.startsWith('/api/')) // two possible places @@ -209,7 +213,7 @@ export const getApiRouteConfigs = async (publish: string, baseDir: string): Prom return await Promise.all( apiRoutes.map(async (apiRoute) => { - const filePath = getSourceFileForPage(apiRoute, [pagesDir, srcPagesDir]) + const filePath = getSourceFileForPage(apiRoute, [pagesDir, srcPagesDir], pageExtensions) return { route: apiRoute, config: await extractConfigFromFile(filePath), compiled: pages[apiRoute] } }), ) @@ -218,8 +222,12 @@ export const getApiRouteConfigs = async (publish: string, baseDir: string): Prom /** * Looks for extended API routes (background and scheduled functions) and extract the config from the source file. */ -export const getExtendedApiRouteConfigs = async (publish: string, baseDir: string): Promise> => { - const settledApiRoutes = await getApiRouteConfigs(publish, baseDir) +export const getExtendedApiRouteConfigs = async ( + publish: string, + baseDir: string, + pageExtensions: string[], +): Promise> => { + const settledApiRoutes = await getApiRouteConfigs(publish, baseDir, pageExtensions) // We only want to return the API routes that are background or scheduled functions return settledApiRoutes.filter((apiRoute) => apiRoute.config.type !== undefined) diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 6fabcb5e0d..58a3e3604e 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -65,6 +65,7 @@ const plugin: NetlifyPlugin = { netlifyConfig.build.environment.NEXT_PRIVATE_TARGET = 'server' }, + // eslint-disable-next-line max-lines-per-function async onBuild({ constants, netlifyConfig, @@ -79,11 +80,22 @@ const plugin: NetlifyPlugin = { checkNextSiteHasBuilt({ publish, failBuild }) - const { appDir, basePath, i18n, images, target, ignore, trailingSlash, outdir, experimental, routesManifest } = - await getNextConfig({ - publish, - failBuild, - }) + const { + appDir, + basePath, + i18n, + images, + target, + ignore, + trailingSlash, + outdir, + experimental, + routesManifest, + pageExtensions, + } = await getNextConfig({ + publish, + failBuild, + }) await cleanupEdgeFunctions(constants) const middlewareManifest = await loadMiddlewareManifest(netlifyConfig) @@ -150,7 +162,7 @@ const plugin: NetlifyPlugin = { const buildId = readFileSync(join(publish, 'BUILD_ID'), 'utf8').trim() await configureHandlerFunctions({ netlifyConfig, ignore, publish: relative(process.cwd(), publish) }) - const apiRoutes = await getExtendedApiRouteConfigs(publish, appDir) + const apiRoutes = await getExtendedApiRouteConfigs(publish, appDir, pageExtensions) await generateFunctions(constants, appDir, apiRoutes) await generatePagesResolver(constants) diff --git a/test/fixtures/page-extensions/custom/pages/api/custom.api.js b/test/fixtures/page-extensions/custom/pages/api/custom.api.js new file mode 100644 index 0000000000..625c0891b2 --- /dev/null +++ b/test/fixtures/page-extensions/custom/pages/api/custom.api.js @@ -0,0 +1 @@ +// noop \ No newline at end of file diff --git a/test/fixtures/page-extensions/default/pages/api/default.js b/test/fixtures/page-extensions/default/pages/api/default.js new file mode 100644 index 0000000000..625c0891b2 --- /dev/null +++ b/test/fixtures/page-extensions/default/pages/api/default.js @@ -0,0 +1 @@ +// noop \ No newline at end of file diff --git a/test/helpers/files.spec.ts b/test/helpers/files.spec.ts index 4e1cb3f596..02e77e3e5b 100644 --- a/test/helpers/files.spec.ts +++ b/test/helpers/files.spec.ts @@ -6,6 +6,7 @@ import { patchNextFiles, unpatchNextFiles, getDependenciesOfFile, + getSourceFileForPage, } from "../../packages/runtime/src/helpers/files" import { readFileSync, @@ -19,6 +20,8 @@ import { join } from "pathe" import { Rewrites } from "../../packages/runtime/src/helpers/types" import { describeCwdTmpDir, moveNextDist } from "../test-utils" +const TEST_DIR = resolve(__dirname, '..') + const REDIRECTS: Rewrites = [ { source: '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/', @@ -215,7 +218,27 @@ describe('dependency tracing', () => { it('generates dependency list from a source file', async () => { const dependencies = await getDependenciesOfFile(resolve(__dirname, '../fixtures/analysis/background.js')) expect(dependencies).toEqual( - ['test/webpack-api-runtime.js', 'package.json'].map((dep) => resolve(dirname(resolve(__dirname, '..')), dep)), + ['test/webpack-api-runtime.js', 'package.json'].map((dep) => resolve(dirname(TEST_DIR), dep)), ) }) +}) + +describe('getSourceFileForPage', () => { + it('handles default pageExtensions', () => { + const pagesDir = resolve(__dirname, '../fixtures/page-extensions/default/pages') + const apiRoute = '/api/default' + + const filePath = getSourceFileForPage(apiRoute, [pagesDir]) + + expect(filePath.replace(TEST_DIR, '')).toBe('/fixtures/page-extensions/default/pages/api/default.js') + }) + + it('handles custom pageExtensions', () => { + const pagesDir = resolve(__dirname, '../fixtures/page-extensions/custom/pages') + const apiRoute = '/api/custom' + + const filePath = getSourceFileForPage(apiRoute, [pagesDir], ['api.js']) + + expect(filePath.replace(TEST_DIR, '')).toBe('/fixtures/page-extensions/custom/pages/api/custom.api.js') + }) }) \ No newline at end of file