diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 72935ed06d0..36c13b3ba41 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -4,6 +4,7 @@ module.exports = { extends: '@netlify/eslint-config-node', plugins: ['sort-destructure-keys'], parserOptions: { + ecmaVersion: '2020', babelOptions: { parserOpts: { sourceType: 'unambiguous', @@ -80,6 +81,7 @@ module.exports = { { files: ['src/**/*.mjs', 'bin/**/*.mjs'], parserOptions: { + ecmaVersion: '2020', sourceType: 'module', babelOptions: { parserOpts: { diff --git a/.gitignore b/.gitignore index 26f3eb3d19e..1a94167da46 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ vendor .DS_STORE # Local Netlify folder -.netlify +/.netlify # site test-site diff --git a/site/package-lock.json b/site/package-lock.json index edcb0b5992f..cca831ab986 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -30,7 +30,7 @@ "strip-ansi": "^7.0.0" }, "engines": { - "node": "^14.16.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" } }, "node_modules/@algolia/cache-browser-local-storage": { diff --git a/src/lib/edge-functions/proxy.mjs b/src/lib/edge-functions/proxy.mjs index 2ff30182c70..fbbb947f9f6 100644 --- a/src/lib/edge-functions/proxy.mjs +++ b/src/lib/edge-functions/proxy.mjs @@ -79,11 +79,12 @@ export const initializeProxy = async ({ const server = prepareServer({ config, configPath, - directories: [internalFunctionsPath, userFunctionsPath].filter(Boolean), + directory: userFunctionsPath, env: configEnv, getUpdatedConfig, importMaps: [importMap].filter(Boolean), inspectSettings, + internalDirectory: internalFunctionsPath, internalFunctions, port: isolatePort, projectDir, @@ -153,11 +154,12 @@ export const isEdgeFunctionsRequest = (req) => req[headersSymbol] !== undefined const prepareServer = async ({ config, configPath, - directories, + directory, env: configEnv, getUpdatedConfig, importMaps, inspectSettings, + internalDirectory, internalFunctions, port, projectDir, @@ -187,9 +189,10 @@ const prepareServer = async ({ bundler, config, configPath, - directories, + directories: [directory].filter(Boolean), env: configEnv, getUpdatedConfig, + internalDirectories: [internalDirectory].filter(Boolean), internalFunctions, projectDir, runIsolate, diff --git a/src/lib/edge-functions/registry.mjs b/src/lib/edge-functions/registry.mjs index d3979c9caed..b8e8bd1edf6 100644 --- a/src/lib/edge-functions/registry.mjs +++ b/src/lib/edge-functions/registry.mjs @@ -8,7 +8,66 @@ import { NETLIFYDEVERR, NETLIFYDEVLOG, chalk, log, warn, watchDebounced } from ' /** @typedef {import('@netlify/edge-bundler').FunctionConfig} FunctionConfig */ /** @typedef {Awaited>} RunIsolate */ +const featureFlags = { edge_functions_correct_order: true } + export class EdgeFunctionsRegistry { + /** @type {import('@netlify/edge-bundler')} */ + #bundler + + /** @type {string} */ + #configPath + + /** @type {string[]} */ + #directories + + /** @type {string[]} */ + #internalDirectories + + /** @type {() => Promise} */ + #getUpdatedConfig + + /** @type {RunIsolate} */ + #runIsolate + + /** @type {Error | null} */ + #buildError = null + + /** @type {Declaration[]} */ + #declarationsFromDeployConfig + + /** @type {Record} */ + #userFunctionConfigs = {} + + /** @type {Record} */ + #internalFunctionConfigs = {} + + /** @type {Declaration[]} */ + #declarationsFromTOML + + /** @type {Record} */ + #env + + /** @type {import('chokidar').FSWatcher} */ + #configWatcher + + /** @type {Map} */ + #directoryWatchers = new Map() + + /** @type {Map} */ + #dependencyPaths = new Map() + + /** @type {Map} */ + #functionPaths = new Map() + + /** @type {EdgeFunction[]} */ + #userFunctions = [] + + /** @type {EdgeFunction[]} */ + #internalFunctions = [] + + /** @type {Promise} */ + #initialScan + /** * @param {Object} opts * @param {import('@netlify/edge-bundler')} opts.bundler @@ -17,6 +76,7 @@ export class EdgeFunctionsRegistry { * @param {string[]} opts.directories * @param {Record} opts.env * @param {() => Promise} opts.getUpdatedConfig + * @param {string[]} opts.internalDirectories * @param {Declaration[]} opts.internalFunctions * @param {string} opts.projectDir * @param {RunIsolate} opts.runIsolate @@ -28,91 +88,71 @@ export class EdgeFunctionsRegistry { directories, env, getUpdatedConfig, + internalDirectories, internalFunctions, projectDir, runIsolate, }) { - this.bundler = bundler - - /** - * @type {string} - */ - this.configPath = configPath - - /** - * @type {string[]} - */ - this.directories = directories - - /** - * @type {() => Promise} - */ - this.getUpdatedConfig = getUpdatedConfig - - /** - * @type {RunIsolate} - */ - this.runIsolate = runIsolate - - /** - * @type {Error | null} - */ - this.buildError = null - - /** - * @type {Declaration[]} - */ - this.declarationsFromDeployConfig = internalFunctions - - /** - * @type {Record} - */ - this.declarationsFromSource = {} - - /** - * @type {Declaration[]} - */ - this.declarationsFromTOML = EdgeFunctionsRegistry.getDeclarationsFromTOML(config) - - /** - * @type {Record} - */ - this.env = EdgeFunctionsRegistry.getEnvironmentVariables(env) - - /** - * @type {Map} - */ - this.directoryWatchers = new Map() - - /** - * @type {Map} - */ - this.dependencyPaths = new Map() - - /** - * @type {Map} - */ - this.functionPaths = new Map() - - /** - * @type {EdgeFunction[]} - */ - this.functions = [] - - /** - * @type {Promise} - */ - this.initialScan = this.scan(directories) - - this.setupWatchers({ projectDir }) + this.#bundler = bundler + this.#configPath = configPath + this.#directories = directories + this.#internalDirectories = internalDirectories + this.#getUpdatedConfig = getUpdatedConfig + this.#runIsolate = runIsolate + + this.#declarationsFromDeployConfig = internalFunctions + this.#declarationsFromTOML = EdgeFunctionsRegistry.#getDeclarationsFromTOML(config) + this.#env = EdgeFunctionsRegistry.#getEnvironmentVariables(env) + + this.#buildError = null + this.#userFunctionConfigs = {} + this.#internalFunctionConfigs = {} + this.#directoryWatchers = new Map() + this.#dependencyPaths = new Map() + this.#functionPaths = new Map() + this.#userFunctions = [] + this.#internalFunctions = [] + + this.#initialScan = this.#doInitialScan() + + this.#setupWatchers(projectDir) } /** - * @param {EdgeFunction[]} functions + * @returns {Promise} */ - async build(functions) { + async #doInitialScan() { + const [internalFunctions, userFunctions] = await Promise.all([ + this.#scanForFunctions(this.#internalDirectories), + this.#scanForFunctions(this.#directories), + ]) + this.#internalFunctions = internalFunctions.all + this.#userFunctions = userFunctions.all + + this.#functions.forEach((func) => { + this.#logAddedFunction(func) + }) + try { - const { functionsConfig, graph, success } = await this.runIsolate(functions, this.env, { + await this.#build() + } catch { + // no-op + } + } + + /** + * @return {EdgeFunction[]} + */ + get #functions() { + return [...this.#internalFunctions, ...this.#userFunctions] + } + + /** + * @return {Promise} + */ + async #build() { + try { + const { functionsConfig, graph, success } = await this.#runIsolate(this.#functions, this.#env, { getFunctionsConfig: true, }) @@ -120,70 +160,81 @@ export class EdgeFunctionsRegistry { throw new Error('Build error') } - this.buildError = null - this.declarationsFromSource = functions.reduce( - (acc, func, index) => ({ ...acc, [func.name]: functionsConfig[index] }), + this.#buildError = null + + // We use one index to loop over both internal and user function, because we know that this.#functions has internalFunctions first. + // functionsConfig therefore contains first all internal functionConfigs and then user functionConfigs + let index = 0 + + this.#internalFunctionConfigs = this.#internalFunctions.reduce( + // eslint-disable-next-line no-plusplus + (acc, func) => ({ ...acc, [func.name]: functionsConfig[index++] }), + {}, + ) + + this.#userFunctionConfigs = this.#userFunctions.reduce( + // eslint-disable-next-line no-plusplus + (acc, func) => ({ ...acc, [func.name]: functionsConfig[index++] }), {}, ) - this.processGraph(graph) + this.#processGraph(graph) } catch (error) { - this.buildError = error + this.#buildError = error throw error } } - async checkForAddedOrDeletedFunctions() { - const functionsFound = await this.bundler.find(this.directories) - const newFunctions = functionsFound.filter((func) => { - const functionExists = this.functions.some( - (existingFunc) => func.name === existingFunc.name && func.path === existingFunc.path, - ) - - return !functionExists - }) - const deletedFunctions = this.functions.filter((existingFunc) => { - const functionExists = functionsFound.some( - (func) => func.name === existingFunc.name && func.path === existingFunc.path, - ) + /** + * @returns {Promise} + */ + async #checkForAddedOrDeletedFunctions() { + const [internalFunctions, userFunctions] = await Promise.all([ + this.#scanForFunctions(this.#internalDirectories), + this.#scanForFunctions(this.#directories), + ]) - return !functionExists - }) + this.#internalFunctions = internalFunctions.all + this.#userFunctions = userFunctions.all - this.functions = functionsFound + const newFunctions = [...internalFunctions.new, ...userFunctions.new] + const deletedFunctions = [...internalFunctions.deleted, ...userFunctions.deleted] if (newFunctions.length === 0 && deletedFunctions.length === 0) { return } try { - await this.build(functionsFound) + await this.#build() deletedFunctions.forEach((func) => { - EdgeFunctionsRegistry.logDeletedFunction(func, this.findDisplayName(func.name)) + this.#logDeletedFunction(func) }) newFunctions.forEach((func) => { - EdgeFunctionsRegistry.logAddedFunction(func, this.findDisplayName(func.name)) + this.#logAddedFunction(func) }) } catch { // no-op } } - static getDeclarationsFromTOML(config) { + /** + * @param {any} config + * @returns {Declaration[]} + */ + static #getDeclarationsFromTOML(config) { const { edge_functions: edgeFunctions = [] } = config return edgeFunctions } /** - * - * @param {Record} envConfig + * @param {Record} envConfig * @returns {Record} */ - static getEnvironmentVariables(envConfig) { + static #getEnvironmentVariables(envConfig) { const env = Object.create(null) Object.entries(envConfig).forEach(([key, variable]) => { if ( @@ -202,23 +253,27 @@ export class EdgeFunctionsRegistry { return env } - async handleFileChange(path) { + /** + * @param {string} path + * @returns {Promise} + */ + async #handleFileChange(path) { const matchingFunctions = new Set( - [this.functionPaths.get(path), ...(this.dependencyPaths.get(path) || [])].filter(Boolean), + [this.#functionPaths.get(path), ...(this.#dependencyPaths.get(path) || [])].filter(Boolean), ) // If the file is not associated with any function, there's no point in // building. However, it might be that the path is in fact associated with // a function but we just haven't registered it due to a build error. So if // there was a build error, let's always build. - if (matchingFunctions.size === 0 && this.buildError === null) { + if (matchingFunctions.size === 0 && this.#buildError === null) { return } log(`${NETLIFYDEVLOG} ${chalk.magenta('Reloading')} edge functions...`) try { - await this.build(this.functions) + await this.#build() const functionNames = [...matchingFunctions] @@ -226,8 +281,11 @@ export class EdgeFunctionsRegistry { log(`${NETLIFYDEVLOG} ${chalk.green('Reloaded')} edge functions`) } else { functionNames.forEach((functionName) => { - const displayName = this.findDisplayName(functionName) - log(`${NETLIFYDEVLOG} ${chalk.green('Reloaded')} edge function ${chalk.yellow(displayName || functionName)}`) + log( + `${NETLIFYDEVLOG} ${chalk.green('Reloaded')} edge function ${chalk.yellow( + this.#getDisplayName(functionName), + )}`, + ) }) } } catch { @@ -235,46 +293,44 @@ export class EdgeFunctionsRegistry { } } - initialize() { - this.initialization = - this.initialization || - // eslint-disable-next-line promise/prefer-await-to-then - this.initialScan.then(async (functions) => { - try { - await this.build(functions) - } catch { - // no-op - } - - return null - }) - - return this.initialization + /** + * @return {Promise} + */ + async initialize() { + return await this.#initialScan } - static logAddedFunction(func, displayName) { - log(`${NETLIFYDEVLOG} ${chalk.green('Loaded')} edge function ${chalk.yellow(displayName || func.name)}`) + /** + * @param {EdgeFunction} func + */ + #logAddedFunction(func) { + log(`${NETLIFYDEVLOG} ${chalk.green('Loaded')} edge function ${chalk.yellow(this.#getDisplayName(func.name))}`) } - static logDeletedFunction(func, displayName) { - log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} edge function ${chalk.yellow(displayName || func.name)}`) + /** + * @param {EdgeFunction} func + */ + #logDeletedFunction(func) { + log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} edge function ${chalk.yellow(this.#getDisplayName(func.name))}`) } /** * @param {string} urlPath */ matchURLPath(urlPath) { - const declarations = this.bundler.mergeDeclarations( - this.declarationsFromTOML, - this.declarationsFromSource, - {}, - this.declarationsFromDeployConfig, + const declarations = this.#bundler.mergeDeclarations( + this.#declarationsFromTOML, + this.#userFunctionConfigs, + this.#internalFunctionConfigs, + this.#declarationsFromDeployConfig, + featureFlags, ) - const manifest = this.bundler.generateManifest({ + const manifest = this.#bundler.generateManifest({ declarations, - userFunctionConfig: this.declarationsFromSource, - internalFunctionConfig: {}, - functions: this.functions, + userFunctionConfig: this.#userFunctionConfigs, + internalFunctionConfig: this.#internalFunctionConfigs, + functions: this.#functions, + featureFlags, }) const invocationMetadata = { function_config: manifest.function_config, @@ -293,22 +349,29 @@ export class EdgeFunctionsRegistry { return !isExcluded }) .map((route) => route.function) - const orphanedDeclarations = this.matchURLPathAgainstOrphanedDeclarations(urlPath) + const orphanedDeclarations = this.#matchURLPathAgainstOrphanedDeclarations(urlPath) return { functionNames, invocationMetadata, orphanedDeclarations } } - matchURLPathAgainstOrphanedDeclarations(urlPath) { + /** + * + * @param {string} urlPath + * @returns {string[]} + */ + #matchURLPathAgainstOrphanedDeclarations(urlPath) { // `generateManifest` will only include functions for which there is both a // function file and a config declaration, but we want to catch cases where // a config declaration exists without a matching function file. To do that // we compute a list of functions from the declarations (the `path` doesn't // really matter). - const functions = this.declarationsFromTOML.map((declaration) => ({ name: declaration.function, path: '' })) - const manifest = this.bundler.generateManifest({ - declarations: this.declarationsFromTOML, - functionConfig: this.declarationsFromSource, + const functions = this.#declarationsFromTOML.map((declaration) => ({ name: declaration.function, path: '' })) + const manifest = this.#bundler.generateManifest({ + declarations: this.#declarationsFromTOML, + userFunctionConfig: this.#userFunctionConfigs, + internalFunctionConfig: this.#internalFunctionConfigs, functions, + featureFlags, }) const routes = [...manifest.routes, ...manifest.post_cache_routes].map((route) => ({ @@ -318,7 +381,7 @@ export class EdgeFunctionsRegistry { const functionNames = routes .filter((route) => { - const hasFunctionFile = this.functions.some((func) => func.name === route.function) + const hasFunctionFile = this.#functions.some((func) => func.name === route.function) if (hasFunctionFile) { return false @@ -331,18 +394,18 @@ export class EdgeFunctionsRegistry { return functionNames } - processGraph(graph) { + #processGraph(graph) { if (!graph) { warn('Could not process edge functions dependency graph. Live reload will not be available.') return } - // Creating a Map from `this.functions` that map function paths to function + // Creating a Map from `this.#functions` that map function paths to function // names. This allows us to match modules against functions in O(1) time as // opposed to O(n). // eslint-disable-next-line unicorn/prefer-spread - const functionPaths = new Map(Array.from(this.functions, (func) => [func.path, func.name])) + const functionPaths = new Map(Array.from(this.#functions, (func) => [func.path, func.name])) // Mapping file URLs to names of functions that use them as dependencies. const dependencyPaths = new Map() @@ -378,32 +441,48 @@ export class EdgeFunctionsRegistry { }) }) - this.dependencyPaths = dependencyPaths - this.functionPaths = functionPaths + this.#dependencyPaths = dependencyPaths + this.#functionPaths = functionPaths } - async scan(directories) { - const functions = await this.bundler.find(directories) + /** + * + * @param {string[]} directories + * @returns {Promise<{all: EdgeFunction[], new: EdgeFunction[], deleted: EdgeFunction[]}>} + */ + async #scanForFunctions(directories) { + const functions = await this.#bundler.find(directories) + const newFunctions = functions.filter((func) => { + const functionExists = this.#functions.some( + (existingFunc) => func.name === existingFunc.name && func.path === existingFunc.path, + ) - functions.forEach((func) => { - EdgeFunctionsRegistry.logAddedFunction(func, this.findDisplayName(func.name)) + return !functionExists }) + const deletedFunctions = this.#functions.filter((existingFunc) => { + const functionExists = functions.some( + (func) => func.name === existingFunc.name && func.path === existingFunc.path, + ) - this.functions = functions + return !functionExists + }) - return functions + return { all: functions, new: newFunctions, deleted: deletedFunctions } } - async setupWatchers({ projectDir }) { + /** + * @param {string} projectDir + */ + async #setupWatchers(projectDir) { // Creating a watcher for the config file. When it changes, we update the // declarations and see if we need to register or unregister any functions. - this.configWatcher = await watchDebounced(this.configPath, { + this.#configWatcher = await watchDebounced(this.#configPath, { onChange: async () => { - const newConfig = await this.getUpdatedConfig() + const newConfig = await this.#getUpdatedConfig() - this.declarationsFromTOML = EdgeFunctionsRegistry.getDeclarationsFromTOML(newConfig) + this.#declarationsFromTOML = EdgeFunctionsRegistry.#getDeclarationsFromTOML(newConfig) - await this.checkForAddedOrDeletedFunctions() + await this.#checkForAddedOrDeletedFunctions() }, }) @@ -411,22 +490,30 @@ export class EdgeFunctionsRegistry { // directories, they might be importing files that are located in // parent directories. So we watch the entire project directory for // changes. - await this.setupWatcherForDirectory(projectDir) + await this.#setupWatcherForDirectory(projectDir) } - async setupWatcherForDirectory(directory) { + /** + * @param {string} directory + * @returns {Promise} + */ + async #setupWatcherForDirectory(directory) { const watcher = await watchDebounced(directory, { - onAdd: () => this.checkForAddedOrDeletedFunctions(), - onChange: (path) => this.handleFileChange(path), - onUnlink: () => this.checkForAddedOrDeletedFunctions(), + onAdd: () => this.#checkForAddedOrDeletedFunctions(), + onChange: (path) => this.#handleFileChange(path), + onUnlink: () => this.#checkForAddedOrDeletedFunctions(), }) - this.directoryWatchers.set(directory, watcher) + this.#directoryWatchers.set(directory, watcher) } - findDisplayName(func) { - const declarations = [...this.declarationsFromTOML, ...this.declarationsFromDeployConfig] + /** + * @param {string} func + * @returns {string | undefined} + */ + #getDisplayName(func) { + const declarations = [...this.#declarationsFromTOML, ...this.#declarationsFromDeployConfig] - return declarations.find((declaration) => declaration.function === func)?.name + return declarations.find((declaration) => declaration.function === func)?.name ?? func } } diff --git a/tests/integration/20.command.functions.test.cjs b/tests/integration/20.command.functions.test.cjs index 56990d3a7c7..d27fc0ae250 100644 --- a/tests/integration/20.command.functions.test.cjs +++ b/tests/integration/20.command.functions.test.cjs @@ -54,7 +54,7 @@ test('should serve functions on default port', async (t) => { .buildAsync() await withFunctionsServer({ builder }, async () => { - const response = await got(`http://localhost:9999/.netlify/functions/ping`, { retry: 1 }).text() + const response = await got(`http://localhost:9999/.netlify/functions/ping`, { retry: { limit: 1 } }).text() t.is(response, 'ping') }) }) diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-iscA.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-iscA.ts new file mode 100644 index 00000000000..5537a8f417d --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-iscA.ts @@ -0,0 +1,8 @@ +import { IntegrationsConfig } from 'https://edge.netlify.com' +import createEdgeFunction from '../../src/edge-function.ts' + +export default createEdgeFunction('integration-iscA') + +export const config: IntegrationsConfig = { + path: '/ordertest', +} diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-iscB.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-iscB.ts new file mode 100644 index 00000000000..1e995fb6441 --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-iscB.ts @@ -0,0 +1,8 @@ +import { IntegrationsConfig } from 'https://edge.netlify.com' +import createEdgeFunction from '../../src/edge-function.ts' + +export default createEdgeFunction('integration-iscB') + +export const config: IntegrationsConfig = { + path: '/ordertest', +} diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-manifestA.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-manifestA.ts new file mode 100644 index 00000000000..d5a1d59e84e --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-manifestA.ts @@ -0,0 +1,3 @@ +import createEdgeFunction from '../../src/edge-function.ts' + +export default createEdgeFunction('integration-manifestA') diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-manifestB.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-manifestB.ts new file mode 100644 index 00000000000..fca1d4f368c --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-manifestB.ts @@ -0,0 +1,3 @@ +import createEdgeFunction from '../../src/edge-function.ts' + +export default createEdgeFunction('integration-manifestB') diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-manifestC.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-manifestC.ts new file mode 100644 index 00000000000..e3d9a7ffe60 --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/integration-manifestC.ts @@ -0,0 +1,3 @@ +import createEdgeFunction from '../../src/edge-function.ts' + +export default createEdgeFunction('integration-manifestC') diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/manifest.json b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/manifest.json new file mode 100644 index 00000000000..cdb57ede21a --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/.netlify/edge-functions/manifest.json @@ -0,0 +1,17 @@ +{ + "functions": [ + { + "function": "integration-manifestB", + "path": "/ordertest" + }, + { + "function": "integration-manifestC", + "path": "/ordertest" + }, + { + "function": "integration-manifestA", + "path": "/ordertest" + } + ], + "version": 1 +} diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify.toml b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify.toml new file mode 100644 index 00000000000..142460a4a04 --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify.toml @@ -0,0 +1,14 @@ +[build] +publish = "public" + +[[edge_functions]] +path = "/ordertest" +function = "user-tomlB" + +[[edge_functions]] +path = "/ordertest" +function = "user-tomlC" + +[[edge_functions]] +path = "/ordertest" +function = "user-tomlA" diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-iscA.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-iscA.ts new file mode 100644 index 00000000000..38cc019b26b --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-iscA.ts @@ -0,0 +1,8 @@ +import { Config } from "https://edge.netlify.com"; +import createEdgeFunction from "../../src/edge-function.ts"; + +export default createEdgeFunction("user-iscA"); + +export const config: Config = { + path: '/ordertest', +} diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-iscB.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-iscB.ts new file mode 100644 index 00000000000..1e82f9322a8 --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-iscB.ts @@ -0,0 +1,8 @@ +import { Config } from 'https://edge.netlify.com' +import createEdgeFunction from '../../src/edge-function.ts' + +export default createEdgeFunction('user-iscB') + +export const config: Config = { + path: '/ordertest', +} diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlA.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlA.ts new file mode 100644 index 00000000000..fe2a5319e2f --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlA.ts @@ -0,0 +1,3 @@ +import createEdgeFunction from '../../src/edge-function.ts' + +export default createEdgeFunction('user-tomlA') diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlB.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlB.ts new file mode 100644 index 00000000000..dfb6f8d0f8f --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlB.ts @@ -0,0 +1,3 @@ +import createEdgeFunction from '../../src/edge-function.ts' + +export default createEdgeFunction('user-tomlB') diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlC.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlC.ts new file mode 100644 index 00000000000..2a2ce0e9144 --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/netlify/edge-functions/user-tomlC.ts @@ -0,0 +1,3 @@ +import createEdgeFunction from '../../src/edge-function.ts' + +export default createEdgeFunction('user-tomlC') diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/public/ordertest.html b/tests/integration/__fixtures__/dev-server-with-edge-functions/public/ordertest.html new file mode 100644 index 00000000000..60924258b56 --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/public/ordertest.html @@ -0,0 +1 @@ +origin diff --git a/tests/integration/__fixtures__/dev-server-with-edge-functions/src/edge-function.ts b/tests/integration/__fixtures__/dev-server-with-edge-functions/src/edge-function.ts new file mode 100644 index 00000000000..8bb4f58bbcc --- /dev/null +++ b/tests/integration/__fixtures__/dev-server-with-edge-functions/src/edge-function.ts @@ -0,0 +1,8 @@ +import { Context } from 'https://edge.netlify.com' + +export default (name: string) => async (_request: Request, context: Context) => { + const response = await context.next() + const content = await response.text() + + return new Response(`${name}|${String(content).trim()}`, response) +} diff --git a/tests/integration/commands/dev/__snapshots__/edge-functions.test.ts.snap b/tests/integration/commands/dev/__snapshots__/edge-functions.test.ts.snap new file mode 100644 index 00000000000..59223bc762d --- /dev/null +++ b/tests/integration/commands/dev/__snapshots__/edge-functions.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`edge functions > fixture: dev-server-with-edge-functions > should run edge functions in correct order 1`] = `"integration-manifestB|integration-manifestC|integration-manifestA|integration-iscA|integration-iscB|user-tomlB|user-tomlC|user-tomlA|user-iscA|user-iscB|origin"`; diff --git a/tests/integration/commands/dev/edge-functions.test.ts b/tests/integration/commands/dev/edge-functions.test.ts new file mode 100644 index 00000000000..6e16d784214 --- /dev/null +++ b/tests/integration/commands/dev/edge-functions.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from 'vitest' + +import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' +import got from '../../utils/got.cjs' + +describe('edge functions', () => { + setupFixtureTests('dev-server-with-edge-functions', { devServer: true }, () => { + test('should run edge functions in correct order', async ({ devServer }) => { + const response = await got(`http://localhost:${devServer.port}/ordertest`, { + throwHttpErrors: false, + retry: { limit: 0 }, + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toMatchSnapshot() + }) + }) +}) diff --git a/tests/integration/commands/dev/functions.test.ts b/tests/integration/commands/dev/scheduled-functions.test.ts similarity index 95% rename from tests/integration/commands/dev/functions.test.ts rename to tests/integration/commands/dev/scheduled-functions.test.ts index 2e2f0129cb1..8f85d6217d5 100644 --- a/tests/integration/commands/dev/functions.test.ts +++ b/tests/integration/commands/dev/scheduled-functions.test.ts @@ -9,7 +9,7 @@ describe('scheduled functions', () => { test('should emulate next_run for scheduled functions', async ({ devServer }) => { const response = await got(`http://localhost:${devServer.port}/.netlify/functions/scheduled-isc`, { throwHttpErrors: false, - retry: null, + retry: { limit: 0 }, }) expect(response.statusCode).toBe(200) @@ -20,7 +20,7 @@ describe('scheduled functions', () => { test('should detect file changes to scheduled function', async ({ devServer, fixture }) => { const { body } = await got(`http://localhost:${devServer.port}/.netlify/functions/ping`, { throwHttpErrors: false, - retry: null, + retry: { limit: 0 }, }) expect(body).toBe('ping') @@ -45,7 +45,7 @@ describe('scheduled functions', () => { const { body: warning } = await got(`http://localhost:${devServer.port}/.netlify/functions/ping`, { throwHttpErrors: false, - retry: null, + retry: { limit: 0 }, }) expect(warning).toContain('Your function returned `body`') diff --git a/tests/integration/utils/got.cjs b/tests/integration/utils/got.cjs index fe71ffda415..517886467cd 100644 --- a/tests/integration/utils/got.cjs +++ b/tests/integration/utils/got.cjs @@ -1,6 +1,6 @@ -const got = require('got') +const got = require('got').default -const TIMEOUT = 3e5 +const TIMEOUT = 300_000 // Default got retry status code with the addition of 403 const STATUS_CODE = [403, 408, 413, 429, 500, 502, 503, 504, 521, 522, 524] @@ -9,7 +9,7 @@ const extendedGot = got.extend({ retry: { statusCodes: STATUS_CODE, }, - timeout: TIMEOUT, + timeout: { request: TIMEOUT }, }) module.exports = extendedGot