diff --git a/docs/netlify-dev.md b/docs/netlify-dev.md index 905573ed6a6..3d9eff585c7 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 e8371ecee64..6d9f6e57c4e 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 3596b25e088..2da0da1ed93 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 c33d621bd62..375df7cf975 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 ae25cf52ce6..453d258524d 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 0bab5f56d98..affa6f2d7c1 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 60abc368b0a..b3024a6e54e 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 aaeedba4be9..0bea217584a 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 3f86b54c6cb..cba340baa21 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 feb3bdf8fa0..73dd99a94ff 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 cbb485fe638..b1cf386705a 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 7f14ae62c9d..137e79c997e 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 8ba07c0a301..b850f9e6771 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 3679cc82fbc..4aee18f5957 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 484df79fcc3..051e41a79b7 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 76e4ada8b9c..660d48c43fc 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 551879c8abd..f5663b4cecd 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 5bef5a701ff..a64407a58c3 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 9e0a7d466e1..4da0d668b80 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 1ba2cff6527..fe7d838ec31 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 5f40da5ebf7..66b7c5fbe0a 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 eb9386d8397..f0b3dbb0433 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 364fde3db3d..74578382307 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 a25e4608c82..46592dd217d 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 9bc8cafef17..ada348cb028 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 d63b8b33bf0..d0bba0316fa 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 84eeda7ee64..749caf89406 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 e442444db8f..474e94b9934 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 43028a4ac08..2e5dc345c00 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 fedf5df769c..3326dc0fadf 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 43860bb2c83..0531b202948 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 38dba0bce8e..46b570e5573 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 74e4e2fd4c4..7993e5621c8 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 8c74dabaada..4a15cc58866 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 b53fe92829a..93bce4238a4 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 895c1631cf9..213791a2dd7 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 6d2a910e03e..b48274513ff 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 f0598f9db34..984cc9e413f 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 9e4592bc132..13057c1864f 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 3d17ca9957a..f8029d9e0e0 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 7fa3d5bc7e8..4875ce06018 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 60abc368b0a..b3024a6e54e 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/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 61ad0413a23..2be0f0c198d 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/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 735a861f7cf..a48cdd3b468 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 551879c8abd..f5663b4cecd 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 0ffcfa46388..2194476e3a6 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 79098fc1226..5d08eaf1f95 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 162add4af51..dede74c13d6 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 43860bb2c83..0531b202948 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 38dba0bce8e..46b570e5573 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 74e4e2fd4c4..7993e5621c8 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 9e4592bc132..13057c1864f 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 fa66308f5aa..b29329fa962 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 947c7837cae..e020609f15d 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 f8333662fa2..d385aa209ee 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 478b87ebccd..eb19c174c26 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 5fb37cccc59..908f5a1d04c 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 8a2c3870fc9..9b7e44d0234 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 39b46307672..61da3908960 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 cc3c5cb40d0..bac12860d3b 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 d910052e902..56b4490749e 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 3ac5119746e..0920c61ea03 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 db0cbe9b801..7629da316e4 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 0f31df019af..87bffd4a8af 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 e9f2f85a474..c24381f4cba 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 fb7431958b7..6128ec39f8d 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 9d545110519..1a797f57741 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 edc12a97fc1..e9bf7e4422a 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 5b100566a0c..42d2d7ca8bf 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 9b79b5b20c0..30cb787c7d8 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 91bf51ca490..83da17da1d4 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 fec0a9c2204..61989420c51 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 7b7a6bf00d3..7752f6eaaf5 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 6b5eafede7f..8ab91cd6910 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." diff --git a/tests/integration/20.command.functions.test.cjs b/tests/integration/20.command.functions.test.cjs index 9b033939e1f..f95cf9600d2 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, /.* { - 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 4fd760ca48f..00000000000 --- 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 00000000000..635a2814313 --- /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 40885bda156..00000000000 --- 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 00000000000..7172f0da871 --- /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 00000000000..29646877cea --- /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 00000000000..58190fc5a5e --- /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 00000000000..67e4d8c010b --- /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(/.* { + 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 40f5769e77c..b39094ad056 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 3315ebdff2c..3d502562d7c 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 5f9f7a7358d..34c7d87f5d7 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 589da9dabc4..bd8fefb48ab 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,