diff --git a/src/utils/deploy/hash-config.ts b/src/utils/deploy/hash-config.ts index 9234fed9681..5c7a42fea33 100644 --- a/src/utils/deploy/hash-config.ts +++ b/src/utils/deploy/hash-config.ts @@ -2,29 +2,25 @@ import { createHash } from 'node:crypto' import tomlify from 'tomlify-j0.4' -// @ts-expect-error TS(7031) FIXME: Binding element 'config' implicitly has an 'any' t... Remove this comment to see the full error message -export const hashConfig = ({ config }) => { - if (!config) throw new Error('Missing config option') +import type { NormalizedCachedConfigConfig } from '../command-helpers.js' + +export const hashConfig = ({ config }: { config: NormalizedCachedConfigConfig }) => { const configString = serializeToml(config) const hash = createHash('sha1').update(configString).digest('hex') return { - assetType: 'file', + assetType: 'file' as const, body: configString, hash, normalizedPath: 'netlify.toml', } } -// @ts-expect-error TS(7006) FIXME: Parameter 'object' implicitly has an 'any' type. -export const serializeToml = function (object) { +export const serializeToml = function (object: unknown) { return tomlify.toToml(object, { space: 2, replace: replaceTomlValue }) } -// `tomlify-j0.4` serializes integers as floats, e.g. `200.0`. -// This is a problem with `redirects[*].status`. -// @ts-expect-error TS(7006) FIXME: Parameter 'key' implicitly has an 'any' type. -const replaceTomlValue = function (key, value) { +const replaceTomlValue = function (key: string, value: unknown) { return Number.isInteger(value) ? String(value) : false } diff --git a/src/utils/deploy/upload-files.ts b/src/utils/deploy/upload-files.ts index 7244c96ceb0..fc1ea03b7b9 100644 --- a/src/utils/deploy/upload-files.ts +++ b/src/utils/deploy/upload-files.ts @@ -2,23 +2,56 @@ import fs from 'fs' import backoff from 'backoff' import pMap from 'p-map' +import type { NetlifyAPI } from '@netlify/api' import { UPLOAD_INITIAL_DELAY, UPLOAD_MAX_DELAY, UPLOAD_RANDOM_FACTOR } from './constants.js' -// @ts-expect-error TS(7006) FIXME: Parameter 'api' implicitly has an 'any' type. -const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRetry, statusCb }) => { - if (!concurrentUpload || !statusCb || !maxRetry) throw new Error('Missing required option concurrentUpload') +export interface FileObject { + assetType: 'file' | 'function' + body?: string | Buffer | fs.ReadStream + filepath?: string + invocationMode?: string + normalizedPath: string + runtime?: string + timeout?: number +} + +interface UploadStatus { + type: 'upload' + msg: string + phase: 'start' | 'progress' | 'stop' +} + +interface UploadOptions { + concurrentUpload: number + maxRetry: number + statusCb: (status: UploadStatus) => void +} + +const uploadFiles = async ( + api: Pick, + deployId: string, + uploadList: FileObject[], + { concurrentUpload, maxRetry, statusCb }: UploadOptions, +) => { statusCb({ type: 'upload', msg: `Uploading ${uploadList.length} files`, phase: 'start', }) - // @ts-expect-error TS(7006) FIXME: Parameter 'fileObj' implicitly has an 'any' type. - const uploadFile = async (fileObj, index) => { + const uploadFile = async (fileObj: FileObject, index: number) => { const { assetType, body, filepath, invocationMode, normalizedPath, runtime, timeout } = fileObj - const readStreamCtor = () => body ?? fs.createReadStream(filepath) + const readStreamCtor = () => { + if (body) { + return body + } + if (filepath) { + return fs.createReadStream(filepath) + } + throw new Error(`Missing body or filepath for asset ${normalizedPath}`) + } statusCb({ type: 'upload', @@ -30,7 +63,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet case 'file': { response = await retryUpload( () => - api.uploadDeployFile({ + (api as unknown as NetlifyAPI).uploadDeployFile({ body: readStreamCtor, deployId, path: encodeURI(normalizedPath), @@ -40,9 +73,8 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet break } case 'function': { - // @ts-expect-error TS(7006) FIXME: Parameter 'retryCount' implicitly has an 'any' typ... Remove this comment to see the full error message - response = await retryUpload((retryCount) => { - const params = { + response = await retryUpload((retryCount: number) => { + const params: Parameters[0] & { xNfRetryCount?: number } = { body: readStreamCtor, deployId, invocationMode, @@ -52,18 +84,16 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet } if (retryCount > 0) { - // @ts-expect-error TS(2339) FIXME: Property 'xNfRetryCount' does not exist on type '{... Remove this comment to see the full error message params.xNfRetryCount = retryCount } - return api.uploadDeployFunction(params) + return (api as unknown as NetlifyAPI).uploadDeployFunction(params) }, maxRetry) break } default: { const error = new Error('File Object missing assetType property') - // @ts-expect-error TS(2339) FIXME: Property 'fileObj' does not exist on type 'Error'. - error.fileObj = fileObj + Object.assign(error, { fileObj }) throw error } } @@ -80,11 +110,9 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet return results } -// @ts-expect-error TS(7006) FIXME: Parameter 'uploadFn' implicitly has an 'any' type. -const retryUpload = (uploadFn, maxRetry) => +const retryUpload = (uploadFn: (retryCount: number) => Promise, maxRetry: number): Promise => new Promise((resolve, reject) => { - // @ts-expect-error TS(7034) FIXME: Variable 'lastError' implicitly has type 'any' in ... Remove this comment to see the full error message - let lastError + let lastError: unknown const fibonacciBackoff = backoff.fibonacci({ randomisationFactor: UPLOAD_RANDOM_FACTOR, @@ -102,18 +130,25 @@ const retryUpload = (uploadFn, maxRetry) => lastError = error // We don't need to retry for 400 or 422 errors - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - if (error.status === 400 || error.status === 422) { - reject(error) - return + if (error && typeof error === 'object' && 'status' in error) { + const { status } = error as { status: number } + if (status === 400 || status === 422) { + reject(error) + return + } + + // observed errors: 408, 401 (4** swallowed), 502 + if (status > 400) { + fibonacciBackoff.backoff() + return + } } - // observed errors: 408, 401 (4** swallowed), 502 - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - if (error.status > 400 || error.name === 'FetchError') { + if (error && typeof error === 'object' && 'name' in error && error.name === 'FetchError') { fibonacciBackoff.backoff() return } + reject(error) return } @@ -130,7 +165,6 @@ const retryUpload = (uploadFn, maxRetry) => fibonacciBackoff.on('ready', tryUpload) fibonacciBackoff.on('fail', () => { - // @ts-expect-error TS(7005) FIXME: Variable 'lastError' implicitly has an 'any' type. reject(lastError) }) diff --git a/src/utils/deploy/util.ts b/src/utils/deploy/util.ts index 6446bcaecf5..9bebfbd235f 100644 --- a/src/utils/deploy/util.ts +++ b/src/utils/deploy/util.ts @@ -1,8 +1,12 @@ import { sep } from 'path' +import type { NetlifyAPI } from '@netlify/api' import pWaitFor from 'p-wait-for' import { DEPLOY_POLL } from './constants.js' +import type { FileObject } from './upload-files.js' + +type Deploy = Awaited> // normalize windows paths to unix paths export const normalizePath = (relname: string): string => { @@ -13,10 +17,14 @@ export const normalizePath = (relname: string): string => { } // poll an async deployId until its done diffing -// @ts-expect-error TS(7006) FIXME: Parameter 'api' implicitly has an 'any' type. -export const waitForDiff = async (api, deployId, siteId, timeout) => { +export const waitForDiff = async ( + api: NetlifyAPI, + deployId: string, + siteId: string, + timeout: number, +): Promise => { // capture ready deploy during poll - let deploy + let deploy: Deploy const loadDeploy = async () => { const siteDeploy = await api.getSiteDeploy({ siteId, deployId }) @@ -25,8 +33,7 @@ export const waitForDiff = async (api, deployId, siteId, timeout) => { // https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33 case 'error': { const deployError = new Error(siteDeploy.error_message || `Deploy ${deployId} had an error`) - // @ts-expect-error TS(2339) FIXME: Property 'deploy' does not exist on type 'Error'. - deployError.deploy = siteDeploy + Object.assign(deployError, { deploy: siteDeploy }) throw deployError } case 'prepared': @@ -51,14 +58,19 @@ export const waitForDiff = async (api, deployId, siteId, timeout) => { }, }) + // @ts-expect-error TS(2454) FIXME: Variable 'deploy' is used before being assigned. return deploy } // Poll a deployId until its ready -// @ts-expect-error TS(7006) FIXME: Parameter 'api' implicitly has an 'any' type. -export const waitForDeploy = async (api, deployId, siteId, timeout) => { +export const waitForDeploy = async ( + api: NetlifyAPI, + deployId: string, + siteId: string, + timeout: number, +): Promise => { // capture ready deploy during poll - let deploy + let deploy: Deploy const loadDeploy = async () => { const siteDeploy = await api.getSiteDeploy({ siteId, deployId }) @@ -66,8 +78,7 @@ export const waitForDeploy = async (api, deployId, siteId, timeout) => { // https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33 case 'error': { const deployError = new Error(siteDeploy.error_message || `Deploy ${deployId} had an error`) - // @ts-expect-error TS(2339) FIXME: Property 'deploy' does not exist on type 'Error'. - deployError.deploy = siteDeploy + Object.assign(deployError, { deploy: siteDeploy }) throw deployError } case 'ready': { @@ -92,13 +103,12 @@ export const waitForDeploy = async (api, deployId, siteId, timeout) => { }, }) + // @ts-expect-error TS(2454) FIXME: Variable 'deploy' is used before being assigned. return deploy } // Transform the fileShaMap and fnShaMap into a generic shaMap that file-uploader.js can use -// @ts-expect-error TS(7006) FIXME: Parameter 'required' implicitly has an 'any' type. -export const getUploadList = (required, shaMap) => { +export const getUploadList = (required?: string[] | null, shaMap?: Record | null) => { if (!required || !shaMap) return [] - // @ts-expect-error TS(7006) FIXME: Parameter 'sha' implicitly has an 'any' type. return required.flatMap((sha) => shaMap[sha]) } diff --git a/src/utils/dev.ts b/src/utils/dev.ts index 8f929be9270..98eac50534e 100644 --- a/src/utils/dev.ts +++ b/src/utils/dev.ts @@ -36,13 +36,12 @@ const ENV_VAR_SOURCES = { name: 'project settings', printFn: chalk.blue, }, -} +} as const const ERROR_CALL_TO_ACTION = "Double-check your login status with 'netlify status' or contact support with details of your error." -// @ts-expect-error TS(7031) FIXME: Binding element 'site' implicitly has an 'any' typ... Remove this comment to see the full error message -const validateSiteInfo = ({ site, siteInfo }) => { +const validateSiteInfo = ({ site, siteInfo }: { site: { id?: string }; siteInfo: SiteInfo }) => { if (isEmpty(siteInfo)) { return logAndThrowError( `Failed to retrieve project information for project ${chalk.yellow(site.id)}. ${ERROR_CALL_TO_ACTION}`, @@ -78,10 +77,9 @@ const getAccounts = async ({ api }: { api: NetlifyAPI }) => { } } -// @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message -const getAddons = async ({ api, site }) => { +const getAddons = async ({ api, site }: { api: NetlifyAPI; site: { id?: string } }) => { try { - const addons = await api.listServiceInstancesForSite({ siteId: site.id }) + const addons = await api.listServiceInstancesForSite({ siteId: site.id || '' }) return addons } catch (error_) { return logAndThrowError( @@ -92,13 +90,16 @@ const getAddons = async ({ api, site }) => { } } -// @ts-expect-error TS(7031) FIXME: Binding element 'addons' implicitly has an 'any' t... Remove this comment to see the full error message -const getAddonsInformation = ({ addons, siteInfo }) => { +const getAddonsInformation = ({ + addons, + siteInfo, +}: { + addons: Awaited> + siteInfo: SiteInfo +}): { urls: Record; env: Record } => { const urls = Object.fromEntries( - // @ts-expect-error TS(7006) FIXME: Parameter 'addon' implicitly has an 'any' type. addons.map((addon) => [addon.service_slug, `${siteInfo.ssl_url}${addon.service_path}`]), ) - // @ts-expect-error TS(7006) FIXME: Parameter 'addon' implicitly has an 'any' type. const env = Object.assign({}, ...addons.map((addon) => addon.env)) return { urls, env } } @@ -181,21 +182,28 @@ export const getSiteInformation = async ({ } } -// @ts-expect-error TS(7006) FIXME: Parameter 'source' implicitly has an 'any' type. -const getEnvSourceName = (source) => { - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message - const { name = source, printFn = chalk.green } = ENV_VAR_SOURCES[source] || {} - - return printFn(name) +const getEnvSourceName = (source: string) => { + if (source in ENV_VAR_SOURCES) { + const envVarSource = ENV_VAR_SOURCES[source as keyof typeof ENV_VAR_SOURCES] + return envVarSource.printFn(envVarSource.name) + } + return chalk.green(source) } -/** - * @param {{devConfig: any, env: Record, site: any}} param0 - */ -// @ts-expect-error TS(7031) FIXME: Binding element 'devConfig' implicitly has an 'any... Remove this comment to see the full error message -export const getDotEnvVariables = async ({ devConfig, env, site }): Promise => { - const dotEnvFiles = await loadDotEnvFiles({ envFiles: devConfig.envFiles, projectDir: site.root }) - // @ts-expect-error TS(2339) FIXME: Property 'env' does not exist on type '{ warning: ... Remove this comment to see the full error message +export const getDotEnvVariables = async ({ + devConfig, + env, + site, +}: { + devConfig: { envFiles?: string[]; env_files?: string[] } & Record + env: EnvironmentVariables + site: { root?: string } +}): Promise => { + const dotEnvFiles = await loadDotEnvFiles({ + envFiles: devConfig.envFiles || devConfig.env_files, + // eslint-disable-next-line no-restricted-properties + projectDir: site.root || process.cwd(), + }) dotEnvFiles.forEach(({ env: fileEnv, file }) => { const newSourceName = `${file} file` @@ -272,11 +280,12 @@ export const acquirePort = async ({ return acquiredPort } -// @ts-expect-error TS(7006) FIXME: Parameter 'fn' implicitly has an 'any' type. -export const processOnExit = (fn) => { - const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP', 'exit'] +export const processOnExit = (fn: (...args: unknown[]) => void | Promise) => { + const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP', 'exit'] as const signals.forEach((signal) => { - process.on(signal, fn) + process.on(signal, (...args) => { + void fn(...args) + }) }) } diff --git a/src/utils/dot-env.ts b/src/utils/dot-env.ts index 27cebc92163..c60fc920ded 100644 --- a/src/utils/dot-env.ts +++ b/src/utils/dot-env.ts @@ -7,28 +7,36 @@ import { isFileAsync } from '../lib/fs.js' import { warn } from './command-helpers.js' -// @ts-expect-error TS(7031) FIXME: Binding element 'envFiles' implicitly has an 'any'... Remove this comment to see the full error message -export const loadDotEnvFiles = async function ({ envFiles, projectDir }) { +export interface DotEnvResult { + file?: string + env?: dotenv.DotenvParseOutput + warning?: string +} + +export const loadDotEnvFiles = async function ({ envFiles, projectDir }: { envFiles?: string[]; projectDir: string }) { const response = await tryLoadDotEnvFiles({ projectDir, dotenvFiles: envFiles }) - // @ts-expect-error TS(2532) FIXME: Object is possibly 'undefined'. - const filesWithWarning = response.filter((el) => el.warning) + const filesWithWarning = response.filter((el): el is { warning: string } => Boolean(el.warning)) filesWithWarning.forEach((el) => { - // @ts-expect-error TS(2532) FIXME: Object is possibly 'undefined'. warn(el.warning) }) - // @ts-expect-error TS(2532) FIXME: Object is possibly 'undefined'. - return response.filter((el) => el.file && el.env) + return response.filter((el): el is { file: string; env: dotenv.DotenvParseOutput } => Boolean(el.file && el.env)) } // in the user configuration, the order is highest to lowest const defaultEnvFiles = ['.env.development.local', '.env.local', '.env.development', '.env'] -// @ts-expect-error TS(7031) FIXME: Binding element 'projectDir' implicitly has an 'an... Remove this comment to see the full error message -export const tryLoadDotEnvFiles = async ({ dotenvFiles = defaultEnvFiles, projectDir }) => { +export const tryLoadDotEnvFiles = async ({ + dotenvFiles, + projectDir, +}: { + dotenvFiles?: string[] + projectDir: string +}): Promise => { + const filesToLoad = dotenvFiles && dotenvFiles.length !== 0 ? dotenvFiles : defaultEnvFiles const results = await Promise.all( - dotenvFiles.map(async (file) => { + filesToLoad.map(async (file): Promise => { const filepath = path.resolve(projectDir, file) try { const isFile = await isFileAsync(filepath) @@ -37,8 +45,9 @@ export const tryLoadDotEnvFiles = async ({ dotenvFiles = defaultEnvFiles, projec } } catch (error) { return { - // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. - warning: `Failed reading env variables from file: ${filepath}: ${error.message}`, + warning: `Failed reading env variables from file: ${filepath}: ${ + error instanceof Error ? error.message : error + }`, } } const content = await readFile(filepath, 'utf-8') @@ -48,5 +57,5 @@ export const tryLoadDotEnvFiles = async ({ dotenvFiles = defaultEnvFiles, projec ) // we return in order of lowest to highest priority - return results.filter(Boolean).reverse() + return results.filter((el): el is DotEnvResult => Boolean(el)).reverse() } diff --git a/src/utils/shell.ts b/src/utils/shell.ts index 6c175a764ba..b3f2b68c5c4 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -100,11 +100,10 @@ export const runCommand = ( )} exists`, ) } else { - const errorMessage = result.failed - ? // @ts-expect-error FIXME(serhalp): We use `reject: false` which means the resolved value is either the resolved value - // or the rejected value, but the types aren't smart enough to know this. - `${NETLIFYDEVERR} ${result.shortMessage as string}` - : `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode.toString()}` + const errorMessage = + result.failed && 'shortMessage' in result + ? `${NETLIFYDEVERR} ${result.shortMessage as string}` + : `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode.toString()}` log(`${errorMessage}. Shutting down Netlify Dev server`) } diff --git a/src/utils/types.ts b/src/utils/types.ts index f2c33e4b541..76aa80304a0 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -219,7 +219,14 @@ export interface Template { } type EnvironmentVariableScope = 'builds' | 'functions' | 'runtime' | 'post_processing' -export type EnvironmentVariableSource = 'account' | 'addons' | 'configFile' | 'general' | 'internal' | 'ui' +export type EnvironmentVariableSource = + | 'account' + | 'addons' + | 'configFile' + | 'general' + | 'internal' + | 'ui' + | (string & {}) export type EnvironmentVariables = Record< string, diff --git a/tests/unit/utils/deploy/upload-files.test.ts b/tests/unit/utils/deploy/upload-files.test.ts index 0e71da25257..8d4802aaafc 100644 --- a/tests/unit/utils/deploy/upload-files.test.ts +++ b/tests/unit/utils/deploy/upload-files.test.ts @@ -1,5 +1,6 @@ import { v4 as generateUUID } from 'uuid' import { afterAll, expect, test, vi } from 'vitest' +import type { NetlifyAPI } from '@netlify/api' import uploadFiles from '../../../../src/utils/deploy/upload-files.js' @@ -18,8 +19,7 @@ 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 + Object.assign(mockError, { status: 500 }) uploadDeployFunction.mockRejectedValueOnce(mockError) uploadDeployFunction.mockRejectedValueOnce(mockError) @@ -27,11 +27,12 @@ test('Adds a retry count to function upload requests', async () => { const mockApi = { uploadDeployFunction, + uploadDeployFile: vi.fn(), } const deployId = generateUUID() const files = [ { - assetType: 'function', + assetType: 'function' as const, filepath: '/some/path/func1.zip', normalizedPath: 'func1.zip', runtime: 'js', @@ -43,7 +44,12 @@ test('Adds a retry count to function upload requests', async () => { statusCb: vi.fn(), } - await uploadFiles(mockApi, deployId, files, options) + await uploadFiles( + mockApi as unknown as Pick, + deployId, + files, + options, + ) expect(uploadDeployFunction).toHaveBeenCalledTimes(3) expect(uploadDeployFunction).toHaveBeenNthCalledWith(1, expect.not.objectContaining({ xNfRetryCount: 1 })) @@ -55,18 +61,18 @@ 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 + Object.assign(mockError, { status: 400 }) uploadDeployFunction.mockRejectedValue(mockError) const mockApi = { uploadDeployFunction, + uploadDeployFile: vi.fn(), } const deployId = generateUUID() const files = [ { - assetType: 'function', + assetType: 'function' as const, filepath: '/some/path/func1.zip', normalizedPath: 'func1.zip', runtime: 'js', @@ -79,7 +85,12 @@ test('Does not retry on 400 response from function upload requests', async () => } try { - await uploadFiles(mockApi, deployId, files, options) + await uploadFiles( + mockApi as unknown as Pick, + deployId, + files, + options, + ) } catch {} expect(uploadDeployFunction).toHaveBeenCalledTimes(1)