diff --git a/package-lock.json b/package-lock.json index 15eb154c577..827e12da66b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -119,6 +119,7 @@ "@sindresorhus/slugify": "2.2.1", "@tsconfig/node18": "^18.2.4", "@tsconfig/recommended": "^1.0.8", + "@types/backoff": "^2.5.5", "@types/content-type": "1.1.8", "@types/debug": "4.1.12", "@types/envinfo": "7.8.4", @@ -162,6 +163,7 @@ "strip-ansi": "7.1.0", "temp-dir": "3.0.0", "tree-kill": "1.2.2", + "tsx": "^4.19.3", "typescript": "5.8.3", "typescript-eslint": "^8.26.0", "verdaccio": "6.1.2", @@ -5186,6 +5188,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/backoff": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@types/backoff/-/backoff-2.5.5.tgz", + "integrity": "sha512-4gv8BiXZMG4yxwn9Jii6ooCBVo+DXYq8kcKg6OvoHiWNh8rSjbYwQANTbUBx0c7ZKFv8aSYg9yDO6fOX2GkkAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -17999,6 +18011,26 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -22694,6 +22726,15 @@ "integrity": "sha512-TotjFaaXveVUdsrXCdalyF6E5RyG6+7hHHQVZonQtdlk1rJZ1myDIvPUUKPhoYv+JAzThb2lQJh9+9ZfF46hsA==", "dev": true }, + "@types/backoff": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@types/backoff/-/backoff-2.5.5.tgz", + "integrity": "sha512-4gv8BiXZMG4yxwn9Jii6ooCBVo+DXYq8kcKg6OvoHiWNh8rSjbYwQANTbUBx0c7ZKFv8aSYg9yDO6fOX2GkkAQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -31750,6 +31791,17 @@ "tslib": "^1.8.1" } }, + "tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dev": true, + "requires": { + "esbuild": "~0.25.0", + "fsevents": "~2.3.3", + "get-tsconfig": "^4.7.5" + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 9951b63a1c4..52420af6dd5 100644 --- a/package.json +++ b/package.json @@ -163,6 +163,7 @@ "@sindresorhus/slugify": "2.2.1", "@tsconfig/node18": "^18.2.4", "@tsconfig/recommended": "^1.0.8", + "@types/backoff": "^2.5.5", "@types/content-type": "1.1.8", "@types/debug": "4.1.12", "@types/envinfo": "7.8.4", @@ -206,6 +207,7 @@ "strip-ansi": "7.1.0", "temp-dir": "3.0.0", "tree-kill": "1.2.2", + "tsx": "^4.19.3", "typescript": "5.8.3", "typescript-eslint": "^8.26.0", "verdaccio": "6.1.2", diff --git a/src/commands/env/env-get.ts b/src/commands/env/env-get.ts index 901fc51cd8a..bbc89513224 100644 --- a/src/commands/env/env-get.ts +++ b/src/commands/env/env-get.ts @@ -17,6 +17,7 @@ export const envGet = async (name: string, options: OptionValues, command: BaseC const { siteInfo } = cachedConfig const env = await getEnvelopeEnv({ api, context, env: cachedConfig.env, key: name, scope, siteInfo }) + // @ts-expect-error FIXME(ndhoule) const { value } = env[name] || {} // Return json response for piping commands diff --git a/src/lib/completion/get-autocompletion.ts b/src/lib/completion/get-autocompletion.ts index a596502d04a..2de37951736 100644 --- a/src/lib/completion/get-autocompletion.ts +++ b/src/lib/completion/get-autocompletion.ts @@ -1,21 +1,17 @@ -/** - * @typedef CompletionItem - * @type import('tabtab').CompletionItem - */ -/** - * - * @param {import('tabtab').TabtabEnv} env - * @param {Record} program - * @returns {CompletionItem[]|void} - */ -// @ts-expect-error TS(7006) FIXME: Parameter 'env' implicitly has an 'any' type. -const getAutocompletion = function (env, program) { +import type { CompletionItem } from '@pnpm/tabtab' + +const getAutocompletion = function ( + env: { complete: boolean; lastPartial: string; line: string; words: number }, + program: Record< + string, + CompletionItem & { description?: string | undefined; name?: string | undefined; options: CompletionItem[] } + >, +): CompletionItem[] | undefined { if (!env.complete) { return } // means that we are currently in the first command (the root command) if (env.words === 1) { - // @ts-expect-error TS(2345) FIXME: Argument of type '({ description, name }: { descri... Remove this comment to see the full error message const rootCommands = Object.values(program).map(({ description, name }) => ({ name, description })) // suggest all commands @@ -26,20 +22,18 @@ const getAutocompletion = function (env, program) { // $ netlify add // we can now check if a command starts with the last partial - // @ts-expect-error TS(2769) FIXME: No overload matches this call. const autocomplete = rootCommands.filter(({ name }) => name.startsWith(env.lastPartial)) return autocomplete } const [, command, ...args] = env.line.split(' ') + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (program[command]) { const usedArgs = new Set(args) - // @ts-expect-error TS(7031) FIXME: Binding element 'name' implicitly has an 'any' typ... Remove this comment to see the full error message const unusedOptions = program[command].options.filter(({ name }) => !usedArgs.has(name)) if (env.lastPartial.length !== 0) { - // @ts-expect-error TS(7031) FIXME: Binding element 'name' implicitly has an 'any' typ... Remove this comment to see the full error message return unusedOptions.filter(({ name }) => name.startsWith(env.lastPartial)) } diff --git a/src/lib/edge-functions/proxy.ts b/src/lib/edge-functions/proxy.ts index 057ec28f6bc..92a7b9fc6bf 100644 --- a/src/lib/edge-functions/proxy.ts +++ b/src/lib/edge-functions/proxy.ts @@ -19,7 +19,7 @@ import { BlobsContextWithEdgeAccess } from '../blobs/blobs.js' import { getGeoLocation } from '../geo-location.js' import { getPathInProject } from '../settings.js' import { type Spinner, startSpinner, stopSpinner } from '../spinner.js' -import type { CLIState, ServerSettings, SiteInfo } from '../../utils/types.js' +import type { CLIState, ServerSettings } from '../../utils/types.js' import { getBootstrapURL } from './bootstrap.js' import { DIST_IMPORT_MAP_PATH, EDGE_FUNCTIONS_SERVE_FOLDER } from './consts.js' @@ -51,13 +51,16 @@ const getDownloadUpdateFunctions = () => { } } -export const handleProxyRequest = (req: ExtendedIncomingMessage, proxyReq: ClientRequest) => { +export const handleProxyRequest = (req: ExtendedIncomingMessage, proxyReq: ClientRequest): void => { Object.entries(req[headersSymbol]).forEach(([header, value]) => { proxyReq.setHeader(header, value) }) } -export const createSiteInfoHeader = (siteInfo: SiteInfo, localURL: string) => { +export const createSiteInfoHeader = ( + siteInfo: { id?: string | undefined; name?: string | undefined; url?: string | undefined }, + localURL?: string, +): string => { const { id, name, url } = siteInfo const site = { id, name, url: localURL ?? url } const siteString = JSON.stringify(site) diff --git a/src/lib/geo-location.ts b/src/lib/geo-location.ts index e2f2ba6bb6a..4ad9e1a806f 100644 --- a/src/lib/geo-location.ts +++ b/src/lib/geo-location.ts @@ -8,21 +8,27 @@ const CACHE_TTL = 8.64e7 // 10 seconds const REQUEST_TIMEOUT = 1e4 -/** - * @typedef GeoLocation - * @type {object} - * @property {string} city - * @property {object} country - * @property {string} country.code - * @property {string} country.name - * @property {object} subdivision - * @property {string} subdivision.code - * @property {string} subdivision.name - * @property {number} longitude - * @property {number} latitude - * @property {string} timezone - */ -export const mockLocation = { +export type Geolocation = { + city: string + country: { + code: string + name: string + } + subdivision: { + code: string + name: string + } + longitude: number + latitude: number + timezone: string +} + +interface State { + get(key: string): unknown + set(key: string, value: unknown): void +} + +export const mockLocation: Geolocation = { city: 'San Francisco', country: { code: 'US', name: 'United States' }, subdivision: { code: 'CA', name: 'California' }, @@ -32,19 +38,21 @@ export const mockLocation = { } /** - * Returns geolocation data from a remote API, the local cache, or a mock - * location, depending on the mode selected. - * - * @param {object} params - * @param {"cache"|"update"|"mock"} params.mode - * @param {string} params.geoCountry - * @param {boolean} params.offline - * @param {import('../utils/cli-state.js').default} params.state - * @returns {Promise} + * Returns geolocation data from a remote API, the local cache, or a mock location, depending on the + * specified mode. */ -// @ts-expect-error TS(7031) FIXME: Binding element 'geoCountry' implicitly has an 'an... Remove this comment to see the full error message -export const getGeoLocation = async ({ geoCountry, mode, offline, state }) => { - const cacheObject = state.get(STATE_GEO_PROPERTY) +export const getGeoLocation = async ({ + geoCountry, + mode, + offline = false, + state, +}: { + mode: 'cache' | 'update' | 'mock' + geoCountry?: string | undefined + offline?: boolean | undefined + state: State +}): Promise => { + const cacheObject = state.get(STATE_GEO_PROPERTY) as { data: Geolocation; timestamp: number } | undefined // If `--country` was used, we also set `--mode=mock`. if (geoCountry) { @@ -103,11 +111,9 @@ export const getGeoLocation = async ({ geoCountry, mode, offline, state }) => { } /** - * Returns geolocation data from a remote API - * - * @returns {Promise} + * Returns geolocation data from a remote API. */ -const getGeoLocationFromAPI = async () => { +const getGeoLocationFromAPI = async (): Promise => { const res = await fetch(API_URL, { method: 'GET', signal: AbortSignal.timeout(REQUEST_TIMEOUT), diff --git a/src/lib/http-agent.ts b/src/lib/http-agent.ts index e742dc2db9e..fd1b0f7312f 100644 --- a/src/lib/http-agent.ts +++ b/src/lib/http-agent.ts @@ -31,8 +31,23 @@ const DEFAULT_HTTPS_PORT = 443 // 50 seconds const AGENT_PORT_TIMEOUT = 50 -// @ts-expect-error TS(7031) FIXME: Binding element 'certificateFile' implicitly has a... Remove this comment to see the full error message -export const tryGetAgent = async ({ certificateFile, httpProxy }) => { +export const tryGetAgent = async ({ + certificateFile, + httpProxy, +}: { + httpProxy?: string | undefined + certificateFile?: string | undefined +}): Promise< + | { + error?: string | undefined + warning?: string | undefined + message?: string | undefined + } + | { + agent: HttpsProxyAgentWithCA + response: unknown + } +> => { if (!httpProxy) { return {} } diff --git a/src/utils/copy-template-dir/copy-template-dir.ts b/src/utils/copy-template-dir/copy-template-dir.ts index c29e60cc6e8..42e320b0212 100644 --- a/src/utils/copy-template-dir/copy-template-dir.ts +++ b/src/utils/copy-template-dir/copy-template-dir.ts @@ -18,7 +18,6 @@ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import assert from 'assert' import fs from 'fs' import path from 'path' import { pipeline } from 'stream' @@ -53,10 +52,6 @@ async function writeFile(outDir: string, vars: Record, file: Ent // High throughput template dir writes export async function copyTemplateDir(srcDir: string, outDir: string, vars: Record): Promise { - assert.strictEqual(typeof srcDir, 'string') - assert.strictEqual(typeof outDir, 'string') - assert.strictEqual(typeof vars, 'object') - await fs.promises.mkdir(outDir, { recursive: true }) const rs: ReaddirpStream = readdirp(srcDir) diff --git a/src/utils/deploy/hash-files.ts b/src/utils/deploy/hash-files.ts index e6785b0858c..38d89ca8225 100644 --- a/src/utils/deploy/hash-files.ts +++ b/src/utils/deploy/hash-files.ts @@ -3,21 +3,25 @@ import { pipeline } from 'stream/promises' import walker from 'folder-walker' import { fileFilterCtor, fileNormalizerCtor, hasherCtor, manifestCollectorCtor } from './hasher-segments.js' +import { $TSFixMe } from '../../commands/types.js' const hashFiles = async ({ assetType = 'file', - // @ts-expect-error TS(7031) FIXME: Binding element 'concurrentHash' implicitly has an... Remove this comment to see the full error message concurrentHash, - // @ts-expect-error TS(7031) FIXME: Binding element 'directories' implicitly has an 'a... Remove this comment to see the full error message directories, - // @ts-expect-error TS(7031) FIXME: Binding element 'filter' implicitly has an 'any' t... Remove this comment to see the full error message filter, hashAlgorithm = 'sha1', - // @ts-expect-error TS(7031) FIXME: Binding element 'normalizer' implicitly has an 'an... Remove this comment to see the full error message normalizer, - // @ts-expect-error TS(7031) FIXME: Binding element 'statusCb' implicitly has an 'any'... Remove this comment to see the full error message statusCb, -}) => { +}: { + assetType?: string | undefined + concurrentHash: $TSFixMe + directories: $TSFixMe + filter: $TSFixMe + hashAlgorithm?: string | undefined + normalizer?: $TSFixMe + statusCb: $TSFixMe +}): Promise<{ files: Record; filesShaMap: Record }> => { if (!filter) throw new Error('Missing filter function option') const fileStream = walker(directories, { filter }) diff --git a/src/utils/deploy/hash-fns.ts b/src/utils/deploy/hash-fns.ts index f1c5cc56459..d025710d9d2 100644 --- a/src/utils/deploy/hash-fns.ts +++ b/src/utils/deploy/hash-fns.ts @@ -26,10 +26,10 @@ const getFunctionZips = async ({ }: { command: BaseCommand directories: string[] - functionsConfig: $TSFixMe + functionsConfig?: $TSFixMe manifestPath: $TSFixMe rootDir: $TSFixMe - skipFunctionsCache: $TSFixMe + skipFunctionsCache?: boolean | undefined statusCb: $TSFixMe tmpDir: $TSFixMe }): Promise<(FunctionResult & { buildData?: unknown })[]> => { @@ -107,19 +107,7 @@ const trafficRulesConfig = (trafficRules?: TrafficRules) => { const hashFns = async ( command: BaseCommand, directories: string[], - config: { - concurrentHash?: number - functionsConfig: $TSFixMe - /** @default 'sha256' */ - hashAlgorithm?: string - manifestPath: $TSFixMe - rootDir: $TSFixMe - skipFunctionsCache: $TSFixMe - statusCb: $TSFixMe - tmpDir: $TSFixMe - }, -): Promise<$TSFixMe> => { - const { + { concurrentHash, functionsConfig, hashAlgorithm = 'sha256', @@ -128,8 +116,25 @@ const hashFns = async ( skipFunctionsCache, statusCb, tmpDir, - } = config || {} - // Early out if no functions directories are configured. + }: { + concurrentHash?: number + functionsConfig?: $TSFixMe + hashAlgorithm?: string | undefined + manifestPath?: string | undefined + rootDir?: string | undefined + skipFunctionsCache?: boolean | undefined + statusCb: $TSFixMe + tmpDir: $TSFixMe + }, +): Promise<{ + functionSchedules?: { name: string; cron: string }[] | undefined + functions: Record + functionsWithNativeModules: $TSFixMe[] + shaMap?: Record | undefined + fnShaMap?: Record | undefined + fnConfig?: Record | undefined +}> => { + // Exit early if no functions directories are configured. if (directories.length === 0) { return { functions: {}, functionsWithNativeModules: [], shaMap: {} } } @@ -202,7 +207,7 @@ const hashFns = async ( ) const functionSchedules = functionZips .map(({ name, schedule }) => schedule && { name, cron: schedule }) - .filter(Boolean) + .filter((schedule) => schedule !== '' && schedule !== undefined) const functionsWithNativeModules = functionZips.filter( ({ nativeNodeModules }) => nativeNodeModules !== undefined && Object.keys(nativeNodeModules).length !== 0, ) diff --git a/src/utils/deploy/upload-files.ts b/src/utils/deploy/upload-files.ts index 8a6f8fe9f9c..7244c96ceb0 100644 --- a/src/utils/deploy/upload-files.ts +++ b/src/utils/deploy/upload-files.ts @@ -1,6 +1,5 @@ import fs from 'fs' -// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'back... Remove this comment to see the full error message import backoff from 'backoff' import pMap from 'p-map' @@ -127,6 +126,7 @@ const retryUpload = (uploadFn, maxRetry) => // user the delay before next reconnection attempt. }) + // eslint-disable-next-line @typescript-eslint/no-misused-promises fibonacciBackoff.on('ready', tryUpload) fibonacciBackoff.on('fail', () => { diff --git a/src/utils/deploy/util.ts b/src/utils/deploy/util.ts index 36e893960e8..6446bcaecf5 100644 --- a/src/utils/deploy/util.ts +++ b/src/utils/deploy/util.ts @@ -5,8 +5,7 @@ import pWaitFor from 'p-wait-for' import { DEPLOY_POLL } from './constants.js' // normalize windows paths to unix paths -// @ts-expect-error TS(7006) FIXME: Parameter 'relname' implicitly has an 'any' type. -export const normalizePath = (relname) => { +export const normalizePath = (relname: string): string => { if (relname.includes('#') || relname.includes('?')) { throw new Error(`Invalid filename ${relname}. Deployed filenames cannot contain # or ? characters`) } diff --git a/src/utils/env/index.ts b/src/utils/env/index.ts index 137f6ef3614..7e089b894ed 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -1,15 +1,27 @@ +import type { NetlifyAPI } from 'netlify' + import { $TSFixMe } from '../../commands/types.js' import { logAndThrowError } from '../command-helpers.js' +import type { SiteInfo, EnvironmentVariableSource } from '../../utils/types.js' export const AVAILABLE_CONTEXTS = ['all', 'production', 'deploy-preview', 'branch-deploy', 'dev'] export const AVAILABLE_SCOPES = ['builds', 'functions', 'runtime', 'post_processing'] +type EnvironmentVariableContext = 'all' | 'production' | 'deploy-preview' | 'branch-deploy' | 'dev' + +type EnvironmentVariableScope = 'builds' | 'functions' | 'runtime' | 'post_processing' + +type EnvironmentVariableValue = { + context: EnvironmentVariableContext + context_parameter?: string | undefined + value: string +} + /** - * @param {string|undefined} context - The deploy context or branch of the environment variable value - * @returns {Array} The normalized context or branch name + * @param context The deploy context or branch of the environment variable value + * @returns The normalized context or branch name */ -// @ts-expect-error TS(7006) FIXME: Parameter 'context' implicitly has an 'any' type. -export const normalizeContext = (context) => { +export const normalizeContext = (context: string): string => { if (!context) { return context } @@ -19,26 +31,28 @@ export const normalizeContext = (context) => { } context = context.toLowerCase() if (context in CONTEXT_SYNONYMS) { - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message - context = CONTEXT_SYNONYMS[context] + context = CONTEXT_SYNONYMS[context as keyof typeof CONTEXT_SYNONYMS] } const forbiddenContexts = AVAILABLE_CONTEXTS.map((ctx) => `branch:${ctx}`) if (forbiddenContexts.includes(context)) { return logAndThrowError(`The context ${context} includes a reserved keyword and is not allowed`) } - context = context.replace(/^branch:/, '') - return context + return context.replace(/^branch:/, '') } /** * Finds a matching environment variable value from a given context - * @param {Array} values - An array of environment variable values from Envelope - * @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 */ -// @ts-expect-error TS(7006) FIXME: Parameter 'values' implicitly has an 'any' type. -export const findValueInValues = (values, context) => - // @ts-expect-error TS(7006) FIXME: Parameter 'val' implicitly has an 'any' type. +export const findValueInValues = ( + /** + * An array of environment variable values from Envelope + */ + values: EnvironmentVariableValue[], + /** + * The deploy context or branch of the environment variable value + */ + context: string, +) => values.find((val) => { if (!AVAILABLE_CONTEXTS.includes(context)) { // the "context" option passed in is actually the name of a branch @@ -49,13 +63,11 @@ export const findValueInValues = (values, context) => /** * Finds environment variables that match a given source - * @param {object} env - The dictionary of environment variables - * @param {enum} source - The source of the environment variable - * @returns {object} The dictionary of env vars that match the given source + * @param env - The dictionary of environment variables + * @param source - The source of the environment variable + * @returns The dictionary of env vars that match the given source */ -// @ts-expect-error TS(7006) FIXME: Parameter 'env' implicitly has an 'any' type. -export const filterEnvBySource = (env, source) => - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. +export const filterEnvBySource = (env: object, source: EnvironmentVariableSource): typeof env => Object.fromEntries(Object.entries(env).filter(([, variable]) => variable.sources[0] === source)) // Fetches data from Envelope @@ -66,10 +78,10 @@ const fetchEnvelopeItems = async function ({ siteId, }: { accountId: string - api: $TSFixMe + api: NetlifyAPI key: string - siteId: string -}): Promise<$TSFixMe[]> { + siteId?: string | undefined +}): Promise>[]> { if (accountId === undefined) { return [] } @@ -91,11 +103,11 @@ const fetchEnvelopeItems = async function ({ /** * Filters and sorts data from Envelope by a given context and/or scope - * @param {string} context - The deploy context or branch of the environment variable value - * @param {Array} envelopeItems - An array of environment variables from the Envelope service - * @param {enum} scope - The scope of the environment variables - * @param {enum} source - The source of the environment variable - * @returns {object} A dicionary in the following format: + * @param context - The deploy context or branch of the environment variable value + * @param envelopeItems - An array of environment variables from the Envelope service + * @param scope - The scope of the environment variables + * @param source - The source of the environment variable + * @returns A dicionary in the following format: * { * FOO: { * context: 'dev', @@ -122,7 +134,16 @@ export const formatEnvelopeData = ({ envelopeItems: $TSFixMe[] scope?: string source: string -}) => +}): Record< + string, + { + context: string + branch: string + scopes: string[] + sources: string[] + value: string + } +> => envelopeItems // filter by context .filter(({ values }) => Boolean(findValueInValues(values, context))) @@ -132,7 +153,11 @@ export const formatEnvelopeData = ({ .sort((left, right) => (left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1)) // format the data .reduce((acc, cur) => { - const { context: ctx, context_parameter: branch, value } = findValueInValues(cur.values, context) + const val = findValueInValues(cur.values, context) + if (val === undefined) { + throw new TypeError(`failed to locate environment variable value for ${context} context`) + } + const { context: ctx, context_parameter: branch, value } = val return { ...acc, [cur.key]: { @@ -147,21 +172,35 @@ export const formatEnvelopeData = ({ /** * Collects env vars from multiple sources and arranges them in the correct order of precedence - * @param {object} api - The api singleton object - * @param {string} context - The deploy context or branch of the environment variable - * @param {object} env - The dictionary of environment variables - * @param {string} key - If present, fetch a single key (case-sensitive) - * @param {boolean} raw - Return a dictionary of raw key/value pairs for only the account and site sources - * @param {enum} scope - The scope of the environment variables - * @param {object} siteInfo - The site object - * @returns {object} An object of environment variables keys and their metadata + * @param opts.api The api singleton object + * @param opts.context The deploy context or branch of the environment variable + * @param opts.env The dictionary of environment variables + * @param opts.key If present, fetch a single key (case-sensitive) + * @param opts.raw Return a dictionary of raw key/value pairs for only the account and site sources + * @param opts.scope The scope of the environment variables + * @param opts.siteInfo The site object + * @returns An object of environment variables keys and their metadata */ -// @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -export const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', raw = false, scope = 'any', siteInfo }) => { +export const getEnvelopeEnv = async ({ + api, + context = 'dev', + env, + key = '', + raw = false, + scope = 'any', + siteInfo, +}: { + api: NetlifyAPI + context?: string | undefined + env: object + key?: string | undefined + raw?: boolean | undefined + scope?: string | undefined + siteInfo: SiteInfo +}) => { const { account_slug: accountId, id: siteId } = siteInfo const [accountEnvelopeItems, siteEnvelopeItems] = await Promise.all([ - // @ts-expect-error TS(2345) FIXME: Argument of type '{ api: any; accountId: any; key:... Remove this comment to see the full error message fetchEnvelopeItems({ api, accountId, key }), fetchEnvelopeItems({ api, accountId, key, siteId }), ]) @@ -174,7 +213,6 @@ export const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', raw return entries.reduce( (obj, [envVarKey, metadata]) => ({ ...obj, - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. [envVarKey]: metadata.value, }), {}, @@ -202,11 +240,10 @@ export const getEnvelopeEnv = async ({ api, context = 'dev', env, key = '', raw /** * Returns a human-readable, comma-separated list of scopes - * @param {Array>} scopes - An array of scopes - * @returns {string} A human-readable, comma-separated list of scopes + * @param scopes An array of scopes + * @returns A human-readable, comma-separated list of scopes */ -// @ts-expect-error TS(7006) FIXME: Parameter 'scopes' implicitly has an 'any' type. -export const getHumanReadableScopes = (scopes) => { +export const getHumanReadableScopes = (scopes?: (EnvironmentVariableScope | 'post-processing')[]): string => { const HUMAN_SCOPES = ['Builds', 'Functions', 'Runtime', 'Post processing'] const SCOPES_MAP = { builds: HUMAN_SCOPES[0], @@ -224,16 +261,15 @@ export const getHumanReadableScopes = (scopes) => { // shorthand instead of listing every available scope return 'All' } - // @ts-expect-error TS(7006) FIXME: Parameter 'scope' implicitly has an 'any' type. return scopes.map((scope) => SCOPES_MAP[scope]).join(', ') } /** * Translates a Mongo env into an Envelope env - * @param {object} env - The site's env as it exists in Mongo - * @returns {Array} The array of Envelope env vars + * @param env The site's env as it exists in Mongo + * @returns The array of Envelope env vars */ -export const translateFromMongoToEnvelope = (env = {}) => { +export const translateFromMongoToEnvelope = (env: Record = {}) => { const envVars = Object.entries(env).map(([key, value]) => ({ key, scopes: AVAILABLE_SCOPES, @@ -250,21 +286,25 @@ export const translateFromMongoToEnvelope = (env = {}) => { /** * Translates an Envelope env into a Mongo env - * @param {Array} envVars - The array of Envelope env vars - * @param {string} context - The deploy context or branch of the environment variable - * @returns {object} The env object as compatible with Mongo + * @param envVars The array of Envelope env vars + * @param context The deploy context or branch of the environment variable + * @returns The env object as compatible with Mongo */ -export const translateFromEnvelopeToMongo = (envVars = [], context = 'dev') => +export const translateFromEnvelopeToMongo = ( + envVars: { + key: string + scopes: string[] + values: { context: string; value: string; context_parameter?: string | undefined }[] + }[] = [], + context = 'dev', +) => envVars - // @ts-expect-error TS(2339) FIXME: Property 'key' does not exist on type 'never'. - .sort((left, right) => (left.key.toLowerCase() < right.key.toLowerCase() ? -1 : 1)) + .sort((a, b) => (a.key.toLowerCase() < b.key.toLowerCase() ? -1 : 1)) .reduce((acc, cur) => { - // @ts-expect-error TS(2339) FIXME: Property 'values' does not exist on type 'never'. - const envVar = cur.values.find((val) => [context, 'all'].includes(val.context_parameter || val.context)) + const envVar = cur.values.find((val) => [context, 'all'].includes((val.context_parameter ?? '') || val.context)) if (envVar && envVar.value) { return { ...acc, - // @ts-expect-error TS(2339) FIXME: Property 'key' does not exist on type 'never'. [cur.key]: envVar.value, } } diff --git a/src/utils/feature-flags.ts b/src/utils/feature-flags.ts index 5d25ab35156..aee5ab4fb4a 100644 --- a/src/utils/feature-flags.ts +++ b/src/utils/feature-flags.ts @@ -1,5 +1,3 @@ -import type { SiteInfo } from './types.js' - /** * Allows us to check if a feature flag is enabled for a site. * Due to versioning of the cli, and the desire to remove flags from @@ -9,13 +7,17 @@ import type { SiteInfo } from './types.js' * Instead, we return that the feature flag is enabled if it isn't * specifically set to false in the response */ -export const isFeatureFlagEnabled = (flagName: string, siteInfo: SiteInfo): boolean => - Boolean(siteInfo.feature_flags && siteInfo.feature_flags[flagName] !== false) +export const isFeatureFlagEnabled = ( + flagName: string, + siteInfo: { feature_flags?: Record | undefined }, +): boolean => Boolean(siteInfo.feature_flags && siteInfo.feature_flags[flagName] !== false) /** * Retrieves all Feature flags from the siteInfo */ -export const getFeatureFlagsFromSiteInfo = (siteInfo: SiteInfo): FeatureFlags => ({ +export const getFeatureFlagsFromSiteInfo = (siteInfo: { + feature_flags?: Record | undefined +}): FeatureFlags => ({ ...siteInfo.feature_flags, // see https://github.com/netlify/pod-dev-foundations/issues/581#issuecomment-1731022753 zisi_golang_use_al2: isFeatureFlagEnabled('cli_golang_use_al2', siteInfo), diff --git a/src/utils/read-repo-url.ts b/src/utils/read-repo-url.ts index cef58de5c90..84fb40b7061 100644 --- a/src/utils/read-repo-url.ts +++ b/src/utils/read-repo-url.ts @@ -1,4 +1,4 @@ -import url from 'url' +import URL from 'url' import fetch from 'node-fetch' @@ -6,24 +6,27 @@ import fetch from 'node-fetch' const GITHUB = 'GitHub' /** - * @param {string} _url * Takes a url like https://github.com/netlify-labs/all-the-functions/tree/master/functions/9-using-middleware * and returns https://api.github.com/repos/netlify-labs/all-the-functions/contents/functions/9-using-middleware */ -// @ts-expect-error TS(7006) FIXME: Parameter '_url' implicitly has an 'any' type. -export const readRepoURL = async function (_url) { - // TODO: use `url.URL()` instead - // eslint-disable-next-line n/no-deprecated-api - const URL = url.parse(_url) - const repoHost = validateRepoURL(_url) - if (repoHost !== GITHUB) throw new Error('only GitHub repos are supported for now') - const [ownerAndRepo, contentsPath] = parseRepoURL(repoHost, URL) +export const readRepoURL = async function (url: string) { + // eslint-disable-next-line n/no-deprecated-api -- TODO: use `url.URL()` instead + const parsedURL = URL.parse(url) + const repoHost = validateRepoURL(url) + if (repoHost !== GITHUB) { + throw new Error('only GitHub repos are supported for now') + } + + const [ownerAndRepo, contentsPath] = parseRepoURL(repoHost, parsedURL) const folderContents = await getRepoURLContents(repoHost, ownerAndRepo, contentsPath) return folderContents } -// @ts-expect-error TS(7006) FIXME: Parameter 'repoHost' implicitly has an 'any' type. -const getRepoURLContents = async function (repoHost, ownerAndRepo, contentsPath) { +const getRepoURLContents = async function ( + repoHost: string, + ownerAndRepo: string, + contentsPath: string, +): Promise { // naive joining strategy for now if (repoHost === GITHUB) { // https://developer.github.com/v3/repos/contents/#get-contents @@ -38,26 +41,21 @@ const getRepoURLContents = async function (repoHost, ownerAndRepo, contentsPath) throw new Error(`unsupported host: ${repoHost}`) } -/** - * @param {string} _url - */ -// @ts-expect-error TS(7006) FIXME: Parameter '_url' implicitly has an 'any' type. -export const validateRepoURL = function (_url) { - // TODO: use `url.URL()` instead - // eslint-disable-next-line n/no-deprecated-api - const URL = url.parse(_url) - if (URL.host !== 'github.com') return null - // other validation logic here - return GITHUB +export const validateRepoURL = function (url: string): typeof GITHUB | null { + // eslint-disable-next-line n/no-deprecated-api -- TODO: use `url.URL()` instead + const parsedURL = URL.parse(url) + if (parsedURL.host === 'github.com') { + return GITHUB + } + return null } -// @ts-expect-error TS(7006) FIXME: Parameter 'repoHost' implicitly has an 'any' type. -export const parseRepoURL = function (repoHost, URL) { +export const parseRepoURL = function (repoHost: string, url: { path: string | null }) { // naive splitting strategy for now if (repoHost === GITHUB) { // https://developer.github.com/v3/repos/contents/#get-contents // what if it's not master? note that our contents retrieval may assume it is master - const [ownerAndRepo, contentsPath] = URL.path.slice(1).split('/tree/master/') + const [ownerAndRepo, contentsPath] = (url.path ?? '').slice(1).split('/tree/master/') return [ownerAndRepo, contentsPath] } throw new Error(`Unsupported host ${repoHost}`) diff --git a/src/utils/rules-proxy.ts b/src/utils/rules-proxy.ts index 67667d2c7fc..94368b5b9ba 100644 --- a/src/utils/rules-proxy.ts +++ b/src/utils/rules-proxy.ts @@ -27,10 +27,13 @@ export const getWatchers = function (): FSWatcher[] { return watchers } -// @ts-expect-error TS(7006) FIXME: Parameter 'headers' implicitly has an 'any' type. -export const getLanguage = function (headers) { +export const getLanguage = function (headers: Record) { if (headers['accept-language']) { - return headers['accept-language'].split(',')[0].slice(0, 2) + return ( + Array.isArray(headers['accept-language']) ? headers['accept-language'].join(', ') : headers['accept-language'] + ) + .split(',')[0] + .slice(0, 2) } return 'en' } diff --git a/src/utils/types.ts b/src/utils/types.ts index 92d10ac0bbb..4d27836517d 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -214,7 +214,7 @@ export interface Template { } type EnvironmentVariableScope = 'builds' | 'functions' | 'runtime' | 'post_processing' -type EnvironmentVariableSource = 'account' | 'addons' | 'configFile' | 'general' | 'internal' | 'ui' +export type EnvironmentVariableSource = 'account' | 'addons' | 'configFile' | 'general' | 'internal' | 'ui' export type EnvironmentVariables = Record< string, diff --git a/tests/integration/__snapshots__/framework-detection.test.js.snap b/tests/integration/__snapshots__/framework-detection.test.ts.snap similarity index 100% rename from tests/integration/__snapshots__/framework-detection.test.js.snap rename to tests/integration/__snapshots__/framework-detection.test.ts.snap diff --git a/tests/integration/commands/build/build-program.test.ts b/tests/integration/commands/build/build-program.test.ts index 2d177b27ee6..5f3c23c994a 100644 --- a/tests/integration/commands/build/build-program.test.ts +++ b/tests/integration/commands/build/build-program.test.ts @@ -61,10 +61,12 @@ describe('command/build', () => { test('should pass feature flags to @netlify/config', async (t) => { // this ensures that the process.exit does not exit the test process - // @ts-expect-error(ndhoule): Cannot mark the return value on this as as `never` - vi.spyOn(process, 'exit').mockImplementation((code) => { - expect(code).toBe(0) - }) + vi.spyOn(process, 'exit').mockImplementation( + // @ts-expect-error(ndhoule): This cannot return `never`, but it doesn't matter in the context of this test. + (code) => { + expect(code).toBe(0) + }, + ) await withSiteBuilder(t, async (builder) => { // eslint-disable-next-line no-restricted-properties process.cwd = () => builder.directory diff --git a/tests/integration/commands/build/build.test.ts b/tests/integration/commands/build/build.test.ts index 058d125f476..ecf47dbf2ab 100644 --- a/tests/integration/commands/build/build.test.ts +++ b/tests/integration/commands/build/build.test.ts @@ -331,7 +331,7 @@ describe.concurrent('command/build', () => { test('should run build without any netlify specific configuration and install auto detected plugins', async ({ fixture, }) => { - const output = await callCli(['build', '--offline'], { cwd: fixture.directory }) + const output = (await callCli(['build', '--offline'], { cwd: fixture.directory })) as string // expect on the output that it installed the next runtime (auto detected the plugin + the build command and therefore had functions to bundle) expect(output).toMatch(/❯ Using Next.js Runtime -/) diff --git a/tests/integration/commands/deploy/deploy.test.ts b/tests/integration/commands/deploy/deploy.test.ts index 7f8e1734c22..489072896fc 100644 --- a/tests/integration/commands/deploy/deploy.test.ts +++ b/tests/integration/commands/deploy/deploy.test.ts @@ -110,7 +110,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co const deploy = await callCli(['deploy', '--json', '--dir', 'public'], { cwd: builder.directory, env: { NETLIFY_SITE_ID: context.siteId }, - }).then((output) => JSON.parse(output)) + }).then((output) => JSON.parse(output as string)) await validateDeploy({ deploy, siteName: SITE_NAME, content }) }) @@ -134,7 +134,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co const deploy = await callCli(['deploy', '--json', '--site', SITE_NAME], { cwd: builder.directory, - }).then((output) => JSON.parse(output)) + }).then((output) => JSON.parse(output as string)) await validateDeploy({ deploy, siteName: SITE_NAME, content }) }) @@ -159,7 +159,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co const deploy = await callCli(['deploy', '--json'], { cwd: builder.directory, env: { NETLIFY_SITE_ID: context.siteId }, - }).then((output) => JSON.parse(output)) + }).then((output) => JSON.parse(output as string)) await validateDeploy({ deploy, siteName: SITE_NAME, content }) }) @@ -192,7 +192,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co } await callCli(['build'], options) - const deploy = await callCli(['deploy', '--json'], options).then((output) => JSON.parse(output)) + const deploy = await callCli(['deploy', '--json'], options).then((output) => JSON.parse(output as string)) // give edge functions manifest a couple ticks to propagate await pause(500) @@ -237,7 +237,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co await callCli(['build', '--cwd', pathPrefix], options) const deploy = await callCli(['deploy', '--json', '--cwd', pathPrefix], options).then((output) => - JSON.parse(output), + JSON.parse(output as string), ) // give edge functions manifest a couple ticks to propagate @@ -279,12 +279,12 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co await builder.build() - const output = await callCli(['deploy', '--build'], { + const output = (await callCli(['deploy', '--build'], { cwd: builder.directory, env: { NETLIFY_SITE_ID: context.siteId }, - }) + })) as string - t.expect(output.includes('Netlify Build completed in')).toBe(true) + t.expect(output).toContain('Netlify Build completed in') const [, deployId] = output.match(/DEPLOY_ID: (\w+)/) ?? [] const [, deployURL] = output.match(/DEPLOY_URL: (.+)/) ?? [] @@ -305,7 +305,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co const deploy = await callCli(['deploy', '--json', '--dir', 'public'], { cwd: builder.directory, env: { NETLIFY_SITE_ID: context.siteId }, - }).then((output) => JSON.parse(output)) + }).then((output) => JSON.parse(output as string)) await validateDeploy({ deploy, siteName: SITE_NAME, content }) expect(deploy).toHaveProperty('logs', `https://app.netlify.com/sites/${SITE_NAME}/deploys/${deploy.deploy_id}`) @@ -332,7 +332,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co const deploy = await callCli(['deploy', '--json', '--dir', 'public', '--prod'], { cwd: builder.directory, env: { NETLIFY_SITE_ID: context.siteId }, - }).then((output) => JSON.parse(output)) + }).then((output) => JSON.parse(output as string)) await validateDeploy({ deploy, siteName: SITE_NAME, content }) expect(deploy).toHaveProperty('logs', `https://app.netlify.com/sites/${SITE_NAME}/deploys/${deploy.deploy_id}`) @@ -365,7 +365,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co env: { NETLIFY_SITE_ID: context.siteId }, }) - JSON.parse(output) + JSON.parse(output as string) }) }) @@ -401,7 +401,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co const deploy = await callCli(['deploy', '--json'], { cwd: builder.directory, env: { NETLIFY_SITE_ID: context.siteId }, - }).then((output) => JSON.parse(output)) + }).then((output) => JSON.parse(output as string)) await validateDeploy({ deploy, siteName: SITE_NAME, content: 'index' }) await validateContent({ @@ -446,7 +446,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co const deploy = await callCli(['deploy', '--json'], { cwd: builder.directory, env: { NETLIFY_SITE_ID: context.siteId }, - }).then((output) => JSON.parse(output)) + }).then((output) => JSON.parse(output as string)) await validateDeploy({ deploy, siteName: SITE_NAME, content: 'index' }) await validateContent({ @@ -481,7 +481,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co const deploy = await callCli(['deploy', '--json'], { cwd: builder.directory, env: { NETLIFY_SITE_ID: context.siteId }, - }).then((output) => JSON.parse(output)) + }).then((output) => JSON.parse(output as string)) await validateDeploy({ deploy, siteName: SITE_NAME, content: 'index' }) await validateContent({ @@ -528,7 +528,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co const { mkdir, writeFile } = require('node:fs/promises') as typeof import('node:fs/promises') const generatedFunctionsDir = 'new_functions' - // @ts-expect-error + // @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'Functions... Remove this comment to see the full error message netlifyConfig.functions.directory = generatedFunctionsDir await mkdir(generatedFunctionsDir) diff --git a/tests/integration/commands/dev/dev-forms-and-redirects.test.js b/tests/integration/commands/dev/dev-forms-and-redirects.test.ts similarity index 81% rename from tests/integration/commands/dev/dev-forms-and-redirects.test.js rename to tests/integration/commands/dev/dev-forms-and-redirects.test.ts index 3ceb4a9cfbd..b16d8696a3c 100644 --- a/tests/integration/commands/dev/dev-forms-and-redirects.test.js +++ b/tests/integration/commands/dev/dev-forms-and-redirects.test.ts @@ -2,13 +2,15 @@ import fs from 'fs/promises' import path from 'path' +import type { HandlerEvent } from '@netlify/functions' +import js from 'dedent' import FormData from 'form-data' import getPort from 'get-port' import fetch from 'node-fetch' import { describe, test } from 'vitest' -import { withDevServer } from '../../utils/dev-server.ts' -import { withSiteBuilder } from '../../utils/site-builder.ts' +import { withDevServer } from '../../utils/dev-server.js' +import { withSiteBuilder } from '../../utils/site-builder.js' describe.concurrent('commands/dev-forms-and-redirects', () => { test('should return 404 when redirecting to a non existing function', async (t) => { @@ -43,22 +45,23 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { }) .withFunction({ path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify(event), + }), }) await builder.build() await withDevServer({ cwd: builder.directory }, async (server) => { - const [response1, response2] = await Promise.all([ - fetch(`${server.url}/.netlify/functions/echo?category[SOMETHING][]=something`).then((res) => res.json()), - fetch(`${server.url}/.netlify/functions/echo?category=one&category=two`).then((res) => res.json()), + const [res1, res2] = await Promise.all([ + fetch(`${server.url}/.netlify/functions/echo?category[SOMETHING][]=something`), + fetch(`${server.url}/.netlify/functions/echo?category=one&category=two`), ]) - t.expect(response1.queryStringParameters).toStrictEqual({ 'category[SOMETHING][]': 'something' }) - t.expect(response2.queryStringParameters).toStrictEqual({ category: 'one, two' }) + t.expect(await res1.json()).toHaveProperty('queryStringParameters', { 'category[SOMETHING][]': 'something' }) + t.expect(await res2.json()).toHaveProperty('queryStringParameters', { category: 'one, two' }) }) }) }) @@ -77,10 +80,11 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { }) .withFunction({ path: 'submission-created.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify(event), + }), }) await builder.build() @@ -93,10 +97,18 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { body: form, }).then((res) => res.json()) - const body = JSON.parse(response.body) + t.expect(response).toHaveProperty('body', t.expect.any(String)) + const body = JSON.parse((response as { body: string }).body) as unknown + t.expect(body).toHaveProperty( + 'payload', + t.expect.objectContaining({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + created_at: t.expect.any(String), + }), + ) const expectedBody = { payload: { - created_at: body.payload.created_at, + created_at: (body as { payload: { created_at: string } }).payload.created_at, data: { ip: '::ffff:127.0.0.1', some: 'thing', @@ -116,14 +128,14 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { }, } - t.expect(response.headers.host).toEqual(`${server.host}:${server.port}`) - t.expect(response.headers['content-length']).toEqual(JSON.stringify(expectedBody).length.toString()) - t.expect(response.headers['content-type']).toEqual('application/json') - t.expect(response.httpMethod).toEqual('POST') - t.expect(response.isBase64Encoded).toBe(false) - t.expect(response.path).toEqual('/') - t.expect(response.queryStringParameters).toStrictEqual({ ding: 'dong' }) t.expect(body).toStrictEqual(expectedBody) + t.expect(response).toHaveProperty('headers.host', `${server.host}:${server.port.toString()}`) + t.expect(response).toHaveProperty('headers.content-length', JSON.stringify(expectedBody).length.toString()) + t.expect(response).toHaveProperty('headers.content-type', 'application/json') + t.expect(response).toHaveProperty('httpMethod', 'POST') + t.expect(response).toHaveProperty('isBase64Encoded', false) + t.expect(response).toHaveProperty('path', '/') + t.expect(response).toHaveProperty('queryStringParameters', { ding: 'dong' }) }) }) }) @@ -142,10 +154,11 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { }) .withFunction({ path: 'submission-created-background.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify(event), + }), }) .build() @@ -176,10 +189,11 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { }) .withFunction({ path: 'submission-created.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify(event), + }), }) await builder.build() @@ -302,7 +316,7 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { await withDevServer({ cwd: builder.directory }, async (server) => { const response = await fetch(`${server.url}/foo.html`, { follow: 0 }) - t.expect(response.headers.location).toBe(undefined) + t.expect(response).not.toHaveProperty('headers.location') t.expect(await response.text()).toEqual('

foo') }) }) @@ -379,7 +393,7 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { // This test plugin starts an HTTP server that we'll hit when the dev server // is ready, asserting that plugins in dev mode can have long-running jobs. - const pluginSource = ` + const pluginSource = js` const http = require("http"); module.exports = { @@ -397,7 +411,7 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { netlifyConfig.redirects.push({ from: '/baz/*', - to: 'http://localhost:${userServerPort}/:splat', + to: 'http://localhost:${userServerPort.toString()}/:splat', status: 200, headers: { "X-NF-Hidden-Proxy": "true", @@ -406,15 +420,15 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { const server = http.createServer((_, res) => res.end("Hello world")); - server.listen(${userServerPort}, "localhost", () => { - console.log("Server is running on port ${userServerPort}"); + server.listen(${userServerPort.toString()}, "localhost", () => { + console.log("Server is running on port ${userServerPort.toString()}"); }); }, }; ` const { temporaryDirectory } = await import('tempy') - const pluginDirectory = await temporaryDirectory() + const pluginDirectory = temporaryDirectory() await fs.writeFile(path.join(pluginDirectory, 'manifest.yml'), pluginManifest) await fs.writeFile(path.join(pluginDirectory, 'index.js'), pluginSource) @@ -436,7 +450,7 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { await withDevServer({ cwd: builder.directory }, async (server) => { const [response1, response2, response3] = await Promise.all([ fetch(`${server.url}/foo`), - fetch(`http://localhost:${userServerPort}`), + fetch(`http://localhost:${userServerPort.toString()}`), fetch(`${server.url}/baz/path`), ]) t.expect(await response1.text()).toEqual('

foo') @@ -444,8 +458,10 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { t.expect(await response2.text()).toEqual('Hello world') t.expect(await response3.text()).toEqual('Hello world') - t.expect(server.output).not.toContain(`Proxying to http://localhost:${userServerPort}/path`) - t.expect(server.output).not.toContain(`[HPM] Proxy created: / -> http://localhost:${userServerPort}`) + t.expect(server.output).not.toContain(`Proxying to http://localhost:${userServerPort.toString()}/path`) + t.expect(server.output).not.toContain( + `[HPM] Proxy created: / -> http://localhost:${userServerPort.toString()}`, + ) }) }) }) @@ -471,7 +487,7 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { ` const { temporaryDirectory } = await import('tempy') - const pluginDirectory = await temporaryDirectory() + const pluginDirectory = temporaryDirectory() await fs.writeFile(path.join(pluginDirectory, 'manifest.yml'), pluginManifest) await fs.writeFile(path.join(pluginDirectory, 'index.js'), pluginSource) @@ -497,12 +513,12 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { async (server) => { const [response1, response2] = await Promise.all([ fetch(`${server.url}/foo`).then((res) => res.text()), - fetch(`http://localhost:${userServerPort}`).then((res) => res.text()), + fetch(`http://localhost:${userServerPort.toString()}`).then((res) => res.text()), ]) - await t.expect(response1).toEqual('

foo') - await t.expect(response2).toEqual('Hello world') + t.expect(response1).toEqual('

foo') + t.expect(response2).toEqual('Hello world') }, - { message: /Error: Something went wrong/ }, + true, ), ) .rejects.toThrowError() diff --git a/tests/integration/commands/dev/dev-miscellaneous.test.ts b/tests/integration/commands/dev/dev-miscellaneous.test.ts index 30775853949..1ace4d3ae7f 100644 --- a/tests/integration/commands/dev/dev-miscellaneous.test.ts +++ b/tests/integration/commands/dev/dev-miscellaneous.test.ts @@ -12,6 +12,7 @@ import fetch from 'node-fetch' import { type TestContext, describe, test } from 'vitest' import type { HandlerEvent, HandlerContext } from '@netlify/functions' import type { Context as EdgeHandlerContext } from '@netlify/edge-functions' +import js from 'dedent' import { cliPath } from '../../utils/cli-path.js' import { getExecaOptions, withDevServer } from '../../utils/dev-server.js' @@ -631,26 +632,24 @@ describe.concurrent('commands/dev-miscellaneous', () => { }, ]) .withEdgeFunction({ - config: { onError: 'bypass', path: '/hello-1' }, - handler: () => { - // @ts-expect-error: Intentionally referencing an undefined global - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - ermThisWillFail() - - return new Response('I will never get here') - }, name: 'hello-1', + config: { onError: 'bypass', path: '/hello-1' }, + handler: js` + export default async () => { + ermThisWillFail() + return new Response('I will never get here') + } + `, }) .withEdgeFunction({ - config: { onError: '/error-page', path: '/hello-2' }, - handler: () => { - // @ts-expect-error: Intentionally referencing an undefined global - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - ermThisWillFail() - - return new Response('I will never get here') - }, name: 'hello-2', + config: { onError: '/error-page', path: '/hello-2' }, + handler: js` + export default async () => { + ermThisWillFail() + return new Response('I will never get here') + } + `, }) await builder.build() @@ -1211,7 +1210,7 @@ describe.concurrent('commands/dev-miscellaneous', () => { }) .withEdgeFunction({ handler: () => { - // @ts-expect-error: We can't import Deno types without polluting the global environment + // @ts-expect-error TS(2304) FIXME: Cannot find name 'Deno'. // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const fromDenoGlobal = Deno.env.toObject() as Record const fromNetlifyGlobal = Netlify.env.toObject() @@ -1272,8 +1271,8 @@ describe.concurrent('commands/dev-miscellaneous', () => { test('should inject the `NETLIFY_DEV` environment variable in the process (legacy environment variables)', async (t) => { const externalServerPort = await getAvailablePort() - const externalServerPath = path.join(__dirname, '../../utils', 'external-server-cli.js') - const command = `node ${externalServerPath} ${externalServerPort.toString()}` + const externalServerPath = path.join(__dirname, '../../utils', 'external-server-cli.ts') + const command = `tsx ${externalServerPath} ${externalServerPort.toString()}` await withSiteBuilder(t, async (builder) => { const publicDir = 'public' @@ -1347,8 +1346,8 @@ describe.concurrent('commands/dev-miscellaneous', () => { ] const externalServerPort = await getAvailablePort() - const externalServerPath = path.join(__dirname, '../../utils', 'external-server-cli.js') - const command = `node ${externalServerPath} ${externalServerPort.toString()}` + const externalServerPath = path.join(__dirname, '../../utils', 'external-server-cli.ts') + const command = `tsx ${externalServerPath} ${externalServerPort.toString()}` await withSiteBuilder(t, async (builder) => { const publicDir = 'public' @@ -1395,8 +1394,8 @@ describe.concurrent('commands/dev-miscellaneous', () => { const { expect } = t const externalServerPort = await getAvailablePort() - const externalServerPath = path.join(__dirname, '../../utils', 'external-server-cli.js') - const command = `node ${externalServerPath} ${externalServerPort.toString()}` + const externalServerPath = path.join(__dirname, '../../utils', 'external-server-cli.ts') + const command = `tsx ${externalServerPath} ${externalServerPort.toString()}` await withSiteBuilder(t, async (builder) => { await builder diff --git a/tests/integration/commands/dev/dev.config.test.js b/tests/integration/commands/dev/dev.config.test.ts similarity index 71% rename from tests/integration/commands/dev/dev.config.test.js rename to tests/integration/commands/dev/dev.config.test.ts index 3fa3ff8dd54..f2e7fa530ab 100644 --- a/tests/integration/commands/dev/dev.config.test.js +++ b/tests/integration/commands/dev/dev.config.test.ts @@ -1,14 +1,16 @@ -import { Buffer } from 'buffer' -import { version } from 'process' +import { Buffer } from 'node:buffer' +import { version } from 'node:process' +import events from 'node:events' +import type { HandlerEvent } from '@netlify/functions' import FormData from 'form-data' import getPort from 'get-port' import fetch from 'node-fetch' import { gte } from 'semver' import { describe, test } from 'vitest' -import { withDevServer } from '../../utils/dev-server.ts' -import { withSiteBuilder } from '../../utils/site-builder.ts' +import { withDevServer } from '../../utils/dev-server.js' +import { withSiteBuilder } from '../../utils/site-builder.js' describe.concurrent('commands/dev/config', () => { test('should use [build.environment] and not [context.production.environment]', async (t) => { @@ -23,10 +25,11 @@ describe.concurrent('commands/dev/config', () => { }) .withFunction({ path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST_BUILD_ENVIRONMENT}`, - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: process.env.TEST_BUILD_ENVIRONMENT ?? '', + }), }) await builder.build() @@ -50,7 +53,7 @@ describe.concurrent('commands/dev/config', () => { }) .withFunction({ path: 'env.js', - handler: async () => ({ statusCode: 200, body: `${process.env.TEST_PRODUCTION_ENVIRONMENT}` }), + handler: async () => Promise.resolve({ statusCode: 200, body: process.env.TEST_PRODUCTION_ENVIRONMENT }), }) await builder.build() @@ -69,7 +72,7 @@ describe.concurrent('commands/dev/config', () => { .withEnvFile({ path: '.env.development', env: { TEST: 'FROM_DEV_FILE' } }) .withFunction({ path: 'env.js', - handler: async () => ({ statusCode: 200, body: `${process.env.TEST}` }), + handler: async () => Promise.resolve({ statusCode: 200, body: process.env.TEST }), }) await builder.build() @@ -89,7 +92,7 @@ describe.concurrent('commands/dev/config', () => { }) .withFunction({ path: 'env.js', - handler: async () => ({ statusCode: 200, body: `${process.env.TEST}` }), + handler: async () => Promise.resolve({ statusCode: 200, body: process.env.TEST }), }) await builder.build() @@ -105,9 +108,8 @@ describe.concurrent('commands/dev/config', () => { await withSiteBuilder(t, async (builder) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ path: 'env.js', - handler: () => ({ - statusCode: 200, - }), + // @ts-expect-error(ndhoule): Intentionally breaks type contract by returning synchronously + handler: () => ({ statusCode: 200 }), }) await builder.build() @@ -126,7 +128,7 @@ describe.concurrent('commands/dev/config', () => { await withSiteBuilder(t, async (builder) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ path: 'env.js', - handler: async () => ({ statusCode: 200, body: `${process.env.NETLIFY_DEV}` }), + handler: async () => Promise.resolve({ statusCode: 200, body: process.env.NETLIFY_DEV }), }) await builder.build() @@ -153,7 +155,7 @@ describe.concurrent('commands/dev/config', () => { NETLIFY_CLI_VERSION: process.env.NETLIFY_CLI_VERSION, })) res.end() - }).listen(${port}); + }).listen(${port.toString()}); `, path: 'devserver.mjs', }) @@ -170,17 +172,19 @@ describe.concurrent('commands/dev/config', () => { await withDevServer({ cwd: builder.directory }, async (server) => { const resp = await fetch(server.url) + // @ts-expect-error TS(2339) FIXME: Property 'NETLIFY_BLOBS_CONTEXT' does not exist on... Remove this comment to see the full error message const { NETLIFY_BLOBS_CONTEXT, NETLIFY_CLI_VERSION } = await resp.json() t.expect(NETLIFY_BLOBS_CONTEXT).toBeTypeOf('string') + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { deployID, edgeURL, siteID, token, uncachedEdgeURL } = JSON.parse( - Buffer.from(NETLIFY_BLOBS_CONTEXT, 'base64').toString(), + Buffer.from(NETLIFY_BLOBS_CONTEXT as string, 'base64').toString(), ) t.expect(deployID).toBe('0') - t.expect(edgeURL.startsWith('http://localhost:')).toBeTruthy() - t.expect(uncachedEdgeURL.startsWith('http://localhost:')).toBeTruthy() + t.expect(edgeURL).toMatch(/^http:\/\/localhost:/) + t.expect(uncachedEdgeURL).toMatch(/^http:\/\/localhost:/) t.expect(siteID).toBeTypeOf('string') t.expect(token).toBeTypeOf('string') @@ -193,7 +197,7 @@ describe.concurrent('commands/dev/config', () => { await withSiteBuilder(t, async (builder) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ path: 'env.js', - handler: async () => ({ statusCode: 200, body: `${process.env.CONTEXT}` }), + handler: async () => Promise.resolve({ statusCode: 200, body: process.env.CONTEXT }), }) await builder.build() @@ -209,7 +213,7 @@ describe.concurrent('commands/dev/config', () => { await withSiteBuilder(t, async (builder) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ path: 'env.js', - handler: async () => ({ statusCode: 200, body: `${process.env.CONTEXT}` }), + handler: async () => Promise.resolve({ statusCode: 200, body: process.env.CONTEXT }), }) await builder.build() @@ -232,10 +236,11 @@ describe.concurrent('commands/dev/config', () => { }) .withFunction({ path: 'ping.js', - handler: async () => ({ - statusCode: 200, - body: 'ping', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'ping', + }), }) await builder.build() @@ -258,22 +263,24 @@ describe.concurrent('commands/dev/config', () => { }) .withFunction({ path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify(event), + }), }) await builder.build() await withDevServer({ cwd: builder.directory }, async (server) => { - const response = await fetch(`${server.url}/api/echo?ding=dong`).then((res) => res.json()) - t.expect(response.body).toBe(undefined) - t.expect(response.headers.host).toEqual(`${server.host}:${server.port}`) - t.expect(response.httpMethod).toEqual('GET') - t.expect(response.isBase64Encoded).toBe(true) - t.expect(response.path).toEqual('/api/echo') - t.expect(response.queryStringParameters).toStrictEqual({ ding: 'dong' }) + const response = await fetch(`${server.url}/api/echo?ding=dong`) + const body = await response.json() + t.expect(body).not.toHaveProperty('body') + t.expect(body).toHaveProperty('headers.host', `${server.host}:${server.port.toString()}`) + t.expect(body).toHaveProperty('httpMethod', 'GET') + t.expect(body).toHaveProperty('isBase64Encoded', true) + t.expect(body).toHaveProperty('path', '/api/echo') + t.expect(body).toHaveProperty('queryStringParameters', { ding: 'dong' }) }) }) }) @@ -289,10 +296,11 @@ describe.concurrent('commands/dev/config', () => { }) .withFunction({ path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify(event), + }), }) await builder.build() @@ -304,16 +312,17 @@ describe.concurrent('commands/dev/config', () => { 'content-type': 'application/x-www-form-urlencoded', }, body: 'some=thing', - }).then((res) => res.json()) - - t.expect(response.body).toEqual('some=thing') - t.expect(response.headers.host).toEqual(`${server.host}:${server.port}`) - t.expect(response.headers['content-type']).toEqual('application/x-www-form-urlencoded') - t.expect(response.headers['content-length']).toEqual('10') - t.expect(response.httpMethod).toEqual('POST') - t.expect(response.isBase64Encoded).toBe(false) - t.expect(response.path).toEqual('/api/echo') - t.expect(response.queryStringParameters).toStrictEqual({ ding: 'dong' }) + }) + const body = await response.json() + + t.expect(body).toHaveProperty('body', 'some=thing') + t.expect(body).toHaveProperty('headers.host', `${server.host}:${server.port.toString()}`) + t.expect(body).toHaveProperty('headers.content-type', 'application/x-www-form-urlencoded') + t.expect(body).toHaveProperty('headers.content-length', '10') + t.expect(body).toHaveProperty('httpMethod', 'POST') + t.expect(body).toHaveProperty('isBase64Encoded', false) + t.expect(body).toHaveProperty('path', '/api/echo') + t.expect(body).toHaveProperty('queryStringParameters', { ding: 'dong' }) }) }) }) @@ -339,10 +348,11 @@ describe.concurrent('commands/dev/config', () => { }) .withFunction({ path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify(event), + }), }) await builder.build() @@ -354,16 +364,18 @@ describe.concurrent('commands/dev/config', () => { 'content-type': 'application/x-www-form-urlencoded', }, body: 'some=thing', - }).then((res) => res.json()) - - t.expect(response.body).toEqual('some=thing') - t.expect(response.headers.host).toEqual(`${server.host}:${server.port}`) - t.expect(response.headers['content-type']).toEqual('application/x-www-form-urlencoded') - t.expect(response.headers['transfer-encoding']).toEqual('chunked') - t.expect(response.httpMethod).toEqual('POST') - t.expect(response.isBase64Encoded).toBe(false) - t.expect(response.path).toEqual('/api/echo') - t.expect(response.queryStringParameters).toStrictEqual({ ding: 'dong' }) + }) + const body = await response.json() + + console.log(body) + t.expect(body).toHaveProperty('body', 'some=thing') + t.expect(body).toHaveProperty('headers.host', `${server.host}:${server.port.toString()}`) + t.expect(body).toHaveProperty('headers.content-type', 'application/x-www-form-urlencoded') + t.expect(body).toHaveProperty('headers.transfer-encoding', 'chunked') + t.expect(body).toHaveProperty('httpMethod', 'POST') + t.expect(body).toHaveProperty('isBase64Encoded', false) + t.expect(body).toHaveProperty('path', '/api/echo') + t.expect(body).toHaveProperty('queryStringParameters', { ding: 'dong' }) }) }) }) @@ -379,9 +391,10 @@ describe.concurrent('commands/dev/config', () => { }) .withFunction({ path: 'echo.js', - handler: async () => ({ - statusCode: 200, - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + }), }) await builder.build() @@ -412,10 +425,11 @@ describe.concurrent('commands/dev/config', () => { }) .withFunction({ path: 'echo.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify(event), + }), }) await builder.build() @@ -430,16 +444,17 @@ describe.concurrent('commands/dev/config', () => { const response = await fetch(`${server.url}/api/echo?ding=dong`, { method: 'POST', body: form, - }).then((res) => res.json()) - - t.expect(response.headers.host).toEqual(`${server.host}:${server.port}`) - t.expect(response.headers['content-type']).toEqual(`multipart/form-data;boundary=${expectedBoundary}`) - t.expect(response.headers['content-length']).toEqual('164') - t.expect(response.httpMethod).toEqual('POST') - t.expect(response.isBase64Encoded).toBe(true) - t.expect(response.path).toEqual('/api/echo') - t.expect(response.queryStringParameters).toStrictEqual({ ding: 'dong' }) - t.expect(response.body).toEqual(expectedResponseBody) + }) + const body = await response.json() + + t.expect(body).toHaveProperty('headers.host', `${server.host}:${server.port.toString()}`) + t.expect(body).toHaveProperty('headers.content-type', `multipart/form-data;boundary=${expectedBoundary}`) + t.expect(body).toHaveProperty('headers.content-length', '164') + t.expect(body).toHaveProperty('httpMethod', 'POST') + t.expect(body).toHaveProperty('isBase64Encoded', true) + t.expect(body).toHaveProperty('path', '/api/echo') + t.expect(body).toHaveProperty('queryStringParameters', { ding: 'dong' }) + t.expect(body).toHaveProperty('body', expectedResponseBody) }) }) }) @@ -496,26 +511,25 @@ describe.concurrent('commands/dev/config', () => { await builder.build() await withDevServer({ cwd: builder.directory }, async (server) => { - const chunks = [] + const chunks: string[] = [] - // eslint-disable-next-line no-async-promise-executor - await new Promise(async (resolve, reject) => { - const stream = await fetch(`${server.url}/.netlify/functions/streamer`).then((res) => res.body) + const res = await fetch(`${server.url}/.netlify/functions/streamer`) - let lastTimestamp = 0 + let lastTimestamp = 0 - stream.on('data', (chunk) => { - const now = Date.now() + t.expect(res.body).not.toBeNull() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const body = res.body! - t.expect(now > lastTimestamp).toBe(true) + body.on('data', (chunk: Buffer) => { + const now = Date.now() - lastTimestamp = now - chunks.push(chunk.toString()) - }) + t.expect(now > lastTimestamp).toBe(true) - stream.on('end', resolve) - stream.on('error', reject) + lastTimestamp = now + chunks.push(chunk.toString()) }) + await events.once(body, 'end') t.expect(chunks).toStrictEqual(['one', 'two', 'three']) }) diff --git a/tests/integration/commands/dev/dev.exec.test.ts b/tests/integration/commands/dev/dev.exec.test.ts index 0ff21b1c7fb..d7275e9e758 100644 --- a/tests/integration/commands/dev/dev.exec.test.ts +++ b/tests/integration/commands/dev/dev.exec.test.ts @@ -14,10 +14,10 @@ test('should pass .env variables to exec command', async (t) => { await withMockApi(routes, async ({ apiUrl }) => { const cmd = process.platform === 'win32' ? 'set' : 'printenv' - const output = await callCli(['dev:exec', cmd], getCLIOptions({ builder, apiUrl })) + const output = (await callCli(['dev:exec', cmd], getCLIOptions({ builder, apiUrl }))) as string - t.expect(output.includes('Injected .env file env var: MY_SUPER_SECRET')).toBe(true) - t.expect(output.includes('MY_SUPER_SECRET=SECRET')).toBe(true) + t.expect(output).toContain('Injected .env file env var: MY_SUPER_SECRET') + t.expect(output).toContain('MY_SUPER_SECRET=SECRET') }) }) }) diff --git a/tests/integration/commands/dev/dev.geo.test.js b/tests/integration/commands/dev/dev.geo.test.ts similarity index 92% rename from tests/integration/commands/dev/dev.geo.test.js rename to tests/integration/commands/dev/dev.geo.test.ts index 300ce0c295d..903a67ff77d 100644 --- a/tests/integration/commands/dev/dev.geo.test.js +++ b/tests/integration/commands/dev/dev.geo.test.ts @@ -3,7 +3,7 @@ import process from 'process' import { test } from 'vitest' import { callCli } from '../../utils/call-cli.js' -import { withSiteBuilder } from '../../utils/site-builder.ts' +import { withSiteBuilder } from '../../utils/site-builder.js' test('should throw if invalid country arg is passed', async (t) => { await withSiteBuilder(t, async (builder) => { diff --git a/tests/integration/commands/dev/images.test.js b/tests/integration/commands/dev/images.test.ts similarity index 94% rename from tests/integration/commands/dev/images.test.js rename to tests/integration/commands/dev/images.test.ts index 84827017871..57e5a82e2aa 100644 --- a/tests/integration/commands/dev/images.test.js +++ b/tests/integration/commands/dev/images.test.ts @@ -5,8 +5,8 @@ import path from 'path' import fetch from 'node-fetch' import { describe, test } from 'vitest' -import { withDevServer } from '../../utils/dev-server.ts' -import { withSiteBuilder } from '../../utils/site-builder.ts' +import { withDevServer } from '../../utils/dev-server.js' +import { withSiteBuilder } from '../../utils/site-builder.js' describe.concurrent('commands/dev/images', () => { test(`should support remote image transformations`, async (t) => { @@ -74,7 +74,6 @@ describe.concurrent('commands/dev/images', () => { path: '/.netlify/functions-internal/ssr.mjs', }) .withContentFile({ - // eslint-disable-next-line no-undef content: fs.readFileSync(path.join(__dirname, `/../../__fixtures__/images/test.jpg`)), path: '/images/test.jpg', }) diff --git a/tests/integration/commands/dev/responses.dev.test.js b/tests/integration/commands/dev/responses.dev.test.ts similarity index 80% rename from tests/integration/commands/dev/responses.dev.test.js rename to tests/integration/commands/dev/responses.dev.test.ts index 137e9480ff2..3ac82e477e7 100644 --- a/tests/integration/commands/dev/responses.dev.test.js +++ b/tests/integration/commands/dev/responses.dev.test.ts @@ -1,11 +1,12 @@ // Handlers are meant to be async outside tests import path from 'path' +import type { HandlerEvent } from '@netlify/functions' import fetch from 'node-fetch' import { describe, test } from 'vitest' -import { withDevServer } from '../../utils/dev-server.ts' -import { withSiteBuilder } from '../../utils/site-builder.ts' +import { withDevServer } from '../../utils/dev-server.js' +import { withSiteBuilder } from '../../utils/site-builder.js' describe.concurrent('commands/responses.dev', () => { test('should return index file when / is accessed', async (t) => { @@ -100,10 +101,11 @@ describe.concurrent('commands/responses.dev', () => { await withSiteBuilder(t, async (builder) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ path: 'builder.js', - handler: async () => ({ - statusCode: 200, - body: 'ping', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'ping', + }), }) await builder.build() @@ -128,11 +130,12 @@ describe.concurrent('commands/responses.dev', () => { await withSiteBuilder(t, async (builder) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ path: path.join('echo', 'echo.js'), - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify({ rawUrl: event.rawUrl }), - metadata: { builder_function: true }, - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify({ rawUrl: event.rawUrl }), + metadata: { builder_function: true }, + }), }) await builder.build() @@ -155,11 +158,12 @@ describe.concurrent('commands/responses.dev', () => { .withEnvFile({ path: '.env.development', env: { ENV_DEV_TEST: 'FROM_DEV_FILE' } }) .withFunction({ path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.ENV_DEV_TEST}`, - metadata: { builder_function: true }, - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: process.env.ENV_DEV_TEST ?? '', + metadata: { builder_function: true }, + }), }) await builder.build() @@ -180,11 +184,12 @@ describe.concurrent('commands/responses.dev', () => { await withSiteBuilder(t, async (builder) => { builder.withNetlifyToml({ config: { functions: { directory: 'functions' } } }).withFunction({ path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST}`, - metadata: { builder_function: true }, - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: process.env.TEST ?? '', + metadata: { builder_function: true }, + }), }) await builder.build() @@ -212,11 +217,12 @@ describe.concurrent('commands/responses.dev', () => { }) .withFunction({ path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.BUILD_ENV_TEST}`, - metadata: { builder_function: true }, - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: process.env.BUILD_ENV_TEST ?? '', + metadata: { builder_function: true }, + }), }) await builder.build() @@ -244,10 +250,11 @@ describe.concurrent('commands/responses.dev', () => { }) .withFunction({ path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.CONTEXT_TEST}`, - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: process.env.CONTEXT_TEST, + }), }) await builder.build() @@ -276,10 +283,11 @@ describe.concurrent('commands/responses.dev', () => { .withEnvFile({ path: '.env', env: { TEST_1: 'FROM_DEFAULT_FILE', TEST2: 'FROM_DEFAULT_FILE' } }) .withFunction({ path: 'env.js', - handler: async () => ({ - statusCode: 200, - body: `${process.env.TEST_1}__${process.env.TEST2}`, - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: `${process.env.TEST_1 ?? ''}__${process.env.TEST2 ?? ''}`, + }), }) await builder.build() @@ -307,13 +315,24 @@ describe.concurrent('commands/responses.dev', () => { name: 'injector', plugin: { onPreDev: async ({ netlifyConfig }) => { + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment netlifyConfig.dev = { + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped ...netlifyConfig.dev, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment processing: { + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access ...netlifyConfig.dev?.processing, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment html: { + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access ...netlifyConfig.dev?.processing?.html, injections: [ + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access ...(netlifyConfig.dev?.processing?.html?.injections ?? []), { location: 'before_closing_head_tag', @@ -323,6 +342,7 @@ describe.concurrent('commands/responses.dev', () => { }, }, } + return Promise.resolve(undefined) }, }, }) @@ -357,13 +377,24 @@ describe.concurrent('commands/responses.dev', () => { name: 'injector', plugin: { onPreDev: async ({ netlifyConfig }) => { + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment netlifyConfig.dev = { + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped ...netlifyConfig.dev, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment processing: { + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access ...netlifyConfig.dev?.processing, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment html: { + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access ...netlifyConfig.dev?.processing?.html, injections: [ + // @ts-expect-error(ndhoule): NetlifyConfig.dev is untyped + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access ...(netlifyConfig.dev?.processing?.html?.injections ?? []), { location: 'before_closing_body_tag', @@ -373,6 +404,7 @@ describe.concurrent('commands/responses.dev', () => { }, }, } + return Promise.resolve(undefined) }, }, }) diff --git a/tests/integration/commands/didyoumean/__snapshots__/didyoumean.test.js.snap b/tests/integration/commands/didyoumean/__snapshots__/didyoumean.test.ts.snap similarity index 100% rename from tests/integration/commands/didyoumean/__snapshots__/didyoumean.test.js.snap rename to tests/integration/commands/didyoumean/__snapshots__/didyoumean.test.ts.snap diff --git a/tests/integration/commands/didyoumean/didyoumean.test.js b/tests/integration/commands/didyoumean/didyoumean.test.js deleted file mode 100644 index 6385ab04f67..00000000000 --- a/tests/integration/commands/didyoumean/didyoumean.test.js +++ /dev/null @@ -1,13 +0,0 @@ -import { test } from 'vitest' - -import { callCli } from '../../utils/call-cli.js' -import { normalize } from '../../utils/snapshots.js' - -test('suggests closest matching command on typo', async (t) => { - // failures are expected since we effectively quit out of the prompts - const errors = await Promise.allSettled([callCli(['sta']), callCli(['opeen']), callCli(['hel']), callCli(['versio'])]) - errors.forEach((error) => { - t.expect(error.status).toEqual('rejected') - t.expect(normalize(error.reason.stdout, { duration: true, filePath: true })).toMatchSnapshot() - }) -}) diff --git a/tests/integration/commands/didyoumean/didyoumean.test.ts b/tests/integration/commands/didyoumean/didyoumean.test.ts new file mode 100644 index 00000000000..0363cb1d0d1 --- /dev/null +++ b/tests/integration/commands/didyoumean/didyoumean.test.ts @@ -0,0 +1,22 @@ +import { test } from 'vitest' + +import { callCli } from '../../utils/call-cli.js' +import { normalize } from '../../utils/snapshots.js' + +test('suggests closest matching command on typo', async (t) => { + // failures are expected since we effectively quit out of the prompts + const errors = await Promise.allSettled([ + callCli(['sta']) as Promise, + callCli(['opeen']) as Promise, + callCli(['hel']) as Promise, + callCli(['versio']) as Promise, + ]) + + for (const error of errors) { + t.expect(error.status).toEqual('rejected') + t.expect(error).toHaveProperty('reason.stdout', t.expect.any(String)) + t.expect( + normalize((error as { reason: { stdout: string } }).reason.stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } +}) diff --git a/tests/integration/commands/env/env.test.ts b/tests/integration/commands/env/env.test.ts index 4737aeda6b1..f0989bc6ac7 100644 --- a/tests/integration/commands/env/env.test.ts +++ b/tests/integration/commands/env/env.test.ts @@ -223,7 +223,10 @@ describe('commands/env', () => { const listRoutes = [...routes, { path: 'accounts/test-account/env', response: [existingVar, otherVar] }] await withMockApi(listRoutes, async ({ apiUrl }) => { - const cliResponse = await callCli(['env:list'], getCLIOptions({ builder, apiUrl, env: { CI: 'true' } })) + const cliResponse = (await callCli( + ['env:list'], + getCLIOptions({ builder, apiUrl, env: { CI: 'true' } }), + )) as string t.expect(normalize(cliResponse)).toMatchSnapshot() }) @@ -396,10 +399,10 @@ describe('commands/env', () => { { path: 'sites/site_id_a', response: { ...siteInfo, build_settings: { env: {} } } }, ] await withMockApi(createRoutes, async ({ apiUrl }) => { - const cliResponse = await callCli( + const cliResponse = (await callCli( ['env:clone', '--to', 'site_id_a', '--force'], getCLIOptions({ builder, apiUrl }), - ) + )) as string t.expect(normalize(cliResponse)).toMatchSnapshot() }) @@ -443,13 +446,13 @@ describe('commands/env', () => { await withSiteBuilder(t, async (builder) => { await builder.build() - const cliResponse = await callCli(['env:clone', '--to', 'site_id_a', '--force'], { + const cliResponse = (await callCli(['env:clone', '--to', 'site_id_a', '--force'], { cwd: builder.directory, extendEnv: false, env: { PATH: process.env.PATH, }, - }) + })) as string t.expect(normalize(cliResponse)).toMatchSnapshot() }) @@ -477,10 +480,10 @@ describe('commands/env', () => { await withSiteBuilder(t, async (builder) => { await builder.build() await withMockApi(cloneRoutes, async ({ apiUrl, requests }) => { - const cliResponse = await callCli( + const cliResponse = (await callCli( ['env:clone', '--to', 'site_id_a', '--force'], getCLIOptions({ apiUrl, builder }), - ) + )) as string t.expect(normalize(cliResponse)).toMatchSnapshot() diff --git a/tests/integration/commands/envelope/envelope.test.ts b/tests/integration/commands/envelope/envelope.test.ts index af1486c0752..5d85bcf3787 100644 --- a/tests/integration/commands/envelope/envelope.test.ts +++ b/tests/integration/commands/envelope/envelope.test.ts @@ -137,7 +137,11 @@ describe.concurrent('command/envelope', () => { .build() await withMockApi(routes, async ({ apiUrl }) => { - const cliResponse = await callCli(['env:import', '--json', '.env'], getCLIOptions({ builder, apiUrl }), true) + const cliResponse = (await callCli( + ['env:import', '--json', '.env'], + getCLIOptions({ builder, apiUrl }), + true, + )) as Record t.expect(cliResponse).toStrictEqual(finalEnv) }) @@ -162,11 +166,11 @@ describe.concurrent('command/envelope', () => { .build() await withMockApi(routes, async ({ apiUrl }) => { - const cliResponse = await callCli( + const cliResponse = (await callCli( ['env:import', '--replace-existing', '--json', '.env'], getCLIOptions({ builder, apiUrl }), true, - ) + )) as Record t.expect(cliResponse).toStrictEqual(finalEnv) }) @@ -219,10 +223,10 @@ describe.concurrent('command/envelope', () => { await withSiteBuilder(t, async (builder) => { await builder.build() await withMockApi(cloneRoutes, async ({ apiUrl, requests }) => { - const cliResponse = await callCli( + const cliResponse = (await callCli( ['env:clone', '--from', 'site_id_a', '--to', 'site_id_b', '--force'], getCLIOptions({ apiUrl, builder }), - ) + )) as string t.expect(normalize(cliResponse)).toMatchSnapshot() diff --git a/tests/integration/commands/functions-serve/functions-serve.test.ts b/tests/integration/commands/functions-serve/functions-serve.test.ts index 7b31aa5fbab..e8397fb31cc 100644 --- a/tests/integration/commands/functions-serve/functions-serve.test.ts +++ b/tests/integration/commands/functions-serve/functions-serve.test.ts @@ -48,7 +48,9 @@ const withFunctionsServer = async ( } return await testHandler() } finally { - await killProcess(ps) + if (ps !== undefined) { + await killProcess(ps) + } } } diff --git a/tests/integration/commands/functions-with-args/functions-with-args.test.js b/tests/integration/commands/functions-with-args/functions-with-args.test.ts similarity index 62% rename from tests/integration/commands/functions-with-args/functions-with-args.test.js rename to tests/integration/commands/functions-with-args/functions-with-args.test.ts index 23bed2f7fc6..8279b23aa01 100644 --- a/tests/integration/commands/functions-with-args/functions-with-args.test.js +++ b/tests/integration/commands/functions-with-args/functions-with-args.test.ts @@ -1,12 +1,15 @@ -import path from 'path' -import { fileURLToPath } from 'url' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import type { HandlerEvent } from '@netlify/functions' import fetch from 'node-fetch' import { describe, test } from 'vitest' +import js from 'dedent' +import ts from 'dedent' -import { tryAndLogOutput, withDevServer } from '../../utils/dev-server.ts' +import { tryAndLogOutput, withDevServer } from '../../utils/dev-server.js' import { pause } from '../../utils/pause.js' -import { withSiteBuilder } from '../../utils/site-builder.ts' +import { withSiteBuilder } from '../../utils/site-builder.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -29,37 +32,38 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args }) .withFunction({ path: 'hello.js', - handler: async () => ({ - statusCode: 200, - body: 'Hello', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'Hello', + }), }) .build() + // eslint-disable-next-line @typescript-eslint/unbound-method await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port, waitForLogMatching }) => { - await tryAndLogOutput( - async () => - t - .expect(await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text())) - .toEqual('Hello'), - outputBuffer, - ) + await tryAndLogOutput(async () => { + t.expect( + await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => res.text()), + ).toEqual('Hello') + }, outputBuffer) await pause(WAIT_WRITE) await builder .withFunction({ path: 'hello.js', - handler: async () => ({ - statusCode: 200, - body: 'Goodbye', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'Goodbye', + }), }) .build() await waitForLogMatching('Reloaded function hello') - const response = await fetch(`http://localhost:${port}/.netlify/functions/hello`) + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`) t.expect(await response.text()).toEqual('Goodbye') }) @@ -80,69 +84,68 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args }) .withContentFile({ path: 'functions/hello.ts', - content: ` - interface Book { - title: string - author: string - } - - const handler = async () => { - const book1: Book = { - title: 'Modern Web Development on the JAMStack', - author: 'Mathias Biilmann & Phil Hawksworth' - } - - return { - statusCode: 200, - body: book1.title - } - } - - export { handler } - `, + content: ts` + interface Book { + title: string + author: string + } + + const handler = async () => { + const book1: Book = { + title: 'Modern Web Development on the JAMStack', + author: 'Mathias Biilmann & Phil Hawksworth' + } + + return { + statusCode: 200, + body: book1.title + } + } + + export { handler } + `, }) .build() + // eslint-disable-next-line @typescript-eslint/unbound-method await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port, waitForLogMatching }) => { - await tryAndLogOutput( - async () => - t - .expect(await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text())) - .toEqual('Modern Web Development on the JAMStack'), - outputBuffer, - ) + await tryAndLogOutput(async () => { + t.expect( + await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => res.text()), + ).toEqual('Modern Web Development on the JAMStack') + }, outputBuffer) await pause(WAIT_WRITE) await builder .withContentFile({ path: 'functions/hello.ts', - content: ` - interface Book { - title: string - author: string - } - - const handler = async () => { - const book1: Book = { - title: 'Modern Web Development on the Jamstack', - author: 'Mathias Biilmann & Phil Hawksworth' - } - - return { - statusCode: 200, - body: book1.title - } - } - - export { handler } + content: ts` + interface Book { + title: string + author: string + } + + const handler = async () => { + const book1: Book = { + title: 'Modern Web Development on the Jamstack', + author: 'Mathias Biilmann & Phil Hawksworth' + } + + return { + statusCode: 200, + body: book1.title + } + } + + export { handler } `, }) .build() await waitForLogMatching('Reloaded function hello') - const response = await fetch(`http://localhost:${port}/.netlify/functions/hello`) + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`) t.expect(await response.text()).toEqual('Modern Web Development on the Jamstack') }) @@ -161,25 +164,26 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args }, }) .withContentFiles([ - { path: 'functions/lib/util.js', content: `exports.bark = () => 'WOOF!'` }, + { path: 'functions/lib/util.js', content: js`exports.bark = () => 'WOOF!'` }, { path: 'functions/hello.js', - content: `const { bark } = require('./lib/util'); exports.handler = async () => ({ statusCode: 200, body: bark() })`, + content: js`const { bark } = require('./lib/util'); exports.handler = async () => ({ statusCode: 200, body: bark() })`, }, ]) .build() + // eslint-disable-next-line @typescript-eslint/unbound-method await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port, waitForLogMatching }) => { await tryAndLogOutput(async () => { - t.expect(await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text())).toEqual( - 'WOOF!', - ) + t.expect( + await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => res.text()), + ).toEqual('WOOF!') }, outputBuffer) await pause(WAIT_WRITE) await builder - .withContentFile({ path: 'functions/lib/util.js', content: `exports.bark = () => 'WOOF WOOF!'` }) + .withContentFile({ path: 'functions/lib/util.js', content: js`exports.bark = () => 'WOOF WOOF!'` }) .build() if (args.includes('esbuild')) { @@ -189,7 +193,9 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args await pause(WAIT_WRITE) } - const response = await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text()) + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => + res.text(), + ) t.expect(response).toEqual('WOOF WOOF!') }) @@ -210,45 +216,46 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args .withContentFiles([ { path: 'functions/lib/util.ts', - content: ` - const title: string = 'Modern Web Development on the JAMStack' + content: ts` + const title: string = 'Modern Web Development on the JAMStack' - export { title } - `, + export { title } + `, }, { path: 'functions/hello.ts', - content: ` - import { title } from './lib/util' - - interface Book { - title: string - author: string - } - - const handler = async () => { - const book1: Book = { - title, - author: 'Mathias Biilmann & Phil Hawksworth' - } - - return { - statusCode: 200, - body: book1.title - } - } - - export { handler } + content: ts` + import { title } from './lib/util' + + interface Book { + title: string + author: string + } + + const handler = async () => { + const book1: Book = { + title, + author: 'Mathias Biilmann & Phil Hawksworth' + } + + return { + statusCode: 200, + body: book1.title + } + } + + export { handler } `, }, ]) .build() + // eslint-disable-next-line @typescript-eslint/unbound-method await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port, waitForLogMatching }) => { await tryAndLogOutput(async () => { - t.expect(await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text())).toEqual( - 'Modern Web Development on the JAMStack', - ) + t.expect( + await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => res.text()), + ).toEqual('Modern Web Development on the JAMStack') }, outputBuffer) await pause(WAIT_WRITE) @@ -256,17 +263,19 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args await builder .withContentFile({ path: 'functions/lib/util.ts', - content: ` - const title: string = 'Modern Web Development on the Jamstack' + content: ts` + const title: string = 'Modern Web Development on the Jamstack' - export { title } - `, + export { title } + `, }) .build() await waitForLogMatching('Reloaded function hello') - const response = await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text()) + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => + res.text(), + ) t.expect(response).toEqual('Modern Web Development on the Jamstack') }) @@ -287,9 +296,10 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args }) .build() + // eslint-disable-next-line @typescript-eslint/unbound-method await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port, waitForLogMatching }) => { await tryAndLogOutput(async () => { - const unauthenticatedResponse = await fetch(`http://localhost:${port}/.netlify/functions/hello`) + const unauthenticatedResponse = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`) t.expect(unauthenticatedResponse.status).toBe(404) }, outputBuffer) @@ -299,16 +309,19 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args await builder .withFunction({ path: 'hello.js', - handler: async () => ({ - statusCode: 200, - body: 'Hello', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'Hello', + }), }) .build() await waitForLogMatching('Loaded function hello') - const response = await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text()) + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => + res.text(), + ) t.expect(response).toEqual('Hello') }) @@ -322,10 +335,11 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args await builder .withFunction({ path: 'functions/help.ts', - handler: async () => ({ - statusCode: 200, - body: 'I need somebody. Not just anybody.', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'I need somebody. Not just anybody.', + }), esm: true, }) .withNetlifyToml({ @@ -337,9 +351,10 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args }) .build() + // eslint-disable-next-line @typescript-eslint/unbound-method await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port, waitForLogMatching }) => { await tryAndLogOutput(async () => { - const unauthenticatedResponse = await fetch(`http://localhost:${port}/.netlify/functions/hello`) + const unauthenticatedResponse = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`) t.expect(unauthenticatedResponse.status).toBe(404) }, outputBuffer) @@ -349,32 +364,34 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args await builder .withContentFile({ path: 'functions/hello.ts', - content: ` - interface Book { - title: string - author: string - } - - const handler = async () => { - const book1: Book = { - title: 'Modern Web Development on the Jamstack', - author: 'Mathias Biilmann & Phil Hawksworth' - } - - return { - statusCode: 200, - body: book1.title - } - } - - export { handler } - `, + content: ts` + interface Book { + title: string + author: string + } + + const handler = async () => { + const book1: Book = { + title: 'Modern Web Development on the Jamstack', + author: 'Mathias Biilmann & Phil Hawksworth' + } + + return { + statusCode: 200, + body: book1.title + } + } + + export { handler } + `, }) .build() await waitForLogMatching('Loaded function hello') - const response = await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text()) + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => + res.text(), + ) t.expect(response).toEqual('Modern Web Development on the Jamstack') }) @@ -395,18 +412,20 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args }) .withFunction({ path: 'hello.js', - handler: async () => ({ - statusCode: 200, - body: 'Hello', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'Hello', + }), }) .build() + // eslint-disable-next-line @typescript-eslint/unbound-method await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port, waitForLogMatching }) => { await tryAndLogOutput(async () => { - t.expect(await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text())).toEqual( - 'Hello', - ) + t.expect( + await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => res.text()), + ).toEqual('Hello') }, outputBuffer) await pause(WAIT_WRITE) @@ -419,14 +438,14 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args await waitForLogMatching('Removed function hello') - const { status } = await fetch(`http://localhost:${port}/.netlify/functions/hello`) + const { status } = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`) t.expect(status).toBe(404) }) }) }) - test(`should pick up new function files even through debounce`, async (t) => { + test('should pick up new function files even through debounce', async (t) => { await withSiteBuilder(t, async (builder) => { await builder .withNetlifyToml({ @@ -435,21 +454,22 @@ describe.concurrent.each(testMatrix)('withSiteBuilder with args: $args', ({ args }, }) .withContentFile({ - path: '/functions/hello/dist/index.js', - content: `module.exports = "foo"`, + path: '/functions/hello/src/index.js', + content: js`module.exports = "foo"`, }) .withContentFile({ - path: '/functions/hello/dist/index.d.ts', - content: `export default "foo"`, + path: '/functions/hello/src/index.d.ts', + content: js`export default "foo"`, }) .withContentFile({ path: '/functions/hello/index.js', - content: ` -const response = require("./dist") -exports.handler = async () => ({ - statusCode: 200, - body: response -})`, + content: js` + const response = require("./src") + exports.handler = async () => ({ + statusCode: 200, + body: response, + }) + `, }) .build() @@ -459,15 +479,15 @@ exports.handler = async () => ({ await builder .withContentFile({ - path: '/functions/hello/dist/index.d.ts', - content: `export default "bar"`, + path: '/functions/hello/src/index.d.ts', + content: ts`export default "bar"`, }) .build() await builder .withContentFile({ - path: '/functions/hello/dist/index.js', - content: `module.exports = "bar"`, + path: '/functions/hello/src/index.js', + content: js`module.exports = "bar"`, }) .build() @@ -494,24 +514,26 @@ exports.handler = async () => ({ }) .build() + // eslint-disable-next-line @typescript-eslint/unbound-method await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port, waitForLogMatching }) => { await builder .withFunction({ path: 'hello.js', pathPrefix: '.netlify/functions-internal', - handler: async () => ({ - statusCode: 200, - body: 'Internal', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'Internal', + }), }) .build() await pause(WAIT_WRITE) await tryAndLogOutput(async () => { - t.expect(await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text())).toEqual( - 'Internal', - ) + t.expect( + await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => res.text()), + ).toEqual('Internal') }, outputBuffer) await pause(WAIT_WRITE) @@ -520,16 +542,19 @@ exports.handler = async () => ({ .withFunction({ path: 'hello.js', pathPrefix: '.netlify/functions-internal', - handler: async () => ({ - statusCode: 200, - body: 'Internal updated', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'Internal updated', + }), }) .build() await waitForLogMatching('Reloaded function hello') - const response = await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text()) + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => + res.text(), + ) t.expect(response).toEqual('Internal updated') }) @@ -550,29 +575,32 @@ exports.handler = async () => ({ }) .withFunction({ path: 'hello.js', - handler: async () => ({ - statusCode: 200, - body: 'User', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'User', + }), }) .build() + // eslint-disable-next-line @typescript-eslint/unbound-method await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port, waitForLogMatching }) => { await builder .withFunction({ path: 'hello.js', pathPrefix: '.netlify/functions-internal', - handler: async () => ({ - statusCode: 200, - body: 'Internal', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'Internal', + }), }) .build() await tryAndLogOutput(async () => { - t.expect(await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text())).toEqual( - 'User', - ) + t.expect( + await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => res.text()), + ).toEqual('User') }, outputBuffer) await pause(WAIT_WRITE) @@ -580,24 +608,28 @@ exports.handler = async () => ({ await builder .withFunction({ path: 'hello.js', - handler: async () => ({ - statusCode: 200, - body: 'User updated', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'User updated', + }), }) .withFunction({ path: 'hello.js', pathPrefix: '.netlify/functions-internal', - handler: async () => ({ - statusCode: 200, - body: 'Internal updated', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'Internal updated', + }), }) .build() await waitForLogMatching('Reloaded function hello') - const response = await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text()) + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => + res.text(), + ) t.expect(response).toEqual('User updated') }) @@ -618,24 +650,24 @@ exports.handler = async () => ({ }) .withContentFile({ path: 'functions/hello.mjs', - content: ` - const handler = async () => { - return { - statusCode: 200, - body: 'Hello, world!' - } - } - - export { handler } - `, + content: js` + const handler = async () => { + return { + statusCode: 200, + body: 'Hello, world!' + } + } + + export { handler } + `, }) .build() await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port }) => { await tryAndLogOutput(async () => { - t.expect(await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text())).toEqual( - 'Hello, world!', - ) + t.expect( + await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => res.text()), + ).toEqual('Hello, world!') }, outputBuffer) }) }) @@ -660,19 +692,20 @@ exports.handler = async () => ({ }) .withFunction({ path: 'hello.js', - handler: async () => ({ - statusCode: 200, - body: 'hello from es module!', - }), + handler: async () => + Promise.resolve({ + statusCode: 200, + body: 'hello from es module!', + }), esm: true, }) .build() await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port }) => { await tryAndLogOutput(async () => { - t.expect(await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => res.text())).toEqual( - 'hello from es module!', - ) + t.expect( + await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then((res) => res.text()), + ).toEqual('hello from es module!') }, outputBuffer) }) }) @@ -692,17 +725,18 @@ exports.handler = async () => ({ }) .withFunction({ path: 'echoEncoding.js', - handler: async (event) => ({ - statusCode: 200, - body: event.isBase64Encoded ? 'base64' : 'plain', - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: event.isBase64Encoded ? 'base64' : 'plain', + }), }) .build() await withDevServer({ cwd: builder.directory, args }, async ({ outputBuffer, port }) => { await tryAndLogOutput(async () => { t.expect( - await fetch(`http://localhost:${port}/.netlify/functions/echoEncoding`, { + await fetch(`http://localhost:${port.toString()}/.netlify/functions/echoEncoding`, { headers: { 'Content-Type': 'multipart/form-data', }, @@ -721,26 +755,30 @@ describe.concurrent('serving functions', () => { .withContentFiles([ { path: 'files/one.json', - content: `{"data": "one"}`, + content: JSON.stringify({ data: 'one' }), }, { path: 'files/two.json', - content: `{"data": "two"}`, + content: JSON.stringify({ data: 'two' }), }, ]) .withFunction({ path: 'hello.js', - handler: async (event) => { - const { name } = event.queryStringParameters + handler: js` + exports.handler = async (event) => { + const fs = require('node:fs') + const path = require('node:path') - // eslint-disable-next-line no-undef, @typescript-eslint/no-require-imports - const { data } = require(`../files/${name}.json`) - return { - statusCode: 200, - body: data, + const { name } = event.queryStringParameters ?? {} + const { data } = require(\`../files/\${name}.json\`) + + return Promise.resolve({ + statusCode: 200, + body: data, + }) } - }, + `, }) .withNetlifyToml({ config: { @@ -753,8 +791,8 @@ describe.concurrent('serving functions', () => { await withDevServer({ cwd: builder.directory }, async ({ outputBuffer, port }) => { await tryAndLogOutput(async () => { const [responseHelloNameOne, responseHelloNameTwo] = await Promise.all([ - fetch(`http://localhost:${port}/.netlify/functions/hello?name=one`).then((res) => res.text()), - fetch(`http://localhost:${port}/.netlify/functions/hello?name=two`).then((res) => res.text()), + fetch(`http://localhost:${port.toString()}/.netlify/functions/hello?name=one`).then((res) => res.text()), + fetch(`http://localhost:${port.toString()}/.netlify/functions/hello?name=two`).then((res) => res.text()), ]) t.expect(responseHelloNameOne).toEqual('one') t.expect(responseHelloNameTwo).toEqual('two') @@ -768,9 +806,7 @@ describe.concurrent('serving functions', () => { await builder .withFunction({ path: 'hello.js', - handler: async () => { - throw new Error('Something went wrong') - }, + handler: async () => Promise.reject(new Error('Something went wrong')), }) .withNetlifyToml({ config: { @@ -781,8 +817,8 @@ describe.concurrent('serving functions', () => { .build() await withDevServer({ cwd: builder.directory }, async ({ port }) => { - const responseWithTrace = await fetch(`http://localhost:${port}/.netlify/functions/hello`).then((res) => - res.text(), + const responseWithTrace = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`).then( + (res) => res.text(), ) t.expect(responseWithTrace.includes(path.join(builder.directory, 'functions', 'hello.js'))).toBe(true) t.expect(responseWithTrace.includes(path.join('.netlify', 'functions-serve'))).toBe(false) @@ -795,10 +831,11 @@ describe.concurrent('serving functions', () => { await builder .withFunction({ path: 'hello.js', - handler: async (event) => ({ - statusCode: 200, - body: JSON.stringify(event), - }), + handler: async (event: HandlerEvent) => + Promise.resolve({ + statusCode: 200, + body: JSON.stringify(event), + }), }) .withNetlifyToml({ config: { @@ -811,18 +848,22 @@ describe.concurrent('serving functions', () => { await withDevServer({ cwd: builder.directory }, async ({ outputBuffer, port }) => { await tryAndLogOutput(async () => { const { + // @ts-expect-error TS(2339) FIXME: Property 'httpMethod' does not exist on type '{}'. httpMethod, + // @ts-expect-error TS(2339) FIXME: Property 'path' does not exist on type '{}'. path: thePath, + // @ts-expect-error TS(2339) FIXME: Property 'rawQuery' does not exist on type '{}'. rawQuery, + // @ts-expect-error TS(2339) FIXME: Property 'rawUrl' does not exist on type '{}'. rawUrl, - } = await fetch(`http://localhost:${port}/.netlify/functions/hello?net=lify&jam=stack`).then((res) => - res.json(), + } = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello?net=lify&jam=stack`).then( + (res) => res.json(), ) t.expect(httpMethod).toEqual('GET') t.expect(thePath).toEqual('/.netlify/functions/hello') t.expect(rawQuery).toEqual('net=lify&jam=stack') - t.expect(rawUrl).toEqual(`http://localhost:${port}/.netlify/functions/hello?net=lify&jam=stack`) + t.expect(rawUrl).toEqual(`http://localhost:${port.toString()}/.netlify/functions/hello?net=lify&jam=stack`) }, outputBuffer) }) }) @@ -833,10 +874,14 @@ describe.concurrent('serving functions', () => { await builder .withFunction({ path: 'hello.js', - handler: async () => ({ - statusCode: 200, - body: 42, - }), + handler: js` + exports.handler = async () => { + return Promise.resolve({ + statusCode: 200, + body: 42, + }); + } + `, }) .withNetlifyToml({ config: { @@ -848,7 +893,7 @@ describe.concurrent('serving functions', () => { await withDevServer({ cwd: builder.directory }, async ({ outputBuffer, port }) => { await tryAndLogOutput(async () => { - const errorResponse = await fetch(`http://localhost:${port}/.netlify/functions/hello`) + const errorResponse = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`) t.expect(errorResponse.status).toBe(500) t.expect(await errorResponse.text()).toEqual( @@ -864,10 +909,12 @@ describe.concurrent('serving functions', () => { await builder .withFunction({ path: 'hello.js', - handler: async () => ({ - statusCode: null, - body: 'hello', - }), + // @ts-expect-error(ndhoule): statusCode breaks type contract + handler: async () => + Promise.resolve({ + statusCode: null, + body: 'hello', + }), }) .withNetlifyToml({ config: { @@ -879,7 +926,7 @@ describe.concurrent('serving functions', () => { await withDevServer({ cwd: builder.directory }, async ({ outputBuffer, port }) => { await tryAndLogOutput(async () => { - const errorResponse = await fetch(`http://localhost:${port}/.netlify/functions/hello`) + const errorResponse = await fetch(`http://localhost:${port.toString()}/.netlify/functions/hello`) t.expect(errorResponse.status).toBe(500) t.expect(await errorResponse.text()).toEqual( @@ -896,26 +943,29 @@ describe.concurrent('serving functions', () => { .withContentFiles([ { path: 'files/one.json', - content: `{"data": "one"}`, + content: JSON.stringify({ data: 'one' }), }, { path: 'files/two.json', - content: `{"data": "two"}`, + content: JSON.stringify({ data: 'two' }), }, ]) .withFunction({ path: 'hello.js', - handler: async (event) => { - // eslint-disable-next-line no-undef, @typescript-eslint/no-require-imports - const { readFileSync } = require('fs') - const { name } = event.queryStringParameters - const { data } = JSON.parse(readFileSync(`${__dirname}/../files/${name}.json`, 'utf-8')) - - return { - statusCode: 200, - body: data, + handler: js` + exports.handler = async (event) => { + const fs = require('node:fs') + const path = require('node:path') + + const { name } = event.queryStringParameters ?? {} + const { data } = JSON.parse(fs.readFileSync(path.join(__dirname, \`../files/\${name}.json\`), 'utf-8')) + + return Promise.resolve({ + statusCode: 200, + body: data, + }) } - }, + `, }) .withNetlifyToml({ config: { @@ -932,8 +982,8 @@ describe.concurrent('serving functions', () => { await withDevServer({ cwd: builder.directory }, async ({ outputBuffer, port }) => { await tryAndLogOutput(async () => { const [responseHelloNameOne, responseHelloNameTwo] = await Promise.all([ - fetch(`http://localhost:${port}/.netlify/functions/hello?name=one`).then((res) => res.text()), - fetch(`http://localhost:${port}/.netlify/functions/hello?name=two`).then((res) => res.text()), + fetch(`http://localhost:${port.toString()}/.netlify/functions/hello?name=one`).then((res) => res.text()), + fetch(`http://localhost:${port.toString()}/.netlify/functions/hello?name=two`).then((res) => res.text()), ]) t.expect(responseHelloNameOne).toEqual('one') t.expect(responseHelloNameTwo).toEqual('two') @@ -943,11 +993,11 @@ describe.concurrent('serving functions', () => { .withContentFiles([ { path: 'files/one.json', - content: `{"data": "three"}`, + content: JSON.stringify({ data: 'three' }), }, { path: 'files/two.json', - content: `{"data": "four"}`, + content: JSON.stringify({ data: 'four' }), }, ]) .build() @@ -959,8 +1009,8 @@ describe.concurrent('serving functions', () => { t.expect(outputBuffer.some((buffer) => /.*Reloaded function hello.*/.test(buffer.toString()))).toBe(true) await tryAndLogOutput(async () => { const [responseHelloNameOne, responseHelloNameTwo] = await Promise.all([ - fetch(`http://localhost:${port}/.netlify/functions/hello?name=one`).then((res) => res.text()), - fetch(`http://localhost:${port}/.netlify/functions/hello?name=two`).then((res) => res.text()), + fetch(`http://localhost:${port.toString()}/.netlify/functions/hello?name=one`).then((res) => res.text()), + fetch(`http://localhost:${port.toString()}/.netlify/functions/hello?name=two`).then((res) => res.text()), ]) t.expect(responseHelloNameOne).toEqual('three') t.expect(responseHelloNameTwo).toEqual('four') diff --git a/tests/integration/commands/help/help.test.ts b/tests/integration/commands/help/help.test.ts index 26bf8fa6258..945ed5c9a48 100644 --- a/tests/integration/commands/help/help.test.ts +++ b/tests/integration/commands/help/help.test.ts @@ -5,12 +5,12 @@ import { normalize } from '../../utils/snapshots.js' describe('help command', () => { test('netlify help', async () => { - const cliResponse = await callCli(['help']) + const cliResponse = (await callCli(['help'])) as string expect(normalize(cliResponse)).toMatchSnapshot() }) test('netlify help completion', async () => { - const cliResponse = await callCli(['help', 'completion']) + const cliResponse = (await callCli(['help', 'completion'])) as string expect(normalize(cliResponse)).toMatchSnapshot() }) }) diff --git a/tests/integration/commands/init/init.test.ts b/tests/integration/commands/init/init.test.ts index 403e8f006dd..aaff97ce02a 100644 --- a/tests/integration/commands/init/init.test.ts +++ b/tests/integration/commands/init/init.test.ts @@ -21,7 +21,7 @@ const assertNetlifyToml = async ( // assert netlify.toml was created with user inputs const netlifyToml: unknown = toml.parse(await readFile(path.join(tomlDir, '/netlify.toml'), 'utf8')) t.expect(netlifyToml).toEqual( - // @ts-expect-error The types on this package are wrong/unusable + // @ts-expect-error(ndhoule): Don't know what's wrong with this typedef cleanDeep({ build: { command, functions, publish }, }), diff --git a/tests/integration/commands/link/link.test.ts b/tests/integration/commands/link/link.test.ts index 44a68e30085..2e27d25c809 100644 --- a/tests/integration/commands/link/link.test.ts +++ b/tests/integration/commands/link/link.test.ts @@ -72,10 +72,10 @@ describe('link command with multiple sites', () => { await withMockApi( routes, async ({ apiUrl }) => { - const stdout = await callCli( + const stdout = (await callCli( ['link', '--name', 'app'], getCLIOptions({ builder, apiUrl, env: { NETLIFY_SITE_ID: '' } }), - ) + )) as string expect(stdout).toContain('Linked to app') }, @@ -91,10 +91,10 @@ describe('link command with multiple sites', () => { await withMockApi( routes, async ({ apiUrl }) => { - const stdout = await callCli( + const stdout = (await callCli( ['link', '--name', 'ap'], getCLIOptions({ builder, apiUrl, env: { NETLIFY_SITE_ID: '' } }), - ) + )) as string expect(stdout).toContain('Linked to next-app-playground') }, diff --git a/tests/integration/commands/recipes/__snapshots__/recipes.test.js.snap b/tests/integration/commands/recipes/__snapshots__/recipes.test.ts.snap similarity index 100% rename from tests/integration/commands/recipes/__snapshots__/recipes.test.js.snap rename to tests/integration/commands/recipes/__snapshots__/recipes.test.ts.snap diff --git a/tests/integration/commands/recipes/recipes.test.js b/tests/integration/commands/recipes/recipes.test.ts similarity index 61% rename from tests/integration/commands/recipes/recipes.test.js rename to tests/integration/commands/recipes/recipes.test.ts index a6034a06d08..b2d59544f60 100644 --- a/tests/integration/commands/recipes/recipes.test.js +++ b/tests/integration/commands/recipes/recipes.test.ts @@ -1,5 +1,5 @@ -import { readFile } from 'fs/promises' -import { resolve } from 'path' +import fs from 'node:fs/promises' +import path from 'node:path' import { parse } from 'comment-json' import execa from 'execa' @@ -8,12 +8,12 @@ import { describe, test } from 'vitest' import { callCli } from '../../utils/call-cli.js' import { cliPath } from '../../utils/cli-path.js' import { CONFIRM, NO, answerWithValue, handleQuestions } from '../../utils/handle-questions.js' -import { withSiteBuilder } from '../../utils/site-builder.ts' +import { withSiteBuilder } from '../../utils/site-builder.js' import { normalize } from '../../utils/snapshots.js' describe.concurrent('commands/recipes', () => { test('Shows a list of all the available recipes', async (t) => { - const cliResponse = await callCli(['recipes:list']) + const cliResponse = (await callCli(['recipes:list'])) as string t.expect(normalize(cliResponse)).toMatchSnapshot() }) @@ -25,7 +25,7 @@ describe.concurrent('commands/recipes', () => { const childProcess = execa(cliPath, ['recipes', 'vscode'], { cwd: builder.directory, }) - const settingsPath = resolve(builder.directory, '.vscode', 'settings.json') + const settingsPath = path.resolve(builder.directory, '.vscode', 'settings.json') handleQuestions(childProcess, [ { @@ -36,11 +36,13 @@ describe.concurrent('commands/recipes', () => { await childProcess - const settings = JSON.parse(await readFile(`${builder.directory}/.vscode/settings.json`)) + const settings = JSON.parse( + await fs.readFile(path.join(builder.directory, `.vscode/settings.json`), 'utf8'), + ) as unknown - t.expect(settings['deno.enable']).toBe(true) - t.expect(settings['deno.importMap']).toEqual('.netlify/edge-functions-import-map.json') - t.expect(settings['deno.enablePaths']).toStrictEqual(['netlify/edge-functions']) + t.expect(settings).toHaveProperty('deno.enable', true) + t.expect(settings).toHaveProperty('deno.importMap', '.netlify/edge-functions-import-map.json') + t.expect(settings).toHaveProperty('deno.enablePaths', ['netlify/edge-functions']) }) }) @@ -56,7 +58,7 @@ describe.concurrent('commands/recipes', () => { const childProcess = execa(cliPath, ['recipes', 'vscode'], { cwd: builder.directory, }) - const settingsPath = resolve(builder.directory, '.vscode', 'settings.json') + const settingsPath = path.resolve(builder.directory, '.vscode', 'settings.json') handleQuestions(childProcess, [ { @@ -67,12 +69,14 @@ describe.concurrent('commands/recipes', () => { await childProcess - const settings = JSON.parse(await readFile(`${builder.directory}/.vscode/settings.json`)) + const settings = JSON.parse( + await fs.readFile(path.join(builder.directory, `.vscode/settings.json`), 'utf8'), + ) as unknown - t.expect(settings.someSetting).toEqual('value') - t.expect(settings['deno.enable']).toBe(true) - t.expect(settings['deno.importMap']).toEqual('.netlify/edge-functions-import-map.json') - t.expect(settings['deno.enablePaths']).toStrictEqual(['/some/path', 'netlify/edge-functions']) + t.expect(settings).toHaveProperty('someSetting', 'value') + t.expect(settings).toHaveProperty('deno.enable', true) + t.expect(settings).toHaveProperty('deno.importMap', '.netlify/edge-functions-import-map.json') + t.expect(settings).toHaveProperty('deno.enablePaths', ['/some/path', 'netlify/edge-functions']) }) }) @@ -83,7 +87,7 @@ describe.concurrent('commands/recipes', () => { const childProcess = execa(cliPath, ['recipes', 'vscode'], { cwd: builder.directory, }) - const settingsPath = resolve(builder.directory, '.vscode', 'settings.json') + const settingsPath = path.resolve(builder.directory, '.vscode', 'settings.json') handleQuestions(childProcess, [ { @@ -94,8 +98,12 @@ describe.concurrent('commands/recipes', () => { await childProcess - const error = await readFile(`${builder.directory}/.vscode/settings.json`).catch((error_) => error_) - t.expect(error.code).toEqual('ENOENT') + try { + await fs.readFile(path.join(builder.directory, `.vscode/settings.json`), 'utf8') + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('code', 'ENOENT') + } }) }) @@ -115,7 +123,7 @@ describe.concurrent('commands/recipes', () => { const childProcess = execa(cliPath, ['recipes', 'vscode'], { cwd: builder.directory, }) - const settingsPath = resolve(builder.directory, '.vscode', 'settings.json') + const settingsPath = path.resolve(builder.directory, '.vscode', 'settings.json') handleQuestions(childProcess, [ { @@ -130,19 +138,19 @@ describe.concurrent('commands/recipes', () => { await childProcess - const settingsText = await readFile(`${builder.directory}/.vscode/settings.json`, { encoding: 'utf8' }) + const settingsText = await fs.readFile(path.join(builder.directory, `.vscode/settings.json`), 'utf8') t.expect(settingsText.includes(comment)).toBe(true) const settings = parse(settingsText, null, true) - t.expect(settings.someSetting).toEqual('value') - t.expect(settings['deno.enable']).toBe(true) - t.expect(settings['deno.importMap']).toEqual('.netlify/edge-functions-import-map.json') - t.expect([...settings['deno.enablePaths']]).toStrictEqual(['/some/path', 'netlify/edge-functions']) + t.expect(settings).toHaveProperty('someSetting', 'value') + t.expect(settings).toHaveProperty('deno.enable', true) + t.expect(settings).toHaveProperty('deno.importMap', '.netlify/edge-functions-import-map.json') + t.expect(settings).toHaveProperty('deno.enablePaths', ['/some/path', 'netlify/edge-functions']) }) }) test('Suggests closest matching recipe on typo', async (t) => { - const cliResponse = await callCli(['recipes', 'vsc']) + const cliResponse = (await callCli(['recipes', 'vsc'])) as string t.expect(normalize(cliResponse)).toMatchSnapshot() }) diff --git a/tests/integration/framework-detection.test.js b/tests/integration/framework-detection.test.ts similarity index 63% rename from tests/integration/framework-detection.test.js rename to tests/integration/framework-detection.test.ts index 7e6fe1a0162..f676cd33eab 100644 --- a/tests/integration/framework-detection.test.js +++ b/tests/integration/framework-detection.test.ts @@ -3,17 +3,20 @@ import fetch from 'node-fetch' import { describe, test } from 'vitest' import { cliPath } from './utils/cli-path.js' -import { getExecaOptions, withDevServer } from './utils/dev-server.ts' +import { getExecaOptions, withDevServer } from './utils/dev-server.js' import { DOWN, answerWithValue, handleQuestions } from './utils/handle-questions.js' -import { withSiteBuilder } from './utils/site-builder.ts' +import { withSiteBuilder } from './utils/site-builder.js' import { normalize } from './utils/snapshots.js' const content = 'Hello World!' // Normalize random ports. Not only are these ports random, but since the number of digits // in the port can vary, the formatting of the ASCII box drawn around it also varies. -const normalizeSnapshot = (output, opts) => - normalize(output, opts).replace( +const normalizeSnapshot = ( + snapshot: string, + opts: { duration?: boolean | undefined; filePath?: boolean | undefined } = {}, +) => + normalize(snapshot, opts).replace( /◈ Static server listening to \d+[\s┌─│─└┐┘]+ ◈ Server now ready on http:\/\/localhost:\d+ [\s┌─│─└┐┘]+/m, `◈ Static server listening to @@ -109,14 +112,20 @@ describe.concurrent('frameworks/framework-detection', () => { await withSiteBuilder(t, async (builder) => { await builder.withNetlifyToml({ config: { build: { publish: 'public' } } }).build() - // a failure is expected since we use `echo hello` instead of starting a server - const error = await withDevServer( - { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, - () => {}, - true, - ).catch((error_) => error_) - - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer( + { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, + async () => {}, + true, + ) + // a failure is expected since we use `echo hello` instead of starting a server + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -124,9 +133,16 @@ describe.concurrent('frameworks/framework-detection', () => { await withSiteBuilder(t, async (builder) => { await builder.withNetlifyToml({ config: { dev: { framework: 'create-react-app' } } }).build() - // a failure is expected since this is not a true create-react-app project - const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer({ cwd: builder.directory }, async () => {}, true) + // a failure is expected since this is not a true create-react-app project + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -134,8 +150,15 @@ describe.concurrent('frameworks/framework-detection', () => { await withSiteBuilder(t, async (builder) => { await builder.withNetlifyToml({ config: { dev: { framework: 'to-infinity-and-beyond-js' } } }).build() - const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer({ cwd: builder.directory }, async () => {}, true) + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -147,9 +170,15 @@ describe.concurrent('frameworks/framework-detection', () => { }) .build() - // a failure is expected since this is not a true create-react-app project - const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer({ cwd: builder.directory }, async () => {}, true) + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -157,12 +186,15 @@ describe.concurrent('frameworks/framework-detection', () => { await withSiteBuilder(t, async (builder) => { await builder.withNetlifyToml({ config: { dev: { framework: '#custom' } } }).build() - const error = await withDevServer( - { cwd: builder.directory, args: ['--target-port', '3000'] }, - () => {}, - true, - ).catch((error_) => error_) - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer({ cwd: builder.directory, args: ['--target-port', '3000'] }, async () => {}, true) + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -170,12 +202,15 @@ describe.concurrent('frameworks/framework-detection', () => { await withSiteBuilder(t, async (builder) => { await builder.withNetlifyToml({ config: { dev: { framework: '#custom' } } }).build() - const error = await withDevServer( - { cwd: builder.directory, args: ['--command', 'echo hello'] }, - () => {}, - true, - ).catch((error_) => error_) - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer({ cwd: builder.directory, args: ['--command', 'echo hello'] }, async () => {}, true) + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -183,12 +218,19 @@ describe.concurrent('frameworks/framework-detection', () => { await withSiteBuilder(t, async (builder) => { await builder.withNetlifyToml({ config: { dev: { framework: '#custom', publish: 'public' } } }).build() - const error = await withDevServer( - { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, - () => {}, - true, - ).catch((error_) => error_) - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer( + { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, + async () => {}, + true, + ) + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -196,23 +238,29 @@ describe.concurrent('frameworks/framework-detection', () => { await withSiteBuilder(t, async (builder) => { await builder.build() - const error = await withDevServer( - { - cwd: builder.directory, - args: [ - '--command', - 'oops-i-did-it-again forgot-to-use-a-valid-command', - '--target-port', - '3000', - '--framework', - '#custom', - ], - }, - () => {}, - true, - ).catch((error_) => error_) - - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer( + { + cwd: builder.directory, + args: [ + '--command', + 'oops-i-did-it-again forgot-to-use-a-valid-command', + '--target-port', + '3000', + '--framework', + '#custom', + ], + }, + async () => {}, + true, + ) + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -245,8 +293,15 @@ describe.concurrent('frameworks/framework-detection', () => { await childProcess } - const error = await asyncErrorBlock().catch((error_) => error_) - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await asyncErrorBlock() + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -267,17 +322,23 @@ describe.concurrent('frameworks/framework-detection', () => { const childProcess = execa( cliPath, ['dev', '--offline'], - getExecaOptions({ cwd: builder.directory, env: { CI: true } }), + getExecaOptions({ cwd: builder.directory, env: { CI: 'true' } }), ) await childProcess } - const error = await asyncErrorBlock().catch((error_) => error_) - t.expect( - normalizeSnapshot(error.stdout, { duration: true, filePath: true }).includes( - 'Detected commands for: Gatsby, Create React App. Update your settings to specify which to use. Refer to https://ntl.fyi/dev-monorepo for more information.', - ), - ) - t.expect(error.exitCode).toBe(1) + try { + await asyncErrorBlock() + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }).includes( + 'Detected commands for: Gatsby, Create React App. Update your settings to specify which to use. Refer to https://ntl.fyi/dev-monorepo for more information.', + ), + ) + t.expect(err).toHaveProperty('exitCode') + t.expect((err as execa.ExecaReturnValue).exitCode).toBe(1) + } }) }) @@ -285,14 +346,20 @@ describe.concurrent('frameworks/framework-detection', () => { await withSiteBuilder(t, async (builder) => { await builder.withContentFile({ path: 'config.toml', content: '' }).build() - // a failure is expected since the command exits early - const error = await withDevServer( - { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, - () => {}, - true, - ).catch((error_) => error_) - - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer( + { cwd: builder.directory, args: ['--command', 'echo hello', '--target-port', '3000'] }, + async () => {}, + true, + ) + // a failure is expected since the command exits early + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -329,9 +396,16 @@ describe.concurrent('frameworks/framework-detection', () => { }) .build() - // a failure is expected since this is not a true Gatsby project - const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) - t.expect(normalizeSnapshot(error.stdout, { duration: true, filePath: true })).toMatchSnapshot() + try { + await withDevServer({ cwd: builder.directory }, async () => {}, true) + // a failure is expected since this is not a true Gatsby project + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect( + normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }), + ).toMatchSnapshot() + } }) }) @@ -339,9 +413,16 @@ describe.concurrent('frameworks/framework-detection', () => { await withSiteBuilder(t, async (builder) => { await builder.withNetlifyToml({ config: { dev: { framework: 'remix' } } }).build() - // a failure is expected since this is not a true remix project - const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) - t.expect(error.stdout.includes(`Failed running command: remix watch. Please verify 'remix' exists`)).toBe(true) + try { + await withDevServer({ cwd: builder.directory }, async () => {}, true) + // a failure is expected since this is not a true remix project + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect((err as execa.ExecaReturnValue).stdout).toContain( + `Failed running command: remix watch. Please verify 'remix' exists`, + ) + } }) }) @@ -357,9 +438,16 @@ describe.concurrent('frameworks/framework-detection', () => { .withContentFile({ path: 'remix.config.js', content: '' }) .build() - // a failure is expected since this is not a true remix project - const error = await withDevServer({ cwd: builder.directory }, () => {}, true).catch((error_) => error_) - t.expect(error.stdout.includes(`Failed running command: remix watch. Please verify 'remix' exists`)).toBe(true) + try { + await withDevServer({ cwd: builder.directory }, async () => {}, true) + // a failure is expected since this is not a true remix project + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + t.expect((err as execa.ExecaReturnValue).stdout).toContain( + `Failed running command: remix watch. Please verify 'remix' exists`, + ) + } }) }) @@ -381,10 +469,11 @@ describe.concurrent('frameworks/framework-detection', () => { name: 'frameworker', plugin: { onPreBuild: async ({ netlifyConfig }) => { - // eslint-disable-next-line no-undef, @typescript-eslint/no-require-imports - const { mkdir, writeFile } = require('fs/promises') + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { mkdir, writeFile } = require('node:fs/promises') as typeof import('node:fs/promises') const generatedFunctionsDir = 'new_functions' + // @ts-expect-error FIXME(ndhoule): Unsure if this is a legitimate error or bad types netlifyConfig.functions.directory = generatedFunctionsDir netlifyConfig.redirects.push({ diff --git a/tests/integration/frameworks/eleventy.test.js b/tests/integration/frameworks/eleventy.test.ts similarity index 65% rename from tests/integration/frameworks/eleventy.test.js rename to tests/integration/frameworks/eleventy.test.ts index d596bd4f52b..3219da530d6 100644 --- a/tests/integration/frameworks/eleventy.test.js +++ b/tests/integration/frameworks/eleventy.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/restrict-template-expressions */ import { Buffer } from 'buffer' import path from 'path' import { fileURLToPath } from 'url' @@ -6,7 +7,7 @@ import fetch from 'node-fetch' import { afterAll, beforeAll, describe, test } from 'vitest' import { clientIP, originalIP } from '../../lib/local-ip.js' -import { startDevServer } from '../utils/dev-server.ts' +import { startDevServer } from '../utils/dev-server.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -19,16 +20,19 @@ beforeAll(async () => { args: ['--framework', '#static'], }) + // @ts-expect-error TS(2339) FIXME: Property 'server' does not exist on type '{}'. context.server = server }) afterAll(async () => { + // @ts-expect-error TS(2339) FIXME: Property 'server' does not exist on type '{}'. const { server } = context await server.close() }) describe.skip('eleventy', () => { test('homepage', async (t) => { + // @ts-expect-error TS(2339) FIXME: Property 'server' does not exist on type '{}'. const { url } = context.server const response = await fetch(`${url}/`).then((res) => res.text()) @@ -36,6 +40,7 @@ describe.skip('eleventy', () => { }) test('redirect test', async (t) => { + // @ts-expect-error TS(2339) FIXME: Property 'server' does not exist on type '{}'. const { url } = context.server const response = await fetch(`${url}/something`, { redirect: 'manual', @@ -43,39 +48,47 @@ describe.skip('eleventy', () => { const { headers, status } = response t.expect(status).toBe(301) + // @ts-expect-error: may be null t.expect(headers.get('location').endsWith('/otherthing')).toBe(true) t.expect(await response.text()).toEqual('Redirecting to /otherthing') }) test('normal rewrite', async (t) => { + // @ts-expect-error TS(2339) FIXME: Property 'server' does not exist on type '{}'. const { url } = context.server const response = await fetch(`${url}/doesnt-exist`) const { headers, status } = response const body = await response.text() t.expect(status).toBe(200) + // @ts-expect-error: may be null t.expect(headers.get('content-type').startsWith('text/html')).toBe(true) t.expect(body.includes('Eleventy Site')).toBe(true) }) test('force rewrite', async (t) => { + // @ts-expect-error TS(2339) FIXME: Property 'server' does not exist on type '{}'. const { url } = context.server const response = await fetch(`${url}/force`) const { headers, status } = response const body = await response.text() t.expect(status).toBe(200) + // @ts-expect-error: may be null t.expect(headers.get('content-type').startsWith('text/html')).toBe(true) t.expect(body.includes('

Test content

')).toBe(true) }) test('functions rewrite echo without body', async (t) => { + // @ts-expect-error TS(2339) FIXME: Property 'server' does not exist on type '{}'. const { host, port, url } = context.server const jsonResponse = await fetch(`${url}/api/echo?ding=dong`, { headers: { accept: 'application/json', 'accept-encoding': 'gzip, deflate, br' }, }).then((res) => res.json()) + // @ts-expect-error TS(2339) FIXME: Property 'headers' does not exist on type 'unknown... Remove this comment to see the full error message const { 'x-nf-request-id': requestID, ...headers } = jsonResponse.headers + // @ts-expect-error TS(2339) FIXME: Property 'body' does not exist on type 'unknown'. t.expect(jsonResponse.body).toBe(undefined) t.expect(headers).toStrictEqual({ accept: 'application/json', @@ -92,13 +105,18 @@ describe.skip('eleventy', () => { ).toString('base64'), }) t.expect(requestID.length).toBe(26) + // @ts-expect-error TS(2339) FIXME: Property 'httpMethod' does not exist on type 'unkn... Remove this comment to see the full error message t.expect(jsonResponse.httpMethod).toEqual('GET') + // @ts-expect-error TS(2339) FIXME: Property 'isBase64Encoded' does not exist on type ... Remove this comment to see the full error message t.expect(jsonResponse.isBase64Encoded).toBe(true) + // @ts-expect-error TS(2339) FIXME: Property 'path' does not exist on type 'unknown'. t.expect(jsonResponse.path).toEqual('/api/echo') + // @ts-expect-error TS(2339) FIXME: Property 'queryStringParameters' does not exist on... Remove this comment to see the full error message t.expect(jsonResponse.queryStringParameters).toStrictEqual({ ding: 'dong' }) }) test('functions rewrite echo with body', async (t) => { + // @ts-expect-error TS(2339) FIXME: Property 'server' does not exist on type '{}'. const { host, port, url } = context.server const response = await fetch(`${url}/api/echo?ding=dong`, { method: 'POST', @@ -109,8 +127,10 @@ describe.skip('eleventy', () => { }, body: 'some=thing', }).then((res) => res.json()) + // @ts-expect-error TS(2339) FIXME: Property 'headers' does not exist on type 'unknown... Remove this comment to see the full error message const { 'x-nf-request-id': requestID, ...headers } = response.headers + // @ts-expect-error TS(2339) FIXME: Property 'body' does not exist on type 'unknown'. t.expect(response.body).toEqual('some=thing') t.expect(headers).toStrictEqual({ accept: 'application/json', @@ -129,13 +149,18 @@ describe.skip('eleventy', () => { ).toString('base64'), }) t.expect(requestID.length).toBe(26) + // @ts-expect-error TS(2339) FIXME: Property 'httpMethod' does not exist on type 'unkn... Remove this comment to see the full error message t.expect(response.httpMethod).toEqual('POST') + // @ts-expect-error TS(2339) FIXME: Property 'isBase64Encoded' does not exist on type ... Remove this comment to see the full error message t.expect(response.isBase64Encoded).toBe(false) + // @ts-expect-error TS(2339) FIXME: Property 'path' does not exist on type 'unknown'. t.expect(response.path).toEqual('/api/echo') + // @ts-expect-error TS(2339) FIXME: Property 'queryStringParameters' does not exist on... Remove this comment to see the full error message t.expect(response.queryStringParameters).toStrictEqual({ ding: 'dong' }) }) test('functions echo with multiple query params', async (t) => { + // @ts-expect-error TS(2339) FIXME: Property 'server' does not exist on type '{}'. const { host, port, url } = context.server const response = await fetch(`${url}/.netlify/functions/echo?category=a&category=b`, { headers: { @@ -143,6 +168,7 @@ describe.skip('eleventy', () => { 'accept-encoding': 'gzip, deflate, br', }, }).then((res) => res.json()) + // @ts-expect-error TS(2339) FIXME: Property 'headers' does not exist on type 'unknown... Remove this comment to see the full error message const { 'x-nf-request-id': requestID, ...headers } = response.headers t.expect(headers).toStrictEqual({ @@ -160,10 +186,15 @@ describe.skip('eleventy', () => { ).toString('base64'), }) t.expect(requestID.length).toBe(26) + // @ts-expect-error TS(2339) FIXME: Property 'httpMethod' does not exist on type 'unkn... Remove this comment to see the full error message t.expect(response.httpMethod).toEqual('GET') + // @ts-expect-error TS(2339) FIXME: Property 'isBase64Encoded' does not exist on type ... Remove this comment to see the full error message t.expect(response.isBase64Encoded).toBe(true) + // @ts-expect-error TS(2339) FIXME: Property 'path' does not exist on type 'unknown'. t.expect(response.path).toEqual('/.netlify/functions/echo') + // @ts-expect-error TS(2339) FIXME: Property 'queryStringParameters' does not exist on... Remove this comment to see the full error message t.expect(response.queryStringParameters).toStrictEqual({ category: 'a, b' }) + // @ts-expect-error TS(2339) FIXME: Property 'multiValueQueryStringParameters' does no... Remove this comment to see the full error message t.expect(response.multiValueQueryStringParameters).toStrictEqual({ category: ['a', 'b'] }) }) }) diff --git a/tests/integration/rules-proxy.test.ts b/tests/integration/rules-proxy.test.ts index 219aef47d33..e70e76df639 100644 --- a/tests/integration/rules-proxy.test.ts +++ b/tests/integration/rules-proxy.test.ts @@ -21,7 +21,7 @@ describe('rules-proxy', () => { await builder.build() const rewriter = await createRewriter({ - // @ts-expect-error(serhalp) -- Lazy test type. Create a factory and use it here. + // @ts-expect-error TS(2322) FIXME: Type '{}' is not assignable to type 'NormalizedCac... Remove this comment to see the full error message config: {}, distDir: builder.directory, projectDir: builder.directory, diff --git a/tests/integration/serve/functions-go.test.js b/tests/integration/serve/functions-go.test.ts similarity index 87% rename from tests/integration/serve/functions-go.test.js rename to tests/integration/serve/functions-go.test.ts index 592d1cf816c..9126467ca21 100644 --- a/tests/integration/serve/functions-go.test.js +++ b/tests/integration/serve/functions-go.test.ts @@ -1,10 +1,10 @@ import fetch from 'node-fetch' import { describe, test } from 'vitest' -import { tryAndLogOutput, withDevServer } from '../utils/dev-server.ts' +import { tryAndLogOutput, withDevServer } from '../utils/dev-server.js' import { createMock as createExecaMock } from '../utils/mock-execa.js' import { pause } from '../utils/pause.js' -import { withSiteBuilder } from '../utils/site-builder.ts' +import { withSiteBuilder } from '../utils/site-builder.js' const WAIT_WRITE = 1000 @@ -78,12 +78,13 @@ describe.concurrent('serve/functions-go', () => { await withDevServer( { cwd: builder.directory, - env: execaMock, + env: typeof execaMock === 'function' ? {} : execaMock, }, + // eslint-disable-next-line @typescript-eslint/unbound-method async ({ outputBuffer, port, waitForLogMatching }) => { await tryAndLogOutput(async () => { - const response = await fetch(`http://localhost:${port}/.netlify/functions/go-func`).then((res) => - res.text(), + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/go-func`).then( + (res) => res.text(), ) t.expect(response).toEqual(originalBody) }, outputBuffer) @@ -96,7 +97,7 @@ describe.concurrent('serve/functions-go', () => { await waitForLogMatching('Reloaded function go-func') - const response = await fetch(`http://localhost:${port}/.netlify/functions/go-func`).then((res) => + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/go-func`).then((res) => res.text(), ) @@ -104,7 +105,9 @@ describe.concurrent('serve/functions-go', () => { }, ) } finally { - await removeExecaMock() + if (typeof removeExecaMock === 'function') { + await removeExecaMock() + } } }) }) @@ -166,10 +169,10 @@ describe.concurrent('serve/functions-go', () => { await withDevServer( { cwd: builder.directory, - env: execaMock, + env: typeof execaMock === 'function' ? {} : execaMock, }, async ({ port }) => { - const response = await fetch(`http://localhost:${port}/.netlify/functions/go-scheduled-function`) + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/go-scheduled-function`) const responseBody = await response.text() t.expect(responseBody).toMatch(/You performed an HTTP request/) t.expect(responseBody).toMatch(/Your function returned `body`/) @@ -178,7 +181,9 @@ describe.concurrent('serve/functions-go', () => { }, ) } finally { - await removeExecaMock() + if (typeof removeExecaMock === 'function') { + await removeExecaMock() + } } }) }) diff --git a/tests/integration/serve/functions-rust.test.js b/tests/integration/serve/functions-rust.test.ts similarity index 80% rename from tests/integration/serve/functions-rust.test.js rename to tests/integration/serve/functions-rust.test.ts index 522a6065f74..bc16040acfe 100644 --- a/tests/integration/serve/functions-rust.test.js +++ b/tests/integration/serve/functions-rust.test.ts @@ -1,10 +1,10 @@ import fetch from 'node-fetch' import { test } from 'vitest' -import { tryAndLogOutput, withDevServer } from '../utils/dev-server.ts' +import { tryAndLogOutput, withDevServer } from '../utils/dev-server.js' import { createMock as createExecaMock } from '../utils/mock-execa.js' import { pause } from '../utils/pause.js' -import { withSiteBuilder } from '../utils/site-builder.ts' +import { withSiteBuilder } from '../utils/site-builder.js' const WAIT_WRITE = 1000 @@ -69,12 +69,16 @@ test('Updates a Rust function when a file is modified', async (t) => { await withDevServer( { cwd: builder.directory, - env: { ...execaMock, NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE: 'true' }, + env: { + ...(typeof execaMock === 'function' ? {} : execaMock), + NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE: 'true', + }, }, + // eslint-disable-next-line @typescript-eslint/unbound-method async ({ outputBuffer, port, waitForLogMatching }) => { await tryAndLogOutput(async () => { - const response = await fetch(`http://localhost:${port}/.netlify/functions/rust-func`).then((res) => - res.text(), + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/rust-func`).then( + (res) => res.text(), ) t.expect(response).toEqual(originalBody) }, outputBuffer) @@ -87,7 +91,7 @@ test('Updates a Rust function when a file is modified', async (t) => { await waitForLogMatching('Reloaded function rust-func') - const response = await fetch(`http://localhost:${port}/.netlify/functions/rust-func`).then((res) => + const response = await fetch(`http://localhost:${port.toString()}/.netlify/functions/rust-func`).then((res) => res.text(), ) @@ -95,7 +99,9 @@ test('Updates a Rust function when a file is modified', async (t) => { }, ) } finally { - await removeExecaMock() + if (typeof removeExecaMock === 'function') { + await removeExecaMock() + } } }) }) diff --git a/tests/integration/utils/call-cli.js b/tests/integration/utils/call-cli.ts similarity index 53% rename from tests/integration/utils/call-cli.js rename to tests/integration/utils/call-cli.ts index f0b967a712e..2f49448564a 100644 --- a/tests/integration/utils/call-cli.js +++ b/tests/integration/utils/call-cli.ts @@ -6,13 +6,17 @@ const CLI_TIMEOUT = 3e5 /** * Calls the Cli with a max timeout. + * * If the `parseJson` argument is specified then the result will be converted into an object. - * @param {readonly string[]} args - * @param {execa.Options} execOptions - * @param {boolean} parseJson - * @returns {Promise} */ -export const callCli = async function (args, execOptions = {}, parseJson = false) { +// FIXME(ndhoule): Discriminate on return type depending on `parseJson` option; it should be a +// `Promise` when false and a `Promise` when true. +export const callCli = async function ( + args: string[] = [], + execOptions: execa.NodeOptions = {}, + parseJson = false, +): // eslint-disable-next-line @typescript-eslint/no-explicit-any +Promise { const { stdout } = await execa.node(cliPath, args, { timeout: CLI_TIMEOUT, nodeOptions: [], diff --git a/tests/integration/utils/cli-path.js b/tests/integration/utils/cli-path.ts similarity index 100% rename from tests/integration/utils/cli-path.js rename to tests/integration/utils/cli-path.ts diff --git a/tests/integration/utils/create-live-test-site.js b/tests/integration/utils/create-live-test-site.ts similarity index 56% rename from tests/integration/utils/create-live-test-site.js rename to tests/integration/utils/create-live-test-site.ts index f2723d7a296..6c05635e99a 100644 --- a/tests/integration/utils/create-live-test-site.js +++ b/tests/integration/utils/create-live-test-site.ts @@ -1,8 +1,8 @@ -import { NETLIFY_TEST_ACCOUNT_SLUG } from 'process' +import process from 'node:process' import { callCli } from './call-cli.js' -export const generateSiteName = function (prefix) { +export const generateSiteName = function (prefix: string) { const randomString = Math.random() .toString(BASE_36) .replace(/[^a-z]+/g, '') @@ -13,24 +13,30 @@ export const generateSiteName = function (prefix) { const BASE_36 = 36 const RANDOM_SITE_LENGTH = 8 -const listAccounts = async function () { - return JSON.parse(await callCli(['api', 'listAccountsForUser'])) +const listAccounts = async () => { + return JSON.parse((await callCli(['api', 'listAccountsForUser'])) as string) as { slug: string }[] } -export const createLiveTestSite = async function (siteName) { +export const createLiveTestSite = async function (siteName: string) { console.log(`Creating new site for tests: ${siteName}`) const accounts = await listAccounts() if (!Array.isArray(accounts) || accounts.length <= 0) { throw new Error(`Can't find suitable account to create a site`) } - const account = NETLIFY_TEST_ACCOUNT_SLUG - ? accounts.find(({ slug }) => slug === NETLIFY_TEST_ACCOUNT_SLUG) - : accounts[0] + const testAccountSlug = process.env.NETLIFY_TEST_ACCOUNT_SLUG ?? '' + const account = testAccountSlug !== '' ? accounts.find(({ slug }) => slug === testAccountSlug) : accounts[0] + if (account === undefined) { + throw new Error( + testAccountSlug !== '' + ? `could not find account with slug ${testAccountSlug}` + : 'user has no associated accounts', + ) + } const accountSlug = account.slug console.log(`Using account ${accountSlug} to create site: ${siteName}`) - const cliResponse = await callCli(['sites:create', '--name', siteName, '--account-slug', accountSlug]) + const cliResponse = (await callCli(['sites:create', '--name', siteName, '--account-slug', accountSlug])) as string - const isSiteCreated = /Site Created/.test(cliResponse) + const isSiteCreated = cliResponse.includes('Site Created') if (!isSiteCreated) { throw new Error(`Failed creating site: ${cliResponse}`) } diff --git a/tests/integration/utils/curl.js b/tests/integration/utils/curl.ts similarity index 65% rename from tests/integration/utils/curl.js rename to tests/integration/utils/curl.ts index 892f54bbc3b..cdd422edc0c 100644 --- a/tests/integration/utils/curl.js +++ b/tests/integration/utils/curl.ts @@ -2,7 +2,7 @@ import execa from 'execa' const CURL_TIMEOUT = 1e5 -export const curl = async (url, args) => { +export const curl = async (url: string, args: string[] = []): Promise => { const { stdout } = await execa('curl', [...args, url], { timeout: CURL_TIMEOUT }) return stdout } diff --git a/tests/integration/utils/dev-server.ts b/tests/integration/utils/dev-server.ts index 344dd1bd92c..5a6f7271dd9 100644 --- a/tests/integration/utils/dev-server.ts +++ b/tests/integration/utils/dev-server.ts @@ -1,5 +1,5 @@ -import path from 'path' -import process from 'process' +import path from 'node:path' +import process from 'node:process' import execa from 'execa' import getPort from 'get-port' @@ -26,8 +26,8 @@ export interface DevServer { url: string host: string port: number - errorBuffer: any[] - outputBuffer: any[] + errorBuffer: Buffer[] + outputBuffer: Buffer[] waitForLogMatching(match: string): Promise output: string error: string @@ -39,7 +39,7 @@ type $FIXME = any interface DevServerOptions { args?: string[] - context?: string + context?: string | null | undefined cwd: string framework?: string command?: string @@ -81,16 +81,16 @@ const startServer = async ({ baseCommand, offline ? '--offline' : '', '-p', - port, + port.toString(), debug ? '--debug' : '', skipWaitPort ? '--skip-wait-port' : '', ] if (targetPort) { - baseArgs.push('--target-port', targetPort) + baseArgs.push('--target-port', targetPort.toString()) } else { const staticPort = await getPort() - baseArgs.push('--staticServerPort', staticPort) + baseArgs.push('--staticServerPort', staticPort.toString()) } if (framework) { @@ -107,7 +107,6 @@ const startServer = async ({ baseArgs.push('--context', context) } - // @ts-expect-error FIXME const ps = execa(cliPath, [...baseArgs, ...args], getExecaOptions({ cwd, env })) if (process.env.DEBUG_TESTS) { @@ -121,14 +120,14 @@ const startServer = async ({ handleQuestions(ps, prompt, promptHistory) } - const outputBuffer: any[] = [] - const errorBuffer: any[] = [] + const outputBuffer: Buffer[] = [] + const errorBuffer: Buffer[] = [] const serverPromise = new Promise((resolve, reject) => { let selfKilled = false - ps.stderr!.on('data', (data) => { + ps.stderr!.on('data', (data: Buffer) => { errorBuffer.push(data) }) - ps.stdout!.on('data', (data) => { + ps.stdout!.on('data', (data: Buffer) => { outputBuffer.push(data) if (!expectFailure && data.includes('Server now ready on')) { setImmediate(() => { @@ -182,11 +181,11 @@ export const startDevServer = async (options: DevServerOptions, expectFailure?: try { // do not use destruction, as we use getters which otherwise would be evaluated here const devServer = await startServer({ ...options, expectFailure }) - // @ts-expect-error FIXME + // @ts-expect-error TS(2339) FIXME: Property 'timeout' does not exist on type 'DevServ... Remove this comment to see the full error message if (devServer.timeout) { throw new Error(`Timed out starting dev server.\nServer Output:\n${devServer.output}`) } - // @ts-expect-error FIXME + // @ts-expect-error TS(2322) FIXME: Type 'DevServer | { timeout: boolean; output: stri... Remove this comment to see the full error message return devServer } catch (error) { if (attempt === maxAttempts || expectFailure) { diff --git a/tests/integration/utils/external-server-cli.js b/tests/integration/utils/external-server-cli.ts similarity index 100% rename from tests/integration/utils/external-server-cli.js rename to tests/integration/utils/external-server-cli.ts diff --git a/tests/integration/utils/external-server.js b/tests/integration/utils/external-server.js deleted file mode 100644 index a8055788cb3..00000000000 --- a/tests/integration/utils/external-server.js +++ /dev/null @@ -1,13 +0,0 @@ -import { env } from 'process' - -import express from 'express' - -export const startExternalServer = ({ host, port } = {}) => { - const app = express() - app.use(express.urlencoded({ extended: true })) - app.all('*', function onRequest(req, res) { - res.json({ url: req.url, body: req.body, method: req.method, headers: req.headers, env }) - }) - - return app.listen({ port, host }) -} diff --git a/tests/integration/utils/external-server.ts b/tests/integration/utils/external-server.ts new file mode 100644 index 00000000000..732b4d3ce5a --- /dev/null +++ b/tests/integration/utils/external-server.ts @@ -0,0 +1,19 @@ +import { env } from 'process' + +import express from 'express' + +export const startExternalServer = ({ host, port }: { host?: string | undefined; port?: number | undefined } = {}) => { + const app = express() + app.use(express.urlencoded({ extended: true })) + app.all('*', function onRequest(req, res) { + res.json({ + url: req.url, + body: req.body as string, + method: req.method, + headers: req.headers, + env, + }) + }) + + return app.listen({ port, host }) +} diff --git a/tests/integration/utils/fixture.ts b/tests/integration/utils/fixture.ts index e80ca7ff990..56e99656336 100644 --- a/tests/integration/utils/fixture.ts +++ b/tests/integration/utils/fixture.ts @@ -94,9 +94,6 @@ export class Fixture { /** * Calls the CLI with a max timeout inside the fixture directory. * If the `parseJson` argument is specified then the result will be converted into an object. - * @param {string[]} args - * @param {any} options - * @returns {Promise} */ async callCli( args: string[], @@ -107,13 +104,14 @@ export class Fixture { cliOptions = getCLIOptions({ apiUrl: this.options.apiUrl, env: execOptions.env }) } - // @ts-expect-error we do not care it is readonly here + // @ts-expect-error: Intentionally ignoring read-only property annotation cliOptions.cwd = this.directory if (offline) { args.push('--offline') } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return callCli(args, cliOptions, parseJson) } } diff --git a/tests/integration/utils/handle-questions.js b/tests/integration/utils/handle-questions.js deleted file mode 100644 index 56ac0f1dbc3..00000000000 --- a/tests/integration/utils/handle-questions.js +++ /dev/null @@ -1,38 +0,0 @@ -import { Buffer } from 'buffer' - -/** - * Utility to mock the stdin of the cli. You must provide the correct number of - * questions correctly typed or the process will keep waiting for input. - * @param {ExecaChildProcess} process - * @param {Array<{question: string, answer: string|string[]}>} questions - * @param {Array} prompts - * - questions that you know the CLI will ask and respective answers to mock - */ -export const handleQuestions = (process, questions, prompts = []) => { - let buffer = '' - process.stdout.on('data', (data) => { - buffer = (buffer + data).replace(/\n/g, '') - const index = questions.findIndex( - ({ question }, questionIndex) => buffer.includes(question) && !prompts.includes(questionIndex), - ) - if (index >= 0) { - prompts.push(index) - buffer = '' - const { answer } = questions[index] - - writeResponse(process, Array.isArray(answer) ? answer : [answer]) - } - }) -} - -const writeResponse = (process, responses) => { - const response = responses.shift() - if (response) process.stdin.write(Buffer.from(response)) - if (responses.length !== 0) setTimeout(() => writeResponse(process, responses), 50) -} - -export const answerWithValue = (value = '') => [value, CONFIRM].flat() - -export const CONFIRM = '\n' -export const DOWN = '\u001B[B' -export const NO = 'n' diff --git a/tests/integration/utils/handle-questions.ts b/tests/integration/utils/handle-questions.ts new file mode 100644 index 00000000000..790df3b0be5 --- /dev/null +++ b/tests/integration/utils/handle-questions.ts @@ -0,0 +1,66 @@ +import { Buffer } from 'node:buffer' + +interface Stdin { + write(data: Buffer): boolean +} + +interface Stdout { + on(event: 'data', listener: (buffer: Buffer) => void): this +} + +interface Process { + stdin: Stdin | null + stdout: Stdout | null +} + +/** + * Utility to mock the stdin of the cli. You must provide the correct number of + * questions correctly typed or the process will keep waiting for input. + * + * @param process + * @param questions + * @param prompts questions that you know the CLI will ask and respective answers to mock + */ +export const handleQuestions = ( + process: Process, + questions: { question: string; answer: string | string[] }[], + prompts: number[] = [], +): void => { + if (process.stdout === null) { + throw new Error('specified process does not have readable stdout') + } + + let buffer = '' + process.stdout.on('data', (data: Buffer) => { + buffer = (buffer + data.toString()).replace(/\n/g, '') + const index = questions.findIndex( + ({ question }, questionIndex) => buffer.includes(question) && !prompts.includes(questionIndex), + ) + if (index >= 0) { + prompts.push(index) + buffer = '' + const { answer } = questions[index] + + writeResponse(process, Array.isArray(answer) ? answer : [answer]) + } + }) +} + +const writeResponse = (process: Process, responses: string[]) => { + if (process.stdin === null) { + throw new Error('specified process does not have writable stdin') + } + + const response = responses.shift() + if (response) process.stdin.write(Buffer.from(response)) + if (responses.length !== 0) + setTimeout(() => { + writeResponse(process, responses) + }, 50) +} + +export const answerWithValue = (value = '') => [value, CONFIRM].flat() + +export const CONFIRM = '\n' +export const DOWN = '\u001B[B' +export const NO = 'n' diff --git a/tests/integration/utils/mock-api.ts b/tests/integration/utils/mock-api.ts index 67c3e8bcac8..2356304e3f3 100644 --- a/tests/integration/utils/mock-api.ts +++ b/tests/integration/utils/mock-api.ts @@ -121,13 +121,12 @@ export const withMockApi = async ( }) => Promise, silent = false, ) => { - let mockApi: Awaited> + let mockApi: Awaited> | undefined try { mockApi = await startMockApi({ routes, silent }) await testHandler({ apiUrl: mockApi.apiUrl, requests: mockApi.requests }) } finally { - // @ts-expect-error Not worth fixing, this file is deprecated in favor of mock-api-vitest - mockApi.server.close() + mockApi?.server.close() } } diff --git a/tests/integration/utils/mock-execa.js b/tests/integration/utils/mock-execa.ts similarity index 92% rename from tests/integration/utils/mock-execa.js rename to tests/integration/utils/mock-execa.ts index a9291d531ec..6efaf9e1d05 100644 --- a/tests/integration/utils/mock-execa.js +++ b/tests/integration/utils/mock-execa.ts @@ -5,7 +5,7 @@ import { pathToFileURL } from 'url' // an environment variable that replaces the `execa` module implementation. // A cleanup method is also returned, allowing the consumer to remove the // mock file. -export const createMock = async (contents) => { +export const createMock = async (contents: string) => { const { temporaryFile } = await import('tempy') const path = temporaryFile({ extension: 'js' }) diff --git a/tests/integration/utils/pause.js b/tests/integration/utils/pause.js deleted file mode 100644 index 74cdccfbcca..00000000000 --- a/tests/integration/utils/pause.js +++ /dev/null @@ -1,4 +0,0 @@ -export const pause = (interval) => - new Promise((resolve) => { - setTimeout(resolve, interval) - }) diff --git a/tests/integration/utils/pause.ts b/tests/integration/utils/pause.ts new file mode 100644 index 00000000000..ef3bf980b31 --- /dev/null +++ b/tests/integration/utils/pause.ts @@ -0,0 +1,4 @@ +export const pause = (interval: number): Promise => + new Promise((resolve) => { + setTimeout(resolve, interval) + }) diff --git a/tests/integration/utils/process.js b/tests/integration/utils/process.ts similarity index 50% rename from tests/integration/utils/process.js rename to tests/integration/utils/process.ts index 8326b2c73a7..6b936eb511d 100644 --- a/tests/integration/utils/process.js +++ b/tests/integration/utils/process.ts @@ -1,9 +1,14 @@ import pTimeout from 'p-timeout' import kill from 'tree-kill' +import type execa from 'execa' const PROCESS_EXIT_TIMEOUT = 5e3 -export const killProcess = async (ps) => { +export const killProcess = async (ps: execa.ExecaChildProcess): Promise => { + if (ps.pid === undefined) { + throw new Error('process.pid is empty; cannot kill a process that is not started') + } + kill(ps.pid) await pTimeout( ps.catch(() => {}), diff --git a/tests/integration/utils/site-builder.ts b/tests/integration/utils/site-builder.ts index b0a9a4bf1dd..a8cca356e01 100644 --- a/tests/integration/utils/site-builder.ts +++ b/tests/integration/utils/site-builder.ts @@ -178,7 +178,7 @@ export class SiteBuilder { return this } - withContentFile({ content, path: filePath }: { content: string; path: string }) { + withContentFile({ content, path: filePath }: { content: Buffer | string; path: string }) { const dest = path.join(this.directory, filePath) this.tasks.push(async () => { await ensureDir(path.dirname(dest)) diff --git a/tests/integration/utils/snapshots.js b/tests/integration/utils/snapshots.ts similarity index 67% rename from tests/integration/utils/snapshots.js rename to tests/integration/utils/snapshots.ts index 1d506134e1d..00fe967affe 100644 --- a/tests/integration/utils/snapshots.js +++ b/tests/integration/utils/snapshots.ts @@ -1,4 +1,4 @@ -const baseNormalizers = [ +const baseNormalizers: Normalizer[] = [ // Information about the package and the OS { pattern: /netlify-cli\/.+node-.+/g, value: 'netlify-cli/test-version test-os test-node-version' }, { pattern: /@netlify\/build (\d+\.\d+\.\d+)/g, value: '@netlify/build 0.0.0' }, @@ -14,7 +14,9 @@ const baseNormalizers = [ { pattern: /It should be one of.+/gm, value: 'It should be one of: *' }, ] -const optionalNormalizers = { +type Normalizer = { pattern: RegExp; value: string } + +const optionalNormalizers: Record = { // File paths filePath: { pattern: /(^|[ "'(=])((?:\.{0,2}|([A-Z]:)|file:\/\/)(\/[^ "')\n]+))/gm, value: '/file/path' }, @@ -22,15 +24,15 @@ const optionalNormalizers = { duration: { pattern: /(\d[\d.]*(ms|m|s)( )?)+/g, value: 'Xms' }, } -export const normalize = (inputString, { duration, filePath } = {}) => { - const normalizers = [ +export const normalize = ( + inputString: string, + { duration, filePath }: { duration?: boolean | undefined; filePath?: boolean | undefined } = {}, +) => + [ ...baseNormalizers, - duration && optionalNormalizers.duration, - filePath && optionalNormalizers.filePath, + duration ? optionalNormalizers.duration : undefined, + filePath ? optionalNormalizers.filePath : undefined, ] - - return normalizers - .filter(Boolean) + .filter((normalizer) => normalizer !== undefined) .reduce((acc, { pattern, value }) => acc.replace(pattern, value), inputString) .trim() -} diff --git a/tests/lib/local-ip.js b/tests/lib/local-ip.ts similarity index 100% rename from tests/lib/local-ip.js rename to tests/lib/local-ip.ts diff --git a/tests/unit/lib/account.test.js b/tests/unit/lib/account.test.ts similarity index 89% rename from tests/unit/lib/account.test.js rename to tests/unit/lib/account.test.ts index 1ff44c289f0..3a41f5ab2cb 100644 --- a/tests/unit/lib/account.test.js +++ b/tests/unit/lib/account.test.ts @@ -1,9 +1,10 @@ import { describe, expect, test } from 'vitest' -import { supportsBackgroundFunctions } from '../../../dist/lib/account.js' +import { supportsBackgroundFunctions } from '../../../src/lib/account.js' describe('supportsBackgroundFunctions', () => { test(`should return false if no account`, () => { + // @ts-expect-error TS(2554) FIXME: Expected 1 arguments, but got 0. expect(supportsBackgroundFunctions()).toEqual(false) }) diff --git a/tests/unit/lib/completion/__snapshots__/generate-autocompletion.test.js.snap b/tests/unit/lib/completion/__snapshots__/generate-autocompletion.test.js.snap deleted file mode 100644 index 2a653d25eb3..00000000000 --- a/tests/unit/lib/completion/__snapshots__/generate-autocompletion.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`generateAutocompletion > should generate a completion file 1`] = `undefined`; diff --git a/tests/unit/lib/completion/__snapshots__/generate-autocompletion.test.ts.snap b/tests/unit/lib/completion/__snapshots__/generate-autocompletion.test.ts.snap new file mode 100644 index 00000000000..1f1e643a77e --- /dev/null +++ b/tests/unit/lib/completion/__snapshots__/generate-autocompletion.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateAutocompletion > should generate a completion file 1`] = `"{\\"bake\\":{\\"name\\":\\"bake\\",\\"description\\":\\"Cooks something\\",\\"options\\":[{\\"name\\":\\"--fast\\",\\"description\\":\\"cook it fast\\"},{\\"name\\":\\"--filter\\",\\"description\\":\\"For monorepos, specify the name of the application to run the command in\\"},{\\"name\\":\\"--debug\\",\\"description\\":\\"Print debugging information\\"},{\\"name\\":\\"--auth\\",\\"description\\":\\"Netlify auth token - can be used to run this command without logging in\\"}]},\\"bake:pizza\\":{\\"name\\":\\"bake:pizza\\",\\"description\\":\\"bakes a pizza\\",\\"options\\":[{\\"name\\":\\"--filter\\",\\"description\\":\\"For monorepos, specify the name of the application to run the command in\\"},{\\"name\\":\\"--type\\",\\"description\\":\\"Type of pizza\\"},{\\"name\\":\\"--debug\\",\\"description\\":\\"Print debugging information\\"},{\\"name\\":\\"--auth\\",\\"description\\":\\"Netlify auth token - can be used to run this command without logging in\\"}]},\\"taste\\":{\\"name\\":\\"taste\\",\\"description\\":\\"tastes something\\",\\"options\\":[{\\"name\\":\\"--filter\\",\\"description\\":\\"For monorepos, specify the name of the application to run the command in\\"},{\\"name\\":\\"--debug\\",\\"description\\":\\"Print debugging information\\"},{\\"name\\":\\"--auth\\",\\"description\\":\\"Netlify auth token - can be used to run this command without logging in\\"}]}}"`; diff --git a/tests/unit/lib/completion/generate-autocompletion.test.js b/tests/unit/lib/completion/generate-autocompletion.test.ts similarity index 71% rename from tests/unit/lib/completion/generate-autocompletion.test.js rename to tests/unit/lib/completion/generate-autocompletion.test.ts index 17fab22d108..eee478df48e 100644 --- a/tests/unit/lib/completion/generate-autocompletion.test.js +++ b/tests/unit/lib/completion/generate-autocompletion.test.ts @@ -3,10 +3,10 @@ import fs from 'fs' import { Argument } from 'commander' import { describe, expect, test, vi } from 'vitest' -import BaseCommand from '../../../../dist/commands/base-command.js' -import generateAutocompletion from '../../../../dist/lib/completion/generate-autocompletion.js' +import BaseCommand from '../../../../src/commands/base-command.js' +import generateAutocompletion from '../../../../src/lib/completion/generate-autocompletion.js' -const createTestCommand = async () => { +const createTestCommand = () => { const program = new BaseCommand('chef') program @@ -28,9 +28,9 @@ const createTestCommand = async () => { } describe('generateAutocompletion', () => { - test('should generate a completion file', async () => { + test('should generate a completion file', () => { vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {}) - const program = await createTestCommand() + const program = createTestCommand() generateAutocompletion(program) @@ -41,8 +41,8 @@ describe('generateAutocompletion', () => { expect.anything(), 'utf-8', ) - expect(fs.writeFileSync.lastCall).toMatchSnapshot() + expect(vi.mocked(fs.writeFileSync).mock.lastCall?.[1]).toMatchSnapshot() - fs.writeFileSync.mockRestore() + vi.mocked(fs.writeFileSync).mockRestore() }) }) diff --git a/tests/unit/lib/completion/get-autocompletion.test.js b/tests/unit/lib/completion/get-autocompletion.test.ts similarity index 78% rename from tests/unit/lib/completion/get-autocompletion.test.js rename to tests/unit/lib/completion/get-autocompletion.test.ts index 45c80e710df..dfeedfab1a2 100644 --- a/tests/unit/lib/completion/get-autocompletion.test.js +++ b/tests/unit/lib/completion/get-autocompletion.test.ts @@ -1,6 +1,6 @@ import { expect, describe, test } from 'vitest' -import getAutocompletion from '../../../../dist/lib/completion/get-autocompletion.js' +import getAutocompletion from '../../../../src/lib/completion/get-autocompletion.js' const cookingFixtures = { cook: { @@ -21,17 +21,16 @@ const cookingFixtures = { describe('getAutocompletion', () => { test('should not autocomplete anything when completion is turned off', () => { - // @ts-expect-error: Intentionally violating type safety + // @ts-expect-error FIXME(ndhoule): Breaks type safety, I don't think this is a realistic scenario? expect(getAutocompletion({}, cookingFixtures)).toBeUndefined() - // @ts-expect-error: Intentionally violating type safety + // @ts-expect-error FIXME(ndhoule): Breaks type safety, I don't think this is a realistic scenario? expect(getAutocompletion({ complete: false }, cookingFixtures)).toBeUndefined() - // @ts-expect-error: Intentionally violating type safety + // @ts-expect-error FIXME(ndhoule): Breaks type safety, I don't think this is a realistic scenario? expect(getAutocompletion({ complete: false, words: 2 }, cookingFixtures)).toBeUndefined() }) test('should get the correct autocompletion for the base command', () => { - // @ts-expect-error: Intentionally violating type safety - const completion = getAutocompletion({ complete: true, words: 1, lastPartial: '' }, cookingFixtures) + const completion = getAutocompletion({ complete: true, words: 1, lastPartial: '', line: '' }, cookingFixtures) expect(completion).toEqual([ { name: 'cook', description: 'cooking' }, { name: 'bake', description: 'baking' }, @@ -39,14 +38,12 @@ describe('getAutocompletion', () => { }) test('should get the correct autocompletion for the base command if there is already a word', () => { - // @ts-expect-error: Intentionally violating type safety - const completion = getAutocompletion({ complete: true, words: 1, lastPartial: 'ba' }, cookingFixtures) + const completion = getAutocompletion({ complete: true, words: 1, lastPartial: 'ba', line: 'ba' }, cookingFixtures) expect(completion).toEqual([{ name: 'bake', description: 'baking' }]) }) test('should get no flags if the command has no flags', () => { const completion = getAutocompletion( - // @ts-expect-error: Intentionally violating type safety { complete: true, words: 2, lastPartial: '', line: 'netlify cook' }, cookingFixtures, ) @@ -55,7 +52,6 @@ describe('getAutocompletion', () => { test('should get the correct flags for the command', () => { const completion = getAutocompletion( - // @ts-expect-error: Intentionally violating type safety { complete: true, words: 2, lastPartial: '', line: 'netlify bake' }, cookingFixtures, ) @@ -64,7 +60,6 @@ describe('getAutocompletion', () => { test('should get the correct left over flags for the command', () => { const completion = getAutocompletion( - // @ts-expect-error: Intentionally violating type safety { complete: true, words: 3, lastPartial: '', line: 'netlify bake --heat' }, cookingFixtures, ) @@ -76,7 +71,6 @@ describe('getAutocompletion', () => { test('should get no results if the command has no left over flags anymore', () => { const completion = getAutocompletion( - // @ts-expect-error: Intentionally violating type safety { complete: true, words: 4, lastPartial: '', line: 'netlify bake --heat --heat-type --duration' }, cookingFixtures, ) @@ -85,7 +79,6 @@ describe('getAutocompletion', () => { test('should autocomplete flags', () => { const completion = getAutocompletion( - // @ts-expect-error: Intentionally violating type safety { complete: true, words: 4, lastPartial: '--hea', line: 'netlify bake --heat --hea' }, cookingFixtures, ) diff --git a/tests/unit/lib/edge-functions/bootstrap.test.js b/tests/unit/lib/edge-functions/bootstrap.test.ts similarity index 87% rename from tests/unit/lib/edge-functions/bootstrap.test.js rename to tests/unit/lib/edge-functions/bootstrap.test.ts index 057ddcb02ef..493f4395dc1 100644 --- a/tests/unit/lib/edge-functions/bootstrap.test.js +++ b/tests/unit/lib/edge-functions/bootstrap.test.ts @@ -2,7 +2,7 @@ import { env } from 'process' import { describe, expect, test } from 'vitest' -import { getBootstrapURL, FALLBACK_BOOTSTRAP_URL } from '../../../../dist/lib/edge-functions/bootstrap.js' +import { getBootstrapURL, FALLBACK_BOOTSTRAP_URL } from '../../../../src/lib/edge-functions/bootstrap.js' describe('`getBootstrapURL()`', () => { test('Returns the URL in the `NETLIFY_EDGE_BOOTSTRAP` URL, if set', async () => { @@ -27,6 +27,6 @@ describe('`getBootstrapURL()`', () => { const res = await fetch(bootstrapURL) expect(res.status).toBe(200) - expect(res.headers.get('content-type').startsWith('application/typescript')).toBe(true) + expect(res.headers.get('content-type')).toMatch(/^application\/typescript/) }) }) diff --git a/tests/unit/lib/edge-functions/proxy.test.js b/tests/unit/lib/edge-functions/proxy.test.ts similarity index 81% rename from tests/unit/lib/edge-functions/proxy.test.js rename to tests/unit/lib/edge-functions/proxy.test.ts index d5248fafe05..665ada27b2f 100644 --- a/tests/unit/lib/edge-functions/proxy.test.js +++ b/tests/unit/lib/edge-functions/proxy.test.ts @@ -2,13 +2,13 @@ import { Buffer } from 'buffer' import { describe, expect, test } from 'vitest' -import { createSiteInfoHeader } from '../../../../dist/lib/edge-functions/proxy.js' +import { createSiteInfoHeader } from '../../../../src/lib/edge-functions/proxy.js' 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')) + const parsedOutput = JSON.parse(Buffer.from(output, 'base64').toString('utf-8')) as unknown expect(parsedOutput).toEqual(siteInfo) }) @@ -16,7 +16,7 @@ describe('createSiteInfoHeader', () => { 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')) + const parsedOutput = JSON.parse(Buffer.from(output, 'base64').toString('utf-8')) as unknown expect(parsedOutput).toEqual({}) }) diff --git a/tests/unit/lib/exec-fetcher.test.js b/tests/unit/lib/exec-fetcher.test.ts similarity index 76% rename from tests/unit/lib/exec-fetcher.test.js rename to tests/unit/lib/exec-fetcher.test.ts index 180c7a6bc02..7b604800ca8 100644 --- a/tests/unit/lib/exec-fetcher.test.js +++ b/tests/unit/lib/exec-fetcher.test.ts @@ -1,12 +1,12 @@ import process from 'process' import { fetchLatest } from 'gh-release-fetch' -import { afterAll, afterEach, beforeAll, expect, test, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, expect, test, vi, type MockInstance } from 'vitest' -import { fetchLatestVersion, getArch, getExecName } from '../../../dist/lib/exec-fetcher.js' +import { fetchLatestVersion, getArch, getExecName } from '../../../src/lib/exec-fetcher.js' vi.mock('gh-release-fetch', async () => { - const actual = await vi.importActual('gh-release-fetch') + const actual = await vi.importActual('gh-release-fetch') return { ...actual, @@ -14,8 +14,8 @@ vi.mock('gh-release-fetch', async () => { } }) -let processArchSpy -let processPlatformSpy +let processArchSpy: MockInstance<() => typeof process.arch> +let processPlatformSpy: MockInstance<() => typeof process.platform> beforeAll(() => { processArchSpy = vi.spyOn(process, 'arch', 'get') @@ -61,9 +61,10 @@ test(`should not append anything on linux to executable`, () => { }) 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') - fetchLatest.mockReturnValue(Promise.reject({ statusCode: 404 })) + processArchSpy.mockReturnValue('x64') + processPlatformSpy.mockReturnValue('win32') + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + vi.mocked(fetchLatest).mockReturnValue(Promise.reject({ statusCode: 404 })) await expect( fetchLatestVersion({ @@ -78,7 +79,7 @@ test('should test if an error is thrown if the cpu architecture and the os are n 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)) + vi.mocked(fetchLatest).mockReturnValue(Promise.reject(error)) await expect( fetchLatestVersion({ @@ -93,7 +94,8 @@ test('should provide the error if it is not a 404', async () => { test('should map linux x64 to amd64 arch', async () => { processArchSpy.mockReturnValue('x64') processPlatformSpy.mockReturnValue('linux') - fetchLatest.mockReturnValue(Promise.reject({ statusCode: 404 })) + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + vi.mocked(fetchLatest).mockReturnValue(Promise.reject({ statusCode: 404 })) await expect( fetchLatestVersion({ @@ -106,7 +108,7 @@ test('should map linux x64 to amd64 arch', async () => { }) test('should not throw when the request passes', async () => { - fetchLatest.mockReturnValue(Promise.resolve()) + vi.mocked(fetchLatest).mockReturnValue(Promise.resolve(undefined)) await expect( fetchLatestVersion({ diff --git a/tests/unit/lib/functions/netlify-function.test.js b/tests/unit/lib/functions/netlify-function.test.ts similarity index 55% rename from tests/unit/lib/functions/netlify-function.test.js rename to tests/unit/lib/functions/netlify-function.test.ts index f10c731fa45..78172557e81 100644 --- a/tests/unit/lib/functions/netlify-function.test.js +++ b/tests/unit/lib/functions/netlify-function.test.ts @@ -1,16 +1,17 @@ import { expect, test } from 'vitest' -import NetlifyFunction from '../../../../dist/lib/functions/netlify-function.js' +import NetlifyFunction from '../../../../src/lib/functions/netlify-function.js' 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 functionUrl = `http://localhost:${port.toString()}/.netlify/functions/${functionName}` const ntlFunction = new NetlifyFunction({ name: functionName, settings: { functionsPort: port }, + // @ts-expect-error TS(2741) FIXME: Property ''*'' is missing in type '{ "test-functio... Remove this comment to see the full error message config: { functions: { [functionName]: {} } }, }) diff --git a/tests/unit/lib/functions/registry.test.js b/tests/unit/lib/functions/registry.test.ts similarity index 55% rename from tests/unit/lib/functions/registry.test.js rename to tests/unit/lib/functions/registry.test.ts index 010947a0e98..6fce5786ca2 100644 --- a/tests/unit/lib/functions/registry.test.js +++ b/tests/unit/lib/functions/registry.test.ts @@ -4,9 +4,9 @@ import { join } from 'path' import { describe, expect, test, vi } from 'vitest' -import { FunctionsRegistry } from '../../../../dist/lib/functions/registry.js' -import { watchDebounced } from '../../../../dist/utils/command-helpers.js' -import { getFrameworksAPIPaths } from '../../../../dist/utils/frameworks-api.js' +import { FunctionsRegistry } from '../../../../src/lib/functions/registry.js' +import { watchDebounced } from '../../../../src/utils/command-helpers.js' +import { getFrameworksAPIPaths } from '../../../../src/utils/frameworks-api.js' const duplicateFunctions = [ { @@ -36,8 +36,8 @@ const duplicateFunctions = [ }, ] -vi.mock('../../../../dist/utils/command-helpers.js', async () => { - const helpers = await vi.importActual('../../../../dist/utils/command-helpers.js') +vi.mock('../../../../src/utils/command-helpers.js', async () => { + const helpers = await vi.importActual('../../../../src/utils/command-helpers.js') return { ...helpers, @@ -45,68 +45,96 @@ vi.mock('../../../../dist/utils/command-helpers.js', async () => { } }) -test('registry should only pass functions config to zip-it-and-ship-it', async () => { +test('registry should only pass functions config to zip-it-and-ship-it', async (t) => { const projectRoot = '/projectRoot' const frameworksAPIPaths = getFrameworksAPIPaths(projectRoot) const functionsRegistry = new FunctionsRegistry({ frameworksAPIPaths, projectRoot, - config: { functions: { '*': {} }, plugins: ['test'] }, + config: { + functions: { '*': {} }, + // @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'Plugin'. + plugins: ['test'], + }, }) - const prepareDirectoryScanStub = vi.spyOn(FunctionsRegistry, 'prepareDirectoryScan').mockImplementation(() => {}) - const setupDirectoryWatcherStub = vi.spyOn(functionsRegistry, 'setupDirectoryWatcher').mockImplementation(() => {}) + const prepareDirectoryScanStub = vi + .spyOn(FunctionsRegistry, 'prepareDirectoryScan') + .mockImplementation(async () => {}) + const setupDirectoryWatcherStub = vi + .spyOn(functionsRegistry, 'setupDirectoryWatcher') + .mockImplementation(async () => {}) // 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]) + t.onTestFinished(() => { + listFunctionsStub.mockRestore() + setupDirectoryWatcherStub.mockRestore() + prepareDirectoryScanStub.mockRestore() + }) + + await functionsRegistry.scan([ + // @ts-expect-error FIXME(ndhoule): We should not be touching this private member in tests + functionsRegistry.projectRoot, + ]) expect(listFunctionsStub).toHaveBeenCalledOnce() expect(listFunctionsStub).toHaveBeenCalledWith( expect.anything(), - expect.objectContaining({ config: functionsRegistry.config.functions }), + expect.objectContaining({ + // @ts-expect-error FIXME(ndhoule): We should not be touching this private member in tests + config: functionsRegistry.config.functions, + }), ) - - await listFunctionsStub.mockRestore() - await setupDirectoryWatcherStub.mockRestore() - await prepareDirectoryScanStub.mockRestore() }) describe('the registry handles duplicate functions based on extension precedence', () => { - test('where .js takes precedence over .go, and .go over .ts', async () => { + test('where .js takes precedence over .go, and .go over .ts', async (t) => { const projectRoot = await mkdtemp(join(tmpdir(), 'functions-extension-precedence')) const functionsDirectory = join(projectRoot, 'functions') await mkdir(functionsDirectory) - duplicateFunctions.forEach(async (func) => { + for (const func of duplicateFunctions) { if (func.subDir) { const subDir = join(functionsDirectory, func.subDir) await mkdir(subDir) } const file = join(functionsDirectory, func.filename) await writeFile(file, func.content) - }) + } const functionsRegistry = new FunctionsRegistry({ projectRoot, + // @ts-expect-error: Not mocking full config interface config: {}, timeouts: { syncFunctions: 1, backgroundFunctions: 1 }, - settings: { port: 8888 }, + settings: { + // @ts-expect-error TS(2322) FIXME: Type '{ port: number; }' is not assignable to type... Remove this comment to see the full error message + port: 8888, + }, frameworksAPIPaths: getFrameworksAPIPaths(projectRoot), }) - const prepareDirectoryScanStub = vi.spyOn(FunctionsRegistry, 'prepareDirectoryScan').mockImplementation(() => {}) - const setupDirectoryWatcherStub = vi.spyOn(functionsRegistry, 'setupDirectoryWatcher').mockImplementation(() => {}) + const prepareDirectoryScanStub = vi + .spyOn(FunctionsRegistry, 'prepareDirectoryScan') + .mockImplementation(async () => {}) + const setupDirectoryWatcherStub = vi + .spyOn(functionsRegistry, 'setupDirectoryWatcher') + .mockImplementation(async () => {}) + + t.onTestFinished(() => { + setupDirectoryWatcherStub.mockRestore() + prepareDirectoryScanStub.mockRestore() + }) await functionsRegistry.scan([functionsDirectory]) + // @ts-expect-error FIXME(ndhoule): We should not be touching this private member in tests const { functions } = functionsRegistry - expect(functions.get('hello').runtime.name).toBe('js') - expect(functions.get('hello2').runtime.name).toBe('go') - - await setupDirectoryWatcherStub.mockRestore() - await prepareDirectoryScanStub.mockRestore() + expect(functions.get('hello')).toHaveProperty('runtime.name', 'js') + expect(functions.get('hello2')).toHaveProperty('runtime.name', 'go') }) }) test('should add included_files to watcher', async () => { + // @ts-expect-error TS(2345) FIXME: Argument of type '{ frameworksAPIPaths: Record<"co... Remove this comment to see the full error message const registry = new FunctionsRegistry({ frameworksAPIPaths: getFrameworksAPIPaths('/project-root'), }) @@ -122,6 +150,7 @@ test('should add included_files to watcher', async () => { }, } + // @ts-expect-error FIXME(ndhoule): We should not be touching this private member in tests await registry.buildFunctionAndWatchFiles(func) expect(watchDebounced).toHaveBeenCalledOnce() diff --git a/tests/unit/lib/functions/runtimes/go/index.test.ts b/tests/unit/lib/functions/runtimes/go/index.test.ts index 87b2ea8ce60..b90edef5921 100644 --- a/tests/unit/lib/functions/runtimes/go/index.test.ts +++ b/tests/unit/lib/functions/runtimes/go/index.test.ts @@ -1,4 +1,5 @@ import { expect, test, vi } from 'vitest' +import type { ExecaReturnValue } from 'execa' import { runFunctionsProxy } from '../../../../../../src/lib/functions/local-proxy.js' import { invokeFunction } from '../../../../../../src/lib/functions/runtimes/go/index.js' @@ -11,10 +12,12 @@ test.each([ ['multiValueHeaders', { 'X-Multi': ['B', 'C'] }] as const, ['statusCode', 200] as const, ])('should return %s', async (prop, expected) => { - // @ts-expect-error -- TODO(serhalp): Lazy test type. Create a factory and use it here. - vi.mocked(runFunctionsProxy).mockResolvedValue({ stdout: JSON.stringify({ [prop]: expected }) }) + vi.mocked(runFunctionsProxy).mockResolvedValue( + // This mock doesn't implement the full execa return value API, just the part put under test + { stdout: JSON.stringify({ [prop]: expected }) } as ExecaReturnValue, + ) - // @ts-expect-error -- TODO(serhalp): Lazy test type. Create a factory and use it here. + // @ts-expect-error TS(2740) FIXME: Type '{ mainFile: string; buildData: { binaryPath:... Remove this comment to see the full error message const match = await invokeFunction({ func: { mainFile: '', buildData: { binaryPath: 'foo' } } }) expect(match[prop]).toEqual(expected) }) diff --git a/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.js b/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.ts similarity index 61% rename from tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.js rename to tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.ts index 2a4018b6885..24b495b7100 100644 --- a/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.js +++ b/tests/unit/lib/functions/runtimes/js/builders/netlify-lambda.test.ts @@ -1,12 +1,13 @@ import { expect, test, vi } from 'vitest' -import { detectNetlifyLambda } from '../../../../../../../dist/lib/functions/runtimes/js/builders/netlify-lambda.js' +import { detectNetlifyLambda } from '../../../../../../../src/lib/functions/runtimes/js/builders/netlify-lambda.js' test(`should not match if netlify-lambda is missing from dependencies`, async () => { const packageJson = { dependencies: {}, devDependencies: {}, } + // @ts-expect-error TS(2739) FIXME: Type '{ dependencies: {}; devDependencies: {}; }' ... Remove this comment to see the full error message expect(await detectNetlifyLambda({ packageJson })).toBe(false) }) @@ -23,6 +24,7 @@ test('should not match if netlify-lambda is missing functions directory with arg const spyConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + // @ts-expect-error TS(2739) FIXME: Type '{ scripts: { 'some-build-step': string; }; d... Remove this comment to see the full error message expect(await detectNetlifyLambda({ packageJson })).toBe(false) // Not checking for exact warning string as it would make this test too specific/brittle @@ -44,6 +46,7 @@ test('should not match if netlify-lambda is missing functions directory', async const spyConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + // @ts-expect-error TS(2739) FIXME: Type '{ scripts: { 'some-build-step': string; }; d... Remove this comment to see the full error message expect(await detectNetlifyLambda({ packageJson })).toBe(false) // Not checking for exact warning string as it would make this test too specific/brittle @@ -65,6 +68,7 @@ test('should not match if netlify-lambda contains multiple function directories' const spyConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + // @ts-expect-error TS(2739) FIXME: Type '{ scripts: { 'some-build-step': string; }; d... Remove this comment to see the full error message expect(await detectNetlifyLambda({ packageJson })).toBe(false) // Not checking for exact warning string as it would make this test too specific/brittle @@ -84,9 +88,12 @@ test(`should match if netlify-lambda is listed in dependencies and is mentioned devDependencies: {}, } + // @ts-expect-error TS(2739) FIXME: Type '{ scripts: { build: string; }; dependencies:... Remove this comment to see the full error message const match = await detectNetlifyLambda({ packageJson }) + // @ts-expect-error TS(2339) FIXME: Property 'builderName' does not exist on type 'fal... Remove this comment to see the full error message expect(match.builderName).toBe('netlify-lambda') + // @ts-expect-error TS(2339) FIXME: Property 'npmScript' does not exist on type 'false... Remove this comment to see the full error message expect(match.npmScript).toBe('build') }) @@ -101,9 +108,12 @@ test(`should match if netlify-lambda is listed in devDependencies and is mention }, } + // @ts-expect-error TS(2739) FIXME: Type '{ scripts: { build: string; }; dependencies:... Remove this comment to see the full error message const match = await detectNetlifyLambda({ packageJson }) + // @ts-expect-error TS(2339) FIXME: Property 'builderName' does not exist on type 'fal... Remove this comment to see the full error message expect(match.builderName).toBe('netlify-lambda') + // @ts-expect-error TS(2339) FIXME: Property 'npmScript' does not exist on type 'false... Remove this comment to see the full error message expect(match.npmScript).toBe('build') }) @@ -118,9 +128,12 @@ test(`should match if netlify-lambda is configured with an additional option`, a }, } + // @ts-expect-error TS(2739) FIXME: Type '{ scripts: { build: string; }; dependencies:... Remove this comment to see the full error message const match = await detectNetlifyLambda({ packageJson }) + // @ts-expect-error TS(2339) FIXME: Property 'builderName' does not exist on type 'fal... Remove this comment to see the full error message expect(match.builderName).toBe('netlify-lambda') + // @ts-expect-error TS(2339) FIXME: Property 'npmScript' does not exist on type 'false... Remove this comment to see the full error message expect(match.npmScript).toBe('build') }) @@ -135,9 +148,12 @@ test(`should match if netlify-lambda is configured with multiple additional opti }, } + // @ts-expect-error TS(2739) FIXME: Type '{ scripts: { build: string; }; dependencies:... Remove this comment to see the full error message const match = await detectNetlifyLambda({ packageJson }) + // @ts-expect-error TS(2339) FIXME: Property 'builderName' does not exist on type 'fal... Remove this comment to see the full error message expect(match.builderName).toBe('netlify-lambda') + // @ts-expect-error TS(2339) FIXME: Property 'npmScript' does not exist on type 'false... Remove this comment to see the full error message expect(match.npmScript).toBe('build') }) @@ -152,9 +168,12 @@ test('should match if netlify-lambda has options that are passed after the funct }, } + // @ts-expect-error TS(2739) FIXME: Type '{ scripts: { build: string; }; dependencies:... Remove this comment to see the full error message const match = await detectNetlifyLambda({ packageJson }) + // @ts-expect-error TS(2339) FIXME: Property 'builderName' does not exist on type 'fal... Remove this comment to see the full error message expect(match.builderName).toBe('netlify-lambda') + // @ts-expect-error TS(2339) FIXME: Property 'npmScript' does not exist on type 'false... Remove this comment to see the full error message expect(match.npmScript).toBe('build') }) @@ -170,8 +189,11 @@ test('should match even if multiple netlify-lambda commands are specified', asyn }, } + // @ts-expect-error TS(2739) FIXME: Type '{ scripts: { 'some-serve-step': string; buil... Remove this comment to see the full error message const match = await detectNetlifyLambda({ packageJson }) + // @ts-expect-error TS(2339) FIXME: Property 'builderName' does not exist on type 'fal... Remove this comment to see the full error message expect(match.builderName).toBe('netlify-lambda') + // @ts-expect-error TS(2339) FIXME: Property 'npmScript' does not exist on type 'false... Remove this comment to see the full error message expect(match.npmScript).toBe('build') }) diff --git a/tests/unit/lib/functions/runtimes/rust/index.test.ts b/tests/unit/lib/functions/runtimes/rust/index.test.ts index 00b2ffe8bb3..39600f15c56 100644 --- a/tests/unit/lib/functions/runtimes/rust/index.test.ts +++ b/tests/unit/lib/functions/runtimes/rust/index.test.ts @@ -11,10 +11,12 @@ test.each([ ['multiValueHeaders', { 'X-Multi': ['B', 'C'] }] as const, ['statusCode', 200] as const, ])('should return %s', async (prop, expected) => { - // @ts-expect-error -- TODO(serhalp): Lazy test type. Create a factory and use it here. - vi.mocked(runFunctionsProxy).mockResolvedValue({ stdout: JSON.stringify({ [prop]: expected }) }) + vi.mocked(runFunctionsProxy).mockResolvedValue( + // @ts-expect-error(ndhoule): Intentionally not mocking entire execa API surface + { stdout: JSON.stringify({ [prop]: expected }) }, + ) - // @ts-expect-error -- TODO(serhalp): Lazy test type. Create a factory and use it here. + // @ts-expect-error TS(2740) FIXME: Type '{ mainFile: string; buildData: { binaryPath:... Remove this comment to see the full error message const match = await invokeFunction({ func: { mainFile: '', buildData: { binaryPath: 'foo' } } }) expect(match[prop]).toEqual(expected) }) diff --git a/tests/unit/lib/functions/scheduled.test.js b/tests/unit/lib/functions/scheduled.test.ts similarity index 84% rename from tests/unit/lib/functions/scheduled.test.js rename to tests/unit/lib/functions/scheduled.test.ts index f49af2bff8c..d2728aab23f 100644 --- a/tests/unit/lib/functions/scheduled.test.js +++ b/tests/unit/lib/functions/scheduled.test.ts @@ -1,11 +1,11 @@ import { expect, describe, test } from 'vitest' -import { buildHelpResponse } from '../../../../dist/lib/functions/scheduled.js' -import { CLOCKWORK_USERAGENT } from '../../../../dist/utils/functions/constants.js' +import { buildHelpResponse } from '../../../../src/lib/functions/scheduled.js' +import { CLOCKWORK_USERAGENT } from '../../../../src/utils/functions/constants.js' -const withAccept = (accept) => +const withAccept = (accept: string) => buildHelpResponse({ - error: undefined, + error: null, headers: { accept, }, @@ -15,7 +15,7 @@ const withAccept = (accept) => }, }) -const withUserAgent = (userAgent) => +const withUserAgent = (userAgent: string) => buildHelpResponse({ error: new Error('test'), headers: { diff --git a/tests/unit/lib/functions/server.test.js b/tests/unit/lib/functions/server.test.ts similarity index 65% rename from tests/unit/lib/functions/server.test.js rename to tests/unit/lib/functions/server.test.ts index 29a9b9b0d14..251a8fdc7d4 100644 --- a/tests/unit/lib/functions/server.test.js +++ b/tests/unit/lib/functions/server.test.ts @@ -1,24 +1,26 @@ -import { mkdir, mkdtemp, writeFile } from 'fs/promises' -import { tmpdir } from 'os' -import { join } from 'path' +import http from 'node:http' +import net from 'node:net' +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import express from 'express' import fetch from 'node-fetch' import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' -import { FunctionsRegistry } from '../../../../dist/lib/functions/registry.js' -import { createHandler } from '../../../../dist/lib/functions/server.js' -import { getFrameworksAPIPaths } from '../../../../dist/utils/frameworks-api.js' -import StateConfig from '../../../../dist/utils/cli-state.js' +import { FunctionsRegistry } from '../../../../src/lib/functions/registry.js' +import { createHandler } from '../../../../src/lib/functions/server.js' +import { getFrameworksAPIPaths } from '../../../../src/utils/frameworks-api.js' +import StateConfig from '../../../../src/utils/cli-state.js' -vi.mock('../../../../dist/utils/command-helpers.js', async () => ({ - ...(await vi.importActual('../../../../dist/utils/command-helpers.js')), +vi.mock('../../../../src/utils/command-helpers.js', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.js')), log: () => {}, })) describe('createHandler', () => { - let server - let serverAddress + let server: http.Server + let serverAddress: string beforeAll(async () => { const projectRoot = await mkdtemp(join(tmpdir(), 'functions-server-project-root')) const functionsDirectory = join(projectRoot, 'functions') @@ -29,9 +31,10 @@ describe('createHandler', () => { const functionsRegistry = new FunctionsRegistry({ projectRoot, + // @ts-expect-error TS(2322) FIXME: Type '{}' is not assignable to type 'NormalizedCac... Remove this comment to see the full error message config: {}, timeouts: { syncFunctions: 1, backgroundFunctions: 1 }, - settings: { port: 8888 }, + settings: { functionsPort: 8888 }, frameworksAPIPaths: getFrameworksAPIPaths(projectRoot), }) await functionsRegistry.scan([functionsDirectory]) @@ -40,23 +43,23 @@ describe('createHandler', () => { // TODO(serhalp): Lazy test type. Create a config factory and use it here. app.all( '*', + // @ts-expect-error TS(2741) FIXME: Property 'processing' is missing in type '{}' but ... Remove this comment to see the full error message createHandler({ functionsRegistry, config: { dev: {} }, geo: 'mock', state: new StateConfig(projectRoot) }), ) return await new Promise((resolve) => { server = app.listen(resolve) - const { port } = server.address() + const { port } = server.address() as net.AddressInfo - serverAddress = `http://localhost:${port}` + serverAddress = `http://localhost:${port.toString()}` }) }) - afterAll( - async () => - await new Promise((resolve) => { - server.close(resolve) - }), - ) + afterAll(async () => { + await new Promise((resolve) => { + server.close(resolve) + }) + }) test('should get the url as the `rawUrl` inside the function', async () => { const response = await fetch(new URL('/.netlify/functions/hello', serverAddress)) diff --git a/tests/unit/lib/geo-location.test.js b/tests/unit/lib/geo-location.test.ts similarity index 92% rename from tests/unit/lib/geo-location.test.js rename to tests/unit/lib/geo-location.test.ts index 4079b731e39..66a2a7505dc 100644 --- a/tests/unit/lib/geo-location.test.js +++ b/tests/unit/lib/geo-location.test.ts @@ -1,7 +1,7 @@ import nock from 'nock' import { describe, expect, test } from 'vitest' -import { getGeoLocation, mockLocation } from '../../../dist/lib/geo-location.js' +import { type Geolocation, getGeoLocation, mockLocation } from '../../../src/lib/geo-location.js' describe('getGeoLocation', () => { test('returns geolocation data from the API if `mode: "cache"`', async () => { @@ -14,12 +14,12 @@ describe('getGeoLocation', () => { } const mockState = { get() {}, - set(key, value) { + set(key: string, value: { data: Geolocation; timestamp: number }) { hasCalledStateSet = true expect(key).toBe('geolocation') - expect(value.data).toEqual(testLocation) - expect(typeof value.timestamp).toBe('number') + expect(value).toHaveProperty('data', testLocation) + expect(value).toHaveProperty('timestamp', expect.any(Number)) }, } const mockRequest = nock('https://netlifind.netlify.app').get('/').reply(200, { @@ -41,7 +41,7 @@ describe('getGeoLocation', () => { subdivision: { code: 'JS', name: 'Jamstack' }, } const mockState = { - get(key) { + get(key: string) { expect(key).toBe('geolocation') return { @@ -72,7 +72,7 @@ describe('getGeoLocation', () => { subdivision: { code: 'JS', name: 'Jamstack' }, } const mockState = { - get(key) { + get(key: string) { expect(key).toBe('geolocation') return { diff --git a/tests/unit/lib/http-agent.test.js b/tests/unit/lib/http-agent.test.ts similarity index 65% rename from tests/unit/lib/http-agent.test.js rename to tests/unit/lib/http-agent.test.ts index d884172533c..fd925032672 100644 --- a/tests/unit/lib/http-agent.test.js +++ b/tests/unit/lib/http-agent.test.ts @@ -1,10 +1,11 @@ -import http from 'http' +import net from 'node:net' +import http from 'node:http' import ProxyServer from 'http-proxy' import { HttpsProxyAgent } from 'https-proxy-agent' import { describe, expect, test } from 'vitest' -import { tryGetAgent } from '../../../dist/lib/http-agent.js' +import { tryGetAgent } from '../../../src/lib/http-agent.js' describe('tryGetAgent', () => { test(`should return an empty object when there is no httpProxy`, async () => { @@ -15,21 +16,21 @@ describe('tryGetAgent', () => { const httpProxy = 'invalid_url' const result = await tryGetAgent({ httpProxy }) - expect(result.error).toBeDefined() + expect(result).toHaveProperty('error', expect.any(String)) }) test(`should return error when scheme is not http or https`, async () => { const httpProxy = 'file://localhost' const result = await tryGetAgent({ httpProxy }) - expect(result.error).toBeDefined() + expect(result).toHaveProperty('error', expect.any(String)) }) test(`should return error when proxy is not available`, async () => { const httpProxy = 'https://unknown:7979' const result = await tryGetAgent({ httpProxy }) - expect(result.error).toBeDefined() + expect(result).toHaveProperty('error', expect.any(String)) }) test(`should return agent for a valid proxy`, async () => { @@ -38,13 +39,18 @@ describe('tryGetAgent', () => { proxy.web(req, res, { target: 'http://localhost:5555' }) }) - await new Promise((resolve) => { - server.listen({ port: 0, hostname: 'localhost' }, resolve) + await new Promise((resolve) => { + server.listen({ port: 0, hostname: 'localhost' }, () => { + resolve() + }) }) - const httpProxyUrl = `http://localhost:${server.address().port}` + const httpProxyUrl = `http://localhost:${(server.address() as net.AddressInfo).port.toString()}` const result = await tryGetAgent({ httpProxy: httpProxyUrl }) + if (!('agent' in result)) { + throw new Error('expected result to include agent') + } expect(result.agent).toBeInstanceOf(HttpsProxyAgent) server.close() diff --git a/tests/unit/lib/images/proxy.test.js b/tests/unit/lib/images/proxy.test.ts similarity index 84% rename from tests/unit/lib/images/proxy.test.js rename to tests/unit/lib/images/proxy.test.ts index e4be93f5597..9cc0a9e7abd 100644 --- a/tests/unit/lib/images/proxy.test.js +++ b/tests/unit/lib/images/proxy.test.ts @@ -1,6 +1,6 @@ import { expect, test } from 'vitest' -import { parseAllRemoteImages, transformImageParams } from '../../../../dist/lib/images/proxy.js' +import { parseAllRemoteImages, transformImageParams } from '../../../../src/lib/images/proxy.js' test('should parse all remote images correctly', () => { const config = { @@ -8,6 +8,7 @@ test('should parse all remote images correctly', () => { remote_images: ['https://example.com/*', 'https://test.com/*'], }, } + // @ts-expect-error TS(2345) FIXME: Argument of type '{ images: { remote_images: strin... Remove this comment to see the full error message const { errors, remotePatterns } = parseAllRemoteImages(config) expect(errors).toEqual([]) expect(remotePatterns).toEqual([/https:\/\/example.com\/*/, /https:\/\/test.com\/*/]) @@ -19,6 +20,7 @@ test('should report invalid remote images', () => { remote_images: ['*'], }, } + // @ts-expect-error TS(2345) FIXME: Argument of type '{ images: { remote_images: strin... Remove this comment to see the full error message const { errors, remotePatterns } = parseAllRemoteImages(config) expect(errors).toEqual([ { diff --git a/tests/unit/utils/command-helpers.test.js b/tests/unit/utils/command-helpers.test.ts similarity index 59% rename from tests/unit/utils/command-helpers.test.js rename to tests/unit/utils/command-helpers.test.ts index 7e958c30194..7e367db8141 100644 --- a/tests/unit/utils/command-helpers.test.js +++ b/tests/unit/utils/command-helpers.test.ts @@ -1,17 +1,19 @@ import { describe, expect, test } from 'vitest' -import { normalizeConfig } from '../../../dist/utils/command-helpers.js' +import { normalizeConfig } from '../../../src/utils/command-helpers.js' describe('normalizeConfig', () => { test('should remove publish and publishOrigin property if publishOrigin is "default"', () => { const config = { build: { publish: 'a', publishOrigin: 'default' } } + // @ts-expect-error TS(2345) FIXME: Argument of type '{ build: { publish: string; publ... Remove this comment to see the full error message expect(normalizeConfig(config)).toEqual({ build: {} }) }) test('should return same config object if publishOrigin is not "default"', () => { const config = { build: { publish: 'a', publishOrigin: 'b' } } + // @ts-expect-error TS(2345) FIXME: Argument of type '{ build: { publish: string; publ... Remove this comment to see the full error message expect(normalizeConfig(config)).toBe(config) }) }) diff --git a/tests/unit/utils/copy-template-dir/copy-template-dir.test.js b/tests/unit/utils/copy-template-dir/copy-template-dir.test.ts similarity index 86% rename from tests/unit/utils/copy-template-dir/copy-template-dir.test.js rename to tests/unit/utils/copy-template-dir/copy-template-dir.test.ts index 287e9a6d80b..9d4a1056167 100644 --- a/tests/unit/utils/copy-template-dir/copy-template-dir.test.js +++ b/tests/unit/utils/copy-template-dir/copy-template-dir.test.ts @@ -5,20 +5,14 @@ import { fileURLToPath } from 'url' import { readdirpPromise } from 'readdirp' import { describe, expect, test } from 'vitest' -import { copyTemplateDir } from '../../../../dist/utils/copy-template-dir/copy-template-dir.js' +import { copyTemplateDir } from '../../../../src/utils/copy-template-dir/copy-template-dir.js' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) describe('copyTemplateDir', () => { - test('should assert input values', async () => { - await expect(copyTemplateDir()).rejects.toThrow(/string/) - await expect(copyTemplateDir('foo')).rejects.toThrow(/string/) - await expect(copyTemplateDir('foo', 'bar', 'err')).rejects.toThrow(/object/) - }) - test('should write a bunch of files', async () => { - const checkCreatedFileNames = (names) => { + const checkCreatedFileNames = (names: string[]) => { expect(names).toContain('.a') expect(names).toContain('c') expect(names).toContain('1.txt') diff --git a/tests/unit/utils/deploy/hash-files.test.js b/tests/unit/utils/deploy/hash-files.test.ts similarity index 82% rename from tests/unit/utils/deploy/hash-files.test.js rename to tests/unit/utils/deploy/hash-files.test.ts index 77eb01a9ddb..0382e9c891b 100644 --- a/tests/unit/utils/deploy/hash-files.test.js +++ b/tests/unit/utils/deploy/hash-files.test.ts @@ -1,8 +1,8 @@ import { expect, test } from 'vitest' -import { DEFAULT_CONCURRENT_HASH } from '../../../../dist/utils/deploy/constants.js' -import hashFiles from '../../../../dist/utils/deploy/hash-files.js' -import { withSiteBuilder } from '../../../integration/utils/site-builder.ts' +import { DEFAULT_CONCURRENT_HASH } from '../../../../src/utils/deploy/constants.js' +import hashFiles from '../../../../src/utils/deploy/hash-files.js' +import { withSiteBuilder } from '../../../integration/utils/site-builder.js' test('Hashes files in a folder', async (t) => { await withSiteBuilder(t, async (builder) => { @@ -31,6 +31,7 @@ test('Hashes files in a folder', async (t) => { const fileObjArray = filesShaMap[sha] fileObjArray.forEach((fileObj) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(fileObj.normalizedPath).toBe(filePath) }) }) diff --git a/tests/unit/utils/deploy/hash-fns.test.js b/tests/unit/utils/deploy/hash-fns.test.ts similarity index 55% rename from tests/unit/utils/deploy/hash-fns.test.js rename to tests/unit/utils/deploy/hash-fns.test.ts index 268d75c7413..16a4669588a 100644 --- a/tests/unit/utils/deploy/hash-fns.test.js +++ b/tests/unit/utils/deploy/hash-fns.test.ts @@ -1,10 +1,12 @@ +import path from 'node:path' + import { temporaryDirectory } from 'tempy' import { expect, test } from 'vitest' -import BaseCommand from '../../../../dist/commands/base-command.js' -import { DEFAULT_CONCURRENT_HASH } from '../../../../dist/utils/deploy/constants.js' -import hashFns from '../../../../dist/utils/deploy/hash-fns.js' -import { withSiteBuilder } from '../../../integration/utils/site-builder.ts' +import BaseCommand from '../../../../src/commands/base-command.js' +import { DEFAULT_CONCURRENT_HASH } from '../../../../src/utils/deploy/constants.js' +import hashFns from '../../../../src/utils/deploy/hash-fns.js' +import { withSiteBuilder } from '../../../integration/utils/site-builder.js' test('Hashes files in a folder', async (t) => { await withSiteBuilder(t, async (builder) => { @@ -12,30 +14,33 @@ test('Hashes files in a folder', async (t) => { .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) .withFunction({ path: 'hello.js', - handler: async () => ({ statusCode: 200, body: 'Hello' }), + handler: async () => Promise.resolve({ statusCode: 200, body: 'Hello' }), }) .withFunction({ path: 'goodbye.js', - handler: async () => ({ statusCode: 200, body: 'Goodbye' }), + handler: async () => Promise.resolve({ statusCode: 200, body: 'Goodbye' }), }) .build() const expectedFunctions = ['hello', 'goodbye'] - const { fnShaMap, functions } = await hashFns(new BaseCommand(), `${builder.directory}/functions`, { + const { fnShaMap, functions } = await hashFns(new BaseCommand(), [path.join(builder.directory, 'functions')], { tmpDir: temporaryDirectory(), concurrentHash: DEFAULT_CONCURRENT_HASH, statusCb() {}, }) expect(Object.entries(functions)).toHaveLength(expectedFunctions.length) - expect(Object.entries(fnShaMap)).toHaveLength(expectedFunctions.length) + expect(Object.entries(fnShaMap ?? {})).toHaveLength(expectedFunctions.length) expectedFunctions.forEach((functionPath) => { const sha = functions[functionPath] expect(sha).toBeDefined() - const functionsObjArray = fnShaMap[sha] + expect(fnShaMap).toBeDefined() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- FIXME + const functionsObjArray = fnShaMap![sha] functionsObjArray.forEach((fileObj) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- FIXME expect(fileObj.normalizedPath).toBe(functionPath) }) }) diff --git a/tests/unit/utils/deploy/upload-files.test.js b/tests/unit/utils/deploy/upload-files.test.ts similarity index 81% rename from tests/unit/utils/deploy/upload-files.test.js rename to tests/unit/utils/deploy/upload-files.test.ts index 2af9dad59f1..0e71da25257 100644 --- a/tests/unit/utils/deploy/upload-files.test.js +++ b/tests/unit/utils/deploy/upload-files.test.ts @@ -1,10 +1,10 @@ import { v4 as generateUUID } from 'uuid' import { afterAll, expect, test, vi } from 'vitest' -import uploadFiles from '../../../../dist/utils/deploy/upload-files.js' +import uploadFiles from '../../../../src/utils/deploy/upload-files.js' -vi.mock('../../../../dist/utils/deploy/constants.js', async () => { - const actual = await vi.importActual('../../../../dist/utils/deploy/constants.js') +vi.mock('../../../../src/utils/deploy/constants.js', async () => { + const actual = await vi.importActual('../../../../src/utils/deploy/constants.js') // Reduce the delay, so these tests do not wait for 10 seconds return { ...actual, UPLOAD_INITIAL_DELAY: 100, UPLOAD_MAX_DELAY: 200 } @@ -18,11 +18,12 @@ test('Adds a retry count to function upload requests', async () => { const uploadDeployFunction = vi.fn() const mockError = new Error('Uh-oh') + // @ts-expect-error TS(2339) FIXME: Property 'status' does not exist on type 'Error'. mockError.status = 500 uploadDeployFunction.mockRejectedValueOnce(mockError) uploadDeployFunction.mockRejectedValueOnce(mockError) - uploadDeployFunction.mockResolvedValueOnce() + uploadDeployFunction.mockResolvedValueOnce(undefined) const mockApi = { uploadDeployFunction, @@ -54,6 +55,7 @@ test('Does not retry on 400 response from function upload requests', async () => const uploadDeployFunction = vi.fn() const mockError = new Error('Uh-oh') + // @ts-expect-error TS(2339) FIXME: Property 'status' does not exist on type 'Error'. mockError.status = 400 uploadDeployFunction.mockRejectedValue(mockError) diff --git a/tests/unit/utils/deploy/util.test.js b/tests/unit/utils/deploy/util.test.ts similarity index 89% rename from tests/unit/utils/deploy/util.test.js rename to tests/unit/utils/deploy/util.test.ts index b0d1393cde0..aae7e3e36de 100644 --- a/tests/unit/utils/deploy/util.test.js +++ b/tests/unit/utils/deploy/util.test.ts @@ -2,7 +2,7 @@ import { join } from 'path' import { describe, expect, test } from 'vitest' -import { normalizePath } from '../../../../dist/utils/deploy/util.js' +import { normalizePath } from '../../../../src/utils/deploy/util.js' describe('normalizePath', () => { test('normalizes relative file paths', () => { diff --git a/tests/unit/utils/dot-env.test.js b/tests/unit/utils/dot-env.test.ts similarity index 98% rename from tests/unit/utils/dot-env.test.js rename to tests/unit/utils/dot-env.test.ts index ee1181bfeee..cb6006ad835 100644 --- a/tests/unit/utils/dot-env.test.js +++ b/tests/unit/utils/dot-env.test.ts @@ -2,8 +2,8 @@ import process from 'process' import { expect, test } from 'vitest' -import { tryLoadDotEnvFiles } from '../../../dist/utils/dot-env.js' -import { withSiteBuilder } from '../../integration/utils/site-builder.ts' +import { tryLoadDotEnvFiles } from '../../../src/utils/dot-env.js' +import { withSiteBuilder } from '../../integration/utils/site-builder.js' test('should return an empty array for a site with no .env file', async (t) => { await withSiteBuilder(t, async (builder) => { diff --git a/tests/unit/utils/env/index.test.js b/tests/unit/utils/env/index.test.ts similarity index 94% rename from tests/unit/utils/env/index.test.js rename to tests/unit/utils/env/index.test.ts index 24a382458ec..8f0d83bef41 100644 --- a/tests/unit/utils/env/index.test.js +++ b/tests/unit/utils/env/index.test.ts @@ -8,37 +8,37 @@ import { normalizeContext, translateFromEnvelopeToMongo, translateFromMongoToEnvelope, -} from '../../../../dist/utils/env/index.js' +} from '../../../../src/utils/env/index.js' test('should find a value from a given context', () => { const values = [ { - context: 'production', + context: 'production' as const, value: 'foo', }, { - context: 'dev', + context: 'dev' as const, value: 'bar', }, ] - const { value } = findValueInValues(values, 'dev') - expect(value).toBe('bar') + const result = findValueInValues(values, 'dev') + expect(result).toHaveProperty('value', 'bar') }) test('should find a value from a given branch', () => { const values = [ { - context: 'branch', + context: 'branch-deploy' as const, context_parameter: 'staging', value: 'foo', }, { - context: 'dev', + context: 'dev' as const, value: 'bar', }, ] - const { value } = findValueInValues(values, 'staging') - expect(value).toBe('foo') + const result = findValueInValues(values, 'staging') + expect(result).toHaveProperty('value', 'foo') }) test('should filter an env from a given source', () => { diff --git a/tests/unit/utils/feature-flags.test.js b/tests/unit/utils/feature-flags.test.ts similarity index 63% rename from tests/unit/utils/feature-flags.test.js rename to tests/unit/utils/feature-flags.test.ts index 61c3f0115e3..a52dbe05d33 100644 --- a/tests/unit/utils/feature-flags.test.js +++ b/tests/unit/utils/feature-flags.test.ts @@ -1,9 +1,9 @@ import { describe, expect, test } from 'vitest' -import { isFeatureFlagEnabled } from '../../../dist/utils/feature-flags.js' +import { isFeatureFlagEnabled } from '../../../src/utils/feature-flags.js' describe('isFeatureFlagEnabled', () => { - test('should return true if feature flag is not present', async () => { + test('should return true if feature flag is not present', () => { const siteInfo = { feature_flags: { cool_new_feature: true, @@ -16,7 +16,7 @@ describe('isFeatureFlagEnabled', () => { expect(result).toBe(true) }) - test('should return true if feature flag is true', async () => { + test('should return true if feature flag is true', () => { const siteInfo = { feature_flags: { cool_new_feature: true, @@ -29,7 +29,7 @@ describe('isFeatureFlagEnabled', () => { expect(result).toBe(true) }) - test('should return true if feature flag is a string', async () => { + test('should return true if feature flag is a string', () => { const siteInfo = { feature_flags: { cool_new_feature: 'my string', @@ -42,7 +42,7 @@ describe('isFeatureFlagEnabled', () => { expect(result).toBe(true) }) - test('should return true if feature flag is a number', async () => { + test('should return true if feature flag is a number', () => { const siteInfo = { feature_flags: { cool_new_feature: 42, @@ -50,12 +50,16 @@ describe('isFeatureFlagEnabled', () => { }, } - const result = isFeatureFlagEnabled('cool_new_feature', siteInfo) + const result = isFeatureFlagEnabled( + 'cool_new_feature', + // @ts-expect-error: Intentionally breaking type contract + siteInfo, + ) expect(result).toBe(true) }) - test('should return true if feature flag is an object', async () => { + test('should return true if feature flag is an object', () => { const siteInfo = { feature_flags: { cool_new_feature: { key: 'value' }, @@ -63,12 +67,16 @@ describe('isFeatureFlagEnabled', () => { }, } - const result = isFeatureFlagEnabled('cool_new_feature', siteInfo) + const result = isFeatureFlagEnabled( + 'cool_new_feature', + // @ts-expect-error: Intentionally breaking type contract + siteInfo, + ) expect(result).toBe(true) }) - test('should return false if feature flag is false', async () => { + test('should return false if feature flag is false', () => { const siteInfo = { feature_flags: { cool_new_feature: true, diff --git a/tests/unit/utils/functions/get-functions.test.js b/tests/unit/utils/functions/get-functions.test.ts similarity index 96% rename from tests/unit/utils/functions/get-functions.test.js rename to tests/unit/utils/functions/get-functions.test.ts index 2c2a229ce71..e1db3e9e5f7 100644 --- a/tests/unit/utils/functions/get-functions.test.js +++ b/tests/unit/utils/functions/get-functions.test.ts @@ -2,8 +2,8 @@ import path from 'path' import { describe, expect, test } from 'vitest' -import { getFunctions } from '../../../../dist/utils/functions/get-functions.js' -import { withSiteBuilder } from '../../../integration/utils/site-builder.ts' +import { getFunctions } from '../../../../src/utils/functions/get-functions.js' +import { withSiteBuilder } from '../../../integration/utils/site-builder.js' describe('getFunctions', () => { test('should return empty object when an empty string is provided', async () => { diff --git a/tests/unit/utils/gh-auth.test.js b/tests/unit/utils/gh-auth.test.ts similarity index 70% rename from tests/unit/utils/gh-auth.test.js rename to tests/unit/utils/gh-auth.test.ts index fe5cd2f4b5b..1186308ee8a 100644 --- a/tests/unit/utils/gh-auth.test.js +++ b/tests/unit/utils/gh-auth.test.ts @@ -2,12 +2,12 @@ import { fibonacci } from 'backoff' import fetch from 'node-fetch' import { afterAll, describe, expect, test, vi } from 'vitest' -import { authWithNetlify } from '../../../dist/utils/gh-auth.js' -import importedOpenBrowser from '../../../dist/utils/open-browser.js' +import { authWithNetlify } from '../../../src/utils/gh-auth.js' +import importedOpenBrowser from '../../../src/utils/open-browser.js' const openBrowser = vi.mocked(importedOpenBrowser) -vi.mock('../../../dist/utils/open-browser.js', () => ({ +vi.mock('../../../src/utils/open-browser.js', () => ({ default: vi.fn(() => Promise.resolve()), })) @@ -19,9 +19,15 @@ describe('gh-auth', () => { test('should check if the authWithNetlify is working', async () => { const promise = authWithNetlify() // wait for server to be started - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const fibonacciBackoff = fibonacci() - const check = () => (openBrowser.mock.calls.length === 0 ? fibonacciBackoff.backoff() : resolve()) + const check = () => { + if (openBrowser.mock.calls.length === 0) { + fibonacciBackoff.backoff() + } else { + resolve() + } + } fibonacciBackoff.failAfter(10) fibonacciBackoff.on('ready', check) @@ -40,7 +46,7 @@ describe('gh-auth', () => { const host = calledParams.get('host') // perform a request like the redirect from the Web ui - await fetch(new URL(`?${params.toString()}`, host)) + await fetch(new URL(`?${params.toString()}`, host ?? undefined)) const result = await promise expect(result).toEqual({ diff --git a/tests/unit/utils/headers.test.js b/tests/unit/utils/headers.test.ts similarity index 68% rename from tests/unit/utils/headers.test.js rename to tests/unit/utils/headers.test.ts index b08ab2f4003..abfda6e346e 100644 --- a/tests/unit/utils/headers.test.js +++ b/tests/unit/utils/headers.test.ts @@ -1,12 +1,12 @@ import { resolve } from 'path' -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { describe, expect, it as baseIt, vi } from 'vitest' -import { headersForPath, parseHeaders } from '../../../dist/utils/headers.js' -import { createSiteBuilder } from '../../integration/utils/site-builder.ts' +import { headersForPath, parseHeaders } from '../../../src/utils/headers.js' +import { createSiteBuilder, type SiteBuilder } from '../../integration/utils/site-builder.js' -vi.mock('../../../dist/utils/command-helpers.js', async () => ({ - ...(await vi.importActual('../../../dist/utils/command-helpers.js')), +vi.mock('../../../src/utils/command-helpers.js', async () => ({ + ...(await vi.importActual('../../../src/utils/command-helpers.js')), log: () => {}, })) @@ -45,54 +45,59 @@ const headers = [ }, ] -const parseHeadersFile = async function (context, fixtureName) { +const parseHeadersFile = async function (context: { builder: { directory: string } }, fixtureName: string) { const normalizedHeadersFile = resolve(context.builder.directory, fixtureName) // TODO(serhalp): Lazy test type. Create a factory and use it here. + // @ts-expect-error TS(2322) FIXME: Type '{}' is not assignable to type 'NormalizedCac... Remove this comment to see the full error message return await parseHeaders({ config: {}, headersFiles: [normalizedHeadersFile] }) } // Ignore added properties like `forRegExp` -const normalizeHeader = function ({ for: forPath, values }) { +const normalizeHeader = function ({ for: forPath, values }: { for: string; values: T }) { 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: ` + const it = baseIt.extend<{ builder: SiteBuilder }>({ + builder: async ( + // eslint-disable-next-line no-empty-pattern + {}, + use, + ) => { + 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.build() + await builder.build() - context.builder = builder - }) + await use(builder) - afterEach(async (context) => { - await context.builder.cleanup() + await builder.cleanup() + }, }) - test('syntax validates as expected', async (context) => { - await expect(parseHeadersFile(context, '_headers')).resolves.not.toThrowError() + it('syntax validates as expected', async ({ builder }) => { + await expect(parseHeadersFile({ builder }, '_headers')).resolves.not.toThrowError() }) - test('does not throw on invalid syntax', async (context) => { - await expect(parseHeadersFile(context, '_invalid_headers')).resolves.not.toThrowError() + it('does not throw on invalid syntax', async ({ builder }) => { + await expect(parseHeadersFile({ builder }, '_invalid_headers')).resolves.not.toThrowError() }) - test('validate rules', async (context) => { - const rules = await parseHeadersFile(context, '_headers') + it('validate rules', async ({ builder }) => { + const rules = await parseHeadersFile({ builder }, '_headers') const normalizedHeaders = rules.map(normalizeHeader) expect(normalizedHeaders).toEqual([ { @@ -142,8 +147,8 @@ describe('_headers', () => { ]) }) - test('headersForPath testing', async (context) => { - const rules = await parseHeadersFile(context, '_headers') + it('headersForPath testing', async ({ builder }) => { + const rules = await parseHeadersFile({ builder }, '_headers') expect(headersForPath(rules, '/')).toEqual({ 'X-Frame-Options': 'SAMEORIGIN', 'X-Frame-Thing': 'SAMEORIGIN', diff --git a/tests/unit/utils/init/config-github.test.js b/tests/unit/utils/init/config-github.test.ts similarity index 65% rename from tests/unit/utils/init/config-github.test.js rename to tests/unit/utils/init/config-github.test.ts index 4377590567d..5d0003dcab8 100644 --- a/tests/unit/utils/init/config-github.test.js +++ b/tests/unit/utils/init/config-github.test.ts @@ -1,15 +1,16 @@ import { Octokit } from '@octokit/rest' import { beforeEach, describe, expect, test, vi } from 'vitest' +import type { GlobalConfigStore } from '../../../../src/utils/get-global-config-store.js' -import { getGitHubToken } from '../../../../dist/utils/init/config-github.js' +import { getGitHubToken } from '../../../../src/utils/init/config-github.js' -vi.mock('../../../../dist/utils/command-helpers.js', async () => ({ - ...(await vi.importActual('../../../../dist/utils/command-helpers.js')), +vi.mock('../../../../src/utils/command-helpers.js', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.js')), log: () => {}, })) // stub the await ghauth() call for a new token -vi.mock('../../../../dist/utils/gh-auth.js', () => ({ +vi.mock('../../../../src/utils/gh-auth.js', () => ({ getGitHubToken: () => Promise.resolve({ provider: 'github', @@ -21,6 +22,7 @@ vi.mock('../../../../dist/utils/gh-auth.js', () => ({ vi.mock('@octokit/rest', () => { const Client = vi.fn() + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access Client.prototype.rest = { users: { getAuthenticated: vi.fn() }, } @@ -32,12 +34,12 @@ vi.mock('@octokit/rest', () => { describe('getGitHubToken', () => { // mocked configstore - let globalConfig + let globalConfig: GlobalConfigStore beforeEach(() => { - const values = new Map() + const values = new Map() + // @ts-expect-error FIXME(ndhoule): mock is not full, make it more realistic globalConfig = { - values: new Map(), get: (key) => values.get(key), set: (key, value) => { values.set(key, value) @@ -50,10 +52,14 @@ describe('getGitHubToken', () => { user: 'spongebob', }) + // @ts-expect-error: Missing from type definition + // eslint-disable-next-line @typescript-eslint/no-unsafe-call Octokit.mockClear() }) test('should create a octokit client with the provided token if the token is valid', async () => { + // @ts-expect-error: Missing from type definition + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access Octokit.prototype.rest.users.getAuthenticated.mockImplementation(() => Promise.resolve({ status: 200 })) const token = await getGitHubToken({ globalConfig }) @@ -70,8 +76,11 @@ describe('getGitHubToken', () => { }) test('should renew the github token when the provided token is not valid', async () => { + // @ts-expect-error: Missing from type definition + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access Octokit.prototype.rest.users.getAuthenticated.mockImplementation(() => { const authError = new Error('Bad Credentials') + // @ts-expect-error TS(2339) FIXME: Property 'status' does not exist on type 'Error'. authError.status = 401 throw authError diff --git a/tests/unit/utils/parse-raw-flags.test.js b/tests/unit/utils/parse-raw-flags.test.ts similarity index 89% rename from tests/unit/utils/parse-raw-flags.test.js rename to tests/unit/utils/parse-raw-flags.test.ts index fbcf0e65599..eb42f3914b9 100644 --- a/tests/unit/utils/parse-raw-flags.test.js +++ b/tests/unit/utils/parse-raw-flags.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest' -import { aggressiveJSONParse, parseRawFlags } from '../../../dist/utils/parse-raw-flags.js' +import { aggressiveJSONParse, parseRawFlags } from '../../../src/utils/parse-raw-flags.js' describe('parse-raw-flags', () => { test('JSONTruthy works with various inputs', () => { diff --git a/tests/unit/utils/read-repo-url.test.js b/tests/unit/utils/read-repo-url.test.ts similarity index 91% rename from tests/unit/utils/read-repo-url.test.js rename to tests/unit/utils/read-repo-url.test.ts index 595403e9a48..d94dc1ae28c 100644 --- a/tests/unit/utils/read-repo-url.test.js +++ b/tests/unit/utils/read-repo-url.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest' -import { parseRepoURL } from '../../../dist/utils/read-repo-url.js' +import { parseRepoURL } from '../../../src/utils/read-repo-url.js' describe('parseRepoURL', () => { test('should parse GitHub URL', () => { diff --git a/tests/unit/utils/redirects.test.js b/tests/unit/utils/redirects.test.ts similarity index 90% rename from tests/unit/utils/redirects.test.js rename to tests/unit/utils/redirects.test.ts index daad6cc41b2..31492fd5da0 100644 --- a/tests/unit/utils/redirects.test.js +++ b/tests/unit/utils/redirects.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'vitest' -import { parseRedirects } from '../../../dist/utils/redirects.js' -import { withSiteBuilder } from '../../integration/utils/site-builder.ts' +import { parseRedirects } from '../../../src/utils/redirects.js' +import { withSiteBuilder } from '../../integration/utils/site-builder.js' const defaultConfig = { redirects: [ @@ -64,6 +64,7 @@ test('should parse redirect rules from netlify.toml', async (t) => { }) .build() + // @ts-expect-error TS(2345) FIXME: Argument of type '{ configPath: string; }' is not ... Remove this comment to see the full error message const redirects = await parseRedirects({ configPath: `${builder.directory}/netlify.toml` }) const expected = [ { @@ -132,6 +133,7 @@ test('should parse redirect rules from _redirects file', async (t) => { }) .build() + // @ts-expect-error TS(2345) FIXME: Argument of type '{ redirectsFiles: string[]; }' i... Remove this comment to see the full error message const redirects = await parseRedirects({ redirectsFiles: [`${builder.directory}/_redirects`] }) const expected = [ { @@ -158,6 +160,7 @@ test('should parse redirect rules from _redirects file and netlify.toml', async }) .build() + // @ts-expect-error TS(2345) FIXME: Argument of type '{ redirectsFiles: string[]; conf... Remove this comment to see the full error message const redirects = await parseRedirects({ redirectsFiles: [`${builder.directory}/_redirects`], configPath: `${builder.directory}/netlify.toml`, diff --git a/tests/unit/utils/rules-proxy.test.js b/tests/unit/utils/rules-proxy.test.ts similarity index 77% rename from tests/unit/utils/rules-proxy.test.js rename to tests/unit/utils/rules-proxy.test.ts index 40fbadbc06c..269593b3074 100644 --- a/tests/unit/utils/rules-proxy.test.js +++ b/tests/unit/utils/rules-proxy.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest' -import { getLanguage } from '../../../dist/utils/rules-proxy.js' +import { getLanguage } from '../../../src/utils/rules-proxy.js' describe('getLanguage', () => { test('detects language', () => { diff --git a/tests/unit/utils/telemetry/validation.test.js b/tests/unit/utils/telemetry/validation.test.ts similarity index 86% rename from tests/unit/utils/telemetry/validation.test.js rename to tests/unit/utils/telemetry/validation.test.ts index 81aa2c73a33..c676cf5fff1 100644 --- a/tests/unit/utils/telemetry/validation.test.js +++ b/tests/unit/utils/telemetry/validation.test.ts @@ -1,11 +1,11 @@ import { describe, expect, test, vi } from 'vitest' -import isValidEventName from '../../../../dist/utils/telemetry/validation.js' +import isValidEventName from '../../../../src/utils/telemetry/validation.js' -const getEventForProject = (projectName, eventName) => `${projectName}:${eventName}` +const getEventForProject = (projectName: string, eventName: string) => `${projectName}:${eventName}` -vi.mock('../../../../dist/utils/command-helpers.ts', async () => ({ - ...(await vi.importActual('../../../../dist/utils/command-helpers.js')), +vi.mock('../../../../src/utils/command-helpers.ts', async () => ({ + ...(await vi.importActual('../../../../src/utils/command-helpers.js')), log: () => {}, }))