diff --git a/src/lib/functions/registry.mjs b/src/lib/functions/registry.mjs index aea023fcdba..ff8e5772b7f 100644 --- a/src/lib/functions/registry.mjs +++ b/src/lib/functions/registry.mjs @@ -212,7 +212,10 @@ export class FunctionsRegistry { await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func.name))) await Promise.all( - functions.map(async ({ displayName, mainFile, name, runtime: runtimeName }) => { + // zip-it-and-ship-it returns an array sorted based on which extension should have precedence, + // where the last ones precede the previous ones. This is why + // we reverse the array so we get the right functions precedence in the CLI. + functions.reverse().map(async ({ displayName, mainFile, name, runtime: runtimeName }) => { const runtime = runtimes[runtimeName] // If there is no matching runtime, it means this function is not yet diff --git a/src/lib/functions/server.mjs b/src/lib/functions/server.mjs index c630a83a170..af43794e1d7 100644 --- a/src/lib/functions/server.mjs +++ b/src/lib/functions/server.mjs @@ -222,9 +222,9 @@ export const startFunctionsServer = async (options) => { functionsDirectories.push(distPath) } } else { - // The order of the function directories matters. Leftmost directories take + // The order of the function directories matters. Rightmost directories take // precedence. - const sourceDirectories = [settings.functions, internalFunctionsDir].filter(Boolean) + const sourceDirectories = [internalFunctionsDir, settings.functions].filter(Boolean) functionsDirectories.push(...sourceDirectories) } diff --git a/tests/unit/lib/functions/registry.test.mjs b/tests/unit/lib/functions/registry.test.mjs index 78b60167abe..2a44990d403 100644 --- a/tests/unit/lib/functions/registry.test.mjs +++ b/tests/unit/lib/functions/registry.test.mjs @@ -1,8 +1,40 @@ -import { expect, test, vi } from 'vitest' +import { mkdir, mkdtemp, writeFile } from 'fs/promises' +import { tmpdir } from 'os' +import { join } from 'path' + +import { describe, expect, test, vi } from 'vitest' import { FunctionsRegistry } from '../../../../src/lib/functions/registry.mjs' import { watchDebounced } from '../../../../src/utils/command-helpers.mjs' +const duplicateFunctions = [ + { + filename: 'hello.js', + content: `exports.handler = async (event) => ({ statusCode: 200, body: JSON.stringify({ message: 'Hello World from .js' }) })`, + }, + { + filename: 'hello.ts', + content: `exports.handler = async (event) => ({ statusCode: 200, body: JSON.stringify({ message: 'Hello World from .ts' }) })`, + }, + { + filename: 'hello2.js', + content: `exports.handler = async (event) => ({ statusCode: 200, body: JSON.stringify({ message: 'Hello World from .ts' }) })`, + }, + { + filename: 'hello2/main.go', + subDir: 'hello2', + content: `package main + import ( + "fmt" + ) + + func main() { + fmt.Println("Hello, world from a go function!") + } + `, + }, +] + vi.mock('../../../../src/utils/command-helpers.mjs', async () => { const helpers = await vi.importActual('../../../../src/utils/command-helpers.mjs') @@ -35,6 +67,40 @@ test('registry should only pass functions config to zip-it-and-ship-it', async ( await prepareDirectoryScanStub.mockRestore() }) +describe('the registry handles duplicate functions based on extension precedence', () => { + test('where .js takes precedence over .go, and .go over .ts', async () => { + const projectRoot = await mkdtemp(join(tmpdir(), 'functions-extension-precedence')) + const functionsDirectory = join(projectRoot, 'functions') + await mkdir(functionsDirectory) + + duplicateFunctions.forEach(async (func) => { + if (func.subDir) { + const subDir = join(functionsDirectory, func.subDir) + await mkdir(subDir) + } + const file = join(functionsDirectory, func.filename) + await writeFile(file, func.content) + }) + const functionsRegistry = new FunctionsRegistry({ + projectRoot, + config: {}, + timeouts: { syncFunctions: 1, backgroundFunctions: 1 }, + settings: { port: 8888 }, + }) + const prepareDirectoryScanStub = vi.spyOn(FunctionsRegistry, 'prepareDirectoryScan').mockImplementation(() => {}) + const setupDirectoryWatcherStub = vi.spyOn(functionsRegistry, 'setupDirectoryWatcher').mockImplementation(() => {}) + + await functionsRegistry.scan([functionsDirectory]) + const { functions } = functionsRegistry + + expect(functions.get('hello').runtime.name).toBe('js') + expect(functions.get('hello2').runtime.name).toBe('go') + + await setupDirectoryWatcherStub.mockRestore() + await prepareDirectoryScanStub.mockRestore() + }) +}) + test('should add included_files to watcher', async () => { const registry = new FunctionsRegistry({}) const func = {