diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1e3f98f3b9c..f6c67316b8e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -76,19 +76,6 @@ module.exports = { }, }, }, - { - files: ['src/**/*.cjs'], - rules: { - 'no-restricted-modules': [ - 'error', - { - name: 'chalk', - message: - 'Please use the safe chalk import that handles colors for json output. `const { chalk } = require("src/utils")`', - }, - ], - }, - }, { files: ['src/**/*.mjs', 'bin/**/*.mjs'], parserOptions: { @@ -106,7 +93,7 @@ module.exports = { { name: 'chalk', message: - 'Please use the safe chalk import that handles colors for json output. `import { chalk } from "src/utils"`', + 'Please use the safe chalk import that handles colors for json output. `import { chalk } from "src/utils/command-helpers.mjs"`', }, ], }, diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 486fa32beb8..fbc5f0ac8c8 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -44,8 +44,5 @@ jobs: run: npm run format:ci if: '${{!steps.release-check.outputs.IS_RELEASE}}' - name: Run unit tests - run: npm run test:ci:ava:unit - if: '${{!steps.release-check.outputs.IS_RELEASE}}' - - name: Run vitest unit tests run: npm run test:ci:vitest:unit if: '${{!steps.release-check.outputs.IS_RELEASE}}' diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index dec756e0b2f..252ce867f1f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -74,7 +74,6 @@ "lodash": "^4.17.20", "log-symbols": "^4.0.0", "log-update": "^5.0.0", - "memoize-one": "^6.0.0", "minimist": "^1.2.5", "multiparty": "^4.2.1", "netlify": "^13.0.2", @@ -84,7 +83,6 @@ "netlify-redirector": "^0.3.1", "node-fetch": "^2.6.0", "node-version-alias": "^1.0.1", - "omit.js": "^2.0.2", "ora": "^5.0.0", "p-filter": "^2.1.0", "p-map": "^4.0.0", @@ -140,7 +138,6 @@ "serialize-javascript": "^6.0.0", "sinon": "^14.0.0", "strip-ansi": "^6.0.0", - "supertest": "^6.1.6", "temp-dir": "^2.0.0", "tomlify-j0.4": "^3.0.0", "tree-kill": "^1.2.2", @@ -6560,12 +6557,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, "node_modules/ascii-table": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/ascii-table/-/ascii-table-0.0.9.tgz", @@ -8655,12 +8646,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "node_modules/cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", - "dev": true - }, "node_modules/cookies": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", @@ -10140,16 +10125,6 @@ "node": ">=12" } }, - "node_modules/dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", - "dev": true, - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, "node_modules/diff": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", @@ -13215,33 +13190,6 @@ "node": ">=12.20.0" } }, - "node_modules/formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", - "dev": true, - "dependencies": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/formidable/node_modules/qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "dev": true, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -14066,15 +14014,6 @@ "node": ">=8" } }, - "node_modules/hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -22083,39 +22022,6 @@ "node": ">=0.8.0" } }, - "node_modules/superagent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.3.tgz", - "integrity": "sha512-oBC+aNsCjzzjmO5AOPBPFS+Z7HPzlx+DQr/aHwM08kI+R24gsDmAS1LMfza1fK+P+SKlTAoNZpOvooE/pRO1HA==", - "dev": true, - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.3", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.0.1", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/supertap": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz", @@ -22192,19 +22098,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/supertest": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.2.tgz", - "integrity": "sha512-mSmbW/sPpBU6K8w8189ZiHdc62zMe7dCHpC2ktS9tc0/d2DN0FaxNbDJJNFknZD4jCrGJpxkiFoVyemvKgOdwA==", - "dev": true, - "dependencies": { - "methods": "^1.1.2", - "superagent": "^8.0.3" - }, - "engines": { - "node": ">=6.4.0" - } - }, "node_modules/supports-color": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.2.tgz", @@ -28980,12 +28873,6 @@ "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==", "dev": true }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, "ascii-table": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/ascii-table/-/ascii-table-0.0.9.tgz", @@ -30573,12 +30460,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", - "dev": true - }, "cookies": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", @@ -31746,16 +31627,6 @@ } } }, - "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", - "dev": true, - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, "diff": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", @@ -33993,26 +33864,6 @@ "fetch-blob": "^3.1.2" } }, - "formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", - "dev": true, - "requires": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" - }, - "dependencies": { - "qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", - "dev": true - } - } - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -34630,12 +34481,6 @@ } } }, - "hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true - }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -40656,32 +40501,6 @@ } } }, - "superagent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.3.tgz", - "integrity": "sha512-oBC+aNsCjzzjmO5AOPBPFS+Z7HPzlx+DQr/aHwM08kI+R24gsDmAS1LMfza1fK+P+SKlTAoNZpOvooE/pRO1HA==", - "dev": true, - "requires": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.3", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.0.1", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "dependencies": { - "mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true - } - } - }, "supertap": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz", @@ -40736,16 +40555,6 @@ } } }, - "supertest": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.2.tgz", - "integrity": "sha512-mSmbW/sPpBU6K8w8189ZiHdc62zMe7dCHpC2ktS9tc0/d2DN0FaxNbDJJNFknZD4jCrGJpxkiFoVyemvKgOdwA==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^8.0.3" - } - }, "supports-color": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.2.tgz", diff --git a/package.json b/package.json index 5f16eca27b3..5fc33279ada 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,6 @@ "test:init:hugo-deps": "npm ci --prefix tests/integration/hugo-site --no-audit", "test:dev:ava": "ava --verbose", "test:dev:vitest": "vitest run", - "test:ci:ava:unit": "c8 -r json ava --no-worker-threads tests/unit/ tools/", "test:ci:ava:integration": "c8 -r json ava --concurrency 1 --no-worker-threads tests/integration/", "test:ci:vitest:unit": "vitest run --coverage tests/unit/", "test:ci:vitest:integration": "vitest run --coverage --no-threads tests/integration/", @@ -295,7 +294,6 @@ "lodash": "^4.17.20", "log-symbols": "^4.0.0", "log-update": "^5.0.0", - "memoize-one": "^6.0.0", "minimist": "^1.2.5", "multiparty": "^4.2.1", "netlify": "^13.0.2", @@ -305,7 +303,6 @@ "netlify-redirector": "^0.3.1", "node-fetch": "^2.6.0", "node-version-alias": "^1.0.1", - "omit.js": "^2.0.2", "ora": "^5.0.0", "p-filter": "^2.1.0", "p-map": "^4.0.0", @@ -357,7 +354,6 @@ "serialize-javascript": "^6.0.0", "sinon": "^14.0.0", "strip-ansi": "^6.0.0", - "supertest": "^6.1.6", "temp-dir": "^2.0.0", "tomlify-j0.4": "^3.0.0", "tree-kill": "^1.2.2", diff --git a/site/scripts/docs.mjs b/site/scripts/docs.mjs index a2da372bedb..bfb09a13a5f 100644 --- a/site/scripts/docs.mjs +++ b/site/scripts/docs.mjs @@ -6,7 +6,7 @@ import { fileURLToPath } from 'url' import markdownMagic from 'markdown-magic' import stripAnsi from 'strip-ansi' -import { normalizeBackslash } from '../../src/lib/path.cjs' +import { normalizeBackslash } from '../../src/lib/path.mjs' import { generateCommandData } from './generate-command-data.mjs' diff --git a/site/scripts/generate-command-data.mjs b/site/scripts/generate-command-data.mjs index 2bedb3e3967..8a033fe48cc 100644 --- a/site/scripts/generate-command-data.mjs +++ b/site/scripts/generate-command-data.mjs @@ -1,6 +1,6 @@ // @ts-check import { createMainCommand } from '../../src/commands/index.mjs' -import utils from '../../src/utils/index.cjs' +import { sortOptions } from '../../src/utils/command-helpers.mjs' const program = createMainCommand() @@ -21,7 +21,7 @@ const parseCommand = function (command) { const flags = command.options .filter((option) => !option.hidden) - .sort(utils.sortOptions) + .sort(sortOptions) .reduce((prev, cur) => { const name = cur.long.replace('--', '') const contentType = cur.argChoices ? cur.argChoices.join(' | ') : 'string' diff --git a/src/commands/addons/addons-auth.mjs b/src/commands/addons/addons-auth.mjs index 1008a2bc82a..d6cb9a1eb60 100644 --- a/src/commands/addons/addons-auth.mjs +++ b/src/commands/addons/addons-auth.mjs @@ -1,8 +1,7 @@ // @ts-check import { ADDON_VALIDATION, prepareAddonCommand } from '../../utils/addons/prepare.mjs' -import utils from '../../utils/index.cjs' - -const { exit, log, openBrowser } = utils +import { exit, log } from '../../utils/command-helpers.mjs' +import openBrowser from '../../utils/open-browser.mjs' /** * The addons:auth command diff --git a/src/commands/addons/addons-config.mjs b/src/commands/addons/addons-config.mjs index 7e70f50eb7d..724df0a97f4 100644 --- a/src/commands/addons/addons-config.mjs +++ b/src/commands/addons/addons-config.mjs @@ -8,9 +8,8 @@ import { ADDON_VALIDATION, prepareAddonCommand } from '../../utils/addons/prepar import generatePrompts from '../../utils/addons/prompts.mjs' import { renderConfigValues } from '../../utils/addons/render.mjs' import { missingConfigValues, requiredConfigValues, updateConfigValues } from '../../utils/addons/validation.mjs' -import utils from '../../utils/index.cjs' - -const { chalk, error, log, parseRawFlags } = utils +import { chalk, error, log } from '../../utils/command-helpers.mjs' +import { parseRawFlags } from '../../utils/parse-raw-flags.mjs' const update = async function ({ addonName, api, currentConfig, instanceId, newConfig, siteId }) { const codeDiff = diffValues(currentConfig, newConfig) diff --git a/src/commands/addons/addons-create.mjs b/src/commands/addons/addons-create.mjs index e3da1f92e9e..362dac577e1 100644 --- a/src/commands/addons/addons-create.mjs +++ b/src/commands/addons/addons-create.mjs @@ -6,9 +6,8 @@ import { ADDON_VALIDATION, prepareAddonCommand } from '../../utils/addons/prepar import generatePrompts from '../../utils/addons/prompts.mjs' import { renderConfigValues, renderMissingValues } from '../../utils/addons/render.mjs' import { missingConfigValues, requiredConfigValues, updateConfigValues } from '../../utils/addons/validation.mjs' -import utils from '../../utils/index.cjs' - -const { chalk, error, log, parseRawFlags } = utils +import { chalk, error, log } from '../../utils/command-helpers.mjs' +import { parseRawFlags } from '../../utils/parse-raw-flags.mjs' const createAddon = async ({ addonName, api, config, siteData, siteId }) => { try { diff --git a/src/commands/addons/addons-delete.mjs b/src/commands/addons/addons-delete.mjs index 6988cc034d4..2ecdfce5f3a 100644 --- a/src/commands/addons/addons-delete.mjs +++ b/src/commands/addons/addons-delete.mjs @@ -2,9 +2,7 @@ import inquirer from 'inquirer' import { ADDON_VALIDATION, prepareAddonCommand } from '../../utils/addons/prepare.mjs' -import utils from '../../utils/index.cjs' - -const { error, exit, log } = utils +import { error, exit, log } from '../../utils/command-helpers.mjs' /** * The addons:delete command diff --git a/src/commands/addons/addons-list.mjs b/src/commands/addons/addons-list.mjs index b646f612eec..cb5368d621d 100644 --- a/src/commands/addons/addons-list.mjs +++ b/src/commands/addons/addons-list.mjs @@ -2,9 +2,7 @@ import AsciiTable from 'ascii-table' import { prepareAddonCommand } from '../../utils/addons/prepare.mjs' -import utils from '../../utils/index.cjs' - -const { log, logJson } = utils +import { log, logJson } from '../../utils/command-helpers.mjs' /** * The addons:list command diff --git a/src/commands/api/api.mjs b/src/commands/api/api.mjs index e764cb57a00..c31744c63fa 100644 --- a/src/commands/api/api.mjs +++ b/src/commands/api/api.mjs @@ -2,9 +2,7 @@ import AsciiTable from 'ascii-table' import { methods } from 'netlify' -import utils from '../../utils/index.cjs' - -const { chalk, error, exit, log, logJson } = utils +import { chalk, error, exit, log, logJson } from '../../utils/command-helpers.mjs' /** * The api command diff --git a/src/commands/base-command.mjs b/src/commands/base-command.mjs index 3d9bf4aaf5d..5c30ce90a62 100644 --- a/src/commands/base-command.mjs +++ b/src/commands/base-command.mjs @@ -22,9 +22,9 @@ import { pollForToken, sortOptions, warn, -} from '../utils/command-helpers.cjs' -import getGlobalConfig from '../utils/get-global-config.cjs' -import { openBrowser } from '../utils/open-browser.cjs' +} from '../utils/command-helpers.mjs' +import getGlobalConfig from '../utils/get-global-config.mjs' +import openBrowser from '../utils/open-browser.mjs' import StateConfig from '../utils/state-config.mjs' import { identify, track } from '../utils/telemetry/index.mjs' diff --git a/src/commands/build/build.mjs b/src/commands/build/build.mjs index 9aa77afc348..d154edf7c85 100644 --- a/src/commands/build/build.mjs +++ b/src/commands/build/build.mjs @@ -2,9 +2,9 @@ import process from 'process' import { getBuildOptions, runBuild } from '../../lib/build.mjs' -import utils from '../../utils/index.cjs' - -const { error, exit, generateNetlifyGraphJWT, getEnvelopeEnv, getToken, normalizeContext } = utils +import { error, exit, getToken } from '../../utils/command-helpers.mjs' +import { generateNetlifyGraphJWT } from '../../utils/dev.mjs' +import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' /** * @param {import('../../lib/build.mjs').BuildConfig} options diff --git a/src/commands/deploy/deploy.mjs b/src/commands/deploy/deploy.mjs index 54b7cda4637..b6eee274c6f 100644 --- a/src/commands/deploy/deploy.mjs +++ b/src/commands/deploy/deploy.mjs @@ -12,30 +12,27 @@ import prettyjson from 'prettyjson' import { cancelDeploy } from '../../lib/api.mjs' import { getBuildOptions, runBuild } from '../../lib/build.mjs' -import { normalizeFunctionsConfig } from '../../lib/functions/config.cjs' -import { getLogMessage } from '../../lib/log.cjs' +import { normalizeFunctionsConfig } from '../../lib/functions/config.mjs' +import { getLogMessage } from '../../lib/log.mjs' import { startSpinner, stopSpinner } from '../../lib/spinner.cjs' -import { getFunctionsManifestPath, getInternalFunctionsDir } from '../../utils/functions/index.mjs' -import utils from '../../utils/index.cjs' -import { link } from '../link/index.mjs' -import { sitesCreate } from '../sites/index.mjs' - -const { - NETLIFYDEV, - NETLIFYDEVERR, - NETLIFYDEVLOG, +import { chalk, - deploySite, error, exit, getToken, log, logJson, - openBrowser, + NETLIFYDEV, + NETLIFYDEVERR, + NETLIFYDEVLOG, warn, -} = utils - -const DEFAULT_DEPLOY_TIMEOUT = 1.2e6 +} from '../../utils/command-helpers.mjs' +import { DEFAULT_DEPLOY_TIMEOUT } from '../../utils/deploy/constants.mjs' +import { deploySite } from '../../utils/deploy/deploy-site.mjs' +import { getFunctionsManifestPath, getInternalFunctionsDir } from '../../utils/functions/index.mjs' +import openBrowser from '../../utils/open-browser.mjs' +import { link } from '../link/index.mjs' +import { sitesCreate } from '../sites/index.mjs' const triggerDeploy = async ({ api, options, siteData, siteId }) => { try { diff --git a/src/commands/dev/dev-exec.mjs b/src/commands/dev/dev-exec.mjs index 94deb7a2663..9bd518fa0c1 100644 --- a/src/commands/dev/dev-exec.mjs +++ b/src/commands/dev/dev-exec.mjs @@ -1,8 +1,7 @@ import execa from 'execa' -import utils from '../../utils/index.cjs' - -const { getEnvelopeEnv, injectEnvVariables, normalizeContext } = utils +import { injectEnvVariables } from '../../utils/dev.mjs' +import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' /** * The dev:exec command diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 0c9e6cec965..eecf81e56e3 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -27,36 +27,30 @@ import { readGraphQLOperationsSourceFile, } from '../../lib/one-graph/cli-netlify-graph.mjs' import { startSpinner, stopSpinner } from '../../lib/spinner.cjs' -import detectServerSettings from '../../utils/detect-server-settings.mjs' -import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' -import utils from '../../utils/index.cjs' -import { startLiveTunnel } from '../../utils/live-tunnel.mjs' -import { startProxy } from '../../utils/proxy.mjs' - -import { createDevExecCommand } from './dev-exec.mjs' - -const { +import { BANG, - NETLIFYDEV, - NETLIFYDEVERR, - NETLIFYDEVLOG, - NETLIFYDEVWARN, chalk, error, exit, - generateNetlifyGraphJWT, - getEnvelopeEnv, - getSiteInformation, getToken, - injectEnvVariables, log, + NETLIFYDEV, + NETLIFYDEVERR, + NETLIFYDEVLOG, + NETLIFYDEVWARN, normalizeConfig, - normalizeContext, - openBrowser, - processOnExit, warn, watchDebounced, -} = utils +} from '../../utils/command-helpers.mjs' +import detectServerSettings from '../../utils/detect-server-settings.mjs' +import { generateNetlifyGraphJWT, getSiteInformation, injectEnvVariables, processOnExit } from '../../utils/dev.mjs' +import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' +import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' +import { startLiveTunnel } from '../../utils/live-tunnel.mjs' +import openBrowser from '../../utils/open-browser.mjs' +import { startProxy } from '../../utils/proxy.mjs' + +import { createDevExecCommand } from './dev-exec.mjs' const netlifyBuildPromise = import('@netlify/build') diff --git a/src/commands/env/env-clone.mjs b/src/commands/env/env-clone.mjs index e2a27eaaccb..bba9a29d34c 100644 --- a/src/commands/env/env-clone.mjs +++ b/src/commands/env/env-clone.mjs @@ -1,7 +1,6 @@ // @ts-check -import utils from '../../utils/index.cjs' - -const { chalk, error: logError, log, translateFromEnvelopeToMongo, translateFromMongoToEnvelope } = utils +import { chalk, error as logError, log } from '../../utils/command-helpers.mjs' +import { translateFromEnvelopeToMongo, translateFromMongoToEnvelope } from '../../utils/env/index.mjs' const safeGetSite = async (api, siteId) => { try { diff --git a/src/commands/env/env-get.mjs b/src/commands/env/env-get.mjs index 0f35ac00aa5..f835e4439bd 100644 --- a/src/commands/env/env-get.mjs +++ b/src/commands/env/env-get.mjs @@ -1,9 +1,8 @@ // @ts-check import { Option } from 'commander' -import utils from '../../utils/index.cjs' - -const { AVAILABLE_CONTEXTS, chalk, error, getEnvelopeEnv, log, logJson, normalizeContext } = utils +import { chalk, error, log, logJson } from '../../utils/command-helpers.mjs' +import { AVAILABLE_CONTEXTS, getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' /** * The env:get command diff --git a/src/commands/env/env-import.mjs b/src/commands/env/env-import.mjs index ef17c43d6c6..7f556c23646 100644 --- a/src/commands/env/env-import.mjs +++ b/src/commands/env/env-import.mjs @@ -4,9 +4,8 @@ import { readFile } from 'fs/promises' import AsciiTable from 'ascii-table' import dotenv from 'dotenv' -import utils from '../../utils/index.cjs' - -const { exit, log, logJson, translateFromEnvelopeToMongo, translateFromMongoToEnvelope } = utils +import { exit, log, logJson } from '../../utils/command-helpers.mjs' +import { translateFromEnvelopeToMongo, translateFromMongoToEnvelope } from '../../utils/env/index.mjs' /** * The env:import command diff --git a/src/commands/env/env-list.mjs b/src/commands/env/env-list.mjs index f62cefa6007..545eeede855 100644 --- a/src/commands/env/env-list.mjs +++ b/src/commands/env/env-list.mjs @@ -6,10 +6,8 @@ import { Option } from 'commander' import inquirer from 'inquirer' import logUpdate from 'log-update' -import utils from '../../utils/index.cjs' - -const { AVAILABLE_CONTEXTS, chalk, error, getEnvelopeEnv, getHumanReadableScopes, log, logJson, normalizeContext } = - utils +import { chalk, error, log, logJson } from '../../utils/command-helpers.mjs' +import { AVAILABLE_CONTEXTS, getEnvelopeEnv, getHumanReadableScopes, normalizeContext } from '../../utils/env/index.mjs' const MASK_LENGTH = 50 const MASK = '*'.repeat(MASK_LENGTH) diff --git a/src/commands/env/env-set.mjs b/src/commands/env/env-set.mjs index 801b5ac49f9..223c5acc78c 100644 --- a/src/commands/env/env-set.mjs +++ b/src/commands/env/env-set.mjs @@ -1,18 +1,13 @@ // @ts-check import { Option } from 'commander' -import utils from '../../utils/index.cjs' - -const { +import { chalk, error, log, logJson } from '../../utils/command-helpers.mjs' +import { AVAILABLE_CONTEXTS, AVAILABLE_SCOPES, - chalk, - error, - log, - logJson, normalizeContext, translateFromEnvelopeToMongo, -} = utils +} from '../../utils/env/index.mjs' /** * The env:set command diff --git a/src/commands/env/env-unset.mjs b/src/commands/env/env-unset.mjs index 8741f755546..665f71a52f5 100644 --- a/src/commands/env/env-unset.mjs +++ b/src/commands/env/env-unset.mjs @@ -1,7 +1,6 @@ // @ts-check -import utils from '../../utils/index.cjs' - -const { AVAILABLE_CONTEXTS, chalk, error, log, logJson, normalizeContext, translateFromEnvelopeToMongo } = utils +import { chalk, error, log, logJson } from '../../utils/command-helpers.mjs' +import { AVAILABLE_CONTEXTS, normalizeContext, translateFromEnvelopeToMongo } from '../../utils/env/index.mjs' /** * The env:unset command diff --git a/src/commands/functions/functions-build.mjs b/src/commands/functions/functions-build.mjs index 544c1241a70..0428e8d05ca 100644 --- a/src/commands/functions/functions-build.mjs +++ b/src/commands/functions/functions-build.mjs @@ -1,10 +1,8 @@ // @ts-check import { mkdir } from 'fs/promises' +import { NETLIFYDEVERR, NETLIFYDEVLOG, exit, log } from '../../utils/command-helpers.mjs' import { getFunctionsDir } from '../../utils/functions/index.mjs' -import utils from '../../utils/index.cjs' - -const { NETLIFYDEVERR, NETLIFYDEVLOG, exit, log } = utils /** * The functions:build command diff --git a/src/commands/functions/functions-create.mjs b/src/commands/functions/functions-create.mjs index a93f9d34aa0..c33d621bd62 100644 --- a/src/commands/functions/functions-create.mjs +++ b/src/commands/functions/functions-create.mjs @@ -17,13 +17,13 @@ import fetch from 'node-fetch' import ora from 'ora' import { getAddons, getCurrentAddon, getSiteData } from '../../utils/addons/prepare.mjs' -import utils from '../../utils/index.cjs' +import { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, error, log } from '../../utils/command-helpers.mjs' +import { injectEnvVariables } from '../../utils/dev.mjs' +import execa from '../../utils/execa.mjs' import { readRepoURL, validateRepoURL } from '../../utils/read-repo-url.mjs' const copyTemplateDir = promisify(copyTemplateDirOriginal) -const { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, error, execa, injectEnvVariables, log } = utils - const require = createRequire(import.meta.url) const templatesDir = path.resolve(dirname(fileURLToPath(import.meta.url)), '../../functions-templates') diff --git a/src/commands/functions/functions-invoke.mjs b/src/commands/functions/functions-invoke.mjs index 6a5c6dceaab..92696415dc9 100644 --- a/src/commands/functions/functions-invoke.mjs +++ b/src/commands/functions/functions-invoke.mjs @@ -7,10 +7,8 @@ import process from 'process' import inquirer from 'inquirer' import fetch from 'node-fetch' +import { NETLIFYDEVWARN, chalk, error, exit } from '../../utils/command-helpers.mjs' import { BACKGROUND, CLOCKWORK_USERAGENT, getFunctions } from '../../utils/functions/index.mjs' -import utils from '../../utils/index.cjs' - -const { NETLIFYDEVWARN, chalk, error, exit } = utils const require = createRequire(import.meta.url) diff --git a/src/commands/functions/functions-list.mjs b/src/commands/functions/functions-list.mjs index de610170d0c..80abde2c6d0 100644 --- a/src/commands/functions/functions-list.mjs +++ b/src/commands/functions/functions-list.mjs @@ -1,10 +1,8 @@ // @ts-check import AsciiTable from 'ascii-table' +import { error, exit, log, logJson, warn } from '../../utils/command-helpers.mjs' import { getFunctions, getFunctionsDir } from '../../utils/functions/index.mjs' -import utils from '../../utils/index.cjs' - -const { error, exit, log, logJson, warn } = utils const normalizeFunction = function (deployedFunctions, { name, urlPath: url }) { const isDeployed = deployedFunctions.some((deployedFunction) => deployedFunction.n === name) diff --git a/src/commands/functions/functions-serve.mjs b/src/commands/functions/functions-serve.mjs index b0f40e540af..f5a0abde131 100644 --- a/src/commands/functions/functions-serve.mjs +++ b/src/commands/functions/functions-serve.mjs @@ -2,10 +2,9 @@ import { join } from 'path' import { startFunctionsServer } from '../../lib/functions/server.mjs' +import { acquirePort, getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' import { getFunctionsDir } from '../../utils/functions/index.mjs' -import utils from '../../utils/index.cjs' -const { acquirePort, getSiteInformation, injectEnvVariables } = utils const DEFAULT_PORT = 9999 /** diff --git a/src/commands/functions/functions.mjs b/src/commands/functions/functions.mjs index ff61207bb22..9d67555285e 100644 --- a/src/commands/functions/functions.mjs +++ b/src/commands/functions/functions.mjs @@ -1,5 +1,5 @@ // @ts-check -import { chalk } from '../../utils/command-helpers.cjs' +import { chalk } from '../../utils/command-helpers.mjs' import { createFunctionsBuildCommand } from './functions-build.mjs' import { createFunctionsCreateCommand } from './functions-create.mjs' diff --git a/src/commands/graph/graph-config-write.mjs b/src/commands/graph/graph-config-write.mjs index 6af9eef2630..a70e62311bd 100644 --- a/src/commands/graph/graph-config-write.mjs +++ b/src/commands/graph/graph-config-write.mjs @@ -4,9 +4,7 @@ import path from 'path' import process from 'process' import { getNetlifyGraphConfig } from '../../lib/one-graph/cli-netlify-graph.mjs' -import utils from '../../utils/index.cjs' - -const { NETLIFYDEVERR, chalk, error, log } = utils +import { NETLIFYDEVERR, chalk, error, log } from '../../utils/command-helpers.mjs' /** * Creates the `netlify graph:config:write` command diff --git a/src/commands/graph/graph-edit.mjs b/src/commands/graph/graph-edit.mjs index 9d98fe8ae4c..85c3c3d8cff 100644 --- a/src/commands/graph/graph-edit.mjs +++ b/src/commands/graph/graph-edit.mjs @@ -8,10 +8,9 @@ import { getNetlifyGraphConfig, readGraphQLOperationsSourceFile, } from '../../lib/one-graph/cli-netlify-graph.mjs' -import utils from '../../utils/index.cjs' -import { openBrowser } from '../../utils/open-browser.cjs' +import { NETLIFYDEVERR, chalk, error, log } from '../../utils/command-helpers.mjs' +import openBrowser from '../../utils/open-browser.mjs' -const { NETLIFYDEVERR, chalk, error, log } = utils const { ensureAppForSite, executeCreatePersistedQueryMutation } = OneGraphCliClient /** diff --git a/src/commands/graph/graph-handler.mjs b/src/commands/graph/graph-handler.mjs index c742e148bc5..2b52c82a558 100644 --- a/src/commands/graph/graph-handler.mjs +++ b/src/commands/graph/graph-handler.mjs @@ -11,9 +11,7 @@ import { getNetlifyGraphConfig, readGraphQLSchemaFile, } from '../../lib/one-graph/cli-netlify-graph.mjs' -import utils from '../../utils/index.cjs' - -const { error, log } = utils +import { error, log } from '../../utils/command-helpers.mjs' /** * Creates the `netlify graph:handler` command diff --git a/src/commands/graph/graph-init.mjs b/src/commands/graph/graph-init.mjs index d406a27b335..b44c8a75583 100644 --- a/src/commands/graph/graph-init.mjs +++ b/src/commands/graph/graph-init.mjs @@ -7,9 +7,8 @@ import { v4 as uuidv4 } from 'uuid' import { OneGraphCliClient, ensureCLISession } from '../../lib/one-graph/cli-client.mjs' import { getNetlifyGraphConfig } from '../../lib/one-graph/cli-netlify-graph.mjs' -import utils from '../../utils/index.cjs' - -const { NETLIFYDEVERR, chalk, error, exit, getToken, log, translateFromEnvelopeToMongo } = utils +import { NETLIFYDEVERR, chalk, error, exit, getToken, log } from '../../utils/command-helpers.mjs' +import { translateFromEnvelopeToMongo } from '../../utils/env/index.mjs' const { ensureAppForSite, executeCreateApiTokenMutation } = OneGraphCliClient diff --git a/src/commands/graph/graph-library.mjs b/src/commands/graph/graph-library.mjs index ddfa69459c7..87a467be743 100644 --- a/src/commands/graph/graph-library.mjs +++ b/src/commands/graph/graph-library.mjs @@ -12,9 +12,7 @@ import { readGraphQLOperationsSourceFile, readGraphQLSchemaFile, } from '../../lib/one-graph/cli-netlify-graph.mjs' -import utils from '../../utils/index.cjs' - -const { NETLIFYDEVERR, chalk, error, log } = utils +import { NETLIFYDEVERR, chalk, error, log } from '../../utils/command-helpers.mjs' /** * Creates the `netlify graph:library` command diff --git a/src/commands/graph/graph-operations.mjs b/src/commands/graph/graph-operations.mjs index 6575e80b01b..d0fda214458 100644 --- a/src/commands/graph/graph-operations.mjs +++ b/src/commands/graph/graph-operations.mjs @@ -7,9 +7,7 @@ import { getNetlifyGraphConfig, readGraphQLOperationsSourceFile, } from '../../lib/one-graph/cli-netlify-graph.mjs' -import utils from '../../utils/index.cjs' - -const { log } = utils +import { log } from '../../utils/command-helpers.mjs' const { parse } = GraphQL diff --git a/src/commands/graph/graph-pull.mjs b/src/commands/graph/graph-pull.mjs index ac1e8d17d32..af9c201a4d5 100644 --- a/src/commands/graph/graph-pull.mjs +++ b/src/commands/graph/graph-pull.mjs @@ -13,9 +13,7 @@ import { refetchAndGenerateFromOneGraph, } from '../../lib/one-graph/cli-client.mjs' import { buildSchema, getNetlifyGraphConfig, readGraphQLSchemaFile } from '../../lib/one-graph/cli-netlify-graph.mjs' -import utils from '../../utils/index.cjs' - -const { NETLIFYDEVERR, chalk, error, log, warn } = utils +import { NETLIFYDEVERR, chalk, error, log, warn } from '../../utils/command-helpers.mjs' /** * Creates the `netlify graph:pull` command diff --git a/src/commands/init/init.mjs b/src/commands/init/init.mjs index d470bd5c625..7127ae66d12 100644 --- a/src/commands/init/init.mjs +++ b/src/commands/init/init.mjs @@ -3,16 +3,14 @@ import dotProp from 'dot-prop' import inquirer from 'inquirer' import isEmpty from 'lodash/isEmpty.js' +import { chalk, exit, log } from '../../utils/command-helpers.mjs' import getRepoData from '../../utils/get-repo-data.mjs' import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' -import utils from '../../utils/index.cjs' -import { configureRepo } from '../../utils/init/config.cjs' +import { configureRepo } from '../../utils/init/config.mjs' import { track } from '../../utils/telemetry/index.mjs' import { link } from '../link/index.mjs' import { sitesCreate } from '../sites/index.mjs' -const { chalk, exit, log } = utils - const persistState = ({ siteInfo, state }) => { // Save to .netlify/state.json file state.set('siteId', siteInfo.id) diff --git a/src/commands/link/link.mjs b/src/commands/link/link.mjs index 0fa2e93771d..5aaa26185dd 100644 --- a/src/commands/link/link.mjs +++ b/src/commands/link/link.mjs @@ -2,13 +2,11 @@ import inquirer from 'inquirer' import { listSites } from '../../lib/api.mjs' +import { chalk, error, exit, log } from '../../utils/command-helpers.mjs' import getRepoData from '../../utils/get-repo-data.mjs' import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' -import utils from '../../utils/index.cjs' import { track } from '../../utils/telemetry/index.mjs' -const { chalk, error, exit, log } = utils - /** * * @param {import('../base-command.mjs').NetlifyOptions} netlify diff --git a/src/commands/lm/lm-setup.mjs b/src/commands/lm/lm-setup.mjs index 913eac66eda..882bd9e0019 100644 --- a/src/commands/lm/lm-setup.mjs +++ b/src/commands/lm/lm-setup.mjs @@ -1,13 +1,12 @@ // @ts-check import Listr from 'listr' -import utils from '../../utils/index.cjs' +import { error } from '../../utils/command-helpers.mjs' +import execa from '../../utils/execa.mjs' import { installPlatform } from '../../utils/lm/install.mjs' import { checkHelperVersion } from '../../utils/lm/requirements.mjs' import { printBanner } from '../../utils/lm/ui.mjs' -const { error, execa } = utils - const installHelperIfMissing = async function ({ force }) { let installHelper = false try { diff --git a/src/commands/login/login.mjs b/src/commands/login/login.mjs index 0a90e361a36..7c9a2a6c383 100644 --- a/src/commands/login/login.mjs +++ b/src/commands/login/login.mjs @@ -1,7 +1,5 @@ // @ts-check -import utils from '../../utils/index.cjs' - -const { chalk, exit, getToken, log } = utils +import { chalk, exit, getToken, log } from '../../utils/command-helpers.mjs' const msg = function (location) { switch (location) { diff --git a/src/commands/logout/logout.mjs b/src/commands/logout/logout.mjs index e9ebe01cf1c..f0e60ff7198 100644 --- a/src/commands/logout/logout.mjs +++ b/src/commands/logout/logout.mjs @@ -1,9 +1,7 @@ // @ts-check -import utils from '../../utils/index.cjs' +import { exit, getToken, log } from '../../utils/command-helpers.mjs' import { track } from '../../utils/telemetry/index.mjs' -const { exit, getToken, log } = utils - /** * The logout command * @param {import('commander').OptionValues} options diff --git a/src/commands/main.mjs b/src/commands/main.mjs index b206028f5ef..511b866721d 100644 --- a/src/commands/main.mjs +++ b/src/commands/main.mjs @@ -5,8 +5,10 @@ import { Option } from 'commander' import inquirer from 'inquirer' import { findBestMatch } from 'string-similarity' +import { BANG, chalk, error, exit, log, NETLIFY_CYAN, USER_AGENT, warn } from '../utils/command-helpers.mjs' +import execa from '../utils/execa.mjs' +import getGlobalConfig from '../utils/get-global-config.mjs' import getPackageJson from '../utils/get-package-json.mjs' -import utils from '../utils/index.cjs' import { track } from '../utils/telemetry/index.mjs' import { createAddonsCommand } from './addons/index.mjs' @@ -32,8 +34,6 @@ import { createSwitchCommand } from './switch/index.mjs' import { createUnlinkCommand } from './unlink/index.mjs' import { createWatchCommand } from './watch/index.mjs' -const { BANG, NETLIFY_CYAN, USER_AGENT, chalk, error, execa, exit, getGlobalConfig, log, warn } = utils - const SUGGESTION_TIMEOUT = 1e4 const getVersionPage = async () => { diff --git a/src/commands/open/open-admin.mjs b/src/commands/open/open-admin.mjs index 20e4b87b37f..7779a1d6ce2 100644 --- a/src/commands/open/open-admin.mjs +++ b/src/commands/open/open-admin.mjs @@ -1,6 +1,5 @@ -import utils from '../../utils/index.cjs' - -const { error, exit, log, openBrowser, warn } = utils +import { error, exit, log, warn } from '../../utils/command-helpers.mjs' +import openBrowser from '../../utils/open-browser.mjs' /** * The open:admin command diff --git a/src/commands/open/open-site.mjs b/src/commands/open/open-site.mjs index 9ba2e745a42..2fae8e07930 100644 --- a/src/commands/open/open-site.mjs +++ b/src/commands/open/open-site.mjs @@ -1,6 +1,5 @@ -import utils from '../../utils/index.cjs' - -const { error, exit, log, openBrowser, warn } = utils +import { error, exit, log, warn } from '../../utils/command-helpers.mjs' +import openBrowser from '../../utils/open-browser.mjs' /** * The open:site command diff --git a/src/commands/open/open.mjs b/src/commands/open/open.mjs index 4ea6e4e89e2..2d9af9518f8 100644 --- a/src/commands/open/open.mjs +++ b/src/commands/open/open.mjs @@ -1,10 +1,8 @@ -import utils from '../../utils/index.cjs' +import { log } from '../../utils/command-helpers.mjs' import { createOpenAdminCommand, openAdmin } from './open-admin.mjs' import { createOpenSiteCommand, openSite } from './open-site.mjs' -const { log } = utils - /** * The open command * @param {import('commander').OptionValues} options diff --git a/src/commands/recipes/recipes.mjs b/src/commands/recipes/recipes.mjs index 4f09e7b3fe2..ad2c74a9b16 100644 --- a/src/commands/recipes/recipes.mjs +++ b/src/commands/recipes/recipes.mjs @@ -4,7 +4,7 @@ import { basename } from 'path' import inquirer from 'inquirer' import { findBestMatch } from 'string-similarity' -import utils from '../../utils/command-helpers.cjs' +import { NETLIFYDEVERR, chalk, log } from '../../utils/command-helpers.mjs' import { getRecipe, listRecipes } from './common.mjs' import { createRecipesListCommand } from './recipes-list.mjs' @@ -32,7 +32,7 @@ const recipesCommand = async (recipeName, options, command) => { throw error } - utils.log(`${utils.NETLIFYDEVERR} ${utils.chalk.yellow(recipeName)} is not a valid recipe name.`) + log(`${NETLIFYDEVERR} ${chalk.yellow(recipeName)} is not a valid recipe name.`) const recipes = await listRecipes() const recipeNames = recipes.map(({ name }) => name) @@ -43,7 +43,7 @@ const recipesCommand = async (recipeName, options, command) => { const prompt = inquirer.prompt({ type: 'confirm', name: 'suggestion', - message: `Did you mean ${utils.chalk.blue(suggestion)}`, + message: `Did you mean ${chalk.blue(suggestion)}`, default: false, }) diff --git a/src/commands/sites/sites-create-template.mjs b/src/commands/sites/sites-create-template.mjs index c4d63aa0365..0c1216aaab2 100644 --- a/src/commands/sites/sites-create-template.mjs +++ b/src/commands/sites/sites-create-template.mjs @@ -3,19 +3,18 @@ import inquirer from 'inquirer' import pick from 'lodash/pick.js' import parseGitHubUrl from 'parse-github-url' -import prettyjson from 'prettyjson' +import { render } from 'prettyjson' +import { chalk, error, getTerminalLink, log, logJson, warn } from '../../utils/command-helpers.mjs' +import execa from '../../utils/execa.mjs' import getRepoData from '../../utils/get-repo-data.mjs' -import utils from '../../utils/index.cjs' -import { getGitHubToken } from '../../utils/init/config-github.cjs' -import { configureRepo } from '../../utils/init/config.cjs' -import { createRepo, getTemplatesFromGitHub, validateTemplate } from '../../utils/sites/utils.cjs' +import { getGitHubToken } from '../../utils/init/config-github.mjs' +import { configureRepo } from '../../utils/init/config.mjs' +import { createRepo, getTemplatesFromGitHub, validateTemplate } from '../../utils/sites/utils.mjs' import { track } from '../../utils/telemetry/index.mjs' import { getSiteNameInput } from './sites-create.mjs' -const { chalk, error, execa, getTerminalLink, log, logJson, warn } = utils - export const fetchTemplates = async (token) => { const templatesFromGithubOrg = await getTemplatesFromGitHub(token) @@ -170,7 +169,7 @@ const sitesCreateTemplate = async (repository, options, command) => { const siteUrl = site.ssl_url || site.url log( - prettyjson.render({ + render({ 'Admin URL': site.admin_url, URL: siteUrl, 'Site ID': site.id, diff --git a/src/commands/sites/sites-create.mjs b/src/commands/sites/sites-create.mjs index 21e18e08abf..dd53806ee40 100644 --- a/src/commands/sites/sites-create.mjs +++ b/src/commands/sites/sites-create.mjs @@ -4,14 +4,12 @@ import inquirer from 'inquirer' import pick from 'lodash/pick.js' import prettyjson from 'prettyjson' +import { chalk, error, log, logJson, warn } from '../../utils/command-helpers.mjs' import getRepoData from '../../utils/get-repo-data.mjs' -import utils from '../../utils/index.cjs' -import { configureRepo } from '../../utils/init/config.cjs' +import { configureRepo } from '../../utils/init/config.mjs' import { track } from '../../utils/telemetry/index.mjs' import { link } from '../link/index.mjs' -const { chalk, error, log, logJson, warn } = utils - export const getSiteNameInput = async (name) => { if (!name) { const { name: nameInput } = await inquirer.prompt([ diff --git a/src/commands/sites/sites-delete.mjs b/src/commands/sites/sites-delete.mjs index ba0477f946f..ec4f0014bec 100644 --- a/src/commands/sites/sites-delete.mjs +++ b/src/commands/sites/sites-delete.mjs @@ -1,9 +1,7 @@ // @ts-check import inquirer from 'inquirer' -import utils from '../../utils/index.cjs' - -const { chalk, error, exit, log } = utils +import { chalk, error, exit, log } from '../../utils/command-helpers.mjs' /** * The sites:delete command diff --git a/src/commands/sites/sites-list.mjs b/src/commands/sites/sites-list.mjs index 25c71819852..ae25cf52ce6 100644 --- a/src/commands/sites/sites-list.mjs +++ b/src/commands/sites/sites-list.mjs @@ -1,9 +1,7 @@ // @ts-check import { listSites } from '../../lib/api.mjs' import { startSpinner, stopSpinner } from '../../lib/spinner.cjs' -import utils from '../../utils/index.cjs' - -const { chalk, log, logJson } = utils +import { chalk, log, logJson } from '../../utils/command-helpers.mjs' /** * The sites:list command diff --git a/src/commands/status/status-hooks.mjs b/src/commands/status/status-hooks.mjs index e4584157590..58acd9961f3 100644 --- a/src/commands/status/status-hooks.mjs +++ b/src/commands/status/status-hooks.mjs @@ -2,9 +2,7 @@ import { get } from 'dot-prop' import prettyjson from 'prettyjson' -import utils from '../../utils/index.cjs' - -const { error, log, warn } = utils +import { error, log, warn } from '../../utils/command-helpers.mjs' /** * The status:hooks command diff --git a/src/commands/status/status.mjs b/src/commands/status/status.mjs index a1e6440c407..6772f244746 100644 --- a/src/commands/status/status.mjs +++ b/src/commands/status/status.mjs @@ -2,12 +2,10 @@ import clean from 'clean-deep' import prettyjson from 'prettyjson' -import utils from '../../utils/index.cjs' +import { chalk, error, exit, getToken, log, logJson, warn } from '../../utils/command-helpers.mjs' import { createStatusHooksCommand } from './status-hooks.mjs' -const { chalk, error, exit, getToken, log, logJson, warn } = utils - /** * The status command * @param {import('commander').OptionValues} options diff --git a/src/commands/switch/switch.mjs b/src/commands/switch/switch.mjs index df817ae7eb8..196d0b2e4ae 100644 --- a/src/commands/switch/switch.mjs +++ b/src/commands/switch/switch.mjs @@ -1,11 +1,9 @@ // @ts-check import inquirer from 'inquirer' -import utils from '../../utils/index.cjs' +import { chalk, log } from '../../utils/command-helpers.mjs' import { login } from '../login/index.mjs' -const { chalk, log } = utils - const LOGIN_NEW = 'I would like to login to a new account' /** diff --git a/src/commands/unlink/unlink.mjs b/src/commands/unlink/unlink.mjs index f80c3adc2fe..39c20cb50cb 100644 --- a/src/commands/unlink/unlink.mjs +++ b/src/commands/unlink/unlink.mjs @@ -1,9 +1,7 @@ // @ts-check -import utils from '../../utils/index.cjs' +import { exit, log } from '../../utils/command-helpers.mjs' import { track } from '../../utils/telemetry/index.mjs' -const { exit, log } = utils - /** * The unlink command * @param {import('commander').OptionValues} options diff --git a/src/commands/watch/watch.mjs b/src/commands/watch/watch.mjs index 070f096455f..0bab5f56d98 100644 --- a/src/commands/watch/watch.mjs +++ b/src/commands/watch/watch.mjs @@ -3,11 +3,9 @@ import pWaitFor from 'p-wait-for' import prettyjson from 'prettyjson' import { startSpinner, stopSpinner } from '../../lib/spinner.cjs' -import utils from '../../utils/index.cjs' +import { chalk, error, log } from '../../utils/command-helpers.mjs' import { init } from '../init/index.mjs' -const { chalk, error, log } = utils - // 1 second const INIT_WAIT = 1e3 diff --git a/src/lib/account.cjs b/src/lib/account.cjs deleted file mode 100644 index be6cd4d12ff..00000000000 --- a/src/lib/account.cjs +++ /dev/null @@ -1,9 +0,0 @@ -const dotProp = require('dot-prop') - -const supportsBooleanCapability = (account, capability) => dotProp.get(account, `capabilities.${capability}.included`) - -const supportsBackgroundFunctions = (account) => supportsBooleanCapability(account, 'background_functions') - -module.exports = { - supportsBackgroundFunctions, -} diff --git a/src/lib/account.mjs b/src/lib/account.mjs new file mode 100644 index 00000000000..c9923609a81 --- /dev/null +++ b/src/lib/account.mjs @@ -0,0 +1,14 @@ +// @ts-check + +/** + * @param {any} account + * @param {string} capability + * @returns {boolean} + */ +const supportsBooleanCapability = (account, capability) => Boolean(account?.capabilities?.[capability]?.included) + +/** + * @param {any} account + * @returns {boolean} + */ +export const supportsBackgroundFunctions = (account) => supportsBooleanCapability(account, 'background_functions') diff --git a/src/lib/api.mjs b/src/lib/api.mjs index 6e0a89f71ef..56a727d0024 100644 --- a/src/lib/api.mjs +++ b/src/lib/api.mjs @@ -1,4 +1,4 @@ -import { warn } from '../utils/command-helpers.cjs' +import { warn } from '../utils/command-helpers.mjs' export const cancelDeploy = async ({ api, deployId }) => { try { diff --git a/src/lib/completion/generate-autocompletion.mjs b/src/lib/completion/generate-autocompletion.mjs index 5a8623725bd..09a34046d5a 100644 --- a/src/lib/completion/generate-autocompletion.mjs +++ b/src/lib/completion/generate-autocompletion.mjs @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs' import { dirname } from 'path' -import { sortOptions, warn } from '../../utils/command-helpers.cjs' +import { sortOptions, warn } from '../../utils/command-helpers.mjs' import { AUTOCOMPLETION_FILE } from './constants.mjs' diff --git a/src/lib/edge-functions/consts.cjs b/src/lib/edge-functions/consts.cjs deleted file mode 100644 index 5c85ee789bf..00000000000 --- a/src/lib/edge-functions/consts.cjs +++ /dev/null @@ -1,11 +0,0 @@ -const DIST_IMPORT_MAP_PATH = 'edge-functions-import-map.json' -const INTERNAL_EDGE_FUNCTIONS_FOLDER = 'edge-functions' -const EDGE_FUNCTIONS_FOLDER = 'edge-functions-dist' -const PUBLIC_URL_PATH = '.netlify/internal/edge-functions' - -module.exports = { - DIST_IMPORT_MAP_PATH, - INTERNAL_EDGE_FUNCTIONS_FOLDER, - EDGE_FUNCTIONS_FOLDER, - PUBLIC_URL_PATH, -} diff --git a/src/lib/edge-functions/consts.mjs b/src/lib/edge-functions/consts.mjs new file mode 100644 index 00000000000..fa8e728de27 --- /dev/null +++ b/src/lib/edge-functions/consts.mjs @@ -0,0 +1,4 @@ +export const DIST_IMPORT_MAP_PATH = 'edge-functions-import-map.json' +export const INTERNAL_EDGE_FUNCTIONS_FOLDER = 'edge-functions' +export const EDGE_FUNCTIONS_FOLDER = 'edge-functions-dist' +export const PUBLIC_URL_PATH = '.netlify/internal/edge-functions' diff --git a/src/lib/edge-functions/deploy.cjs b/src/lib/edge-functions/deploy.mjs similarity index 56% rename from src/lib/edge-functions/deploy.cjs rename to src/lib/edge-functions/deploy.mjs index 009be8e8d03..947c7837cae 100644 --- a/src/lib/edge-functions/deploy.cjs +++ b/src/lib/edge-functions/deploy.mjs @@ -1,14 +1,14 @@ // @ts-check -const { stat } = require('fs').promises -const { join } = require('path') +import { stat } from 'fs/promises' +import { join } from 'path' -const { getPathInProject } = require('../settings.cjs') +import { getPathInProject } from '../settings.cjs' -const { EDGE_FUNCTIONS_FOLDER, PUBLIC_URL_PATH } = require('./consts.cjs') +import { EDGE_FUNCTIONS_FOLDER, PUBLIC_URL_PATH } from './consts.mjs' const distPath = getPathInProject([EDGE_FUNCTIONS_FOLDER]) -const deployFileNormalizer = (rootDir, file) => { +export const deployFileNormalizer = (rootDir, file) => { const absoluteDistPath = join(rootDir, distPath) const isEdgeFunction = file.root === absoluteDistPath const normalizedPath = isEdgeFunction ? `${PUBLIC_URL_PATH}/${file.normalizedPath}` : file.normalizedPath @@ -19,7 +19,7 @@ const deployFileNormalizer = (rootDir, file) => { } } -const getDistPathIfExists = async ({ rootDir }) => { +export const getDistPathIfExists = async ({ rootDir }) => { try { const absoluteDistPath = join(rootDir, distPath) const stats = await stat(absoluteDistPath) @@ -34,10 +34,4 @@ const getDistPathIfExists = async ({ rootDir }) => { } } -const isEdgeFunctionFile = (filePath) => filePath.startsWith(`${PUBLIC_URL_PATH}/`) - -module.exports = { - deployFileNormalizer, - getDistPathIfExists, - isEdgeFunctionFile, -} +export const isEdgeFunctionFile = (filePath) => filePath.startsWith(`${PUBLIC_URL_PATH}/`) diff --git a/src/lib/edge-functions/headers.cjs b/src/lib/edge-functions/headers.mjs similarity index 85% rename from src/lib/edge-functions/headers.cjs rename to src/lib/edge-functions/headers.mjs index 80f4e74d3f0..980e45878ce 100644 --- a/src/lib/edge-functions/headers.cjs +++ b/src/lib/edge-functions/headers.mjs @@ -1,4 +1,4 @@ -module.exports = { +const headers = { ForwardedHost: 'x-forwarded-host', ForwardedProtocol: 'x-forwarded-proto', Functions: 'x-deno-functions', @@ -8,3 +8,5 @@ module.exports = { IP: 'x-nf-client-connection-ip', Site: 'X-NF-Site-Info', } + +export default headers diff --git a/src/lib/edge-functions/index.cjs b/src/lib/edge-functions/index.cjs deleted file mode 100644 index 807b925926f..00000000000 --- a/src/lib/edge-functions/index.cjs +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-check -const constants = require('./consts.cjs') -const deploy = require('./deploy.cjs') -const proxy = require('./proxy.cjs') - -module.exports = { ...constants, ...deploy, ...proxy } diff --git a/src/lib/edge-functions/internal.cjs b/src/lib/edge-functions/internal.mjs similarity index 67% rename from src/lib/edge-functions/internal.cjs rename to src/lib/edge-functions/internal.mjs index 1240d8deccf..decb0268239 100644 --- a/src/lib/edge-functions/internal.cjs +++ b/src/lib/edge-functions/internal.mjs @@ -1,13 +1,13 @@ // @ts-check -const { promises: fs } = require('fs') -const { dirname, join, resolve } = require('path') -const { cwd } = require('process') -const { pathToFileURL } = require('url') +import { readFile, stat } from 'fs/promises' +import { dirname, join, resolve } from 'path' +import { cwd } from 'process' +import { pathToFileURL } from 'url' -const { warn } = require('../../utils/command-helpers.cjs') -const { getPathInProject } = require('../settings.cjs') +import { warn } from '../../utils/command-helpers.mjs' +import { getPathInProject } from '../settings.cjs' -const { INTERNAL_EDGE_FUNCTIONS_FOLDER } = require('./consts.cjs') +import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from './consts.mjs' /** * Reads an import map from a path and returns the parsed data, if it exists @@ -18,7 +18,7 @@ const { INTERNAL_EDGE_FUNCTIONS_FOLDER } = require('./consts.cjs') */ const getImportMap = async (importMapPath) => { try { - const data = await fs.readFile(importMapPath) + const data = await readFile(importMapPath) const importMap = JSON.parse(data) return importMap @@ -29,19 +29,18 @@ const getImportMap = async (importMapPath) => { } } -const getInternalFunctions = async () => { +export const getInternalFunctions = async () => { const path = join(cwd(), getPathInProject([INTERNAL_EDGE_FUNCTIONS_FOLDER])) try { - const stats = await fs.stat(path) + const stats = await stat(path) if (!stats.isDirectory()) { throw new Error('Path is not a directory') } const manifestPath = join(path, 'manifest.json') - // eslint-disable-next-line import/no-dynamic-require, n/global-require - const manifest = require(manifestPath) + const manifest = JSON.parse(await readFile(manifestPath)) if (manifest.version !== 1) { throw new Error('Unsupported manifest format') @@ -75,5 +74,3 @@ const getInternalFunctions = async () => { } } } - -module.exports = { getInternalFunctions } diff --git a/src/lib/edge-functions/proxy.cjs b/src/lib/edge-functions/proxy.mjs similarity index 82% rename from src/lib/edge-functions/proxy.cjs rename to src/lib/edge-functions/proxy.mjs index d92b34d1d31..bf3a5149e8e 100644 --- a/src/lib/edge-functions/proxy.cjs +++ b/src/lib/edge-functions/proxy.mjs @@ -1,20 +1,20 @@ // @ts-check -const { Buffer } = require('buffer') -const { relative } = require('path') -const { cwd, env } = require('process') +import { Buffer } from 'buffer' +import { relative } from 'path' +import { cwd, env } from 'process' -const getAvailablePort = require('get-port') -const { v4: generateUUID } = require('uuid') +import getAvailablePort from 'get-port' +import { v4 as generateUUID } from 'uuid' -const { NETLIFYDEVERR, NETLIFYDEVWARN, chalk, error: printError, log } = require('../../utils/command-helpers.cjs') -const { getGeoLocation } = require('../geo-location.cjs') -const { getPathInProject } = require('../settings.cjs') -const { startSpinner, stopSpinner } = require('../spinner.cjs') +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' -const { DIST_IMPORT_MAP_PATH } = require('./consts.cjs') -const headers = require('./headers.cjs') -const { getInternalFunctions } = require('./internal.cjs') -const { EdgeFunctionsRegistry } = require('./registry.cjs') +import { DIST_IMPORT_MAP_PATH } from './consts.mjs' +import headers from './headers.mjs' +import { getInternalFunctions } from './internal.mjs' +import { EdgeFunctionsRegistry } from './registry.mjs' const headersSymbol = Symbol('Edge Functions Headers') @@ -40,20 +40,20 @@ const getDownloadUpdateFunctions = () => { } } -const handleProxyRequest = (req, proxyReq) => { +export const handleProxyRequest = (req, proxyReq) => { Object.entries(req[headersSymbol]).forEach(([header, value]) => { proxyReq.setHeader(header, value) }) } -const createSiteInfoHeader = (siteInfo = {}) => { +export const createSiteInfoHeader = (siteInfo = {}) => { const { id, name, url } = siteInfo const site = { id, name, url } const siteString = JSON.stringify(site) return Buffer.from(siteString).toString('base64') } -const initializeProxy = async ({ +export const initializeProxy = async ({ config, configPath, env: configEnv, @@ -145,7 +145,7 @@ const initializeProxy = async ({ } } -const isEdgeFunctionsRequest = (req) => req[headersSymbol] !== undefined +export const isEdgeFunctionsRequest = (req) => req[headersSymbol] !== undefined const prepareServer = async ({ certificatePath, @@ -195,5 +195,3 @@ const prepareServer = async ({ printError(error.message, { exit: false }) } } - -module.exports = { handleProxyRequest, initializeProxy, isEdgeFunctionsRequest, createSiteInfoHeader } diff --git a/src/lib/edge-functions/registry.cjs b/src/lib/edge-functions/registry.mjs similarity index 98% rename from src/lib/edge-functions/registry.cjs rename to src/lib/edge-functions/registry.mjs index 2a5c1b4799e..f5aeb7d5077 100644 --- a/src/lib/edge-functions/registry.cjs +++ b/src/lib/edge-functions/registry.mjs @@ -1,7 +1,7 @@ // @ts-check -const { fileURLToPath } = require('url') +import { fileURLToPath } from 'url' -const { NETLIFYDEVERR, NETLIFYDEVLOG, chalk, log, warn, watchDebounced } = require('../../utils/command-helpers.cjs') +import { NETLIFYDEVERR, NETLIFYDEVLOG, chalk, log, warn, watchDebounced } from '../../utils/command-helpers.mjs' /** * @typedef EdgeFunction @@ -26,7 +26,7 @@ const { NETLIFYDEVERR, NETLIFYDEVLOG, chalk, log, warn, watchDebounced } = requi /** @typedef {(EdgeFunctionDeclarationWithPath | EdgeFunctionDeclarationWithPattern) } EdgeFunctionDeclaration */ -class EdgeFunctionsRegistry { +export class EdgeFunctionsRegistry { /** * @param {Object} opts * @param {import('@netlify/edge-bundler')} opts.bundler @@ -448,5 +448,3 @@ class EdgeFunctionsRegistry { this.directoryWatchers.set(directory, watcher) } } - -module.exports = { EdgeFunctionsRegistry } diff --git a/src/lib/exec-fetcher.cjs b/src/lib/exec-fetcher.mjs similarity index 81% rename from src/lib/exec-fetcher.cjs rename to src/lib/exec-fetcher.mjs index 8f11df0811f..4d85c838a91 100644 --- a/src/lib/exec-fetcher.cjs +++ b/src/lib/exec-fetcher.mjs @@ -1,18 +1,18 @@ // @ts-check -const path = require('path') -const process = require('process') +import path from 'path' +import process from 'process' -const { fetchLatest, fetchVersion, newerVersion, updateAvailable } = require('gh-release-fetch') -const isExe = require('isexe') +import { fetchLatest, fetchVersion, newerVersion, updateAvailable } from 'gh-release-fetch' +import isExe from 'isexe' -const { NETLIFYDEVWARN, error, getTerminalLink, log } = require('../utils/command-helpers.cjs') -const execa = require('../utils/execa.cjs') +import { NETLIFYDEVWARN, error, getTerminalLink, log } from '../utils/command-helpers.mjs' +import execa from '../utils/execa.mjs' const isWindows = () => process.platform === 'win32' const getRepository = ({ packageName }) => `netlify/${packageName}` -const getExecName = ({ execName }) => (isWindows() ? `${execName}.exe` : execName) +export const getExecName = ({ execName }) => (isWindows() ? `${execName}.exe` : execName) const getOptions = () => { // this is used in out CI tests to avoid hitting GitHub API limit @@ -33,7 +33,14 @@ const isVersionOutdated = async ({ currentVersion, latestVersion, packageName }) return outdated } -const shouldFetchLatestVersion = async ({ binPath, execArgs, execName, latestVersion, packageName, pattern }) => { +export const shouldFetchLatestVersion = async ({ + binPath, + execArgs, + execName, + latestVersion, + packageName, + pattern, +}) => { const execPath = path.join(binPath, getExecName({ execName })) const exists = await isExe(execPath, { ignoreErrors: true }) @@ -69,7 +76,7 @@ const shouldFetchLatestVersion = async ({ binPath, execArgs, execName, latestVer } } -const getArch = () => { +export const getArch = () => { switch (process.arch) { case 'x64': return 'amd64' @@ -91,7 +98,7 @@ const getArch = () => { * @param {string} config.packageName * @param {string} [config.latestVersion ] */ -const fetchLatestVersion = async ({ destination, execName, extension, latestVersion, packageName }) => { +export const fetchLatestVersion = async ({ destination, execName, extension, latestVersion, packageName }) => { const win = isWindows() const arch = getArch() const platform = win ? 'windows' : process.platform @@ -133,5 +140,3 @@ ${issueLink}`) error(error_) } } - -module.exports = { getArch, getExecName, shouldFetchLatestVersion, fetchLatestVersion } diff --git a/src/lib/functions/background.cjs b/src/lib/functions/background.mjs similarity index 59% rename from src/lib/functions/background.cjs rename to src/lib/functions/background.mjs index 55a2a0fa22f..f30bd4c23bd 100644 --- a/src/lib/functions/background.cjs +++ b/src/lib/functions/background.mjs @@ -1,16 +1,16 @@ -const { NETLIFYDEVERR, NETLIFYDEVLOG } = require('../../utils/index.cjs') +import { NETLIFYDEVERR, NETLIFYDEVLOG } from '../../utils/command-helpers.mjs' -const { formatLambdaError, styleFunctionName } = require('./utils.cjs') +import { formatLambdaError, styleFunctionName } from './utils.mjs' const BACKGROUND_FUNCTION_STATUS_CODE = 202 -const handleBackgroundFunction = (functionName, response) => { +export const handleBackgroundFunction = (functionName, response) => { console.log(`${NETLIFYDEVLOG} Queueing background function ${styleFunctionName(functionName)} for execution`) response.status(BACKGROUND_FUNCTION_STATUS_CODE) response.end() } -const handleBackgroundFunctionResult = (functionName, err) => { +export const handleBackgroundFunctionResult = (functionName, err) => { if (err) { console.log( `${NETLIFYDEVERR} Error during background function ${styleFunctionName(functionName)} execution:`, @@ -20,5 +20,3 @@ const handleBackgroundFunctionResult = (functionName, err) => { console.log(`${NETLIFYDEVLOG} Done executing background function ${styleFunctionName(functionName)}`) } } - -module.exports = { handleBackgroundFunction, handleBackgroundFunctionResult } diff --git a/src/lib/functions/config.cjs b/src/lib/functions/config.mjs similarity index 84% rename from src/lib/functions/config.cjs rename to src/lib/functions/config.mjs index f892edb9bd8..01d01163452 100644 --- a/src/lib/functions/config.cjs +++ b/src/lib/functions/config.mjs @@ -1,7 +1,7 @@ // The function configuration keys returned by @netlify/config are not an exact // match to the properties that @netlify/zip-it-and-ship-it expects. We do that // translation here. -const normalizeFunctionsConfig = ({ functionsConfig = {}, projectRoot, siteEnv = {} }) => +export const normalizeFunctionsConfig = ({ functionsConfig = {}, projectRoot, siteEnv = {} }) => Object.entries(functionsConfig).reduce( (result, [pattern, config]) => ({ ...result, @@ -19,5 +19,3 @@ const normalizeFunctionsConfig = ({ functionsConfig = {}, projectRoot, siteEnv = }), {}, ) - -module.exports = { normalizeFunctionsConfig } diff --git a/src/lib/functions/form-submissions-handler.mjs b/src/lib/functions/form-submissions-handler.mjs index a61e17b1356..c9258ddc37e 100644 --- a/src/lib/functions/form-submissions-handler.mjs +++ b/src/lib/functions/form-submissions-handler.mjs @@ -5,9 +5,9 @@ import { parse as parseContentType } from 'content-type' import multiparty from 'multiparty' import getRawBody from 'raw-body' -import { warn } from '../../utils/command-helpers.cjs' +import { warn } from '../../utils/command-helpers.mjs' import { BACKGROUND } from '../../utils/functions/index.mjs' -import { capitalize } from '../string.cjs' +import { capitalize } from '../string.mjs' const getFormHandler = function ({ functionsRegistry }) { const handlers = ['submission-created', `submission-created${BACKGROUND}`] diff --git a/src/lib/functions/local-proxy.cjs b/src/lib/functions/local-proxy.mjs similarity index 71% rename from src/lib/functions/local-proxy.cjs rename to src/lib/functions/local-proxy.mjs index 33d5123efef..1b1c002653a 100644 --- a/src/lib/functions/local-proxy.cjs +++ b/src/lib/functions/local-proxy.mjs @@ -1,11 +1,11 @@ // @ts-check -const { stdout } = require('process') +import { stdout } from 'process' -const { getBinaryPath: getFunctionsProxyPath } = require('@netlify/local-functions-proxy') +import { getBinaryPath as getFunctionsProxyPath } from '@netlify/local-functions-proxy' -const { execa } = require('../../utils/index.cjs') +import execa from '../../utils/execa.mjs' -const runFunctionsProxy = ({ binaryPath, context, directory, event, name, timeout }) => { +export const runFunctionsProxy = ({ binaryPath, context, directory, event, name, timeout }) => { const functionsProxyPath = getFunctionsProxyPath() const requestData = { resource: '', @@ -43,5 +43,3 @@ const runFunctionsProxy = ({ binaryPath, context, directory, event, name, timeou return proxyProcess } - -module.exports = { runFunctionsProxy } diff --git a/src/lib/functions/memoized-build.cjs b/src/lib/functions/memoized-build.mjs similarity index 92% rename from src/lib/functions/memoized-build.cjs rename to src/lib/functions/memoized-build.mjs index 98c6930c90e..2802ccbcfb6 100644 --- a/src/lib/functions/memoized-build.cjs +++ b/src/lib/functions/memoized-build.mjs @@ -8,7 +8,7 @@ const DEBOUNCE_INTERVAL = 300 // This allows us to discard any duplicate filesystem events, while ensuring // that actual updates happening during the zip operation will be executed // after it finishes (only the last update will run). -const memoizedBuild = ({ cache, cacheKey, command }) => { +export const memoizedBuild = ({ cache, cacheKey, command }) => { if (cache[cacheKey] === undefined) { cache[cacheKey] = { // eslint-disable-next-line promise/prefer-await-to-then @@ -29,5 +29,3 @@ const memoizedBuild = ({ cache, cacheKey, command }) => { return cache[cacheKey].task } - -module.exports = { memoizedBuild } diff --git a/src/lib/functions/netlify-function.cjs b/src/lib/functions/netlify-function.mjs similarity index 93% rename from src/lib/functions/netlify-function.cjs rename to src/lib/functions/netlify-function.mjs index c8cd826f0f9..2debed4f833 100644 --- a/src/lib/functions/netlify-function.cjs +++ b/src/lib/functions/netlify-function.mjs @@ -1,9 +1,8 @@ // @ts-check -const CronParser = require('cron-parser') +import CronParser from 'cron-parser' -const { error: errorExit } = require('../../utils/command-helpers.cjs') - -const BACKGROUND_SUFFIX = '-background' +import { error as errorExit } from '../../utils/command-helpers.mjs' +import { BACKGROUND } from '../../utils/functions/get-functions.mjs' // Returns a new set with all elements of `setA` that don't exist in `setB`. const difference = (setA, setB) => new Set([...setA].filter((item) => !setB.has(item))) @@ -15,7 +14,7 @@ const getNextRun = function (schedule) { return cron.next().toDate() } -class NetlifyFunction { +export default class NetlifyFunction { constructor({ config, directory, @@ -40,7 +39,7 @@ class NetlifyFunction { // Determines whether this is a background function based on the function // name. - this.isBackground = name.endsWith(BACKGROUND_SUFFIX) + this.isBackground = name.endsWith(BACKGROUND) const functionConfig = config.functions && config.functions[name] this.schedule = functionConfig && functionConfig.schedule @@ -143,5 +142,3 @@ class NetlifyFunction { return url.href } } - -module.exports = { NetlifyFunction } diff --git a/src/lib/functions/registry.cjs b/src/lib/functions/registry.mjs similarity index 95% rename from src/lib/functions/registry.cjs rename to src/lib/functions/registry.mjs index 39d5e2ddbc9..c985a6cec15 100644 --- a/src/lib/functions/registry.cjs +++ b/src/lib/functions/registry.mjs @@ -1,26 +1,26 @@ // @ts-check -const { mkdir } = require('fs').promises -const { extname, isAbsolute, join } = require('path') -const { env } = require('process') +import { mkdir } from 'fs/promises' +import { extname, isAbsolute, join } from 'path' +import { env } from 'process' -const { - NETLIFYDEVERR, - NETLIFYDEVLOG, - NETLIFYDEVWARN, +import { chalk, getTerminalLink, log, + NETLIFYDEVERR, + NETLIFYDEVLOG, + NETLIFYDEVWARN, warn, watchDebounced, -} = require('../../utils/index.cjs') -const { getLogMessage } = require('../log.cjs') +} from '../../utils/command-helpers.mjs' +import { getLogMessage } from '../log.mjs' -const { NetlifyFunction } = require('./netlify-function.cjs') -const runtimes = require('./runtimes/index.cjs') +import NetlifyFunction from './netlify-function.mjs' +import runtimes from './runtimes/index.mjs' const ZIP_EXTENSION = '.zip' -class FunctionsRegistry { +export class FunctionsRegistry { constructor({ capabilities, config, isConnected = false, projectRoot, settings, timeouts }) { this.capabilities = capabilities this.config = config @@ -262,5 +262,3 @@ class FunctionsRegistry { } } } - -module.exports = { FunctionsRegistry } diff --git a/src/lib/functions/runtimes/go/index.cjs b/src/lib/functions/runtimes/go/index.mjs similarity index 75% rename from src/lib/functions/runtimes/go/index.cjs rename to src/lib/functions/runtimes/go/index.mjs index 2da24cff2ba..87d674e6959 100644 --- a/src/lib/functions/runtimes/go/index.cjs +++ b/src/lib/functions/runtimes/go/index.mjs @@ -1,13 +1,15 @@ // @ts-check -const { dirname, extname } = require('path') -const { platform } = require('process') +import { dirname, extname } from 'path' +import { platform } from 'process' -const tempy = require('tempy') +import tempy from 'tempy' + +import execa from '../../../../utils/execa.mjs' +import { runFunctionsProxy } from '../../local-proxy.mjs' const isWindows = platform === 'win32' -const { execa } = require('../../../../utils/index.cjs') -const { runFunctionsProxy } = require('../../local-proxy.cjs') +export const name = 'go' const build = async ({ binaryPath, functionDirectory }) => { try { @@ -37,14 +39,14 @@ const checkGoInstallation = async ({ cwd }) => { } } -const getBuildFunction = ({ func }) => { +export const getBuildFunction = ({ func }) => { const functionDirectory = dirname(func.mainFile) const binaryPath = tempy.file(isWindows ? { extension: 'exe' } : undefined) return () => build({ binaryPath, functionDirectory }) } -const invokeFunction = async ({ context, event, func, timeout }) => { +export const invokeFunction = async ({ context, event, func, timeout }) => { const { stdout } = await runFunctionsProxy({ binaryPath: func.buildData.binaryPath, context, @@ -70,10 +72,8 @@ const invokeFunction = async ({ context, event, func, timeout }) => { } } -const onRegister = (func) => { +export const onRegister = (func) => { const isSource = extname(func.mainFile) === '.go' return isSource ? func : null } - -module.exports = { getBuildFunction, invokeFunction, name: 'go', onRegister } diff --git a/src/lib/functions/runtimes/index.cjs b/src/lib/functions/runtimes/index.mjs similarity index 72% rename from src/lib/functions/runtimes/index.cjs rename to src/lib/functions/runtimes/index.mjs index c91c51f4914..7323faf5f54 100644 --- a/src/lib/functions/runtimes/index.cjs +++ b/src/lib/functions/runtimes/index.mjs @@ -1,11 +1,12 @@ -const go = require('./go/index.cjs') -const js = require('./js/index.cjs') -const rust = require('./rust/index.cjs') - +/* eslint-disable import/no-namespace */ +import * as go from './go/index.mjs' +import * as js from './js/index.mjs' +import * as rust from './rust/index.mjs' +/* eslint-enable import/no-namespace */ /** * @callback BuildFunction * @param {object} func - * @returns {Promise<{srcFiles: string[], buildPath?: string>} + * @returns {Promise<{srcFiles: string[], buildPath?: string}>} */ /** @@ -41,6 +42,10 @@ const rust = require('./rust/index.cjs') * @property {string} name */ -const runtimes = [js, go, rust].reduce((res, runtime) => ({ ...res, [runtime.name]: runtime }), {}) +const runtimes = { + [go.name]: go, + [js.name]: js, + [rust.name]: rust, +} -module.exports = runtimes +export default runtimes diff --git a/src/lib/functions/runtimes/js/builders/netlify-lambda.cjs b/src/lib/functions/runtimes/js/builders/netlify-lambda.mjs similarity index 74% rename from src/lib/functions/runtimes/js/builders/netlify-lambda.cjs rename to src/lib/functions/runtimes/js/builders/netlify-lambda.mjs index 4c846e7ce7a..8a2c3870fc9 100644 --- a/src/lib/functions/runtimes/js/builders/netlify-lambda.cjs +++ b/src/lib/functions/runtimes/js/builders/netlify-lambda.mjs @@ -1,14 +1,14 @@ // @ts-check -const { readFile } = require('fs').promises -const { resolve } = require('path') +import { readFile } from 'fs/promises' +import { resolve } from 'path' -const minimist = require('minimist') +import minimist from 'minimist' -const { execa } = require('../../../../../utils/index.cjs') -const { fileExistsAsync } = require('../../../../fs.cjs') -const { memoizedBuild } = require('../../../memoized-build.cjs') +import execa from '../../../../../utils/execa.mjs' +import { fileExistsAsync } from '../../../../fs.cjs' +import { memoizedBuild } from '../../../memoized-build.mjs' -const detectNetlifyLambda = async function ({ packageJson } = {}) { +export const detectNetlifyLambda = async function ({ packageJson } = {}) { const { dependencies, devDependencies, scripts } = packageJson || {} if (!((dependencies && dependencies['netlify-lambda']) || (devDependencies && devDependencies['netlify-lambda']))) { return false @@ -19,7 +19,11 @@ const detectNetlifyLambda = async function ({ packageJson } = {}) { // eslint-disable-next-line fp/no-loops for (const [key, script] of matchingScripts) { // E.g. ["netlify-lambda", "build", "functions/folder"] - const match = minimist(script.split(' ')) + const match = minimist(script.split(' '), { + // these are all valid options for netlify-lambda + boolean: ['s', 'static'], + string: ['c', 'config', 'p', 'port', 'b', 'babelrc', 't', 'timeout'], + }) // We are not interested in 'netlify-lambda' and 'build' commands const functionDirectories = match._.slice(2) if (functionDirectories.length === 1) { @@ -54,7 +58,7 @@ const detectNetlifyLambda = async function ({ packageJson } = {}) { return false } -module.exports = async function handler() { +export default async function handler() { const exists = await fileExistsAsync('package.json') if (!exists) { return false @@ -64,4 +68,3 @@ module.exports = async function handler() { const packageJson = JSON.parse(content) return detectNetlifyLambda({ packageJson }) } -module.exports.detectNetlifyLambda = detectNetlifyLambda diff --git a/src/lib/functions/runtimes/js/builders/zisi.cjs b/src/lib/functions/runtimes/js/builders/zisi.mjs similarity index 85% rename from src/lib/functions/runtimes/js/builders/zisi.cjs rename to src/lib/functions/runtimes/js/builders/zisi.mjs index a55f54c7faf..39b46307672 100644 --- a/src/lib/functions/runtimes/js/builders/zisi.cjs +++ b/src/lib/functions/runtimes/js/builders/zisi.mjs @@ -1,14 +1,17 @@ -const { mkdir, writeFile } = require('fs').promises -const path = require('path') +import { mkdir, writeFile } from 'fs/promises' +import { createRequire } from 'module' +import path from 'path' -const decache = require('decache') -const readPkgUp = require('read-pkg-up') -const sourceMapSupport = require('source-map-support') +import decache from 'decache' +import readPkgUp from 'read-pkg-up' +import sourceMapSupport from 'source-map-support' -const { NETLIFYDEVERR } = require('../../../../../utils/index.cjs') -const { getPathInProject } = require('../../../../settings.cjs') -const { normalizeFunctionsConfig } = require('../../../config.cjs') -const { memoizedBuild } = require('../../../memoized-build.cjs') +import { NETLIFYDEVERR } from '../../../../../utils/command-helpers.mjs' +import { getPathInProject } from '../../../../settings.cjs' +import { normalizeFunctionsConfig } from '../../../config.mjs' +import { memoizedBuild } from '../../../memoized-build.mjs' + +const require = createRequire(import.meta.url) const addFunctionsConfigDefaults = (config) => ({ ...config, @@ -26,6 +29,7 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr } const functionDirectory = path.dirname(func.mainFile) + // performance const { zipFunction } = await import('@netlify/zip-it-and-ship-it') // If we have a function at `functions/my-func/index.js` and we pass @@ -71,7 +75,7 @@ const buildFunction = async ({ cache, config, directory, func, hasTypeModule, pr * @param {string} params.mainFile * @param {string} params.projectRoot */ -const parseForSchedule = async ({ config, mainFile, projectRoot }) => { +export const parseForSchedule = async ({ config, mainFile, projectRoot }) => { const { listFunction } = await import('@netlify/zip-it-and-ship-it') const listedFunction = await listFunction(mainFile, { config: netlifyConfigToZisiConfig({ config, projectRoot }), @@ -104,7 +108,7 @@ const getTargetDirectory = async ({ errorExit }) => { const netlifyConfigToZisiConfig = ({ config, projectRoot }) => addFunctionsConfigDefaults(normalizeFunctionsConfig({ functionsConfig: config.functions, projectRoot })) -module.exports = async ({ config, directory, errorExit, func, projectRoot }) => { +export default async function handler({ config, directory, errorExit, func, projectRoot }) { const functionsConfig = netlifyConfigToZisiConfig({ config, projectRoot }) const packageJson = await readPkgUp(func.mainFile) @@ -138,5 +142,3 @@ module.exports = async ({ config, directory, errorExit, func, projectRoot }) => target: targetDirectory, } } - -module.exports.parseForSchedule = parseForSchedule diff --git a/src/lib/functions/runtimes/js/index.cjs b/src/lib/functions/runtimes/js/index.mjs similarity index 75% rename from src/lib/functions/runtimes/js/index.cjs rename to src/lib/functions/runtimes/js/index.mjs index dfa9b195fce..0af80c7c836 100644 --- a/src/lib/functions/runtimes/js/index.cjs +++ b/src/lib/functions/runtimes/js/index.mjs @@ -1,12 +1,14 @@ -const { dirname } = require('path') +import { dirname } from 'path' -const lambdaLocal = require('lambda-local') -const winston = require('winston') +import lambdaLocal from 'lambda-local' +import winston from 'winston' -const detectNetlifyLambdaBuilder = require('./builders/netlify-lambda.cjs') -const detectZisiBuilder = require('./builders/zisi.cjs') +import detectNetlifyLambdaBuilder from './builders/netlify-lambda.mjs' +import detectZisiBuilder, { parseForSchedule } from './builders/zisi.mjs' -const SECONDS_TO_MILLISECONDS = 1e3 +export const name = 'js' + +const SECONDS_TO_MILLISECONDS = 1000 let netlifyLambdaDetectorCache @@ -28,7 +30,7 @@ const detectNetlifyLambdaWithCache = () => { return netlifyLambdaDetectorCache } -const getBuildFunction = async ({ config, directory, errorExit, func, projectRoot }) => { +export const getBuildFunction = async ({ config, directory, errorExit, func, projectRoot }) => { const netlifyLambdaBuilder = await detectNetlifyLambdaWithCache() if (netlifyLambdaBuilder) { @@ -46,12 +48,12 @@ const getBuildFunction = async ({ config, directory, errorExit, func, projectRoo // main file otherwise. const functionDirectory = dirname(func.mainFile) const srcFiles = functionDirectory === directory ? [func.mainFile] : [functionDirectory] - const schedule = await detectZisiBuilder.parseForSchedule({ mainFile: func.mainFile, config, projectRoot }) + const schedule = await parseForSchedule({ mainFile: func.mainFile, config, projectRoot }) return () => ({ schedule, srcFiles }) } -const invokeFunction = async ({ context, event, func, timeout }) => { +export const invokeFunction = async ({ context, event, func, timeout }) => { // If a function builder has defined a `buildPath` property, we use it. // Otherwise, we'll invoke the function's main file. const lambdaPath = (func.buildData && func.buildData.buildPath) || func.mainFile @@ -66,7 +68,7 @@ const invokeFunction = async ({ context, event, func, timeout }) => { return result } -const onDirectoryScan = async () => { +export const onDirectoryScan = async () => { const netlifyLambdaBuilder = await detectNetlifyLambdaWithCache() // Before we start a directory scan, we check whether netlify-lambda is being @@ -76,5 +78,3 @@ const onDirectoryScan = async () => { await netlifyLambdaBuilder.build() } } - -module.exports = { getBuildFunction, invokeFunction, name: 'js', onDirectoryScan } diff --git a/src/lib/functions/runtimes/rust/index.cjs b/src/lib/functions/runtimes/rust/index.mjs similarity index 67% rename from src/lib/functions/runtimes/rust/index.cjs rename to src/lib/functions/runtimes/rust/index.mjs index 6aac2e87cae..cc3c5cb40d0 100644 --- a/src/lib/functions/runtimes/rust/index.cjs +++ b/src/lib/functions/runtimes/rust/index.mjs @@ -1,16 +1,18 @@ // @ts-check -const { readFile } = require('fs').promises -const { dirname, extname, join, resolve } = require('path') -const { platform } = require('process') +import { readFile } from 'fs/promises' +import { dirname, extname, join, resolve } from 'path' +import { platform } from 'process' -const findUp = require('find-up') -const toml = require('toml') +import findUp from 'find-up' +import toml from 'toml' + +import execa from '../../../../utils/execa.mjs' +import { getPathInProject } from '../../../settings.cjs' +import { runFunctionsProxy } from '../../local-proxy.mjs' const isWindows = platform === 'win32' -const { execa } = require('../../../../utils/index.cjs') -const { getPathInProject } = require('../../../settings.cjs') -const { runFunctionsProxy } = require('../../local-proxy.cjs') +export const name = 'rs' const build = async ({ func }) => { const functionDirectory = dirname(func.mainFile) @@ -30,7 +32,7 @@ const build = async ({ func }) => { } } -const getBuildFunction = +export const getBuildFunction = ({ func }) => () => build({ func }) @@ -38,12 +40,12 @@ const getBuildFunction = const getCrateName = async (cwd) => { const manifestPath = await findUp('Cargo.toml', { cwd, type: 'file' }) const manifest = await readFile(manifestPath, 'utf-8') - const { package } = toml.parse(manifest) + const { package: CargoPackage } = toml.parse(manifest) - return package.name + return CargoPackage.name } -const invokeFunction = async ({ context, event, func, timeout }) => { +export const invokeFunction = async ({ context, event, func, timeout }) => { const { stdout } = await runFunctionsProxy({ binaryPath: func.buildData.binaryPath, context, @@ -69,10 +71,8 @@ const invokeFunction = async ({ context, event, func, timeout }) => { } } -const onRegister = (func) => { +export const onRegister = (func) => { const isSource = extname(func.mainFile) === '.rs' return isSource ? func : null } - -module.exports = { getBuildFunction, invokeFunction, name: 'rs', onRegister } diff --git a/src/lib/functions/scheduled.mjs b/src/lib/functions/scheduled.mjs index 0e03395ce55..fa7d76ef26b 100644 --- a/src/lib/functions/scheduled.mjs +++ b/src/lib/functions/scheduled.mjs @@ -2,7 +2,7 @@ import AnsiToHtml from 'ansi-to-html' import { CLOCKWORK_USERAGENT } from '../../utils/functions/index.mjs' -import { formatLambdaError } from './utils.cjs' +import { formatLambdaError } from './utils.mjs' const ansiToHtml = new AnsiToHtml() diff --git a/src/lib/functions/server.mjs b/src/lib/functions/server.mjs index cbfbcf3864c..f53d6c164d8 100644 --- a/src/lib/functions/server.mjs +++ b/src/lib/functions/server.mjs @@ -2,17 +2,16 @@ import { get } from 'dot-prop' import jwtDecode from 'jwt-decode' +import { NETLIFYDEVERR, NETLIFYDEVLOG, error as errorExit, log } from '../../utils/command-helpers.mjs' +import { generateNetlifyGraphJWT } from '../../utils/dev.mjs' import { CLOCKWORK_USERAGENT, getInternalFunctionsDir } from '../../utils/functions/index.mjs' -import utils from '../../utils/index.cjs' -import { handleBackgroundFunction, handleBackgroundFunctionResult } from './background.cjs' +import { handleBackgroundFunction, handleBackgroundFunctionResult } from './background.mjs' import { createFormSubmissionHandler } from './form-submissions-handler.mjs' -import { FunctionsRegistry } from './registry.cjs' +import { FunctionsRegistry } from './registry.mjs' import { handleScheduledFunction } from './scheduled.mjs' -import { handleSynchronousFunction } from './synchronous.cjs' -import { shouldBase64Encode } from './utils.cjs' - -const { NETLIFYDEVERR, NETLIFYDEVLOG, error: errorExit, generateNetlifyGraphJWT, log } = utils +import { handleSynchronousFunction } from './synchronous.mjs' +import { shouldBase64Encode } from './utils.mjs' const buildClientContext = function (headers) { // inject a client context based on auth header, ported over from netlify-lambda (https://github.com/netlify/netlify-lambda/pull/57) diff --git a/src/lib/functions/synchronous.cjs b/src/lib/functions/synchronous.mjs similarity index 86% rename from src/lib/functions/synchronous.cjs rename to src/lib/functions/synchronous.mjs index 1ed4e4dac0f..479dd66611a 100644 --- a/src/lib/functions/synchronous.cjs +++ b/src/lib/functions/synchronous.mjs @@ -1,10 +1,10 @@ // @ts-check -const { Buffer } = require('buffer') +import { Buffer } from 'buffer' -const { NETLIFYDEVERR } = require('../../utils/index.cjs') -const renderErrorTemplate = require('../render-error-remplate.cjs') +import { NETLIFYDEVERR } from '../../utils/command-helpers.mjs' +import renderErrorTemplate from '../render-error-template.mjs' -const { detectAwsSdkError } = require('./utils.cjs') +import { detectAwsSdkError } from './utils.mjs' const addHeaders = (headers, response) => { if (!headers) { @@ -16,7 +16,7 @@ const addHeaders = (headers, response) => { }) } -const handleSynchronousFunction = function (err, result, request, response) { +export const handleSynchronousFunction = function (err, result, request, response) { if (err) { return handleErr(err, request, response) } @@ -82,5 +82,3 @@ const validateLambdaResponse = (lambdaResponse) => { return {} } - -module.exports = { handleSynchronousFunction } diff --git a/src/lib/functions/utils.cjs b/src/lib/functions/utils.mjs similarity index 63% rename from src/lib/functions/utils.cjs rename to src/lib/functions/utils.mjs index 4af03986350..974abfd1cb9 100644 --- a/src/lib/functions/utils.cjs +++ b/src/lib/functions/utils.mjs @@ -1,14 +1,8 @@ // @ts-check -const { chalk, warn } = require('../../utils/index.cjs') -const { getLogMessage } = require('../log.cjs') +import { chalk, warn } from '../../utils/command-helpers.mjs' +import { getLogMessage } from '../log.mjs' -const DEFAULT_LAMBDA_OPTIONS = { - verboseLevel: 3, -} - -const SECONDS_TO_MILLISECONDS = 1000 - -const detectAwsSdkError = ({ error }) => { +export const detectAwsSdkError = ({ error }) => { const isAwsSdkError = error && error.errorMessage && error.errorMessage.includes("Cannot find module 'aws-sdk'") if (isAwsSdkError) { @@ -16,7 +10,7 @@ const detectAwsSdkError = ({ error }) => { } } -const formatLambdaError = (err) => chalk.red(`${err.errorType}: ${err.errorMessage}`) +export const formatLambdaError = (err) => chalk.red(`${err.errorType}: ${err.errorMessage}`) // should be equivalent to https://github.com/netlify/proxy/blob/main/pkg/functions/request.go#L105 const exceptionsList = new Set([ @@ -33,7 +27,7 @@ const exceptionsList = new Set([ * @param {string | undefined} contentType * @returns {boolean} */ -const shouldBase64Encode = function (contentType) { +export const shouldBase64Encode = function (contentType) { if (!contentType) { return true } @@ -57,13 +51,4 @@ const shouldBase64Encode = function (contentType) { return true } -const styleFunctionName = (name) => chalk.magenta(name) - -module.exports = { - detectAwsSdkError, - DEFAULT_LAMBDA_OPTIONS, - formatLambdaError, - SECONDS_TO_MILLISECONDS, - shouldBase64Encode, - styleFunctionName, -} +export const styleFunctionName = (name) => chalk.magenta(name) diff --git a/src/lib/geo-location.cjs b/src/lib/geo-location.mjs similarity index 94% rename from src/lib/geo-location.cjs rename to src/lib/geo-location.mjs index 81b352ccc9d..5da6f3aeff5 100644 --- a/src/lib/geo-location.cjs +++ b/src/lib/geo-location.mjs @@ -1,5 +1,5 @@ // @ts-check -const fetch = require('node-fetch') +import fetch from 'node-fetch' const API_URL = 'https://netlifind.netlify.app' const STATE_GEO_PROPERTY = 'geolocation' @@ -24,7 +24,7 @@ const REQUEST_TIMEOUT = 1e4 * @property {string} timezone */ -const mockLocation = { +export const mockLocation = { city: 'San Francisco', country: { code: 'US', name: 'United States' }, subdivision: { code: 'CA', name: 'California' }, @@ -44,7 +44,7 @@ const mockLocation = { * @param {import('../utils/state-config.mjs').default} params.state * @returns {Promise} */ -const getGeoLocation = async ({ geoCountry, mode, offline, state }) => { +export const getGeoLocation = async ({ geoCountry, mode, offline, state }) => { const cacheObject = state.get(STATE_GEO_PROPERTY) // If `--country` was used, we also set `--mode=mock`. @@ -117,5 +117,3 @@ const getGeoLocationFromAPI = async () => { return geo } - -module.exports = { getGeoLocation, mockLocation } diff --git a/src/lib/http-agent.mjs b/src/lib/http-agent.mjs index 3f4c98f2a10..6f17e8fc51f 100644 --- a/src/lib/http-agent.mjs +++ b/src/lib/http-agent.mjs @@ -4,7 +4,7 @@ import { readFile } from 'fs/promises' import HttpsProxyAgent from 'https-proxy-agent' import waitPort from 'wait-port' -import { NETLIFYDEVERR, NETLIFYDEVWARN, exit, log } from '../utils/command-helpers.cjs' +import { NETLIFYDEVERR, NETLIFYDEVWARN, exit, log } from '../utils/command-helpers.mjs' // https://github.com/TooTallNate/node-https-proxy-agent/issues/89 // Maybe replace with https://github.com/delvedor/hpagent diff --git a/src/lib/log.cjs b/src/lib/log.mjs similarity index 87% rename from src/lib/log.cjs rename to src/lib/log.mjs index da356834071..6ab741e9df0 100644 --- a/src/lib/log.cjs +++ b/src/lib/log.mjs @@ -1,6 +1,6 @@ -const dotProp = require('dot-prop') +import dotProp from 'dot-prop' -const { chalk } = require('../utils/index.cjs') +import { chalk } from '../utils/command-helpers.mjs' const RED_BACKGROUND = chalk.red('-background') const [PRO, BUSINESS, ENTERPRISE] = ['Pro', 'Business', 'Enterprise'].map((plan) => chalk.magenta(plan)) @@ -24,8 +24,4 @@ const messages = { }, } -const getLogMessage = (key) => dotProp.get(messages, key, 'Missing Log Message Key') - -module.exports = { - getLogMessage, -} +export const getLogMessage = (key) => dotProp.get(messages, key, 'Missing Log Message Key') diff --git a/src/lib/one-graph/cli-client.mjs b/src/lib/one-graph/cli-client.mjs index b4b1131400f..6b6783c9282 100644 --- a/src/lib/one-graph/cli-client.mjs +++ b/src/lib/one-graph/cli-client.mjs @@ -13,8 +13,9 @@ import gitRepoInfo from 'git-repo-info' import WSL from 'is-wsl' import { GraphQL, InternalConsole, NetlifyGraph, NetlifyGraphLockfile, OneGraphClient } from 'netlify-onegraph-internal' +import { chalk, error, log, warn, watchDebounced } from '../../utils/command-helpers.mjs' +import execa from '../../utils/execa.mjs' import getPackageJson from '../../utils/get-package-json.mjs' -import utils from '../../utils/index.cjs' import { generateFunctionsFile, @@ -28,7 +29,6 @@ import { writeGraphQLSchemaFile, } from './cli-netlify-graph.mjs' -const { chalk, error, execa, log, warn, watchDebounced } = utils const { parse } = GraphQL const { defaultExampleOperationsDoc, extractFunctionsFromOperationDoc } = NetlifyGraph diff --git a/src/lib/one-graph/cli-netlify-graph.mjs b/src/lib/one-graph/cli-netlify-graph.mjs index 9c122e19e0a..57f88465e62 100644 --- a/src/lib/one-graph/cli-netlify-graph.mjs +++ b/src/lib/one-graph/cli-netlify-graph.mjs @@ -10,11 +10,10 @@ import inquirer from 'inquirer' import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt' import { GraphQL, GraphQLHelpers, IncludedCodegen, InternalConsole, NetlifyGraph } from 'netlify-onegraph-internal' +import { chalk, error, log, warn } from '../../utils/command-helpers.mjs' import detectServerSettings from '../../utils/detect-server-settings.mjs' +import execa from '../../utils/execa.mjs' import { getFunctionsDir } from '../../utils/functions/index.mjs' -import utils from '../../utils/index.cjs' - -const { chalk, error, execa, log, warn } = utils const { printSchema } = GraphQL diff --git a/src/lib/path.cjs b/src/lib/path.cjs deleted file mode 100644 index fb2d46f2403..00000000000 --- a/src/lib/path.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const normalizeBackslash = (path) => path.replace(/\\/g, '/') - -module.exports = { normalizeBackslash } diff --git a/src/lib/path.mjs b/src/lib/path.mjs new file mode 100644 index 00000000000..1b18ca45995 --- /dev/null +++ b/src/lib/path.mjs @@ -0,0 +1 @@ +export const normalizeBackslash = (path) => path.replace(/\\/g, '/') diff --git a/src/lib/render-error-remplate.cjs b/src/lib/render-error-template.mjs similarity index 64% rename from src/lib/render-error-remplate.cjs rename to src/lib/render-error-template.mjs index 766f153b666..8d90c96d505 100644 --- a/src/lib/render-error-remplate.cjs +++ b/src/lib/render-error-template.mjs @@ -1,17 +1,21 @@ -const { readFile } = require('fs').promises -const { join } = require('path') +import { readFile } from 'fs/promises' +import { dirname, join } from 'path' +import { fileURLToPath } from 'url' let errorTemplateFile +const dir = dirname(fileURLToPath(import.meta.url)) const renderErrorTemplate = async (errString, templatePath, functionType) => { const errorDetailsRegex = //g const functionTypeRegex = //g + try { - errorTemplateFile = errorTemplateFile || (await readFile(join(__dirname, templatePath), 'utf-8')) + errorTemplateFile = errorTemplateFile || (await readFile(join(dir, templatePath), 'utf-8')) + return errorTemplateFile.replace(errorDetailsRegex, errString).replace(functionTypeRegex, functionType) } catch { return errString } } -module.exports = renderErrorTemplate +export default renderErrorTemplate diff --git a/src/lib/string.cjs b/src/lib/string.mjs similarity index 51% rename from src/lib/string.cjs rename to src/lib/string.mjs index 4a12fc76f96..dc13d74157c 100644 --- a/src/lib/string.cjs +++ b/src/lib/string.mjs @@ -1,5 +1,3 @@ -const capitalize = function (t) { +export const capitalize = function (t) { return t.replace(/(^\w|\s\w)/g, (string) => string.toUpperCase()) } - -module.exports = { capitalize } diff --git a/src/recipes/vscode/index.mjs b/src/recipes/vscode/index.mjs index ea6ab0a470c..dc4ef3c395c 100644 --- a/src/recipes/vscode/index.mjs +++ b/src/recipes/vscode/index.mjs @@ -3,7 +3,7 @@ import { join } from 'path' import execa from 'execa' import inquirer from 'inquirer' -import { NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, error, log } from '../../utils/command-helpers.cjs' +import { NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, error, log } from '../../utils/command-helpers.mjs' import { applySettings, getSettings, writeSettings } from './settings.mjs' diff --git a/src/utils/addons/diffs/options.mjs b/src/utils/addons/diffs/options.mjs index 276dd7fcce9..4815833653c 100644 --- a/src/utils/addons/diffs/options.mjs +++ b/src/utils/addons/diffs/options.mjs @@ -1,7 +1,7 @@ // @ts-check import ansiStyles from 'ansi-styles' -import { chalk } from '../../command-helpers.cjs' +import { chalk } from '../../command-helpers.mjs' const forceColor = new chalk.Instance({ level: 1 }) diff --git a/src/utils/addons/prepare.mjs b/src/utils/addons/prepare.mjs index 0a9f272b98a..e5cbc19e9b1 100644 --- a/src/utils/addons/prepare.mjs +++ b/src/utils/addons/prepare.mjs @@ -1,5 +1,5 @@ // @ts-check -import { chalk, error, exit, log, warn } from '../command-helpers.cjs' +import { chalk, error, exit, log, warn } from '../command-helpers.mjs' export const ADDON_VALIDATION = { EXISTS: 'EXISTS', diff --git a/src/utils/addons/prompts.mjs b/src/utils/addons/prompts.mjs index b1dc7ce7fd2..dad247ff2db 100644 --- a/src/utils/addons/prompts.mjs +++ b/src/utils/addons/prompts.mjs @@ -1,5 +1,5 @@ // @ts-check -import { chalk } from '../command-helpers.cjs' +import { chalk } from '../command-helpers.mjs' /* programmatically generate CLI prompts */ export default function generatePrompts(settings) { diff --git a/src/utils/addons/render.mjs b/src/utils/addons/render.mjs index bb9d1d2bd15..b2b1acf663f 100644 --- a/src/utils/addons/render.mjs +++ b/src/utils/addons/render.mjs @@ -1,7 +1,7 @@ // @ts-check import AsciiTable from 'ascii-table' -import { chalk } from '../command-helpers.cjs' +import { chalk } from '../command-helpers.mjs' export const renderMissingValues = function (values, manifest) { const display = values diff --git a/src/utils/command-helpers.cjs b/src/utils/command-helpers.mjs similarity index 70% rename from src/utils/command-helpers.cjs rename to src/utils/command-helpers.mjs index 9e969b5deb7..db0cbe9b801 100644 --- a/src/utils/command-helpers.cjs +++ b/src/utils/command-helpers.mjs @@ -1,22 +1,21 @@ // @ts-check -const { once } = require('events') -const os = require('os') -const process = require('process') -const { format, inspect } = require('util') +import { once } from 'events' +import os from 'os' +import process from 'process' +import { format, inspect } from 'util' -// eslint-disable-next-line no-restricted-modules -const { Instance: ChalkInstance } = require('chalk') -const chokidar = require('chokidar') -const decache = require('decache') -const WSL = require('is-wsl') -const debounce = require('lodash/debounce') -const { default: omit } = require('omit.js') -const terminalLink = require('terminal-link') +// eslint-disable-next-line no-restricted-imports +import chalkModule from 'chalk' +import chokidar from 'chokidar' +import decache from 'decache' +import WSL from 'is-wsl' +import debounce from 'lodash/debounce.js' +import terminalLink from 'terminal-link' -const { name, version } = require('../../package.json') -const { clearSpinner, startSpinner } = require('../lib/spinner.cjs') +import { clearSpinner, startSpinner } from '../lib/spinner.cjs' -const getGlobalConfig = require('./get-global-config.cjs') +import getGlobalConfig from './get-global-config.mjs' +import getPackageJson from './get-package-json.mjs' /** The parsed process argv without the binary only arguments and flags */ const argv = process.argv.slice(2) @@ -28,13 +27,13 @@ const argv = process.argv.slice(2) */ const safeChalk = function (noColors) { if (noColors) { - const colorlessChalk = new ChalkInstance({ level: 0 }) + const colorlessChalk = new chalkModule.Instance({ level: 0 }) return colorlessChalk } - return new ChalkInstance() + return new chalkModule.Instance() } -const chalk = safeChalk(argv.includes('--json')) +export const chalk = safeChalk(argv.includes('--json')) /** * Adds the filler to the start of the string @@ -43,24 +42,26 @@ const chalk = safeChalk(argv.includes('--json')) * @param {string} [filler] * @returns {string} */ -const padLeft = (str, count, filler = ' ') => str.padStart(str.length + count, filler) +export const padLeft = (str, count, filler = ' ') => str.padStart(str.length + count, filler) const platform = WSL ? 'wsl' : os.platform() const arch = os.arch() === 'ia32' ? 'x86' : os.arch() -const USER_AGENT = `${name}/${version} ${platform}-${arch} node-${process.version}` +const { name, version } = await getPackageJson() + +export const USER_AGENT = `${name}/${version} ${platform}-${arch} node-${process.version}` /** A list of base command flags that needs to be sorted down on documentation and on help pages */ const BASE_FLAGS = new Set(['--debug', '--httpProxy', '--httpProxyCertificateFilename']) -const NETLIFY_CYAN = chalk.rgb(40, 180, 170) +export const NETLIFY_CYAN = chalk.rgb(40, 180, 170) -const NETLIFYDEV = `${chalk.greenBright('◈')} ${NETLIFY_CYAN('Netlify Dev')} ${chalk.greenBright('◈')}` -const NETLIFYDEVLOG = `${chalk.greenBright('◈')}` -const NETLIFYDEVWARN = `${chalk.yellowBright('◈')}` -const NETLIFYDEVERR = `${chalk.redBright('◈')}` +export const NETLIFYDEV = `${chalk.greenBright('◈')} ${NETLIFY_CYAN('Netlify Dev')} ${chalk.greenBright('◈')}` +export const NETLIFYDEVLOG = `${chalk.greenBright('◈')}` +export const NETLIFYDEVWARN = `${chalk.yellowBright('◈')}` +export const NETLIFYDEVERR = `${chalk.redBright('◈')}` -const BANG = process.platform === 'win32' ? '»' : '›' +export const BANG = process.platform === 'win32' ? '»' : '›' /** * Sorts two options so that the base flags are at the bottom of the list @@ -70,7 +71,7 @@ const BANG = process.platform === 'win32' ? '»' : '›' * @example * options.sort(sortOptions) */ -const sortOptions = (optionA, optionB) => { +export const sortOptions = (optionA, optionB) => { // base flags should be always at the bottom if (BASE_FLAGS.has(optionA.long) || BASE_FLAGS.has(optionB.long)) { return -1 @@ -88,7 +89,7 @@ const TOKEN_TIMEOUT = 3e5 * @param {object} config.ticket * @returns */ -const pollForToken = async ({ api, ticket }) => { +export const pollForToken = async ({ api, ticket }) => { const spinner = startSpinner({ text: 'Waiting for authorization...' }) try { const accessToken = await api.getAccessToken(ticket, { timeout: TOKEN_TIMEOUT }) @@ -118,7 +119,7 @@ const pollForToken = async ({ api, ticket }) => { * @param {string} [tokenFromOptions] optional token from the provided --auth options * @returns {Promise<[null|string, 'flag' | 'env' |'config' |'not found']>} */ -const getToken = async (tokenFromOptions) => { +export const getToken = async (tokenFromOptions) => { // 1. First honor command flag --auth if (tokenFromOptions) { return [tokenFromOptions, 'flag'] @@ -146,13 +147,13 @@ const isDefaultJson = () => argv[0] === 'functions:invoke' || (argv[0] === 'api' * logs a json message * @param {string|object} message */ -const logJson = (message = '') => { +export const logJson = (message = '') => { if (argv.includes('--json') || isDefaultJson()) { process.stdout.write(JSON.stringify(message, null, 2)) } } -const log = (message = '', ...args) => { +export const log = (message = '', ...args) => { // If --silent or --json flag passed disable logger if (argv.includes('--json') || argv.includes('--silent') || isDefaultJson()) { return @@ -165,7 +166,7 @@ const log = (message = '', ...args) => { * logs a warning message * @param {string} message */ -const warn = (message = '') => { +export const warn = (message = '') => { const bang = chalk.yellow(BANG) log(` ${bang} Warning: ${message}`) } @@ -176,7 +177,7 @@ const warn = (message = '') => { * @param {object} [options] * @param {boolean} [options.exit] */ -const error = (message = '', options = {}) => { +export const error = (message = '', options = {}) => { const err = message instanceof Error ? message : new Error(message) if (options.exit === false) { const bang = chalk.red(BANG) @@ -190,18 +191,21 @@ const error = (message = '', options = {}) => { } } -const exit = (code = 0) => { +export const exit = (code = 0) => { process.exit(code) } // When `build.publish` is not set by the user, the CLI behavior differs in // several ways. It detects it by checking if `build.publish` is `undefined`. // However, `@netlify/config` adds a default value to `build.publish`. -// This removes it. -const normalizeConfig = (config) => - config.build.publishOrigin === 'default' - ? { ...config, build: omit(config.build, ['publish', 'publishOrigin']) } - : config +// This removes 'publish' and 'publishOrigin' in this case. +export const normalizeConfig = (config) => { + // Unused var here is in order to omit 'publish' from build config + // eslint-disable-next-line no-unused-vars + const { publish, publishOrigin, ...build } = config.build + + return publishOrigin === 'default' ? { ...config, build } : config +} const DEBOUNCE_WAIT = 100 @@ -215,7 +219,7 @@ const DEBOUNCE_WAIT = 100 * @param {() => any} [opts.onChange] * @param {() => any} [opts.onUnlink] */ -const watchDebounced = async (target, { depth, onAdd = () => {}, onChange = () => {}, onUnlink = () => {} }) => { +export const watchDebounced = async (target, { depth, onAdd = () => {}, onChange = () => {}, onUnlink = () => {} }) => { const watcher = chokidar.watch(target, { depth, ignored: /node_modules/, ignoreInitial: true }) await once(watcher, 'ready') @@ -241,27 +245,4 @@ const watchDebounced = async (target, { depth, onAdd = () => {}, onChange = () = return watcher } -const getTerminalLink = (text, url) => terminalLink(text, url, { fallback: () => `${text} ${url}` }) - -module.exports = { - BANG, - chalk, - error, - exit, - getTerminalLink, - getToken, - log, - logJson, - NETLIFY_CYAN, - NETLIFYDEV, - NETLIFYDEVERR, - NETLIFYDEVLOG, - NETLIFYDEVWARN, - normalizeConfig, - padLeft, - pollForToken, - sortOptions, - USER_AGENT, - warn, - watchDebounced, -} +export const getTerminalLink = (text, url) => terminalLink(text, url, { fallback: () => `${text} ${url}` }) diff --git a/src/utils/deferred.cjs b/src/utils/create-deferred.mjs similarity index 88% rename from src/utils/deferred.cjs rename to src/utils/create-deferred.mjs index 2cfa3fa948b..376730e4da9 100644 --- a/src/utils/deferred.cjs +++ b/src/utils/create-deferred.mjs @@ -10,4 +10,4 @@ const createDeferred = () => { return { promise, reject: rejectDeferred, resolve: resolveDeferred } } -module.exports = { createDeferred } +export default createDeferred diff --git a/src/utils/deploy/constants.cjs b/src/utils/deploy/constants.cjs deleted file mode 100644 index ce715fe8d78..00000000000 --- a/src/utils/deploy/constants.cjs +++ /dev/null @@ -1,31 +0,0 @@ -// Local deploy timeout: 20 mins -const DEFAULT_DEPLOY_TIMEOUT = 1.2e6 -// Concurrent file hash calls -const DEFAULT_CONCURRENT_HASH = 1e2 -// Number of concurrent uploads -const DEFAULT_CONCURRENT_UPLOAD = 5 -// Number of files -const DEFAULT_SYNC_LIMIT = 1e2 -// Number of times to retry an upload -const DEFAULT_MAX_RETRY = 5 - -const UPLOAD_RANDOM_FACTOR = 0.5 -// 5 seconds -const UPLOAD_INITIAL_DELAY = 5e3 -// 1.5 minute -const UPLOAD_MAX_DELAY = 9e4 - -// 1 second -const DEPLOY_POLL = 1e3 - -module.exports = { - DEFAULT_DEPLOY_TIMEOUT, - DEFAULT_CONCURRENT_HASH, - DEFAULT_CONCURRENT_UPLOAD, - DEFAULT_SYNC_LIMIT, - DEFAULT_MAX_RETRY, - UPLOAD_RANDOM_FACTOR, - UPLOAD_INITIAL_DELAY, - UPLOAD_MAX_DELAY, - DEPLOY_POLL, -} diff --git a/src/utils/deploy/constants.mjs b/src/utils/deploy/constants.mjs new file mode 100644 index 00000000000..55669840ac6 --- /dev/null +++ b/src/utils/deploy/constants.mjs @@ -0,0 +1,19 @@ +// Local deploy timeout in ms: 20 mins +export const DEFAULT_DEPLOY_TIMEOUT = 1_200_000 +// Concurrent file hash calls +export const DEFAULT_CONCURRENT_HASH = 100 +// Number of concurrent uploads +export const DEFAULT_CONCURRENT_UPLOAD = 5 +// Number of files +export const DEFAULT_SYNC_LIMIT = 100 +// Number of times to retry an upload +export const DEFAULT_MAX_RETRY = 5 + +export const UPLOAD_RANDOM_FACTOR = 0.5 +// 5 seconds +export const UPLOAD_INITIAL_DELAY = 5000 +// 1.5 minute (90s) +export const UPLOAD_MAX_DELAY = 90_000 + +// 1 second +export const DEPLOY_POLL = 1000 diff --git a/src/utils/deploy/deploy-site.cjs b/src/utils/deploy/deploy-site.mjs similarity index 84% rename from src/utils/deploy/deploy-site.cjs rename to src/utils/deploy/deploy-site.mjs index 1e501d37f9e..e3f8fef67ed 100644 --- a/src/utils/deploy/deploy-site.cjs +++ b/src/utils/deploy/deploy-site.mjs @@ -1,23 +1,23 @@ -const cleanDeep = require('clean-deep') -const tempy = require('tempy') +import cleanDeep from 'clean-deep' +import tempy from 'tempy' -const edgeFunctions = require('../../lib/edge-functions/index.cjs') -const { rmdirRecursiveAsync } = require('../../lib/fs.cjs') -const { warn } = require('../command-helpers.cjs') +import { deployFileNormalizer, getDistPathIfExists, isEdgeFunctionFile } from '../../lib/edge-functions/deploy.mjs' +import { rmdirRecursiveAsync } from '../../lib/fs.cjs' +import { warn } from '../command-helpers.mjs' -const { +import { DEFAULT_CONCURRENT_HASH, DEFAULT_CONCURRENT_UPLOAD, DEFAULT_DEPLOY_TIMEOUT, DEFAULT_MAX_RETRY, DEFAULT_SYNC_LIMIT, -} = require('./constants.cjs') -const { hashFiles } = require('./hash-files.cjs') -const { hashFns } = require('./hash-fns.cjs') -const { uploadFiles } = require('./upload-files.cjs') -const { getUploadList, waitForDeploy, waitForDiff } = require('./util.cjs') +} from './constants.mjs' +import hashFiles from './hash-files.mjs' +import hashFns from './hash-fns.mjs' +import uploadFiles from './upload-files.mjs' +import { getUploadList, waitForDeploy, waitForDiff } from './util.mjs' -const deploySite = async ( +export const deploySite = async ( api, siteId, dir, @@ -54,7 +54,7 @@ const deploySite = async ( phase: 'start', }) - const edgeFunctionsDistPath = await edgeFunctions.getDistPathIfExists({ rootDir }) + const edgeFunctionsDistPath = await getDistPathIfExists({ rootDir }) const [{ files, filesShaMap }, { fnShaMap, functionSchedules, functions, functionsWithNativeModules }] = await Promise.all([ hashFiles({ @@ -63,7 +63,7 @@ const deploySite = async ( directories: [configPath, dir, edgeFunctionsDistPath].filter(Boolean), filter, hashAlgorithm, - normalizer: edgeFunctions.deployFileNormalizer.bind(null, rootDir), + normalizer: deployFileNormalizer.bind(null, rootDir), statusCb, }), hashFns(fnDir, { @@ -79,7 +79,7 @@ const deploySite = async ( siteEnv, }), ]) - const edgeFunctionsCount = Object.keys(files).filter(edgeFunctions.isEdgeFunctionFile).length + const edgeFunctionsCount = Object.keys(files).filter(isEdgeFunctionFile).length const filesCount = Object.keys(files).length - edgeFunctionsCount const functionsCount = Object.keys(functions).length const stats = buildStatsString([ @@ -186,5 +186,3 @@ const buildStatsString = (possibleParts) => { return parts.length > 1 ? `${message} and ${parts[parts.length - 1]}` : message } - -module.exports = { deploySite } diff --git a/src/utils/deploy/hash-files.cjs b/src/utils/deploy/hash-files.mjs similarity index 75% rename from src/utils/deploy/hash-files.cjs rename to src/utils/deploy/hash-files.mjs index beef3892fac..1b73d111150 100644 --- a/src/utils/deploy/hash-files.cjs +++ b/src/utils/deploy/hash-files.mjs @@ -1,9 +1,11 @@ -const { promisify } = require('util') +import { promisify } from 'util' -const walker = require('folder-walker') -const pump = promisify(require('pump')) +import walker from 'folder-walker' +import pumpModule from 'pump' -const { fileFilterCtor, fileNormalizerCtor, hasherCtor, manifestCollectorCtor } = require('./hasher-segments.cjs') +import { fileFilterCtor, fileNormalizerCtor, hasherCtor, manifestCollectorCtor } from './hasher-segments.mjs' + +const pump = promisify(pumpModule) const hashFiles = async ({ assetType = 'file', @@ -33,4 +35,4 @@ const hashFiles = async ({ return { files, filesShaMap } } -module.exports = { hashFiles } +export default hashFiles diff --git a/src/utils/deploy/hash-fns.cjs b/src/utils/deploy/hash-fns.mjs similarity index 88% rename from src/utils/deploy/hash-fns.cjs rename to src/utils/deploy/hash-fns.mjs index 49119ec3d4b..7b1bba50a74 100644 --- a/src/utils/deploy/hash-fns.cjs +++ b/src/utils/deploy/hash-fns.mjs @@ -1,10 +1,13 @@ -const path = require('path') -const { promisify } = require('util') +import { readFile } from 'fs/promises' +import path from 'path' +import { promisify } from 'util' -const fromArray = require('from2-array') -const pump = promisify(require('pump')) +import fromArray from 'from2-array' +import pumpModule from 'pump' -const { hasherCtor, manifestCollectorCtor } = require('./hasher-segments.cjs') +import { hasherCtor, manifestCollectorCtor } from './hasher-segments.mjs' + +const pump = promisify(pumpModule) // Maximum age of functions manifest (2 minutes). const MANIFEST_FILE_TTL = 12e4 @@ -26,8 +29,8 @@ const getFunctionZips = async ({ if (manifestPath) { try { - // eslint-disable-next-line import/no-dynamic-require, n/global-require - const { functions, timestamp } = require(manifestPath) + // read manifest.json file + const { functions, timestamp } = JSON.parse(await readFile(manifestPath)) const manifestAge = Date.now() - timestamp if (manifestAge > MANIFEST_FILE_TTL) { @@ -131,4 +134,4 @@ const hashFns = async ( return { functionSchedules, functions, functionsWithNativeModules, fnShaMap } } -module.exports = { hashFns } +export default hashFns diff --git a/src/utils/deploy/hasher-segments.cjs b/src/utils/deploy/hasher-segments.mjs similarity index 71% rename from src/utils/deploy/hasher-segments.cjs rename to src/utils/deploy/hasher-segments.mjs index d36a8388e3f..c2c5acb6726 100644 --- a/src/utils/deploy/hasher-segments.cjs +++ b/src/utils/deploy/hasher-segments.mjs @@ -1,15 +1,15 @@ -const flushWriteStream = require('flush-write-stream') -const hasha = require('hasha') -const transform = require('parallel-transform') -const { objCtor: objFilterCtor } = require('through2-filter') -const { obj: map } = require('through2-map') +import flushWriteStream from 'flush-write-stream' +import hasha from 'hasha' +import transform from 'parallel-transform' +import { objCtor as objFilterCtor } from 'through2-filter' +import { obj as map } from 'through2-map' -const { normalizePath } = require('./util.cjs') +import { normalizePath } from './util.mjs' // a parallel transform stream segment ctor that hashes fileObj's created by folder-walker // TODO: use promises instead of callbacks /* eslint-disable promise/prefer-await-to-callbacks */ -const hasherCtor = ({ concurrentHash, hashAlgorithm }) => { +export const hasherCtor = ({ concurrentHash, hashAlgorithm }) => { const hashaOpts = { algorithm: hashAlgorithm } if (!concurrentHash) throw new Error('Missing required opts') return transform(concurrentHash, { objectMode: true }, async (fileObj, cb) => { @@ -24,7 +24,7 @@ const hasherCtor = ({ concurrentHash, hashAlgorithm }) => { } // Inject normalized file names into normalizedPath and assetType -const fileNormalizerCtor = ({ assetType, normalizer: normalizeFunction }) => +export const fileNormalizerCtor = ({ assetType, normalizer: normalizeFunction }) => map((fileObj) => { const normalizedFile = { ...fileObj, assetType, normalizedPath: normalizePath(fileObj.relname) } @@ -36,7 +36,7 @@ const fileNormalizerCtor = ({ assetType, normalizer: normalizeFunction }) => }) // A writable stream segment ctor that normalizes file paths, and writes shaMap's -const manifestCollectorCtor = (filesObj, shaMap, { assetType, statusCb }) => { +export const manifestCollectorCtor = (filesObj, shaMap, { assetType, statusCb }) => { if (!statusCb || !assetType) throw new Error('Missing required options') return flushWriteStream.obj((fileObj, _, cb) => { filesObj[fileObj.normalizedPath] = fileObj.hash @@ -60,11 +60,4 @@ const manifestCollectorCtor = (filesObj, shaMap, { assetType, statusCb }) => { /* eslint-enable promise/prefer-await-to-callbacks */ // transform stream ctor that filters folder-walker results for only files -const fileFilterCtor = objFilterCtor((fileObj) => fileObj.type === 'file') - -module.exports = { - hasherCtor, - fileNormalizerCtor, - manifestCollectorCtor, - fileFilterCtor, -} +export const fileFilterCtor = objFilterCtor((fileObj) => fileObj.type === 'file') diff --git a/src/utils/deploy/index.cjs b/src/utils/deploy/index.cjs deleted file mode 100644 index eb474096d06..00000000000 --- a/src/utils/deploy/index.cjs +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-check -const { deploySite } = require('./deploy-site.cjs') - -module.exports = { deploySite } diff --git a/src/utils/deploy/upload-files.cjs b/src/utils/deploy/upload-files.mjs similarity index 92% rename from src/utils/deploy/upload-files.cjs rename to src/utils/deploy/upload-files.mjs index f809330d3cf..708650bec5b 100644 --- a/src/utils/deploy/upload-files.cjs +++ b/src/utils/deploy/upload-files.mjs @@ -1,9 +1,9 @@ -const fs = require('fs') +import fs from 'fs' -const backoff = require('backoff') -const pMap = require('p-map') +import backoff from 'backoff' +import pMap from 'p-map' -const { UPLOAD_INITIAL_DELAY, UPLOAD_MAX_DELAY, UPLOAD_RANDOM_FACTOR } = require('./constants.cjs') +import { UPLOAD_INITIAL_DELAY, UPLOAD_MAX_DELAY, UPLOAD_RANDOM_FACTOR } from './constants.mjs' const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRetry, statusCb }) => { if (!concurrentUpload || !statusCb || !maxRetry) throw new Error('Missing required option concurrentUpload') @@ -115,4 +115,4 @@ const retryUpload = (uploadFn, maxRetry) => tryUpload() }) -module.exports = { uploadFiles } +export default uploadFiles diff --git a/src/utils/deploy/util.cjs b/src/utils/deploy/util.mjs similarity index 77% rename from src/utils/deploy/util.cjs rename to src/utils/deploy/util.mjs index a93deb4dd49..33c9f8b790c 100644 --- a/src/utils/deploy/util.cjs +++ b/src/utils/deploy/util.mjs @@ -1,24 +1,19 @@ -const { sep } = require('path') +import { sep } from 'path' -const pWaitFor = require('p-wait-for') +import pWaitFor from 'p-wait-for' -const { DEPLOY_POLL } = require('./constants.cjs') +import { DEPLOY_POLL } from './constants.mjs' // normalize windows paths to unix paths -const normalizePath = (relname) => { +export const normalizePath = (relname) => { if (relname.includes('#') || relname.includes('?')) { throw new Error(`Invalid filename ${relname}. Deployed filenames cannot contain # or ? characters`) } - return ( - relname - .split(sep) - // .map(segment => encodeURI(segment)) // TODO I'm fairly certain we shouldn't encodeURI here, thats only for the file upload step - .join('/') - ) + return relname.split(sep).join('/') } // poll an async deployId until its done diffing -const waitForDiff = async (api, deployId, siteId, timeout) => { +export const waitForDiff = async (api, deployId, siteId, timeout) => { // capture ready deploy during poll let deploy @@ -56,7 +51,7 @@ const waitForDiff = async (api, deployId, siteId, timeout) => { } // Poll a deployId until its ready -const waitForDeploy = async (api, deployId, siteId, timeout) => { +export const waitForDeploy = async (api, deployId, siteId, timeout) => { // capture ready deploy during poll let deploy @@ -93,14 +88,7 @@ const waitForDeploy = async (api, deployId, siteId, timeout) => { } // Transform the fileShaMap and fnShaMap into a generic shaMap that file-uploader.js can use -const getUploadList = (required, shaMap) => { +export const getUploadList = (required, shaMap) => { if (!required || !shaMap) return [] return required.flatMap((sha) => shaMap[sha]) } - -module.exports = { - normalizePath, - waitForDiff, - waitForDeploy, - getUploadList, -} diff --git a/src/utils/detect-server-settings.mjs b/src/utils/detect-server-settings.mjs index ae7cb2dc1ff..1d1fc371b02 100644 --- a/src/utils/detect-server-settings.mjs +++ b/src/utils/detect-server-settings.mjs @@ -10,8 +10,8 @@ import isPlainObject from 'is-plain-obj' import { readFileAsyncCatchError } from '../lib/fs.cjs' -import { NETLIFYDEVWARN, chalk, log } from './command-helpers.cjs' -import { acquirePort } from './dev.cjs' +import { NETLIFYDEVWARN, chalk, log } from './command-helpers.mjs' +import { acquirePort } from './dev.mjs' import { getInternalFunctionsDir } from './functions/index.mjs' const formatProperty = (str) => chalk.magenta(`'${str}'`) diff --git a/src/utils/dev.cjs b/src/utils/dev.mjs similarity index 87% rename from src/utils/dev.cjs rename to src/utils/dev.mjs index 8e1e970df21..294c2347e2b 100644 --- a/src/utils/dev.cjs +++ b/src/utils/dev.mjs @@ -1,15 +1,15 @@ // @ts-check -const process = require('process') +import process from 'process' -const { get } = require('dot-prop') -const getPort = require('get-port') -const jwt = require('jsonwebtoken') -const isEmpty = require('lodash/isEmpty') +import { get } from 'dot-prop' +import getPort from 'get-port' +import jwt from 'jsonwebtoken' +import isEmpty from 'lodash/isEmpty.js' -const { supportsBackgroundFunctions } = require('../lib/account.cjs') +import { supportsBackgroundFunctions } from '../lib/account.mjs' -const { NETLIFYDEVLOG, chalk, error, log, warn } = require('./command-helpers.cjs') -const { loadDotEnvFiles } = require('./dot-env.cjs') +import { NETLIFYDEVLOG, chalk, error, log, warn } from './command-helpers.mjs' +import { loadDotEnvFiles } from './dot-env.mjs' // Possible sources of environment variables. For the purpose of printing log messages only. Order does not matter. const ENV_VAR_SOURCES = { @@ -98,7 +98,7 @@ const BACKGROUND_FUNCTION_TIMEOUT = 900 * @param {*} config.siteInfo * @returns */ -const getSiteInformation = async ({ api, offline, site, siteInfo }) => { +export const getSiteInformation = async ({ api, offline, site, siteInfo }) => { if (site.id && !offline) { validateSiteInfo({ site, siteInfo }) const [accounts, addons] = await Promise.all([getAccounts({ api }), getAddons({ api, site })]) @@ -139,7 +139,7 @@ const getEnvSourceName = (source) => { // Takes a set of environment variables in the format provided by @netlify/config, augments it with variables from both // dot-env files and the process itself, and injects into `process.env`. -const injectEnvVariables = async ({ devConfig, env, site }) => { +export const injectEnvVariables = async ({ devConfig, env, site }) => { const environment = new Map(Object.entries(env)) const dotEnvFiles = await loadDotEnvFiles({ envFiles: devConfig.envFiles, projectDir: site.root }) @@ -186,7 +186,7 @@ const injectEnvVariables = async ({ devConfig, env, site }) => { process.env.NETLIFY_DEV = 'true' } -const acquirePort = async ({ configuredPort, defaultPort, errorMessage }) => { +export const acquirePort = async ({ configuredPort, defaultPort, errorMessage }) => { const acquiredPort = await getPort({ port: configuredPort || defaultPort }) if (configuredPort && acquiredPort !== configuredPort) { throw new Error(`${errorMessage}: '${configuredPort}'`) @@ -199,7 +199,7 @@ const acquirePort = async ({ configuredPort, defaultPort, errorMessage }) => { // - netlify_token -- the bearer token for the Netlify API // - authlify_token_id -- the authlify token ID stored for the site after // enabling API Authentication. -const generateNetlifyGraphJWT = ({ authlifyTokenId, netlifyToken, siteId }) => { +export const generateNetlifyGraphJWT = ({ authlifyTokenId, netlifyToken, siteId }) => { const claims = { netlify_token: netlifyToken, authlify_token_id: authlifyTokenId, @@ -215,17 +215,9 @@ const generateNetlifyGraphJWT = ({ authlifyTokenId, netlifyToken, siteId }) => { ) } -const processOnExit = (fn) => { +export const processOnExit = (fn) => { const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP', 'exit'] signals.forEach((signal) => { process.on(signal, fn) }) } - -module.exports = { - getSiteInformation, - injectEnvVariables, - acquirePort, - generateNetlifyGraphJWT, - processOnExit, -} diff --git a/src/utils/dot-env.cjs b/src/utils/dot-env.mjs similarity index 71% rename from src/utils/dot-env.cjs rename to src/utils/dot-env.mjs index 4e0afd5ba3b..0f31df019af 100644 --- a/src/utils/dot-env.cjs +++ b/src/utils/dot-env.mjs @@ -1,14 +1,14 @@ // @ts-check -const { readFile } = require('fs').promises -const path = require('path') +import { readFile } from 'fs/promises' +import path from 'path' -const dotenv = require('dotenv') +import dotenv from 'dotenv' -const { isFileAsync } = require('../lib/fs.cjs') +import { isFileAsync } from '../lib/fs.cjs' -const { warn } = require('./command-helpers.cjs') +import { warn } from './command-helpers.mjs' -const loadDotEnvFiles = async function ({ envFiles, projectDir }) { +export const loadDotEnvFiles = async function ({ envFiles, projectDir }) { const response = await tryLoadDotEnvFiles({ projectDir, dotenvFiles: envFiles }) const filesWithWarning = response.filter((el) => el.warning) @@ -22,7 +22,7 @@ const loadDotEnvFiles = async function ({ envFiles, projectDir }) { // in the user configuration, the order is highest to lowest const defaultEnvFiles = ['.env.development.local', '.env.local', '.env.development', '.env'] -const tryLoadDotEnvFiles = async ({ projectDir, dotenvFiles = defaultEnvFiles }) => { +export const tryLoadDotEnvFiles = async ({ projectDir, dotenvFiles = defaultEnvFiles }) => { const results = await Promise.all( dotenvFiles.map(async (file) => { const filepath = path.resolve(projectDir, file) @@ -45,5 +45,3 @@ const tryLoadDotEnvFiles = async ({ projectDir, dotenvFiles = defaultEnvFiles }) // we return in order of lowest to highest priority return results.filter(Boolean).reverse() } - -module.exports = { loadDotEnvFiles, tryLoadDotEnvFiles } diff --git a/src/utils/env/index.cjs b/src/utils/env/index.mjs similarity index 89% rename from src/utils/env/index.cjs rename to src/utils/env/index.mjs index 63c3929ee0c..3793e52615d 100644 --- a/src/utils/env/index.cjs +++ b/src/utils/env/index.mjs @@ -1,13 +1,13 @@ -const { error } = require('../command-helpers.cjs') +import { error } from '../command-helpers.mjs' -const AVAILABLE_CONTEXTS = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] -const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing'] +export const AVAILABLE_CONTEXTS = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] +export const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing'] /** * @param {string|undefined} context - The deploy context or branch of the environment variable value * @returns {Array} The normalized context or branch name */ -const normalizeContext = (context) => { +export const normalizeContext = (context) => { if (!context) { return context } @@ -33,7 +33,7 @@ const normalizeContext = (context) => { * @param {string} context - The deploy context or branch of the environment variable value * @returns {object, context_parameter: , value: string>} The matching environment variable value object */ -const findValueInValues = (values, context) => +export const findValueInValues = (values, context) => values.find((val) => { if (!AVAILABLE_CONTEXTS.includes(context)) { // the "context" option passed in is actually the name of a branch @@ -48,7 +48,7 @@ const findValueInValues = (values, context) => * @param {enum} source - The source of the environment variable * @returns {object} The dictionary of env vars that match the given source */ -const filterEnvBySource = (env, source) => +export const filterEnvBySource = (env, source) => Object.fromEntries(Object.entries(env).filter(([, variable]) => variable.sources[0] === source)) /** @@ -102,7 +102,7 @@ const fetchEnvelopeItems = async function ({ accountId, api, key, siteId }) { * }, * } */ -const formatEnvelopeData = ({ context = 'dev', envelopeItems = [], scope = 'any', source }) => +export const formatEnvelopeData = ({ context = 'dev', envelopeItems = [], scope = 'any', source }) => envelopeItems // filter by context .filter(({ values }) => Boolean(findValueInValues(values, context))) @@ -135,7 +135,7 @@ const formatEnvelopeData = ({ context = 'dev', envelopeItems = [], scope = 'any' * @param {object} siteInfo - The site object * @returns {object} An object of environment variables keys and their metadata */ -const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', scope = 'any', siteInfo }) => { +export const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', scope = 'any', siteInfo }) => { const { account_slug: accountId, id: siteId } = siteInfo const [accountEnvelopeItems, siteEnvelopeItems] = await Promise.all([ @@ -167,7 +167,7 @@ const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', scope = 'an * @param {Array>} scopes - An array of scopes * @returns {string} A human-readable, comma-separated list of scopes */ -const getHumanReadableScopes = (scopes) => { +export const getHumanReadableScopes = (scopes) => { const HUMAN_SCOPES = ['Builds', 'Functions', 'Runtime', 'Post processing'] const SCOPES_MAP = { builds: HUMAN_SCOPES[0], @@ -193,7 +193,7 @@ const getHumanReadableScopes = (scopes) => { * @param {object} env - The site's env as it exists in Mongo * @returns {Array} The array of Envelope env vars */ -const translateFromMongoToEnvelope = (env = {}) => { +export const translateFromMongoToEnvelope = (env = {}) => { const envVars = Object.entries(env).map(([key, value]) => ({ key, scopes: AVAILABLE_SCOPES, @@ -214,7 +214,7 @@ const translateFromMongoToEnvelope = (env = {}) => { * @param {string} context - The deploy context or branch of the environment variable * @returns {object} The env object as compatible with Mongo */ -const translateFromEnvelopeToMongo = (envVars = [], context = 'dev') => +export const translateFromEnvelopeToMongo = (envVars = [], context = 'dev') => envVars .sort((left, right) => (left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1)) .reduce((acc, cur) => { @@ -227,16 +227,3 @@ const translateFromEnvelopeToMongo = (envVars = [], context = 'dev') => } return acc }, {}) - -module.exports = { - AVAILABLE_CONTEXTS, - AVAILABLE_SCOPES, - findValueInValues, - filterEnvBySource, - formatEnvelopeData, - getEnvelopeEnv, - getHumanReadableScopes, - normalizeContext, - translateFromEnvelopeToMongo, - translateFromMongoToEnvelope, -} diff --git a/src/utils/execa.cjs b/src/utils/execa.cjs deleted file mode 100644 index aee604dde18..00000000000 --- a/src/utils/execa.cjs +++ /dev/null @@ -1,12 +0,0 @@ -const { env } = require('process') - -const execaLib = require('execa') - -// This is a thin layer on top of `execa` that allows consumers to provide an -// alternative path to the module location, making it easier to mock its logic -// in tests (see `tests/utils/mock-execa.js`). - -// eslint-disable-next-line import/no-dynamic-require -const execa = env.NETLIFY_CLI_EXECA_PATH ? require(env.NETLIFY_CLI_EXECA_PATH) : execaLib - -module.exports = execa diff --git a/src/utils/execa.mjs b/src/utils/execa.mjs new file mode 100644 index 00000000000..1dde8a3578b --- /dev/null +++ b/src/utils/execa.mjs @@ -0,0 +1,17 @@ +import { env } from 'process' +// This is a thin layer on top of `execa` that allows consumers to provide an +// alternative path to the module location, making it easier to mock its logic +// in tests (see `tests/utils/mock-execa.js`). +// eslint-disable-next-line import/no-mutable-exports +let execa + +if (env.NETLIFY_CLI_EXECA_PATH) { + // eslint-disable-next-line import/no-dynamic-require + const execaMock = await import(env.NETLIFY_CLI_EXECA_PATH) + execa = execaMock.default +} else { + const execaLib = await import('execa') + execa = execaLib.default +} + +export default execa diff --git a/src/utils/get-global-config.cjs b/src/utils/get-global-config.cjs deleted file mode 100644 index ec58d1b13fe..00000000000 --- a/src/utils/get-global-config.cjs +++ /dev/null @@ -1,47 +0,0 @@ -const { readFile } = require('fs').promises - -const Configstore = require('configstore') -const memoizeOne = require('memoize-one') -const { v4: uuidv4 } = require('uuid') - -const { getLegacyPathInHome, getPathInHome } = require('../lib/settings.cjs') - -const globalConfigDefaults = { - /* disable stats from being sent to Netlify */ - telemetryDisabled: false, - /* cliId */ - cliId: uuidv4(), -} - -const getGlobalConfigOnce = async function () { - const configPath = getPathInHome(['config.json']) - // Legacy config file in home ~/.netlify/config.json - const legacyPath = getLegacyPathInHome(['config.json']) - let legacyConfig - // Read legacy config if exists - try { - legacyConfig = JSON.parse(await readFile(legacyPath)) - } catch {} - // Use legacy config as default values - const defaults = { ...globalConfigDefaults, ...legacyConfig } - const configStore = new Configstore(null, defaults, { configPath }) - - return configStore -} - -const getGlobalConfig = async function () { - const retries = 3 - // eslint-disable-next-line fp/no-loops - for (let retry = 1; retry <= retries; retry++) { - try { - return await getGlobalConfigOnce() - } catch (error) { - if (retry === retries) { - throw error - } - } - } -} - -// Memoise config result so that we only load it once -module.exports = memoizeOne(getGlobalConfig) diff --git a/src/utils/get-global-config.mjs b/src/utils/get-global-config.mjs new file mode 100644 index 00000000000..9d545110519 --- /dev/null +++ b/src/utils/get-global-config.mjs @@ -0,0 +1,40 @@ +import { readFile } from 'fs/promises' + +import Configstore from 'configstore' +import { v4 as uuidv4 } from 'uuid' + +import { getLegacyPathInHome, getPathInHome } from '../lib/settings.cjs' + +const globalConfigDefaults = { + /* disable stats from being sent to Netlify */ + telemetryDisabled: false, + /* cliId */ + cliId: uuidv4(), +} + +// Memoise config result so that we only load it once +let configStore + +const getGlobalConfig = async function () { + if (!configStore) { + const configPath = getPathInHome(['config.json']) + // Legacy config file in home ~/.netlify/config.json + const legacyPath = getLegacyPathInHome(['config.json']) + let legacyConfig + // Read legacy config if exists + try { + legacyConfig = JSON.parse(await readFile(legacyPath)) + } catch {} + // Use legacy config as default values + const defaults = { ...globalConfigDefaults, ...legacyConfig } + configStore = new Configstore(null, defaults, { configPath }) + } + + return configStore +} + +export const resetConfigCache = () => { + configStore = undefined +} + +export default getGlobalConfig diff --git a/src/utils/get-repo-data.mjs b/src/utils/get-repo-data.mjs index dea7075454e..39a76cd84c9 100644 --- a/src/utils/get-repo-data.mjs +++ b/src/utils/get-repo-data.mjs @@ -8,7 +8,7 @@ import gitRepoInfo from 'git-repo-info' import gitconfiglocal from 'gitconfiglocal' import parseGitRemote from 'parse-github-url' -import { log } from './command-helpers.cjs' +import { log } from './command-helpers.mjs' /** * diff --git a/src/utils/gh-auth.cjs b/src/utils/gh-auth.mjs similarity index 89% rename from src/utils/gh-auth.cjs rename to src/utils/gh-auth.mjs index bd03f975c3a..d65f7129f07 100644 --- a/src/utils/gh-auth.cjs +++ b/src/utils/gh-auth.mjs @@ -1,15 +1,15 @@ // @ts-check // A simple ghauth inspired library for getting a personal access token -const http = require('http') -const process = require('process') +import http from 'http' +import process from 'process' -const { Octokit } = require('@octokit/rest') -const getPort = require('get-port') -const inquirer = require('inquirer') +import { Octokit } from '@octokit/rest' +import getPort from 'get-port' +import inquirer from 'inquirer' -const { log } = require('./command-helpers.cjs') -const { createDeferred } = require('./deferred.cjs') -const { openBrowser } = require('./open-browser.cjs') +import { log } from './command-helpers.mjs' +import createDeferred from './create-deferred.mjs' +import openBrowser from './open-browser.mjs' const SERVER_PORT = 3000 @@ -44,7 +44,7 @@ const promptForAuthMethod = async () => { * Authenticate with the netlify app * @returns {Promise} Returns a Promise with a token object */ -const authWithNetlify = async () => { +export const authWithNetlify = async () => { const port = await getPort({ port: SERVER_PORT }) const { promise: deferredPromise, reject: deferredReject, resolve: deferredResolve } = createDeferred() @@ -116,12 +116,10 @@ const authWithToken = async () => { * Get a GitHub token * @returns {Promise} Returns a Promise with a token object */ -const getGitHubToken = async () => { +export const getGitHubToken = async () => { log('') const withNetlify = await promptForAuthMethod() return withNetlify ? await authWithNetlify() : await authWithToken() } - -module.exports = { getGitHubToken, authWithNetlify } diff --git a/src/utils/gitignore.mjs b/src/utils/gitignore.mjs index 30f7920d34a..edc12a97fc1 100644 --- a/src/utils/gitignore.mjs +++ b/src/utils/gitignore.mjs @@ -6,7 +6,7 @@ import parseIgnore from 'parse-gitignore' import { fileExistsAsync } from '../lib/fs.cjs' -import { log } from './command-helpers.cjs' +import { log } from './command-helpers.mjs' const hasGitIgnore = async function (dir) { const gitIgnorePath = path.join(dir, '.gitignore') diff --git a/src/utils/headers.cjs b/src/utils/headers.mjs similarity index 69% rename from src/utils/headers.cjs rename to src/utils/headers.mjs index 06ed7c0a2a8..271366dda49 100644 --- a/src/utils/headers.cjs +++ b/src/utils/headers.mjs @@ -1,7 +1,6 @@ -// TODO: use static `import` after migrating this repository to pure ES modules -const netlifyHeadersParser = import('netlify-headers-parser') +import { parseAllHeaders } from 'netlify-headers-parser' -const { NETLIFYDEVERR, log } = require('./command-helpers.cjs') +import { NETLIFYDEVERR, log } from './command-helpers.mjs' /** * Get the matching headers for `path` given a set of `rules`. @@ -14,7 +13,7 @@ const { NETLIFYDEVERR, log } = require('./command-helpers.cjs') * * @returns {Object} */ -const headersForPath = function (headers, path) { +export const headersForPath = function (headers, path) { const matchingHeaders = headers.filter(({ forRegExp }) => forRegExp.test(path)).map(getHeaderValues) const headersRules = Object.assign({}, ...matchingHeaders) return headersRules @@ -24,8 +23,7 @@ const getHeaderValues = function ({ values }) { return values } -const parseHeaders = async function ({ configPath, headersFiles }) { - const { parseAllHeaders } = await netlifyHeadersParser +export const parseHeaders = async function ({ configPath, headersFiles }) { const { errors, headers } = await parseAllHeaders({ headersFiles, netlifyConfigPath: configPath, @@ -47,8 +45,3 @@ const handleHeadersErrors = function (errors) { const getErrorMessage = function ({ message }) { return message } - -module.exports = { - headersForPath, - parseHeaders, -} diff --git a/src/utils/index.cjs b/src/utils/index.cjs deleted file mode 100644 index 831bd51be4d..00000000000 --- a/src/utils/index.cjs +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-check -const commandHelpers = require('./command-helpers.cjs') -const deploy = require('./deploy/index.cjs') -const dev = require('./dev.cjs') -const env = require('./env/index.cjs') -const execa = require('./execa.cjs') -const getGlobalConfig = require('./get-global-config.cjs') -const ghAuth = require('./gh-auth.cjs') -const openBrowser = require('./open-browser.cjs') -const parseRawFlags = require('./parse-raw-flags.cjs') - -module.exports = { - ...commandHelpers, - ...deploy, - ...dev, - ...env, - ...ghAuth, - ...openBrowser, - ...parseRawFlags, - execa, - getGlobalConfig, -} diff --git a/src/utils/init/config-github.cjs b/src/utils/init/config-github.mjs similarity index 93% rename from src/utils/init/config-github.cjs rename to src/utils/init/config-github.mjs index 8e98d334c5b..3847669d783 100644 --- a/src/utils/init/config-github.cjs +++ b/src/utils/init/config-github.mjs @@ -1,10 +1,10 @@ // @ts-check -const { Octokit } = require('@octokit/rest') +import { Octokit } from '@octokit/rest' -const { chalk, error: failAndExit, log } = require('../command-helpers.cjs') -const { getGitHubToken: ghauth } = require('../gh-auth.cjs') +import { chalk, error as failAndExit, log } from '../command-helpers.mjs' +import { getGitHubToken as ghauth } from '../gh-auth.mjs' -const { createDeployKey, formatErrorMessage, getBuildSettings, saveNetlifyToml, setupSite } = require('./utils.cjs') +import { createDeployKey, formatErrorMessage, getBuildSettings, saveNetlifyToml, setupSite } from './utils.mjs' /** * @typedef Token @@ -25,7 +25,7 @@ const PAGE_SIZE = 100 * Get a valid GitHub token * @returns {Promise} */ -const getGitHubToken = async ({ globalConfig }) => { +export const getGitHubToken = async ({ globalConfig }) => { const userId = globalConfig.get('userId') /** @type {Token} */ @@ -203,7 +203,7 @@ const addNotificationHooks = async ({ api, siteId, token }) => { * @param {string} config.repoOwner * @param {string} config.siteId */ -const configGithub = async ({ command, repoName, repoOwner, siteId }) => { +export const configGithub = async ({ command, repoName, repoOwner, siteId }) => { const { netlify } = command const { api, @@ -256,5 +256,3 @@ const configGithub = async ({ command, repoName, repoOwner, siteId }) => { log() await addNotificationHooks({ siteId, api, token }) } - -module.exports = { configGithub, getGitHubToken } diff --git a/src/utils/init/config-manual.cjs b/src/utils/init/config-manual.mjs similarity index 90% rename from src/utils/init/config-manual.cjs rename to src/utils/init/config-manual.mjs index 050a63547ac..f72a814b0f4 100644 --- a/src/utils/init/config-manual.cjs +++ b/src/utils/init/config-manual.mjs @@ -1,9 +1,9 @@ // @ts-check -const inquirer = require('inquirer') +import inquirer from 'inquirer' -const { exit, log } = require('../command-helpers.cjs') +import { exit, log } from '../command-helpers.mjs' -const { createDeployKey, getBuildSettings, saveNetlifyToml, setupSite } = require('./utils.cjs') +import { createDeployKey, getBuildSettings, saveNetlifyToml, setupSite } from './utils.mjs' const addDeployKey = async ({ deployKey }) => { log('\nGive this Netlify SSH public key access to your repository:\n') @@ -58,7 +58,7 @@ const addDeployHook = async ({ deployHook }) => { * @param {*} config.repoData * @param {string} config.siteId */ -module.exports = async function configManual({ command, repoData, siteId }) { +export default async function configManual({ command, repoData, siteId }) { const { netlify } = command const { api, diff --git a/src/utils/init/config.cjs b/src/utils/init/config.mjs similarity index 79% rename from src/utils/init/config.cjs rename to src/utils/init/config.mjs index 2b76f99808c..5b02fde4c8d 100644 --- a/src/utils/init/config.cjs +++ b/src/utils/init/config.mjs @@ -1,8 +1,8 @@ // @ts-check -const { chalk, log } = require('../command-helpers.cjs') +import { chalk, log } from '../command-helpers.mjs' -const { configGithub } = require('./config-github.cjs') -const configManual = require('./config-manual.cjs') +import { configGithub } from './config-github.mjs' +import configManual from './config-manual.mjs' const logSuccess = (repoData) => { log() @@ -24,7 +24,7 @@ const logSuccess = (repoData) => { * @param {*} config.repoData * @param {string} config.siteId */ -const configureRepo = async ({ command, manual, repoData, siteId }) => { +export const configureRepo = async ({ command, manual, repoData, siteId }) => { if (manual) { await configManual({ command, siteId, repoData }) } else if (repoData.provider === 'github') { @@ -36,4 +36,3 @@ const configureRepo = async ({ command, manual, repoData, siteId }) => { logSuccess(repoData) } -module.exports = { configureRepo } diff --git a/src/utils/init/frameworks.cjs b/src/utils/init/frameworks.mjs similarity index 68% rename from src/utils/init/frameworks.cjs rename to src/utils/init/frameworks.mjs index 22580f88129..a6918640582 100644 --- a/src/utils/init/frameworks.cjs +++ b/src/utils/init/frameworks.mjs @@ -1,8 +1,7 @@ // @ts-check -const frameworkInfoPromise = import('@netlify/framework-info') +import { listFrameworks } from '@netlify/framework-info' -const getFrameworkInfo = async ({ baseDirectory, nodeVersion }) => { - const { listFrameworks } = await frameworkInfoPromise +export const getFrameworkInfo = async ({ baseDirectory, nodeVersion }) => { const frameworks = await listFrameworks({ projectDir: baseDirectory, nodeVersion }) // several frameworks can be detected - first one has highest priority if (frameworks.length !== 0) { @@ -22,5 +21,3 @@ const getFrameworkInfo = async ({ baseDirectory, nodeVersion }) => { } return {} } - -module.exports = { getFrameworkInfo } diff --git a/src/utils/init/node-version.cjs b/src/utils/init/node-version.mjs similarity index 71% rename from src/utils/init/node-version.cjs rename to src/utils/init/node-version.mjs index c67cc5ac488..95978289503 100644 --- a/src/utils/init/node-version.cjs +++ b/src/utils/init/node-version.mjs @@ -1,11 +1,11 @@ // @ts-check -const { readFile } = require('fs').promises +import { readFile } from 'fs/promises' -const { get } = require('dot-prop') -const locatePath = require('locate-path') -const nodeVersionAlias = require('node-version-alias') +import { get } from 'dot-prop' +import locatePath from 'locate-path' +import nodeVersionAlias from 'node-version-alias' -const { warn } = require('../command-helpers.cjs') +import { warn } from '../command-helpers.mjs' const DEFAULT_NODE_VERSION = '12.18.0' const NVM_FLAG_PREFIX = '--' @@ -14,7 +14,7 @@ const NVM_FLAG_PREFIX = '--' const normalizeConfiguredVersion = (version) => version.startsWith(NVM_FLAG_PREFIX) ? version.slice(NVM_FLAG_PREFIX.length) : version -const detectNodeVersion = async ({ baseDirectory, env }) => { +export const detectNodeVersion = async ({ baseDirectory, env }) => { try { const nodeVersionFile = await locatePath(['.nvmrc', '.node-version'], { cwd: baseDirectory }) const configuredVersion = @@ -31,5 +31,3 @@ const detectNodeVersion = async ({ baseDirectory, env }) => { return DEFAULT_NODE_VERSION } } - -module.exports = { detectNodeVersion } diff --git a/src/utils/init/plugins.cjs b/src/utils/init/plugins.mjs similarity index 60% rename from src/utils/init/plugins.cjs rename to src/utils/init/plugins.mjs index 9090891d603..910434689dd 100644 --- a/src/utils/init/plugins.cjs +++ b/src/utils/init/plugins.mjs @@ -1,10 +1,8 @@ const isPluginInstalled = (configPlugins, plugin) => configPlugins.some(({ package: configPlugin }) => configPlugin === plugin) -const getRecommendPlugins = (frameworkPlugins, config) => +export const getRecommendPlugins = (frameworkPlugins, config) => frameworkPlugins.filter((plugin) => !isPluginInstalled(config.plugins, plugin)) -const getUIPlugins = (configPlugins) => - configPlugins.filter(({ origin }) => origin === 'ui').map(({ package }) => ({ package })) - -module.exports = { getRecommendPlugins, getUIPlugins } +export const getUIPlugins = (configPlugins) => + configPlugins.filter(({ origin }) => origin === 'ui').map(({ package: pkg }) => ({ package: pkg })) diff --git a/src/utils/init/utils.cjs b/src/utils/init/utils.mjs similarity index 82% rename from src/utils/init/utils.cjs rename to src/utils/init/utils.mjs index 6a7b69397ca..5b100566a0c 100644 --- a/src/utils/init/utils.cjs +++ b/src/utils/init/utils.mjs @@ -1,20 +1,18 @@ // @ts-check -const { existsSync } = require('fs') -const { writeFile } = require('fs').promises -const path = require('path') -const process = require('process') +import { writeFile } from 'fs/promises' +import path from 'path' +import process from 'process' -const cleanDeep = require('clean-deep') -const inquirer = require('inquirer') -const isEmpty = require('lodash/isEmpty') +import cleanDeep from 'clean-deep' +import inquirer from 'inquirer' -const { normalizeBackslash } = require('../../lib/path.cjs') -const { log } = require('../command-helpers.cjs') -const { chalk, error: failAndExit, warn } = require('../command-helpers.cjs') +import { fileExistsAsync } from '../../lib/fs.cjs' +import { normalizeBackslash } from '../../lib/path.mjs' +import { chalk, error as failAndExit, log, warn } from '../command-helpers.mjs' -const { getFrameworkInfo } = require('./frameworks.cjs') -const { detectNodeVersion } = require('./node-version.cjs') -const { getRecommendPlugins, getUIPlugins } = require('./plugins.cjs') +import { getFrameworkInfo } from './frameworks.mjs' +import { detectNodeVersion } from './node-version.mjs' +import { getRecommendPlugins, getUIPlugins } from './plugins.mjs' const normalizeDir = ({ baseDirectory, defaultValue, dir }) => { if (dir === undefined) { @@ -85,7 +83,7 @@ const getPromptInputs = ({ defaultBaseDir, defaultBuildCmd, defaultBuildDir }) = const getBaseDirectory = ({ repositoryRoot, siteRoot }) => path.normalize(repositoryRoot) === path.normalize(siteRoot) ? process.cwd() : siteRoot -const getBuildSettings = async ({ config, env, repositoryRoot, siteRoot }) => { +export const getBuildSettings = async ({ config, env, repositoryRoot, siteRoot }) => { const baseDirectory = getBaseDirectory({ repositoryRoot, siteRoot }) const nodeVersion = await detectNodeVersion({ baseDirectory, env }) const { @@ -153,17 +151,25 @@ const getNetlifyToml = ({ ## more info on configuring this file: https://docs.netlify.com/configure-builds/file-based-configuration/ ` -const saveNetlifyToml = async ({ baseDir, buildCmd, buildDir, config, configPath, functionsDir, repositoryRoot }) => { +export const saveNetlifyToml = async ({ + baseDir, + buildCmd, + buildDir, + config, + configPath, + functionsDir, + repositoryRoot, +}) => { const tomlPathParts = [repositoryRoot, baseDir, 'netlify.toml'].filter(Boolean) const tomlPath = path.join(...tomlPathParts) - if (existsSync(tomlPath)) { + if (await fileExistsAsync(tomlPath)) { return } // We don't want to create a `netlify.toml` file that overrides existing configuration // In a monorepo the configuration can come from a repo level netlify.toml // so we make sure it doesn't by checking `configPath === undefined` - if (configPath === undefined && !isEmpty(cleanDeep(config))) { + if (configPath === undefined && Object.keys(cleanDeep(config)).length !== 0) { return } @@ -188,14 +194,14 @@ const saveNetlifyToml = async ({ baseDir, buildCmd, buildDir, config, configPath } } -const formatErrorMessage = ({ error, message }) => { +export const formatErrorMessage = ({ error, message }) => { const errorMessage = error.json ? `${error.message} - ${JSON.stringify(error.json)}` : error.message return `${message} with error: ${chalk.red(errorMessage)}` } const formatTitle = (title) => chalk.cyan(title) -const createDeployKey = async ({ api }) => { +export const createDeployKey = async ({ api }) => { try { const deployKey = await api.createDeployKey() return deployKey @@ -205,7 +211,7 @@ const createDeployKey = async ({ api }) => { } } -const updateSite = async ({ api, options, siteId }) => { +export const updateSite = async ({ api, options, siteId }) => { try { const updatedSite = await api.updateSite({ siteId, body: options }) return updatedSite @@ -215,7 +221,7 @@ const updateSite = async ({ api, options, siteId }) => { } } -const setupSite = async ({ api, configPlugins, pluginsToInstall, repo, siteId }) => { +export const setupSite = async ({ api, configPlugins, pluginsToInstall, repo, siteId }) => { const updatedSite = await updateSite({ siteId, api, @@ -225,5 +231,3 @@ const setupSite = async ({ api, configPlugins, pluginsToInstall, repo, siteId }) return updatedSite } - -module.exports = { getBuildSettings, saveNetlifyToml, formatErrorMessage, createDeployKey, updateSite, setupSite } diff --git a/src/utils/live-tunnel.mjs b/src/utils/live-tunnel.mjs index a95fa6e0722..9b79b5b20c0 100644 --- a/src/utils/live-tunnel.mjs +++ b/src/utils/live-tunnel.mjs @@ -4,11 +4,11 @@ import process from 'process' import fetch from 'node-fetch' import pWaitFor from 'p-wait-for' -import { fetchLatestVersion, shouldFetchLatestVersion } from '../lib/exec-fetcher.cjs' +import { fetchLatestVersion, shouldFetchLatestVersion } from '../lib/exec-fetcher.mjs' import { getPathInHome } from '../lib/settings.cjs' -import { NETLIFYDEVERR, NETLIFYDEVLOG, chalk, log } from './command-helpers.cjs' -import execa from './execa.cjs' +import { NETLIFYDEVERR, NETLIFYDEVLOG, chalk, log } from './command-helpers.mjs' +import execa from './execa.mjs' const PACKAGE_NAME = 'live-tunnel-client' const EXEC_NAME = PACKAGE_NAME diff --git a/src/utils/lm/install.mjs b/src/utils/lm/install.mjs index c4ae8a774a4..a02d174dea2 100644 --- a/src/utils/lm/install.mjs +++ b/src/utils/lm/install.mjs @@ -10,11 +10,11 @@ import hasbin from 'hasbin' import Listr from 'listr' import pathKey from 'path-key' -import { fetchLatestVersion, shouldFetchLatestVersion } from '../../lib/exec-fetcher.cjs' +import { fetchLatestVersion, shouldFetchLatestVersion } from '../../lib/exec-fetcher.mjs' import { fileExistsAsync, rmdirRecursiveAsync } from '../../lib/fs.cjs' -import { normalizeBackslash } from '../../lib/path.cjs' +import { normalizeBackslash } from '../../lib/path.mjs' import { getLegacyPathInHome, getPathInHome } from '../../lib/settings.cjs' -import { chalk } from '../command-helpers.cjs' +import { chalk } from '../command-helpers.mjs' import { checkGitLFSVersionStep, checkGitVersionStep, checkLFSFiltersStep } from './steps.mjs' diff --git a/src/utils/lm/requirements.mjs b/src/utils/lm/requirements.mjs index f119ca87f42..c9c15995ac2 100644 --- a/src/utils/lm/requirements.mjs +++ b/src/utils/lm/requirements.mjs @@ -1,7 +1,7 @@ // @ts-check import semver from 'semver' -import execa from '../execa.cjs' +import execa from '../execa.mjs' export const checkLFSFilters = async function () { try { diff --git a/src/utils/lm/steps.mjs b/src/utils/lm/steps.mjs index 9a3fe5c6bfe..07ee9814cc5 100644 --- a/src/utils/lm/steps.mjs +++ b/src/utils/lm/steps.mjs @@ -1,4 +1,4 @@ -import { chalk } from '../command-helpers.cjs' +import { chalk } from '../command-helpers.mjs' import { checkGitVersion, checkHelperVersion, checkLFSFilters, checkLFSVersion } from './requirements.mjs' diff --git a/src/utils/lm/ui.mjs b/src/utils/lm/ui.mjs index 3b152a53f0e..3009d0da559 100644 --- a/src/utils/lm/ui.mjs +++ b/src/utils/lm/ui.mjs @@ -2,7 +2,7 @@ import os from 'os' import boxen from 'boxen' -import { chalk, log } from '../command-helpers.cjs' +import { chalk, log } from '../command-helpers.mjs' import { getShellInfo, isBinInPath } from './install.mjs' diff --git a/src/utils/open-browser.cjs b/src/utils/open-browser.mjs similarity index 83% rename from src/utils/open-browser.cjs rename to src/utils/open-browser.mjs index e7788c9bebd..f6dc20a773c 100644 --- a/src/utils/open-browser.cjs +++ b/src/utils/open-browser.mjs @@ -1,9 +1,9 @@ -const process = require('process') +import process from 'process' -const open = require('better-opn') -const isDockerContainer = require('is-docker') +import open from 'better-opn' +import isDockerContainer from 'is-docker' -const { chalk, log } = require('./command-helpers.cjs') +import { chalk, log } from './command-helpers.mjs' const unableToOpenBrowserMessage = function ({ message, url }) { log('---------------------------') @@ -39,4 +39,4 @@ const openBrowser = async function ({ silentBrowserNoneError, url }) { } } -module.exports = { openBrowser } +export default openBrowser diff --git a/src/utils/parse-raw-flags.cjs b/src/utils/parse-raw-flags.mjs similarity index 86% rename from src/utils/parse-raw-flags.cjs rename to src/utils/parse-raw-flags.mjs index f8f576848a0..c2d36be1019 100644 --- a/src/utils/parse-raw-flags.cjs +++ b/src/utils/parse-raw-flags.mjs @@ -10,7 +10,7 @@ // // rawFlags = {stuff: yay!} // -const parseRawFlags = function (raw) { +export const parseRawFlags = function (raw) { const rawFlags = raw.reduce((acc, curr, index, array) => { if (/^-{1,2}/.test(curr)) { const key = curr.replace(/^-{1,2}/, '') @@ -28,7 +28,7 @@ const parseRawFlags = function (raw) { return rawFlags } -const aggressiveJSONParse = function (value) { +export const aggressiveJSONParse = function (value) { if (value === 'true') { return true } @@ -47,8 +47,3 @@ const aggressiveJSONParse = function (value) { } return parsed } - -module.exports = { - parseRawFlags, - aggressiveJSONParse, -} diff --git a/src/utils/proxy.mjs b/src/utils/proxy.mjs index d247aca48b0..fec0a9c2204 100644 --- a/src/utils/proxy.mjs +++ b/src/utils/proxy.mjs @@ -20,14 +20,18 @@ import locatePath from 'locate-path' import pFilter from 'p-filter' import toReadableStream from 'to-readable-stream' -import edgeFunctions from '../lib/edge-functions/index.cjs' +import { + handleProxyRequest, + initializeProxy as initializeEdgeFunctionsProxy, + isEdgeFunctionsRequest, +} from '../lib/edge-functions/proxy.mjs' import { fileExistsAsync, isFileAsync } from '../lib/fs.cjs' -import renderErrorTemplate from '../lib/render-error-remplate.cjs' +import renderErrorTemplate from '../lib/render-error-template.mjs' -import { NETLIFYDEVLOG, NETLIFYDEVWARN } from './command-helpers.cjs' +import { NETLIFYDEVLOG, NETLIFYDEVWARN } from './command-helpers.mjs' import createStreamPromise from './create-stream-promise.mjs' -import { headersForPath, parseHeaders } from './headers.cjs' -import { createRewriter, onChanges } from './rules-proxy.cjs' +import { headersForPath, parseHeaders } from './headers.mjs' +import { createRewriter, onChanges } from './rules-proxy.mjs' const decompress = util.promisify(zlib.gunzip) const shouldGenerateETag = Symbol('Internal: response should generate ETag') @@ -338,15 +342,15 @@ const initializeProxy = async function ({ configPath, distDir, host, port, proje 'Content-Type': 'text/plain', }) - const message = edgeFunctions.isEdgeFunctionsRequest(req) + const message = isEdgeFunctionsRequest(req) ? 'There was an error with an Edge Function. Please check the terminal for more details.' : 'Could not proxy request.' res.end(message) }) proxy.on('proxyReq', (proxyReq, req) => { - if (edgeFunctions.isEdgeFunctionsRequest(req)) { - edgeFunctions.handleProxyRequest(req, proxyReq) + if (isEdgeFunctionsRequest(req)) { + handleProxyRequest(req, proxyReq) } // eslint-disable-next-line no-underscore-dangle @@ -428,7 +432,7 @@ const initializeProxy = async function ({ configPath, distDir, host, port, proje const isUncaughtError = proxyRes.headers['x-nf-uncaught-error'] === '1' - if (edgeFunctions.isEdgeFunctionsRequest(req) && isUncaughtError) { + if (isEdgeFunctionsRequest(req) && isUncaughtError) { const acceptsHtml = req.headers && req.headers.accept && req.headers.accept.includes('text/html') const decompressedBody = await decompress(responseBody) const formattedBody = formatEdgeFunctionError(decompressedBody, acceptsHtml) @@ -541,7 +545,7 @@ export const startProxy = async function ({ state, }) { const functionsServer = settings.functionsPort ? `http://127.0.0.1:${settings.functionsPort}` : null - const edgeFunctionsProxy = await edgeFunctions.initializeProxy({ + const edgeFunctionsProxy = await initializeEdgeFunctionsProxy({ config, configPath, env, diff --git a/src/utils/redirects.cjs b/src/utils/redirects.mjs similarity index 76% rename from src/utils/redirects.cjs rename to src/utils/redirects.mjs index 8af1cbc4516..6a0ac422f61 100644 --- a/src/utils/redirects.cjs +++ b/src/utils/redirects.mjs @@ -1,13 +1,11 @@ // @ts-check -// TODO: use static `import` after migrating this repository to pure ES modules -const netlifyRedirectParser = import('netlify-redirect-parser') +import { parseAllRedirects } from 'netlify-redirect-parser' -const { NETLIFYDEVERR, log } = require('./command-helpers.cjs') +import { NETLIFYDEVERR, log } from './command-helpers.mjs' // Parse, normalize and validate all redirects from `_redirects` files // and `netlify.toml` -const parseRedirects = async function ({ configPath, redirectsFiles }) { - const { parseAllRedirects } = await netlifyRedirectParser +export const parseRedirects = async function ({ configPath, redirectsFiles }) { const { errors, redirects } = await parseAllRedirects({ redirectsFiles, netlifyConfigPath: configPath, @@ -52,5 +50,3 @@ const normalizeRedirect = function ({ }, } } - -module.exports = { parseRedirects } diff --git a/src/utils/rules-proxy.cjs b/src/utils/rules-proxy.mjs similarity index 78% rename from src/utils/rules-proxy.cjs rename to src/utils/rules-proxy.mjs index e5c8e16eac0..7b7a6bf00d3 100644 --- a/src/utils/rules-proxy.cjs +++ b/src/utils/rules-proxy.mjs @@ -1,19 +1,19 @@ // @ts-check -const path = require('path') +import path from 'path' -const chokidar = require('chokidar') -const cookie = require('cookie') -const redirector = require('netlify-redirector') -const pFilter = require('p-filter') +import chokidar from 'chokidar' +import cookie from 'cookie' +import redirector from 'netlify-redirector' +import pFilter from 'p-filter' -const { fileExistsAsync } = require('../lib/fs.cjs') +import { fileExistsAsync } from '../lib/fs.cjs' -const { NETLIFYDEVLOG } = require('./command-helpers.cjs') -const { parseRedirects } = require('./redirects.cjs') +import { NETLIFYDEVLOG } from './command-helpers.mjs' +import { parseRedirects } from './redirects.mjs' const watchers = [] -const onChanges = function (files, listener) { +export const onChanges = function (files, listener) { files.forEach((file) => { const watcher = chokidar.watch(file) watcher.on('change', listener) @@ -22,18 +22,25 @@ const onChanges = function (files, listener) { }) } -const getWatchers = function () { +export const getWatchers = function () { return watchers } -const getLanguage = function (headers) { +export const getLanguage = function (headers) { if (headers['accept-language']) { return headers['accept-language'].split(',')[0].slice(0, 2) } return 'en' } -const createRewriter = async function ({ configPath, distDir, geoCountry, jwtRoleClaim, jwtSecret, projectDir }) { +export const createRewriter = async function ({ + configPath, + distDir, + geoCountry, + jwtRoleClaim, + jwtSecret, + projectDir, +}) { let matcher = null const redirectsFiles = [...new Set([path.resolve(distDir, '_redirects'), path.resolve(projectDir, '_redirects')])] let redirects = await parseRedirects({ redirectsFiles, configPath }) @@ -95,10 +102,3 @@ const createRewriter = async function ({ configPath, distDir, geoCountry, jwtRol return match } } - -module.exports = { - onChanges, - getLanguage, - createRewriter, - getWatchers, -} diff --git a/src/utils/sites/utils.cjs b/src/utils/sites/utils.mjs similarity index 82% rename from src/utils/sites/utils.cjs rename to src/utils/sites/utils.mjs index ae7fd494043..7894a804f21 100644 --- a/src/utils/sites/utils.cjs +++ b/src/utils/sites/utils.mjs @@ -1,6 +1,6 @@ -const fetch = require('node-fetch') +import fetch from 'node-fetch' -const getTemplatesFromGitHub = async (token) => { +export const getTemplatesFromGitHub = async (token) => { const getPublicGitHubReposFromOrg = new URL(`https://api.github.com/orgs/netlify-templates/repos`) // GitHub returns 30 by default and we want to avoid our limit // due to our archived repositories at any given time @@ -21,7 +21,7 @@ const getTemplatesFromGitHub = async (token) => { return allTemplates } -const validateTemplate = async ({ ghToken, templateName }) => { +export const validateTemplate = async ({ ghToken, templateName }) => { const response = await fetch(`https://api.github.com/repos/${templateName}`, { headers: { Authorization: `token ${ghToken}`, @@ -37,10 +37,11 @@ const validateTemplate = async ({ ghToken, templateName }) => { } const data = await response.json() + return { exists: true, isTemplate: data.is_template } } -const createRepo = async (templateName, ghToken, siteName) => { +export const createRepo = async (templateName, ghToken, siteName) => { const resp = await fetch(`https://api.github.com/repos/${templateName}/generate`, { method: 'POST', headers: { @@ -52,7 +53,6 @@ const createRepo = async (templateName, ghToken, siteName) => { }) const data = await resp.json() + return data } - -module.exports = { getTemplatesFromGitHub, createRepo, validateTemplate } diff --git a/src/utils/telemetry/telemetry.mjs b/src/utils/telemetry/telemetry.mjs index 93cd34cdb58..66f70b85a83 100644 --- a/src/utils/telemetry/telemetry.mjs +++ b/src/utils/telemetry/telemetry.mjs @@ -5,8 +5,8 @@ import { fileURLToPath } from 'url' import { isCI } from 'ci-info' -import execa from '../execa.cjs' -import getGlobalConfig from '../get-global-config.cjs' +import execa from '../execa.mjs' +import getGlobalConfig from '../get-global-config.mjs' import isValidEventName from './validation.mjs' diff --git a/src/utils/telemetry/validation.mjs b/src/utils/telemetry/validation.mjs index 386968bb2a0..1d70d954563 100644 --- a/src/utils/telemetry/validation.mjs +++ b/src/utils/telemetry/validation.mjs @@ -1,7 +1,7 @@ /** * Utility to validating analytic event names for clean data */ -import { log } from '../command-helpers.cjs' +import { log } from '../command-helpers.mjs' export default function isValidEventName(eventName, config) { const validProject = [config.projectName] diff --git a/tests/integration/140.command.sites.test.cjs b/tests/integration/140.command.sites.test.cjs deleted file mode 100644 index b96035177aa..00000000000 --- a/tests/integration/140.command.sites.test.cjs +++ /dev/null @@ -1,223 +0,0 @@ -const process = require('process') - -const test = require('ava') -const inquirer = require('inquirer') -const prettyjson = require('prettyjson') -const sinon = require('sinon') - -// Important to do the mocks before the code that uses it is required -// in this case the mocks have to be done before the createSitesFromTemplateCommand -// is required! -/* eslint-disable import/order */ -const baseCommandPromise = import('../../src/commands/base-command.mjs') -const github = require('../../src/utils/init/config-github.cjs') -// mock the getGithubToken method with a fake token -const gitMock = sinon.stub(github, 'getGitHubToken').callsFake(() => 'my-token') - -const templatesUtils = require('../../src/utils/sites/utils.cjs') - -const getTemplatesStub = sinon.stub(templatesUtils, 'getTemplatesFromGitHub').callsFake(() => [ - { - name: 'next-starter', - html_url: 'http://github.com/netlify-templates/next-starter', - full_name: 'netlify-templates/next-starter', - }, - { - name: 'archived-starter', - html_url: 'https://github.com/netlify-templates/fake-repo', - full_name: 'netlify-templates/fake-repo', - archived: true, - }, -]) - -const createRepoStub = sinon.stub(templatesUtils, 'createRepo').callsFake(() => ({ - full_name: 'Next starter', - private: false, - branch: 'main', -})) - -const validateTemplateStub = sinon.stub(templatesUtils, 'validateTemplate').callsFake(() => ({ - exists: true, - isTemplate: true, -})) - -const jsonRenderSpy = sinon.spy(prettyjson, 'render') - -/* eslint-enable import/order */ -const { withMockApi } = require('./utils/mock-api.cjs') - -const inquirerStub = sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve({ accountSlug: 'test-account' })) - -test.beforeEach(() => { - inquirerStub.resetHistory() - gitMock.resetHistory() - getTemplatesStub.resetHistory() - createRepoStub.resetHistory() - jsonRenderSpy.resetHistory() - validateTemplateStub.resetHistory() -}) - -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', - response: [], - }, - { path: 'sites/site_id', response: siteInfo }, - { - path: 'user', - response: { name: 'test user', slug: 'test-user', email: 'user@test.com' }, - }, - { - path: 'test-account/sites', - method: 'post', - response: siteInfo, - }, -] - -test.serial('netlify sites:create-template', async (t) => { - const { createSitesFromTemplateCommand } = await import('../../src/commands/sites/sites-create-template.mjs') - - await withMockApi(routes, async ({ apiUrl }) => { - Object.defineProperty(process, 'env', { - value: { - NETLIFY_API_URL: apiUrl, - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - }) - - const { default: BaseCommand } = await baseCommandPromise - const program = new BaseCommand('netlify') - - createSitesFromTemplateCommand(program) - - await program.parseAsync(['', '', 'sites:create-template']) - - t.truthy(gitMock.called) - t.truthy(getTemplatesStub.called) - t.truthy(createRepoStub.called) - - t.truthy( - jsonRenderSpy.calledWith({ - 'Admin URL': siteInfo.admin_url, - URL: siteInfo.ssl_url, - 'Site ID': siteInfo.id, - 'Repo URL': siteInfo.build_settings.repo_url, - }), - ) - }) -}) - -test.serial('should not fetch templates if one is passed as option', async (t) => { - const { createSitesFromTemplateCommand } = await import('../../src/commands/sites/sites-create-template.mjs') - - await withMockApi(routes, async ({ apiUrl }) => { - Object.defineProperty(process, 'env', { - value: { - NETLIFY_API_URL: apiUrl, - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - }) - - const { default: BaseCommand } = await baseCommandPromise - const program = new BaseCommand('netlify') - - createSitesFromTemplateCommand(program) - - await program.parseAsync([ - '', - '', - 'sites:create-template', - '-u', - 'http://github.com/netlify-templates/next-starter', - ]) - - t.truthy(getTemplatesStub.notCalled) - }) -}) - -test.serial('should throw an error if the URL option is not a valid URL', async (t) => { - const { createSitesFromTemplateCommand } = await import('../../src/commands/sites/sites-create-template.mjs') - - await withMockApi(routes, async ({ apiUrl }) => { - Object.defineProperty(process, 'env', { - value: { - NETLIFY_API_URL: apiUrl, - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - }) - - const { default: BaseCommand } = await baseCommandPromise - const program = new BaseCommand('netlify') - - createSitesFromTemplateCommand(program) - - const error = await t.throwsAsync(async () => { - await program.parseAsync(['', '', 'sites:create-template', '-u', 'not-a-url']) - }) - - t.truthy(error.message.includes('Invalid URL')) - }) -}) - -test.serial('should return an array of templates with name, source code url and slug', async (t) => { - const { fetchTemplates } = await import('../../src/commands/sites/sites-create-template.mjs') - - await withMockApi(routes, async ({ apiUrl }) => { - Object.defineProperty(process, 'env', { - value: { - NETLIFY_API_URL: apiUrl, - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - }) - - const templates = await fetchTemplates('fake-token') - - t.truthy(getTemplatesStub.calledWith('fake-token')) - t.deepEqual(templates, [ - { - name: 'next-starter', - sourceCodeUrl: 'http://github.com/netlify-templates/next-starter', - slug: 'netlify-templates/next-starter', - }, - ]) - }) -}) - -test.serial('should throw error when name flag is incorrect', async (t) => { - const { createSitesCreateCommand } = await import('../../src/commands/sites/sites-create.mjs') - - await withMockApi(routes, async ({ apiUrl }) => { - Object.defineProperty(process, 'env', { - value: { - NETLIFY_API_URL: apiUrl, - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - }) - const exitSpy = sinon.stub(process, 'exit') - - const { default: BaseCommand } = await baseCommandPromise - const program = new BaseCommand('netlify') - - createSitesCreateCommand(program) - - const lengthError = await t.throwsAsync(async () => { - const LENGTH = 64 - await program.parseAsync(['', '', 'sites:create', '--name', Array.from({ length: LENGTH }).fill('a').join('')]) - }) - t.truthy(lengthError.message.includes('--name should be less than 64 characters')) - - exitSpy.restore() - }) -}) diff --git a/tests/integration/140.command.sites.test.mjs b/tests/integration/140.command.sites.test.mjs new file mode 100644 index 00000000000..ce6073959de --- /dev/null +++ b/tests/integration/140.command.sites.test.mjs @@ -0,0 +1,207 @@ +import process from 'process' + +import inquirer from 'inquirer' +import { render } from 'prettyjson' +import { afterAll, beforeEach, describe, expect, test, vi } from 'vitest' + +import BaseCommand from '../../src/commands/base-command.mjs' +import { createSitesFromTemplateCommand, fetchTemplates } from '../../src/commands/sites/sites-create-template.mjs' +import { createSitesCreateCommand } from '../../src/commands/sites/sites-create.mjs' +import { getGitHubToken } from '../../src/utils/init/config-github.mjs' +import { createRepo, getTemplatesFromGitHub } from '../../src/utils/sites/utils.mjs' + +import { getEnvironmentVariables, withMockApi } from './utils/mock-api.cjs' + +vi.mock('../../src/utils/command-helpers.mjs', async () => ({ + ...(await vi.importActual('../../src/utils/command-helpers.mjs')), + log: () => {}, +})) + +// mock the getGithubToken method with a fake token +vi.mock('../../src/utils/init/config-github.mjs', () => ({ + getGitHubToken: vi.fn().mockImplementation(() => 'my-token'), +})) + +vi.mock('../../src/utils/sites/utils.mjs', () => ({ + getTemplatesFromGitHub: vi.fn().mockImplementation(() => [ + { + name: 'next-starter', + html_url: 'http://github.com/netlify-templates/next-starter', + full_name: 'netlify-templates/next-starter', + }, + { + name: 'archived-starter', + html_url: 'https://github.com/netlify-templates/fake-repo', + full_name: 'netlify-templates/fake-repo', + archived: true, + }, + ]), + createRepo: vi.fn().mockImplementation(() => ({ + full_name: 'Next starter', + private: false, + branch: 'main', + })), + validateTemplate: vi.fn().mockImplementation(() => ({ + exists: true, + isTemplate: true, + })), +})) + +vi.mock('prettyjson', async () => { + const realRender = await vi.importActual('prettyjson') + + return { ...realRender, render: vi.fn().mockImplementation((...args) => realRender.render(...args)) } +}) + +vi.spyOn(inquirer, 'prompt').mockImplementation(() => Promise.resolve({ accountSlug: 'test-account' })) + +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', + response: [], + }, + { path: 'sites/site_id', response: siteInfo }, + { path: 'sites/site_id/service-instances', response: [] }, + { + path: 'user', + response: { name: 'test user', slug: 'test-user', email: 'user@test.com' }, + }, + { + path: 'test-account/sites', + method: 'post', + response: siteInfo, + }, +] + +const OLD_ENV = process.env + +describe('sites', () => { + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() + + Object.defineProperty(process, 'env', { value: {} }) + }) + + afterAll(() => { + vi.resetModules() + vi.restoreAllMocks() + + Object.defineProperty(process, 'env', { + value: OLD_ENV, + }) + }) + describe('sites:create-template', () => { + test('basic', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + + createSitesFromTemplateCommand(program) + + await program.parseAsync(['', '', 'sites:create-template']) + }) + + expect(getGitHubToken).toHaveBeenCalledOnce() + expect(getTemplatesFromGitHub).toHaveBeenCalledOnce() + expect(createRepo).toHaveBeenCalledOnce() + expect(render).toHaveBeenCalledOnce() + expect(render).toHaveBeenCalledWith({ + 'Admin URL': siteInfo.admin_url, + URL: siteInfo.ssl_url, + 'Site ID': siteInfo.id, + 'Repo URL': siteInfo.build_settings.repo_url, + }) + }) + + test('should not fetch templates if one is passed as option', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + + createSitesFromTemplateCommand(program) + + await program.parseAsync([ + '', + '', + 'sites:create-template', + '-u', + 'http://github.com/netlify-templates/next-starter', + ]) + + expect(getTemplatesFromGitHub).not.toHaveBeenCalled() + }) + }) + + test('should throw an error if the URL option is not a valid URL', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + + createSitesFromTemplateCommand(program) + + await expect(async () => { + await program.parseAsync(['', '', 'sites:create-template', '-u', 'not-a-url']) + }).rejects.toThrowError('Invalid URL') + }) + }) + }) + + describe('fetchTemplates', () => { + test('should return an array of templates with name, source code url and slug', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const program = new BaseCommand('netlify') + + createSitesFromTemplateCommand(program) + + const templates = await fetchTemplates('fake-token') + + expect(getTemplatesFromGitHub).toHaveBeenCalledWith('fake-token') + expect(templates).toEqual([ + { + name: 'next-starter', + sourceCodeUrl: 'http://github.com/netlify-templates/next-starter', + slug: 'netlify-templates/next-starter', + }, + ]) + }) + }) + }) + + describe('sites:create', () => { + test('should throw error when name flag is incorrect', async () => { + await withMockApi(routes, async ({ apiUrl }) => { + Object.assign(process.env, getEnvironmentVariables({ apiUrl })) + + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {}) + + const program = new BaseCommand('netlify') + + createSitesCreateCommand(program) + + await expect(async () => { + await program.parseAsync(['', '', 'sites:create', '--name', Array.from({ length: 64 }).fill('a').join('')]) + }).rejects.toThrowError('--name should be less than 64 characters') + + exitSpy.mockRestore() + }) + }) + }) +}) diff --git a/tests/integration/20.command.functions.test.cjs b/tests/integration/20.command.functions.test.cjs index bfb3aac386a..9b033939e1f 100644 --- a/tests/integration/20.command.functions.test.cjs +++ b/tests/integration/20.command.functions.test.cjs @@ -8,35 +8,19 @@ const getPort = require('get-port') const waitPort = require('wait-port') const fs = require('../../src/lib/fs.cjs') -const { NetlifyFunction } = require('../../src/lib/functions/netlify-function.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 { withMockApi } = require('./utils/mock-api.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 the correct function url for a NetlifyFunction object', (t) => { - const port = 7331 - const functionName = 'test-function' - - const functionUrl = `http://localhost:${port}/.netlify/functions/${functionName}` - - const ntlFunction = new NetlifyFunction({ - name: functionName, - settings: { functionsPort: port }, - config: { functions: { [functionName]: {} } }, - }) - - t.is(ntlFunction.url, functionUrl) -}) - 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({ @@ -129,14 +113,7 @@ test('should create a new function directory when none is found', async (t) => { ] await withMockApi(routes, async ({ apiUrl }) => { - const childProcess = execa(cliPath, ['functions:create'], { - env: { - NETLIFY_API_URL: apiUrl, - NETLIFY_SITE_ID: 'site_id', - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - cwd: builder.directory, - }) + const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder })) handleQuestions(childProcess, createFunctionQuestions) @@ -197,14 +174,7 @@ test('should create a new edge function directory when none is found', async (t) ] await withMockApi(routes, async ({ apiUrl }) => { - const childProcess = execa(cliPath, ['functions:create'], { - env: { - NETLIFY_API_URL: apiUrl, - NETLIFY_SITE_ID: 'site_id', - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - cwd: builder.directory, - }) + const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder })) handleQuestions(childProcess, createFunctionQuestions) @@ -267,14 +237,7 @@ test('should use specified edge function directory when found', async (t) => { ] await withMockApi(routes, async ({ apiUrl }) => { - const childProcess = execa(cliPath, ['functions:create'], { - env: { - NETLIFY_API_URL: apiUrl, - NETLIFY_SITE_ID: 'site_id', - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - cwd: builder.directory, - }) + const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder })) handleQuestions(childProcess, createFunctionQuestions) @@ -343,14 +306,7 @@ test('should install function template dependencies on a site-level `package.jso ] await withMockApi(routes, async ({ apiUrl }) => { - const childProcess = execa(cliPath, ['functions:create'], { - env: { - NETLIFY_API_URL: apiUrl, - NETLIFY_SITE_ID: 'site_id', - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - cwd: builder.directory, - }) + const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder })) handleQuestions(childProcess, createFunctionQuestions) @@ -423,14 +379,7 @@ test('should install function template dependencies in the function sub-director ] await withMockApi(routes, async ({ apiUrl }) => { - const childProcess = execa(cliPath, ['functions:create'], { - env: { - NETLIFY_API_URL: apiUrl, - NETLIFY_SITE_ID: 'site_id', - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - cwd: builder.directory, - }) + const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder })) handleQuestions(childProcess, createFunctionQuestions) @@ -495,14 +444,7 @@ test('should not create a new function directory when one is found', async (t) = ] await withMockApi(routes, async ({ apiUrl }) => { - const childProcess = execa(cliPath, ['functions:create'], { - env: { - NETLIFY_API_URL: apiUrl, - NETLIFY_SITE_ID: 'site_id', - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - cwd: builder.directory, - }) + const childProcess = execa(cliPath, ['functions:create'], getCLIOptions({ apiUrl, builder })) handleQuestions(childProcess, createFunctionQuestions) @@ -559,14 +501,11 @@ test('should only show function templates for the language specified via the --l ] await withMockApi(routes, async ({ apiUrl }) => { - const childProcess = execa(cliPath, ['functions:create', '--language', 'javascript'], { - env: { - NETLIFY_API_URL: apiUrl, - NETLIFY_SITE_ID: 'site_id', - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - cwd: builder.directory, - }) + const childProcess = execa( + cliPath, + ['functions:create', '--language', 'javascript'], + getCLIOptions({ apiUrl, builder }), + ) handleQuestions(childProcess, createFunctionQuestions) @@ -623,14 +562,11 @@ test('throws an error when the --language flag contains an unsupported value', a ] await withMockApi(routes, async ({ apiUrl }) => { - const childProcess = execa(cliPath, ['functions:create', '--language', 'coffeescript'], { - env: { - NETLIFY_API_URL: apiUrl, - NETLIFY_SITE_ID: 'site_id', - NETLIFY_AUTH_TOKEN: 'fake-token', - }, - cwd: builder.directory, - }) + const childProcess = execa( + cliPath, + ['functions:create', '--language', 'coffeescript'], + getCLIOptions({ apiUrl, builder }), + ) handleQuestions(childProcess, createFunctionQuestions) diff --git a/tests/integration/230.rules-proxy.test.cjs b/tests/integration/230.rules-proxy.test.cjs deleted file mode 100644 index 64b7f40b938..00000000000 --- a/tests/integration/230.rules-proxy.test.cjs +++ /dev/null @@ -1,63 +0,0 @@ -const http = require('http') -const path = require('path') - -const test = require('ava') -const getPort = require('get-port') - -const { createRewriter, getWatchers } = require('../../src/utils/rules-proxy.cjs') - -const got = require('./utils/got.cjs') -const { createSiteBuilder } = require('./utils/site-builder.cjs') - -test.before(async (t) => { - const builder = createSiteBuilder({ siteName: 'site-with-redirects-file' }) - builder.withRedirectsFile({ - redirects: [{ from: '/something ', to: '/ping', status: 200 }], - }) - - await builder.buildAsync() - - const rewriter = await createRewriter({ - distDir: builder.directory, - projectDir: builder.directory, - jwtSecret: '', - jwtRoleClaim: '', - configPath: path.join(builder.directory, 'netlify.toml'), - }) - const port = await getPort({ port: PORT }) - const server = http.createServer(async function onRequest(req, res) { - const match = await rewriter(req) - res.end(JSON.stringify(match)) - }) - - t.context.port = port - t.context.server = server - t.context.builder = builder - - await new Promise((resolve) => { - server.listen(port, 'localhost', resolve) - }) -}) - -const PORT = 8888 - -test.after(async (t) => { - await new Promise((resolve) => { - t.context.server.on('close', resolve) - t.context.server.close() - }) - await Promise.all(getWatchers().map((watcher) => watcher.close())) - await t.context.builder.cleanupAsync() -}) - -test('should apply re-write rule based on _redirects file', async (t) => { - const response = await got(`http://localhost:${t.context.port}/something`).json() - - t.is(response.from, '/something') - t.is(response.to, '/ping') - t.is(response.force, false) - t.is(response.host, '') - t.is(response.negative, false) - t.is(response.scheme, '') - t.is(response.status, 200) -}) diff --git a/tests/integration/230.rules-proxy.test.mjs b/tests/integration/230.rules-proxy.test.mjs new file mode 100644 index 00000000000..96b932d22ba --- /dev/null +++ b/tests/integration/230.rules-proxy.test.mjs @@ -0,0 +1,59 @@ +import http from 'http' +import path from 'path' + +import { afterAll, beforeAll, describe, expect, test } from 'vitest' + +import { createRewriter, getWatchers } from '../../src/utils/rules-proxy.mjs' + +import got from './utils/got.cjs' +import { createSiteBuilder } from './utils/site-builder.cjs' + +describe('rules-proxy', () => { + let server + let builder + beforeAll(async () => { + builder = createSiteBuilder({ siteName: 'site-with-redirects-file' }) + builder.withRedirectsFile({ + redirects: [{ from: '/something ', to: '/ping', status: 200 }], + }) + + await builder.buildAsync() + + const rewriter = await createRewriter({ + distDir: builder.directory, + projectDir: builder.directory, + jwtSecret: '', + jwtRoleClaim: '', + configPath: path.join(builder.directory, 'netlify.toml'), + }) + server = http.createServer(async function onRequest(req, res) { + const match = await rewriter(req) + res.end(JSON.stringify(match)) + }) + + return new Promise((resolve) => { + server.listen(resolve) + }) + }) + + afterAll(async () => { + await new Promise((resolve) => { + server.on('close', resolve) + server.close() + }) + await Promise.all(getWatchers().map((watcher) => watcher.close())) + await builder.cleanupAsync() + }) + + test('should apply re-write rule based on _redirects file', async () => { + const response = await got(`http://localhost:${server.address().port}/something`).json() + + expect(response.from).toBe('/something') + expect(response.to).toBe('/ping') + expect(response.force).toBe(false) + expect(response.host).toBe('') + expect(response.negative).toBe(false) + expect(response.scheme).toBe('') + expect(response.status).toBe(200) + }) +}) diff --git a/tests/integration/30.command.lm.test.cjs b/tests/integration/30.command.lm.test.cjs index 8d228e1f196..4fd760ca48f 100644 --- a/tests/integration/30.command.lm.test.cjs +++ b/tests/integration/30.command.lm.test.cjs @@ -100,7 +100,6 @@ test.serial('netlify lm:setup', async (t) => { test.after('cleanup', async (t) => { const { builder, mockApi } = t.context - console.log('Performing cleanup') await callCli(['lm:uninstall'], t.context.execOptions) await builder.cleanupAsync() mockApi.close() diff --git a/tests/integration/300.command.dev.test.cjs b/tests/integration/300.command.dev.test.cjs index 2446097e410..9a9ba7ce2cf 100644 --- a/tests/integration/300.command.dev.test.cjs +++ b/tests/integration/300.command.dev.test.cjs @@ -74,7 +74,6 @@ test('should return response from a function with setTimeout', async (t) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ path: 'timeout.js', handler: async () => { - console.log('ding') // Wait for 4 seconds const FUNCTION_TIMEOUT = 4e3 await new Promise((resolve) => { diff --git a/tests/integration/utils/dev-server.cjs b/tests/integration/utils/dev-server.cjs index 1c416608166..c5029ae53d8 100644 --- a/tests/integration/utils/dev-server.cjs +++ b/tests/integration/utils/dev-server.cjs @@ -3,21 +3,24 @@ const process = require('process') const execa = require('execa') const getPort = require('get-port') -const omit = require('omit.js').default const pTimeout = require('p-timeout') const cliPath = require('./cli-path.cjs') const { handleQuestions } = require('./handle-questions.cjs') const { killProcess } = require('./process.cjs') -const ENVS_TO_OMIT = ['LANG', 'LC_ALL'] +const getExecaOptions = ({ cwd, env }) => { + // Unused vars here are in order to omit LANg and LC_ALL from envs + // eslint-disable-next-line no-unused-vars + const { LANG, LC_ALL, ...baseEnv } = process.env -const getExecaOptions = ({ cwd, env }) => ({ - cwd, - extendEnv: false, - env: { ...omit(process.env, ENVS_TO_OMIT), BROWSER: 'none', ...env }, - encoding: 'utf8', -}) + return { + cwd, + extendEnv: false, + env: { ...baseEnv, BROWSER: 'none', ...env }, + encoding: 'utf8', + } +} const startServer = async ({ cwd, diff --git a/tests/integration/utils/mock-api.cjs b/tests/integration/utils/mock-api.cjs index 79d6ea49a05..40f5769e77c 100644 --- a/tests/integration/utils/mock-api.cjs +++ b/tests/integration/utils/mock-api.cjs @@ -65,10 +65,16 @@ const withMockApi = async (routes, testHandler) => { } } +const getEnvironmentVariables = ({ apiUrl }) => ({ + NETLIFY_AUTH_TOKEN: 'fake-token', + NETLIFY_SITE_ID: 'site_id', + NETLIFY_API_URL: apiUrl, +}) + const getCLIOptions = ({ apiUrl, builder: { directory: cwd }, extendEnv = true, env = {} }) => ({ cwd, - env: { NETLIFY_AUTH_TOKEN: 'fake-token', NETLIFY_SITE_ID: 'site_id', NETLIFY_API_URL: apiUrl, ...env }, + env: { ...getEnvironmentVariables({ apiUrl }), ...env }, extendEnv, }) -module.exports = { withMockApi, startMockApi, getCLIOptions } +module.exports = { withMockApi, startMockApi, getEnvironmentVariables, getCLIOptions } diff --git a/tests/integration/utils/mock-execa.cjs b/tests/integration/utils/mock-execa.cjs index eaa44ebaf0a..164eaa22b82 100644 --- a/tests/integration/utils/mock-execa.cjs +++ b/tests/integration/utils/mock-execa.cjs @@ -1,4 +1,5 @@ const { writeFile } = require('fs').promises +const { pathToFileURL } = require('url') const tempy = require('tempy') @@ -14,7 +15,8 @@ const createMock = async (contents) => { await writeFile(path, contents) const env = { - NETLIFY_CLI_EXECA_PATH: path, + // windows needs 'file://' paths + NETLIFY_CLI_EXECA_PATH: pathToFileURL(path).href, } const cleanup = () => // eslint-disable-next-line promise/prefer-await-to-then diff --git a/tests/unit/lib/account.test.mjs b/tests/unit/lib/account.test.mjs new file mode 100644 index 00000000000..0605898f38e --- /dev/null +++ b/tests/unit/lib/account.test.mjs @@ -0,0 +1,35 @@ +import { describe, expect, test } from 'vitest' + +import { supportsBackgroundFunctions } from '../../../src/lib/account.mjs' + +describe('supportsBackgroundFunctions', () => { + test(`should return false if no account`, () => { + expect(supportsBackgroundFunctions()).toEqual(false) + }) + + test(`should return false if no capabilities`, () => { + expect(supportsBackgroundFunctions({})).toEqual(false) + }) + + test(`should return false if capability missing`, () => { + expect(supportsBackgroundFunctions({ capabilities: {} })).toEqual(false) + }) + + test(`should return false if included property missing on capability missing`, () => { + expect(supportsBackgroundFunctions({ capabilities: { background_functions: {} } })).toEqual(false) + }) + + test(`should return false if included property is false`, () => { + expect(supportsBackgroundFunctions({ capabilities: { background_functions: { included: false } } })).toEqual(false) + }) + + test(`should return true if included property is truthy`, () => { + expect(supportsBackgroundFunctions({ capabilities: { background_functions: { included: 'string' } } })).toEqual( + true, + ) + }) + + test(`should return true if included property is true`, () => { + expect(supportsBackgroundFunctions({ capabilities: { background_functions: { included: true } } })).toEqual(true) + }) +}) diff --git a/tests/unit/lib/edge-functions/create-site-info-header.test.cjs b/tests/unit/lib/edge-functions/create-site-info-header.test.cjs deleted file mode 100644 index d582c9a0c8f..00000000000 --- a/tests/unit/lib/edge-functions/create-site-info-header.test.cjs +++ /dev/null @@ -1,21 +0,0 @@ -const { Buffer } = require('buffer') - -const test = require('ava') - -const { createSiteInfoHeader } = require('../../../../src/lib/edge-functions/proxy.cjs') - -test('createSiteInfoHeader builds a base64 string', (t) => { - const siteInfo = { id: 'site_id', name: 'site_name', url: 'site_url' } - const output = createSiteInfoHeader(siteInfo) - const parsedOutput = JSON.parse(Buffer.from(output, 'base64').toString('utf-8')) - - t.deepEqual(parsedOutput, siteInfo) -}) - -test('createSiteInfoHeader builds a base64 string if there is no siteInfo passed', (t) => { - const siteInfo = {} - const output = createSiteInfoHeader(siteInfo) - const parsedOutput = JSON.parse(Buffer.from(output, 'base64').toString('utf-8')) - - t.deepEqual(parsedOutput, {}) -}) diff --git a/tests/unit/lib/edge-functions/proxy.test.mjs b/tests/unit/lib/edge-functions/proxy.test.mjs new file mode 100644 index 00000000000..a0db4f7c527 --- /dev/null +++ b/tests/unit/lib/edge-functions/proxy.test.mjs @@ -0,0 +1,23 @@ +import { Buffer } from 'buffer' + +import { describe, expect, test } from 'vitest' + +import { createSiteInfoHeader } from '../../../../src/lib/edge-functions/proxy.mjs' + +describe('createSiteInfoHeader', () => { + test('builds a base64 string', () => { + const siteInfo = { id: 'site_id', name: 'site_name', url: 'site_url' } + const output = createSiteInfoHeader(siteInfo) + const parsedOutput = JSON.parse(Buffer.from(output, 'base64').toString('utf-8')) + + expect(parsedOutput).toEqual(siteInfo) + }) + + test('builds a base64 string if there is no siteInfo passed', () => { + const siteInfo = {} + const output = createSiteInfoHeader(siteInfo) + const parsedOutput = JSON.parse(Buffer.from(output, 'base64').toString('utf-8')) + + expect(parsedOutput).toEqual({}) + }) +}) diff --git a/tests/unit/lib/exec-fetcher.test.cjs b/tests/unit/lib/exec-fetcher.test.cjs deleted file mode 100644 index 6d6dd24f83d..00000000000 --- a/tests/unit/lib/exec-fetcher.test.cjs +++ /dev/null @@ -1,120 +0,0 @@ -// @ts-check -const process = require('process') - -/** @type {import('ava').TestInterface} */ -// @ts-ignore -const test = require('ava') -const sinon = require('sinon') - -const { rewiremock } = require('../../integration/utils/rewiremock.cjs') - -const fetchLatestSpy = sinon.stub() -const { fetchLatestVersion, getArch, getExecName } = rewiremock.proxy( - // eslint-disable-next-line n/global-require - () => require('../../../src/lib/exec-fetcher.cjs'), - { - 'gh-release-fetch': { - fetchLatest: fetchLatestSpy, - }, - }, -) - -test.beforeEach((t) => { - t.context.sandbox = sinon.createSandbox() -}) - -test.afterEach((t) => { - t.context.sandbox.restore() -}) - -test(`should use 386 if process architecture is ia32`, (t) => { - t.context.sandbox.stub(process, 'arch').value('ia32') - t.is(getArch(), '386') -}) - -test(`should use amd64 if process architecture is x64`, (t) => { - t.context.sandbox.stub(process, 'arch').value('x64') - t.is(getArch(), 'amd64') -}) - -test(`should append .exe on windows for the executable name`, (t) => { - t.context.sandbox.stub(process, 'platform').value('win32') - const execName = 'some-binary-file' - t.is(getExecName({ execName }), `${execName}.exe`) -}) - -test(`should not append anything on linux or darwin to executable`, (t) => { - t.context.sandbox.stub(process, 'platform').value('darwin') - const execName = 'some-binary-file' - t.is(getExecName({ execName }), execName) - t.context.sandbox.stub(process, 'platform').value('linux') - t.is(getExecName({ execName }), execName) -}) - -test('should test if an error is thrown if the cpu architecture and the os are not available', async (t) => { - t.context.sandbox.stub(process, 'platform').value('windows') - t.context.sandbox.stub(process, 'arch').value('amd64') - - // eslint-disable-next-line prefer-promise-reject-errors - fetchLatestSpy.returns(Promise.reject({ statusCode: 404 })) - - const { message } = await t.throwsAsync( - fetchLatestVersion({ - packageName: 'traffic-mesh-agent', - execName: 'traffic-mesh', - destination: t.context.binPath, - extension: 'zip', - }), - ) - - t.regex(message, /The operating system windows with the CPU architecture amd64 is currently not supported!/) -}) - -test('should provide the error if it is not a 404', async (t) => { - const error = new Error('Got Rate limited for example') - - fetchLatestSpy.returns(Promise.reject(error)) - - const { message } = await t.throwsAsync( - fetchLatestVersion({ - packageName: 'traffic-mesh-agent', - execName: 'traffic-mesh', - destination: t.context.binPath, - extension: 'zip', - }), - ) - - t.is(message, error.message) -}) - -test('should map linux x64 to amd64 arch', async (t) => { - t.context.sandbox.stub(process, 'platform').value('linux') - t.context.sandbox.stub(process, 'arch').value('x64') - - // eslint-disable-next-line prefer-promise-reject-errors - fetchLatestSpy.returns(Promise.reject({ statusCode: 404 })) - - const { message } = await t.throwsAsync( - fetchLatestVersion({ - packageName: 'traffic-mesh-agent', - execName: 'traffic-mesh', - destination: t.context.binPath, - extension: 'zip', - }), - ) - - t.regex(message, /The operating system linux with the CPU architecture amd64 is currently not supported!/) -}) - -test('should not throw when the request passes', async (t) => { - fetchLatestSpy.returns(Promise.resolve()) - - await t.notThrowsAsync( - fetchLatestVersion({ - packageName: 'traffic-mesh-agent', - execName: 'traffic-mesh', - destination: t.context.binPath, - extension: 'zip', - }), - ) -}) diff --git a/tests/unit/lib/exec-fetcher.test.mjs b/tests/unit/lib/exec-fetcher.test.mjs new file mode 100644 index 00000000000..e71fa51f45e --- /dev/null +++ b/tests/unit/lib/exec-fetcher.test.mjs @@ -0,0 +1,124 @@ +// @ts-check +import process from 'process' + +import { fetchLatest } from 'gh-release-fetch' +import { afterAll, afterEach, beforeAll, expect, test, vi } from 'vitest' + +import { fetchLatestVersion, getArch, getExecName } from '../../../src/lib/exec-fetcher.mjs' + +vi.mock('gh-release-fetch', async () => { + const actual = await vi.importActual('gh-release-fetch') + + return { + ...actual, + fetchLatest: vi.fn(actual.fetchLatest), + } +}) + +let processArchSpy +let processPlatformSpy + +beforeAll(() => { + processArchSpy = vi.spyOn(process, 'arch', 'get') + processPlatformSpy = vi.spyOn(process, 'platform', 'get') +}) + +afterEach(() => { + vi.clearAllMocks() + processArchSpy.mockReset() + processPlatformSpy.mockReset() +}) + +afterAll(() => { + vi.restoreAllMocks() +}) + +test(`should use 386 if process architecture is ia32`, () => { + processArchSpy.mockReturnValue('ia32') + expect(getArch()).toBe('386') +}) + +test(`should use amd64 if process architecture is x64`, () => { + processArchSpy.mockReturnValue('x64') + expect(getArch()).toBe('amd64') +}) + +test(`should append .exe on windows for the executable name`, () => { + processPlatformSpy.mockReturnValue('win32') + const execName = 'some-binary-file' + expect(getExecName({ execName })).toBe(`${execName}.exe`) +}) + +test(`should not append anything on darwin to executable`, () => { + processPlatformSpy.mockReturnValue('darwin') + const execName = 'some-binary-file' + expect(getExecName({ execName })).toBe(execName) +}) + +test(`should not append anything on linux to executable`, () => { + processPlatformSpy.mockReturnValue('linux') + const execName = 'some-binary-file' + expect(getExecName({ execName })).toBe(execName) +}) + +test('should test if an error is thrown if the cpu architecture and the os are not available', async () => { + processArchSpy.mockReturnValue('amd64') + processPlatformSpy.mockReturnValue('windows') + + // eslint-disable-next-line prefer-promise-reject-errors + fetchLatest.mockReturnValue(Promise.reject({ statusCode: 404 })) + + await expect( + fetchLatestVersion({ + packageName: 'traffic-mesh-agent', + execName: 'traffic-mesh', + destination: '', + extension: 'zip', + }), + ).rejects.toThrowError(/The operating system windows with the CPU architecture amd64 is currently not supported!/) +}) + +test('should provide the error if it is not a 404', async () => { + const error = new Error('Got Rate limited for example') + + fetchLatest.mockReturnValue(Promise.reject(error)) + + await expect( + fetchLatestVersion({ + packageName: 'traffic-mesh-agent', + execName: 'traffic-mesh', + destination: '', + extension: 'zip', + }), + ).rejects.toThrowError(error.message) +}) + +test('should map linux x64 to amd64 arch', async () => { + processArchSpy.mockReturnValue('x64') + processPlatformSpy.mockReturnValue('linux') + + // eslint-disable-next-line prefer-promise-reject-errors + fetchLatest.mockReturnValue(Promise.reject({ statusCode: 404 })) + + await expect( + fetchLatestVersion({ + packageName: 'traffic-mesh-agent', + execName: 'traffic-mesh', + destination: '', + extension: 'zip', + }), + ).rejects.toThrowError(/The operating system linux with the CPU architecture amd64 is currently not supported!/) +}) + +test('should not throw when the request passes', async () => { + fetchLatest.mockReturnValue(Promise.resolve()) + + await expect( + fetchLatestVersion({ + packageName: 'traffic-mesh-agent', + execName: 'traffic-mesh', + destination: '', + extension: 'zip', + }), + ).resolves.not.toThrowError() +}) diff --git a/tests/unit/lib/functions/netlify-function.test.mjs b/tests/unit/lib/functions/netlify-function.test.mjs new file mode 100644 index 00000000000..ddda982839b --- /dev/null +++ b/tests/unit/lib/functions/netlify-function.test.mjs @@ -0,0 +1,18 @@ +import { expect, test } from 'vitest' + +import NetlifyFunction from '../../../../src/lib/functions/netlify-function.mjs' + +test('should return the correct function url for a NetlifyFunction object', () => { + const port = 7331 + const functionName = 'test-function' + + const functionUrl = `http://localhost:${port}/.netlify/functions/${functionName}` + + const ntlFunction = new NetlifyFunction({ + name: functionName, + settings: { functionsPort: port }, + config: { functions: { [functionName]: {} } }, + }) + + expect(ntlFunction.url).toBe(functionUrl) +}) diff --git a/tests/unit/lib/functions/registry.test.cjs b/tests/unit/lib/functions/registry.test.cjs deleted file mode 100644 index 9bfd9a5b6a0..00000000000 --- a/tests/unit/lib/functions/registry.test.cjs +++ /dev/null @@ -1,53 +0,0 @@ -const test = require('ava') -const sinon = require('sinon') - -const { rewiremock } = require('../../../integration/utils/rewiremock.cjs') - -const watchDebouncedSpy = sinon.stub() -// eslint-disable-next-line n/global-require -const { FunctionsRegistry } = rewiremock.proxy(() => require('../../../../src/lib/functions/registry.cjs'), { - '../../../../src/utils/index.cjs': { - watchDebounced: watchDebouncedSpy, - }, -}) -watchDebouncedSpy.resolves({}) - -test('registry should only pass functions config to zip-it-and-ship-it', async (t) => { - const functionsRegistry = new FunctionsRegistry({ - projectRoot: '/projectRoot', - config: { functions: { '*': {} }, plugins: ['test'] }, - }) - const prepareDirectoryScanStub = sinon.stub(FunctionsRegistry, 'prepareDirectoryScan') - const setupDirectoryWatcherStub = sinon.stub(functionsRegistry, 'setupDirectoryWatcher') - // To verify that only the functions config is passed to zip-it-ship-it - const listFunctionsStub = sinon.stub(functionsRegistry, 'listFunctions') - listFunctionsStub.returns(Promise.resolve([])) - - await functionsRegistry.scan([functionsRegistry.projectRoot]) - - const spyCall = listFunctionsStub.getCall(0) - - t.is(spyCall.lastArg.config, functionsRegistry.config.functions) - - t.teardown(async () => { - await listFunctionsStub.restore() - await setupDirectoryWatcherStub.restore() - await prepareDirectoryScanStub.restore() - }) -}) - -test('should add included_files to watcher', async (t) => { - const registry = new FunctionsRegistry({}) - const func = { - name: '', - config: { functions: { '*': { included_files: ['include/*', '!include/a.txt'] } } }, - build() { - return { srcFilesDiff: { added: ['myfile'] }, includedFiles: ['include/*'] } - }, - } - - await registry.buildFunctionAndWatchFiles(func) - - t.is(watchDebouncedSpy.callCount, 1) - t.deepEqual(watchDebouncedSpy.args[0][0], ['myfile', 'include/*']) -}) diff --git a/tests/unit/lib/functions/registry.test.mjs b/tests/unit/lib/functions/registry.test.mjs new file mode 100644 index 00000000000..78b60167abe --- /dev/null +++ b/tests/unit/lib/functions/registry.test.mjs @@ -0,0 +1,52 @@ +import { expect, test, vi } from 'vitest' + +import { FunctionsRegistry } from '../../../../src/lib/functions/registry.mjs' +import { watchDebounced } from '../../../../src/utils/command-helpers.mjs' + +vi.mock('../../../../src/utils/command-helpers.mjs', async () => { + const helpers = await vi.importActual('../../../../src/utils/command-helpers.mjs') + + return { + ...helpers, + watchDebounced: vi.fn().mockImplementation(() => Promise.resolve({})), + } +}) + +test('registry should only pass functions config to zip-it-and-ship-it', async () => { + const functionsRegistry = new FunctionsRegistry({ + projectRoot: '/projectRoot', + config: { functions: { '*': {} }, plugins: ['test'] }, + }) + const prepareDirectoryScanStub = vi.spyOn(FunctionsRegistry, 'prepareDirectoryScan').mockImplementation(() => {}) + const setupDirectoryWatcherStub = vi.spyOn(functionsRegistry, 'setupDirectoryWatcher').mockImplementation(() => {}) + // To verify that only the functions config is passed to zip-it-ship-it + const listFunctionsStub = vi.spyOn(functionsRegistry, 'listFunctions').mockImplementation(() => Promise.resolve([])) + + await functionsRegistry.scan([functionsRegistry.projectRoot]) + + expect(listFunctionsStub).toHaveBeenCalledOnce() + expect(listFunctionsStub).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ config: functionsRegistry.config.functions }), + ) + + await listFunctionsStub.mockRestore() + await setupDirectoryWatcherStub.mockRestore() + await prepareDirectoryScanStub.mockRestore() +}) + +test('should add included_files to watcher', async () => { + const registry = new FunctionsRegistry({}) + const func = { + name: '', + config: { functions: { '*': { included_files: ['include/*', '!include/a.txt'] } } }, + build() { + return { srcFilesDiff: { added: ['myfile'] }, includedFiles: ['include/*'] } + }, + } + + await registry.buildFunctionAndWatchFiles(func) + + expect(watchDebounced).toHaveBeenCalledOnce() + expect(watchDebounced).toHaveBeenCalledWith(['myfile', 'include/*'], expect.anything()) +}) diff --git a/tests/unit/lib/functions/runtimes/go/index.test.cjs b/tests/unit/lib/functions/runtimes/go/index.test.cjs deleted file mode 100644 index 26630794bd7..00000000000 --- a/tests/unit/lib/functions/runtimes/go/index.test.cjs +++ /dev/null @@ -1,32 +0,0 @@ -const test = require('ava') -const sinon = require('sinon') - -const { rewiremock } = require('../../../../../integration/utils/rewiremock.cjs') - -const runFunctionsProxySpy = sinon.stub() -const { invokeFunction } = rewiremock.proxy( - // eslint-disable-next-line n/global-require - () => require('../../../../../../src/lib/functions/runtimes/go/index.cjs'), - { - '../../../../../../src/lib/functions/local-proxy.cjs': { - runFunctionsProxy: runFunctionsProxySpy, - }, - }, -) - -const invokeFunctionMacro = test.macro({ - async exec(t, prop, expected) { - runFunctionsProxySpy.resolves({ stdout: JSON.stringify({ [prop]: expected }) }) - - const match = await invokeFunction({ func: { mainFile: '', buildData: {} } }) - t.deepEqual(match[prop], expected) - }, - title(providedTitle, prop) { - return `should return ${prop}` - }, -}) - -test(invokeFunctionMacro, 'body', 'thebody') -test(invokeFunctionMacro, 'headers', { 'X-Single': 'A' }) -test(invokeFunctionMacro, 'multiValueHeaders', { 'X-Multi': ['B', 'C'] }) -test(invokeFunctionMacro, 'statusCode', 200) diff --git a/tests/unit/lib/functions/runtimes/go/index.test.mjs b/tests/unit/lib/functions/runtimes/go/index.test.mjs new file mode 100644 index 00000000000..41234c60930 --- /dev/null +++ b/tests/unit/lib/functions/runtimes/go/index.test.mjs @@ -0,0 +1,18 @@ +import { expect, test, vi } from 'vitest' + +import { runFunctionsProxy } from '../../../../../../src/lib/functions/local-proxy.mjs' +import { invokeFunction } from '../../../../../../src/lib/functions/runtimes/go/index.mjs' + +vi.mock('../../../../../../src/lib/functions/local-proxy.mjs', () => ({ runFunctionsProxy: vi.fn() })) + +test.each([ + ['body', 'thebody'], + ['headers', { 'X-Single': 'A' }], + ['multiValueHeaders', { 'X-Multi': ['B', 'C'] }], + ['statusCode', 200], +])('should return %s', async (prop, expected) => { + runFunctionsProxy.mockImplementation(() => Promise.resolve({ stdout: JSON.stringify({ [prop]: expected }) })) + + const match = await invokeFunction({ func: { mainFile: '', buildData: {} } }) + expect(match[prop]).toEqual(expected) +}) diff --git a/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.cjs b/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.mjs similarity index 55% rename from tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.cjs rename to tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.mjs index 5032adff9c4..11c232558be 100644 --- a/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.cjs +++ b/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.mjs @@ -1,25 +1,20 @@ -const test = require('ava') -const sinon = require('sinon') +import { expect, test, vi } from 'vitest' -const { - detectNetlifyLambda, -} = require('../../../../../../../src/lib/functions/runtimes/js/builders/netlify-lambda.cjs') +import { detectNetlifyLambda } from '../../../../../../../src/lib/functions/runtimes/js/builders/netlify-lambda.mjs' -test(`should not find netlify-lambda from netlify-cli package.json`, async (t) => { - t.is(await detectNetlifyLambda(), false) +test(`should not find netlify-lambda from netlify-cli package.json`, async () => { + expect(await detectNetlifyLambda()).toBe(false) }) -test(`should not match if netlify-lambda is missing from dependencies`, async (t) => { +test(`should not match if netlify-lambda is missing from dependencies`, async () => { const packageJson = { dependencies: {}, devDependencies: {}, } - t.is(await detectNetlifyLambda({ packageJson }), false) + expect(await detectNetlifyLambda({ packageJson })).toBe(false) }) -test.serial('should not match if netlify-lambda is missing functions directory', async (t) => { - const sandbox = sinon.createSandbox() - +test('should not match if netlify-lambda is missing functions directory with argument', async () => { const packageJson = { scripts: { 'some-build-step': 'netlify-lambda build --config config/webpack.config.js', @@ -30,22 +25,20 @@ test.serial('should not match if netlify-lambda is missing functions directory', }, } - const spyConsoleWarn = sandbox.spy(console, 'warn') + const spyConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - t.is(await detectNetlifyLambda({ packageJson }), false) + expect(await detectNetlifyLambda({ packageJson })).toBe(false) // Not checking for exact warning string as it would make this test too specific/brittle - t.is(spyConsoleWarn.calledWithMatch('contained no functions folder'), true) + expect(spyConsoleWarn).toHaveBeenCalledWith(expect.stringMatching('contained no functions folder')) - sandbox.restore() + spyConsoleWarn.mockRestore() }) -test.serial('should not match if netlify-lambda contains multiple function directories', async (t) => { - const sandbox = sinon.createSandbox() - +test('should not match if netlify-lambda is missing functions directory', async () => { const packageJson = { scripts: { - 'some-build-step': 'netlify-lambda build -config config/webpack.config.js build/dir another/build/dir', + 'some-build-step': 'netlify-lambda build', }, dependencies: {}, devDependencies: { @@ -53,54 +46,58 @@ test.serial('should not match if netlify-lambda contains multiple function direc }, } - const spyConsoleWarn = sandbox.spy(console, 'warn') + const spyConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - t.is(await detectNetlifyLambda({ packageJson }), false) + expect(await detectNetlifyLambda({ packageJson })).toBe(false) // Not checking for exact warning string as it would make this test too specific/brittle - t.is(spyConsoleWarn.calledWithMatch('contained 2 or more function folders'), true) + expect(spyConsoleWarn).toHaveBeenCalledWith(expect.stringMatching('contained no functions folder')) - sandbox.restore() + spyConsoleWarn.mockRestore() }) -test(`should match if netlify-lambda is listed in dependencies and is mentioned in scripts`, async (t) => { +test('should not match if netlify-lambda contains multiple function directories', async () => { const packageJson = { scripts: { - build: 'netlify-lambda build some/directory', + 'some-build-step': 'netlify-lambda build -config config/webpack.config.js build/dir another/build/dir', }, - dependencies: { + dependencies: {}, + devDependencies: { 'netlify-lambda': 'ignored', }, - devDependencies: {}, } - const match = await detectNetlifyLambda({ packageJson }) + const spyConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + expect(await detectNetlifyLambda({ packageJson })).toBe(false) - t.is(match.builderName, 'netlify-lambda') - t.is(match.npmScript, 'build') + // Not checking for exact warning string as it would make this test too specific/brittle + expect(spyConsoleWarn).toHaveBeenCalledWith(expect.stringMatching('contained 2 or more function folders')) + + spyConsoleWarn.mockRestore() }) -test(`should match if netlify-lambda is listed in devDependencies and is mentioned in scripts`, async (t) => { +test(`should match if netlify-lambda is listed in dependencies and is mentioned in scripts`, async () => { const packageJson = { scripts: { build: 'netlify-lambda build some/directory', }, - dependencies: {}, - devDependencies: { + dependencies: { 'netlify-lambda': 'ignored', }, + devDependencies: {}, } const match = await detectNetlifyLambda({ packageJson }) - t.is(match.builderName, 'netlify-lambda') - t.is(match.npmScript, 'build') + expect(match.builderName).toBe('netlify-lambda') + expect(match.npmScript).toBe('build') }) -test(`should not match if netlify-lambda misses function directory`, async (t) => { +test(`should match if netlify-lambda is listed in devDependencies and is mentioned in scripts`, async () => { const packageJson = { scripts: { - 'some-build-step': 'netlify-lambda build', + build: 'netlify-lambda build some/directory', }, dependencies: {}, devDependencies: { @@ -109,10 +106,12 @@ test(`should not match if netlify-lambda misses function directory`, async (t) = } const match = await detectNetlifyLambda({ packageJson }) - t.is(match, false) + + expect(match.builderName).toBe('netlify-lambda') + expect(match.npmScript).toBe('build') }) -test(`should match if netlify-lambda is configured with an additional option`, async (t) => { +test(`should match if netlify-lambda is configured with an additional option`, async () => { const packageJson = { scripts: { build: 'netlify-lambda build --config config/webpack.config.js some/directory', @@ -124,14 +123,15 @@ test(`should match if netlify-lambda is configured with an additional option`, a } const match = await detectNetlifyLambda({ packageJson }) - t.is(match.builderName, 'netlify-lambda') - t.is(match.npmScript, 'build') + + expect(match.builderName).toBe('netlify-lambda') + expect(match.npmScript).toBe('build') }) -test(`should match if netlify-lambda is configured with multiple additional options`, async (t) => { +test(`should match if netlify-lambda is configured with multiple additional options`, async () => { const packageJson = { scripts: { - build: 'netlify-lambda build -s --another-option --config config/webpack.config.js some/directory', + build: 'netlify-lambda build -p 2345 --config config/webpack.config.js --static some/directory', }, dependencies: {}, devDependencies: { @@ -140,11 +140,12 @@ test(`should match if netlify-lambda is configured with multiple additional opti } const match = await detectNetlifyLambda({ packageJson }) - t.is(match.builderName, 'netlify-lambda') - t.is(match.npmScript, 'build') + + expect(match.builderName).toBe('netlify-lambda') + expect(match.npmScript).toBe('build') }) -test('should match if netlify-lambda has options that are passed after the functions directory', async (t) => { +test('should match if netlify-lambda has options that are passed after the functions directory', async () => { const packageJson = { scripts: { build: 'netlify-lambda build some/directory --config config/webpack.config.js', @@ -156,11 +157,12 @@ test('should match if netlify-lambda has options that are passed after the funct } const match = await detectNetlifyLambda({ packageJson }) - t.is(match.builderName, 'netlify-lambda') - t.is(match.npmScript, 'build') + + expect(match.builderName).toBe('netlify-lambda') + expect(match.npmScript).toBe('build') }) -test('should match even if multiple netlify-lambda commands are specified', async (t) => { +test('should match even if multiple netlify-lambda commands are specified', async () => { const packageJson = { scripts: { 'some-serve-step': 'netlify-lambda serve serve/directory', @@ -173,6 +175,7 @@ test('should match even if multiple netlify-lambda commands are specified', asyn } const match = await detectNetlifyLambda({ packageJson }) - t.is(match.builderName, 'netlify-lambda') - t.is(match.npmScript, 'build') + + expect(match.builderName).toBe('netlify-lambda') + expect(match.npmScript).toBe('build') }) diff --git a/tests/unit/lib/functions/runtimes/rust/index.test.cjs b/tests/unit/lib/functions/runtimes/rust/index.test.cjs deleted file mode 100644 index 27abeca5bd8..00000000000 --- a/tests/unit/lib/functions/runtimes/rust/index.test.cjs +++ /dev/null @@ -1,32 +0,0 @@ -const test = require('ava') -const sinon = require('sinon') - -const { rewiremock } = require('../../../../../integration/utils/rewiremock.cjs') - -const runFunctionsProxySpy = sinon.stub() -const { invokeFunction } = rewiremock.proxy( - // eslint-disable-next-line n/global-require - () => require('../../../../../../src/lib/functions/runtimes/rust/index.cjs'), - { - '../../../../../../src/lib/functions/local-proxy.cjs': { - runFunctionsProxy: runFunctionsProxySpy, - }, - }, -) - -const invokeFunctionMacro = test.macro({ - async exec(t, prop, expected) { - runFunctionsProxySpy.resolves({ stdout: JSON.stringify({ [prop]: expected }) }) - - const match = await invokeFunction({ func: { mainFile: '', buildData: {} } }) - t.deepEqual(match[prop], expected) - }, - title(providedTitle, prop) { - return `should return ${prop}` - }, -}) - -test(invokeFunctionMacro, 'body', 'thebody') -test(invokeFunctionMacro, 'headers', { 'X-Single': 'A' }) -test(invokeFunctionMacro, 'multiValueHeaders', { 'X-Multi': ['B', 'C'] }) -test(invokeFunctionMacro, 'statusCode', 200) diff --git a/tests/unit/lib/functions/runtimes/rust/index.test.mjs b/tests/unit/lib/functions/runtimes/rust/index.test.mjs new file mode 100644 index 00000000000..faa2a23186f --- /dev/null +++ b/tests/unit/lib/functions/runtimes/rust/index.test.mjs @@ -0,0 +1,18 @@ +import { expect, test, vi } from 'vitest' + +import { runFunctionsProxy } from '../../../../../../src/lib/functions/local-proxy.mjs' +import { invokeFunction } from '../../../../../../src/lib/functions/runtimes/rust/index.mjs' + +vi.mock('../../../../../../src/lib/functions/local-proxy.mjs', () => ({ runFunctionsProxy: vi.fn() })) + +test.each([ + ['body', 'thebody'], + ['headers', { 'X-Single': 'A' }], + ['multiValueHeaders', { 'X-Multi': ['B', 'C'] }], + ['statusCode', 200], +])('should return %s', async (prop, expected) => { + runFunctionsProxy.mockImplementation(() => Promise.resolve({ stdout: JSON.stringify({ [prop]: expected }) })) + + const match = await invokeFunction({ func: { mainFile: '', buildData: {} } }) + expect(match[prop]).toEqual(expected) +}) diff --git a/tests/unit/lib/functions/server.test.cjs b/tests/unit/lib/functions/server.test.cjs deleted file mode 100644 index 9b549b654ca..00000000000 --- a/tests/unit/lib/functions/server.test.cjs +++ /dev/null @@ -1,63 +0,0 @@ -const { mkdirSync, mkdtempSync, writeFileSync } = require('fs') -const { tmpdir } = require('os') -const { join } = require('path') - -const test = require('ava') -const express = require('express') -const request = require('supertest') - -const { FunctionsRegistry } = require('../../../../src/lib/functions/registry.cjs') - -/** @type { express.Express} */ -let app - -test.before(async () => { - const projectRoot = mkdtempSync(join(tmpdir(), 'functions-server-project-root')) - const functionsDirectory = join(projectRoot, 'functions') - mkdirSync(functionsDirectory) - - const mainFile = join(functionsDirectory, 'hello.js') - writeFileSync(mainFile, `exports.handler = async (event) => ({ statusCode: 200, body: event.rawUrl })`) - - const functionsRegistry = new FunctionsRegistry({ - projectRoot, - config: {}, - timeouts: { syncFunctions: 1, backgroundFunctions: 1 }, - settings: { port: 8888 }, - }) - await functionsRegistry.scan([functionsDirectory]) - app = express() - const server = await import('../../../../src/lib/functions/server.mjs') - app.all('*', server.createHandler({ functionsRegistry })) -}) - -test('should get the url as the `rawUrl` inside the function', async (t) => { - await request(app) - .get('/hello') - .expect((res) => { - t.is(res.status, 200) - t.regex(res.text, /^http:\/\/127.0.0.1:\d+?\/hello$/) - }) -}) - -test('should get the original url as the `rawUrl` when the header was provided by the proxy', async (t) => { - await request(app) - .get('/hello') - .set('x-netlify-original-pathname', '/orig') - .expect((res) => { - t.is(res.status, 200) - console.log(res.text) - t.regex(res.text, /^http:\/\/127.0.0.1:\d+?\/orig$/) - }) -}) - -test('should check if query params are passed to the `rawUrl` when redirected', async (t) => { - await request(app) - .get('/hello?jam=stack') - .set('x-netlify-original-pathname', '/orig') - .expect((res) => { - t.is(res.status, 200) - console.log(res.text) - t.regex(res.text, /^http:\/\/127.0.0.1:\d+?\/orig\?jam=stack$/) - }) -}) diff --git a/tests/unit/lib/functions/server.test.mjs b/tests/unit/lib/functions/server.test.mjs new file mode 100644 index 00000000000..873ef977b5e --- /dev/null +++ b/tests/unit/lib/functions/server.test.mjs @@ -0,0 +1,77 @@ +import { mkdir, mkdtemp, writeFile } from 'fs/promises' +import { tmpdir } from 'os' +import { join } from 'path' + +import express from 'express' +import got from 'got' +import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' + +import { FunctionsRegistry } from '../../../../src/lib/functions/registry.mjs' +import { createHandler } from '../../../../src/lib/functions/server.mjs' + +vi.mock('../../../../src/utils/command-helpers.mjs', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.mjs')), + log: () => {}, +})) + +describe('createHandler', () => { + let server + let serverAddress + beforeAll(async () => { + const projectRoot = await mkdtemp(join(tmpdir(), 'functions-server-project-root')) + const functionsDirectory = join(projectRoot, 'functions') + await mkdir(functionsDirectory) + + const mainFile = join(functionsDirectory, 'hello.js') + await writeFile(mainFile, `exports.handler = async (event) => ({ statusCode: 200, body: event.rawUrl })`) + + const functionsRegistry = new FunctionsRegistry({ + projectRoot, + config: {}, + timeouts: { syncFunctions: 1, backgroundFunctions: 1 }, + settings: { port: 8888 }, + }) + await functionsRegistry.scan([functionsDirectory]) + const app = express() + app.all('*', createHandler({ functionsRegistry })) + + return await new Promise((resolve) => { + server = app.listen(resolve) + const { port } = server.address() + + serverAddress = `http://localhost:${port}` + }) + }) + + afterAll( + async () => + await new Promise((resolve) => { + server.close(resolve) + }), + ) + + test('should get the url as the `rawUrl` inside the function', async () => { + const response = await got.get(new URL('/hello', serverAddress)) + + expect(response.statusCode).toBe(200) + expect(response.body).toMatch(/^http:\/\/localhost:\d+?\/hello$/) + }) + + test('should get the original url as the `rawUrl` when the header was provided by the proxy', async () => { + const response = await got.get(new URL('/hello', serverAddress), { + headers: { 'x-netlify-original-pathname': '/orig' }, + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toMatch(/^http:\/\/localhost:\d+?\/orig$/) + }) + + test('should check if query params are passed to the `rawUrl` when redirected', async () => { + const response = await got.get(new URL('/hello?jam=stack', serverAddress), { + headers: { 'x-netlify-original-pathname': '/orig' }, + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toMatch(/^http:\/\/localhost:\d+?\/orig\?jam=stack$/) + }) +}) diff --git a/tests/unit/lib/geo-location.test.cjs b/tests/unit/lib/geo-location.test.cjs deleted file mode 100644 index c2f524bf3b6..00000000000 --- a/tests/unit/lib/geo-location.test.cjs +++ /dev/null @@ -1,158 +0,0 @@ -const test = require('ava') -const nock = require('nock') - -const { getGeoLocation, mockLocation } = require('../../../src/lib/geo-location.cjs') - -test('`getGeoLocation` returns geolocation data from the API if `mode: "cache"`', async (t) => { - let hasCalledStateSet = false - - const testLocation = { - city: 'Netlify Town', - country: { code: 'NF', name: 'Netlify' }, - subdivision: { code: 'JS', name: 'Jamstack' }, - } - const mockState = { - get() {}, - set(key, value) { - hasCalledStateSet = true - - t.is(key, 'geolocation') - t.deepEqual(value.data, testLocation) - t.is(typeof value.timestamp, 'number') - }, - } - const mockRequest = nock('https://netlifind.netlify.app').get('/').reply(200, { - geo: testLocation, - }) - const geo = await getGeoLocation({ mode: 'cache', state: mockState }) - - t.true(mockRequest.isDone()) - t.true(hasCalledStateSet) - t.deepEqual(geo, testLocation) -}) - -test('`getGeoLocation` returns geolocation data from cache if data is younger than TTL', async (t) => { - let hasCalledStateSet = false - - const testLocation = { - city: 'Netlify Town', - country: { code: 'NF', name: 'Netlify' }, - subdivision: { code: 'JS', name: 'Jamstack' }, - } - const mockState = { - get(key) { - t.is(key, 'geolocation') - - return { - data: testLocation, - timestamp: Date.now() - 3, - } - }, - set() { - hasCalledStateSet = true - }, - } - const mockRequest = nock('https://netlifind.netlify.app').get('/').reply(200, { - geo: testLocation, - }) - const geo = await getGeoLocation({ mode: 'cache', state: mockState }) - - t.false(mockRequest.isDone()) - t.false(hasCalledStateSet) - t.deepEqual(geo, testLocation) -}) - -test('`getGeoLocation` returns geolocation data from cache, even if older than TTL, if the application is offline', async (t) => { - let hasCalledStateSet = false - - const testLocation = { - city: 'Netlify Town', - country: { code: 'NF', name: 'Netlify' }, - subdivision: { code: 'JS', name: 'Jamstack' }, - } - const mockState = { - get(key) { - t.is(key, 'geolocation') - - return { - data: testLocation, - timestamp: 0, - } - }, - set() { - hasCalledStateSet = true - }, - } - const mockRequest = nock('https://netlifind.netlify.app').get('/').reply(200, { - geo: testLocation, - }) - const geo = await getGeoLocation({ mode: 'cache', offline: true, state: mockState }) - - t.false(mockRequest.isDone()) - t.false(hasCalledStateSet) - t.deepEqual(geo, testLocation) -}) - -test('`getGeoLocation` returns mock geolocation data if `mode: "mock"`', async (t) => { - let hasCalledStateSet = false - - const testLocation = { - city: 'Netlify Town', - country: { code: 'NF', name: 'Netlify' }, - subdivision: { code: 'JS', name: 'Jamstack' }, - } - const mockState = { - get() {}, - set() { - hasCalledStateSet = true - }, - } - const mockRequest = nock('https://netlifind.netlify.app').get('/').reply(200, { - geo: testLocation, - }) - const geo = await getGeoLocation({ mode: 'mock', state: mockState }) - - t.false(mockRequest.isDone()) - t.false(hasCalledStateSet) - t.deepEqual(geo, mockLocation) -}) - -test('`getGeoLocation` returns mock geolocation data if valid country code set', async (t) => { - const returnedLocation = { - city: 'Mock City', - country: { code: 'CA', name: 'Mock Country' }, - subdivision: { code: 'SD', name: 'Mock Subdivision' }, - latitude: 0, - longitude: 0, - timezone: 'UTC', - } - - const mockState = { - get() {}, - set() {}, - } - - const geo = await getGeoLocation({ mode: 'mock', state: mockState, geoCountry: 'CA' }) - - t.deepEqual(geo, returnedLocation) -}) - -test('`getGeoLocation` mocks country code when not using mock flag', async (t) => { - const mockState = { - get() {}, - set() {}, - } - - const returnedLocation = { - city: 'Mock City', - country: { code: 'CA', name: 'Mock Country' }, - subdivision: { code: 'SD', name: 'Mock Subdivision' }, - latitude: 0, - longitude: 0, - timezone: 'UTC', - } - - const geo = await getGeoLocation({ mode: 'update', offline: false, state: mockState, geoCountry: 'CA' }) - - t.deepEqual(geo, returnedLocation) -}) diff --git a/tests/unit/lib/geo-location.test.mjs b/tests/unit/lib/geo-location.test.mjs new file mode 100644 index 00000000000..909dc3070cb --- /dev/null +++ b/tests/unit/lib/geo-location.test.mjs @@ -0,0 +1,160 @@ +import nock from 'nock' +import { describe, expect, test } from 'vitest' + +import { getGeoLocation, mockLocation } from '../../../src/lib/geo-location.mjs' + +describe('getGeoLocation', () => { + test('returns geolocation data from the API if `mode: "cache"`', async () => { + let hasCalledStateSet = false + + const testLocation = { + city: 'Netlify Town', + country: { code: 'NF', name: 'Netlify' }, + subdivision: { code: 'JS', name: 'Jamstack' }, + } + const mockState = { + get() {}, + set(key, value) { + hasCalledStateSet = true + + expect(key).toBe('geolocation') + expect(value.data).toEqual(testLocation) + expect(typeof value.timestamp).toBe('number') + }, + } + const mockRequest = nock('https://netlifind.netlify.app').get('/').reply(200, { + geo: testLocation, + }) + const geo = await getGeoLocation({ mode: 'cache', state: mockState }) + + expect(mockRequest.isDone()).toBe(true) + expect(hasCalledStateSet).toBe(true) + expect(geo).toEqual(testLocation) + }) + + test('returns geolocation data from cache if data is younger than TTL', async () => { + let hasCalledStateSet = false + + const testLocation = { + city: 'Netlify Town', + country: { code: 'NF', name: 'Netlify' }, + subdivision: { code: 'JS', name: 'Jamstack' }, + } + const mockState = { + get(key) { + expect(key).toBe('geolocation') + + return { + data: testLocation, + timestamp: Date.now() - 3, + } + }, + set() { + hasCalledStateSet = true + }, + } + const mockRequest = nock('https://netlifind.netlify.app').get('/').reply(200, { + geo: testLocation, + }) + const geo = await getGeoLocation({ mode: 'cache', state: mockState }) + + expect(mockRequest.isDone()).toBe(false) + expect(hasCalledStateSet).toBe(false) + expect(geo).toEqual(testLocation) + }) + + test('returns geolocation data from cache, even if older than TTL, if the application is offline', async () => { + let hasCalledStateSet = false + + const testLocation = { + city: 'Netlify Town', + country: { code: 'NF', name: 'Netlify' }, + subdivision: { code: 'JS', name: 'Jamstack' }, + } + const mockState = { + get(key) { + expect(key).toBe('geolocation') + + return { + data: testLocation, + timestamp: 0, + } + }, + set() { + hasCalledStateSet = true + }, + } + const mockRequest = nock('https://netlifind.netlify.app').get('/').reply(200, { + geo: testLocation, + }) + const geo = await getGeoLocation({ mode: 'cache', offline: true, state: mockState }) + + expect(mockRequest.isDone()).toBe(false) + expect(hasCalledStateSet).toBe(false) + expect(geo).toEqual(testLocation) + }) + + test('returns mock geolocation data if `mode: "mock"`', async () => { + let hasCalledStateSet = false + + const testLocation = { + city: 'Netlify Town', + country: { code: 'NF', name: 'Netlify' }, + subdivision: { code: 'JS', name: 'Jamstack' }, + } + const mockState = { + get() {}, + set() { + hasCalledStateSet = true + }, + } + const mockRequest = nock('https://netlifind.netlify.app').get('/').reply(200, { + geo: testLocation, + }) + const geo = await getGeoLocation({ mode: 'mock', state: mockState }) + + expect(mockRequest.isDone()).toBe(false) + expect(hasCalledStateSet).toBe(false) + expect(geo).toEqual(mockLocation) + }) + + test('returns mock geolocation data if valid country code set', async () => { + const returnedLocation = { + city: 'Mock City', + country: { code: 'CA', name: 'Mock Country' }, + subdivision: { code: 'SD', name: 'Mock Subdivision' }, + latitude: 0, + longitude: 0, + timezone: 'UTC', + } + + const mockState = { + get() {}, + set() {}, + } + + const geo = await getGeoLocation({ mode: 'mock', state: mockState, geoCountry: 'CA' }) + + expect(geo).toEqual(returnedLocation) + }) + + test('mocks country code when not using mock flag', async () => { + const mockState = { + get() {}, + set() {}, + } + + const returnedLocation = { + city: 'Mock City', + country: { code: 'CA', name: 'Mock Country' }, + subdivision: { code: 'SD', name: 'Mock Subdivision' }, + latitude: 0, + longitude: 0, + timezone: 'UTC', + } + + const geo = await getGeoLocation({ mode: 'update', offline: false, state: mockState, geoCountry: 'CA' }) + + expect(geo).toEqual(returnedLocation) + }) +}) diff --git a/tests/unit/utils/command-helpers.test.mjs b/tests/unit/utils/command-helpers.test.mjs new file mode 100644 index 00000000000..5b2b7e9a30e --- /dev/null +++ b/tests/unit/utils/command-helpers.test.mjs @@ -0,0 +1,17 @@ +import { describe, expect, test } from 'vitest' + +import { normalizeConfig } from '../../../src/utils/command-helpers.mjs' + +describe('normalizeConfig', () => { + test('should remove publish and publishOrigin property if publishOrigin is "default"', () => { + const config = { build: { publish: 'a', publishOrigin: 'default' } } + + expect(normalizeConfig(config)).toEqual({ build: {} }) + }) + + test('should return same config object if publishOrigin is not "default"', () => { + const config = { build: { publish: 'a', publishOrigin: 'b' } } + + expect(normalizeConfig(config)).toBe(config) + }) +}) diff --git a/tests/unit/utils/deploy/hash-files.test.cjs b/tests/unit/utils/deploy/hash-files.test.mjs similarity index 53% rename from tests/unit/utils/deploy/hash-files.test.cjs rename to tests/unit/utils/deploy/hash-files.test.mjs index c6ed5570c00..5c035a1bee9 100644 --- a/tests/unit/utils/deploy/hash-files.test.cjs +++ b/tests/unit/utils/deploy/hash-files.test.mjs @@ -1,10 +1,10 @@ -const test = require('ava') +import { expect, test } from 'vitest' -const { DEFAULT_CONCURRENT_HASH } = require('../../../../src/utils/deploy/constants.cjs') -const { hashFiles } = require('../../../../src/utils/deploy/hash-files.cjs') -const { withSiteBuilder } = require('../../../integration/utils/site-builder.cjs') +import { DEFAULT_CONCURRENT_HASH } from '../../../../src/utils/deploy/constants.mjs' +import hashFiles from '../../../../src/utils/deploy/hash-files.mjs' +import { withSiteBuilder } from '../../../integration/utils/site-builder.cjs' -test('Hashes files in a folder', async (t) => { +test('Hashes files in a folder', async () => { await withSiteBuilder('site-with-content', async (builder) => { await builder .withNetlifyToml({ config: { build: { publish: 'public' } } }) @@ -22,20 +22,16 @@ test('Hashes files in a folder', async (t) => { statusCb() {}, }) - t.is(Object.entries(files).length, expectedFiles.length) - t.is(Object.entries(filesShaMap).length, expectedFiles.length) + expect(Object.entries(files)).toHaveLength(expectedFiles.length) + expect(Object.entries(filesShaMap)).toHaveLength(expectedFiles.length) expectedFiles.forEach((filePath) => { const sha = files[filePath] - t.truthy(sha, `includes the ${filePath} file`) + expect(sha).toBeDefined() const fileObjArray = filesShaMap[sha] fileObjArray.forEach((fileObj) => { - t.is( - fileObj.normalizedPath, - filePath, - 'fileObj normalizedPath property should equal to file path from files array', - ) + expect(fileObj.normalizedPath).toBe(filePath) }) }) }) diff --git a/tests/unit/utils/deploy/hash-fns.test.cjs b/tests/unit/utils/deploy/hash-fns.test.mjs similarity index 52% rename from tests/unit/utils/deploy/hash-fns.test.cjs rename to tests/unit/utils/deploy/hash-fns.test.mjs index 90fddc7987a..eef29ec6aff 100644 --- a/tests/unit/utils/deploy/hash-fns.test.cjs +++ b/tests/unit/utils/deploy/hash-fns.test.mjs @@ -1,21 +1,22 @@ -/* eslint-disable require-await */ -const test = require('ava') -const tempy = require('tempy') +import tempy from 'tempy' +import { expect, test } from 'vitest' -const { DEFAULT_CONCURRENT_HASH } = require('../../../../src/utils/deploy/constants.cjs') -const { hashFns } = require('../../../../src/utils/deploy/hash-fns.cjs') -const { withSiteBuilder } = require('../../../integration/utils/site-builder.cjs') +import { DEFAULT_CONCURRENT_HASH } from '../../../../src/utils/deploy/constants.mjs' +import hashFns from '../../../../src/utils/deploy/hash-fns.mjs' +import { withSiteBuilder } from '../../../integration/utils/site-builder.cjs' -test('Hashes files in a folder', async (t) => { +test('Hashes files in a folder', async () => { await withSiteBuilder('site-with-functions', async (builder) => { await builder .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) .withFunction({ path: 'hello.js', + // eslint-disable-next-line require-await handler: async () => ({ statusCode: 200, body: 'Hello' }), }) .withFunction({ path: 'goodbye.js', + // eslint-disable-next-line require-await handler: async () => ({ statusCode: 200, body: 'Goodbye' }), }) .buildAsync() @@ -27,22 +28,17 @@ test('Hashes files in a folder', async (t) => { statusCb() {}, }) - t.is(Object.entries(functions).length, expectedFunctions.length) - t.is(Object.entries(fnShaMap).length, expectedFunctions.length) + expect(Object.entries(functions)).toHaveLength(expectedFunctions.length) + expect(Object.entries(fnShaMap)).toHaveLength(expectedFunctions.length) expectedFunctions.forEach((functionPath) => { const sha = functions[functionPath] - t.truthy(sha, `includes the ${functionPath} file`) + expect(sha).toBeDefined() const functionsObjArray = fnShaMap[sha] functionsObjArray.forEach((fileObj) => { - t.is( - fileObj.normalizedPath, - functionPath, - 'functionPath normalizedPath property should equal to function path from functions array', - ) + expect(fileObj.normalizedPath).toBe(functionPath) }) }) }) }) -/* eslint-enable require-await */ diff --git a/tests/unit/utils/deploy/upload-files.test.cjs b/tests/unit/utils/deploy/upload-files.test.cjs deleted file mode 100644 index 91a16fc009a..00000000000 --- a/tests/unit/utils/deploy/upload-files.test.cjs +++ /dev/null @@ -1,41 +0,0 @@ -const test = require('ava') -const sinon = require('sinon') -const { v4: generateUUID } = require('uuid') - -const { uploadFiles } = require('../../../../src/utils/deploy/upload-files.cjs') - -test('Adds a retry count to function upload requests', async (t) => { - const uploadDeployFunction = sinon.stub() - const mockError = new Error('Uh-oh') - - mockError.status = 500 - - uploadDeployFunction.onCall(0).throws(mockError) - uploadDeployFunction.onCall(1).throws(mockError) - uploadDeployFunction.onCall(2).resolves() - - const mockApi = { - uploadDeployFunction, - } - const deployId = generateUUID() - const files = [ - { - assetType: 'function', - filepath: '/some/path/func1.zip', - normalizedPath: 'func1.zip', - runtime: 'js', - }, - ] - const options = { - concurrentUpload: 1, - maxRetry: 3, - statusCb: sinon.stub(), - } - - await uploadFiles(mockApi, deployId, files, options) - - t.is(uploadDeployFunction.callCount, 3) - t.is(uploadDeployFunction.firstCall.args[0].xNfRetryCount, undefined) - t.is(uploadDeployFunction.secondCall.args[0].xNfRetryCount, 1) - t.is(uploadDeployFunction.thirdCall.args[0].xNfRetryCount, 2) -}) diff --git a/tests/unit/utils/deploy/upload-files.test.mjs b/tests/unit/utils/deploy/upload-files.test.mjs new file mode 100644 index 00000000000..ab47c44ffcf --- /dev/null +++ b/tests/unit/utils/deploy/upload-files.test.mjs @@ -0,0 +1,51 @@ +import { v4 as generateUUID } from 'uuid' +import { afterAll, expect, test, vi } from 'vitest' + +import uploadFiles from '../../../../src/utils/deploy/upload-files.mjs' + +vi.mock('../../../../src/utils/deploy/constants.mjs', async () => { + const actual = await vi.importActual('../../../../src/utils/deploy/constants.mjs') + + // Reduce the delay, so these tests do not wait for 10 seconds + return { ...actual, UPLOAD_INITIAL_DELAY: 100, UPLOAD_MAX_DELAY: 200 } +}) + +afterAll(() => { + vi.restoreAllMocks() +}) + +test('Adds a retry count to function upload requests', async () => { + const uploadDeployFunction = vi.fn() + const mockError = new Error('Uh-oh') + + mockError.status = 500 + + uploadDeployFunction.mockRejectedValueOnce(mockError) + uploadDeployFunction.mockRejectedValueOnce(mockError) + uploadDeployFunction.mockResolvedValueOnce() + + const mockApi = { + uploadDeployFunction, + } + const deployId = generateUUID() + const files = [ + { + assetType: 'function', + filepath: '/some/path/func1.zip', + normalizedPath: 'func1.zip', + runtime: 'js', + }, + ] + const options = { + concurrentUpload: 1, + maxRetry: 3, + statusCb: vi.fn(), + } + + await uploadFiles(mockApi, deployId, files, options) + + expect(uploadDeployFunction).toHaveBeenCalledTimes(3) + expect(uploadDeployFunction).toHaveBeenNthCalledWith(1, expect.not.objectContaining({ xNfRetryCount: 1 })) + expect(uploadDeployFunction).toHaveBeenNthCalledWith(2, expect.objectContaining({ xNfRetryCount: 1 })) + expect(uploadDeployFunction).toHaveBeenNthCalledWith(3, expect.objectContaining({ xNfRetryCount: 2 })) +}) diff --git a/tests/unit/utils/deploy/util.test.cjs b/tests/unit/utils/deploy/util.test.cjs deleted file mode 100644 index 59150481fab..00000000000 --- a/tests/unit/utils/deploy/util.test.cjs +++ /dev/null @@ -1,17 +0,0 @@ -const { join } = require('path') - -const test = require('ava') - -const { normalizePath } = require('../../../../src/utils/deploy/util.cjs') - -test('normalizes relative file paths', (t) => { - const input = join('foo', 'bar', 'baz.js') - t.is(normalizePath(input), 'foo/bar/baz.js') -}) - -test('normalizePath should throw the error if name is invalid', (t) => { - t.throws(() => normalizePath('invalid name#')) - t.throws(() => normalizePath('invalid name?')) - t.throws(() => normalizePath('??')) - t.throws(() => normalizePath('#')) -}) diff --git a/tests/unit/utils/deploy/util.test.mjs b/tests/unit/utils/deploy/util.test.mjs new file mode 100644 index 00000000000..eea00816e94 --- /dev/null +++ b/tests/unit/utils/deploy/util.test.mjs @@ -0,0 +1,19 @@ +import { join } from 'path' + +import { describe, expect, test } from 'vitest' + +import { normalizePath } from '../../../../src/utils/deploy/util.mjs' + +describe('normalizePath', () => { + test('normalizes relative file paths', () => { + const input = join('foo', 'bar', 'baz.js') + expect(normalizePath(input)).toBe('foo/bar/baz.js') + }) + + test('normalizePath should throw the error if name is invalid', () => { + expect(() => normalizePath('invalid name#')).toThrowError() + expect(() => normalizePath('invalid name?')).toThrowError() + expect(() => normalizePath('??')).toThrowError() + expect(() => normalizePath('#')).toThrowError() + }) +}) diff --git a/tests/unit/utils/dot-env.test.cjs b/tests/unit/utils/dot-env.test.mjs similarity index 75% rename from tests/unit/utils/dot-env.test.cjs rename to tests/unit/utils/dot-env.test.mjs index b7b3630a1b5..d124681f2ff 100644 --- a/tests/unit/utils/dot-env.test.cjs +++ b/tests/unit/utils/dot-env.test.mjs @@ -1,20 +1,20 @@ -const process = require('process') +import process from 'process' -const test = require('ava') +import { expect, test } from 'vitest' -const { tryLoadDotEnvFiles } = require('../../../src/utils/dot-env.cjs') -const { withSiteBuilder } = require('../../integration/utils/site-builder.cjs') +import { tryLoadDotEnvFiles } from '../../../src/utils/dot-env.mjs' +import { withSiteBuilder } from '../../integration/utils/site-builder.cjs' -test('should return an empty array for a site with no .env file', async (t) => { +test('should return an empty array for a site with no .env file', async () => { await withSiteBuilder('site-without-env-file', async (builder) => { await builder.buildAsync() const results = await tryLoadDotEnvFiles({ projectDir: builder.directory }) - t.deepEqual(results, []) + expect(results).toEqual([]) }) }) -test('should read env vars from .env file', async (t) => { +test('should read env vars from .env file', async () => { process.env.NODE_ENV = 'development' await withSiteBuilder('site-with-envs-file', async (builder) => { builder.withEnvFile({ @@ -24,11 +24,11 @@ test('should read env vars from .env file', async (t) => { await builder.buildAsync() const results = await tryLoadDotEnvFiles({ projectDir: builder.directory }) - t.deepEqual(results, [{ file: '.env', env: { TEST: 'FROM_ENV' } }]) + expect(results).toEqual([{ file: '.env', env: { TEST: 'FROM_ENV' } }]) }) }) -test('should read env vars from .env.development file', async (t) => { +test('should read env vars from .env.development file', async () => { process.env.NODE_ENV = 'development' await withSiteBuilder('site-with-envs-file', async (builder) => { builder.withEnvFile({ @@ -38,11 +38,11 @@ test('should read env vars from .env.development file', async (t) => { await builder.buildAsync() const results = await tryLoadDotEnvFiles({ projectDir: builder.directory }) - t.deepEqual(results, [{ file: '.env.development', env: { TEST: 'FROM_DEVELOPMENT_ENV' } }]) + expect(results).toEqual([{ file: '.env.development', env: { TEST: 'FROM_DEVELOPMENT_ENV' } }]) }) }) -test('should read env vars from .env.local file', async (t) => { +test('should read env vars from .env.local file', async () => { process.env.NODE_ENV = 'development' await withSiteBuilder('site-with-envs-file', async (builder) => { builder.withEnvFile({ @@ -52,11 +52,11 @@ test('should read env vars from .env.local file', async (t) => { await builder.buildAsync() const results = await tryLoadDotEnvFiles({ projectDir: builder.directory }) - t.deepEqual(results, [{ file: '.env.local', env: { TEST: 'FROM_LOCAL_ENV' } }]) + expect(results).toEqual([{ file: '.env.local', env: { TEST: 'FROM_LOCAL_ENV' } }]) }) }) -test('should read env vars from .env.development.local file', async (t) => { +test('should read env vars from .env.development.local file', async () => { process.env.NODE_ENV = 'development' await withSiteBuilder('site-with-envs-file', async (builder) => { builder.withEnvFile({ @@ -66,11 +66,11 @@ test('should read env vars from .env.development.local file', async (t) => { await builder.buildAsync() const results = await tryLoadDotEnvFiles({ projectDir: builder.directory }) - t.deepEqual(results, [{ file: '.env.development.local', env: { TEST: 'FROM_LOCAL_DEVELOPMENT_ENV' } }]) + expect(results).toEqual([{ file: '.env.development.local', env: { TEST: 'FROM_LOCAL_DEVELOPMENT_ENV' } }]) }) }) -test('should read env vars from all four .env[.development][.local] files', async (t) => { +test('should read env vars from all four .env[.development][.local] files', async () => { process.env.NODE_ENV = 'development' await withSiteBuilder('site-with-envs-file', async (builder) => { builder @@ -93,7 +93,7 @@ test('should read env vars from all four .env[.development][.local] files', asyn await builder.buildAsync() const results = await tryLoadDotEnvFiles({ projectDir: builder.directory }) - t.deepEqual(results, [ + expect(results).toEqual([ { file: '.env', env: { ONE: 'FROM_ENV', TWO: 'FROM_ENV' } }, { file: '.env.development', env: { ONE: 'FROM_DEVELOPMENT_ENV', THREE: 'FROM_DEVELOPMENT_ENV' } }, { file: '.env.local', env: { ONE: 'FROM_LOCAL_ENV', FOUR: 'FROM_LOCAL_ENV' } }, @@ -105,7 +105,7 @@ test('should read env vars from all four .env[.development][.local] files', asyn }) }) -test('should handle empty .env file', async (t) => { +test('should handle empty .env file', async () => { await withSiteBuilder('site-with-empty-env-file', async (builder) => { builder.withEnvFile({ path: '.env', @@ -114,6 +114,6 @@ test('should handle empty .env file', async (t) => { await builder.buildAsync() const results = await tryLoadDotEnvFiles({ projectDir: builder.directory }) - t.deepEqual(results, [{ file: '.env', env: {} }]) + expect(results).toEqual([{ file: '.env', env: {} }]) }) }) diff --git a/tests/unit/utils/env/index.test.cjs b/tests/unit/utils/env/index.test.mjs similarity index 65% rename from tests/unit/utils/env/index.test.cjs rename to tests/unit/utils/env/index.test.mjs index c9ef39b51af..0b77be248fd 100644 --- a/tests/unit/utils/env/index.test.cjs +++ b/tests/unit/utils/env/index.test.mjs @@ -1,6 +1,6 @@ -const test = require('ava') +import { expect, test } from 'vitest' -const { +import { filterEnvBySource, findValueInValues, formatEnvelopeData, @@ -8,9 +8,9 @@ const { normalizeContext, translateFromEnvelopeToMongo, translateFromMongoToEnvelope, -} = require('../../../../src/utils/env/index.cjs') +} from '../../../../src/utils/env/index.mjs' -test('should find a value from a given context', (t) => { +test('should find a value from a given context', () => { const values = [ { context: 'production', @@ -22,10 +22,10 @@ test('should find a value from a given context', (t) => { }, ] const { value } = findValueInValues(values, 'dev') - t.is(value, 'bar') + expect(value).toBe('bar') }) -test('should find a value from a given branch', (t) => { +test('should find a value from a given branch', () => { const values = [ { context: 'branch', @@ -38,10 +38,10 @@ test('should find a value from a given branch', (t) => { }, ] const { value } = findValueInValues(values, 'staging') - t.is(value, 'foo') + expect(value).toBe('foo') }) -test('should filter an env from a given source', (t) => { +test('should filter an env from a given source', () => { const env = { FOO: { value: 'sup', @@ -53,7 +53,7 @@ test('should filter an env from a given source', (t) => { }, } const filteredEnv = filterEnvBySource(env, 'ui') - t.deepEqual(filteredEnv, { + expect(filteredEnv).toEqual({ FOO: { value: 'sup', sources: ['ui'], @@ -61,7 +61,7 @@ test('should filter an env from a given source', (t) => { }) }) -test("should filter, sort, and format Envelope's response correctly", (t) => { +test("should filter, sort, and format Envelope's response correctly", () => { const envelopeItems = [ { key: 'FOO', @@ -90,10 +90,10 @@ test("should filter, sort, and format Envelope's response correctly", (t) => { }, ] - t.deepEqual(formatEnvelopeData({ context: 'dev', envelopeItems, scope: 'any', source: 'ui' }), { + expect(formatEnvelopeData({ context: 'dev', envelopeItems, scope: 'any', source: 'ui' })).toEqual({ FOO: { branch: undefined, context: 'all', scopes: ['functions'], sources: ['ui'], value: 'bar' }, }) - t.deepEqual(formatEnvelopeData({ context: 'staging', envelopeItems, scope: 'runtime', source: 'account' }), { + expect(formatEnvelopeData({ context: 'staging', envelopeItems, scope: 'runtime', source: 'account' })).toEqual({ BAZ: { branch: 'staging', context: 'branch', @@ -102,7 +102,7 @@ test("should filter, sort, and format Envelope's response correctly", (t) => { value: 'blah', }, }) - t.deepEqual(formatEnvelopeData({ context: 'production', envelopeItems, source: 'general' }), { + expect(formatEnvelopeData({ context: 'production', envelopeItems, source: 'general' })).toEqual({ BAZ: { branch: undefined, context: 'production', @@ -114,40 +114,40 @@ test("should filter, sort, and format Envelope's response correctly", (t) => { }) }) -test('should convert scope keys into a human-readable list', (t) => { - t.is(getHumanReadableScopes([]), '') - t.is(getHumanReadableScopes(), 'Builds, Post processing') - t.is(getHumanReadableScopes(['post_processing']), 'Post processing') - t.is(getHumanReadableScopes(['post-processing']), 'Post processing') - t.is(getHumanReadableScopes(['builds', 'functions']), 'Builds, Functions') - t.is(getHumanReadableScopes(['builds', 'functions', 'runtime', 'post_processing']), 'All') - t.is(getHumanReadableScopes(['builds', 'functions', 'runtime', 'post-processing']), 'All') +test('should convert scope keys into a human-readable list', () => { + expect(getHumanReadableScopes([])).toBe('') + expect(getHumanReadableScopes()).toBe('Builds, Post processing') + expect(getHumanReadableScopes(['post_processing'])).toBe('Post processing') + expect(getHumanReadableScopes(['post-processing'])).toBe('Post processing') + expect(getHumanReadableScopes(['builds', 'functions'])).toBe('Builds, Functions') + expect(getHumanReadableScopes(['builds', 'functions', 'runtime', 'post_processing'])).toBe('All') + expect(getHumanReadableScopes(['builds', 'functions', 'runtime', 'post-processing'])).toBe('All') }) -test('should normalize a branch name or context', (t) => { - t.is(normalizeContext('branch:prod'), 'prod') - t.is(normalizeContext('branch:staging'), 'staging') - t.is(normalizeContext('dev'), 'dev') - t.is(normalizeContext('development'), 'development') - t.is(normalizeContext('dp'), 'deploy-preview') - t.is(normalizeContext('prod'), 'production') - t.is(normalizeContext('qa'), 'qa') - t.is(normalizeContext('staging'), 'staging') +test('should normalize a branch name or context', () => { + expect(normalizeContext('branch:prod')).toBe('prod') + expect(normalizeContext('branch:staging')).toBe('staging') + expect(normalizeContext('dev')).toBe('dev') + expect(normalizeContext('development')).toBe('development') + expect(normalizeContext('dp')).toBe('deploy-preview') + expect(normalizeContext('prod')).toBe('production') + expect(normalizeContext('qa')).toBe('qa') + expect(normalizeContext('staging')).toBe('staging') }) -test('should translate from Mongo format to Envelope format when undefined', (t) => { +test('should translate from Mongo format to Envelope format when undefined', () => { const env = translateFromMongoToEnvelope() - t.deepEqual(env, []) + expect(env).toEqual([]) }) -test('should translate from Mongo format to Envelope format when empty object', (t) => { +test('should translate from Mongo format to Envelope format when empty object', () => { const env = translateFromMongoToEnvelope({}) - t.deepEqual(env, []) + expect(env).toEqual([]) }) -test('should translate from Mongo format to Envelope format with one env var', (t) => { +test('should translate from Mongo format to Envelope format with one env var', () => { const env = translateFromMongoToEnvelope({ foo: 'bar' }) - t.deepEqual(env, [ + expect(env).toEqual([ { key: 'foo', scopes: ['builds', 'functions', 'runtime', 'post_processing'], @@ -161,9 +161,9 @@ test('should translate from Mongo format to Envelope format with one env var', ( ]) }) -test('should translate from Mongo format to Envelope format with two env vars', (t) => { +test('should translate from Mongo format to Envelope format with two env vars', () => { const env = translateFromMongoToEnvelope({ foo: 'bar', baz: 'bang' }) - t.deepEqual(env, [ + expect(env).toEqual([ { key: 'foo', scopes: ['builds', 'functions', 'runtime', 'post_processing'], @@ -187,17 +187,17 @@ test('should translate from Mongo format to Envelope format with two env vars', ]) }) -test('should translate from Envelope format to Mongo format when undefined', (t) => { +test('should translate from Envelope format to Mongo format when undefined', () => { const env = translateFromEnvelopeToMongo() - t.deepEqual(env, {}) + expect(env).toEqual({}) }) -test('should translate from Envelope format to Mongo format when empty array', (t) => { +test('should translate from Envelope format to Mongo format when empty array', () => { const env = translateFromEnvelopeToMongo([]) - t.deepEqual(env, {}) + expect(env).toEqual({}) }) -test('should translate from Envelope format to Mongo format with one env var', (t) => { +test('should translate from Envelope format to Mongo format with one env var', () => { const env = translateFromEnvelopeToMongo([ { key: 'foo', @@ -210,10 +210,10 @@ test('should translate from Envelope format to Mongo format with one env var', ( ], }, ]) - t.deepEqual(env, { foo: 'bar' }) + expect(env).toEqual({ foo: 'bar' }) }) -test('should translate from Envelope format to Mongo format with two env vars', (t) => { +test('should translate from Envelope format to Mongo format with two env vars', () => { const env = translateFromEnvelopeToMongo([ { key: 'foo', @@ -236,5 +236,5 @@ test('should translate from Envelope format to Mongo format with two env vars', ], }, ]) - t.deepEqual(env, { foo: 'bar', baz: 'bang' }) + expect(env).toEqual({ foo: 'bar', baz: 'bang' }) }) diff --git a/tests/unit/utils/get-global-config.test.cjs b/tests/unit/utils/get-global-config.test.mjs similarity index 52% rename from tests/unit/utils/get-global-config.test.cjs rename to tests/unit/utils/get-global-config.test.mjs index 580d7c5f955..1e0f458855f 100644 --- a/tests/unit/utils/get-global-config.test.cjs +++ b/tests/unit/utils/get-global-config.test.mjs @@ -1,24 +1,25 @@ -const { copyFile, mkdir, readFile, unlink, writeFile } = require('fs').promises -const os = require('os') -const path = require('path') +import { copyFile, mkdir, readFile, unlink, writeFile } from 'fs/promises' +import os from 'os' +import { join } from 'path' -const test = require('ava') +import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest' -const { rmdirRecursiveAsync } = require('../../../src/lib/fs.cjs') -const { getLegacyPathInHome, getPathInHome } = require('../../../src/lib/settings.cjs') -const getGlobalConfig = require('../../../src/utils/get-global-config.cjs') +import { rmdirRecursiveAsync } from '../../../src/lib/fs.cjs' +import { getLegacyPathInHome, getPathInHome } from '../../../src/lib/settings.cjs' +import getGlobalConfig, { resetConfigCache } from '../../../src/utils/get-global-config.mjs' const configPath = getPathInHome(['config.json']) const legacyConfigPath = getLegacyPathInHome(['config.json']) -const tmpConfigBackupPath = path.join(os.tmpdir(), `netlify-config-backup-${Date.now()}`) +const tmpConfigBackupPath = join(os.tmpdir(), `netlify-config-backup-${Date.now()}`) -test.before('backup current user config if exists', async () => { +beforeAll(async () => { try { + // backup current user config if exists await copyFile(configPath, tmpConfigBackupPath) } catch {} }) -test.after.always('cleanup tmp directory and legacy config', async () => { +afterAll(async () => { try { // Restore user config if exists await mkdir(getPathInHome([])) @@ -30,39 +31,43 @@ test.after.always('cleanup tmp directory and legacy config', async () => { await rmdirRecursiveAsync(getLegacyPathInHome([])) }) -test.beforeEach('recreate clean config directories', async () => { +beforeEach(async () => { // Remove config dirs await rmdirRecursiveAsync(getPathInHome([])) await rmdirRecursiveAsync(getLegacyPathInHome([])) // Make config dirs await mkdir(getPathInHome([])) await mkdir(getLegacyPathInHome([])) -}) -// Not running tests in parallel as we're messing with the same config files + // reset the memoized config for the tests + resetConfigCache() +}) -test.serial('should use legacy config values as default if exists', async (t) => { +test('should use legacy config values as default if exists', async () => { const legacyConfig = { someOldKey: 'someOldValue', overrideMe: 'oldValue' } const newConfig = { overrideMe: 'newValue' } await writeFile(legacyConfigPath, JSON.stringify(legacyConfig)) await writeFile(configPath, JSON.stringify(newConfig)) const globalConfig = await getGlobalConfig() - t.is(globalConfig.get('someOldKey'), legacyConfig.someOldKey) - t.is(globalConfig.get('overrideMe'), newConfig.overrideMe) + + expect(globalConfig.get('someOldKey')).toBe(legacyConfig.someOldKey) + expect(globalConfig.get('overrideMe')).toBe(newConfig.overrideMe) }) -test.serial('should not throw if legacy config is invalid JSON', async (t) => { +test('should not throw if legacy config is invalid JSON', async () => { await writeFile(legacyConfigPath, 'NotJson') - await t.notThrowsAsync(getGlobalConfig) + + await expect(getGlobalConfig()).resolves.not.toThrowError() }) -test.serial("should create config in netlify's config dir if none exists and store new values", async (t) => { +test("should create config in netlify's config dir if none exists and store new values", async () => { // Remove config dirs await rmdirRecursiveAsync(getPathInHome([])) await rmdirRecursiveAsync(getLegacyPathInHome([])) const globalConfig = await getGlobalConfig() globalConfig.set('newProp', 'newValue') const configFile = JSON.parse(await readFile(configPath, 'utf-8')) - t.deepEqual(globalConfig.all, configFile) + + expect(globalConfig.all).toEqual(configFile) }) diff --git a/tests/unit/utils/gh-auth.test.cjs b/tests/unit/utils/gh-auth.test.cjs deleted file mode 100644 index 356bbfe3bdd..00000000000 --- a/tests/unit/utils/gh-auth.test.cjs +++ /dev/null @@ -1,52 +0,0 @@ -const test = require('ava') -const backoff = require('backoff') -const fetch = require('node-fetch') -const sinon = require('sinon') - -// eslint-disable-next-line import/order -const openBrowser = require('../../../src/utils/open-browser.cjs') -// Stub needs to be required before './gh-auth' as this uses the module -/** @type {string} */ -let host -const stubbedModule = sinon.stub(openBrowser, 'openBrowser').callsFake(({ url }) => { - const params = new URLSearchParams(url.slice(url.indexOf('?') + 1)) - host = params.get('host') - return Promise.resolve() -}) - -// eslint-disable-next-line import/order -const { authWithNetlify } = require('../../../src/utils/gh-auth.cjs') - -test.after(() => { - stubbedModule.restore() -}) - -test('should check if the authWithNetlify is working', async (t) => { - const promise = authWithNetlify() - // wait for server to be started - await new Promise((resolve, reject) => { - const fibonacciBackoff = backoff.fibonacci() - const check = () => (host ? resolve() : fibonacciBackoff.backoff()) - - fibonacciBackoff.failAfter(10) - fibonacciBackoff.on('ready', check) - fibonacciBackoff.on('fail', reject) - check() - }) - - const params = new URLSearchParams([ - ['user', 'spongebob'], - ['token', 'gho_some-token'], - ['provider', 'github'], - ]) - // perform a request like the redirect from the Web ui - await fetch(new URL(`?${params.toString()}`, host)) - const result = await promise - - t.is(typeof result, 'object') - t.deepEqual(result, { - user: 'spongebob', - token: 'gho_some-token', - provider: 'github', - }) -}) diff --git a/tests/unit/utils/gh-auth.test.mjs b/tests/unit/utils/gh-auth.test.mjs new file mode 100644 index 00000000000..9007e9c5b2e --- /dev/null +++ b/tests/unit/utils/gh-auth.test.mjs @@ -0,0 +1,50 @@ +import { fibonacci } from 'backoff' +import fetch from 'node-fetch' +import { afterAll, describe, expect, test, vi } from 'vitest' + +import { authWithNetlify } from '../../../src/utils/gh-auth.mjs' +import openBrowser from '../../../src/utils/open-browser.mjs' + +vi.mock('../../../src/utils/open-browser.mjs', () => ({ + default: vi.fn(() => Promise.resolve()), +})) + +describe('gh-auth', () => { + afterAll(() => { + vi.restoreAllMocks() + }) + + test('should check if the authWithNetlify is working', async () => { + const promise = authWithNetlify() + // wait for server to be started + await new Promise((resolve, reject) => { + const fibonacciBackoff = fibonacci() + const check = () => (openBrowser.called ? resolve() : fibonacciBackoff.backoff()) + + fibonacciBackoff.failAfter(10) + fibonacciBackoff.on('ready', check) + fibonacciBackoff.on('fail', reject) + check() + }) + + const params = new URLSearchParams([ + ['user', 'spongebob'], + ['token', 'gho_some-token'], + ['provider', 'github'], + ]) + + const [[{ url }]] = openBrowser.calls + const calledParams = new URLSearchParams(url.slice(url.indexOf('?') + 1)) + const host = calledParams.get('host') + + // perform a request like the redirect from the Web ui + await fetch(new URL(`?${params.toString()}`, host)) + const result = await promise + + expect(result).toEqual({ + user: 'spongebob', + token: 'gho_some-token', + provider: 'github', + }) + }) +}) diff --git a/tests/unit/utils/headers.test.cjs b/tests/unit/utils/headers.test.cjs deleted file mode 100644 index 4009af8a682..00000000000 --- a/tests/unit/utils/headers.test.cjs +++ /dev/null @@ -1,174 +0,0 @@ -const path = require('path') - -const test = require('ava') - -const { headersForPath, parseHeaders } = require('../../../src/utils/headers.cjs') -const { createSiteBuilder } = require('../../integration/utils/site-builder.cjs') - -const headers = [ - { path: '/', headers: ['X-Frame-Options: SAMEORIGIN'] }, - { path: '/*', headers: ['X-Frame-Thing: SAMEORIGIN'] }, - { - path: '/static-path/*', - headers: [ - 'X-Frame-Options: DENY', - 'X-XSS-Protection: 1; mode=block', - 'cache-control: max-age=0', - 'cache-control: no-cache', - 'cache-control: no-store', - 'cache-control: must-revalidate', - ], - }, - { path: '/:placeholder/index.html', headers: ['X-Frame-Options: SAMEORIGIN'] }, - /** - * Do not force * to appear at end of path. - * - * @see https://github.com/netlify/next-on-netlify/issues/151 - * @see https://github.com/netlify/cli/issues/1148 - */ - { - path: '/*/_next/static/chunks/*', - headers: ['cache-control: public', 'cache-control: max-age=31536000', 'cache-control: immutable'], - }, - { - path: '/directory/*/test.html', - headers: ['X-Frame-Options: test'], - }, - { - path: '/with-colon', - headers: ['Custom-header: http://www.example.com'], - }, -] - -test.before(async (t) => { - const builder = createSiteBuilder({ siteName: 'site-for-detecting-server' }) - builder - .withHeadersFile({ - headers, - }) - .withContentFile({ - path: '_invalid_headers', - content: ` -/ - # This is valid - X-Frame-Options: SAMEORIGIN - # This is not valid - X-Frame-Thing: -`, - }) - - await builder.buildAsync() - - t.context.builder = builder -}) - -test.after(async (t) => { - await t.context.builder.cleanupAsync() -}) - -const parseHeadersFile = async function (t, fixtureName) { - const normalizedHeadersFile = path.resolve(t.context.builder.directory, fixtureName) - return await parseHeaders({ headersFiles: [normalizedHeadersFile] }) -} - -// Ignore added properties like `forRegExp` -const normalizeHeader = function ({ for: forPath, values }) { - return { for: forPath, values } -} - -/** - * Pass if we can load the test headers without throwing an error. - */ -test('_headers: syntax validates as expected', async (t) => { - await parseHeadersFile(t, '_headers') -}) - -test('_headers: does not throw on invalid syntax', async (t) => { - await t.notThrowsAsync(parseHeadersFile(t, '_invalid_headers')) -}) - -test('_headers: validate rules', async (t) => { - const rules = await parseHeadersFile(t, '_headers') - const normalizedHeaders = rules.map(normalizeHeader) - t.deepEqual(normalizedHeaders, [ - { - for: '/', - values: { - 'X-Frame-Options': 'SAMEORIGIN', - }, - }, - { - for: '/*', - values: { - 'X-Frame-Thing': 'SAMEORIGIN', - }, - }, - { - for: '/static-path/*', - values: { - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', - }, - }, - { - for: '/:placeholder/index.html', - values: { - 'X-Frame-Options': 'SAMEORIGIN', - }, - }, - { - for: '/*/_next/static/chunks/*', - values: { - 'cache-control': 'public, max-age=31536000, immutable', - }, - }, - { - for: '/directory/*/test.html', - values: { - 'X-Frame-Options': 'test', - }, - }, - { - for: '/with-colon', - values: { - 'Custom-header': 'http://www.example.com', - }, - }, - ]) -}) - -test('_headers: headersForPath testing', async (t) => { - const rules = await parseHeadersFile(t, '_headers') - t.deepEqual(headersForPath(rules, '/'), { - 'X-Frame-Options': 'SAMEORIGIN', - 'X-Frame-Thing': 'SAMEORIGIN', - }) - t.deepEqual(headersForPath(rules, '/placeholder'), { - 'X-Frame-Thing': 'SAMEORIGIN', - }) - t.deepEqual(headersForPath(rules, '/static-path/placeholder'), { - 'X-Frame-Thing': 'SAMEORIGIN', - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', - }) - t.deepEqual(headersForPath(rules, '/static-path'), { - 'X-Frame-Thing': 'SAMEORIGIN', - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', - }) - t.deepEqual(headersForPath(rules, '/placeholder/index.html'), { - 'X-Frame-Options': 'SAMEORIGIN', - 'X-Frame-Thing': 'SAMEORIGIN', - }) - t.deepEqual(headersForPath(rules, '/placeholder/_next/static/chunks/placeholder'), { - 'X-Frame-Thing': 'SAMEORIGIN', - 'cache-control': 'public, max-age=31536000, immutable', - }) - t.deepEqual(headersForPath(rules, '/directory/placeholder/test.html'), { - 'X-Frame-Thing': 'SAMEORIGIN', - 'X-Frame-Options': 'test', - }) -}) diff --git a/tests/unit/utils/headers.test.mjs b/tests/unit/utils/headers.test.mjs new file mode 100644 index 00000000000..4821524dc3e --- /dev/null +++ b/tests/unit/utils/headers.test.mjs @@ -0,0 +1,178 @@ +import { resolve } from 'path' + +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' + +import { headersForPath, parseHeaders } from '../../../src/utils/headers.mjs' +import { createSiteBuilder } from '../../integration/utils/site-builder.cjs' + +vi.mock('../../../src/utils/command-helpers.mjs', async () => ({ + ...(await vi.importActual('../../../src/utils/command-helpers.mjs')), + log: () => {}, +})) + +const headers = [ + { path: '/', headers: ['X-Frame-Options: SAMEORIGIN'] }, + { path: '/*', headers: ['X-Frame-Thing: SAMEORIGIN'] }, + { + path: '/static-path/*', + headers: [ + 'X-Frame-Options: DENY', + 'X-XSS-Protection: 1; mode=block', + 'cache-control: max-age=0', + 'cache-control: no-cache', + 'cache-control: no-store', + 'cache-control: must-revalidate', + ], + }, + { path: '/:placeholder/index.html', headers: ['X-Frame-Options: SAMEORIGIN'] }, + /** + * Do not force * to appear at end of path. + * + * @see https://github.com/netlify/next-on-netlify/issues/151 + * @see https://github.com/netlify/cli/issues/1148 + */ + { + path: '/*/_next/static/chunks/*', + headers: ['cache-control: public', 'cache-control: max-age=31536000', 'cache-control: immutable'], + }, + { + path: '/directory/*/test.html', + headers: ['X-Frame-Options: test'], + }, + { + path: '/with-colon', + headers: ['Custom-header: http://www.example.com'], + }, +] + +const parseHeadersFile = async function (context, fixtureName) { + const normalizedHeadersFile = resolve(context.builder.directory, fixtureName) + return await parseHeaders({ headersFiles: [normalizedHeadersFile] }) +} + +// Ignore added properties like `forRegExp` +const normalizeHeader = function ({ for: forPath, values }) { + return { for: forPath, values } +} + +describe('_headers', () => { + beforeEach(async (context) => { + const builder = createSiteBuilder({ siteName: 'site-for-detecting-server' }) + builder + .withHeadersFile({ + headers, + }) + .withContentFile({ + path: '_invalid_headers', + content: ` +/ + # This is valid + X-Frame-Options: SAMEORIGIN + # This is not valid + X-Frame-Thing: +`, + }) + + await builder.buildAsync() + + context.builder = builder + }) + + afterEach(async (context) => { + await context.builder.cleanupAsync() + }) + + test('syntax validates as expected', async (context) => { + await expect(parseHeadersFile(context, '_headers')).resolves.not.toThrowError() + }) + + test('does not throw on invalid syntax', async (context) => { + await expect(parseHeadersFile(context, '_invalid_headers')).resolves.not.toThrowError() + }) + + test('validate rules', async (context) => { + const rules = await parseHeadersFile(context, '_headers') + const normalizedHeaders = rules.map(normalizeHeader) + expect(normalizedHeaders).toEqual([ + { + for: '/', + values: { + 'X-Frame-Options': 'SAMEORIGIN', + }, + }, + { + for: '/*', + values: { + 'X-Frame-Thing': 'SAMEORIGIN', + }, + }, + { + for: '/static-path/*', + values: { + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', + }, + }, + { + for: '/:placeholder/index.html', + values: { + 'X-Frame-Options': 'SAMEORIGIN', + }, + }, + { + for: '/*/_next/static/chunks/*', + values: { + 'cache-control': 'public, max-age=31536000, immutable', + }, + }, + { + for: '/directory/*/test.html', + values: { + 'X-Frame-Options': 'test', + }, + }, + { + for: '/with-colon', + values: { + 'Custom-header': 'http://www.example.com', + }, + }, + ]) + }) + + test('headersForPath testing', async (context) => { + const rules = await parseHeadersFile(context, '_headers') + expect(headersForPath(rules, '/')).toEqual({ + 'X-Frame-Options': 'SAMEORIGIN', + 'X-Frame-Thing': 'SAMEORIGIN', + }) + expect(headersForPath(rules, '/placeholder')).toEqual({ + 'X-Frame-Thing': 'SAMEORIGIN', + }) + expect(headersForPath(rules, '/static-path/placeholder')).toEqual({ + 'X-Frame-Thing': 'SAMEORIGIN', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', + }) + expect(headersForPath(rules, '/static-path')).toEqual({ + 'X-Frame-Thing': 'SAMEORIGIN', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', + }) + expect(headersForPath(rules, '/placeholder/index.html')).toEqual({ + 'X-Frame-Options': 'SAMEORIGIN', + 'X-Frame-Thing': 'SAMEORIGIN', + }) + expect(headersForPath(rules, '/placeholder/_next/static/chunks/placeholder')).toEqual({ + 'X-Frame-Thing': 'SAMEORIGIN', + 'cache-control': 'public, max-age=31536000, immutable', + }) + expect(headersForPath(rules, '/directory/placeholder/test.html')).toEqual({ + 'X-Frame-Thing': 'SAMEORIGIN', + 'X-Frame-Options': 'test', + }) + }) +}) diff --git a/tests/unit/utils/init/config-github.test.cjs b/tests/unit/utils/init/config-github.test.cjs deleted file mode 100644 index d6d3b69641a..00000000000 --- a/tests/unit/utils/init/config-github.test.cjs +++ /dev/null @@ -1,82 +0,0 @@ -// @ts-check -const octokit = require('@octokit/rest') -const test = require('ava') -const sinon = require('sinon') - -// eslint-disable-next-line import/order -const githubAuth = require('../../../../src/utils/gh-auth.cjs') - -let getAuthenticatedResponse - -const octokitStub = sinon.stub(octokit, 'Octokit').callsFake(() => ({ - rest: { - users: { - getAuthenticated: () => { - if (getAuthenticatedResponse instanceof Error) { - throw getAuthenticatedResponse - } - return Promise.resolve(getAuthenticatedResponse) - }, - }, - }, -})) - -// stub the await ghauth() call for a new token -sinon.stub(githubAuth, 'getGitHubToken').callsFake(() => - Promise.resolve({ - provider: 'github', - token: 'new_token', - user: 'spongebob', - }), -) - -const { getGitHubToken } = require('../../../../src/utils/init/config-github.cjs') - -// mocked configstore -let globalConfig - -test.beforeEach(() => { - const values = new Map() - globalConfig = { - values: new Map(), - get: (key) => values.get(key), - set: (key, value) => { - values.set(key, value) - }, - } - globalConfig.set('userId', 'spongebob') - globalConfig.set(`users.spongebob.auth.github`, { - provider: 'github', - token: 'old_token', - user: 'spongebob', - }) -}) - -test.serial('should create a octokit client with the provided token if the token is valid', async (t) => { - getAuthenticatedResponse = { status: 200 } - const token = await getGitHubToken({ globalConfig }) - t.is(octokitStub.callCount, 1) - t.deepEqual(octokitStub.getCall(0).args[0], { auth: 'token old_token' }) - t.is(token, 'old_token') - t.deepEqual(globalConfig.get(`users.spongebob.auth.github`), { - provider: 'github', - token: 'old_token', - user: 'spongebob', - }) - octokitStub.resetHistory() -}) - -test.serial('should renew the github token when the provided token is not valid', async (t) => { - getAuthenticatedResponse = new Error('Bad Credentials') - getAuthenticatedResponse.status = 401 - const token = await getGitHubToken({ globalConfig }) - t.is(octokitStub.callCount, 1) - t.is(token, 'new_token') - t.deepEqual(octokitStub.getCall(0).args[0], { auth: 'token old_token' }) - t.deepEqual(globalConfig.get(`users.spongebob.auth.github`), { - provider: 'github', - token: 'new_token', - user: 'spongebob', - }) - octokitStub.resetHistory() -}) diff --git a/tests/unit/utils/init/config-github.test.mjs b/tests/unit/utils/init/config-github.test.mjs new file mode 100644 index 00000000000..8f1d9b965e2 --- /dev/null +++ b/tests/unit/utils/init/config-github.test.mjs @@ -0,0 +1,92 @@ +// @ts-check +import { Octokit } from '@octokit/rest' +import { beforeEach, describe, expect, test, vi } from 'vitest' + +import { getGitHubToken } from '../../../../src/utils/init/config-github.mjs' + +vi.mock('../../../../src/utils/command-helpers.mjs', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.mjs')), + log: () => {}, +})) + +// stub the await ghauth() call for a new token +vi.mock('../../../../src/utils/gh-auth.mjs', () => ({ + getGitHubToken: () => + Promise.resolve({ + provider: 'github', + token: 'new_token', + user: 'spongebob', + }), +})) + +vi.mock('@octokit/rest', () => { + const Client = vi.fn() + + Client.prototype.rest = { + users: { getAuthenticated: vi.fn() }, + } + + return { + Octokit: Client, + } +}) + +describe('getGitHubToken', () => { + // mocked configstore + let globalConfig + + beforeEach(() => { + const values = new Map() + globalConfig = { + values: new Map(), + get: (key) => values.get(key), + set: (key, value) => { + values.set(key, value) + }, + } + globalConfig.set('userId', 'spongebob') + globalConfig.set(`users.spongebob.auth.github`, { + provider: 'github', + token: 'old_token', + user: 'spongebob', + }) + + Octokit.mockClear() + }) + + test('should create a octokit client with the provided token if the token is valid', async () => { + Octokit.prototype.rest.users.getAuthenticated.mockImplementation(() => Promise.resolve({ status: 200 })) + + const token = await getGitHubToken({ globalConfig }) + + expect(Octokit).toHaveBeenCalledOnce() + expect(Octokit).toHaveBeenCalledWith({ auth: 'token old_token' }) + + expect(token).toBe('old_token') + expect(globalConfig.get(`users.spongebob.auth.github`)).toEqual({ + provider: 'github', + token: 'old_token', + user: 'spongebob', + }) + }) + + test('should renew the github token when the provided token is not valid', async () => { + Octokit.prototype.rest.users.getAuthenticated.mockImplementation(() => { + const authError = new Error('Bad Credentials') + authError.status = 401 + + throw authError + }) + const token = await getGitHubToken({ globalConfig }) + + expect(Octokit).toHaveBeenCalledOnce() + expect(Octokit).toHaveBeenCalledWith({ auth: 'token old_token' }) + + expect(token).toBe('new_token') + expect(globalConfig.get(`users.spongebob.auth.github`)).toEqual({ + provider: 'github', + token: 'new_token', + user: 'spongebob', + }) + }) +}) diff --git a/tests/unit/utils/parse-raw-flags.test.cjs b/tests/unit/utils/parse-raw-flags.test.cjs deleted file mode 100644 index 8b635c65a86..00000000000 --- a/tests/unit/utils/parse-raw-flags.test.cjs +++ /dev/null @@ -1,36 +0,0 @@ -const test = require('ava') - -const { aggressiveJSONParse, parseRawFlags } = require('../../../src/utils/parse-raw-flags.cjs') - -test.serial('JSONTruthy works with various inputs', (t) => { - const testPairs = [ - { - input: 'true', - wanted: true, - }, - { - input: 'false', - wanted: false, - }, - { - input: JSON.stringify({ foo: 'bar' }), - wanted: { foo: 'bar' }, - }, - { - input: 'Hello-world 1234', - wanted: 'Hello-world 1234', - }, - ] - - testPairs.forEach((pair) => { - t.deepEqual(aggressiveJSONParse(pair.input), pair.wanted) - }) -}) - -test.serial('parseRawFlags works', (t) => { - const input = ['FAUNA', 'FOO', 'BAR', '--hey', 'hi', '--heep'] - - const expected = { hey: 'hi', heep: true } - - t.deepEqual(parseRawFlags(input), expected, 'parse raw flag parses flags in an expected way') -}) diff --git a/tests/unit/utils/parse-raw-flags.test.mjs b/tests/unit/utils/parse-raw-flags.test.mjs new file mode 100644 index 00000000000..6b94cec7ac8 --- /dev/null +++ b/tests/unit/utils/parse-raw-flags.test.mjs @@ -0,0 +1,38 @@ +import { describe, expect, test } from 'vitest' + +import { aggressiveJSONParse, parseRawFlags } from '../../../src/utils/parse-raw-flags.mjs' + +describe('parse-raw-flags', () => { + test('JSONTruthy works with various inputs', () => { + const testPairs = [ + { + input: 'true', + wanted: true, + }, + { + input: 'false', + wanted: false, + }, + { + input: JSON.stringify({ foo: 'bar' }), + wanted: { foo: 'bar' }, + }, + { + input: 'Hello-world 1234', + wanted: 'Hello-world 1234', + }, + ] + + testPairs.forEach((pair) => { + expect(aggressiveJSONParse(pair.input)).toEqual(pair.wanted) + }) + }) + + test('parseRawFlags works', () => { + const input = ['FAUNA', 'FOO', 'BAR', '--hey', 'hi', '--heep'] + + const expected = { hey: 'hi', heep: true } + + expect(parseRawFlags(input)).toEqual(expected) + }) +}) diff --git a/tests/unit/utils/redirects.test.cjs b/tests/unit/utils/redirects.test.mjs similarity index 91% rename from tests/unit/utils/redirects.test.cjs rename to tests/unit/utils/redirects.test.mjs index b27a60df57b..d7f63e08560 100644 --- a/tests/unit/utils/redirects.test.cjs +++ b/tests/unit/utils/redirects.test.mjs @@ -1,7 +1,7 @@ -const test = require('ava') +import { expect, test } from 'vitest' -const { parseRedirects } = require('../../../src/utils/redirects.cjs') -const { withSiteBuilder } = require('../../integration/utils/site-builder.cjs') +import { parseRedirects } from '../../../src/utils/redirects.mjs' +import { withSiteBuilder } from '../../integration/utils/site-builder.cjs' const defaultConfig = { redirects: [ @@ -56,7 +56,7 @@ const BASE_REDIRECT = { params: {}, } -test('should parse redirect rules from netlify.toml', async (t) => { +test('should parse redirect rules from netlify.toml', async () => { await withSiteBuilder('site-with-redirects-in-netlify-toml', async (builder) => { await builder .withNetlifyToml({ @@ -120,11 +120,11 @@ test('should parse redirect rules from netlify.toml', async (t) => { }, ] - t.deepEqual(redirects, expected) + expect(redirects).toEqual(expected) }) }) -test('should parse redirect rules from _redirects file', async (t) => { +test('should parse redirect rules from _redirects file', async () => { await withSiteBuilder('site-with-redirects-file', async (builder) => { await builder .withRedirectsFile({ @@ -143,11 +143,11 @@ test('should parse redirect rules from _redirects file', async (t) => { }, ] - t.deepEqual(redirects, expected) + expect(redirects).toEqual(expected) }) }) -test('should parse redirect rules from _redirects file and netlify.toml', async (t) => { +test('should parse redirect rules from _redirects file and netlify.toml', async () => { await withSiteBuilder('site-with-redirects-file-and-netlify-toml', async (builder) => { await builder .withRedirectsFile({ @@ -224,6 +224,6 @@ test('should parse redirect rules from _redirects file and netlify.toml', async }, ] - t.deepEqual(redirects, expected) + expect(redirects).toEqual(expected) }) }) diff --git a/tests/unit/utils/rules-proxy.test.cjs b/tests/unit/utils/rules-proxy.test.cjs deleted file mode 100644 index d3affa80fff..00000000000 --- a/tests/unit/utils/rules-proxy.test.cjs +++ /dev/null @@ -1,9 +0,0 @@ -const test = require('ava') - -const { getLanguage } = require('../../../src/utils/rules-proxy.cjs') - -test('getLanguage', (t) => { - const language = getLanguage({ 'accept-language': 'ur' }) - - t.is(language, 'ur') -}) diff --git a/tests/unit/utils/rules-proxy.test.mjs b/tests/unit/utils/rules-proxy.test.mjs new file mode 100644 index 00000000000..3f3ef78a1e8 --- /dev/null +++ b/tests/unit/utils/rules-proxy.test.mjs @@ -0,0 +1,11 @@ +import { describe, expect, test } from 'vitest' + +import { getLanguage } from '../../../src/utils/rules-proxy.mjs' + +describe('getLanguage', () => { + test('detects language', () => { + const language = getLanguage({ 'accept-language': 'ur' }) + + expect(language).toBe('ur') + }) +}) diff --git a/tests/unit/utils/telemetry/validation.test.mjs b/tests/unit/utils/telemetry/validation.test.mjs index 34c6986bffb..14769550d77 100644 --- a/tests/unit/utils/telemetry/validation.test.mjs +++ b/tests/unit/utils/telemetry/validation.test.mjs @@ -1,9 +1,14 @@ -import { describe, expect, test } from 'vitest' +import { describe, expect, test, vi } from 'vitest' import isValidEventName from '../../../../src/utils/telemetry/validation.mjs' const getEventForProject = (projectName, eventName) => `${projectName}:${eventName}` +vi.mock('../../../../src/utils/command-helpers.mjs', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.mjs')), + log: () => {}, +})) + describe('isValidEventName', () => { test('validate failed with eventName without underscore', () => { const projectName = 'testProject'