Skip to content
3 changes: 1 addition & 2 deletions src/utils/command-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ export const pollForToken = async ({
}
return accessToken
} catch (error_) {
// @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'.
if (error_.name === 'TimeoutError') {
if (error_ instanceof Error && error_.name === 'TimeoutError') {
return logAndThrowError(
`Timed out waiting for authorization. If you do not have a ${chalk.bold.greenBright(
'Netlify',
Expand Down
63 changes: 40 additions & 23 deletions src/utils/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ const ENV_VAR_SOURCES = {
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}`,
Expand Down Expand Up @@ -78,10 +77,13 @@ 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 } }) => {
const { id } = site
if (!id) {
return []
}
try {
const addons = await api.listServiceInstancesForSite({ siteId: site.id })
const addons = await api.listServiceInstancesForSite({ siteId: id })
return addons
} catch (error_) {
return logAndThrowError(
Expand All @@ -92,21 +94,21 @@ 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 }) => {
type Addon = Awaited<ReturnType<NetlifyAPI['listServiceInstancesForSite']>>[number]

const getAddonsInformation = ({ addons, siteInfo }: { addons: Addon[]; siteInfo: SiteInfo }) => {
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 }
}

const getSiteAccount = ({ accounts, siteInfo }: { accounts: Account[]; siteInfo: SiteInfo }): Account | undefined => {
const siteAccount = accounts.find((account) => account.slug === siteInfo.account_slug)
const { account_id: accountId, account_slug: accountSlug } = siteInfo
const siteAccount = accounts.find((account) => account.slug === accountSlug || account.id === accountId)
if (!siteAccount) {
warn(`Could not find account for project '${siteInfo.name}' with account slug '${siteInfo.account_slug}'`)
warn(`Could not find account for project '${siteInfo.name}' with account slug '${accountSlug}'`)
return undefined
}
return siteAccount
Expand Down Expand Up @@ -155,7 +157,7 @@ export const getSiteInformation = async ({
return {
addonsUrls,
siteUrl: siteInfo.ssl_url,
accountId: account?.id,
accountId: account?.id ?? siteInfo.account_id,
capabilities: {
backgroundFunctions: supportsBackgroundFunctions(account),
aiGatewayDisabled: siteInfo.capabilities?.ai_gateway_disabled ?? false,
Expand All @@ -181,21 +183,30 @@ 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] || {}
const getEnvSourceName = (source: string) => {
const sourceConfig = (
ENV_VAR_SOURCES as Record<string, { name: string; printFn: (s: string) => string } | undefined>
)[source]
const { name = source, printFn = chalk.green } = sourceConfig || {}

return printFn(name)
}

/**
* @param {{devConfig: any, env: Record<string, { sources: string[], value: string}>, 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<EnvironmentVariables> => {
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[]; [key: string]: unknown }
env: EnvironmentVariables
site: { root?: string; [key: string]: unknown }
}): Promise<EnvironmentVariables> => {
const envFiles = devConfig.envFiles || devConfig.env_files
// eslint-disable-next-line no-restricted-properties
const dotEnvFiles = await loadDotEnvFiles({ envFiles, projectDir: site.root || process.cwd() })
dotEnvFiles.forEach(({ env: fileEnv, file }) => {
const newSourceName = `${file} file`

Expand Down Expand Up @@ -272,11 +283,17 @@ export const acquirePort = async ({
return acquiredPort
}

// @ts-expect-error TS(7006) FIXME: Parameter 'fn' implicitly has an 'any' type.
export const processOnExit = (fn) => {
export const processOnExit = (fn: (codeOrSignal: string | number) => void | Promise<void>) => {
const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP', 'exit']
signals.forEach((signal) => {
process.on(signal, fn)
process.on(signal, (codeOrSignal) => {
const result = fn(codeOrSignal)
if (result instanceof Promise) {
result.catch(() => {
// ignore
})
}
})
})
}

Expand Down
45 changes: 32 additions & 13 deletions src/utils/dot-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,46 @@ 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 type DotEnvFileResult = {
file: string
env: Record<string, string>
}

export type DotEnvWarningResult = {
warning: string
}

export type DotEnvResult = DotEnvFileResult | DotEnvWarningResult

export const loadDotEnvFiles = async function ({
envFiles,
projectDir,
}: {
envFiles?: string[]
projectDir: string
}): Promise<DotEnvFileResult[]> {
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 DotEnvWarningResult => 'warning' in el)
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 DotEnvFileResult => 'file' in el && 'env' in el)
}

// 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 = defaultEnvFiles,
projectDir,
}: {
dotenvFiles?: string[]
projectDir: string
}): Promise<DotEnvResult[]> => {
const results = await Promise.all(
dotenvFiles.map(async (file) => {
dotenvFiles.map(async (file): Promise<DotEnvResult | undefined> => {
const filepath = path.resolve(projectDir, file)
try {
const isFile = await isFileAsync(filepath)
Expand All @@ -37,8 +55,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 : String(error)
}`,
}
}
const content = await readFile(filepath, 'utf-8')
Expand All @@ -48,5 +67,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((result): result is DotEnvResult => Boolean(result)).reverse()
}
87 changes: 60 additions & 27 deletions src/utils/telemetry/report-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import os from 'os'
import { dirname, join } from 'path'
import process, { version as nodejsVersion } from 'process'
import { fileURLToPath } from 'url'
import { inspect } from 'util'

import { type Event } from '@bugsnag/js'
import { getGlobalConfigStore } from '@netlify/dev-utils'
import { isCI } from 'ci-info'

Expand All @@ -12,44 +14,75 @@ import { cliVersion } from './utils.js'

const dirPath = dirname(fileURLToPath(import.meta.url))

interface ReportErrorConfig {
severity?: Event['severity']
metadata?: Record<string, Record<string, unknown>>
}

/**
*
* @param {import('@bugsnag/js').NotifiableError} error
* @param {object} config
* @param {import('@bugsnag/js').Event['severity']} config.severity
* @param {Record<string, Record<string, any>>} [config.metadata]
* @returns {Promise<void>}
* Report an error to telemetry
*/
// @ts-expect-error TS(7006) FIXME: Parameter 'error' implicitly has an 'any' type.
export const reportError = async function (error, config = {}) {
export const reportError = async function (error: unknown, config: ReportErrorConfig = {}): Promise<void> {
if (isCI) {
return
}

// convert a NotifiableError to an error class
const err = error instanceof Error ? error : typeof error === 'string' ? new Error(error) : error
let err: Error
if (error instanceof Error) {
err = error
} else if (typeof error === 'string') {
err = new Error(error)
} else if (typeof error === 'object' && error !== null && ('message' in error || 'name' in error)) {
const errorObject = error as Record<string, unknown>
const message = typeof errorObject.message === 'string' ? errorObject.message : 'Unknown error'
err = new Error(message)
if (typeof errorObject.name === 'string') {
err.name = errorObject.name
}
if (typeof errorObject.stack === 'string') {
err.stack = errorObject.stack
}
} else {
err = new Error(typeof error === 'object' && error !== null ? inspect(error) : String(error))
}

const globalConfig = await getGlobalConfigStore()

const options = JSON.stringify({
type: 'error',
data: {
message: err.message,
name: err.name,
stack: err.stack,
cause: err.cause,
// @ts-expect-error TS(2339) FIXME: Property 'severity' does not exist on type '{}'.
severity: config.severity,
user: {
id: globalConfig.get('userId'),
let options: string
try {
options = JSON.stringify({
type: 'error',
data: {
message: err.message,
name: err.name,
stack: err.stack,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cause: (err as any).cause ? inspect((err as any).cause) : undefined,
severity: config.severity,
user: {
id: globalConfig.get('userId'),
},
metadata: config.metadata ? inspect(config.metadata) : undefined,
osName: `${os.platform()}-${os.arch()}`,
cliVersion,
nodejsVersion,
},
// @ts-expect-error TS(2339) FIXME: Property 'metadata' does not exist on type '{}'.
metadata: config.metadata,
osName: `${os.platform()}-${os.arch()}`,
cliVersion,
nodejsVersion,
},
})
})
} catch {
// If stringify fails, we at least try to report a simplified error
options = JSON.stringify({
type: 'error',
data: {
message: `Error reporting failed: ${err.message}`,
name: 'ErrorReportingError',
severity: config.severity,
osName: `${os.platform()}-${os.arch()}`,
cliVersion,
nodejsVersion,
},
})
}

// spawn detached child process to handle send and wait for the http request to finish
// otherwise it can get canceled
Expand Down
9 changes: 8 additions & 1 deletion src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading