Repository: netlify/cli
Pull request #5345 (2023-01-03 13:18:19)

chore: migrate everything to ESM
This is the last ESM PR for the `src` codebase. There are no commonjs files anymore in in the `src` folder after this PR. (except templates for functions/sites, which I did not touch)

There are still some cjs test files, which I want to convert separately, as I also have some Ideas on how to improve them.


github-actions[bot]: ## 📊 Benchmark results

*Comparing with aa3939a4ba5666a9a567d4e4fb1ad39f19e3eb3e*

### Package size: 259 MB
 ⬇️ **0.00% decrease** vs. aa3939a4ba5666a9a567d4e4fb1ad39f19e3eb3e
```
^                                                                                  259 MB  259 MB  259 MB 
│  249 MB  249 MB  249 MB  249 MB  249 MB  249 MB  249 MB  249 MB  249 MB  242 MB   ┌──┐    ┌──┐    ┌──┐  
│ ──┌──┐────┌──┐────┌──┐────┌──┐────┌──┐────┌──┐────┌──┐────┌──┐────┌──┐────┌──┐────┼──┼────┼──┼────|▒▒|──
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
└───┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴──>
    T-12    T-11    T-10    T-9     T-8     T-7     T-6     T-5     T-4     T-3     T-2     T-1      T    
```
<details>
<summary>Legend</summary>

- T-30 (004155908afa96a9673d02eaf17c9c2bd4eebc5c): 254 MB
- T-29 (e65f97cb607b1fe36517e8621f0c2f09eea54393): 254 MB
- T-28 (35123df3b0d956b49f51bfe12fc879ca2d99af71): 254 MB
- T-27 (a49e5fcbfa0671d0f7eb60836cfa3821a5bdc888): 249 MB
- T-26 (62147688065f939a594d4cf9581c521d9f7ae3a5): 242 MB
- T-25 (acdcbb6e59a9e75b7d69f4b30104b53f189e6749): 242 MB
- T-24 (743b4d68306409f3a7608d2fa0b4fd79685007b5): 242 MB
- T-23 (f9f45edd211ab91b08af71d74d5bd1ac585665b1): 249 MB
- T-22 (22de9e2288b4da12f59f8be5a0493e64a2db0f8b): 249 MB
- T-21 (d64c875130979b9ca08daa0dd188c65fc620b938): 249 MB
- T-20 (cd993b0aa9cc38737510158675cdfbc6db74995b): 249 MB
- T-19 (9a7c66697aa219a872e9d436f17791f323cfedfb): 249 MB
- T-18 (cf2f4020f5956faddd3aa1c718dc353f43d7757f): 249 MB
- T-17 (6102e77f1b106ffda3eea5cbcfef71dff7711679): 249 MB
- T-16 (d3dfa172383a21396294973a666d9665582864c4): 249 MB
- T-15 (9bb51a1b43572a1bcb110b19dd960f0f4c3ed82e): 249 MB
- T-14 (81ac0c6500f2167e0d0a1c8bb4b5f853e1578005): 249 MB
- T-13 (bb16cec831d12114327b19c984367294cfecca41): 249 MB
- T-12 (3ff4a57a95c445a185f9a210aced06bfc2b117be): 249 MB
- T-11 (3b491be71ea7a634c31733eb9d8a81180e4bba2a): 249 MB
- T-10 (0bc18bb6939d62ef8b3d993d98f465094b8dc72b): 249 MB
- T-9 (12664f31b6b4b4cf6bea98840cf181340ca3f2ce): 249 MB
- T-8 (7ebf238644369492ab32c81dd8c48824928a4d07): 249 MB
- T-7 (df9260a7a72b66f7ab073e9dc729fbb7c9bcfebc): 249 MB
- T-6 (e3db3144d77f7a3c7abc9b2fe803d701242492f4): 249 MB
- T-5 (7eb44798bf2c113a118c60d38990fdb7a536ae06): 249 MB
- T-4 (89b14e280df05cd84e931d1b3ca66208c79f4d91): 249 MB
- T-3 (e1f661ade9da2a36d6fb4ebf5b2556ca97ae31f9): 242 MB
- T-2 (2994d3eb57e2fd35bda68917db84d17b7d3cf08f): 259 MB
- T-1 (aa3939a4ba5666a9a567d4e4fb1ad39f19e3eb3e): 259 MB
- **T (current commit): 259 MB**</details>
<!--delta:post-package-size@{}-->

Patch:
diff --git a/docs/netlify-dev.md b/docs/netlify-dev.md
index 905573ed..3d9eff58 100644
--- a/docs/netlify-dev.md
+++ b/docs/netlify-dev.md
@@ -304,11 +304,11 @@ to do it for you.
 **Writing your own Function Templates**
 
 Function templates can specify `addons` that they rely on as well as execute arbitrary code after installation in an
-`onComplete` hook, if a special `.netlify-function-template.js` file exists in the directory:
+`onComplete` hook, if a special `.netlify-function-template.mjs` file exists in the directory:
 
 ```js
-// .netlify-function-template.js
-module.exports = {
+// .netlify-function-template.mjs
+export default {
   addons: [
     {
       addonName: 'fauna',
diff --git a/src/commands/deploy/deploy.mjs b/src/commands/deploy/deploy.mjs
index e8371ece..6d9f6e57 100644
--- a/src/commands/deploy/deploy.mjs
+++ b/src/commands/deploy/deploy.mjs
@@ -15,7 +15,7 @@ import { getBuildOptions, runBuild } from '../../lib/build.mjs'
 import { featureFlags as edgeFunctionsFeatureFlags } from '../../lib/edge-functions/consts.mjs'
 import { normalizeFunctionsConfig } from '../../lib/functions/config.mjs'
 import { getLogMessage } from '../../lib/log.mjs'
-import { startSpinner, stopSpinner } from '../../lib/spinner.cjs'
+import { startSpinner, stopSpinner } from '../../lib/spinner.mjs'
 import {
   chalk,
   error,
diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs
index 3596b25e..2da0da1e 100644
--- a/src/commands/dev/dev.mjs
+++ b/src/commands/dev/dev.mjs
@@ -26,7 +26,7 @@ import {
   getNetlifyGraphConfig,
   readGraphQLOperationsSourceFile,
 } from '../../lib/one-graph/cli-netlify-graph.mjs'
-import { startSpinner, stopSpinner } from '../../lib/spinner.cjs'
+import { startSpinner, stopSpinner } from '../../lib/spinner.mjs'
 import {
   BANG,
   chalk,
diff --git a/src/commands/functions/functions-create.mjs b/src/commands/functions/functions-create.mjs
index c33d621b..375df7cf 100644
--- a/src/commands/functions/functions-create.mjs
+++ b/src/commands/functions/functions-create.mjs
@@ -1,11 +1,11 @@
 // @ts-check
 import cp from 'child_process'
 import fs from 'fs'
-import { mkdir } from 'fs/promises'
+import { mkdir, readdir, unlink } from 'fs/promises'
 import { createRequire } from 'module'
 import path, { dirname } from 'path'
 import process from 'process'
-import { fileURLToPath } from 'url'
+import { fileURLToPath, pathToFileURL } from 'url'
 import { promisify } from 'util'
 
 import copyTemplateDirOriginal from 'copy-template-dir'
@@ -16,6 +16,7 @@ import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt'
 import fetch from 'node-fetch'
 import ora from 'ora'
 
+import { fileExistsAsync } from '../../lib/fs.mjs'
 import { getAddons, getCurrentAddon, getSiteData } from '../../utils/addons/prepare.mjs'
 import { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, error, log } from '../../utils/command-helpers.mjs'
 import { injectEnvVariables } from '../../utils/dev.mjs'
@@ -90,19 +91,26 @@ const filterRegistry = function (registry, input) {
     })
 }
 
-const formatRegistryArrayForInquirer = function (lang, funcType) {
-  const folderNames = fs.readdirSync(path.join(templatesDir, lang))
-  const registry = folderNames
-    // filter out markdown files
-    .filter((folderName) => !folderName.endsWith('.md'))
+const formatRegistryArrayForInquirer = async function (lang, funcType) {
+  const folderNames = await readdir(path.join(templatesDir, lang))
 
-    .map((folderName) =>
-      // eslint-disable-next-line import/no-dynamic-require
-      require(path.join(templatesDir, lang, folderName, '.netlify-function-template.cjs')),
-    )
-    .filter((folderName) => folderName.functionType === funcType)
-    .sort((folderNameA, folderNameB) => {
-      const priorityDiff = (folderNameA.priority || DEFAULT_PRIORITY) - (folderNameB.priority || DEFAULT_PRIORITY)
+  const imports = await Promise.all(
+    folderNames
+      // filter out markdown files
+      .filter((folderName) => !folderName.endsWith('.md'))
+      .map(async (folderName) => {
+        const templatePath = path.join(templatesDir, lang, folderName, '.netlify-function-template.mjs')
+        // eslint-disable-next-line import/no-dynamic-require
+        const template = await import(pathToFileURL(templatePath))
+
+        return template.default
+      }),
+  )
+
+  const registry = imports
+    .filter((template) => template.functionType === funcType)
+    .sort((templateA, templateB) => {
+      const priorityDiff = (templateA.priority || DEFAULT_PRIORITY) - (templateB.priority || DEFAULT_PRIORITY)
 
       if (priorityDiff !== 0) {
         return priorityDiff
@@ -112,7 +120,7 @@ const formatRegistryArrayForInquirer = function (lang, funcType) {
       // until Node 11, so the original sorting order from `fs.readdirSync`
       // was not respected. We can simplify this once we drop support for
       // Node 10.
-      return folderNameA - folderNameB
+      return templateA - templateB
     })
     .map((t) => {
       t.lang = lang
@@ -170,7 +178,7 @@ const pickTemplate = async function ({ language: languageFromFlag }, funcType) {
   let templatesForLanguage
 
   try {
-    templatesForLanguage = formatRegistryArrayForInquirer(language, funcType)
+    templatesForLanguage = await formatRegistryArrayForInquirer(language, funcType)
   } catch {
     throw error(`Invalid language: ${language}`)
   }
@@ -292,14 +300,14 @@ const ensureFunctionDirExists = async function (command) {
     }
   }
 
-  if (!fs.existsSync(functionsDirHolder)) {
+  if (!(await fileExistsAsync(functionsDirHolder))) {
     log(
       `${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(
         functionsDirHolder,
       )} does not exist yet, creating it...`,
     )
 
-    fs.mkdirSync(functionsDirHolder, { recursive: true })
+    await mkdir(functionsDirHolder, { recursive: true })
 
     log(`${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(functionsDirHolder)} created`)
   }
@@ -350,15 +358,17 @@ const downloadFromURL = async function (command, options, argumentName, function
   })
 
   // read, execute, and delete function template file if exists
-  const fnTemplateFile = path.join(fnFolder, '.netlify-function-template.cjs')
-  if (fs.existsSync(fnTemplateFile)) {
-    // eslint-disable-next-line import/no-dynamic-require
-    const { onComplete, addons = [] } = require(fnTemplateFile)
+  const fnTemplateFile = path.join(fnFolder, '.netlify-function-template.mjs')
+  if (await fileExistsAsync(fnTemplateFile)) {
+    const {
+      default: { onComplete, addons = [] },
+      // eslint-disable-next-line import/no-dynamic-require
+    } = await import(pathToFileURL(fnTemplateFile).href)
 
     await installAddons(command, addons, path.resolve(fnFolder))
     await handleOnComplete({ command, onComplete })
     // delete
-    fs.unlinkSync(fnTemplateFile)
+    await unlink(fnTemplateFile)
   }
 }
 
@@ -471,7 +481,7 @@ const scaffoldFromTemplate = async function (command, options, argumentName, fun
 
     // These files will not be part of the log message because they'll likely
     // be removed before the command finishes.
-    const omittedFromOutput = new Set(['.netlify-function-template.cjs', 'package.json', 'package-lock.json'])
+    const omittedFromOutput = new Set(['.netlify-function-template.mjs', 'package.json', 'package-lock.json'])
     const createdFiles = await copyTemplateDir(pathToTemplate, functionPath, vars)
     createdFiles.forEach((filePath) => {
       const filename = path.basename(filePath)
@@ -487,7 +497,7 @@ const scaffoldFromTemplate = async function (command, options, argumentName, fun
     })
 
     // delete function template file that was copied over by copydir
-    fs.unlinkSync(path.join(functionPath, '.netlify-function-template.cjs'))
+    await unlink(path.join(functionPath, '.netlify-function-template.mjs'))
 
     // npm install
     if (functionPackageJson !== undefined) {
diff --git a/src/commands/sites/sites-list.mjs b/src/commands/sites/sites-list.mjs
index ae25cf52..453d2585 100644
--- a/src/commands/sites/sites-list.mjs
+++ b/src/commands/sites/sites-list.mjs
@@ -1,6 +1,6 @@
 // @ts-check
 import { listSites } from '../../lib/api.mjs'
-import { startSpinner, stopSpinner } from '../../lib/spinner.cjs'
+import { startSpinner, stopSpinner } from '../../lib/spinner.mjs'
 import { chalk, log, logJson } from '../../utils/command-helpers.mjs'
 
 /**
diff --git a/src/commands/watch/watch.mjs b/src/commands/watch/watch.mjs
index 0bab5f56..affa6f2d 100644
--- a/src/commands/watch/watch.mjs
+++ b/src/commands/watch/watch.mjs
@@ -2,7 +2,7 @@
 import pWaitFor from 'p-wait-for'
 import prettyjson from 'prettyjson'
 
-import { startSpinner, stopSpinner } from '../../lib/spinner.cjs'
+import { startSpinner, stopSpinner } from '../../lib/spinner.mjs'
 import { chalk, error, log } from '../../utils/command-helpers.mjs'
 import { init } from '../init/index.mjs'
 
diff --git a/src/functions-templates/go/hello-world/.netlify-function-template.cjs b/src/functions-templates/go/hello-world/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/go/hello-world/.netlify-function-template.cjs
rename to src/functions-templates/go/hello-world/.netlify-function-template.mjs
index 60abc368..b3024a6e 100644
--- a/src/functions-templates/go/hello-world/.netlify-function-template.cjs
+++ b/src/functions-templates/go/hello-world/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'hello-world',
   priority: 1,
   description: 'Basic function that shows how to create a handler and return a response',
diff --git a/src/functions-templates/javascript/apollo-graphql-rest/.netlify-function-template.cjs b/src/functions-templates/javascript/apollo-graphql-rest/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/javascript/apollo-graphql-rest/.netlify-function-template.cjs
rename to src/functions-templates/javascript/apollo-graphql-rest/.netlify-function-template.mjs
index aaeedba4..0bea2175 100644
--- a/src/functions-templates/javascript/apollo-graphql-rest/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/apollo-graphql-rest/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'apollo-graphql-rest',
   description: 'GraphQL function to wrap REST API using apollo-server-lambda and apollo-datasource-rest!',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/apollo-graphql/.netlify-function-template.cjs b/src/functions-templates/javascript/apollo-graphql/.netlify-function-template.mjs
similarity index 86%
rename from src/functions-templates/javascript/apollo-graphql/.netlify-function-template.cjs
rename to src/functions-templates/javascript/apollo-graphql/.netlify-function-template.mjs
index 3f86b54c..cba340ba 100644
--- a/src/functions-templates/javascript/apollo-graphql/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/apollo-graphql/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'apollo-graphql',
   description: 'GraphQL function using Apollo-Server-Lambda!',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/auth-fetch/.netlify-function-template.cjs b/src/functions-templates/javascript/auth-fetch/.netlify-function-template.mjs
similarity index 95%
rename from src/functions-templates/javascript/auth-fetch/.netlify-function-template.cjs
rename to src/functions-templates/javascript/auth-fetch/.netlify-function-template.mjs
index feb3bdf8..73dd99a9 100644
--- a/src/functions-templates/javascript/auth-fetch/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/auth-fetch/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'auth-fetch',
   description: 'Use `node-fetch` library and Netlify Identity to access APIs',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/create-user/.netlify-function-template.cjs b/src/functions-templates/javascript/create-user/.netlify-function-template.mjs
similarity index 95%
rename from src/functions-templates/javascript/create-user/.netlify-function-template.cjs
rename to src/functions-templates/javascript/create-user/.netlify-function-template.mjs
index cbb485fe..b1cf3867 100644
--- a/src/functions-templates/javascript/create-user/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/create-user/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'create-user',
   description: 'Programmatically create a Netlify Identity user by invoking a function',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/fauna-crud/.netlify-function-template.cjs b/src/functions-templates/javascript/fauna-crud/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/javascript/fauna-crud/.netlify-function-template.cjs
rename to src/functions-templates/javascript/fauna-crud/.netlify-function-template.mjs
index 7f14ae62..137e79c9 100644
--- a/src/functions-templates/javascript/fauna-crud/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/fauna-crud/.netlify-function-template.mjs
@@ -1,5 +1,6 @@
-const execa = require('execa')
-module.exports = {
+import execa from 'execa'
+
+export default {
   name: 'fauna-crud',
   description: 'CRUD function using Fauna DB',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/fauna-graphql/.netlify-function-template.cjs b/src/functions-templates/javascript/fauna-graphql/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/javascript/fauna-graphql/.netlify-function-template.cjs
rename to src/functions-templates/javascript/fauna-graphql/.netlify-function-template.mjs
index 8ba07c0a..b850f9e6 100644
--- a/src/functions-templates/javascript/fauna-graphql/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/fauna-graphql/.netlify-function-template.mjs
@@ -1,5 +1,6 @@
-const execa = require('execa')
-module.exports = {
+import execa from 'execa'
+
+export default {
   name: 'fauna-graphql',
   description: 'GraphQL Backend using Fauna DB',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/google-analytics/.netlify-function-template.cjs b/src/functions-templates/javascript/google-analytics/.netlify-function-template.mjs
similarity index 88%
rename from src/functions-templates/javascript/google-analytics/.netlify-function-template.cjs
rename to src/functions-templates/javascript/google-analytics/.netlify-function-template.mjs
index 3679cc82..4aee18f5 100644
--- a/src/functions-templates/javascript/google-analytics/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/google-analytics/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'google-analytics',
   description: 'Google Analytics: proxy for GA on your domain to avoid adblock',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/graphql-gateway/.netlify-function-template.cjs b/src/functions-templates/javascript/graphql-gateway/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/javascript/graphql-gateway/.netlify-function-template.cjs
rename to src/functions-templates/javascript/graphql-gateway/.netlify-function-template.mjs
index 484df79f..051e41a7 100644
--- a/src/functions-templates/javascript/graphql-gateway/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/graphql-gateway/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'graphql-gateway',
   description: 'Apollo Server Lambda Gateway stitching schemas from other GraphQL Functions!',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/hasura-event-triggered/.netlify-function-template.cjs b/src/functions-templates/javascript/hasura-event-triggered/.netlify-function-template.mjs
similarity index 90%
rename from src/functions-templates/javascript/hasura-event-triggered/.netlify-function-template.cjs
rename to src/functions-templates/javascript/hasura-event-triggered/.netlify-function-template.mjs
index 76e4ada8..660d48c4 100644
--- a/src/functions-templates/javascript/hasura-event-triggered/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/hasura-event-triggered/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'hasura-event-triggered',
   description: 'Hasura Cleaning: process a Hasura event and fire off a GraphQL mutation with processed text data',
   functionType: 'serverless',
diff --git a/src/functions-templates/typescript/hello-world/.netlify-function-template.cjs b/src/functions-templates/javascript/hello-world/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/typescript/hello-world/.netlify-function-template.cjs
rename to src/functions-templates/javascript/hello-world/.netlify-function-template.mjs
index 551879c8..f5663b4c 100644
--- a/src/functions-templates/typescript/hello-world/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/hello-world/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'hello-world',
   priority: 1,
   description: 'Basic function that shows async/await usage, and response formatting',
diff --git a/src/functions-templates/javascript/hello/.netlify-function-template.cjs b/src/functions-templates/javascript/hello/.netlify-function-template.mjs
similarity index 87%
rename from src/functions-templates/javascript/hello/.netlify-function-template.cjs
rename to src/functions-templates/javascript/hello/.netlify-function-template.mjs
index 5bef5a70..a64407a5 100644
--- a/src/functions-templates/javascript/hello/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/hello/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'hello',
   description: 'Basic function that shows async/await usage, and response formatting',
   functionType: 'edge',
diff --git a/src/functions-templates/javascript/identity-signup/.netlify-function-template.cjs b/src/functions-templates/javascript/identity-signup/.netlify-function-template.mjs
similarity index 90%
rename from src/functions-templates/javascript/identity-signup/.netlify-function-template.cjs
rename to src/functions-templates/javascript/identity-signup/.netlify-function-template.mjs
index 9e0a7d46..4da0d668 100644
--- a/src/functions-templates/javascript/identity-signup/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/identity-signup/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'identity-signup',
   description: 'Identity Signup: Triggered when a new Netlify Identity user confirms. Assigns roles and extra metadata',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/image-external/.netlify-function-template.cjs b/src/functions-templates/javascript/image-external/.netlify-function-template.mjs
similarity index 86%
rename from src/functions-templates/javascript/image-external/.netlify-function-template.cjs
rename to src/functions-templates/javascript/image-external/.netlify-function-template.mjs
index 1ba2cff6..fe7d838e 100644
--- a/src/functions-templates/javascript/image-external/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/image-external/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'image-external',
   description: 'Fetches and serves an image from an external site',
   functionType: 'edge',
diff --git a/src/functions-templates/javascript/localized-content/.netlify-function-template.cjs b/src/functions-templates/javascript/localized-content/.netlify-function-template.mjs
similarity index 88%
rename from src/functions-templates/javascript/localized-content/.netlify-function-template.cjs
rename to src/functions-templates/javascript/localized-content/.netlify-function-template.mjs
index 5f40da5e..66b7c5fb 100644
--- a/src/functions-templates/javascript/localized-content/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/localized-content/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'localized-content',
   description: 'Uses geolocation data to serve localized countent according to country code',
   functionType: 'edge',
diff --git a/src/functions-templates/javascript/node-fetch/.netlify-function-template.cjs b/src/functions-templates/javascript/node-fetch/.netlify-function-template.mjs
similarity index 88%
rename from src/functions-templates/javascript/node-fetch/.netlify-function-template.cjs
rename to src/functions-templates/javascript/node-fetch/.netlify-function-template.mjs
index eb9386d8..f0b3dbb0 100644
--- a/src/functions-templates/javascript/node-fetch/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/node-fetch/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'node-fetch',
   description: 'Fetch function: uses node-fetch to hit an external API without CORS issues',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/oauth-passport/.netlify-function-template.cjs b/src/functions-templates/javascript/oauth-passport/.netlify-function-template.mjs
similarity index 88%
rename from src/functions-templates/javascript/oauth-passport/.netlify-function-template.cjs
rename to src/functions-templates/javascript/oauth-passport/.netlify-function-template.mjs
index 364fde3d..74578382 100644
--- a/src/functions-templates/javascript/oauth-passport/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/oauth-passport/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'oauth-passport',
   description: 'oauth-passport: template for Oauth workflow using Passport + Express.js',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/protected-function/.netlify-function-template.cjs b/src/functions-templates/javascript/protected-function/.netlify-function-template.mjs
similarity index 87%
rename from src/functions-templates/javascript/protected-function/.netlify-function-template.cjs
rename to src/functions-templates/javascript/protected-function/.netlify-function-template.mjs
index a25e4608..46592dd2 100644
--- a/src/functions-templates/javascript/protected-function/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/protected-function/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'protected-function',
   description: 'Function protected Netlify Identity authentication',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/sanity-create/.netlify-function-template.cjs b/src/functions-templates/javascript/sanity-create/.netlify-function-template.mjs
similarity index 84%
rename from src/functions-templates/javascript/sanity-create/.netlify-function-template.cjs
rename to src/functions-templates/javascript/sanity-create/.netlify-function-template.mjs
index 9bc8cafe..ada348cb 100644
--- a/src/functions-templates/javascript/sanity-create/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/sanity-create/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'sanity-create',
   description: 'Create documents in Sanity.io',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/sanity-groq/.netlify-function-template.cjs b/src/functions-templates/javascript/sanity-groq/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/javascript/sanity-groq/.netlify-function-template.cjs
rename to src/functions-templates/javascript/sanity-groq/.netlify-function-template.mjs
index d63b8b33..d0bba031 100644
--- a/src/functions-templates/javascript/sanity-groq/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/sanity-groq/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'sanity-groq',
   description: 'Query a Sanity.io dataset with GROQ',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/scheduled-function/.netlify-function-template.cjs b/src/functions-templates/javascript/scheduled-function/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/javascript/scheduled-function/.netlify-function-template.cjs
rename to src/functions-templates/javascript/scheduled-function/.netlify-function-template.mjs
index 84eeda7e..749caf89 100644
--- a/src/functions-templates/javascript/scheduled-function/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/scheduled-function/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'scheduled-function',
   priority: 1,
   description: 'Basic implementation of a scheduled function in JavaScript.',
diff --git a/src/functions-templates/javascript/send-email/.netlify-function-template.cjs b/src/functions-templates/javascript/send-email/.netlify-function-template.mjs
similarity index 87%
rename from src/functions-templates/javascript/send-email/.netlify-function-template.cjs
rename to src/functions-templates/javascript/send-email/.netlify-function-template.mjs
index e442444d..474e94b9 100644
--- a/src/functions-templates/javascript/send-email/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/send-email/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'send-email',
   description: "Send Email: Send email with no SMTP server via 'sendmail' pkg",
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/serverless-ssr/.netlify-function-template.cjs b/src/functions-templates/javascript/serverless-ssr/.netlify-function-template.mjs
similarity index 86%
rename from src/functions-templates/javascript/serverless-ssr/.netlify-function-template.cjs
rename to src/functions-templates/javascript/serverless-ssr/.netlify-function-template.mjs
index 43028a4a..2e5dc345 100644
--- a/src/functions-templates/javascript/serverless-ssr/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/serverless-ssr/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'serverless-ssr',
   description: 'Dynamic serverside rendering via functions',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/set-cookie/.netlify-function-template.cjs b/src/functions-templates/javascript/set-cookie/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/javascript/set-cookie/.netlify-function-template.cjs
rename to src/functions-templates/javascript/set-cookie/.netlify-function-template.mjs
index fedf5df7..3326dc0f 100644
--- a/src/functions-templates/javascript/set-cookie/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/set-cookie/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'set-cookie',
   description: 'Set a cookie alongside your function',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/set-cookies/.netlify-function-template.cjs b/src/functions-templates/javascript/set-cookies/.netlify-function-template.mjs
similarity index 83%
rename from src/functions-templates/javascript/set-cookies/.netlify-function-template.cjs
rename to src/functions-templates/javascript/set-cookies/.netlify-function-template.mjs
index 43860bb2..0531b202 100644
--- a/src/functions-templates/javascript/set-cookies/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/set-cookies/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'set-cookies',
   description: 'Create and manage HTTP cookies',
   functionType: 'edge',
diff --git a/src/functions-templates/typescript/set-req-header/.netlify-function-template.cjs b/src/functions-templates/javascript/set-req-header/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/typescript/set-req-header/.netlify-function-template.cjs
rename to src/functions-templates/javascript/set-req-header/.netlify-function-template.mjs
index 38dba0bc..46b570e5 100644
--- a/src/functions-templates/typescript/set-req-header/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/set-req-header/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'set-req-header',
   description: 'Adds a custom HTTP header to HTTP request.',
   functionType: 'edge',
diff --git a/src/functions-templates/javascript/set-res-header/.netlify-function-template.cjs b/src/functions-templates/javascript/set-res-header/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/javascript/set-res-header/.netlify-function-template.cjs
rename to src/functions-templates/javascript/set-res-header/.netlify-function-template.mjs
index 74e4e2fd..7993e562 100644
--- a/src/functions-templates/javascript/set-res-header/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/set-res-header/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'set-res-header',
   description: 'Adds a custom HTTP header to HTTP response.',
   functionType: 'edge',
diff --git a/src/functions-templates/javascript/slack-rate-limit/.netlify-function-template.cjs b/src/functions-templates/javascript/slack-rate-limit/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/javascript/slack-rate-limit/.netlify-function-template.cjs
rename to src/functions-templates/javascript/slack-rate-limit/.netlify-function-template.mjs
index 8c74daba..4a15cc58 100644
--- a/src/functions-templates/javascript/slack-rate-limit/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/slack-rate-limit/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'slack-rate-limit',
   description: 'Slack Rate-limit: post to Slack, at most once an hour, using Netlify Identity metadata',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/stripe-charge/.netlify-function-template.cjs b/src/functions-templates/javascript/stripe-charge/.netlify-function-template.mjs
similarity index 94%
rename from src/functions-templates/javascript/stripe-charge/.netlify-function-template.cjs
rename to src/functions-templates/javascript/stripe-charge/.netlify-function-template.mjs
index b53fe928..93bce423 100644
--- a/src/functions-templates/javascript/stripe-charge/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/stripe-charge/.netlify-function-template.mjs
@@ -1,6 +1,6 @@
-const chalk = require('chalk')
+import chalk from 'chalk'
 
-module.exports = {
+export default {
   name: 'stripe-charge',
   description: 'Stripe Charge: Charge a user with Stripe',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/stripe-subscription/.netlify-function-template.cjs b/src/functions-templates/javascript/stripe-subscription/.netlify-function-template.mjs
similarity index 94%
rename from src/functions-templates/javascript/stripe-subscription/.netlify-function-template.cjs
rename to src/functions-templates/javascript/stripe-subscription/.netlify-function-template.mjs
index 895c1631..213791a2 100644
--- a/src/functions-templates/javascript/stripe-subscription/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/stripe-subscription/.netlify-function-template.mjs
@@ -1,6 +1,6 @@
-const chalk = require('chalk')
+import chalk from 'chalk'
 
-module.exports = {
+export default {
   name: 'stripe-subscription',
   description: 'Stripe subscription: Create a subscription with Stripe',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/submission-created/.netlify-function-template.cjs b/src/functions-templates/javascript/submission-created/.netlify-function-template.mjs
similarity index 90%
rename from src/functions-templates/javascript/submission-created/.netlify-function-template.cjs
rename to src/functions-templates/javascript/submission-created/.netlify-function-template.mjs
index 6d2a910e..b4827451 100644
--- a/src/functions-templates/javascript/submission-created/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/submission-created/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'submission-created',
   description: 'submission-created: template for event triggered function when a new Netlify Form is submitted',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/token-hider/.netlify-function-template.cjs b/src/functions-templates/javascript/token-hider/.netlify-function-template.mjs
similarity index 94%
rename from src/functions-templates/javascript/token-hider/.netlify-function-template.cjs
rename to src/functions-templates/javascript/token-hider/.netlify-function-template.mjs
index f0598f9d..984cc9e4 100644
--- a/src/functions-templates/javascript/token-hider/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/token-hider/.netlify-function-template.mjs
@@ -1,6 +1,6 @@
-const chalk = require('chalk')
+import chalk from 'chalk'
 
-module.exports = {
+export default {
   name: 'token-hider',
   description: 'Token Hider: access APIs without exposing your API keys',
   functionType: 'serverless',
diff --git a/src/functions-templates/typescript/transform-response/.netlify-function-template.cjs b/src/functions-templates/javascript/transform-response/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/typescript/transform-response/.netlify-function-template.cjs
rename to src/functions-templates/javascript/transform-response/.netlify-function-template.mjs
index 9e4592bc..13057c18 100644
--- a/src/functions-templates/typescript/transform-response/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/transform-response/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'transform-response',
   description: 'Transform the content of an HTTP response',
   functionType: 'edge',
diff --git a/src/functions-templates/javascript/url-shortener/.netlify-function-template.cjs b/src/functions-templates/javascript/url-shortener/.netlify-function-template.mjs
similarity index 94%
rename from src/functions-templates/javascript/url-shortener/.netlify-function-template.cjs
rename to src/functions-templates/javascript/url-shortener/.netlify-function-template.mjs
index 3d17ca99..f8029d9e 100644
--- a/src/functions-templates/javascript/url-shortener/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/url-shortener/.netlify-function-template.mjs
@@ -1,6 +1,6 @@
-const chalk = require('chalk')
+import chalk from 'chalk'
 
-module.exports = {
+export default {
   name: 'url-shortener',
   description: 'URL Shortener: simple URL shortener with Netlify Forms!',
   functionType: 'serverless',
diff --git a/src/functions-templates/javascript/using-middleware/.netlify-function-template.cjs b/src/functions-templates/javascript/using-middleware/.netlify-function-template.mjs
similarity index 84%
rename from src/functions-templates/javascript/using-middleware/.netlify-function-template.cjs
rename to src/functions-templates/javascript/using-middleware/.netlify-function-template.mjs
index 7fa3d5bc..4875ce06 100644
--- a/src/functions-templates/javascript/using-middleware/.netlify-function-template.cjs
+++ b/src/functions-templates/javascript/using-middleware/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'using-middleware',
   description: 'Using Middleware with middy',
   functionType: 'serverless',
diff --git a/src/functions-templates/rust/hello-world/.netlify-function-template.cjs b/src/functions-templates/rust/hello-world/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/rust/hello-world/.netlify-function-template.cjs
rename to src/functions-templates/rust/hello-world/.netlify-function-template.mjs
index 60abc368..b3024a6e 100644
--- a/src/functions-templates/rust/hello-world/.netlify-function-template.cjs
+++ b/src/functions-templates/rust/hello-world/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'hello-world',
   priority: 1,
   description: 'Basic function that shows how to create a handler and return a response',
diff --git a/src/functions-templates/typescript/geolocation/.netlify-function-template.cjs b/src/functions-templates/typescript/geolocation/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/typescript/geolocation/.netlify-function-template.cjs
rename to src/functions-templates/typescript/geolocation/.netlify-function-template.mjs
index 735a861f..a48cdd3b 100644
--- a/src/functions-templates/typescript/geolocation/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/geolocation/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'geolocation',
   description: "Returns info about user's geolocation, which can be used to serve location-specific content.",
   functionType: 'edge',
diff --git a/src/functions-templates/javascript/hello-world/.netlify-function-template.cjs b/src/functions-templates/typescript/hello-world/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/javascript/hello-world/.netlify-function-template.cjs
rename to src/functions-templates/typescript/hello-world/.netlify-function-template.mjs
index 551879c8..f5663b4c 100644
--- a/src/functions-templates/javascript/hello-world/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/hello-world/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'hello-world',
   priority: 1,
   description: 'Basic function that shows async/await usage, and response formatting',
diff --git a/src/functions-templates/typescript/json/.netlify-function-template.cjs b/src/functions-templates/typescript/json/.netlify-function-template.mjs
similarity index 84%
rename from src/functions-templates/typescript/json/.netlify-function-template.cjs
rename to src/functions-templates/typescript/json/.netlify-function-template.mjs
index 0ffcfa46..2194476e 100644
--- a/src/functions-templates/typescript/json/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/json/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'json',
   description: 'Function that returns a simple json response',
   functionType: 'edge',
diff --git a/src/functions-templates/typescript/log/.netlify-function-template.cjs b/src/functions-templates/typescript/log/.netlify-function-template.mjs
similarity index 84%
rename from src/functions-templates/typescript/log/.netlify-function-template.cjs
rename to src/functions-templates/typescript/log/.netlify-function-template.mjs
index 79098fc1..5d08eaf1 100644
--- a/src/functions-templates/typescript/log/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/log/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'log',
   description: 'Basic function logging context of request',
   functionType: 'edge',
diff --git a/src/functions-templates/typescript/scheduled-function/.netlify-function-template.cjs b/src/functions-templates/typescript/scheduled-function/.netlify-function-template.mjs
similarity index 89%
rename from src/functions-templates/typescript/scheduled-function/.netlify-function-template.cjs
rename to src/functions-templates/typescript/scheduled-function/.netlify-function-template.mjs
index 162add4a..dede74c1 100644
--- a/src/functions-templates/typescript/scheduled-function/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/scheduled-function/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'scheduled-function',
   priority: 1,
   description: 'Basic implementation of a scheduled function in TypeScript.',
diff --git a/src/functions-templates/typescript/set-cookies/.netlify-function-template.cjs b/src/functions-templates/typescript/set-cookies/.netlify-function-template.mjs
similarity index 83%
rename from src/functions-templates/typescript/set-cookies/.netlify-function-template.cjs
rename to src/functions-templates/typescript/set-cookies/.netlify-function-template.mjs
index 43860bb2..0531b202 100644
--- a/src/functions-templates/typescript/set-cookies/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/set-cookies/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'set-cookies',
   description: 'Create and manage HTTP cookies',
   functionType: 'edge',
diff --git a/src/functions-templates/javascript/set-req-header/.netlify-function-template.cjs b/src/functions-templates/typescript/set-req-header/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/javascript/set-req-header/.netlify-function-template.cjs
rename to src/functions-templates/typescript/set-req-header/.netlify-function-template.mjs
index 38dba0bc..46b570e5 100644
--- a/src/functions-templates/javascript/set-req-header/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/set-req-header/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'set-req-header',
   description: 'Adds a custom HTTP header to HTTP request.',
   functionType: 'edge',
diff --git a/src/functions-templates/typescript/set-res-header/.netlify-function-template.cjs b/src/functions-templates/typescript/set-res-header/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/typescript/set-res-header/.netlify-function-template.cjs
rename to src/functions-templates/typescript/set-res-header/.netlify-function-template.mjs
index 74e4e2fd..7993e562 100644
--- a/src/functions-templates/typescript/set-res-header/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/set-res-header/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'set-res-header',
   description: 'Adds a custom HTTP header to HTTP response.',
   functionType: 'edge',
diff --git a/src/functions-templates/javascript/transform-response/.netlify-function-template.cjs b/src/functions-templates/typescript/transform-response/.netlify-function-template.mjs
similarity index 85%
rename from src/functions-templates/javascript/transform-response/.netlify-function-template.cjs
rename to src/functions-templates/typescript/transform-response/.netlify-function-template.mjs
index 9e4592bc..13057c18 100644
--- a/src/functions-templates/javascript/transform-response/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/transform-response/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'transform-response',
   description: 'Transform the content of an HTTP response',
   functionType: 'edge',
diff --git a/src/lib/completion/constants.mjs b/src/lib/completion/constants.mjs
index fa66308f..b29329fa 100644
--- a/src/lib/completion/constants.mjs
+++ b/src/lib/completion/constants.mjs
@@ -1,4 +1,4 @@
 // @ts-check
-import { getPathInHome } from '../settings.cjs'
+import { getPathInHome } from '../settings.mjs'
 
 export const AUTOCOMPLETION_FILE = getPathInHome(['autocompletion.json'])
diff --git a/src/lib/edge-functions/deploy.mjs b/src/lib/edge-functions/deploy.mjs
index 947c7837..e020609f 100644
--- a/src/lib/edge-functions/deploy.mjs
+++ b/src/lib/edge-functions/deploy.mjs
@@ -2,7 +2,7 @@
 import { stat } from 'fs/promises'
 import { join } from 'path'
 
-import { getPathInProject } from '../settings.cjs'
+import { getPathInProject } from '../settings.mjs'
 
 import { EDGE_FUNCTIONS_FOLDER, PUBLIC_URL_PATH } from './consts.mjs'
 
diff --git a/src/lib/edge-functions/internal.mjs b/src/lib/edge-functions/internal.mjs
index f8333662..d385aa20 100644
--- a/src/lib/edge-functions/internal.mjs
+++ b/src/lib/edge-functions/internal.mjs
@@ -3,7 +3,7 @@ import { readFile, stat } from 'fs/promises'
 import { dirname, join, resolve } from 'path'
 import { cwd } from 'process'
 
-import { getPathInProject } from '../settings.cjs'
+import { getPathInProject } from '../settings.mjs'
 
 import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from './consts.mjs'
 
diff --git a/src/lib/edge-functions/proxy.mjs b/src/lib/edge-functions/proxy.mjs
index 478b87eb..eb19c174 100644
--- a/src/lib/edge-functions/proxy.mjs
+++ b/src/lib/edge-functions/proxy.mjs
@@ -8,8 +8,8 @@ import { v4 as generateUUID } from 'uuid'
 
 import { NETLIFYDEVERR, NETLIFYDEVWARN, chalk, error as printError, log } from '../../utils/command-helpers.mjs'
 import { getGeoLocation } from '../geo-location.mjs'
-import { getPathInProject } from '../settings.cjs'
-import { startSpinner, stopSpinner } from '../spinner.cjs'
+import { getPathInProject } from '../settings.mjs'
+import { startSpinner, stopSpinner } from '../spinner.mjs'
 
 import { DIST_IMPORT_MAP_PATH } from './consts.mjs'
 import headers from './headers.mjs'
diff --git a/src/lib/fs.cjs b/src/lib/fs.mjs
similarity index 70%
rename from src/lib/fs.cjs
rename to src/lib/fs.mjs
index 5fb37ccc..908f5a1d 100644
--- a/src/lib/fs.cjs
+++ b/src/lib/fs.mjs
@@ -1,10 +1,8 @@
 // @ts-check
-const {
-  constants,
-  promises: { access, stat },
-} = require('fs')
+import { constants } from 'fs'
+import { access, stat } from 'fs/promises'
 
-const fileExistsAsync = async (filePath) => {
+export const fileExistsAsync = async (filePath) => {
   try {
     await access(filePath, constants.F_OK)
     return true
@@ -36,16 +34,10 @@ const isType = async (filePath, type) => {
  * Checks if the provided filePath is a file
  * @param {string} filePath
  */
-const isFileAsync = (filePath) => isType(filePath, 'isFile')
+export const isFileAsync = (filePath) => isType(filePath, 'isFile')
 
 /**
  * Checks if the provided filePath is a directory
  * @param {string} filePath
  */
-const isDirectoryAsync = (filePath) => isType(filePath, 'isDirectory')
-
-module.exports = {
-  fileExistsAsync,
-  isDirectoryAsync,
-  isFileAsync,
-}
+export const isDirectoryAsync = (filePath) => isType(filePath, 'isDirectory')
diff --git a/src/lib/functions/runtimes/js/builders/netlify-lambda.mjs b/src/lib/functions/runtimes/js/builders/netlify-lambda.mjs
index 8a2c3870..9b7e44d0 100644
--- a/src/lib/functions/runtimes/js/builders/netlify-lambda.mjs
+++ b/src/lib/functions/runtimes/js/builders/netlify-lambda.mjs
@@ -5,7 +5,7 @@ import { resolve } from 'path'
 import minimist from 'minimist'
 
 import execa from '../../../../../utils/execa.mjs'
-import { fileExistsAsync } from '../../../../fs.cjs'
+import { fileExistsAsync } from '../../../../fs.mjs'
 import { memoizedBuild } from '../../../memoized-build.mjs'
 
 export const detectNetlifyLambda = async function ({ packageJson } = {}) {
diff --git a/src/lib/functions/runtimes/js/builders/zisi.mjs b/src/lib/functions/runtimes/js/builders/zisi.mjs
index 39b46307..61da3908 100644
--- a/src/lib/functions/runtimes/js/builders/zisi.mjs
+++ b/src/lib/functions/runtimes/js/builders/zisi.mjs
@@ -7,7 +7,7 @@ import readPkgUp from 'read-pkg-up'
 import sourceMapSupport from 'source-map-support'
 
 import { NETLIFYDEVERR } from '../../../../../utils/command-helpers.mjs'
-import { getPathInProject } from '../../../../settings.cjs'
+import { getPathInProject } from '../../../../settings.mjs'
 import { normalizeFunctionsConfig } from '../../../config.mjs'
 import { memoizedBuild } from '../../../memoized-build.mjs'
 
diff --git a/src/lib/functions/runtimes/rust/index.mjs b/src/lib/functions/runtimes/rust/index.mjs
index cc3c5cb4..bac12860 100644
--- a/src/lib/functions/runtimes/rust/index.mjs
+++ b/src/lib/functions/runtimes/rust/index.mjs
@@ -7,7 +7,7 @@ import findUp from 'find-up'
 import toml from 'toml'
 
 import execa from '../../../../utils/execa.mjs'
-import { getPathInProject } from '../../../settings.cjs'
+import { getPathInProject } from '../../../settings.mjs'
 import { runFunctionsProxy } from '../../local-proxy.mjs'
 
 const isWindows = platform === 'win32'
diff --git a/src/lib/settings.cjs b/src/lib/settings.mjs
similarity index 70%
rename from src/lib/settings.cjs
rename to src/lib/settings.mjs
index d910052e..56b44907 100644
--- a/src/lib/settings.cjs
+++ b/src/lib/settings.mjs
@@ -1,7 +1,7 @@
-const os = require('os')
-const path = require('path')
+import os from 'os'
+import path from 'path'
 
-const envPaths = require('env-paths')
+import envPaths from 'env-paths'
 
 const OSBasedPaths = envPaths('netlify', { suffix: '' })
 const NETLIFY_HOME = '.netlify'
@@ -12,7 +12,7 @@ const NETLIFY_HOME = '.netlify'
  * @param {string[]} paths
  * @returns {string}
  */
-const getLegacyPathInHome = (paths) => {
+export const getLegacyPathInHome = (paths) => {
   const pathInHome = path.join(os.homedir(), NETLIFY_HOME, ...paths)
   return pathInHome
 }
@@ -22,7 +22,7 @@ const getLegacyPathInHome = (paths) => {
  * @param {string[]} paths
  * @returns {string}
  */
-const getPathInHome = (paths) => {
+export const getPathInHome = (paths) => {
   const pathInHome = path.join(OSBasedPaths.config, ...paths)
   return pathInHome
 }
@@ -32,9 +32,7 @@ const getPathInHome = (paths) => {
  * @param {string[]} paths
  * @returns {string}
  */
-const getPathInProject = (paths) => {
+export const getPathInProject = (paths) => {
   const pathInProject = path.join(NETLIFY_HOME, ...paths)
   return pathInProject
 }
-
-module.exports = { getLegacyPathInHome, getPathInHome, getPathInProject }
diff --git a/src/lib/spinner.cjs b/src/lib/spinner.mjs
similarity index 74%
rename from src/lib/spinner.cjs
rename to src/lib/spinner.mjs
index 3ac51197..0920c61e 100644
--- a/src/lib/spinner.cjs
+++ b/src/lib/spinner.mjs
@@ -1,6 +1,6 @@
 // @ts-check
-const logSymbols = require('log-symbols')
-const ora = require('ora')
+import logSymbols from 'log-symbols'
+import ora from 'ora'
 
 /**
  * Creates a spinner with the following text
@@ -8,7 +8,7 @@ const ora = require('ora')
  * @param {string} config.text
  * @returns {ora.Ora}
  */
-const startSpinner = ({ text }) =>
+export const startSpinner = ({ text }) =>
   ora({
     text,
   }).start()
@@ -21,7 +21,7 @@ const startSpinner = ({ text }) =>
  * @param {string} [config.text]
  * @returns {void}
  */
-const stopSpinner = ({ error, spinner, text }) => {
+export const stopSpinner = ({ error, spinner, text }) => {
   if (!spinner) {
     return
   }
@@ -39,10 +39,8 @@ const stopSpinner = ({ error, spinner, text }) => {
  * @param {ora.Ora} config.spinner
  * @returns {void}
  */
-const clearSpinner = ({ spinner }) => {
+export const clearSpinner = ({ spinner }) => {
   if (spinner) {
     spinner.stop()
   }
 }
-
-module.exports = { clearSpinner, startSpinner, stopSpinner }
diff --git a/src/utils/command-helpers.mjs b/src/utils/command-helpers.mjs
index db0cbe9b..7629da31 100644
--- a/src/utils/command-helpers.mjs
+++ b/src/utils/command-helpers.mjs
@@ -12,7 +12,7 @@ import WSL from 'is-wsl'
 import debounce from 'lodash/debounce.js'
 import terminalLink from 'terminal-link'
 
-import { clearSpinner, startSpinner } from '../lib/spinner.cjs'
+import { clearSpinner, startSpinner } from '../lib/spinner.mjs'
 
 import getGlobalConfig from './get-global-config.mjs'
 import getPackageJson from './get-package-json.mjs'
diff --git a/src/utils/dot-env.mjs b/src/utils/dot-env.mjs
index 0f31df01..87bffd4a 100644
--- a/src/utils/dot-env.mjs
+++ b/src/utils/dot-env.mjs
@@ -4,7 +4,7 @@ import path from 'path'
 
 import dotenv from 'dotenv'
 
-import { isFileAsync } from '../lib/fs.cjs'
+import { isFileAsync } from '../lib/fs.mjs'
 
 import { warn } from './command-helpers.mjs'
 
diff --git a/src/utils/functions/functions.mjs b/src/utils/functions/functions.mjs
index e9f2f85a..c24381f4 100644
--- a/src/utils/functions/functions.mjs
+++ b/src/utils/functions/functions.mjs
@@ -1,8 +1,8 @@
 // @ts-check
 import { resolve } from 'path'
 
-import { isDirectoryAsync, isFileAsync } from '../../lib/fs.cjs'
-import { getPathInProject } from '../../lib/settings.cjs'
+import { isDirectoryAsync, isFileAsync } from '../../lib/fs.mjs'
+import { getPathInProject } from '../../lib/settings.mjs'
 
 /**
  * retrieves the function directory out of the flags or config
diff --git a/src/utils/functions/get-functions.mjs b/src/utils/functions/get-functions.mjs
index fb743195..6128ec39 100644
--- a/src/utils/functions/get-functions.mjs
+++ b/src/utils/functions/get-functions.mjs
@@ -1,5 +1,5 @@
 // @ts-check
-import { fileExistsAsync } from '../../lib/fs.cjs'
+import { fileExistsAsync } from '../../lib/fs.mjs'
 
 const getUrlPath = (functionName) => `/.netlify/functions/${functionName}`
 
diff --git a/src/utils/get-global-config.mjs b/src/utils/get-global-config.mjs
index 9d545110..1a797f57 100644
--- a/src/utils/get-global-config.mjs
+++ b/src/utils/get-global-config.mjs
@@ -3,7 +3,7 @@ import { readFile } from 'fs/promises'
 import Configstore from 'configstore'
 import { v4 as uuidv4 } from 'uuid'
 
-import { getLegacyPathInHome, getPathInHome } from '../lib/settings.cjs'
+import { getLegacyPathInHome, getPathInHome } from '../lib/settings.mjs'
 
 const globalConfigDefaults = {
   /* disable stats from being sent to Netlify */
diff --git a/src/utils/gitignore.mjs b/src/utils/gitignore.mjs
index edc12a97..e9bf7e44 100644
--- a/src/utils/gitignore.mjs
+++ b/src/utils/gitignore.mjs
@@ -4,7 +4,7 @@ import path from 'path'
 
 import parseIgnore from 'parse-gitignore'
 
-import { fileExistsAsync } from '../lib/fs.cjs'
+import { fileExistsAsync } from '../lib/fs.mjs'
 
 import { log } from './command-helpers.mjs'
 
diff --git a/src/utils/init/utils.mjs b/src/utils/init/utils.mjs
index 5b100566..42d2d7ca 100644
--- a/src/utils/init/utils.mjs
+++ b/src/utils/init/utils.mjs
@@ -6,7 +6,7 @@ import process from 'process'
 import cleanDeep from 'clean-deep'
 import inquirer from 'inquirer'
 
-import { fileExistsAsync } from '../../lib/fs.cjs'
+import { fileExistsAsync } from '../../lib/fs.mjs'
 import { normalizeBackslash } from '../../lib/path.mjs'
 import { chalk, error as failAndExit, log, warn } from '../command-helpers.mjs'
 
diff --git a/src/utils/live-tunnel.mjs b/src/utils/live-tunnel.mjs
index 9b79b5b2..30cb787c 100644
--- a/src/utils/live-tunnel.mjs
+++ b/src/utils/live-tunnel.mjs
@@ -5,7 +5,7 @@ import fetch from 'node-fetch'
 import pWaitFor from 'p-wait-for'
 
 import { fetchLatestVersion, shouldFetchLatestVersion } from '../lib/exec-fetcher.mjs'
-import { getPathInHome } from '../lib/settings.cjs'
+import { getPathInHome } from '../lib/settings.mjs'
 
 import { NETLIFYDEVERR, NETLIFYDEVLOG, chalk, log } from './command-helpers.mjs'
 import execa from './execa.mjs'
diff --git a/src/utils/lm/install.mjs b/src/utils/lm/install.mjs
index 91bf51ca..83da17da 100644
--- a/src/utils/lm/install.mjs
+++ b/src/utils/lm/install.mjs
@@ -11,9 +11,9 @@ import Listr from 'listr'
 import pathKey from 'path-key'
 
 import { fetchLatestVersion, shouldFetchLatestVersion } from '../../lib/exec-fetcher.mjs'
-import { fileExistsAsync } from '../../lib/fs.cjs'
+import { fileExistsAsync } from '../../lib/fs.mjs'
 import { normalizeBackslash } from '../../lib/path.mjs'
-import { getLegacyPathInHome, getPathInHome } from '../../lib/settings.cjs'
+import { getLegacyPathInHome, getPathInHome } from '../../lib/settings.mjs'
 import { chalk } from '../command-helpers.mjs'
 
 import { checkGitLFSVersionStep, checkGitVersionStep, checkLFSFiltersStep } from './steps.mjs'
diff --git a/src/utils/proxy.mjs b/src/utils/proxy.mjs
index fec0a9c2..61989420 100644
--- a/src/utils/proxy.mjs
+++ b/src/utils/proxy.mjs
@@ -25,7 +25,7 @@ import {
   initializeProxy as initializeEdgeFunctionsProxy,
   isEdgeFunctionsRequest,
 } from '../lib/edge-functions/proxy.mjs'
-import { fileExistsAsync, isFileAsync } from '../lib/fs.cjs'
+import { fileExistsAsync, isFileAsync } from '../lib/fs.mjs'
 import renderErrorTemplate from '../lib/render-error-template.mjs'
 
 import { NETLIFYDEVLOG, NETLIFYDEVWARN } from './command-helpers.mjs'
diff --git a/src/utils/rules-proxy.mjs b/src/utils/rules-proxy.mjs
index 7b7a6bf0..7752f6ea 100644
--- a/src/utils/rules-proxy.mjs
+++ b/src/utils/rules-proxy.mjs
@@ -6,7 +6,7 @@ import cookie from 'cookie'
 import redirector from 'netlify-redirector'
 import pFilter from 'p-filter'
 
-import { fileExistsAsync } from '../lib/fs.cjs'
+import { fileExistsAsync } from '../lib/fs.mjs'
 
 import { NETLIFYDEVLOG } from './command-helpers.mjs'
 import { parseRedirects } from './redirects.mjs'
diff --git a/src/utils/state-config.mjs b/src/utils/state-config.mjs
index 6b5eafed..8ab91cd6 100644
--- a/src/utils/state-config.mjs
+++ b/src/utils/state-config.mjs
@@ -6,7 +6,7 @@ import dotProp from 'dot-prop'
 import findUp from 'find-up'
 import writeFileAtomic from 'write-file-atomic'
 
-import { getPathInProject } from '../lib/settings.cjs'
+import { getPathInProject } from '../lib/settings.mjs'
 
 const STATE_PATH = getPathInProject(['state.json'])
 const permissionError = "You don't have access to this file."


Test patch:
diff --git a/src/functions-templates/typescript/abtest/.netlify-function-template.cjs b/src/functions-templates/typescript/abtest/.netlify-function-template.mjs
similarity index 88%
rename from src/functions-templates/typescript/abtest/.netlify-function-template.cjs
rename to src/functions-templates/typescript/abtest/.netlify-function-template.mjs
index 61ad0413..2be0f0c1 100644
--- a/src/functions-templates/typescript/abtest/.netlify-function-template.cjs
+++ b/src/functions-templates/typescript/abtest/.netlify-function-template.mjs
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   name: 'abtest',
   description: "Function that randomly assigns users to group 'a' or 'b' and sets this value as a cookie.",
   functionType: 'edge',
diff --git a/tests/integration/20.command.functions.test.cjs b/tests/integration/20.command.functions.test.cjs
index 9b033939..f95cf960 100644
--- a/tests/integration/20.command.functions.test.cjs
+++ b/tests/integration/20.command.functions.test.cjs
@@ -7,582 +7,13 @@ const execa = require('execa')
 const getPort = require('get-port')
 const waitPort = require('wait-port')
 
-const fs = require('../../src/lib/fs.cjs')
-
-const callCli = require('./utils/call-cli.cjs')
 const cliPath = require('./utils/cli-path.cjs')
-const { withDevServer } = require('./utils/dev-server.cjs')
 const got = require('./utils/got.cjs')
-const { CONFIRM, DOWN, answerWithValue, handleQuestions } = require('./utils/handle-questions.cjs')
-const { getCLIOptions, withMockApi } = require('./utils/mock-api.cjs')
-const { pause } = require('./utils/pause.cjs')
 const { killProcess } = require('./utils/process.cjs')
 const { withSiteBuilder } = require('./utils/site-builder.cjs')
 
 const test = isCI ? avaTest.serial.bind(avaTest) : avaTest
 
-test('should return function response when invoked with no identity argument', async (t) => {
-  await withSiteBuilder('function-invoke-with-no-identity-argument', async (builder) => {
-    builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
-      path: 'test-invoke.js',
-      handler: async () => ({
-        statusCode: 200,
-        body: 'success',
-      }),
-    })
-
-    await builder.buildAsync()
-
-    await withDevServer({ cwd: builder.directory }, async (server) => {
-      const stdout = await callCli(['functions:invoke', 'test-invoke', `--port=${server.port}`], {
-        cwd: builder.directory,
-      })
-      t.is(stdout, 'success')
-    })
-  })
-})
-
-test('should return function response when invoked', async (t) => {
-  await withSiteBuilder('site-with-ping-function', async (builder) => {
-    builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
-      path: 'ping.js',
-      handler: async () => ({
-        statusCode: 200,
-        body: 'ping',
-      }),
-    })
-
-    await builder.buildAsync()
-
-    await withDevServer({ cwd: builder.directory }, async (server) => {
-      const stdout = await callCli(['functions:invoke', 'ping', '--identity', `--port=${server.port}`], {
-        cwd: builder.directory,
-      })
-      t.is(stdout, 'ping')
-    })
-  })
-})
-
-test('should create a new function directory when none is found', async (t) => {
-  const siteInfo = {
-    admin_url: 'https://app.netlify.com/sites/site-name/overview',
-    ssl_url: 'https://site-name.netlify.app/',
-    id: 'site_id',
-    name: 'site-name',
-    build_settings: { repo_url: 'https://github.com/owner/repo' },
-  }
-
-  const routes = [
-    {
-      path: 'accounts',
-      response: [{ slug: 'test-account' }],
-    },
-    { path: 'sites/site_id/service-instances', response: [] },
-    { path: 'sites/site_id', response: siteInfo },
-    {
-      path: 'sites',
-      response: [siteInfo],
-    },
-    { path: 'sites/site_id', method: 'patch', response: {} },
-  ]
-
-  await withSiteBuilder('site-with-no-functions-dir', async (builder) => {
-    await builder.buildAsync()
-
-    const createFunctionQuestions = [
-      {
-        question: "Select the type of function you'd like to create",
-        answer: answerWithValue(`${DOWN}${CONFIRM}`),
-      },
-      {
-        question: 'Enter the path, relative to your site',
-        answer: answerWithValue('test/functions'),
-      },
-      {
-        question: 'Select the language of your function',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Pick a template',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Name your function',
-        answer: answerWithValue(CONFIRM),
-      },
-    ]
-
-    await withMockApi(routes, async ({ apiUrl }) => {
-      const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
-
-      handleQuestions(childProcess, createFunctionQuestions)
-
-      await childProcess
-
-      t.is(await fs.fileExistsAsync(`${builder.directory}/test/functions/hello-world/hello-world.js`), true)
-    })
-  })
-})
-
-test('should create a new edge function directory when none is found', async (t) => {
-  const siteInfo = {
-    admin_url: 'https://app.netlify.com/sites/site-name/overview',
-    ssl_url: 'https://site-name.netlify.app/',
-    id: 'site_id',
-    name: 'site-name',
-    build_settings: { repo_url: 'https://github.com/owner/repo' },
-  }
-
-  const routes = [
-    {
-      path: 'accounts',
-      response: [{ slug: 'test-account' }],
-    },
-    { path: 'sites/site_id/service-instances', response: [] },
-    { path: 'sites/site_id', response: siteInfo },
-    {
-      path: 'sites',
-      response: [siteInfo],
-    },
-    { path: 'sites/site_id', method: 'patch', response: {} },
-  ]
-
-  await withSiteBuilder('site-with-no-functions-dir', async (builder) => {
-    await builder.buildAsync()
-
-    const createFunctionQuestions = [
-      {
-        question: "Select the type of function you'd like to create",
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Select the language of your function',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Pick a template',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Name your function',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'What route do you want your edge function to be invoked on?',
-        answer: answerWithValue('/test'),
-      },
-    ]
-
-    await withMockApi(routes, async ({ apiUrl }) => {
-      const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
-
-      handleQuestions(childProcess, createFunctionQuestions)
-
-      await childProcess
-
-      t.is(await fs.fileExistsAsync(`${builder.directory}/netlify/edge-functions/hello/hello.js`), true)
-    })
-  })
-})
-
-test('should use specified edge function directory when found', async (t) => {
-  const siteInfo = {
-    admin_url: 'https://app.netlify.com/sites/site-name/overview',
-    ssl_url: 'https://site-name.netlify.app/',
-    id: 'site_id',
-    name: 'site-name',
-    build_settings: { repo_url: 'https://github.com/owner/repo' },
-  }
-
-  const routes = [
-    {
-      path: 'accounts',
-      response: [{ slug: 'test-account' }],
-    },
-    { path: 'sites/site_id/service-instances', response: [] },
-    { path: 'sites/site_id', response: siteInfo },
-    {
-      path: 'sites',
-      response: [siteInfo],
-    },
-    { path: 'sites/site_id', method: 'patch', response: {} },
-  ]
-
-  await withSiteBuilder('site-with-custom-edge-functions-dir', async (builder) => {
-    builder.withNetlifyToml({ config: { build: { edge_functions: 'somethingEdgy' } } })
-
-    await builder.buildAsync()
-
-    const createFunctionQuestions = [
-      {
-        question: "Select the type of function you'd like to create",
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Select the language of your function',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Pick a template',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Name your function',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'What route do you want your edge function to be invoked on?',
-        answer: answerWithValue('/test'),
-      },
-    ]
-
-    await withMockApi(routes, async ({ apiUrl }) => {
-      const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
-
-      handleQuestions(childProcess, createFunctionQuestions)
-
-      await childProcess
-
-      t.true(await fs.fileExistsAsync(`${builder.directory}/somethingEdgy/hello/hello.js`))
-    })
-  })
-})
-
-test('should install function template dependencies on a site-level `package.json` if one is found', async (t) => {
-  const siteInfo = {
-    admin_url: 'https://app.netlify.com/sites/site-name/overview',
-    ssl_url: 'https://site-name.netlify.app/',
-    id: 'site_id',
-    name: 'site-name',
-    build_settings: { repo_url: 'https://github.com/owner/repo' },
-  }
-
-  const routes = [
-    {
-      path: 'accounts',
-      response: [{ slug: 'test-account' }],
-    },
-    { path: 'sites/site_id/service-instances', response: [] },
-    { path: 'sites/site_id', response: siteInfo },
-    {
-      path: 'sites',
-      response: [siteInfo],
-    },
-    { path: 'sites/site_id', method: 'patch', response: {} },
-  ]
-
-  await withSiteBuilder('site-with-no-functions-dir-with-package-json', async (builder) => {
-    builder.withPackageJson({
-      packageJson: {
-        dependencies: {
-          '@netlify/functions': '^0.1.0',
-        },
-      },
-    })
-
-    await builder.buildAsync()
-
-    const createFunctionQuestions = [
-      {
-        question: "Select the type of function you'd like to create",
-        answer: answerWithValue(`${DOWN}${CONFIRM}`),
-      },
-      {
-        question: 'Enter the path, relative to your site',
-        answer: answerWithValue('test/functions'),
-      },
-      {
-        question: 'Select the language of your function',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Pick a template',
-        answer: answerWithValue(`${DOWN}${DOWN}${CONFIRM}`),
-      },
-      {
-        question: 'Name your function',
-        answer: answerWithValue(CONFIRM),
-      },
-    ]
-
-    await withMockApi(routes, async ({ apiUrl }) => {
-      const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
-
-      handleQuestions(childProcess, createFunctionQuestions)
-
-      await childProcess
-
-      // eslint-disable-next-line import/no-dynamic-require, n/global-require
-      const { dependencies } = require(`${builder.directory}/package.json`)
-
-      // NOTE: Ideally we should be running this test with a specific template,
-      // but `inquirer-autocomplete-prompt` doesn't seem to work with the way
-      // we're mocking prompt responses with `handleQuestions`. Instead, we're
-      // choosing the second template in the list, assuming it's the first one
-      // that contains a `package.json` (currently that's `apollo-graphql`).
-      t.is(await fs.fileExistsAsync(`${builder.directory}/test/functions/apollo-graphql/apollo-graphql.js`), true)
-      t.is(await fs.fileExistsAsync(`${builder.directory}/test/functions/apollo-graphql/package.json`), false)
-      t.is(typeof dependencies['apollo-server-lambda'], 'string')
-
-      t.is(dependencies['@netlify/functions'], '^0.1.0')
-    })
-  })
-})
-
-test('should install function template dependencies in the function sub-directory if no site-level `package.json` is found', async (t) => {
-  const siteInfo = {
-    admin_url: 'https://app.netlify.com/sites/site-name/overview',
-    ssl_url: 'https://site-name.netlify.app/',
-    id: 'site_id',
-    name: 'site-name',
-    build_settings: { repo_url: 'https://github.com/owner/repo' },
-  }
-
-  const routes = [
-    {
-      path: 'accounts',
-      response: [{ slug: 'test-account' }],
-    },
-    { path: 'sites/site_id/service-instances', response: [] },
-    { path: 'sites/site_id', response: siteInfo },
-    {
-      path: 'sites',
-      response: [siteInfo],
-    },
-    { path: 'sites/site_id', method: 'patch', response: {} },
-  ]
-
-  await withSiteBuilder('site-with-no-functions-dir-without-package-json', async (builder) => {
-    await builder.buildAsync()
-
-    const createFunctionQuestions = [
-      {
-        question: "Select the type of function you'd like to create",
-        answer: answerWithValue(`${DOWN}${CONFIRM}`),
-      },
-      {
-        question: 'Enter the path, relative to your site',
-        answer: answerWithValue('test/functions'),
-      },
-      {
-        question: 'Select the language of your function',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Pick a template',
-        answer: answerWithValue(`${DOWN}${DOWN}${CONFIRM}`),
-      },
-      {
-        question: 'Name your function',
-        answer: answerWithValue(CONFIRM),
-      },
-    ]
-
-    await withMockApi(routes, async ({ apiUrl }) => {
-      const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
-
-      handleQuestions(childProcess, createFunctionQuestions)
-
-      await childProcess
-
-      // NOTE: Ideally we should be running this test with a specific template,
-      // but `inquirer-autocomplete-prompt` doesn't seem to work with the way
-      // we're mocking prompt responses with `handleQuestions`. Instead, we're
-      // choosing the second template in the list, assuming it's the first one
-      // that contains a `package.json` (currently that's `apollo-graphql`).
-      t.is(await fs.fileExistsAsync(`${builder.directory}/test/functions/apollo-graphql/apollo-graphql.js`), true)
-      t.is(await fs.fileExistsAsync(`${builder.directory}/test/functions/apollo-graphql/package.json`), true)
-    })
-  })
-})
-
-test('should not create a new function directory when one is found', async (t) => {
-  const siteInfo = {
-    admin_url: 'https://app.netlify.com/sites/site-name/overview',
-    ssl_url: 'https://site-name.netlify.app/',
-    id: 'site_id',
-    name: 'site-name',
-    build_settings: { repo_url: 'https://github.com/owner/repo' },
-  }
-
-  const routes = [
-    {
-      path: 'accounts',
-      response: [{ slug: 'test-account' }],
-    },
-    { path: 'sites/site_id/service-instances', response: [] },
-    { path: 'sites/site_id', response: siteInfo },
-    {
-      path: 'sites',
-      response: [siteInfo],
-    },
-    { path: 'sites/site_id', method: 'patch', response: {} },
-  ]
-
-  await withSiteBuilder('site-with-functions-dir', async (builder) => {
-    builder.withNetlifyToml({ config: { build: { functions: 'functions' } } })
-
-    await builder.buildAsync()
-
-    const createFunctionQuestions = [
-      {
-        question: "Select the type of function you'd like to create",
-        answer: answerWithValue(`${DOWN}${CONFIRM}`),
-      },
-      {
-        question: 'Select the language of your function',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Pick a template',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Name your function',
-        answer: answerWithValue(CONFIRM),
-      },
-    ]
-
-    await withMockApi(routes, async ({ apiUrl }) => {
-      const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
-
-      handleQuestions(childProcess, createFunctionQuestions)
-
-      await childProcess
-
-      t.is(await fs.fileExistsAsync(`${builder.directory}/functions/hello-world/hello-world.js`), true)
-    })
-  })
-})
-
-test('should only show function templates for the language specified via the --language flag, if one is present', async (t) => {
-  const siteInfo = {
-    admin_url: 'https://app.netlify.com/sites/site-name/overview',
-    ssl_url: 'https://site-name.netlify.app/',
-    id: 'site_id',
-    name: 'site-name',
-    build_settings: { repo_url: 'https://github.com/owner/repo' },
-  }
-
-  const routes = [
-    {
-      path: 'accounts',
-      response: [{ slug: 'test-account' }],
-    },
-    { path: 'sites/site_id/service-instances', response: [] },
-    { path: 'sites/site_id', response: siteInfo },
-    {
-      path: 'sites',
-      response: [siteInfo],
-    },
-    { path: 'sites/site_id', method: 'patch', response: {} },
-  ]
-
-  await withSiteBuilder('site-with-no-functions-dir', async (builder) => {
-    await builder.buildAsync()
-
-    const createFunctionQuestions = [
-      {
-        question: "Select the type of function you'd like to create",
-        answer: answerWithValue(`${DOWN}${CONFIRM}`),
-      },
-      {
-        question: 'Enter the path, relative to your site',
-        answer: answerWithValue('test/functions'),
-      },
-      {
-        question: 'Pick a template',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Name your function',
-        answer: answerWithValue(CONFIRM),
-      },
-    ]
-
-    await withMockApi(routes, async ({ apiUrl }) => {
-      const childProcess = execa(
-        cliPath,
-        ['functions:create', '--language', 'javascript'],
-        getCLIOptions({ apiUrl, builder }),
-      )
-
-      handleQuestions(childProcess, createFunctionQuestions)
-
-      await childProcess
-
-      t.is(await fs.fileExistsAsync(`${builder.directory}/test/functions/hello-world/hello-world.js`), true)
-    })
-  })
-})
-
-test('throws an error when the --language flag contains an unsupported value', async (t) => {
-  const siteInfo = {
-    admin_url: 'https://app.netlify.com/sites/site-name/overview',
-    ssl_url: 'https://site-name.netlify.app/',
-    id: 'site_id',
-    name: 'site-name',
-    build_settings: { repo_url: 'https://github.com/owner/repo' },
-  }
-
-  const routes = [
-    {
-      path: 'accounts',
-      response: [{ slug: 'test-account' }],
-    },
-    { path: 'sites/site_id/service-instances', response: [] },
-    { path: 'sites/site_id', response: siteInfo },
-    {
-      path: 'sites',
-      response: [siteInfo],
-    },
-    { path: 'sites/site_id', method: 'patch', response: {} },
-  ]
-
-  await withSiteBuilder('site-with-no-functions-dir', async (builder) => {
-    await builder.buildAsync()
-
-    const createFunctionQuestions = [
-      {
-        question: "Select the type of function you'd like to create",
-        answer: answerWithValue(`${DOWN}${CONFIRM}`),
-      },
-      {
-        question: 'Enter the path, relative to your site',
-        answer: answerWithValue('test/functions'),
-      },
-      {
-        question: 'Pick a template',
-        answer: answerWithValue(CONFIRM),
-      },
-      {
-        question: 'Name your function',
-        answer: answerWithValue(CONFIRM),
-      },
-    ]
-
-    await withMockApi(routes, async ({ apiUrl }) => {
-      const childProcess = execa(
-        cliPath,
-        ['functions:create', '--language', 'coffeescript'],
-        getCLIOptions({ apiUrl, builder }),
-      )
-
-      handleQuestions(childProcess, createFunctionQuestions)
-
-      try {
-        await childProcess
-
-        t.fail()
-      } catch (error) {
-        t.true(error.message.includes('Invalid language: coffeescript'))
-      }
-
-      t.is(await fs.fileExistsAsync(`${builder.directory}/test/functions/hello-world/hello-world.js`), false)
-    })
-  })
-})
-
 const DEFAULT_PORT = 9999
 const SERVE_TIMEOUT = 180_000
 
@@ -675,220 +106,6 @@ test('should use settings from netlify.toml dev', async (t) => {
   })
 })
 
-test('should trigger background function from event', async (t) => {
-  await withSiteBuilder('site-with-ping-function', async (builder) => {
-    await builder
-      .withNetlifyToml({ config: { functions: { directory: 'functions' } } })
-      .withFunction({
-        path: 'identity-validate-background.js',
-        handler: async (event) => ({
-          statusCode: 200,
-          body: JSON.stringify(event.body),
-        }),
-      })
-      .buildAsync()
-
-    await withDevServer({ cwd: builder.directory }, async (server) => {
-      const stdout = await callCli(
-        ['functions:invoke', 'identity-validate-background', '--identity', `--port=${server.port}`],
-        {
-          cwd: builder.directory,
-        },
-      )
-      // background functions always return an empty response
-      t.is(stdout, '')
-    })
-  })
-})
-
-test('should serve helpful tips and tricks', async (t) => {
-  await withSiteBuilder('site-with-isc-ping-function', async (builder) => {
-    await builder
-      .withNetlifyToml({
-        config: { functions: { directory: 'functions' } },
-      })
-      // mocking until https://github.com/netlify/functions/pull/226 landed
-      .withContentFile({
-        path: 'node_modules/@netlify/functions/package.json',
-        content: `{}`,
-      })
-      .withContentFile({
-        path: 'node_modules/@netlify/functions/index.js',
-        content: `
-          module.exports.schedule = (schedule, handler) => handler
-          `,
-      })
-      .withContentFile({
-        path: 'functions/hello-world.js',
-        content: `
-          const { schedule } = require('@netlify/functions')
-
-          module.exports.handler = schedule('@daily', async () => {
-            return {
-              statusCode: 200,
-              body: "hello world"
-            }
-          })
-          `.trim(),
-      })
-      .buildAsync()
-
-    await withDevServer({ cwd: builder.directory }, async (server) => {
-      const plainTextResponse = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
-        throwHttpErrors: false,
-        retry: null,
-      })
-      const youReturnedBodyRegex = /.*Your function returned `body`. Is this an accident\?.*/
-      t.regex(plainTextResponse.body, youReturnedBodyRegex)
-      t.regex(plainTextResponse.body, /.*You performed an HTTP request.*/)
-      t.is(plainTextResponse.statusCode, 200)
-
-      const htmlResponse = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
-        throwHttpErrors: false,
-        retry: null,
-        headers: {
-          accept: 'text/html',
-        },
-      })
-      t.regex(htmlResponse.body, /.*<link.*/)
-      t.is(htmlResponse.statusCode, 200)
-
-      const stdout = await callCli(['functions:invoke', 'hello-world', '--identity', `--port=${server.port}`], {
-        cwd: builder.directory,
-      })
-      t.regex(stdout, youReturnedBodyRegex)
-    })
-  })
-})
-
-test('should emulate next_run for scheduled functions', async (t) => {
-  await withSiteBuilder('site-with-isc-ping-function', async (builder) => {
-    await builder
-      .withNetlifyToml({
-        config: { functions: { directory: 'functions' } },
-      })
-      // mocking until https://github.com/netlify/functions/pull/226 landed
-      .withContentFile({
-        path: 'node_modules/@netlify/functions/package.json',
-        content: `{}`,
-      })
-      .withContentFile({
-        path: 'node_modules/@netlify/functions/index.js',
-        content: `
-          module.exports.schedule = (schedule, handler) => handler
-          `,
-      })
-      .withContentFile({
-        path: 'functions/hello-world.js',
-        content: `
-          const { schedule } = require('@netlify/functions')
-          module.exports.handler = schedule("@daily", async (event) => {
-            const { next_run } = JSON.parse(event.body)
-            return {
-              statusCode: !!next_run ? 200 : 400,
-            }
-          })
-          `.trim(),
-      })
-      .buildAsync()
-
-    await withDevServer({ cwd: builder.directory }, async (server) => {
-      const response = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
-        throwHttpErrors: false,
-        retry: null,
-      })
-
-      t.is(response.statusCode, 200)
-    })
-  })
-})
-
-test('should detect netlify-toml defined scheduled functions', async (t) => {
-  await withSiteBuilder('site-with-netlify-toml-ping-function', async (builder) => {
-    await builder
-      .withNetlifyToml({
-        config: { functions: { directory: 'functions', 'test-1': { schedule: '@daily' } } },
-      })
-      .withFunction({
-        path: 'test-1.js',
-        handler: async () => ({
-          statusCode: 200,
-        }),
-      })
-      .buildAsync()
-
-    await withDevServer({ cwd: builder.directory }, async (server) => {
-      const stdout = await callCli(['functions:invoke', 'test-1', `--port=${server.port}`], {
-        cwd: builder.directory,
-      })
-      t.is(stdout, '')
-    })
-  })
-})
-
-test('should detect file changes to scheduled function', async (t) => {
-  await withSiteBuilder('site-with-isc-ping-function', async (builder) => {
-    await builder
-      .withNetlifyToml({
-        config: { functions: { directory: 'functions' } },
-      })
-      // mocking until https://github.com/netlify/functions/pull/226 landed
-      .withContentFile({
-        path: 'node_modules/@netlify/functions/package.json',
-        content: `{}`,
-      })
-      .withContentFile({
-        path: 'node_modules/@netlify/functions/index.js',
-        content: `
-          module.exports.schedule = (schedule, handler) => handler
-          `,
-      })
-      .withContentFile({
-        path: 'functions/hello-world.js',
-        content: `
-          module.exports.handler = async () => {
-            return {
-              statusCode: 200
-            }
-          }
-          `.trim(),
-      })
-      .buildAsync()
-
-    await withDevServer({ cwd: builder.directory }, async (server) => {
-      const helloWorldBody = () =>
-        got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
-          throwHttpErrors: false,
-          retry: null,
-        }).then((response) => response.body)
-
-      t.is(await helloWorldBody(), '')
-
-      await builder
-        .withContentFile({
-          path: 'functions/hello-world.js',
-          content: `
-          const { schedule } = require('@netlify/functions')
-
-          module.exports.handler = schedule("@daily", async () => {
-            return {
-              statusCode: 200,
-              body: "test"
-            }
-          })
-          `.trim(),
-        })
-        .buildAsync()
-
-      const DETECT_FILE_CHANGE_DELAY = 500
-      await pause(DETECT_FILE_CHANGE_DELAY)
-
-      const warningMessage = await helloWorldBody()
-      t.true(warningMessage.includes('Your function returned `body`'))
-    })
-  })
-})
-
 test('should inject env variables', async (t) => {
   await withSiteBuilder('site-with-env-function', async (builder) => {
     await builder
diff --git a/tests/integration/30.command.lm.test.cjs b/tests/integration/30.command.lm.test.cjs
deleted file mode 100644
index 4fd760ca..00000000
--- a/tests/integration/30.command.lm.test.cjs
+++ /dev/null
@@ -1,106 +0,0 @@
-const { readFile } = require('fs').promises
-const os = require('os')
-const process = require('process')
-
-const test = require('ava')
-const execa = require('execa')
-const ini = require('ini')
-
-const { getPathInHome } = require('../../src/lib/settings.cjs')
-
-const callCli = require('./utils/call-cli.cjs')
-const { getCLIOptions, startMockApi } = require('./utils/mock-api.cjs')
-const { createSiteBuilder } = require('./utils/site-builder.cjs')
-
-test.before(async (t) => {
-  const builder = createSiteBuilder({ siteName: 'site-with-lm' })
-  await builder.buildAsync()
-
-  const siteInfo = {
-    account_slug: 'test-account',
-    id: 'site_id',
-    name: 'site-name',
-    id_domain: 'localhost',
-  }
-  const { server } = await startMockApi({
-    routes: [
-      { path: 'sites/site_id', response: siteInfo },
-      { path: 'sites/site_id/service-instances', response: [] },
-      {
-        path: 'accounts',
-        response: [{ slug: siteInfo.account_slug }],
-      },
-      { method: 'post', path: 'sites/site_id/services/large-media/instances', status: 201 },
-    ],
-  })
-
-  const execOptions = getCLIOptions({ builder, apiUrl: `http://localhost:${server.address().port}/api/v1` })
-  t.context.execOptions = { ...execOptions, env: { ...execOptions.env, SHELL: process.env.SHELL || 'bash' } }
-  t.context.builder = builder
-  t.context.mockApi = server
-
-  await callCli(['lm:uninstall'], t.context.execOptions)
-})
-
-test.serial('netlify lm:info', async (t) => {
-  const cliResponse = await callCli(['lm:info'], t.context.execOptions)
-  t.true(cliResponse.includes('Checking Git version'))
-  t.true(cliResponse.includes('Checking Git LFS version'))
-  t.true(cliResponse.includes('Checking Git LFS filters'))
-  t.true(cliResponse.includes("Checking Netlify's Git Credentials version"))
-})
-
-test.serial('netlify lm:install', async (t) => {
-  const cliResponse = await callCli(['lm:install'], t.context.execOptions)
-  t.true(cliResponse.includes('Checking Git version'))
-  t.true(cliResponse.includes('Checking Git LFS version'))
-  t.true(cliResponse.includes('Checking Git LFS filters'))
-  t.true(cliResponse.includes("Installing Netlify's Git Credential Helper"))
-  t.true(cliResponse.includes("Configuring Git to use Netlify's Git Credential Helper [started]"))
-  t.true(cliResponse.includes("Configuring Git to use Netlify's Git Credential Helper [completed]"))
-
-  // verify git-credential-netlify was added to the PATH
-  if (os.platform() === 'win32') {
-    t.true(cliResponse.includes(`Adding ${getPathInHome(['helper', 'bin'])} to the`))
-    t.true(cliResponse.includes('Netlify Credential Helper for Git was installed successfully.'))
-    // no good way to test that it was added to the PATH on windows so we test it was installed
-    // in the expected location
-    const { stdout } = await execa('git-credential-netlify', ['version'], {
-      cwd: `${os.homedir()}\\AppData\\Roaming\\netlify\\config\\helper\\bin`,
-    })
-    t.true(stdout.startsWith('git-credential-netlify'))
-  } else {
-    t.true(cliResponse.includes('Run this command to use Netlify Large Media in your current shell'))
-    // The source path is always an absolute path so we can match for starting with `/`.
-    // The reasoning behind this regular expression is, that on different shells the border of the box inside the command output
-    // can infer with line breaks and split the source with the path.
-    // https://regex101.com/r/2d5BUn/1
-    //                                       /source[\s\S]+?(\/.+inc)/
-    //                                       /      [\s\S]           / \s matches any whitespace character and \S any non whitespace character
-    //                                       /            +?         / matches at least one character but until the next group
-    //                                       /              (\/.+inc)/ matches any character until `inc` (the path starting with a `\`)
-    const [, sourcePath] = cliResponse.match(/source[\s\S]+?(\/.+inc)/)
-    const { stdout } = await execa.command(`source ${sourcePath} && git-credential-netlify version`, {
-      shell: t.context.execOptions.env.SHELL,
-    })
-    t.true(stdout.startsWith('git-credential-netlify'))
-  }
-})
-
-test.serial('netlify lm:setup', async (t) => {
-  const cliResponse = await callCli(['lm:setup'], t.context.execOptions)
-  t.true(cliResponse.includes('Provisioning Netlify Large Media [started]'))
-  t.true(cliResponse.includes('Provisioning Netlify Large Media [completed]'))
-  t.true(cliResponse.includes('Configuring Git LFS for this site [started]'))
-  t.true(cliResponse.includes('Configuring Git LFS for this site [completed]'))
-
-  const lfsConfig = ini.parse(await readFile(`${t.context.builder.directory}/.lfsconfig`, 'utf8'))
-  t.is(lfsConfig.lfs.url, 'https://localhost/.netlify/large-media')
-})
-
-test.after('cleanup', async (t) => {
-  const { builder, mockApi } = t.context
-  await callCli(['lm:uninstall'], t.context.execOptions)
-  await builder.cleanupAsync()
-  mockApi.close()
-})
diff --git a/tests/integration/30.command.lm.test.mjs b/tests/integration/30.command.lm.test.mjs
new file mode 100644
index 00000000..635a2814
--- /dev/null
+++ b/tests/integration/30.command.lm.test.mjs
@@ -0,0 +1,109 @@
+import { readFile } from 'fs/promises'
+import os from 'os'
+import process from 'process'
+
+import execa from 'execa'
+import ini from 'ini'
+import { afterAll, beforeAll, expect, test } from 'vitest'
+
+import { getPathInHome } from '../../src/lib/settings.mjs'
+
+import callCli from './utils/call-cli.cjs'
+import { getCLIOptions, startMockApi } from './utils/mock-api.cjs'
+import { createSiteBuilder } from './utils/site-builder.cjs'
+
+let execOptions
+let builder
+let mockApi
+beforeAll(async () => {
+  builder = createSiteBuilder({ siteName: 'site-with-lm' })
+  await builder.buildAsync()
+
+  const siteInfo = {
+    account_slug: 'test-account',
+    id: 'site_id',
+    name: 'site-name',
+    id_domain: 'localhost',
+  }
+  const { server } = await startMockApi({
+    routes: [
+      { path: 'sites/site_id', response: siteInfo },
+      { path: 'sites/site_id/service-instances', response: [] },
+      {
+        path: 'accounts',
+        response: [{ slug: siteInfo.account_slug }],
+      },
+      { method: 'post', path: 'sites/site_id/services/large-media/instances', status: 201 },
+    ],
+  })
+
+  const CLIOptions = getCLIOptions({ builder, apiUrl: `http://localhost:${server.address().port}/api/v1` })
+  execOptions = { ...CLIOptions, env: { ...CLIOptions.env, SHELL: process.env.SHELL || 'bash' } }
+  mockApi = server
+
+  await callCli(['lm:uninstall'], execOptions)
+}, 30_000)
+
+afterAll(async () => {
+  await callCli(['lm:uninstall'], execOptions)
+  await builder.cleanupAsync()
+  mockApi.close()
+}, 30_000)
+
+test('netlify lm:info', async () => {
+  const cliResponse = await callCli(['lm:info'], execOptions)
+  expect(cliResponse).toContain('Checking Git version')
+  expect(cliResponse).toContain('Checking Git LFS version')
+  expect(cliResponse).toContain('Checking Git LFS filters')
+  expect(cliResponse).toContain("Checking Netlify's Git Credentials version")
+})
+
+test('netlify lm:install', async () => {
+  const cliResponse = await callCli(['lm:install'], execOptions)
+  expect(cliResponse).toContain('Checking Git version')
+  expect(cliResponse).toContain('Checking Git LFS version')
+  expect(cliResponse).toContain('Checking Git LFS filters')
+  expect(cliResponse).toContain("Installing Netlify's Git Credential Helper")
+  expect(cliResponse).toContain("Configuring Git to use Netlify's Git Credential Helper [started]")
+  expect(cliResponse).toContain("Configuring Git to use Netlify's Git Credential Helper [completed]")
+
+  // verify git-credential-netlify was added to the PATH
+  if (os.platform() === 'win32') {
+    expect(cliResponse).toContain(`Adding ${getPathInHome(['helper', 'bin'])} to the`)
+    expect(cliResponse).toContain('Netlify Credential Helper for Git was installed successfully.')
+    // no good way to test that it was added to the PATH on windows so we test it was installed
+    // in the expected location
+    const { stdout } = await execa('git-credential-netlify', ['version'], {
+      cwd: `${os.homedir()}\\AppData\\Roaming\\netlify\\config\\helper\\bin`,
+    })
+
+    expect(stdout.startsWith('git-credential-netlify')).toBe(true)
+  } else {
+    expect(cliResponse).toContain('Run this command to use Netlify Large Media in your current shell')
+    // The source path is always an absolute path so we can match for starting with `/`.
+    // The reasoning behind this regular expression is, that on different shells the border of the box inside the command output
+    // can infer with line breaks and split the source with the path.
+    // https://regex101.com/r/2d5BUn/1
+    //                                       /source[\s\S]+?(\/.+inc)/
+    //                                       /      [\s\S]           / \s matches any whitespace character and \S any non whitespace character
+    //                                       /            +?         / matches at least one character but until the next group
+    //                                       /              (\/.+inc)/ matches any character until `inc` (the path starting with a `\`)
+    const [, sourcePath] = cliResponse.match(/source[\s\S]+?(\/.+inc)/)
+    const { stdout } = await execa.command(`source ${sourcePath} && git-credential-netlify version`, {
+      shell: execOptions.env.SHELL,
+    })
+
+    expect(stdout.startsWith('git-credential-netlify')).toBe(true)
+  }
+})
+
+test('netlify lm:setup', async () => {
+  const cliResponse = await callCli(['lm:setup'], execOptions)
+  expect(cliResponse).toContain('Provisioning Netlify Large Media [started]')
+  expect(cliResponse).toContain('Provisioning Netlify Large Media [completed]')
+  expect(cliResponse).toContain('Configuring Git LFS for this site [started]')
+  expect(cliResponse).toContain('Configuring Git LFS for this site [completed]')
+
+  const lfsConfig = ini.parse(await readFile(`${builder.directory}/.lfsconfig`, 'utf8'))
+  expect(lfsConfig.lfs.url).toBe('https://localhost/.netlify/large-media')
+})
diff --git a/tests/integration/520.command.link.test.cjs b/tests/integration/520.command.link.test.cjs
deleted file mode 100644
index 40885bda..00000000
--- a/tests/integration/520.command.link.test.cjs
+++ /dev/null
@@ -1,42 +0,0 @@
-const process = require('process')
-
-const test = require('ava')
-
-const { isFileAsync } = require('../../src/lib/fs.cjs')
-
-const callCli = require('./utils/call-cli.cjs')
-const { getCLIOptions, withMockApi } = require('./utils/mock-api.cjs')
-const { withSiteBuilder } = require('./utils/site-builder.cjs')
-
-// TODO: Flaky tests enable once fixed
-/**
- * As some of the tests are flaky on windows machines I will skip them for now
- * @type {import('ava').TestInterface}
- */
-const windowsSkip = process.platform === 'win32' ? test.skip : test
-
-test('should create gitignore in repository root when is root', async (t) => {
-  await withSiteBuilder('repo', async (builder) => {
-    await builder.withGit().buildAsync()
-
-    await withMockApi([], async ({ apiUrl }) => {
-      await callCli(['link'], getCLIOptions({ builder, apiUrl }))
-
-      t.true(await isFileAsync(`${builder.directory}/.gitignore`))
-    })
-  })
-})
-
-windowsSkip('should create gitignore in repository root when cwd is subdirectory', async (t) => {
-  await withSiteBuilder('monorepo', async (builder) => {
-    const projectPath = 'projects/project1'
-    await builder.withGit().withNetlifyToml({ config: {}, pathPrefix: projectPath }).buildAsync()
-
-    await withMockApi([], async ({ apiUrl }) => {
-      const options = getCLIOptions({ builder, apiUrl })
-      await callCli(['link'], { ...options, cwd: `${builder.directory}/${projectPath}` })
-
-      t.true(await isFileAsync(`${builder.directory}/.gitignore`))
-    })
-  })
-})
diff --git a/tests/integration/520.command.link.test.mjs b/tests/integration/520.command.link.test.mjs
new file mode 100644
index 00000000..7172f0da
--- /dev/null
+++ b/tests/integration/520.command.link.test.mjs
@@ -0,0 +1,47 @@
+import { join } from 'path'
+import process from 'process'
+
+import { expect, test } from 'vitest'
+
+import { isFileAsync } from '../../src/lib/fs.mjs'
+
+import callCli from './utils/call-cli.cjs'
+import { getCLIOptions, withMockApi } from './utils/mock-api.cjs'
+import { withSiteBuilder } from './utils/site-builder.cjs'
+
+test('should create gitignore in repository root when is root', async () => {
+  await withSiteBuilder('repo', async (builder) => {
+    await builder.withGit().buildAsync()
+
+    await withMockApi(
+      [],
+      async ({ apiUrl }) => {
+        await callCli(['link'], getCLIOptions({ builder, apiUrl }))
+
+        expect(await isFileAsync(join(builder.directory, '.gitignore'))).toBe(true)
+      },
+      true,
+    )
+  })
+})
+
+test.skipIf(process.platform === 'win32')(
+  'should create gitignore in repository root when cwd is subdirectory',
+  async () => {
+    await withSiteBuilder('monorepo', async (builder) => {
+      const projectPath = join('projects', 'project1')
+      await builder.withGit().withNetlifyToml({ config: {}, pathPrefix: projectPath }).buildAsync()
+
+      await withMockApi(
+        [],
+        async ({ apiUrl }) => {
+          const options = getCLIOptions({ builder, apiUrl })
+          await callCli(['link'], { ...options, cwd: join(builder.directory, projectPath) })
+
+          expect(await isFileAsync(join(builder.directory, '.gitignore'))).toBe(true)
+        },
+        true,
+      )
+    })
+  },
+)
diff --git a/tests/integration/commands/dev/functions.test.mjs b/tests/integration/commands/dev/functions.test.mjs
new file mode 100644
index 00000000..29646877
--- /dev/null
+++ b/tests/integration/commands/dev/functions.test.mjs
@@ -0,0 +1,111 @@
+import { expect, test } from 'vitest'
+
+import { withDevServer } from '../../utils/dev-server.cjs'
+import got from '../../utils/got.cjs'
+import { pause } from '../../utils/pause.cjs'
+import { withSiteBuilder } from '../../utils/site-builder.cjs'
+
+test('should emulate next_run for scheduled functions', async () => {
+  await withSiteBuilder('site-with-isc-ping-function', async (builder) => {
+    await builder
+      .withNetlifyToml({
+        config: { functions: { directory: 'functions' } },
+      })
+      // mocking until https://github.com/netlify/functions/pull/226 landed
+      .withContentFile({
+        path: 'node_modules/@netlify/functions/package.json',
+        content: `{}`,
+      })
+      .withContentFile({
+        path: 'node_modules/@netlify/functions/index.js',
+        content: `
+          module.exports.schedule = (schedule, handler) => handler
+          `,
+      })
+      .withContentFile({
+        path: 'functions/hello-world.js',
+        content: `
+          const { schedule } = require('@netlify/functions')
+          module.exports.handler = schedule("@daily", async (event) => {
+            const { next_run } = JSON.parse(event.body)
+            return {
+              statusCode: !!next_run ? 200 : 400,
+            }
+          })
+          `.trim(),
+      })
+      .buildAsync()
+
+    await withDevServer({ cwd: builder.directory }, async (server) => {
+      const response = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
+        throwHttpErrors: false,
+        retry: null,
+      })
+
+      expect(response.statusCode).toBe(200)
+    })
+  })
+})
+
+test('should detect file changes to scheduled function', async () => {
+  await withSiteBuilder('site-with-isc-ping-function', async (builder) => {
+    await builder
+      .withNetlifyToml({
+        config: { functions: { directory: 'functions' } },
+      })
+      // mocking until https://github.com/netlify/functions/pull/226 landed
+      .withContentFile({
+        path: 'node_modules/@netlify/functions/package.json',
+        content: `{}`,
+      })
+      .withContentFile({
+        path: 'node_modules/@netlify/functions/index.js',
+        content: `
+          module.exports.schedule = (schedule, handler) => handler
+          `,
+      })
+      .withContentFile({
+        path: 'functions/hello-world.js',
+        content: `
+          module.exports.handler = async () => {
+            return {
+              statusCode: 200
+            }
+          }
+          `.trim(),
+      })
+      .buildAsync()
+
+    await withDevServer({ cwd: builder.directory }, async (server) => {
+      const helloWorldBody = () =>
+        got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
+          throwHttpErrors: false,
+          retry: null,
+        }).then((response) => response.body)
+
+      expect(await helloWorldBody()).toBe('')
+
+      await builder
+        .withContentFile({
+          path: 'functions/hello-world.js',
+          content: `
+          const { schedule } = require('@netlify/functions')
+
+          module.exports.handler = schedule("@daily", async () => {
+            return {
+              statusCode: 200,
+              body: "test"
+            }
+          })
+          `.trim(),
+        })
+        .buildAsync()
+
+      const DETECT_FILE_CHANGE_DELAY = 500
+      await pause(DETECT_FILE_CHANGE_DELAY)
+
+      const warningMessage = await helloWorldBody()
+      expect(warningMessage).toContain('Your function returned `body`')
+    })
+  })
+})
diff --git a/tests/integration/commands/functions-create/functions-create.test.mjs b/tests/integration/commands/functions-create/functions-create.test.mjs
new file mode 100644
index 00000000..58190fc5
--- /dev/null
+++ b/tests/integration/commands/functions-create/functions-create.test.mjs
@@ -0,0 +1,525 @@
+import { readFile } from 'fs/promises'
+
+import execa from 'execa'
+import { describe, expect, test } from 'vitest'
+
+import { fileExistsAsync } from '../../../../src/lib/fs.mjs'
+import cliPath from '../../utils/cli-path.cjs'
+import { answerWithValue, CONFIRM, DOWN, handleQuestions } from '../../utils/handle-questions.cjs'
+import { getCLIOptions, withMockApi } from '../../utils/mock-api.cjs'
+import { withSiteBuilder } from '../../utils/site-builder.cjs'
+
+describe('functions:create command', () => {
+  test('should create a new function directory when none is found', async () => {
+    const siteInfo = {
+      admin_url: 'https://app.netlify.com/sites/site-name/overview',
+      ssl_url: 'https://site-name.netlify.app/',
+      id: 'site_id',
+      name: 'site-name',
+      build_settings: { repo_url: 'https://github.com/owner/repo' },
+    }
+
+    const routes = [
+      {
+        path: 'accounts',
+        response: [{ slug: 'test-account' }],
+      },
+      { path: 'sites/site_id/service-instances', response: [] },
+      { path: 'sites/site_id', response: siteInfo },
+      {
+        path: 'sites',
+        response: [siteInfo],
+      },
+      { path: 'sites/site_id', method: 'patch', response: {} },
+    ]
+
+    await withSiteBuilder('site-with-no-functions-dir', async (builder) => {
+      await builder.buildAsync()
+
+      const createFunctionQuestions = [
+        {
+          question: "Select the type of function you'd like to create",
+          answer: answerWithValue(`${DOWN}${CONFIRM}`),
+        },
+        {
+          question: 'Enter the path, relative to your site',
+          answer: answerWithValue('test/functions'),
+        },
+        {
+          question: 'Select the language of your function',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Pick a template',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Name your function',
+          answer: answerWithValue(CONFIRM),
+        },
+      ]
+
+      await withMockApi(routes, async ({ apiUrl }) => {
+        const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
+
+        handleQuestions(childProcess, createFunctionQuestions)
+
+        await childProcess
+
+        expect(await fileExistsAsync(`${builder.directory}/test/functions/hello-world/hello-world.js`)).toBe(true)
+      })
+    })
+  })
+
+  test('should create a new edge function directory when none is found', async () => {
+    const siteInfo = {
+      admin_url: 'https://app.netlify.com/sites/site-name/overview',
+      ssl_url: 'https://site-name.netlify.app/',
+      id: 'site_id',
+      name: 'site-name',
+      build_settings: { repo_url: 'https://github.com/owner/repo' },
+    }
+
+    const routes = [
+      {
+        path: 'accounts',
+        response: [{ slug: 'test-account' }],
+      },
+      { path: 'sites/site_id/service-instances', response: [] },
+      { path: 'sites/site_id', response: siteInfo },
+      {
+        path: 'sites',
+        response: [siteInfo],
+      },
+      { path: 'sites/site_id', method: 'patch', response: {} },
+    ]
+
+    await withSiteBuilder('site-with-no-functions-dir', async (builder) => {
+      await builder.buildAsync()
+
+      const createFunctionQuestions = [
+        {
+          question: "Select the type of function you'd like to create",
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Select the language of your function',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Pick a template',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Name your function',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'What route do you want your edge function to be invoked on?',
+          answer: answerWithValue('/test'),
+        },
+      ]
+
+      await withMockApi(routes, async ({ apiUrl }) => {
+        const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
+
+        handleQuestions(childProcess, createFunctionQuestions)
+
+        await childProcess
+
+        expect(await fileExistsAsync(`${builder.directory}/netlify/edge-functions/hello/hello.js`)).toBe(true)
+      })
+    })
+  })
+
+  test('should use specified edge function directory when found', async () => {
+    const siteInfo = {
+      admin_url: 'https://app.netlify.com/sites/site-name/overview',
+      ssl_url: 'https://site-name.netlify.app/',
+      id: 'site_id',
+      name: 'site-name',
+      build_settings: { repo_url: 'https://github.com/owner/repo' },
+    }
+
+    const routes = [
+      {
+        path: 'accounts',
+        response: [{ slug: 'test-account' }],
+      },
+      { path: 'sites/site_id/service-instances', response: [] },
+      { path: 'sites/site_id', response: siteInfo },
+      {
+        path: 'sites',
+        response: [siteInfo],
+      },
+      { path: 'sites/site_id', method: 'patch', response: {} },
+    ]
+
+    await withSiteBuilder('site-with-custom-edge-functions-dir', async (builder) => {
+      builder.withNetlifyToml({ config: { build: { edge_functions: 'somethingEdgy' } } })
+
+      await builder.buildAsync()
+
+      const createFunctionQuestions = [
+        {
+          question: "Select the type of function you'd like to create",
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Select the language of your function',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Pick a template',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Name your function',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'What route do you want your edge function to be invoked on?',
+          answer: answerWithValue('/test'),
+        },
+      ]
+
+      await withMockApi(routes, async ({ apiUrl }) => {
+        const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
+
+        handleQuestions(childProcess, createFunctionQuestions)
+
+        await childProcess
+
+        expect(await fileExistsAsync(`${builder.directory}/somethingEdgy/hello/hello.js`)).toBe(true)
+      })
+    })
+  })
+
+  test('should install function template dependencies on a site-level `package.json` if one is found', async () => {
+    const siteInfo = {
+      admin_url: 'https://app.netlify.com/sites/site-name/overview',
+      ssl_url: 'https://site-name.netlify.app/',
+      id: 'site_id',
+      name: 'site-name',
+      build_settings: { repo_url: 'https://github.com/owner/repo' },
+    }
+
+    const routes = [
+      {
+        path: 'accounts',
+        response: [{ slug: 'test-account' }],
+      },
+      { path: 'sites/site_id/service-instances', response: [] },
+      { path: 'sites/site_id', response: siteInfo },
+      {
+        path: 'sites',
+        response: [siteInfo],
+      },
+      { path: 'sites/site_id', method: 'patch', response: {} },
+    ]
+
+    await withSiteBuilder('site-with-no-functions-dir-with-package-json', async (builder) => {
+      builder.withPackageJson({
+        packageJson: {
+          dependencies: {
+            '@netlify/functions': '^0.1.0',
+          },
+        },
+      })
+
+      await builder.buildAsync()
+
+      const createFunctionQuestions = [
+        {
+          question: "Select the type of function you'd like to create",
+          answer: answerWithValue(`${DOWN}${CONFIRM}`),
+        },
+        {
+          question: 'Enter the path, relative to your site',
+          answer: answerWithValue('test/functions'),
+        },
+        {
+          question: 'Select the language of your function',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Pick a template',
+          answer: answerWithValue(`${DOWN}${DOWN}${CONFIRM}`),
+        },
+        {
+          question: 'Name your function',
+          answer: answerWithValue(CONFIRM),
+        },
+      ]
+
+      await withMockApi(routes, async ({ apiUrl }) => {
+        const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
+
+        handleQuestions(childProcess, createFunctionQuestions)
+
+        await childProcess
+
+        const { dependencies } = JSON.parse(await readFile(`${builder.directory}/package.json`))
+
+        // NOTE: Ideally we should be running this test with a specific template,
+        // but `inquirer-autocomplete-prompt` doesn't seem to work with the way
+        // we're mocking prompt responses with `handleQuestions`. Instead, we're
+        // choosing the second template in the list, assuming it's the first one
+        // that contains a `package.json` (currently that's `apollo-graphql`).
+        expect(await fileExistsAsync(`${builder.directory}/test/functions/apollo-graphql/apollo-graphql.js`)).toBe(true)
+        expect(await fileExistsAsync(`${builder.directory}/test/functions/apollo-graphql/package.json`)).toBe(false)
+        expect(typeof dependencies['apollo-server-lambda']).toBe('string')
+
+        expect(dependencies['@netlify/functions']).toBe('^0.1.0')
+      })
+    })
+  })
+
+  test('should install function template dependencies in the function sub-directory if no site-level `package.json` is found', async () => {
+    const siteInfo = {
+      admin_url: 'https://app.netlify.com/sites/site-name/overview',
+      ssl_url: 'https://site-name.netlify.app/',
+      id: 'site_id',
+      name: 'site-name',
+      build_settings: { repo_url: 'https://github.com/owner/repo' },
+    }
+
+    const routes = [
+      {
+        path: 'accounts',
+        response: [{ slug: 'test-account' }],
+      },
+      { path: 'sites/site_id/service-instances', response: [] },
+      { path: 'sites/site_id', response: siteInfo },
+      {
+        path: 'sites',
+        response: [siteInfo],
+      },
+      { path: 'sites/site_id', method: 'patch', response: {} },
+    ]
+
+    await withSiteBuilder('site-with-no-functions-dir-without-package-json', async (builder) => {
+      await builder.buildAsync()
+
+      const createFunctionQuestions = [
+        {
+          question: "Select the type of function you'd like to create",
+          answer: answerWithValue(`${DOWN}${CONFIRM}`),
+        },
+        {
+          question: 'Enter the path, relative to your site',
+          answer: answerWithValue('test/functions'),
+        },
+        {
+          question: 'Select the language of your function',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Pick a template',
+          answer: answerWithValue(`${DOWN}${DOWN}${CONFIRM}`),
+        },
+        {
+          question: 'Name your function',
+          answer: answerWithValue(CONFIRM),
+        },
+      ]
+
+      await withMockApi(routes, async ({ apiUrl }) => {
+        const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
+
+        handleQuestions(childProcess, createFunctionQuestions)
+
+        await childProcess
+
+        // NOTE: Ideally we should be running this test with a specific template,
+        // but `inquirer-autocomplete-prompt` doesn't seem to work with the way
+        // we're mocking prompt responses with `handleQuestions`. Instead, we're
+        // choosing the second template in the list, assuming it's the first one
+        // that contains a `package.json` (currently that's `apollo-graphql`).
+        expect(await fileExistsAsync(`${builder.directory}/test/functions/apollo-graphql/apollo-graphql.js`)).toBe(true)
+        expect(await fileExistsAsync(`${builder.directory}/test/functions/apollo-graphql/package.json`)).toBe(true)
+      })
+    })
+  })
+
+  test('should not create a new function directory when one is found', async () => {
+    const siteInfo = {
+      admin_url: 'https://app.netlify.com/sites/site-name/overview',
+      ssl_url: 'https://site-name.netlify.app/',
+      id: 'site_id',
+      name: 'site-name',
+      build_settings: { repo_url: 'https://github.com/owner/repo' },
+    }
+
+    const routes = [
+      {
+        path: 'accounts',
+        response: [{ slug: 'test-account' }],
+      },
+      { path: 'sites/site_id/service-instances', response: [] },
+      { path: 'sites/site_id', response: siteInfo },
+      {
+        path: 'sites',
+        response: [siteInfo],
+      },
+      { path: 'sites/site_id', method: 'patch', response: {} },
+    ]
+
+    await withSiteBuilder('site-with-functions-dir', async (builder) => {
+      builder.withNetlifyToml({ config: { build: { functions: 'functions' } } })
+
+      await builder.buildAsync()
+
+      const createFunctionQuestions = [
+        {
+          question: "Select the type of function you'd like to create",
+          answer: answerWithValue(`${DOWN}${CONFIRM}`),
+        },
+        {
+          question: 'Select the language of your function',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Pick a template',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Name your function',
+          answer: answerWithValue(CONFIRM),
+        },
+      ]
+
+      await withMockApi(routes, async ({ apiUrl }) => {
+        const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder }))
+
+        handleQuestions(childProcess, createFunctionQuestions)
+
+        await childProcess
+
+        expect(await fileExistsAsync(`${builder.directory}/functions/hello-world/hello-world.js`)).toBe(true)
+      })
+    })
+  })
+
+  test('should only show function templates for the language specified via the --language flag, if one is present', async () => {
+    const siteInfo = {
+      admin_url: 'https://app.netlify.com/sites/site-name/overview',
+      ssl_url: 'https://site-name.netlify.app/',
+      id: 'site_id',
+      name: 'site-name',
+      build_settings: { repo_url: 'https://github.com/owner/repo' },
+    }
+
+    const routes = [
+      {
+        path: 'accounts',
+        response: [{ slug: 'test-account' }],
+      },
+      { path: 'sites/site_id/service-instances', response: [] },
+      { path: 'sites/site_id', response: siteInfo },
+      {
+        path: 'sites',
+        response: [siteInfo],
+      },
+      { path: 'sites/site_id', method: 'patch', response: {} },
+    ]
+
+    await withSiteBuilder('site-with-no-functions-dir', async (builder) => {
+      await builder.buildAsync()
+
+      const createFunctionQuestions = [
+        {
+          question: "Select the type of function you'd like to create",
+          answer: answerWithValue(`${DOWN}${CONFIRM}`),
+        },
+        {
+          question: 'Enter the path, relative to your site',
+          answer: answerWithValue('test/functions'),
+        },
+        {
+          question: 'Pick a template',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Name your function',
+          answer: answerWithValue(CONFIRM),
+        },
+      ]
+
+      await withMockApi(routes, async ({ apiUrl }) => {
+        const childProcess = execa(
+          cliPath,
+          ['functions:create', '--language', 'javascript'],
+          getCLIOptions({ apiUrl, builder }),
+        )
+
+        handleQuestions(childProcess, createFunctionQuestions)
+
+        await childProcess
+
+        expect(await fileExistsAsync(`${builder.directory}/test/functions/hello-world/hello-world.js`)).toBe(true)
+      })
+    })
+  })
+
+  test('throws an error when the --language flag contains an unsupported value', async () => {
+    const siteInfo = {
+      admin_url: 'https://app.netlify.com/sites/site-name/overview',
+      ssl_url: 'https://site-name.netlify.app/',
+      id: 'site_id',
+      name: 'site-name',
+      build_settings: { repo_url: 'https://github.com/owner/repo' },
+    }
+
+    const routes = [
+      {
+        path: 'accounts',
+        response: [{ slug: 'test-account' }],
+      },
+      { path: 'sites/site_id/service-instances', response: [] },
+      { path: 'sites/site_id', response: siteInfo },
+      {
+        path: 'sites',
+        response: [siteInfo],
+      },
+      { path: 'sites/site_id', method: 'patch', response: {} },
+    ]
+
+    await withSiteBuilder('site-with-no-functions-dir', async (builder) => {
+      await builder.buildAsync()
+
+      const createFunctionQuestions = [
+        {
+          question: "Select the type of function you'd like to create",
+          answer: answerWithValue(`${DOWN}${CONFIRM}`),
+        },
+        {
+          question: 'Enter the path, relative to your site',
+          answer: answerWithValue('test/functions'),
+        },
+        {
+          question: 'Pick a template',
+          answer: answerWithValue(CONFIRM),
+        },
+        {
+          question: 'Name your function',
+          answer: answerWithValue(CONFIRM),
+        },
+      ]
+
+      await withMockApi(routes, async ({ apiUrl }) => {
+        const childProcess = execa(
+          cliPath,
+          ['functions:create', '--language', 'coffeescript'],
+          getCLIOptions({ apiUrl, builder }),
+        )
+
+        handleQuestions(childProcess, createFunctionQuestions)
+
+        await expect(childProcess).rejects.toThrowError('Invalid language: coffeescript')
+
+        expect(await fileExistsAsync(`${builder.directory}/test/functions/hello-world/hello-world.js`)).toBe(false)
+      })
+    })
+  })
+})
diff --git a/tests/integration/commands/functions-invoke/functions-invoke.test.mjs b/tests/integration/commands/functions-invoke/functions-invoke.test.mjs
new file mode 100644
index 00000000..67e4d8c0
--- /dev/null
+++ b/tests/integration/commands/functions-invoke/functions-invoke.test.mjs
@@ -0,0 +1,164 @@
+/* eslint-disable require-await */
+import { describe, expect, test } from 'vitest'
+
+import callCli from '../../utils/call-cli.cjs'
+import { withDevServer } from '../../utils/dev-server.cjs'
+import got from '../../utils/got.cjs'
+import { withSiteBuilder } from '../../utils/site-builder.cjs'
+
+describe('functions:invoke command', () => {
+  test('should return function response when invoked with no identity argument', async () => {
+    await withSiteBuilder('function-invoke-with-no-identity-argument', async (builder) => {
+      builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
+        path: 'test-invoke.js',
+        handler: async () => ({
+          statusCode: 200,
+          body: 'success',
+        }),
+      })
+
+      await builder.buildAsync()
+
+      await withDevServer({ cwd: builder.directory }, async (server) => {
+        const stdout = await callCli(['functions:invoke', 'test-invoke', `--port=${server.port}`], {
+          cwd: builder.directory,
+        })
+
+        expect(stdout).toBe('success')
+      })
+    })
+  })
+
+  test('should return function response when invoked', async () => {
+    await withSiteBuilder('site-with-ping-function', async (builder) => {
+      builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({
+        path: 'ping.js',
+        handler: async () => ({
+          statusCode: 200,
+          body: 'ping',
+        }),
+      })
+
+      await builder.buildAsync()
+
+      await withDevServer({ cwd: builder.directory }, async (server) => {
+        const stdout = await callCli(['functions:invoke', 'ping', '--identity', `--port=${server.port}`], {
+          cwd: builder.directory,
+        })
+
+        expect(stdout).toBe('ping')
+      })
+    })
+  })
+
+  test('should trigger background function from event', async () => {
+    await withSiteBuilder('site-with-ping-function', async (builder) => {
+      await builder
+        .withNetlifyToml({ config: { functions: { directory: 'functions' } } })
+        .withFunction({
+          path: 'identity-validate-background.js',
+          handler: async (event) => ({
+            statusCode: 200,
+            body: JSON.stringify(event.body),
+          }),
+        })
+        .buildAsync()
+
+      await withDevServer({ cwd: builder.directory }, async (server) => {
+        const stdout = await callCli(
+          ['functions:invoke', 'identity-validate-background', '--identity', `--port=${server.port}`],
+          {
+            cwd: builder.directory,
+          },
+        )
+
+        // background functions always return an empty response
+        expect(stdout).toBe('')
+      })
+    })
+  })
+
+  test('should serve helpful tips and tricks', async () => {
+    await withSiteBuilder('site-with-isc-ping-function', async (builder) => {
+      await builder
+        .withNetlifyToml({
+          config: { functions: { directory: 'functions' } },
+        })
+        // mocking until https://github.com/netlify/functions/pull/226 landed
+        .withContentFile({
+          path: 'node_modules/@netlify/functions/package.json',
+          content: `{}`,
+        })
+        .withContentFile({
+          path: 'node_modules/@netlify/functions/index.js',
+          content: `module.exports.schedule = (schedule, handler) => handler`,
+        })
+        .withContentFile({
+          path: 'functions/hello-world.js',
+          content: `
+          const { schedule } = require('@netlify/functions')
+
+          module.exports.handler = schedule('@daily', async () => {
+            return {
+              statusCode: 200,
+              body: "hello world"
+            }
+          })
+          `.trim(),
+        })
+        .buildAsync()
+
+      await withDevServer({ cwd: builder.directory }, async (server) => {
+        const plainTextResponse = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
+          throwHttpErrors: false,
+          retry: null,
+        })
+
+        const youReturnedBodyRegex = /.*Your function returned `body`. Is this an accident\?.*/
+        expect(plainTextResponse.body).toMatch(youReturnedBodyRegex)
+        expect(plainTextResponse.body).toMatch(/.*You performed an HTTP request.*/)
+        expect(plainTextResponse.statusCode).toBe(200)
+
+        const htmlResponse = await got(`http://localhost:${server.port}/.netlify/functions/hello-world`, {
+          throwHttpErrors: false,
+          retry: null,
+          headers: {
+            accept: 'text/html',
+          },
+        })
+
+        expect(htmlResponse.body).toMatch(/.*<link.*/)
+        expect(htmlResponse.statusCode).toBe(200)
+
+        const stdout = await callCli(['functions:invoke', 'hello-world', '--identity', `--port=${server.port}`], {
+          cwd: builder.directory,
+        })
+        expect(stdout).toMatch(youReturnedBodyRegex)
+      })
+    })
+  })
+
+  test('should detect netlify-toml defined scheduled functions', async () => {
+    await withSiteBuilder('site-with-netlify-toml-ping-function', async (builder) => {
+      await builder
+        .withNetlifyToml({
+          config: { functions: { directory: 'functions', 'test-1': { schedule: '@daily' } } },
+        })
+        .withFunction({
+          path: 'test-1.js',
+          handler: async () => ({
+            statusCode: 200,
+          }),
+        })
+        .buildAsync()
+
+      await withDevServer({ cwd: builder.directory }, async (server) => {
+        const stdout = await callCli(['functions:invoke', 'test-1', `--port=${server.port}`], {
+          cwd: builder.directory,
+        })
+        expect(stdout).toBe('')
+      })
+    })
+  })
+})
+/* eslint-enable require-await */
diff --git a/tests/integration/utils/mock-api.cjs b/tests/integration/utils/mock-api.cjs
index 40f5769e..b39094ad 100644
--- a/tests/integration/utils/mock-api.cjs
+++ b/tests/integration/utils/mock-api.cjs
@@ -11,7 +11,7 @@ const addRequest = (requests, request) => {
   })
 }
 
-const startMockApi = ({ routes }) => {
+const startMockApi = ({ routes, silent }) => {
   const requests = []
   const app = express()
   app.use(express.urlencoded({ extended: true }))
@@ -34,7 +34,9 @@ const startMockApi = ({ routes }) => {
 
   app.all('*', function onRequest(req, res) {
     addRequest(requests, req)
-    console.warn(`Route not found: (${req.method.toUpperCase()}) ${req.url}`)
+    if (!silent) {
+      console.warn(`Route not found: (${req.method.toUpperCase()}) ${req.url}`)
+    }
     res.status(404)
     res.json({ message: 'Not found' })
   })
@@ -54,10 +56,10 @@ const startMockApi = ({ routes }) => {
   return returnPromise
 }
 
-const withMockApi = async (routes, testHandler) => {
+const withMockApi = async (routes, testHandler, silent = false) => {
   let mockApi
   try {
-    mockApi = await startMockApi({ routes })
+    mockApi = await startMockApi({ routes, silent })
     const apiUrl = `http://localhost:${mockApi.server.address().port}/api/v1`
     return await testHandler({ apiUrl, requests: mockApi.requests })
   } finally {
diff --git a/tests/unit/utils/get-global-config.test.mjs b/tests/unit/utils/get-global-config.test.mjs
index 3315ebdf..3d502562 100644
--- a/tests/unit/utils/get-global-config.test.mjs
+++ b/tests/unit/utils/get-global-config.test.mjs
@@ -4,7 +4,7 @@ import { join } from 'path'
 
 import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'
 
-import { getLegacyPathInHome, getPathInHome } from '../../../src/lib/settings.cjs'
+import { getLegacyPathInHome, getPathInHome } from '../../../src/lib/settings.mjs'
 import getGlobalConfig, { resetConfigCache } from '../../../src/utils/get-global-config.mjs'
 
 const configPath = getPathInHome(['config.json'])
diff --git a/tools/e2e/setup.mjs b/tools/e2e/setup.mjs
index 5f9f7a73..34c7d87f 100644
--- a/tools/e2e/setup.mjs
+++ b/tools/e2e/setup.mjs
@@ -8,7 +8,7 @@ import execa from 'execa'
 import getPort from 'get-port'
 import verdaccio from 'verdaccio'
 
-import { fileExistsAsync } from '../../src/lib/fs.cjs'
+import { fileExistsAsync } from '../../src/lib/fs.mjs'
 
 const VERDACCIO_TIMEOUT_MILLISECONDS = 60 * 1000
 const START_PORT_RANGE = 5000
diff --git a/vitest.config.mjs b/vitest.config.mjs
index 589da9da..bd8fefb4 100644
--- a/vitest.config.mjs
+++ b/vitest.config.mjs
@@ -6,7 +6,7 @@ import { defineConfig } from 'vite'
 export default defineConfig({
   test: {
     include: ['tests/**/*.test.mjs'],
-    testTimeout: 30_000,
+    testTimeout: 60_000,
     deps: {
       external: ['**/fixtures/**', '**/node_modules/**'],
       interopDefault: false,